Widget Developer Documentation

JavaScript

Using JavaScript was covered on the mechanics page. Please review the discussion there, as this page expands on it.

But just as a review, there are three methods for outputting JavaScript from your widget.

  1. Direct output, using <script> tags
  2. Direct output, using Moboom::registerScript() 
  3. Indirect output, using Moboom::registerScriptFile() 

Here the term direct output indicates that the script is sent to the browser as part of the widget. Each instance of the widget will output a fresh copy of the script, just as it outputs a fresh copy of whatever HTML code is built by your widget.

Indirect output, in contrast, does not output the script as part of the widget. Instead, you give the API the location of a script file. When the page is finally built, after all widgets have been processed, the script files are collated, merged, and output in the proper location on the page as <script src=""> tags. Most importantly, only one copy of each script file is installed, no matter how many instances of your widget are on the page. (See below for more on cross-widget duplication.)

Communicating to your scripts

How can you communicate from your server-side code to your client-side code? For example, how can a widget setting be used to trigger specific behavior in a script?

This is a challenge in all environments, not just Moboom. Here are some ways you might think about the problem.

Sample widget: JS-Image

For the purpose of illustration, we will build a new widget called “JS Image.” This widget takes a single setting (an image), and outputs the image in the DOM.

Here is the normal way to create an image in Moboom:

<?php
$image = Moboom::getWidgetSetting('image');
$imageURL = Moboom::createImageUrl($image);
echo "<img src='$imageURL'>";

However, we won’t be doing it the normal way. We’ll instead use JavaScript to insert the image into the DOM dynamically. It’s an artificial example, but it helps illustrate some of the challenges of working with scripts in Moboom.

Version 1: Direct output

Here is the first iteration of our JS-Image widget. It uses direct JavaScript output with <script> tags.

<?php
$image = Moboom::getWidgetSetting('image');
$imageURL = Moboom::createImageUrl($image);

// prepare the wrapper
echo "<div class='js-image-container'></div>";

// output a script, which inserts the image
echo <<< SCRIPT
<script>
    $(function() {
        var wrapper = $('.js-image-container'),
            newImage = '<img src="$imageURL">';
        wrapper.append(newImage);
    });
</script>
SCRIPT;

The entire script block will be processed after jQuery is finished loading (see line 11). It looks for the wrapper element which we created on line 6, then fills it with a new image. The src attribute of the image is set by linking it to the PHP variable $imageURL, which is interpolated in the HEREDOC on line 13. If you look at the page source after installing this widget, you’ll see the correct URL inserted directly into the script.

This version of the widget fails when multiple instances of it are installed on the same page. For now, just think over why that’s the case. After the next section, we’ll discuss this in more detail.

Version 2: Direct output using API

The second iteration of the widget is almost identical to the first. In this example, we use the Moboom API to output the script:

<?php
$image = Moboom::getWidgetSetting('image');
$imageURL = Moboom::createImageUrl($image);

// prepare the wrapper
echo "<div class='js-image-container'></div>";

// prepare a script, which inserts the image
$scr = <<< SCRIPT
    var wrapper = $('.js-image-container'),
        newImage = '<img src="$imageURL">';
    wrapper.append(newImage);
SCRIPT;

// install the script using the API
Moboom::registerScript('js-image-script', $scr, Moboom::POS_READY);

The HEREDOC script is now stored into a PHP variable, rather than being echoed out. The script is then sent to the browser using Moboom::registerScript(). The last parameter (Moboom::POS_READY) tells the API to wrap the script inside a jQuery–ready block, just as we did explicitly in version 1 above.

If you view the source of a rendered page, you’ll see that the script output is nearly identical. This widget suffers from the same problem as the version 1 widget.

Note
Despite the problems mentioned, both techniques shown above have a place in your bag of tricks. Outputting JavaScript directly is sometimes the correct strategy, especially with widgets that you know will only be used once on a page, such as a custom menu or a header animation.

Version 3: Direct output with multiple instances

The most obvious problem with the above widgets is that they search the DOM for an element of a specific class (in this case, .js-image.container). When there are multiple instances of the widget installed, you’ll find the results are not what you had planned. The scripts collide, because their effects are not properly firewalled from each other, and images are not placed correctly.

