Note:

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

YUI/Modules: Difference between revisions

From MoodleDocs
m (Text replacement - "<code bash>" to "<syntaxhighlight lang="bash">")
m (Text replacement - "<code (.*)>" to "<syntaxhighlight lang="$1">")
(2 intermediate revisions by the same user not shown)
Line 79: Line 79:
         |-- meta
         |-- meta
             |-- fruitbowl.json
             |-- fruitbowl.json
</code>
</syntaxhighlight>
''Note: Moodle 2.5 onwards is backwards compatibly with the previous structure.''
''Note: Moodle 2.5 onwards is backwards compatibly with the previous structure.''


Line 107: Line 107:
function on the page requirements class. This can be accessed using:
function on the page requirements class. This can be accessed using:


<code php>
<syntaxhighlight lang="php">
$PAGE->requires->yui_module();
$PAGE->requires->yui_module();
// or:
// or:
$this->page->requires->yui_module();
$this->page->requires->yui_module();
</code>
</syntaxhighlight>


The yui_module function takes the module name, and the init function to
The yui_module function takes the module name, and the init function to
Line 117: Line 117:
third argument:
third argument:


<code php>
<syntaxhighlight lang="php">
$PAGE->requires->yui_module('moodle-block_fruit-fruitbowl',
$PAGE->requires->yui_module('moodle-block_fruit-fruitbowl',
         'M.block_fruit.fruitbowl.init');
         'M.block_fruit.fruitbowl.init');
</code>
</syntaxhighlight>


==== Passing arguments from your PHP to your module ====
==== Passing arguments from your PHP to your module ====
Line 128: Line 128:
arguments.
arguments.


<code php>
<syntaxhighlight lang="php">
$PAGE->requires->yui_module('moodle-block_fruit-fruitbowl',
$PAGE->requires->yui_module('moodle-block_fruit-fruitbowl',
         'M.block_fruit.fruitbowl.init',
         'M.block_fruit.fruitbowl.init',
Line 135: Line 135:
             'value2',
             'value2',
         ));
         ));
</code>
</syntaxhighlight>


These will be presented to the JavaScript function you name:
These will be presented to the JavaScript function you name:


<code javascript>
<syntaxhighlight lang="javascript">
init = function(argument1, argument2) {
init = function(argument1, argument2) {
     // Use the arguments here.
     // Use the arguments here.
};
};
</code>
</syntaxhighlight>


Often it makes more sense to pass a single argument with an associative array:
Often it makes more sense to pass a single argument with an associative array:


<code php>
<syntaxhighlight lang="php">
$PAGE->requires->yui_module('moodle-block_fruit-fruitbowl',
$PAGE->requires->yui_module('moodle-block_fruit-fruitbowl',
         'M.block_fruit.fruitbowl.init',
         'M.block_fruit.fruitbowl.init',
Line 154: Line 154:
             'key2' => 'value2',
             'key2' => 'value2',
         )));
         )));
</code>
</syntaxhighlight>


These will be presented to the JavaScript function you name:
These will be presented to the JavaScript function you name:


<code javascript>
<syntaxhighlight lang="javascript">
init = function(params) {
init = function(params) {
     // Use the arguments through params.key1 and params.key2.
     // Use the arguments through params.key1 and params.key2.
};
};
</code>
</syntaxhighlight>


=== A basic module ===
=== A basic module ===
Line 193: Line 193:
   }
   }
}
}
</code>
</syntaxhighlight>


The easiest way of creating this structure is by using the logical OR operator.
The easiest way of creating this structure is by using the logical OR operator.


<code javascript>
<syntaxhighlight lang="javascript">
M.block_fruit = M.block_fruit || {};
M.block_fruit = M.block_fruit || {};
M.block_fruit.fruitbowl = {};
M.block_fruit.fruitbowl = {};
var NS = M.block_fruit.fruitbowl;
var NS = M.block_fruit.fruitbowl;
</code>
</syntaxhighlight>


