Obsolete:Moodle forms library

Jump to: navigation, search

Note: You are currently viewing documentation for Moodle 1.9. Up-to-date documentation for the latest stable version is available here: Moodle forms library.

OBSOLETE INFORMATION

This was a proposal for discussion that was not used.

Former discussions about documentation of the forms library that is used in Moodle

The Development:lib/formslib.php page is about the documentation of the forms library that was implemented as a result of old discussions and is archived here.

And now, on with the history ...

The OU has developed quite a nice in-house library to simplify creating Moodle editing forms. We would like to contribute it back to the community, and then as part of the accessibility work we are comissioning, get all Moodle forms modified to use it. However, before we can contribute it back, we need to get consensus withing the community that it is right for Moodle, and we need to tidy up the code a bit.

This document outlines how the library should work after cleaning up, so the community can see whether they like it, and OU developers know what is involved in the cleanup.

What the system will be capable of

The library allows developers to create editing forms by creating a high-level representation of the form as a data-structure in memory, configuring all necessary options, and setting initial values of fields, and then the library generates the actual HTML of the form to include in the page.

We are not planning to change significantly the way that Moodle editing forms look and function. However, by putting all the HTML (and JavaScript, CSS, ...) generation in one place, we make it much easier to make systematic improvements to accessibility, usability, client-side validation, etc. in future. Indeed, the OU's code already generates HTML that is a bit more accessible that most Moodle forms.

By allowing developers to think at a higher level, we make their life easier, in the same way that datalib saves them from worrying about the details of SQL most of the time.

The general way the library will work is that it will assume sensible defaults for everything to reduce the amount that has to be typed. So for a lot of input fields, you will only have to specify

  1. the field type (e.g. text, think <input type="" ... />, However the hope would be to move towards higher-level types, such as integer, path, url, as in require_param, etc.)
  2. the field name (think <input name="" ... />).

Everything else that is needed will be derived from these using sensible defaults. We need label text? We'll look up the field name in the lang file (each form will specify which lang file to use). We need a help link? Well, use /lang/en_utf8/help$langfile/$name.html. We need a field size? Each type will have a sensible default.

However, if you want to override any of these defaults, you can by setting that option explicitly. Because there will be lots of options, and normally you will only need to change a few, the library will use a named attribute style, so example code would look like:

$field = new text_field('fieldname');
$field->set_set('size', 30);
$field->set_set('label', 'lablestring');
 
// or 
 
$field = new text_field('fieldname');
$field->set_set(array('size' => 30, 'label' => 'lablestring'));
 
// or 
 
$field = new text_field('fieldname', array(
        'size' => 30, 
        'label' => 'lablestring'
));

In this example, 'lablestring' would again get looked up in the lang file automatically.

For this situation, where there are lots of options available, but most people only need to change a few, I think this style of API works better than PHP function calls with lots of optional parameters. The options available for each field type will be documented in the comment where that class is defined.

The library is designed to make it easy to add new field types, or new options to existing field types. Indeed, that is how the library has evolved so far: a bit at a time as needed.

New field types are just new PHP classes, and they are likely to be subclasses of a base class that only change a few things. They don't even have to be defined in the central form library. If you need an specialist field type only within one (e.g. contrib) module, you can create a new field-type subclass in that module code and use it in that modules forms without having to touch core code. And if we later want to move that field type into core, it is trivial to move the class definition.

Since we will have to tidy up the code anyway, all the function/class/method/option names in the API are potentially changable.

PHP API

Each thing in the API will be a PHP class, these basically fall into three categories: the whole form, groupings of form fields, and individual field types. Note that the groupings are currently only used for functional reasons, like showing or hiding groups of elements. Logical groupings, that would correspond to <fieldset> tags for accessibility, are not included yet, but could be added.

I am just going to list all the possible method calls. I will assume that it is clear what they do from their names (and I am running out of time).

The whole form

