Note:

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

Advanced tutorial: MForm in a modal

Note: This tutorial is for advanced Moodle devs. You need to know a lot of APIs in order to understand all of this. See the list of related pages at the end of this tutorial for some of them.


This is a walkthrough of the steps required to convert an existing page in moodle with an mform - to an mform that displays in an mform and submits data via AJAX.

Lets get started - first lets pick a page (spins a wheel)..... "Create a group".

We will modify the "Create group" button on the groups page to open a modal with the create a group form, instead of directing you to a new page. Whether this is actually a useful improvement or not does not really matter for now - we are just trying to show how to do it.

Step 1 - Attach some javascript to the button

The first step is to add some javascript to the "Create group" button so that we can open a modal. The page we are modifying is group/index.php which we get by visiting the page and checking the url. If we read through that page, we can see it uses a renderable to generate the HTML for the page.

$renderable = new \core_group\output\index_page($courseid, $groupoptions, $selectedname, $members, $disableaddedit, $disabledelete,

             $preventgroupremoval);                                                                                                      

$output = $PAGE->get_renderer('core_group'); echo $output->render($renderable);

The renderer is in group/classes/output/renderer.php and the renderable is in group/classes/output/index_page.php (see Automatic_class_loading) for how we worked that out.

Looking at the render_index_page function of the renderer (see https://docs.moodle.org/dev/Output_renderers) we can see that this renderer uses Templates to separate the HTML from the php (good).

public function render_index_page(index_page $page) {

   $data = $page->export_for_template($this);                                                                                  
   return parent::render_from_template('core_group/index', $data);                                                             

}

The name of the template is "core_group/index". This maps to a mustache template located at group/templates/index.mustache.

Hooray - we found the HTML we want to modify to add our javascript.

This template already has a {{#js}} block we can use to add our custom javascript. We don't want to code all the javascript in a mustache template - we will just add the minimal code to call a new AMD module for creating a group.

{{#js}}

   require(['jquery', 'core/yui'], function($) {                                                                                   
       $("#groups").change(function() {                                                                                            
           M.core_group.membersCombo.refreshMembers();                                                                             
       });                                                                                                                         
       M.core_group.init_index(Y, "Template:wwwroot", Template:courseid);                                                                    
       var undeletableGroups = JSON.parse('{{{undeletablegroups}}}');                                                              
       M.core_group.groupslist(Y, undeletableGroups);                                                                              
   });                                                                                                                             
   // New code to create groups in a modal.                                                                                        
   require(['core_group/newgroup'], function(NewGroup) {                                                                           
       NewGroup.init('[data-action=creategroupmodal]', Template:contextid);                                                                            
   });

Template:/js


We added that code at the end to call the core_group/newgroup AMD module. The argument we will pass to this new module is a "selector" which is a flexible way to allow this same module to be used on other pages with different markup. In this case for the selector, we are looking for a node with a data attribute. We also need to modify the create group button to contain this data-attribute. In general data-attributes are a great way to add hooks to the DOM that are used for Javascript, but should not affect the appearance (unlike classes which are just gross and disgusting).

    <input type="submit" name="act_showcreateorphangroupform" id="showcreateorphangroupform" data-action="creategroupmodal" value="{{#str}}creategroup, group{{/str}}" class="btn btn-default" />

Step 2 - Add a new AMD module to show the modal

So we tried to load an AMD module "core_group/newgroup" but it doesn't exist yet. Time to create it.

Make a new folder at group/amd/src. "group" in this case is the directory for the core_group component, so amd modules for this component go in amd/src relative to that directory. When we run Grunt it will build the javascript to group/amd/build.

Make a new file in this group/amd/src folder named "newgroup.js". This will be our new javascript module. It exports a single function "init" which attaches event listeners to the elements that match the selector.

/**

* Add a create new group modal to the page.
*
* @module     core_group/newgroup
* @class      NewGroup
* @package    core_group
* @copyright  2017 Damyon Wiese <damyon@moodle.com>
* @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/fragment'],

       function($, Str, ModalFactory, ModalEvents, Fragment) {
   /**
    * Constructor
    *
    * @param {String} selector used to find triggers for the new group modal.
    * @param {int} contextid
    *
    * Each call to init gets it's own instance of this class.
    */
   var NewGroup = function(selector, contextid) {
       this.contextid = contextid;
       this.init(selector);
   };
   /**
    * @var {Modal} modal
    * @private
    */
   NewGroup.prototype.modal = null;
   /**
    * @var {int} contextid
    * @private
    */
   NewGroup.prototype.contextid = -1;
   /**
    * Initialise the class.
    *
    * @param {String} selector used to find triggers for the new group modal.
    * @private
    * @return {Promise}
    */
   NewGroup.prototype.init = function(selector) {
       var triggers = $(selector);
       // Fetch the title string.
       return Str.get_string('creategroup', 'core_group').then(function(title) {
           // Create the modal.
           return ModalFactory.create({
               type: ModalFactory.types.SAVE_CANCEL,
               title: title,
               body: this.getBody()
           }, triggers);
       }.bind(this)).then(function(modal) {
           // Keep a reference to the modal.
           this.modal = modal;
           // Forms are big, we want a big modal.
           this.modal.setLarge();
           // We want to reset the form every time it is opened.
           this.modal.getRoot().on(ModalEvents.hidden, function() {
               this.modal.setBody(this.getBody());
           }.bind(this));
           // We want to hide the submit buttons every time it is opened.
           this.modal.getRoot().on(ModalEvents.shown, function() {
               this.modal.getRoot().find('[data-fieldtype=submit]').hide();
           }.bind(this));
           // We catch the modal save event, and use it to submit the form inside the modal.
           // Triggering a form submission will give JS validation scripts a chance to check for errors.
           this.modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this));
           // We also catch the form submit event and use it to submit the form with ajax.
           this.modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this));
           return this.modal;
       }.bind(this));
   };
   /**
    * @method getBody
    * @private
    * @return {Promise}
    */
   NewGroup.prototype.getBody = function() {
       // Get the content of the modal.
       return Fragment.loadFragment('core_group', 'new_group_form', this.contextid, {});
   };
   /**
    * Private method
    *
    * @method submitFormAjax
    * @private
    * @param {Event} e Form submission event.
    */
   NewGroup.prototype.submitFormAjax = function(e) {
       // We don't want to do a real form submission.
       e.preventDefault();
       // Convert all the form elements values to a serialised string.
       var formData = this.modal.getRoot().find('form').serialize();
       // More to come...
   };
   /**
    * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed.
    *
    * @method submitForm
    * @param {Event} e Form submission event.
    * @private
    */
   NewGroup.prototype.submitForm = function(e) {
       e.preventDefault();
       this.modal.getRoot().find('form').submit();
   };
   return /** @alias module:core_group/newgroup */ {
       // Public variables and functions.
       /**
        * Attach event listeners to initialise this module.
        *
        * @method init
        * @param {string} selector The CSS selector used to find nodes that will trigger this module.
        * @param {int} contextid The contextid for the course.
        * @return {Promise}
        */
       init: function(selector, contextid) {
           return new NewGroup(selector, contextid);
       }
   };

});

Some things to note: This is the standard boilerplate for an AMD module that only returns a list of functions (in this case one function named "init"). Everything else is private to the module.

Step 3 - How to we open the modal?

Related APIs