This ensures that any existing code on the M.block_fruit object is not
This ensures that any existing code on the M.block_fruit object is not
Line 222: Line 222:
* use event delegation where possible.
* use event delegation where possible.


<code javascript>
<syntaxhighlight lang="javascript">
M.block_fruit = M.block_fruit || {};
M.block_fruit = M.block_fruit || {};
var NS = M.block_fruit.fruitbowl = {};
var NS = M.block_fruit.fruitbowl = {};
Line 235: Line 235:
     }, Y.config.doc, '.fruit');
     }, Y.config.doc, '.fruit');
};
};
</code>
</syntaxhighlight>


==== Separation of code from configuration and style (loose coupling) ====
==== Separation of code from configuration and style (loose coupling) ====
Line 247: Line 247:
recommend creating two static objects at the top of your code: one for CSS
recommend creating two static objects at the top of your code: one for CSS
selectors, and one for CSS class names.
selectors, and one for CSS class names.
<code javascript>
<syntaxhighlight lang="javascript">
var CSS = {
var CSS = {
         FRUIT: 'fruit',
         FRUIT: 'fruit',
Line 269: Line 269:
     }, Y.config.doc, SELECTORS.FRUIT, this);
     }, Y.config.doc, SELECTORS.FRUIT, this);
};
};
</code>
</syntaxhighlight>


With this change, if at a later date you need to change the styling on a
With this change, if at a later date you need to change the styling on a
Line 287: Line 287:
call it accordingly.
call it accordingly.


<code javascript>
<syntaxhighlight lang="javascript">
var CSS = {
var CSS = {
         FRUIT: 'fruit',
         FRUIT: 'fruit',
Line 311: Line 311:
     e.target.addClass(CSS.SELECTED);
     e.target.addClass(CSS.SELECTED);
};
};
</code>
</syntaxhighlight>


Now you can clearly see your what your initialiser does, and you can call handle_selection for other events too without duplicating code.
Now you can clearly see your what your initialiser does, and you can call handle_selection for other events too without duplicating code.

Revision as of 08:15, 15 July 2021

This feature has been marked as deprecated since Moodle 2.9


