Totara forms library

Forms in Totara are managed using a forms library, known internally as formslib. Formslib was inherited from Moodle but was originally based on a PEAR library. However that library is no longer supported and Moodle now maintains its own version which has been modified to suit the platform.

The Moodle formslib has a number of problems and limitations that make developing forms challenging in some areas, so we decided to implement a new Totara forms library to replace the current library. The new Totara forms library is available from Totara 9, and is added in addition to the Moodle formslib. That means all existing forms will continue to work. For new development a Totara developer can choose which library they wish to use. As of the Totara 9 release most forms continue to use the Moodle formslib, but new forms will be developed using the new library and we will migrate old forms to the new library when we want to make use of new features.

Using the Totara forms library (for developer who knows the current form library)

Summary

The table below lists the key differences between old and new forms libraries:

FeatureLegacy forms libraryNew forms library
ExtensibleTo add new form elements you must customise core code.Any Totara component can define reusable form elements that are then available for use by any form.
Object orientedForm elements are not fully object orientated. It is hard to modify the properties or behaviour of a form element.Form elements are fully object orientated. Form element objects can be passed around and modified. More control is delegated to the form element rather than being handled centrally by the form.
TestableThe forms library was hard to test.The new forms library is easy to test, and comes with a complete set of unit tests covering all existing functionality and elements.
HTML5No HTML5 form elements.A number of new HTML5 form elements have been created. This offers elements such as number, email, and date fields that support native interfaces depending on the platform (for example custom keyboard layouts on mobile devices).
Data modelPoor data model. Data was defined in several locations (Form definition, custom data, setDefault, definition_after_data), which made it hard to know the state of data during processing.Greatly simplified and improved data handling with clear handling of data processing. This requires a slightly different approach to passing data into a form but greatly simplifies the data handling and improves security.
TemplatesNo support for form templates.All form elements now implemented using mustache templates, allowing for templates to be overridden in themes.
Javascript form handlingDifficult to support rendering and submission of forms via Javascript.

Much more straightforward to render, submit and handle forms dynamically via Javascript. Native support for Javascript controllers, loading the same form multiple times on the same page, or loading forms within Javascript dialogs. Forms can be reloaded via AJAX to support fully dynamic forms.

Javascript in elementsNo consistent handling of form element Javascript.Form elements can implement their own AMD module for isolating element specific Javascript. Form Javascript is handled in an object orientated way via a central controller - so responsibility for the element is fully delegated to each element but centrally managed within the form JS.
Complex element supportPre- and post-processing is required for complex element types such as the File manager or HTML editor.Due the improved data handling, elements have more control over the processing, reducing the boilerplate code required to manage complex fields.

Differences by example

This section compares the differences in syntax between the two libraries, showing by example how to implement the same functionality in each case.

Form definition

Both libraries define a form via the creation of a class, but the new library extends a different base class and uses autoloading and namespaces.

Old library

// /component/path/example_edit_form.php
 
require_once($CFG->libdir.'/formslib.php');
 
class example_edit_form extends moodleform {
    function definition() {
        // define form elements here
    }
}

New library

// /component/path/classes/form/example.php
 
namespace component_name\form;
 
class example extends \totara_form\form {
    protected function definition() {
        // define form elements here
    }
}

Form instantiation

To render or process a form you need to instantiate a form instance.  In both cases modern best practice is to autoload the class by locating it in the component's "classes/form" folder. In the case of the legacy library you will also see plenty of manually required form classes (shown below).

Notice that the constructor arguments have changed in the new library:

Old library

// /component/path/edit.php
 
require_once('example_edit_form.php');
 
// ...
 
$mform = new example_edit_form($action, $customdata);

New library

// /component/path/edit.php
 
// ...
 
$form = new \component_name\form\example($currentdata, $parameters, $idsuffix);

Some key differences here are:

  • The old library accepts a URL to define the form action attribute (where the form is submitted to). The new forms library always submits to the current page, or in the case of AJAX submissions via a central controller at /totara/form/ajax.php.

  • The old library accepts a method argument to define how the form should be submitted (GET, POST, etc). The new library always uses POST for security reasons.

  • The old library accepts a $customdata argument that allows you to pass data into the form definition. In the new library this is now called $parameters. Parameters are available via $this->parameters in the form definition class. Parameters should only be used to pass data required to define the form. Data that makes up the form submission (e.g. element values) should never be passed as parameters - instead it must be passed in via the new $currentdata argument. Conversely, don't use a hidden form field to pass data around unless it needs to be part of the form submission, use parameters instead.

  • In the old library there was a distinction between default form values (set using setDefault() in the form definition), and the current form data (set using $mform->set_data()). In the new library this has all been merged into the $currentdata attribute. See below for an example of how to deal with this now.

  • The old library had other arguments for $target and $attributes. This is now handled by modifying the template.

  • The old library supported $formidprefix. The new library supports an optional $idsuffix parameter which you can use to ensure forms are unique if you need to reuse the same definition within a single page.

