MForm Modal: Difference between revisions
Damyon Wiese (talk | contribs) |
Damyon Wiese (talk | contribs) |
||
Line 70: | Line 70: | ||
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 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 | 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. | ||
<code javascript> | <code javascript> | ||
/** | // This file is part of Moodle - http://moodle.org/ | ||
* Add a create new group modal to the page. | // | ||
* | // Moodle is free software: you can redistribute it and/or modify | ||
* @module core_group/newgroup | // it under the terms of the GNU General Public License as published by | ||
* @class NewGroup | // the Free Software Foundation, either version 3 of the License, or | ||
* @package core_group | // (at your option) any later version. | ||
* @copyright 2017 Damyon Wiese <damyon@moodle.com> | // | ||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | // Moodle is distributed in the hope that it will be useful, | ||
*/ | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
define(['jquery', 'core/ | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | |||
/** | // | ||
* @method | // You should have received a copy of the GNU General Public License | ||
* @private | // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | |||
/** | |||
// | * Add a create new group modal to the page. | ||
}; | * | ||
* @module core_group/newgroup | |||
return /** @alias module:core_group/newgroup */ { | * @class NewGroup | ||
// Public variables and functions. | * @package core_group | ||
/** | * @copyright 2017 Damyon Wiese <damyon@moodle.com> | ||
* Attach event listeners to initialise this module. | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | ||
* | */ | ||
* @method init | define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/fragment'], | ||
* @param {string} selector The CSS selector used to find nodes that will trigger this module. | function($, Str, ModalFactory, ModalEvents, Fragment) { | ||
*/ | |||
init: function(selector) { | /** | ||
* 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); | |||
} | |||
}; | |||
}); | }); | ||
</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. | |||
=== Step 3 - How to we open the modal? === | |||
=== Related APIs === | === Related APIs === |
Revision as of 04:55, 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]');
});
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.
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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.