For all but the most special-purpose of widgets, you should consider how your widget will perform when multiple instances exist on the same page. This is a lesson born of experience: several times we have made the incorrect assumption that a widget will be used only once per page, and been forced into a late-game refactor.

To ensure that each widget instance is managed independently, you can assign a unique ID to each wrapper. In this example, we use the PHP function mt_rand as a way of assigning a random ID to each instance of the widget. We then use that ID to insert the image tag into the correct wrapper.

This version of the widget is otherwise identical to version 1.

<?php
$image = Moboom::getWidgetSetting('image');
$imageURL = Moboom::createImageUrl($image);

// assign a random number as the unique instance ID
$id = mt_rand();

// prepare the wrapper, now with a unique ID
echo "<div id='js-image-container-$id'></div>";

// output a script, which inserts the image
// this time, we target the unique ID rather than a class
echo <<< SCRIPT
<script>
    $(function() {
        var wrapper = $('#js-image-container-$id'),
            newImage = '<img src="$imageURL">';
        wrapper.append(newImage);
    });
</script>
SCRIPT;

Each instance of the widget will have a different unique ID assigned. The script for each widget is tied to that unique ID, which ensures that widgets do not affect each other. This version of the JS-Image widget will indeed work properly when multiple instances are installed on the same page. Look over the code carefully, as the shift from classes to IDs is subtle.

This widget has a new problem, or at least one that we haven’t yet discussed. What would happen if the script were more complex?  What if instead of a single line of JavaScript, you were wrapping a jQuery plugin with 500 lines of code? The way this widget is structured, each instance of the widget would load the full script, which is not only wasteful, but slows down your page load time. Ideally, we would extract the common code into an external script file, so that it is cached, and loaded only once per page.

In other words: how can we handle multiple instances when the script is loaded with Moboom::registerScriptFile(), rather than emitted inline as part of the widget?

Version 4: Indirect output with multiple instances

We are going to parameterize the script, separating the per–instance configuration from the common code. If you’re wrapping a third-party jQuery plugin, it probably was already written in such a way that each instance can take a separate set of configuration options.

For this example, we’ll be using a global variable to build up a list of widget instances and their specific configurations. This global variable will then be used by the external script file to loop through all instances.

This widget is a bit more complex, so we’ll go slowly.

Part 1: building the configuration options
window.jsImageList = window.jsImageList || {};
window.jsImageList[$id] = "$imageURL";

The script above will be printed inline with each instance of the widget. On line 1, we create (or reuse) a new global variable. This code is smart about handling multiple instances: the first instance of the widget will create the image list object, and subsequent instances will augment it.

Line 2 adds a new property to the image list object. The key is the unique ID for this instance of the widget, and the value is the image URL. If you need more per–widget configuration properties, you can easily expand this to be a full JSON object. For example:

window.jsImageList[$id] = {
  color: '$color',
  size:  '$size'
};

This script is not wrapped inside a jQuery–ready function. It is executed synchronously as the page loads, once for each instance. By the time our jQuery–ready function is executed, the global object is fully populated with information about all widget instances and their configuration parameters.

Here is the complete PHP code for our test widget.

<?php
$image = Moboom::getWidgetSetting('image');
$imageURL = Moboom::createImageUrl($image);

// assign a random number as the unique instance ID
$id = mt_rand();

// prepare the wrapper, now with a unique ID
echo "<div id='js-image-container-$id'></div>";

// and add this ID to the global JS variable,
// which keeps a running list of instances
echo <<< SCRIPT
<script>
    window.jsImageList = window.jsImageList || {};
    window.jsImageList[$id] = "$imageURL";
</script>
SCRIPT;
Part 2: looping through the instances

The following script will be loaded with Moboom::registerScriptFile(). This ensures that it is executed only once, regardless how many instances of the widget are on the page. Because the contents of the script are wrapped in a jQuery–ready function, it executes after all of the inline widget scripts, so the global configuration object is up-to-date.

$(function() {
    $.each(window.jsImageList, function(id,imageURL) {
        
        // process each image
        var wrapper = $('#js-image-container-' + id),
            newImage = '<img src="' + imageURL + '">';
        wrapper.append(newImage);
    });
});