There is a class to represent a form, and most of the time you will be able to do everything you want to do by calling methods on this class, and you don't have to worry about the rest of the API.

$mf = new moodle_form($langfile);
 
$field = &$mf->add($fieldtype, $fieldname, $fieldoptions, $insertbefore);
$field = &$mf->add_item($fieldobject, $insertbefore); // $insertbefore is optional. By default things are inserted at the end of the form.
$field = &$mf->remove($fieldname);
$field = &$mf->get($fieldname);

These let you add or remove things from the form. The field you have just done stuff to is returned, but most of the time you will not bother to do anything with the return value. These method work with groups, etc. not just fields.

$mf->set_action($url);
$mf->set_init_html_editor(); // There was a reason why the library can't guess this from the list of fields.
$mf->set_submit_caption($langstringid);
$mf->set_hidden($fieldname, $value);
$mf->set_value($fieldname, $value); // The initial value for this field. (or use the $form attribute on show()).  
$mf->set_error($fieldname, $message); // Error message to be displayed in red next to this field if you are doing server-side validation.
 
$mf->show($action, $form);

Show the form. $form is an object holding the default value of each field that has not already been set using set_value().

$mf->add_xhtml($htmlphpcode, $insertbefore);
$mf->set_xhtml_param($name, $value);

You can insert arbitrary bits of HTML or PHP code into the form using this. When outputting the form, the library does eval('?>'.$htmlphpcode.'<?php '); in a place where $form is in scope, and the variables passed in via set_xhtml_param are available from an array as $xmlform[$name].

$new_mf = moodle_form::loadfile($xmlfilename);
$new_mf = moodle_form::loadstring($xmlstring);

Create a whole form using the XML syntax mentioned below.

Fields

The library also has classes representing the different field types, which you can use if you want.

$field = new xxxx_field($name, $options_array); // where xxxx is one of the field types below.
$field->set($optionname, $optionvalue);
$field->set($optionarray); // Array of $optionname => $optionvalue pairs.
$optionvalue = $field->get($optionname);
$field->set_value($value); // Sets the initial value of this field.
$field->output($form); // Print this form element. 

Normally you won't call output() directly. You will call $mf->output, which will call $field->output on each field for you, but you can use this yourself if you just want to print one form control anywhere you like, not as part of a form.

Groups

