User:Mark Johnson/Mforms and AJAX

Jump to: navigation, search

With a little bit of trickery, it's possible to load, display and sumbit a Moodle Form (mform) defined using the Form API entirely via AJAX, without having to reload the page.

Please note that this page isn't a full step-by-step example, it just shows you an overview of the steps that are necessary. You'll need a working knowledge of AJAX and Moodle's Javascript API to make use of it.

The process works in two steps:

  1. Loading and displaying the form
  2. Submitting and processing the form, then updating the page

The second set isn't reliant on the first step, so it's possible to have page that displays a form in the ususal manner, then submits it via AJAX. The solution consists of a server-side script to output and process the form (both steps use this script), and a couple of javascript functions to display and submit the form.

Displaying the form

To start with, you'll need a PHP script that looks much like a normal script used to display an mform, with a few modifications. Note that this is a seperate script to the one for the page on which you're displaying the form.

require_once('config.php');
require_once('form_class.php');
 
...
// Check permissions etc
...
// Get record for editing
$record = $DB->get_record('table', $params);
...
// Initialise the form class
$form = new form_class();
 
if ($record) {
    $form->set_data($record);
}
 
if ($data = $form->get_data()) {
  // Process the submission and save data to the database
}

The end of the script will look a bit different to normal. Rather than displaying a page containing the form, we just want to capture the HTML for the form

ob_start();
$form->display();
$formhtml = ob_get_clean();

Now there's a slightly trickier bit. The form API has built-in javascript validation, an in addition we might have our own javascript which needs to be initialised after the form is displayed. For this, we need to generate the javascript using the Output API and rip out the bits we need:

// First we get the script generated by the Form API
if (strpos($formhtml, '</script>') !== false) {
    $outputparts = explode('</script>', $formhtml);
    $html = $outputparts[1];
    $script = str_replace('<script type="text/javascript">', '', $outputparts[0]);
} else {
    $html = $formhtml;
}
// Next we get the M.yui.loader call which includes the Javascript libraries
$headcode = $PAGE->requires->get_head_code($PAGE, $OUTPUT);
$loadpos = strpos($headcode, 'M.yui.loader');
$cfgpos = strpos($headcode, 'M.cfg');
$script .= substr($headcode, $loadpos, $cfgpos-$loadpos);
// And finally the initalisation calls for those libraries
$endcode = $PAGE->requires->get_end_code();
$script .= preg_replace('/<\/?(script|link)[^>]*>/', '', $endcode);

We've now got the HTML of the form, and the Javascript that goes with it. We just need to send it back to the client.

echo json_encode(array('html' => $html, 'script' => $script));

That's the server side done for now. Let's look at the code for displaying and posting the form (using YUI3). Your Javascript module will require the following YUI modules: base, node, io, json

In out Javascript module, we'll have a function that looks something like this. It can be triggered on load, off a button press, or whatever:

display_form: function() {
    Y = this.Y;
    Y.io(M.cfg.wwwroot+'/form_ajax.php', {
       on : {
           success: function(id, o) {
               response = Y.JSON.parse(o.responseText)
               form = Y.Node.create(response.html); // Create a Node from the HTML
               Y.one('#form-container').setContent(form) // Display the form on the page before we do anything with javascript, since the javascript will expect the form elements to already be on the page
 
               scriptel = document.createElement('script'); // Create a <script> tag
               scriptel.textContent = response.script; // Put the Javascript inside the script tag
               document.body.appendChild(scriptel); // Add the script tag to the page.
           }
       } 
    });
}

Node that when you do document.body.appendChild(scriptel), the javascript will be executed immediately, meaning any libraries will be included, defined functions will be come available, and init functions will be run.

Submitting the Form

To submit the form, we'll serialise the form data using YUI, and send it via a second AJAX request. For this, your Javascript will need the additional YUI module io-form

You'll need to create the following Javascript function, and attach it to run when the form's submit button is clicked:

submit_form: function(e) {
    var Y = this.Y;
 
    // Stop the form submitting normally
    e.preventDefault();
 
    // Form serialisation works best if we get the form using getElementById, for some reason
    var form = document.getElementById('form_id');
    // If your form has a cancel button, you need to disable it, otherwise it'll be sent with the request
    // and Moodle will think your form was cancelled
    Y.one('#id_cancel').set('disabled', 'disabled');
 
    // Send the request
    Y.io(M.cfg.wwwroot+'/form_ajax.php', {
        method: post,
        on: {
            success: function(id, o) {
                response = Y.JSON.parse(o.responseText);
                // Display some feedback to the user
            }
        },
        form: form,
        context: this
    });
 
}

Finally, we need to a revisit the PHP script to provide some feedback for the user. Instead of redirecting to a confirmation page as you would normally do, you could return a message to display to the user. If the set of records which you've just added to is displayed on the page, my preferred approach is to generate the HTML for those records again and send it back, so the page can be updated on-the-fly:

...
if ($data = $form->get_data()) {
    // Process and save data
 
    $data = my_plugin_get_data(..., ...);
    $renderer = $PAGE->get_renderer('my_plugin');
    $html = $renderer->record_list($data);
    echo json_encode(array('output' => $html));
    exit();
}
...