Silverstripe Multiple Userforms on one page - silverstripe

I am trying to create a single page that will display multiple userforms in a tabbed view. For example basic contact form, request a quote form etc.
I thought I could make a new page type and loop through the children to display the forms, but the $Form variable isn't rendering the form.
<% loop $Children %>
<div>
<h2>$Title</h2>
$Form
</div>
<% end_loop %>
Am I missing something here, or is there a different way to render a form using a its ID in a template file?

You could try the following.
Create a function in your page holder controller to get the form from a specific child (must be a UserDefinedForm page). To do this you'll need to create the controller of this child page.
public function ChildForm($pageID) {
$page = UserDefinedForm::get()->byID($pageID);
$controller = UserDefinedForm_Controller::create($page);
return $controller->Form();
}
afterwards you'll call this function in your loop and pass the current child id to it
<% loop $Children %>
<div>
<h2>$Title</h2>
$Top.ChildForm($ID)
</div>
<% end_loop %>
This should (code is untested) return the forms you want.

The problem at play here is the difference between the DataObject/Page and the Controller. Looping over $Children returns you a DataObject whereas the Form function and template variable are part of UserDefinedForm's controller.
The other answer shows one working solution however it has some hair on it:
Jumping scope to your controller to pass an ID to get your form
Additional DB query
Requires all the child pages to be of type UserDefinedForm
We can implement a more generic solution that removes some of those elements and making your code a little more maintainable.
Take the following which would be added to the Page class (not the controller):
function getInLoopForm() {
if (in_array('UserDefinedForm', $this->ClassAncestry)) {
$controllerName = $this->ClassName . '_Controller';
$controller = $controllerName::create($this);
if ($controller->hasMethod('Form')) {
return $controller->Form();
}
}
return false;
}
The first part of that checks whether the current object has UserDefinedForm in its class ancestry. If it is, we then create the appropriate controller and return the form.
Your template code would look like this instead:
<% loop $Children %>
<div>
<h2>$Title</h2>
$InLoopForm
</div>
<% end_loop %>
This solution is generic for three reasons:
In our getInLoopForm function, the value "UserDefinedForm" can be replaced with any class that extends Page. It could even be brought out to a YML value if you were so inclined.
For SilverStripe, controller names for pages must match "{PageClassName}_Controller" so we can abuse that by working out the controller name dynamically. This allows for you to extend UserDefinedForm and its controller and we can still call the right function.
You only require your DataObject to access the form, you don't need your own controller.

For SS 4 there is a small code change needed:
public function getInLoopForm() {
if (in_array('SilverStripe\UserForms\Model\UserDefinedForm', $this->ClassAncestry)) {
$controller = UserDefinedFormController::create($this);
if ($controller->hasMethod('Form')) {
return $controller->Form();
}
}
return false;
}

Related

Silverstripe 4 - Adding a FormAction via getCMSFields