Note: As of Moodle 2.9 we are transitioning away from YUI to AMD modules. This transition will take a long time, but it is important because the YUI team have stopped all new development on the YUI library ( http://yahooeng.tumblr.com/post/96098168666/important-announcement-regarding-yui ). See Javascript Modules for more information.

Moodle 2.5

Moodle 2.6

Moodle 2.7


YUI modules

You may want to Jump straight to the complete example.

What is a module and why would I want to use one?

Many people are used to writing their JavaScript code either inline, or in a separate file included using <script> tags in their HTML. Although both of these are supported within Moodle, we highly recommend investing a little time in looking into the YUI module system as it offers some really great features which will benefit you in the long run.

These features and benefits include:

  • a host of existing modules that you can hook into and use; which offer
  • code sandboxing to ensure that you always have a good/clean copy of the code; with
  • powerful dependency management; which can utilise
  • asynchronous loading to load dependencies in parallel; and
  • loading of dependencies in any order (you don't have to include script tags in a set order); and
  • separation of loading from execution; and
  • combo loading to reduce the number of individual HTTP transactions.

By using existing modules in your code, you can benefit in other ways too, such as:

  • a consistent look and feel across Moodle; and
  • most of the edge cases already catered for.

The Moodle community is in the process of improving much of its JavaScript in areas such as code, documentation, and processes. This includes updating older code, adding documentation, seeding dependency information for modules to improve load efficiency for browsers, and much more.

You may find the following resources particularly helpful:

Structure and naming

A YUI module is structured in a particular way, both on file, and in the file itself. Before you start writing your module, you need to know a few bits of information:

  • what is the Frankenstyle name of your Moodle plugin;
  • what does your YUI module do in a single word.

You can use these pieces of information to work out the namespace for your plugin. This namespace fits into the template:

 moodle-FRANKENSTYLE-MODULENAME

As an example, writing a plugin for the fictional 'fruit' block which offers fruit to users:

  • you may decide to call this module the 'fruitbowl' as the module offers fruit to users, and presents it in an appealing way so that they can see it.
  • The block is fruit, therefore the frankenstyle name is block_fruit.

Therefor the name for this module will be:

   moodle-block_fruit-fruitbowl


The other thing that you need to know in order to create your first module is where in your plugin the code needs to be.

As described in the Javascript/Shifter#How_do_I_write_a_YUI_module_which_uses_Shifter.3F documentation, the code will need to go in your plugin directory inside a yui directory.

Since Moodle 2.5, the structure for this directory is:

yui/
|-- src
    |-- fruitbowl
        |-- build.json
        |-- js
        |   |-- fruitbowl.js
        |-- meta
            |-- fruitbowl.json

Note: Moodle 2.5 onwards is backwards compatibly with the previous structure.

So in the case of moodle-block_fruit-fruitbowl, the JavaScript code will need to go into:

   /block/fruit/yui/src/fruitbowl/js/fruitbowl.js

(Sorry, it does look a little more complicated than it really is)

See the notes in the Shifter documentation for details on the build.json and meta/MODULENAME.json file contents.

Now that you know where your code needs to be on disk, you can actually include and write it.

Including your module from your PHP code

Rather than using <script> tags to include JavaScript code, Moodle makes use of the YUI loader and a single inline script tag in the page footer.

Moving JavaScript to a page footer is important to the perceived performance of a page, and the YUI loader helps further.

The YUI loader also intelligiently loads page dependencies, in as few requests as possible by making use of a combo-loader system. It will also attempt to use the appropriate version of your JavaScript depending on your $CFG->jsrev settings. In normal operation, a minified version of your code is used; and with $CFG->cachejs false, then a debugging version is used.

To ensure that your module is loaded, there is a yui_module() function on the page requirements class. This can be accessed using:

$PAGE->requires->yui_module();
// or:
$this->page->requires->yui_module();

The yui_module function takes the module name, and the init function to call as it's first two arguments, and an optional list of arguments as a third argument:

$PAGE->requires->yui_module('moodle-block_fruit-fruitbowl',
        'M.block_fruit.fruitbowl.init');

Passing arguments from your PHP to your module

You may want to pass a set of arguments to your JavaScript. You can do so using the optional third argument to yui_module which accepts an array of arguments.

$PAGE->requires->yui_module('moodle-block_fruit-fruitbowl',
        'M.block_fruit.fruitbowl.init',
        array(
            'value1',
            'value2',
        ));

These will be presented to the JavaScript function you name:

init = function(argument1, argument2) {
    // Use the arguments here.
};

Often it makes more sense to pass a single argument with an associative array:

$PAGE->requires->yui_module('moodle-block_fruit-fruitbowl',
        'M.block_fruit.fruitbowl.init',
        array(array(
            'key1' => 'value1',
            'key2' => 'value2',
        )));

These will be presented to the JavaScript function you name:

init = function(params) {
    // Use the arguments through params.key1 and params.key2.
};

A basic module

The most common use-case for JavaScript in Moodle is to utilise existing modules to do something. Your module isn't intended to be reused, or extended by some other piece of code. In these cases, you want to keep things simple.

This section will build up the YUI module gradually, but you can cheat and skip to the completed example.

Namespacing

One of the great benefits of using YUI is the ability to sandbox code in such a way that you can be sure that it won't interfere with other JS code in Moodle, this is done by placing your object in a JavaScript object. Using our fictitious example, this namespace is very similar to the module name namespace:

 M.FRANKENSTYLE.MODNAME

As a JavaScript object this would look like:

M: {
  FRANKENSTYLE: {
    MODNAME: {
      // Your code and functions go here.
    }
  }
}

The easiest way of creating this structure is by using the logical OR operator.

M.block_fruit = M.block_fruit || {};
M.block_fruit.fruitbowl = {};
var NS = M.block_fruit.fruitbowl;

This ensures that any existing code on the M.block_fruit object is not inadvertently overwritten, but also ensures that the definition for the fruitbowl code is clean.

That's to say that, if you have already used your fruitbowl code once on the page, and have modified it in some way (for example to override a function), then the next time it is used, it will be in it's original state with the original function.

Initialisation

Once you have your namespace, you'll want to create one or more functions on this namespace in which to put your code. Typically, we use an 'init' function to perform the initial setup. The amount that this code does should be kept to a minimum because every piece of JavaScript which has to be executed at page load is a piece of code slowing the page down. As a best practice:

  • don't modify the DOM unless you need to do so;
  • don't create anything you aren't using immediately (for example a JS dialogue hidden until first used); and
  • use event delegation where possible.
M.block_fruit = M.block_fruit || {};
var NS = M.block_fruit.fruitbowl = {};

NS.init = function() {
    Y.delegate('click', function(e) {
        // Alert users when they've clicked on some fruit to tell them the obvious.
        alert("You clicked on some fruit");
        
        // Add a border to the fruit so we can see that it was selected.
        e.setStyle('border', '1px solid black');
    }, Y.config.doc, '.fruit');
};

Separation of code from configuration and style (loose coupling)

As another best practice, we encourage you to separate out the code from configuration, and not to apply any CSS styles directly. Instead, use techniques to move the configuration out of the code itself and into variables or attributes, and use CSS classes which the theme can modify.

Typically to make it easier to read your code, and to modify it later, we recommend creating two static objects at the top of your code: one for CSS selectors, and one for CSS class names.

var CSS = {
        FRUIT: 'fruit',
        SELECTED: 'selected'
    },
    SELECTORS = {
        FRUIT: '.' + CSS.FRUIT
    },
    NS;

M.block_fruit = M.block_fruit || {};
NS = M.block_fruit.fruitbowl = {};

NS.init = function() {
    Y.delegate('click', function() {
        // Alert users when they've clicked on some fruit to tell them the obvious.
        alert("You clicked on some fruit");
        
        // Apply the relevant class which contains indications that this fruit was selected.
        e.target.addClass(CSS.SELECTED);
    }, Y.config.doc, SELECTORS.FRUIT, this);
};

With this change, if at a later date you need to change the styling on a selected fruit, this can be done in the theme. An alternative theme can also choose to theme the same item in a completely different way.

It also means that if you need to change the class names, you can do so once in the JavaScript without making it difficult to work out the purpose of a particular line of code from git.


Additional functions instead of anonymous functions

Although it's often quicker to write an inline anonymous function when you write your event handlers, loops, and at other times, it's often much easier to read the code if you move it to a function on your namespace and call it accordingly.

var CSS = {
        FRUIT: 'fruit',
        SELECTED: 'selected'
    },
    SELECTORS = {
        FRUIT: '.' + CSS.FRUIT
    },
    NS;

M.block_fruit = M.block_fruit || {};
NS = M.block_fruit.fruitbowl = {};

NS.init = function() {
    Y.delegate('click', this.handle_selection, Y.config.doc, SELECTORS.FRUIT, this);
};

NS.handle_selection = function(e) {
    // Alert users when they've clicked on some fruit to tell them the obvious.
    alert("You clicked on some fruit");
    
    // Apply the relevant class which contains indications that this fruit was selected.
    e.target.addClass(CSS.SELECTED);
};

Now you can clearly see your what your initialiser does, and you can call handle_selection for other events too without duplicating code.

A re-usable module

Note: This page is a work-in-progress. Feedback and suggested improvements are welcome. Please join the discussion on moodle.org or use the page comments.

This section of documentation has yet to be written.

Notes

There is a proposal to alter the namespace for YUI modules. This is likely to be implemented from Moodle 2.7 onwards.