Javascript

Moodle makes heavy use of Javascript to improve the experience for its users.

All new Javascript in Moodle should be written in the ES2015+ module format, which is transpiled into the CommonJS format. Modules are loaded in the browser using the RequireJS loader.

All Moodle Javascript can use the same Mustache templates and translated strings which are available to Moodle PHP code, and the standard Moodle web service framework can be used to fetch and store data.

This guide covers how to get started with Javascript in Moodle, and introduces key concepts and features including module format and structure, including your code, using templates, using translation features, tooling, and handling events.

Note

You may see the terms ES6 and ES2015 used interchangably. ES2015 is the 6th generation of the Ecma Script specification. ES2015 respresents a big change from previous versions of the Ecma Script specification.

Useful References

Moodle uses vanilla Javascript combined with a number of helpers for performing common actions, and a small collection of libraries for serving and managing dependencies.

The Javascript documentation available on the Mozilla Developer Network is one of the best reference documentations available. You may find the following references particularly useful:

Modules

Javascript in Moodle is structured into ES2015 modules which are transpiled into the CommonJS format.

Like our PHP classes and Mustache templates, our Javascript modules each belong to a particular component and must be named according to our standard name and namespace conventions.

The naming scheme for Moodle’s Javascript fits into the pattern:

[component_name]/[optional/sub/namespace/][modulename]

The first directory in any subfolder must be either a Moodle API, or local.

The following are examples of valid module names:

// For a module named `discussion` in the `mod_forum` component:
mod_forum/discussion

// For a module named `grader` in the `mod_assign` component which is
// part of the `grades` API:
mod_assign/grades/grader

// For a module named `confirmation` in the `block_newsitems` component
// which is a modal and not part of a core API:
block_newsitems/local/modal/confirmation

// For a module name `selectors` in the `core_user` component and relates
// to the `participants` module:
core_user/local/participants/selectors

Tip

When structuring a new module you may find it clearer to create a main module with a number of related modules. You can create a clear relationship between your modules using subdirectories.

For example when creating a new module which controls interactions on the Participants page and which is part of the core_user component you will create a participants module. The full namespace for this module will be core_user/participants.

The core_user/participants module may interact with DOM elements which are identified by CSS Selectors. The Moodle convention is to place the selectors in a selectors module.

The module will also call a set of Web Services. The Moodle convention is to place calls to Web Services in a repository module.

Since participants is not a formal API in Moodle you must create your submodules in the local/participants directory.

.
├── local
│   └── participants
│       ├── repository.js       // core_user/local/participants/selectors
│       └── selectors.js        // core_user/local/participants/repository
└── participants.js             // core_user/participants

Writing your first module

The convention in Moodle is to have one Javascript Module which is your initial entrypoint. This usually provides a function called init which you then export from the module. This init function will be called by Moodle.

Your module will probably also have one or more dependencies which you will import.

As you start to build out the structure of your code you will start to export more functions, as well as Objects, Classes, and other data structures.

Note

This guide is not intended to teach you how to write Javascript. If you are new to Javascript, you may want to start with the MDN Javascript basics guide.

A module which calls to the browser console.log function would look like:

1 // mod/example/lib/amd/src/helloworld.js
2 export const init = () => {
3     window.console.log('Hello, world!');
4 };

In this example a new variable called init is created and exported using the ES2015 export keyword. The variable is assigned an arrow function expression which takes no arguments, and when executed will call the browser console.log function with the text "Hello, world!".

Listen to a DOM Event

In most cases you will want to perform an action in response to a user interacting with the page.

You can use the document.addEventListener() method to do this.

To add a click listener to the entire body you would write:

1 // mod/example/lib/amd/src/helloworld.js
2 export const init = () => {
3     document.addEventListener('click', e => {
4         window.console.log(e.target);
5     });
6 };

In this example any time that a user clicks anywhere on the document the item that was clicked on will be logged to the browser console.

Usually you won’t want to listen for every click in the document but only for some of the Elements in the page.

If you wanted to display a browser alert every time a user clicks on a buttn, you might have a template like the following example:

1 {{! mod/example/templates/helloworld.mustache }}
2 <button data-action="mod_example/helloworld-update_button">Click me</button>