Goal:
I have a DataObject called "Event". This is in a managed_model for "EventsAdmin" (extending ModelAdmin). When editing an Event, I want a tab on the record called "Moderation" that has a few fields and two buttons: "Approve" and "Reject". These two buttons call an action each that performs relevant actions.
Event extends DataObject
public function getCMSFields() {
$fields = parent::getCMSFields();
$eventStatus = $fields->dataFieldByName("EventStatus")
->setTitle('Current Status')
->setDisabled(true);
$approveButton = FormAction::create('doApproveEvent', _t('SiteBlockAdmin.Approve', 'Approve'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-success font-icon-check-mark-circle');
$rejectButton = FormAction::create('doRejectEvent', _t('SiteBlockAdmin.Reject', 'Reject'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-danger font-icon-cancel-circled');
$fields->addFieldsToTab('Root.Moderation', [
$eventStatus,
$approveButton,
$rejectButton
]);
return $fields;
}
This displays the buttons just fine. But they don't do anything. So I am trying to work out how they can plug into action methods doApproveEvent and doRejectEvent (And where they should go)
I did find docs that led me to adding the buttons to the action bar at the bottom of the CMS page via updateFormActions(). But this isn't what I want as the other fields I am adding above the buttons are part of the Approve/Reject process. Here is the code for this method. This works fine barring the buttons are not in a logical place for the process I'm trying to create.
class CMSActionButtonExtension extends DataExtension
{
public function updateFormActions(FieldList $actions)
{
$record = $this->owner->getRecord();
if (!$record instanceof Event || !$record->exists()) {
return;
}
$approveButton = FormAction::create('doApproveEvent', _t('SiteBlockAdmin.Approve', 'Approve'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-success font-icon-check-mark-circle');
$rejectButton = FormAction::create('doRejectEvent', _t('SiteBlockAdmin.Reject', 'Reject'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-danger font-icon-cancel-circled');
$actions->push($approveButton);
$actions->push($rejectButton);
}
public function doApproveEvent($data, $form) {
$record = $this->owner->getRecord();
// Approve logic
}
public function doRejectEvent($data, $form) {
$record = $this->owner->getRecord();
// Reject logic
}
}
The above Extension is attached to GridFieldDetailForm_ItemRequest
extension.yml
SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest:
extensions:
- My\Namespace\CMSActionButtonExtension
Interestingly, if I have both sets of buttons on the page at the same time, the updateFormActions option works while my desired option still doesn't. Despite the buttons being of identical markup and sitting inside the exact same form tag. I assume that has something to do with how Silverstripe loads the main content panel and the DOM.
Any thoughts on achieving this? Anyone seen a button added to the main CMS panel in a module that I could take a look at? I found this post from 5 years ago, but it's for SS3 and the answer doesn't work for me.
Short answer:
you have to add custom FormActions through an Extension on the Controller that controls the form (or on the form itself
Long Answer:
A bit of background on how SilverStripe does forms:
Generally speaking, forms are always served through Controllers/RequestHandlers (they need to be accessible on some route, usually that's an Action on a Controller that is often named Form, EditForm, ItemEditoForm, ...).
Fields
Inside the CMS you rarely ever have to create your own form, that's done by the CMSs built in Controllers/RequestHandlers for the admin area (GridFieldDetailForm_ItemRequest in this case).
Basically (pseudo code here), what those controllers do is:
public function EditForm() {
$fields = $myCurrentlyEditingDataObject->getCMSFields();
$actions = ...;
$validator = ...;
$this->updateFormActions(&$actions);
$form = new Form('ItemRequestForm', $fields, $actions, $validator);
$this->updateItemEditForm(&$form); // or $this->updateEditForm()
return $form;
}
So, getCMSFields() and in some cases getCMSActions()/getCMSValidator() (not sure if those 2 are still used in SilverStripe 4.x), you can add things to the form, without ever seeing the form object.
Also, the getCMSFields() will always be put into the ``` section of the Form, that's why your button is somewhere in the middle with all the fields and not with the other actions.
Submission
When a form is submitted (eg to /admin/pages/edit/EditForm/265/field/NameOfMyGridField/item/542/ItemEditForm), it will call the action GridFieldDetailForm_ItemRequest->ItemEditForm() which returns the Form object where subsequently FormRequestHandler->httpSubmission() is called. This will then look at the submitted data to figure out what action was clicked (eg $_REQUEST['action_doApproveEvent']) and try to find that action.
The way it tries to find that, is checking if it itself has a method called doApproveEvent, if that fails, it will try Form->getController()->doApproveEvent() or something like that. In the case of a GridField, that controller is GridFieldDetailForm_ItemRequest which means it will try to call GridFieldDetailForm_ItemRequest->doApproveEvent()
So, that means DataObject->getCMSFields() lets you easily add FormFields (and FormActions) into your form body.
But it does not provide a means of adding a method to handle the submission.
That's why, for custom actions you need to modify the Controller (GridFieldDetailForm_ItemRequest in this case).
You are doing this by creating a Extension which you attached to GridFieldDetailForm_ItemRequest.
Any method in your Extension is added to the thing it's attached to, so if you add a method called updateFormActions, it will kind of become GridFieldDetailForm_ItemRequest->updateFormActions().
And if you recall from earlier, the controller will call $this->updateFormActions() during the creation of the form.
Additionally, as I explained earlier, when a FormAction is named doApproveEvent it will look for a GridFieldDetailForm_ItemRequest->doApproveEvent(), which now exists because you added it through that Extension.
So, in summary: you have to add custom FormActions through an Extension on the Controller that controls the form (or on the form itself
PS: the old post from
bummzack you linked to worked in 3.x, because the Controller in his example that created the form was an instance of LeftAndMain.

SilverStripe enum values to menu

I'm trying to pull out enum values from a dataobject to act as a menu/filter. I have not been able to find any documentation on how to do this and my attempts have all failed.
For example I have:
class Specification extends DataObject {
private static $db = array (
'Standard' => 'Enum("BS 1400,AS 1565")'
);
}
I'm trying to do something like:
public function Standards() {
$stnds = Specification::get()->dbObject('Standard')->enumValues();
$list = ArrayList::create();
foreach ($stnds as $stnd) {
$list->push($stnd);
}
return $list;
}
I want to be able to loop the resulting values in the template, but can't access the labels - if I do:
<% loop Standards %>
$Pos
<% end_loop %>
This gives me 1 2, which suggests it is working, but cannot access the enum value labels in the template.
How do I correctly return these values so they can be looped through in the template?
When you push items into an ArrayList object, they are just stored inside a php array. The SS template parser does not deal with php arrays, therefore one solution to your issue is to wrap your item inside an ArrayData before pushing it, like below:
public function Standards(){
$stnds = Specification::get()->dbObject('Standard')->enumValues();
$list = ArrayList::create();
foreach ($stnds as $stnd) {
$list->push(new ArrayData(array('Standard' => $stnd)));
}
return $list;
}
Then, in your template:
<% loop Standards %>
<h1>$Pos $Standard</h1>
<% end_loop %>

Concrete5 - Why is my block controller set() not working?

I have a custom block which has a default view with a form in it. When that form is submitted I set a controller flag and the block is (should be) updated to display more information.
The problem is my view is treating it like I have no data/variables set
Controller.php
public $unlocked = false;
public $employer;
public $shortname = "not loaded";
public function on_page_view() { //already overridden because I'm compiling LESS
...
$this->setViewVariables();
}
function setViewVariables() {
$this->set('shortname', $this->shortname);
$this->set('is_unlocked', $this->unlocked);
...
}
public function action_accesscode_unlock() {
$this->unlocked = true;
$this->shortname = "fred";
//Have also tried calling $this->setViewVariables(); as well,
//before I realised view() and on_page_view() were called after this anyway
}
View.php
<?php if ( !$is_unlocked ) {
echo $shortname; //does correctly display the default value
?>
<form action="<?php echo $this->action('accesscode_unlock')?>" id="accessform" method="post">
...
</form>
<?php } else {
//THIS section is never displayed (always reloads form with default name)
echo $shortname;
} ?>
What am I doing wrong here so that the new variable values are never set in the view?
Edit
After replying to JohnTheFish I just realised, the LESS compilation code I use includes the following lines (used to get block path). Could this be changing the instance used for different parts of the lifecycle?
$bv = new BlockView();
$bv->setController($this);
$bv->setBlockObject($this->getBlockObject());
on_page_view runs before action_accesscode_unlock, so the logic of action_accesscode_unlock does not happen until after the variables are set.
You could try adding a call to setViewVariables to the end of action_accesscode_unlock.
(In answer to your edit, yes, it could)

silverstripe custom form field error message

In my form processing function I have:
$form->addErrorMessage('Email', 'Your email address is already registered in our system','bad');
and in my template:
$Fields.FieldByName(Email)
I can see that the bad class has been added to the input, but how do I display an inline error message?
I'm not sure if this is what you are looking for, but I do something like this:
public function MyFormAction(){
//do something
if(//something is wrong){
$this->MyErrorMessage = 'Something is wrong';
return $this->renderWith(array('MyFormPage', 'Page'));
}
}
And then in the template I can put where I want:
<% if $MyErrorMessage %>
<p class='bad'>$MyErrorMessage</p>
<% end_if %>
You have to only add bellow in your $form->addErrorMessage('Email', 'Your email address is already registered in our system','bad');
return $this->redirectBack();
I'm using this on all my forms. Be sure to use this inside a Submit function. You will be redirected to your form with the error message below the field Email. You dont need to create a custom template for this.

SilverStripe 3: function in controller called within a loop in the template

I've written a simple custom function to take care of more than (>)
This works fine and returns 1 or 0.
It stops working when I pass $Pos to the function when it's called withing a loop.
SilverStripe Version 3
Controller
function MoreThen($pos, $value) {
if($pos > $value) {
return TRUE;
} else {
return FALSE;
}
}
Template.ss
<% loop GalleryObjects %>
<% if Top.MoreThen($Pos,2) %>
$Pos
<% end_if %>
AFAIK it is not possible to use variables as arguments of function calls, only concrete values. Depending on what you want to do, you might want to look at using GalleryObjects.limit() in your template, or write a specific getter that would return only GalleryObjects with an offset greater than 2. Hope this helps

Resources