Adding form elements

In the new library form elements are added to the form model. Form elements can have their properties modified (even after they've been added to the form).

Old library

 // /component/path/example_edit_form.php

require_once($CFG->libdir.'/formslib.php');

class example_edit_form extends moodleform {
    function definition() {
        $mform = $this->_form;

        $mform->addElement('text', 'textfieldname', 'Text label', 'maxlength="10"');
        $mform->setType('textfieldname', PARAM_TEXT);
        $mform->addHelpButton('textfieldname', 'stringkey', 'componentname');
        $mform->addRule('textfieldname', 'Text must be provided', 'required');
        $this->model->add_action_buttons();
    }
}

New library

// /component/path/classes/form/example.php
 
namespace component_name\form;

use totara_form\form\element\text;
 
class example extends \totara_form\form {
    protected function definition() {

        $textelement = $this->model->add(new text('textfieldname', 'Text label', PARAM_TEXT));
        $textelement->set_attribute('maxlength', 10);
        $textelement->add_help_button('stringkey', 'componentname');
        $textelement->set_attribute('required', true);
        $this->model->add_action_buttons();
    }
}

Data handling

To simplify data handling, all data is passed in at the time of form definition. In the example below note how the default value is passed into $currentdata instead of being set in the form definition via setDefault(). This means the form always knows the state of data before processing:

Old library

// /component/path/edit.php
 
$mform = new example_edit_form($action, $customdata);
if (!empty($id)) {
    // This is the 'edit' form, fill with existing item.
    $item = load_item_by_id($id);
    $mform->set_data($item);
}
// /component/path/example_edit_form.php
class example_edit_form extends moodleform {
    function definition() {
        $mform = $this->_form;

        $mform->addElement('text', 'textfieldname', 'Text label');
        $mform->setDefault('textfieldname', 'Default value');
        $this->model->add_action_buttons();
    }
}

New library

// /component/path/edit.php
 
if (empty($id)){
    // This is a new item form.
    $currentdata['textfieldname'] = 'Default value';
} else {
    // This is an existing item, populate the data.
    $item = load_item_by_id($id);
    $currentdata['textfieldname'] = $item->textfieldname;
}
 
$form = new \component_name\form\example($currentdata);
// /component/path/classes/form/example.php

namespace component_name\form;

use totara_form\form\element\text;
 
class example extends \totara_form\form {
    protected function definition() {
        $this->model->add(new text('textfieldname', 'Text label', PARAM_TEXT));
        $this->model->add_action_buttons();
    }
}

Data processing

Once the form has been submitted you'll want to do something to handle the new data. This is very similar to with the old library.

Old library

// /component/path/edit.php
 
$mform = new example_edit_form($action, $customdata);
if ($mform->is_cancelled()) {
    // redirect
} else if ($data = $mform->get_data()) {
	// create or update record, then redirect.
}

New library

// /component/path/edit.php

$form = new \component_name\form\example($currentdata);
if ($form->is_cancelled()) {
    // redirect
} else if ($data = $form->get_data()) {
	// create or update record, then redirect.
}
 

Form rendering

To render a form use the render() method. This replaces the display() method used by the old library.

Old library

// /component/path/edit.php
 
$mform = new example_edit_form($action, $customdata);

// handle processing
 
echo $OUTPUT->header();
 
echo $mform->display();
 
echo $OUTPUT->footer();

New library

// /component/path/edit.php
 
$form = new \component_name\form\example($currentdata);

// handle processing

echo $OUTPUT->header();

echo $form->render();

echo $OUTPUT->footer();

Complex element types

Some form elements are more advanced and require special handling. Two common examples are the file picker and HTML editor. The new library significantly simplifies the handling of these elements, avoiding the need for manual processing in every form.

The old approach is complex and not worth repeating here - see this link for more details of the old process: https://docs.moodle.org/dev/Using_the_File_API_in_Moodle_forms#Form_elements

The code below details the new approach. Notice that pre and post processing functions (such as file_prepare_standard_editor() and file_postupdate_standard_editor() are no longer required.

New library

// /component/path/edit.php

// No need for any preprocessing here!
$form = new \component_name\form\example($currentdata);
if ($data = $form->get_data()) {
 
    // Editor data is automatically post-processed, no need to handle anything here before saving to DB!
    $htmleditordata = $data->editorhtmlname;
 
    // To obtain and manipulate files stored by form elements use get_files():
	$files = $form->get_files();
    foreach($files as $el => $list) {
        echo "$el: ";
        foreach ($list as $file) {
            /** @var stored_file $file */
            echo "\n   * " . $file->get_filepath();
            if (!$file->is_directory()) {
                echo $file->get_filename() . ' (' . $file->get_filesize() . ' bytes)';
            }
        }
        echo "\n";
    }
}

// /component/path/classes/form/example.php

namespace component_name\form;

use totara_form\form\element\filepicker,
    totara_form\form\element\filemanager,
    totara_form\form\element\editor;
 
class example extends \totara_form\form {
    protected function definition() {
        // complex elements are just defined in the form like any other element:
        $this->model->add(new filepicker('filepickername', 'File picker label'));
        $this->model->add(new filemanager('filemanagername', 'File manager label'));
        $this->model->add(new editor('editorhtmlname', 'HTML editor label', array('maxfiles' => -1)));
        $this->model->add_action_buttons();
    }
}

Using files within the editor

If you want the editor to offer the option to access files from a repository you need to add some additional code to pass in the file area information. If you are creating a new item, it is likely that the file area for the item will not exist when the form is being built. To support this situation you can set the $context and item $id later when the form is being processed. You must update the file area for the files uploaded in order for them to be saved correctly. Here is an example:

// /component/path/edit.php

if (empty($id)){
    // This is a new item form.
    $currentdata = [
        'editorhtmlname' => 'Default value for new items',
        // $context and item $id may not be known for new items yet so leave null.
        // Note: the key for this must be of the form: "{$editorelementname}filearea".
        'editorhtmlnamefilearea' => new \totara_form\file_area(null, $component, $filearea, null)
    ];
} else {
    // This is an existing item, populate the data.
    $currentdata = [
        'editorhtmlname' => "Data for item {$id} from DB",
        // This time $context and item $id are known (or can be calculated) so set them here.
        // Note: the key for this must be of the form: "{$editorelementname}filearea".
        'editorhtmlnamefilearea' => new \totara_form\file_area($context, $component, $filearea, $id)
    ];
}

$form = new \component_name\form\example($currentdata);
if ($data = $form->get_data()) {
    if (empty($id)) {
        // New item: Create it here and obtain item $id;
        $id = $DB->insert_record('itemtablename', $data);
    }
    // This saves the uploaded files and moves them from the user's draft file area into the component file area.
    $form->update_file_area('editorhtmlname', $context, $id);
    redirect();
}

Freezing form elements

The old library supported freezing of elements but the data handling meant that the behaviour was complex - it wasn't always clear what value a frozen element would have. In the new library this has been simplified: A frozen form element is displayed as read only in the interface, and the original value from $currentdata is always returned on submission. Since all data is now passed via current data there is no need for $mform->setConstant() - just set the value in $currentdata and freeze the element to get the same effect.

New library

// /component/path/example.php
 
// Whatever you set here...
$currentdata['textfieldname'] = 'Constant value';
 
$form = new \component_name\form\example($currentdata);
 
if ($data = $form->get_data()) {
    // is guaranteed to be the same here for a frozen element.
    $textfieldvalue = $data->textfieldvalue;
}
// /component/path/classes/form/example.php

namespace component_name\form;

use totara_form\form\element\text;
 
class example extends \totara_form\form {
    protected function definition() {

        $textelement = $this->model->add(new text('textname', 'Text label', PARAM_TEXT));
        $textelement->set_frozen(true);
 
		$section = $this->model->add(new section('sectionname', 'Section label'));
        // You can freeze a section, which will freeze all elements within it.
        $section->set_frozen(true);
        $section->add(new text('text1name', 'Text 1 label', PARAM_TEXT));
        $section->add(new text('text2name', 'Text 2 label', PARAM_TEXT));
 
        $this->model->add_action_buttons();
    }
}

 In the new library you can freeze a group and it will freeze all child elements. See below for more details on groups.

Groups

The old library uses groups to group together form elements. This is typically used to rearrange a set of elements onto a single line against a single label (for example a date selector). The old library also had a 'header' element type for displaying a field set around a set of elements.

In the new library there are two types of groups - buttons are a group for combining action buttons together. Sections are a group for creating a set of elements that is surrounded by a field set and can be acted on as a group. To put a set of elements on a single line in the new library you not use groups. Instead you should create a new element and override the template with the appropriate HTML.

Old library

// /component/path/example_edit_form.php
 
class example_edit_form extends moodleform {
    function definition() {
        $mform = $this->_form;
 
        $mform->addElement('header', 'headername', 'Header label');
 
        // These elements end up instead the header above.
        $mform->addElement('text', 'text1name', 'Text 1 label');
        $mform->addElement('text', 'text1name', 'Text 2 label');
        // The code below puts two text elements together into a single form row.
        $sectiongroup=array();
        $sectiongroup[] =& $mform->createElement('text3', 'text3name', 'Text 3 label');
        $sectiongroup[] =& $mform->createElement('text4', 'text4name', 'Text 4 label');
        $mform->addGroup($sectiongroup, 'sectiongroup', 'Group label', ' ', false);
 
        // This is the manual way to add a button group.
        // You can also use $this->model->add_action_buttons() as a shortcut.
        $buttonarray=array();
        $buttonarray[] =& $mform->createElement('submit', 'submitbutton', get_string('savechanges'));
        $buttonarray[] =& $mform->createElement('submit', 'cancel', get_string('cancel'));
        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
    }
}

New library

// /component/path/classes/form/example.php

namespace component_name\form;

use totara_form\form\element\text,
    totara_form\form\group\section,
    totara_form\form\group\buttons,
    totara_form\form\element\action_button,
    component_name\form\element\mynewelement;

class example extends \totara_form\form {
    protected function definition() {

        // A section is a type of group that contains elements.
        $section = $this->model->add(new section('section1', 'Header label'));
        // Adding elements to a section.
        $text1 = $section->add(new text('text1fieldname', 'Text 1 label', PARAM_TEXT));
        $text2 = $section->add(new text('text2fieldname', 'Text 2 label', PARAM_TEXT));
 
        // To combine multiple inputs into a row you would create a new element.
        // This would require an element definition (not shown). See section of custom form elements for details.
        $this->model->add(new mynewelement('mynewelementname', 'Group label'));
        // Button groups are another type of group
        // This is the manual way to add a button group.
        // You can also use $this->model->add_action_buttons() as a shortcut.
        $actionbuttonsgroup = $this->model->add(new buttons('actionbuttonsgroup'));
        $actionbuttonsgroup->add(new action_button('submitbutton', 'Submit', action_button::TYPE_SUBMIT));
        $actionbuttonsgroup->add(new action_button('cancelbutton', 'Cancel', action_button::TYPE_CANCEL));
    }
}

In the old library headers followed set rules (expand the first and any with required elements, collapse the rest). You can force a collapsed element to be expanded, but you cannot turn off expanding or force an expanded header to be collapsed.

In the new library sections can be set to be collapsible or not (via set_collapsible()) and you can also set whether they are expanded or not by default (via set_expanded()). The following rules apply:

  • Sections are collapsible by default, unless set_collapsible(false) is used. When not collapsible the expanded option is ignored.
  • If set_expanded() is not used the default behaviour is as follows:
    • If there are any required fields, sections containing required fields are always expanded. All other sections are not expanded.
    • If there are no required fields, the first section is expanded and all others are not expanded.
  • If set_expanded() is used then all sections are collapsed by default, and any sections with set_expanded(true) set are expanded.
  • If you want all sections, including sections with required elements to be not expanded you need to explicitly call set_expanded(false).
 

Old library

 
 // /component/path/example_edit_form.php

require_once($CFG->libdir.'/formslib.php');

class example_edit_form extends moodleform {
    function definition() {
        $mform = $this->_form;

        // No way to disable expansion and collapsing of a header in old library.

        // Not possible to prevent expansion of header with required fields in old library.
 
        $mform->addElement('header', 'headername', 'Expanded despite no required fields');
        $mform->setExpanded('headername');
        $mform->addElement('text', 'text1', 'Text 1 label');
        $mform->addElement('text', 'text2', 'Text 2 label');
 
        $this->model->add_action_buttons();
    }
}
 

New library

 
// /component/path/classes/form/example.php
 
namespace component_name\form;

use totara_form\form\element\text,
    totara_form\form\group\section;
 
class example extends \totara_form\form {
    protected function definition() {

        $section1 = $this->model->add(new section('section1', 'Non collapsible'));
        $section1->set_collapsible(false);
        $section1->add(new text('text1', 'Text label 1', PARAM_TEXT));
        $section1->add(new text('text2', 'Text label 2', PARAM_TEXT));

        $section2 = $this->model->add(new section('section2', 'Collapsed despite required fields'));
        $section2->set_expanded(false);
        $section2->add(new text('text3', 'Text label 3', PARAM_TEXT));
        $text4 = new text('text4', 'Text label 4', PARAM_TEXT);
        $text4->set_attribute('required', true);
        $section2->add($text4);

        $section3 = $this->model->add(new section('section3', 'Expanded despite no required fields'));
        $section3->set_expanded(true);
        $section3->add(new text('text5', 'Text label 5', PARAM_TEXT));
        $section3->add(new text('text6', 'Text label 6', PARAM_TEXT));
        $this->model->add_action_buttons();
    }
}

Wizard group

The new library provides a new group option called wizard. This allows you to split a large form over multiple stages while providing navigation elements to move between them. A wizard represents the entire form, rather than a set of independent forms. If you want to be able to submit each stage independently you'd be better off using separate forms. Because moving between stages is done via Javascript, forms that use the wizard group must define a form controller. See AJAX form handling section below for more details.

// /component/path/classes/form/example_wizard.php

namespace component_name\form;

class example_wizard extends \totara_form\form {
    public static function get_form_controller() {
        // Forms using wizards must define a form controller.
        return new example_wizard_form_controller();
    }

    public function definition() {

        $wizard = $this->model->add(new wizard('wizard_name'));
        // Optional setting to allow user to jump forward without completing sections.
        // Jump ahead is prevented by default.
        $wizard->prevent_jump_ahead(false);

        $s1 = $wizard->add_stage(new wizard_stage('stage_one', 'Stage one'));
        $s1a = new \totara_form\form\element\text('s1a', 'Stage one input a', PARAM_TEXT);
        $s1->add($s1a);
        $s1b = new \totara_form\form\element\text('s1b', 'Stage one input b', PARAM_TEXT);
        $s1->add($s1b);

        $s2 = $wizard->add_stage(new wizard_stage('stage_two', 'Stage two'));
        $s2a = new \totara_form\form\element\text('s2a', 'Stage two input a', PARAM_TEXT);
        $s2->add($s2a);
        $s2b = new \totara_form\form\element\text('s2b', 'Stage two input b', PARAM_TEXT);
        $s2->add($s2b);

        $wizard->set_submit_label('Complete form action');
        $wizard->finalise();
    }
}

Validation

In the old library, the addRule() method was used to enforce client and/or server side validation. The new library defines validator classes which can be defined, extended and reused by any component. Validator classes can be applied against all elements of a particular type, or against any element that specifies a particular attribute. Note that in the new library validators typically enforce server side validation only, but are complemented by client side validation that is applied by the element (via its AMD module) or browser (in the case of HTML5 attributes such as required).

Old library

// /component/path/example_edit_form.php
 
require_once($CFG->libdir.'/formslib.php');
 
class example_edit_form extends moodleform {
    function definition() {
        $mform = $this->_form;
        $mform->addElement('text', 'textname', 'Text label');
        $mform->addRule('textname', 'This element is required', 'required', null, 'client');
 
        $mform->addElement('text', 'emailname', 'Email label');
        $mform->addRule('emailname', 'Enter an email address', 'email', null, 'client');
 
		$mform->addElement('text', 'evennumbername', 'Even number label');
        $callback = function($val) {
            return (is_numeric($val) && $val % 2 == 0);
        };
        $mform->addRule('evennumbername', 'Enter an even number', 'callback', $callback);
    }
}

 

New library

// /component/path/classes/form/example.php
 
namespace component_name\form;
 
use totara_form\form\element\text,
    totara_form\form\element\email,
    totara_form\form\element\number,
    component_name\form\validator\evennumber;
class example extends \totara_form\form {
    protected function definition() {
        $text = $this->model->add(new text('textname', 'Text label', PARAM_TEXT));
        // An element is made required by setting the required attribute.
        $text->set_attribute('required', true);
 
        // The email element automatically applies the email validator so nothing else is needed here.
        $text = $this->model->add(new email('emailname', 'Email label'));
 
        // The number type will automatically apply the number validator. For custom requirements
        // such as even number we define a new validator and apply that to the element (or we could
        // have created a custom form element and applied the validator there).
        $evennum = $this->model->add(new number('evennumbername', 'Even number label'));
        $evennum->add_validator(new evennumber());
    }
}

 

// /component/path/classes/form/validator/evennumber.php
 
namespace component_name\form\validator;

use totara_form\item,
    totara_form\element_validator;
 
class evennumber extends element_validator {
    // Implement your validator here.
}

It is also possible to define a validation() method on the form class, to handle more complex validation across multiple elements. This works almost the same in the new library, other than some increased strictness in the method signature: 

Old library 

// /component/path/example_edit_form.php
 
require_once($CFG->libdir.'/formslib.php');
 
class example_edit_form extends moodleform {
    function definition() {
        // define form elements here
    }
 
    function validation($data, $files) {
        $errors = parent::validation($data, $files);
 
        // Check form submission 
		if ($data['elementname'] == 'somecondition') {
            $errors['elementname'] = 'Error to display against elementname';
        }
 
        // Form submission fails unless $errors is empty.
        return $errors;
    }
}

New library

// /component/path/classes/form/example.php
 
namespace component_name\form;
 
class example extends \totara_form\form {
    protected function definition() {
        // define form elements here
    }
 
    protected function validation(array $data, array $files) {
        $errors = parent::validation($data, $files);

        // Check form submission 
		if ($data['elementname'] == 'somecondition') {
            // Note that if the elementname is invalid error will be displayed against
            // the overall form.
            $errors['elementname'] = 'Error to display against elementname';
        }

        // Form submission fails unless $errors is empty.
        return $errors;
    }
}

New features added by new library

The following functionality is new to the current library.

Overriding site-wide form templates

You can override any form template for all forms site-wide by creating an appropriate template file in your theme. For example, to override the text element template which is stored here:

totara/form/templates/element_text.mustache

copy the file to:

theme/yourthemename/templates/totara_form/element_text.mustache

then modify the template as required. That will use the new template for all text elements across the whole site. See the "totara/form/templates/" folder for a full list of templates that can be overridden.

Overriding templates for a specific form

To override the template for an element in a single form, you need to create a new element by extending the element class. Fortunately that is now easy and can be done from within any component. See the sections below on custom forms and form elements for more details.

Reloading forms

The new forms library supports reloading a form. This allows the $currentdata to be updated based on user input without the form being submitted. This can be used to modify the structure of the form since the form is rebuilt based on the new $currentdata when reloaded.

The simplest way to reload the form is to include a reload button that the user can press:

New library

// /component/path/classes/form/example.php

namespace component_name\form;

use totara_form\form\element\action_button;

class example extends \totara_form\form {
    protected function definition() {

        // Add other form elements here.
 
		$actionbuttons = $this->model->add_action_buttons(); 
        $actionbuttons->add(new action_button('reloadbutton', 'Reload form', action_button::TYPE_RELOAD));
    }
}

It is possible for any element to prevent form submission, see the section of custom form elements for more details on how to build a custom element with reload functionality.

Typically you don't need to add any explicit action on reload - the form will be updated according to the new data. However if you want to handle reloads explicitly you can add an option into the form processing code:

New library

// /component/path/edit.php

$form = new \component_name\form\example($currentdata);
if ($form->is_cancelled()) {
    // redirect
} else if ($form->is_reloaded()) {
    // form reloading is a new feature, add any reload
    // handling code here.
} else if ($data = $form->get_data()) {
	// create or update record, then redirect.
}

Reloads can also be triggered via Javascript (for instance when an element changes to allow you to support dynamic forms). If your form uses and AJAX controller you can reload just the contents of the form DOM element to avoid having to reload the whole page. See the section on AJAX form handling for more details.

AJAX form handling 

The new library supports rendering and submitting forms directly via Javascript, which makes it possible to load, reload, submit and process the whole form via AJAX. Below is a simple example showing a form being dynamically inserted into a page and handled client side.

The first thing you need is a PHP form definition, just like for a regular form. This is exactly the same as it would be in a non-AJAX example, so it is possible to make any form work via AJAX.

New library

// /component/path/classes/form/example.php

namespace component_name\form;

use totara_form\form\element\text,
    totara_form\form\element\action_button;
class example extends \totara_form\form {
    protected function definition() {

        $this->model->add(new text('textname', 'Text label', PARAM_TEXT));
 
		$actionbuttons = $this->model->add_action_buttons(); 
        $actionbuttons->add(new action_button('reloadbutton', 'Reload form', action_button::TYPE_RELOAD));
    }
}

Next you need to define an AJAX controller for the form. The AJAX controller provides the necessary methods so that the core form Javascript can handle this particular form via AJAX. In particular it must define two methods:

  • get_ajax_form_instance() - This method is called when the form is requested and is responsible for access control, getting and checking input parameters, and defining the $currentdata for the form. It returns a valid form instance. You can think of it as doing the same things as would happen in the early part of a page when displaying a regular form on a page.

  • process_ajax_data() - This method is called when the form is submitted and is responsible for processing the form submission. You can think of it as doing the same things as would happen inside the if ($data = $form->get_data()) { ... } part of the page when processing a regular form.

Here is an example controller:

// /component/path/classes/form/example_controller.php

namespace component_name\form;

use totara_form\form,
    totara_form\form_controller;
 
class example_controller extends form_controller {
    /** @var form $form */
    protected $form;

    /**
     * @param string|false $idsuffix string for already submitted form, false on initial access
     * @return form
     */
    public function get_ajax_form_instance($idsuffix) {
        // Access control checks.
        require_login();
        require_sesskey();
        require_capability('moodle/site:config', \context_system::instance());

        $id = optional_param('id', 0, PARAM_INT);
        // Here you might also check the current user is able to modify item with this id.
        if (empty($id)){
            // This is a new item form.
            $currentdata['textfieldname'] = 'Default value';
        } else {
            // This is an existing item, populate the data.
            $item = load_item_by_id($id);
            $currentdata['textfieldname'] = $item->textfieldname;
        }
        // Set any parameters to pass into the form.
        $parameters = array();

        // Create the form instance.
        $this->form = new example($currentdata, $parameters, $idsuffix);

        return $this->form;
    }

    /**
     * Process the submitted form.
     *
     * @return array processed data
     */
    public function process_ajax_data() {
        $result = array();
        $result['data'] = (array)$this->form->get_data();
        $result['files'] = array();

        $files = $this->form->get_files();
        foreach($files as $elname => $list) {
            $result['files'][$elname] = array();
            foreach ($list as $file) {
                /** @var \stored_file $file */
                if ($file->is_directory()) {
                    $path = $file->get_filepath();
                } else {
                    $path = $file->get_filepath() . $file->get_filename();
                }
                $result['files'][$elname][] = $path;
            }
        }

        return $result;
    }
}

The underlying page that will have the form embedded in just needs a DOM element to hook into, plus the Javascript to initialise it. It might look something like this:

// /component/path/example.php

echo $OUTPUT->header();

echo '<div id="example_form_div"></div>';
$PAGE->requires->js_call_amd('component_name/example', 'initialize');

echo $OUTPUT->footer();

Finally there is the Javascript.

// /component/path/amd/src/example.js

define(['jquery', 'totara_form/form', 'core/notification'], function($, form, notification) {
    return {
        initialize : function() {
            var formclass = 'component_name\\form\\example';
            var parameters = {id : '1'};
            var formelement = 'example_form_element'; // The HTML id where you would like the form to be loaded
            var idsuffix = ''; // Set if same form used multiple times within a page.

            var preset_data = function(data) {
                data = JSON.stringify(data).replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;');
                return '<pre>' + data + '</pre>';
            };

			form.load(formclass, idsuffix, parameters, formelement).done(function(result, formId) {
				var handlers = {
                	cancelled : function() {
                    	notification.alert('Example', 'Form cancelled', 'Restart');
                	},
                	submitted : function(data) {
                    	notification.alert('Example', 'Form submitted: ' + preset_data(data), 'Restart');
                	}
            	};

				form.addActionListeners(formId, handlers);
			});
        }
    };
});

HTML5 features

There are a number of new form elements that support HTML5 input types:

  • Datetime
  • Email
  • Number
  • Tel
  • URL

The advantage of using these element types is that their is validation built into the browser, plus the operating system can modify input methods to suit the data type. For example on mobile devices the keyboard layout will change to suit the input type. Some elements support additional attributes - for example the Number element supports the min/max/step attributes to further constrain input and text field types support the placeholder attribute for providing some descriptive text when the field is empty.

The new library also makes use of the required attribute which also provides validation via the browser.

Custom form elements classes

Adding elements in components

In the Totara forms library, custom form elements are first-class citizens, that is they have the ability to do everything that a regular form element can do. In fact all the existing system form elements are merely form elements defined by the totara_form component. To create a new form element you simply create a new auto-loaded class that extends the totara_form\element abstract class or another existing element. By convention new elements should be stored in component/path/classes/form/element/. To see a good example of how to extend an existing element take a look at the totara_form\email element which extends the base 'text' element. It is located in code at /totara/form/classes/form/element/email.php.

Here is a new form element that extends the 'text' element:

// /component/path/classes/form/element/customtext.php
 
namespace component_name\form\element;

class customtext extends \totara_form\form\element\text {
}

Just by creating the class it is now possible to start using this new element. At this stage it will behave exactly the same as the 'text' element because no changes have been made:

// /component/path/classes/form/example.php
 
namespace component_name\form;

use component_name\form\element\customtext;
 
class example extends \totara_form\form {
    protected function definition() {
        $customtextelement = $this->model->add(new customtext('customtextfieldname', 'Custom Text label', PARAM_TEXT));
        $this->model->add_action_buttons();
    }
}

In this example the element is being used by a form within the same component, but new elements are available to forms from any component in the site.

Modifying the element template

One simple customisation that you might want to do is to modify a specific element template. While this can be done site-wide by overriding the default element template in your theme, if you want to change an element in only a few specific locations this approach gives you the flexibility to do so. The template used for a specific element is determined dynamically inside the template based on the 'form_item_template' property in the template data. Therefore to override the template you need to extend and modify export_for_template() as follows:

// /component/path/classes/form/element/customtext.php
 
namespace component_name\form\element;

class customtext extends \totara_form\form\element\text {
    public function export_for_template(\renderer_base $output) {
        $result = parent::export_for_template($output);
        $result['form_item_template'] = 'component_name/element_customtext';
        return $result;
    }
}

You can now copy and modify the original template from /totara/form/templates/element_text.mustache to /component/path/templates/element_customtext.mustache and modify it to suit your needs.

Element Javascript

Form elements can include their own AMD javascript module to allow them to use Javascript to add interactions to the page. Like the element template above, the AMD module is specified via the 'amdmodule' property in the template data. By convention the amd module for an element would be named form_element_elementname and be located in /component/path/amd/src/elementname.js where component/path is the module which defines the element.

// /component/path/classes/form/element/customtext.php
 
namespace component_name\form\element;

class customtext extends \totara_form\form\element\text {
    public function export_for_template(\renderer_base $output) {
        $result = parent::export_for_template($output);
        $result['form_item_template'] = 'component_name/element_customtext';
        $result['amdmodule'] = 'component_name/form_element_customtext';
        return $result;
    }
}

Preventing submission via an element (reload button)

Any individual form element can prevent submission of the form, and instead reload the form with the submitted data. This allows for the creation of buttons which change the underlying form. To create an element which uses this functionality, simply define the method is_form_reloaded() and return true from it when you want to reload instead of submitting.

// /component/path/classes/form/element/customtext.php
 
namespace component_name\form\element;

class customtext extends \totara_form\form\element\text {
    public function is_form_reloaded() {
        $elementvalue = $this->get_field_value();
        if ($elementvalue = ...) { // specify some criteria here.
            // This will force the form to reload without submitting.
            return true;
        } else {
            // This will allow the form to be submitted as normal.
            return false;
        }
    }
}

More complex elements

 Sometimes you may want to wrap a form in HTML to generate a more complex composite form element to achieve a particular goal. In these cases you may want to create a completely new element by extending \totara_form\form\element directly.

Custom form class

Typically a form extends the main Totara form class \totara_form\form, overriding specific methods such as definition() and validation(). If you want to make more substantial changes to the form itself you can choose to extend other methods. In particular if you want to alter the forms templates you can do so as follows:

// /component/path/classes/form/example.php
 
namespace component_name\form;

class example extends \totara_form\form {
    protected function definition() {

       // define form elements here.
    }
 
    public function get_template() {
        // override this method to use your own form mustache template, e.g.:
        return 'component_name/form';
    }
}

You can copy the default template from /totara/form/templates/form.mustache to /component/path/templates/form.mustache and update to suit your needs. Note that if you do this you will need to review future changes to the core template and reapply to your customised version when necessary.