You can write a listener which only looks for clicks to this button:

 1 // mod/example/lib/amd/src/helloworld.js
 2 const Selectors = {
 3     actions: {
 4         showAlertButton: '[data-action="mod_example/helloworld-update_button"]',
 5     },
 6 };
 7
 8 export const init = () => {
 9     document.addEventListener('click', e => {
10         if (e.target.closest(Selectors.actions.showAlertButton)) {
11             window.alert("Thank you for clicking on the button");
12         }
13     });
14 };

This example shows several conventions that are used in Moodle:

  • CSS Selectors are often stored separately to the code in a Selectors object. This allows you to easily re-use a Selector and to group them together in different ways. It also places all selectors in one place so that you can update them more easily.

  • The Selectors object is stored in a const variable which is _not_ exported. This means that it is private and only available within your module.

  • A data-* attribute is used to identify the button in the Javascript module. Moodle advises not to use class selectors when attaching event listeners because so that it is easier to restyle for different themes without any changes to the Javascript later.

  • A namespace is used for the data-action to clearly identify what the button is intended for.

  • By using e.target.closest() you can check whether the element that was clicked on, or any of its parent elements matches the supplied CSS Selector.

Instead of having one event listener for every button in your page, you can have one event listener which checks which button was pressed. If you have a template like the following:

1 {{! mod/example/templates/helloworld.mustache }}
2 <div>
3     <button data-action="mod_example/helloworld-update_button">Click me</button>
4     <button data-action="mod_example/helloworld-big_red_button">Do not click me</button>
5 </div>

Then you can write one event listener which looks at all buttons on the page. For example:

1 // mod/example/lib/amd/src/local/helloworld/selectors.js
2 export default {
3     actions: {
4         showAlertButton: '[data-action="mod_example/helloworld-update_button"],
5         bigRedButton: '[data-action="mod_example/helloworld-big_red_button"],
6     },
7 };
 1 // mod/example/lib/amd/src/helloworld.js
 2 import Selectors from './local/helloworld/selectors';
 3
 4 const registerEventListeners = () => {
 5     document.addEventListener('click', e => {
 6         if (e.target.closest(Selectors.actions.showAlertButton)) {
 7             window.alert("Thank you for clicking on the button");
 8         }
 9
10         if (e.target.closest(Selectors.actions.bigRedButton)) {
11             window.alert("You shouldn't have clicked on that one!");
12         }
13     });
14 };
15
16 export const init = () => {
17     registerEventListeners();
18 };

You will notice a number of key differences in this example when compared with the previous one:

  • The list of Selectors has been moved to a new Module which is included using the import keyword. The new selectors module is a dependency of the helloworld module.

  • The call to document.addEventListener has been moved to a new registerEventListeners function. This is another Moodle convention which encourages you to structure your code so that each part has clear responsibilites.

  • There is only one event listener and it checks if the Element clicked on was one that it is interested in.

Including Javascript from your pages

Once you have written a Javascript module you need a way to include it within your content.

There are three main ways to include your Javascript and the best way will depend on your content. These are:

  • from a template via requirejs;

  • from PHP via the output requirements API; and

  • from other Javascript via import or requirejs.

Including from a template

Most recent code in Moodle makes heavy use of Mustache templates and you will usually find that your Javascript is directly linked to the content of one of your templates.

