Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Published by Scroll Versions from space TDDM and version 1

Table of Contents
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.

...

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.

...

  • 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.

...

Some form elements are more advanced and require special handling. Two common examples are the filepicker 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 current old process: https://docs.moodle.org/dev/Using_the_File_API_in_Moodle_forms#Form_elements

...

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 fieldset 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 fieldset 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.

...

  • 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

 
Code Block
languagephp
 // /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

 
Code Block
languagephp
// /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  section below for more details.

Code Block
languagephp
// /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 

Code Block
languagephp
// /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 

Code Block
languagephp
// /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());
    }
}

...

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:

...

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 
Anchor
ajaxform
ajaxform

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.

...

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:

...

Code Block
languagejs
// /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');
                    	ajaxform.display(formclass, idsuffix, parameters, formdiv, handler);
                	},
                	submitted : function(data) {
                    	notification.alert('Example', 'Form submitted: ' + preset_data(data), 'Restart');
                    	ajaxform.display(formclass, idsuffix, parameters, formdiv, handler);
                	}
            	};

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

...