Note:

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

Javascript Modules: Difference between revisions

From MoodleDocs
(15 intermediate revisions by 7 users not shown)
Line 25: Line 25:
== Install grunt ==
== Install grunt ==


The AMD modules in Moodle must be processed by some build tools before they will be visible to your web browser. We use "[http://gruntjs.com/ grunt]" as a build tool to wrap our different processes. Grunt is a build tool written in Javascript that runs in the "[http://nodejs.org/ nodejs]" environment.
The AMD modules in Moodle must be processed by some build tools before they will be visible to your web browser. We use "[[grunt]]" as a build tool to wrap our different processes. Grunt is a build tool written in Javascript that runs in the "[http://nodejs.org/ nodejs]" environment.


This means you first have to '''install nodejs''' - and its package manager [https://www.npmjs.com/ npm]. The details of how to install those packages will vary by operating system, but on Linux it's probably similar to "sudo apt-get install nodejs npm". There are downloadable packages for other operating systems here: http://nodejs.org/download/. Moodle currently requires node "v4" and does not work with the latest node "v6".
This means you first have to '''install nodejs''' - and its package manager [https://www.npmjs.com/ npm]. The details of how to install those packages will vary by operating system, but on Linux it's probably similar to "sudo apt-get install nodejs npm". There are downloadable packages for other operating systems here: http://nodejs.org/download/. Moodle currently requires node {{NodeJSVersion}}.


Once this is done, you can '''run the command''':
Once this is done, you can '''run the command''':
Line 47: Line 47:
Moodle will now run your module from the amd/src module. Don't forget to switch this off and run 'grunt' before deploying the new version!
Moodle will now run your module from the amd/src module. Don't forget to switch this off and run 'grunt' before deploying the new version!


In this mode - if you get a strange message in your javascript console like "No define call for core/first" it means you have a syntax error in the javascript you are developing.
In this mode - if you get a strange message in your javascript console like "No define call for core/first" it means you have a syntax error in the javascript you are developing.  
Or, "No define call for theme_XXX/loader" as you are probably missing the 'src' folder with relevant JS files. which might happen when you turn debugging ON on a theme that was bought, without 'src' folder :-(


== Running grunt ==
== Running grunt ==
Line 64: Line 65:


  sudo ln -fs /usr/bin/nodejs /usr/local/bin/node
  sudo ln -fs /usr/bin/nodejs /usr/local/bin/node
Note: Once you have run grunt and built your code, you will then need to purge Moodle caches otherwise the changes made to your minified files may not be picked up by Moodle.


== Minimum (getting started) module for plugins ==
== Minimum (getting started) module for plugins ==
Line 86: Line 89:
     };
     };
});
});
</code>
This code passes the jquery module into our function (parameter $). There are a number of other useful modules available in Moodle, some of which you'll probably need in a practical application. See [[Useful_core_Javascript_modules]]. Simply list them in both the define() first parameter and the function callback. E.g.,
<code javascript>
    define(['jquery', 'core/str', 'core/ajax'], function($, str, ajax) {
</code>
</code>


Line 94: Line 102:
</code>
</code>


Don't forget to supply the complete 'frankenstyle' path. The .js is not needed.  
Don't forget to supply the complete '[[Frankenstyle]]' path. The .js is not needed.  


js_call_amd takes a third parameter which is an ''array'' of parameters. These will translate to individual parameters in the 'init' function call. For example...
js_call_amd takes a third parameter which is an ''array'' of parameters. These will translate to individual parameters in the 'init' function call. For example...
Line 291: Line 299:
== But I have a mega JS file I don't want loaded on every page? ==
== But I have a mega JS file I don't want loaded on every page? ==
Loading all JS files at once and stuffing them in the browser cache is the right choice for MOST js files, there are probably some exceptions. For these files, you can rename the javascript file to end with the suffix "-lazy.js" which indicates that the module will not be loaded by default, it will be requested the first time it is used. There is no difference in usage for lazy loaded modules, the require() call looks exactly the same, it's just that the module name will also have the "-lazy" suffix.
Loading all JS files at once and stuffing them in the browser cache is the right choice for MOST js files, there are probably some exceptions. For these files, you can rename the javascript file to end with the suffix "-lazy.js" which indicates that the module will not be loaded by default, it will be requested the first time it is used. There is no difference in usage for lazy loaded modules, the require() call looks exactly the same, it's just that the module name will also have the "-lazy" suffix.
== Useful links ==
* [https://assets.moodlemoot.org/sites/15/20171004085436/JavaScript-AMD-with-RequireJS-presented-by-Daniel-Roperto-Catalyst.pdf JavaScript AMD with RequireJS] presented by Daniel Roperto, Catalyst. (MoodleMOOT AU 2017)
* [[Useful_core_Javascript_modules]]
*[[Guide_to_adding_third_party_jQuery_for_AMD]] by Patrick Thibaudeau
*[https://moodle.org/mod/forum/discuss.php?d=378112#p1524459 How to get variables from PHP into javascript AMD modules in M3.5] Justin Hunt, on Moodle forums.


[[Category:AJAX]]
[[Category:AJAX]]
[[Category:Javascript]]
[[Category:Javascript]]

Revision as of 10:53, 3 November 2018

Moodle 2.9


Javascript Modules

What is a Javascript module and why do I care?

A Javascript module is nothing more than a collection of Javascript code that can be used (reliably) from other pieces of Javascript.

Why should I package my code as a module?

By packaging your code as a module you break your code up into smaller reusable pieces. This is good because:

a) Each smaller piece is simpler to understand / debug

b) Each smaller piece is simpler to test

c) You can re-use common code instead of duplicating it

How do I write a Javascript module in Moodle?

Since version 2.9, Moodle supports Javascript modules written using the Asynchronous Module Definition (AMD) API. This is a standard API for creating Javascript modules and you will find many useful third party libraries that are already using this format.

To edit or create an AMD module in Moodle you need to do a couple of things.

Install grunt

The AMD modules in Moodle must be processed by some build tools before they will be visible to your web browser. We use "grunt" as a build tool to wrap our different processes. Grunt is a build tool written in Javascript that runs in the "nodejs" environment.

This means you first have to install nodejs - and its package manager npm. The details of how to install those packages will vary by operating system, but on Linux it's probably similar to "sudo apt-get install nodejs npm". There are downloadable packages for other operating systems here: http://nodejs.org/download/. Moodle currently requires node >=16.14.0 <17 (a.k.a. "lts/gallium"). Right now pointing to nodejs v16.15.1 (see MDL-73915 for more details)..

Once this is done, you can run the command:

npm install
npm install -g grunt-cli

from the top of the Moodle directory to install all of the required tools. (You may need extra permissions to use the -g option.)

Development mode

To avoid having to constantly run grunt, make sure you set the following in your config.php

// Prevent JS caching $CFG->cachejs = false;

Moodle will now run your module from the amd/src module. Don't forget to switch this off and run 'grunt' before deploying the new version!

In this mode - if you get a strange message in your javascript console like "No define call for core/first" it means you have a syntax error in the javascript you are developing. Or, "No define call for theme_XXX/loader" as you are probably missing the 'src' folder with relevant JS files. which might happen when you turn debugging ON on a theme that was bought, without 'src' folder :-(

Running grunt

You can run grunt in your plugin's 'amd' directory and it will only operate on your modules. If you're having problems or just want to check your work it is worth running for the 'lint' feature. This can find basic problems. This sub-directory support wont work on Windows unfortunately but there is an alternative: Run grunt from the top directory with the --root=path/to/dir to limit execution to a sub-directory.

See Grunt#Running_grunt for more details of specific grunt commands which can be used.

If you get the error message

/usr/bin/env: node: No such file or directory

Then see the thread https://github.com/nodejs/node-v0.x-archive/issues/3911

On Ubuntu 14.04 this fixed it for me:

sudo ln -fs /usr/bin/nodejs /usr/local/bin/node

Note: Once you have run grunt and built your code, you will then need to purge Moodle caches otherwise the changes made to your minified files may not be picked up by Moodle.

Minimum (getting started) module for plugins

This shows the absolute minimum module you need to get started adding modules to your plugins. It's actually quite simple...

// Put this file in path/to/plugin/amd/src // You can call it anything you like

define(['jquery'], function($) {

   return {
       init: function() {
           // Put whatever you like here. $ is available
           // to you as normal.
           $(".someclass").change(function() {
               alert("It changed!!");
           });
       }
   };

});

This code passes the jquery module into our function (parameter $). There are a number of other useful modules available in Moodle, some of which you'll probably need in a practical application. See Useful_core_Javascript_modules. Simply list them in both the define() first parameter and the function callback. E.g.,

   define(['jquery', 'core/str', 'core/ajax'], function($, str, ajax) {

The idea here is that we will run the 'init' function from our (PHP) code to set things up. This is called from PHP like this...

   $PAGE->requires->js_call_amd('frankenstyle_path/your_js_filename', 'init');

Don't forget to supply the complete 'Frankenstyle' path. The .js is not needed.

js_call_amd takes a third parameter which is an array of parameters. These will translate to individual parameters in the 'init' function call. For example...

   $PAGE->requires->js_call_amd('block_iomad_company_admin/department_select', 'init', array($first, $last));

...calls

   return {
       init: function(first, last) {
   }

A more comprehensive explanation follows...

"Hello World" I am a Javascript Module

Lets now create a simple Javascript module so we can see how to lay things out.

Each Javascript module is contained in a single source file in the <componentdir>/amd/src folder. The final name of the module is taken from the file name and the component name. E.g. block_overview/amd/src/helloworld.js would be a module named "block_overview/helloworld". the name of the module is important when you want to call it from somewhere else in the code.

After running grunt - the minified Javascript files are stored in the <componentdir>/amd/build folder. The javascript files are renamed to show that they are minified (helloworld.js becomes helloworld.min.js).

Don't forget to add the built files (the ones in amd/build) to your git commits, or in production no-one will see your changes.

Lets create a simple module now:

blocks/overview/amd/src/helloworld.js // Standard license block omitted. /*

* @package    block_overview
* @copyright  2015 Someone cool
* @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
 * @module block_overview/helloworld
 */

define(['jquery'], function($) {

    /** 
     * Give me blue.
     * @access private
     * @return {string}
     */
    var makeItBlue = function() {
         // We can use our jquery dependency here.
         return $('.blue').show();
    };
     
   /**
    * @constructor
    * @alias module:block_overview/helloworld
    */
   var greeting = function() {
       /** @access private */
       var privateThoughts = 'I like the colour blue';
       
       /** @access public */
       this.publicThoughts = 'I like the colour orange';
   };
   /**
    * A formal greeting.
    * @access public
    * @return {string}
    */
   greeting.prototype.formal = function() {
       return 'How do you do?';
   };
   /**
    * An informal greeting.
    * @access public
    * @return {string}
    */
   greeting.prototype.informal = function() {
       return 'Wassup!';
   };
   return greeting;

});

The most interesting line above is: define(['jquery'], function($) {

All AMD modules must call "define()" as the first and only global scoped piece of code. This ensures the javascript code contains no global variables and will not conflict with any other loaded module. The name of the module does not need to be specified because it is determined from the filename and component (but it can be listed in a comment for JSDoc as shown here).

The first argument to "define" is the list of dependencies for the module. This argument must be passed as an array, even if there is only one. In this example "jquery" is a dependency. "jquery" is shipped as a core module is available to all AMD modules.

The second argument to "define" is the function that defines the module. This function will receive as arguments, each of the requested dependencies in the same order they were requested. In this example we receive JQuery as an argument and we name the variable "$" (it's a JQuery thing). We can then access JQuery normally through the $ variable which is in scope for any code in our module.

The rest of the code in this example is a standard way to define a Javascript module with public/private variables and methods. There are many ways to do this, this is only one.

It is important that we are returning 'greeting'. If there is no return then your module will be declared as undefined.

Loading modules dynamically

What do you do if you don't know in advance which modules will be required? Stuffing all possible required modules in the define call is one solution, but it's ugly and it only works for code that is in an AMD module (what about inline code in the page?). AMD lets you load a dependency any time you like.

// Load a new dependency. require(['mod_wiki/timer'], function(timer) {

  // timer is available to do my bidding.

});

Including an external javascript/jquery library

If you want to include a javascript / jquery library downloaded from the internet you can do so as follows:

Warning: if the library you download, supports AMD but is already "named" you will not be able to include it directly e.g.

       define("typeahead.js", *[ "jquery" ], function(a0) {
           return factory(a0);
       });

will not work, as moodle injects it's own define name when loading the library.

If the library is in AMD format and has a define: e.g. i want to include the jquery final countdown timer on my page ( hilios.github.io/jQuery.countdown/ )

  • download the module in both normal and minified versions
  • place the modules in your moodle install e.g. your custom theme dir, or plugin dir
  • /theme/mytheme/amd/src/jquery.countdown.js

you can now include the module and initialise it (there are multiple ways to do this) php:

1. Create your own AMD module and initialise it:

In your PHP file: $this->page->requires->js_call_amd('theme_mytheme/countdowntimer', 'initialise', $params);

Javascript module: // /theme/mytheme/amd/src/countdowntimer.js define(['jquery', 'theme_mytheme/jquery.countdown'], function($, c) {

   return {
       initialise: function ($params) {
          $('#clock').countdown('2020/10/10', function(event) {
            $(this).html(event.strftime('%D days %H:%M:%S'));
          });
       }
   };

});

2. Put the javascript into a mustache template: // /theme/mytheme/templates/countdowntimer.mustache {{#js}} require(['jquery', 'theme_mytheme/jquery.countdown'], function($) {

          $('#clock').countdown('2020/10/10', function(event) {
            $(this).html(event.strftime('%D days %H:%M:%S'));
          });

}); Template:/js

3. Call the javascript directly from php (although who would want to put javascript into php? ergh): $PAGE->requires->js_amd_inline(' require(['theme_mytheme/jquery.countdown'], function(min) {

          $('#clock').countdown('2020/10/10', function(event) {
            $(this).html(event.strftime('%D days %H:%M:%S'));
          });

}); ');

Embedding AMD code in a page

So you have created lots of cool Javascript modules. Great. How do we actually call them? Any javascript code that calls an AMD module must execute AFTER the requirejs module loader has finished loading. We have provided a function "js_call_amd" that will call a single function from an AMD module with parameters.

$PAGE->requires->js_call_amd($modulename, $functionname, $params);

that will "do the right thing" with your block of AMD code and execute it at the end of the page, after our AMD module loader has loaded. Notes:

  • the $modulename is the 'componentname/modulename' discussed above
  • the $functionname is the name of a public function exposed by the amd module.
  • the $params is an array of params passed as arguments to the function. These should be simple types that can be handled by json_encode (no recursive arrays, or complex classes please).
  • if the size of the params array is too large (> 1Kb), this will produce a developer warning. Do not attempt to pass large amounts of data through this function, it will pollute the page size. A preferred approach is to pass css selectors for DOM elements that contain data-attributes for any required data, or fetch data via ajax in the background.

AMD / JS code can also be embedded on a page via mustache templates see here: https://docs.moodle.org/dev/Templates#What_if_a_template_contains_javascript.3F

But I have a mega JS file I don't want loaded on every page?

Loading all JS files at once and stuffing them in the browser cache is the right choice for MOST js files, there are probably some exceptions. For these files, you can rename the javascript file to end with the suffix "-lazy.js" which indicates that the module will not be loaded by default, it will be requested the first time it is used. There is no difference in usage for lazy loaded modules, the require() call looks exactly the same, it's just that the module name will also have the "-lazy" suffix.

Useful links