Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

User:Mark Johnson/Mforms and AJAX: Difference between revisions

From MoodleDocs
No edit summary
m (Text replacement - "<code (.*)>" to "<syntaxhighlight lang="$1">")
 
(4 intermediate revisions by 2 users not shown)
Line 1: Line 1:
With a little bit of trickey, it's possible to load, display and sumbit a Moodle Form (mform) defined using the [Forms API] entirely via AJAX, without having to reload the page.
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.


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:
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.
<code php>
 
The process works in two steps:
# Loading and displaying the form
# 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.
<syntaxhighlight lang="php">
require_once('config.php');
require_once('config.php');
require_once('form_class.php');
require_once('form_class.php');
Line 22: Line 31:
   // Process the submission and save data to the database
   // Process the submission and save data to the database
}
}
</code>
</syntaxhighlight>


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
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
<code php>
<syntaxhighlight lang="php">
ob_start();
ob_start();
$form->display();
$form->display();
$formhtml = ob_get_clean();
$formhtml = ob_get_clean();
</code>
</syntaxhighlight>
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:
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:
<code php>
<syntaxhighlight lang="php">
// First we get the script generated by the Forms API
// First we get the script generated by the Form API
if (strpos($formhtml, '</script>') !== false) {
if (strpos($formhtml, '</script>') !== false) {
     $outputparts = explode('</script>', $formhtml);
     $outputparts = explode('</script>', $formhtml);
Line 40: Line 49:
     $html = $formhtml;
     $html = $formhtml;
}
}
// Next we get the Javascript libraries included by YUI
// Next we get the M.yui.loader call which includes the Javascript libraries
$headcode = $PAGE->requires->get_head_code($PAGE, $OUTPUT);
$headcode = $PAGE->requires->get_head_code($PAGE, $OUTPUT);
$loadpos = strpos($headcode, 'M.yui.loader');
$loadpos = strpos($headcode, 'M.yui.loader');
Line 48: Line 57:
$endcode = $PAGE->requires->get_end_code();
$endcode = $PAGE->requires->get_end_code();
$script .= preg_replace('/<\/?(script|link)[^>]*>/', '', $endcode);
$script .= preg_replace('/<\/?(script|link)[^>]*>/', '', $endcode);
</code>
</syntaxhighlight>
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.
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.
<code php>
<syntaxhighlight lang="php">
echo json_encode(array('html' => $html, 'script' => $script));
echo json_encode(array('html' => $html, 'script' => $script));
</code>
</syntaxhighlight>


That's the server side done for now. Let's look at the code for displaying and posting the form (using YUI3)
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:
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:
<code javascript>
<syntaxhighlight lang="javascript">
display_form: function() {
display_form: function() {
     Y = this.Y;
     Y = this.Y;
Line 74: Line 83:
     });
     });
}
}
</code>
</syntaxhighlight>
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.
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:
<syntaxhighlight lang="javascript">
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
    });
}</syntaxhighlight>
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:
<syntaxhighlight lang="php">
...
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();
}
...
</syntaxhighlight>

Latest revision as of 08:24, 15 July 2021

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();
}
...