On line 2, we use a jQuery iterator to loop through each property of the image list object (one per widget instance). The iterator takes a callback function, which takes two parameters: the key (in this case, the ID) and the value (in this case, the image URL). If you built a JSON configuration object, as we did in the sample above, then that object would be passed into the callback as its second parameter.

Now, what you do with each entry in the image list is up to you, and that’s where all of the unique goodness of your widget actually happens. Everything else is just prep. 

In this case, the small amount of code shown on lines 5–7 is the core of the widget. If you were building a widget to wrap a jQuery plugin, you would configure your plugin here. 

Many of our internal uses of this technique look very much like this:

A complete example

The last example is actually a fairly common pattern for us at Moboom. Just to reinforce the learning, here is a complete widget (well, semi-complete) built around a third-party jQuery plugin called fancy plugin. There are three files in the widget: 

  1. main.php is the main back-end widget code
  2. fancyplugin.js is the jQuery plugin code, just as we downloaded it from the maintainer’s repository
  3. setup.js is the mediator, which links the widget instances to the plugin code 

 

<?php
// load the widget settings
$speed = Moboom::getWidgetSetting('speed');
$power = Moboom::getWidgetSetting('power');
$shape = Moboom::getWidgetSetting('shape');
$spin  = Moboom::getWidgetSetting('spin');

// assign a random number as the unique instance ID
$id = mt_rand();

// prepare the wrapper with a unique ID
echo "<div id='fancyplugin-wrapper-$id'></div>";

// and add this ID to the global JS variable,
// which keeps a running list of instances
echo <<< SCRIPT
<script>
    window.fancyPluginInstances = window.fancyPluginInstances || {};
    window.fancyPluginInstances[$id] = {
        speed: $speed,
        power: $power,
        shape: $shape,
        spin:  $spin
    };
</script>
SCRIPT;

// load the setup script, which mediates between
// the moboom widget instances and the core
// fancyplugin code
//
$baseURL = Moboom::getBaseUrl();
Moboom::registerScriptFile($baseURL . 'setup.js', Moboom::POS_END);

// and load the fancyplugin code itself
Moboom::registerScriptFile($baseURL . 'fancyplugin.js', Moboom::POS_END);
/*
    setup.js
    expects to find window.fancyPluginInstances populated
*/

$(function() {
    $.each(window.fancyPluginInstances, function(eltID, config) {

        $(eltID).fancyPlugin({
            speed: config.speed,
            power: config.power,
            shape: config.shape,
            spin:  config.spin
        });
    });
});
/*
  fancy plugin
  this code is used straight from the fancyplugin repo;
  no changes are needed in order to use in a widget
*/

!function ($) {
    // . . .
}(window.jQuery);

Cross-widget duplication

At the beginning of this article, we discussed how using indirect output (i.e., Moboom::registerScriptFile()) can ensure that your scripts are not duplicated. The examples above use this technique to ensure that only one copy of the fancy plugin script is loaded, regardless how many instances of the widget are installed on a page.

However, we haven’t addressed how to ensure that you don’t create duplicates across multiple widgets. Not multiple instances of the same widget, but separate widgets.

Consider an application built atop the D3 graphing library. This application has several different widgets that use this library: MapView, ChartView, and ChordDiagram. How can we ensure that exactly one instance of the script is installed?

One way to load the library is to install it into each widget, as we did with fancyPlugin.js above:

$baseURL = Moboom::getBaseUrl();
Moboom::registerScriptFile($baseURL . 'd3.js', Moboom::POS_END);

The problem with this method is that Moboom::getBaseUrl() will return a different value for each of the three widgets. (Of course, all instances of the same widget will return the same value for the base URL.) The page renderer will not recognize that the script files are the same, so it will load three separate <script> tags, and three copies of d3.js.

The reliable way to prevent duplication is to load the library from a CDN:

Moboom::registerScriptFile('http://d3js.org/d3.v3.min.js', Moboom::POS_END);

This is the correct way to prevent cross-widget duplication.

We’ve seen some clients use another (dangerous!) technique. They will copy the output of one widget’s Moboom::getBaseUrl() and hard-code it into another widget’s Moboom::registerScriptFile(). This is dangerous simply because the location returned by Moboom::getBaseUrl() is not guaranteed to be accurate in the future.