Note:

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

MForm Modal: Difference between revisions

From MoodleDocs
Line 215: Line 215:
</code>
</code>


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.
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. There is a bunch of code here but the main parts are:
 
a) opening the modal
b) using the fragment api to load the body of the modal
c) catching the submit from the form so we can do something else with it


=== Step 3 - How to we open the modal? ===
=== Step 3 - How to we open the modal? ===

Revision as of 05:07, 14 July 2017

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 contextid to the template (We always need a contextid to use the fragments API) but it's not in the context for the template yet. We need to add it to the export_for_template function for the renderable group/classes/output/index_page.php

$context = context_course::instance($this->courseid);
$data->contextid = $context->id;

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. There is a bunch of code here but the main parts are:

a) opening the modal b) using the fragment api to load the body of the modal c) catching the submit from the form so we can do something else with it

Step 3 - How to we open the modal?

Related APIs