Groups (which will probably get renamed to conditionalshow, but I don't want to change all the examples just now) allow you to show or hide a collection of fields, depending on the setting of another field.

groups have basically the same methods as fields, but $name is optional. $name has no function, unless you need to identify the group later for a call the $mf->get($groupname);

There are a few extra methods they have over fields:

$field = &$group->add($fieldtype, $fieldname, $fieldoptions, $insertbefore);
$field = &$group->add_item($fieldobject, $insertbefore);
$field = &$group->remove($fieldname);
$field = &$group->get($fieldname);
 
$group->set_condition($conditionfield, $conditionvalue);

The elements within the group will only be visible when the field $conditionfield elsewhere in the form has value $conditionvalue. This works best when $conditionfield is a dropdown.

Do we also want a conditionalenable group that enables/disables a bunch of fields, rather than showing or hiding them?

Multiples

This lets you do things like enter multiple usernames, as in the 'working example' below. You would also use it on the editing form for multiple choice questions, to enter any number of answers with matching grades.

Again, they support the same methods as fields, and also:

$field = &$multiple->add($fieldtype, $fieldname, $fieldoptions, $insertbefore);
$field = &$multiple->add_item($fieldobject, $insertbefore);
$field = &$multiple->remove($fieldname);
$field = &$multiple->get($fieldname);
 
$multiple->set_max($number);

For accessibility reasons, multiples work by printing a certain number of copies into the HTML, and these are then shown or hidden by the JavaScript. This is for accessibility reasons, and so the forms work without JavaScript. max is the number of copies that are printed. It defaults to 10.

Field types initially supported

All field will support the options:

lable the field lable. This is a string that is looked up in the language file. Or, if the string starts with an '=' character, then this is trimmed off, and the rest of the string is used literally. Defaults to name.

help the name of the help file to link to. Defaults to name. Setting to removes the help icon.

helpalt the langstring to use for the tooltip of the help icon.

optional if set to 'yes', then adds a checkbox before the field to enable or diable it. The checkbox's name is name is constructed by taking the field name and appending 'enable'. If you want another name, set this option to the checkbox name, instead of 'yes'.

needcapability this is mainly aimed at 1.7 roles and permissions. Will hide this field unless the user has the named capability. At first, this will only recognise the values admin, teacher, student, noneditingteacher, guest, and translate these into the obvious isadmin() type calls.

More options will probably get added in future.

display

Just diplay a value with a label, the value can't be edited.

text

Text input box.

Supports the additional options:

required a regexp that is used for client-side validation against that regexp.

size as in <input size="" ... />

file

File upload box.

date

Date, like quiz open and close dates.

html

The standard HTML editor, or just a text area, depending on the settings. (This calls print_textarea).

dropdown

A dropdown menu. This field has the additional methods:

$dropdown->add_option($value, $label);
$dropdown->remove_option($value);

$label is optional. By default: if $value is an integer, use that integer as the label, otherwise look $value up in the langfile.

Dropdown supports the additional option:

default which option to select by default.

radio

A set of linked radio buttons, which are defined in the same way as dropdown menu options.

multiselect

A list box where you can select multiple options, which are defined in the same way as dropdown menu options.

Actually, I think we should change the implementation of this to use a set of checkboxes when there are fewer than about a dozen options, and automatically switch to useing a list box when there are more than that. And maybe add an optional parameter to force the listbox/checkbox decision one way or the other.

yesno

A dropdown menu with just the two options 'yes' and 'no'.

user

A flashy text box where you can enter user's names, and it does AJAX stuff to help you auto-complete them.

visible

This generates the standard "Visible to students" field that appears on add/update module forms.

groupmode

This generates the standard "Group mode" field that appears on add/update module forms.

XML form definition format

This provides the quickest way to create most forms. It should be clear how this translates into the PHP API calls defined above. XML elements $mf->add() calls. XML attributes correspond either to required information, or to set_opt calls. Form filds, like dropdowns, that need extra information get it from child elements. See the examples below for what it looks like.

A working example

Here is an example of a form produced with the current (un-cleaned-up) version of the OU's library. The form looks like this:

Formproposal Add newsfeed form.png

There is some OU-specific stuff here, like presentation and authids, and the fact that we reveal $user->username to teachers. I am assuming you can filter that out.

More interestingly, notice that

  • The required field Name has not been filled it, so its label is red, and the create button is disabled. Below, you will see that anything typed into this field will actually be validated against a regular expression.
  • The user field type does cool AJAX to save you typing the whole name.
  • After you have added one user as a poster, a second box appeared where we could type a second user name. And when we finish typing here, a third box will appear.

The XML defining the form looks like this:

<editform langfile='block_newsfeed'>
     <text name='location'/>
     <text name='name' required='\S'/>
     <text name='pres' required='^([0-9]{2}[a-zA-Z]?)?$'/>
     <html name='summary'/>
     <dropdown name='type'>
       <option value='internal'>type_internal</option>
       <option value='external'>type_external</option>
     </dropdown >
     <group requiredname="type" requiredvalue="external">
         <item type='text' name='url'/>
     </group>
     <group requiredname="type" requiredvalue="internal">
         <date name='startdate'/>
         <dropdown name='public'>
           <option value='1'>access_public</option>
           <option value='0'>access_private</option>
         </dropdown >
         <text name='defaultauthid'/>
         <multiple>
             <text name='optionalauthids' required='^([A-Z0-9]+)?$'/>
         </multiple>
         <multiple>
             <user name='posters'/>
         </multiple>
         <multiple>
             <user name='approvers'/>
         </multiple>
     </group>          
</editform>

The PHP code that creates the form definition ($xf), and the object with all the current values ($form) and sets all the default values looks like this:

$xf=xml_form::load_file('editfeed.xml');
$xf->set_init_html_editor(true);
$form=new stdClass;
$newsfeedid=optional_param('newsfeedid',0,PARAM_INT);
if($newsfeedid) {
    $nf=feed_system::$inst->get_feed($newsfeedid);
    $form->location=$nf->get_folder()->get_path();
    $xf->set_hidden('newsfeedid',$newsfeedid);
 
    $form->name=$nf->get_name();
    $form->pres=$nf->get_pres();
    if($form->pres==null) {
        $form->pres='';
    }
    $form->summary=$nf->get_summary();
    if(is_a($nf,'external_news_feed')) {
        $form->type='external';
        $form->url=$form->get_url();
    } else {
        $form->type='internal';
        $form->startdate=$nf->get_start_date();
        $form->public=$nf->is_public();
        $form->defaultauthid=$nf->get_default_authid();
        xml_form::set_multiple($form,'optionalauthids',$nf->get_optional_authids());
        xml_form::set_multiple($form,'posters',$nf->get_poster_usernames());
        xml_form::set_multiple($form,'approvers',$nf->get_approver_usernames());
        $form->newsfeedid=$newsfeedid;
    }
 
} else {
    $folderid=required_param('folderid',PARAM_INT);
    $lf=feed_system::$inst->get_location_folder($folderid,null);
    $nf=null;
    $xf->set_submit_caption(get_string('create'));
    $form->location=$lf->get_path();
    $xf->set_hidden('newsfeedid',0);
}
 
// ... print_header and other boring bits.
 
print_simple_box_start('center');
$xf->show(basename(__FILE__), $form);
print_simple_box_end();

Examples of converting existing Moodle forms

These are four example forms that Marting Dougiamas asked to see how we would handle.

Add resource -> Link to a file or web site

Formproposal Add link resource.png

I have taken the liberty of changing the way the window options work on this form. Instead of having a show/hide window settings button, and when that is turned on showing all the options for both same window and new window, I have changed it to be a same window/popup window dropdown, and depending on the setting there, showing or hiding the particular set of options. It would also be possible to use the library to generate the existing form.

This form contains some very specific bits, namely the location field and the parameters sections. For now I have kept the existing code to generate these bits. If fields like the location field were used in other places, we could add a url field type. I don't see any future in generalising the parameters bit, though the code to generate it could be cleaned up. I just copied and pasted the existing code, and made the minimal changes.

I've used a multiselect for the window options. That will require a small change to the response processing code.

$mf = moodle_form::loadstring('
<editform langfile="resource">
    <text name="name" help=""/>
    <htmlarea name="summary"/>
    <xhtml label="location"><![CDATA[
        echo "<input type=\"text\" name=\"reference\" size=\"90\" value=\"$form->reference\" alt=\"reference\" /><br />";
        button_to_popup_window ("/files/index.php?id=$form->course&choose=form.reference", "coursefiles", $xmlform['strchooseafile'], 500, 750, $xmlform['strchooseafile']);
        echo "<input type=\"button\" name=\"searchbutton\" value=\"$xmlform['strsearch'] ...\" ".
             "onclick=\"return window.open('$CFG->resource_websearch', 'websearch', 'menubar=1,location=1,directories=1,toolbar=1,scrollbars,resizable,width=800,height=600');\" />\n";
        if ($CFG->resource_allowlocalfiles) {
            button_to_popup_window ("/mod/resource/type/file/localfile.php?choose=form.reference", 
            "localfiles", get_string('localfilechoose', 'resource'), 400, 600, 
            get_string('localfilechoose', 'resource'));
        }
    ]]></xhtml>
    <url name="location" showuploadbutton="1" showsearchbutton="1" help=""/>
    <dropdown name="windowpopup">
        <option value='0'>pagewindow</option>
        <option value='1'>newwindow</option>
    </dropdown>
    <group requiredname="newwindow" requiredvalue="0">
        <hidden name="hframepage" value="0"/>
        <multiselect name="framepage" help="">
            <option value="1">frameifpossible</option>
        </multiselect>
    </group>
    <group requiredname="newwindow" requiredvalue="1">
        <multiselect name="windowoptions">
        </multiselect>
        <integer name="resource_popupwidth" label="resource_popupwidth"/>
        <integer name="resource_popupheight" label="resource_popupheight"/>
    </group>
    <yesno name="parameters"/>
    <group requiredname="newwindow" requiredvalue="1">
    <xhtml><![CDATA[
<table align="center">

        <tr>
            <td align="center"><?php print_string("parameter", "resource") ?></td>
            <td align="center"><?php print_string("variablename", "resource") ?></td>
        </tr>

<?php

for ($i=0; $i < $xmlform['maxparameters']; $i++) {
    echo "<tr>\n";
    echo "<td valign=\"top\">\n";
    echo "<select name=\"parameter$i\">\n";
    echo "<option value=\"-\">-- ".get_string('chooseparameter', 'resource')." --</option>\n";
    foreach ($xmlform['parameters'] as $field=>$fieldarr) {
        if ($fieldarr['value'] === "optgroup") {
            echo "<optgroup label=\"{$fieldarr['langstr']}\">\n";
        } elseif ($fieldarr['value'] === "/optgroup") {
            echo "</optgroup>\n";
        } else {
            echo "<option value=\"$field\"";
            if ($xmlform['alltextfield'][$i]['parameter'] == $field) {
                echo " selected=\"selected\"";
            }
            echo ">{$fieldarr['langstr']}</option>\n";
        }
    }
    echo "</select>\n";
    echo "</td>\n";
    echo "<td valign=\"top\">\n";
    echo "<input type=\"text\" name=\"parse$i\" value=\"{$xmlform['alltextfield'][$i]['parse']}\" alt=\"parameter$i\"/>\n";
    echo "</td>\n";
    echo "</tr>\n";
}

?>
</table>
    ]]></xhtml>
    </group>
')
$mf->set_xhtml_param('strchooseafile', get_string(...));
$mf->set_xhtml_param('strsearch', get_string(...));

$mf->set_xhtml_param('maxparameters', $this->maxparameters);
$mf->set_xhtml_param('parameters', $this->parameters);
$mf->set_xhtml_param('alltextfield', $alltextfield); // Assuming that this code is after the end of setup() in mod\resource\type\file\resource.class.php

$winopt = $mf->get('windowoptions');
foreach ($RESOURCE_WINDOW_OPTIONS as $optionname) {
    $defaultvalue = "resource_popup$optionname";
    $form->$optionname = $CFG->$defaultvalue;
    if ($optionname != 'height' && $optionname != 'width') {
        $winopt->add_option($optionname, "str$optionname");
    }
}

Add quiz

Formproposal Add quiz form.png

To convert this form, we need to do something special for the "students may review" bit. You could either invent an new <optiongrid> type, which would be quite easy to implement. That is the option used below. Alternatively, you could use the feature for including arbitrary HTML and PHP in the form, and keep the existing code for generating this part of the form. Since the existing layout breaks when you make the browser window narrow, as in the screenshot, I would be inclined to redo it.

$mf = moodle_form::loadstring('
<editform langfile="quiz" help="">
    <text name="name"/>
    <htmlarea name="introduction"/>
    <date name="available" label="quizopen" optional="yes"/>
    <date name="due" label="quizclose" optional="yes"/>
    <real name="timelimit" lableafter="minutes" helpalt="quiztimer" optional="yes"/>
    <dropdown name="questionsperpage">
        <option value='0'>unlimited</option>
        <!-- other options will be added programmatically -->
    </dropdown>
    <yesno name="shufflequestions"/>
 
    <!-- ... skipping the next few boring fields ... -->
 
    <optiongrid name="reviewoptions">
        <col name="responses"/>
        <col name="scores"/>
        <col name="feedback"/>
        <col name="answers"/>
        <row name="immediately" lable="reviewimmediately"/>
        <row name="open" lable="reviewopen"/>
        <row name="closed" lable="reviewclosed"/>
    </optiongrid>
 
    <!-- ... skipping the next few boring fields ... -->
 
    <groupmode/>
    <visible/>
')
$questionsperpage =& $mf->get('questionsperpage');
for ($i = 1; $i <= 50; $i += 1) {
    $questionsperpage->add_option($i);
}

This does not take into account the settings on http://example.com/moodle/admin/module.php?module=quiz, which lets the admin move certain quiz options to be hidden behind an "advanced" option. To implement this you would need to add a show/hide advanced control (I would do this as a <yesno name="showadvanced"/>, and an empty advanced group, then use some PHP code like

$fix = 0;
$advgroup = $mf->get_field('advanced');
 
if ($CFG->quiz_fix_timelimit) {
    $item = &$mf->remove('timelimit');
    $advgroup->add($item)
    $fix = 1;
}
// ... and so on, for all the other options. Or you could try to be clever 
// and do this as a loop over an array of field names ... then
if (!$fix) {
    $form->remove('showadvanced');
    $form->remove('advanced');
}

Add database

Formproposal Add database form.png

I won't type out full code for this example most of it is simple, and it should be clear how to do it given the above examples, just comment on a couple of things:

Entries required before viewing I assume this is only indented because the label is so long that having it all lined up would break the table layout. I think our code using CSS for layout just word-wraps long labels and keeps everything aligned, which I think is better.

Allow posts to be rated? I would put the disabled controls in a group that only appears if the checkbox is checked, so that those controls show and hide, rather than enabling or disabling. However, if we want to keep this the same as it is now, you could implemnt the conditionalenable group type.

Manage groups for a course

This sort of form is currently beyond what the form library was designed to produce. I would leave this as hand-coded HTML. In time, the form library may gain some AJAX field types for selecting students, and such like, that could usefully be within a redesigned version of this form. Since the form field types just output HTML and Javascript, it should be possible to use them within hand-crafted forms.

Formproposal Groups form.png

Questions

Do we need a syntax like lable='langfile:langstring' for using lang strings from other lang files where necessary? yes

Where does this live. I think the main library file that people have to include should be lib/formlib.php, and that is all anyone needs to include. However, to we break each field type into its own PHP file, perhaps in a lib/formlib directory, to make it easier to add new field types in future? Probably all in one file

Some form fields will need bits of CSS and JavaScript to work. Do we add the CSS to the standard theme, and combine all the javascript into a single library somewhere, or do we break it up into the individual field types, and recombine it dynamically at runtime? I think I favour breaking up the PHP code, but keeping all the JS and CSS in one place. Should use YUI were possible. We will still need some custom JavaScript and CSS though. Where?

Do we use this for vaidation as well? Yes. Currently Moodle PHP files that do forms tend to look like:

$var1 = require/optional_param(...);
// etc. for all the other form variables.

if (data_submitted && confirm_sesskey()) {
   // Try to process submitted data.
   if (/*processing ok*/)
	   // Redirect away
} else {
   // Set initial form values.
}

// Lots of code to output the form HTML. Of maybe include(somthing.html);

The proposal is to change this to something like

$mf = new moodle_form();
// Setup $mf with the form structure

$form = new stdClass;
if ($mf->validate_submitted_data($form)) { // Pass by reference to get data back.
   // Try to process submitted data in $form
   if (/*processing ok*/)
	   // Redirect away
} else {
   // Set initial values in $form
}

$mf->display($form)