All javascript in Mustache templates must be places in a {{#js}} tag. This tag ensures that all Javascript is called in a consistent and reliable way.

Caution

You should not add too much Javascript directly to a template. Javascript placed directly into Templates is not transpiled for consistent use in all browsers and it is not passed through minification processes. Some browser-specific features will not be available.

This simplest form of this is:

 1 {{! mod/forum/templates/discussion.mustache }}
 2 <div>
 3     <!—- Your template content goes here. —->
 4 </div>
 5
 6 {{#js}}
 7 require(['mod_forum/discussion'], function(Discussion) {
 8     Discussion.init();
 9 });
10 {{/js}}

Any time that this template is rendered and placed on the page the mod_forum/discussion module will be fetched and the init() function called on it.

Important

Do not use arrow functions directly in templates. Internet Explorer does not support arrow functions in any version.

Often you may want to link the Javascript to a specific DOMElement in the template. You can easily use the {{uniqid}} Mustache tag to give that DOM Element a unique ID and then pass that into the Module.

1<div id=“mod_forum-discussion-wrapper-{{uniqid}}”>
2    <!—- Your template content goes here. —->
3</div>
4
5{{#js}}
6require([‘mod_forum/discussion’], function(Discussion) {
7    Discussion.init(document.querySelector(“mod_forum-discussion-wrapper-{{uniqid}}”));
8});
9{{/js}}

In this example you have added a new id to the div element. You then fetch the DOM Element using this id and pass it into the init function.

Note

The {{uniqid}} tag gives a new unique string for each rendered template including all of its children. It is not a true unique id and must be combined with other information in the template to make it unique.

Including from PHP

Much of Moodle’s code still creates HTML content in PHP directly. This might be a simple echo statement or using the html_writer output functions. A lot of this content is being migrated to use Mustache Templates which are the recommended approach for new content.

Where content is generated in PHP you will need to include your Javascript at the same time.

There are several older ways to include Javascript from PHP, but for all new Javascript we recommend only using the js_call_amd function on the page_requirements_manager. This has a very similar format to the version used in Templates:

1// Call the `init` function on `mod_forum/discussion`.
2$PAGE->requires->js_call_amd('mod_forum/discussion', 'init');

The js_call_amd function turns this into a requirejs call.

You can also pass arguments to your function by passing an array as the third argument to js_call_amd, for example:

1// Call the `init` function on `mod_forum/discussion`.
2$PAGE->requires->js_call_amd(‘mod_forum/discussion’, ‘init’, [$course->id]);

If you pass a multi-dimensional array as the third argument, then you can use Array destructuring within the Javascript to get named values.

1$PAGE->requires->js_call_amd(‘mod_forum/discussion’, ‘init’, [[
2    ‘courseid’ => $course->id,
3    ‘categoryid’ => $course->category,
4]]);
1export const init = ({courseid, category}) => {
2    window.console.log(courseid);
3    window.console.log(category);
4};

Caution

There is a limit to the length of the parameters passed in the third argument. You should only pass information required by the Javascript which is not alrady available in the DOM.

Passing data to your Module

You will often need to work with data as part of your Javascript module. This might be simple data, like the a database id, or it may be more complex like full Objects.

In most cases you will be able to store this data in the DOM as a data attribute which you can fetch with your Javascript. In some cases you will need to use Moodle Web Services to fetch this data instead. A small amount of data can be passed into the initialisation of your Javascript module, but this is no longer recommended.

Using data attributes

The easiest way to pass data is to use data attributes.

Todo

Document the main ways that we pass data.

Focus on:

  • data- attributes in HTML being ready

  • the limitations of the data passed into js_call_amd

  • web services

Promises

Todo

We should document things like:

  • Use then and catch consistently (thennables)

  • Don’t use catch if you are returning a Promise just by habit - only use it if you mean to

  • You _must_ return at the end of a thennable

  • It’s generally a good idea to return a Promise from a fucntion if the function is primarily tasked with creating that Promise

Important

You should not use the done, fail, or always functions on Promises. These are a jQuery feature which is not present in the Native Promise implementation.

Examples

 1const getModal = questionBody => {
 2    return ModalFactory.create({
 3        title: getString('mytitle', 'mod_example'),
 4        body: renderTemplate('mod_example/example_body', questionBody),
 5        removeOnClose: true,
 6    })
 7    .then(modal => {
 8        modal.show();
 9
10        return modal;
11    });
12};

Working with Strings

One of the most helpful core modules is core/str which allows you to easily fetch and render language Strings in Javascript.

The core/str module has two main functions, which both return Promises containing the resolved string.

Strings are fetched on request from Moodle, and are then cached in LocalStorage.

Example

1import {get_string as getString} from 'core/str';
2
3getString('close', 'core')
4.then(closeString => {
5    window.console.log(closeString);
6
7    return closeString;
8})
9.catch();

Templates

Modals

Notifications

AJAX Calls

Preferences

Prefetch

Tools

We make use of a number of common and popular tools to ensure the quality of our code, and to improve the end-user experience.

Most of our Javascript tooling requires NodeJS.

Grunt

Grunt is a command-line tool used to compile Javascript, and CSS, and to validate and lint Javascript, CSS, Behat tests, and more.

Tip

Rather than running grunt on the entire Moodle source every time you make changes, you can use grunt watch in the background to build just the files you change as you write them.

Installing grunt

$ npm -g install grunt-cli

Using grunt

grunt

ESLint

Glossary

Arrow functions

An arrow function is a shorthand way of writing a regular function. They have a number of small but important differences to regular functions which make them easier to use in most cases, but unsuitable in some others.

They are not suitable for use in code which is not transpiled as Internet Explorer does not offer any support for them.

For more information see the MDN documentation for Arrow function expressions.