How To Migrate a Module to AMD

By on July 24, 2012 11:20 am

Dojo 1.7 added full support for asynchronous module loading, defined with the widely adopted asynchronous module definition (AMD) format. The new module loader and module format offer faster module loading, better performance, and wide interoperability. However, for many, upgrading from the legacy Dojo module format to AMD is daunting, since the new module formats look much different than the old. Despite the large visual difference though, upgrading is a fairly straightforward process. There may be more effort involved in leveraging the new features and APIs in 1.7 and 1.8, but that is a different topic.

Define a Module

The first step in upgrading a module to AMD is to switch the module declaration from a dojo.provide call to an AMD define() call. This is very simple. The new module loader utilizes some fancy tricks to determine a module’s path, and therefore a module no longer needs to declare its own module name (this has important benefits because files can be renamed and moved without any internal code changes). In this post, we are going to look at how to convert the dojox.gantt.GanttChart module. The module used to start with:

dojo.provide("dojox.gantt.GanttChart");
... rest of module ...

And we simply switch the beginning code to a define() call:

define([], function(){
... rest of module ...
});

Note that the entire module is now within the function body of the define‘s callback function (called the factory function).

Specify Dependencies

The next and most important step of upgrading a module to AMD is to switch the dependency declarations from legacy dojo.require() calls to dependency ids within the dependency array for the define() call (the first argument). We are replacing each dojo.require() call, and converting the legacy module id to an AMD module id. Converting the module id consists of replacing the dots with slashes (like a URL path). We also add dojo as a dependency (unless we want to use Dojo in baseless mode, as discussed below). In the original dojox/gantt/GanttChart.js module we had five dojo.require() calls:

dojo.require("dijit.Tooltip");
dojo.require("dojox.gantt.GanttProjectItem");
dojo.require("dojox.gantt.GanttResourceItem");
dojo.require("dojox.gantt.TabMenu");
dojo.require("dojo.date.locale");

And now the module should start with (note that we also include dojo as a dependency as well):

define([
	"dojo",
	"dijit/Tooltip", 
	"dojox/gantt/GanttProjectItem", 
	"dojox/gantt/GanttResourceItem",
	"dojox/gantt/TabMenu",
	"dojo/date/locale"
], function(dojo){

At this point, we should now have a functioning AMD module that can be loaded with an AMD module loader. (assuming all dependency modules declared their own namespace and are AMD, as all Dojo modules do).

Local Module References

Now, our code has specified dependencies, but it isn’t really using the full power of AMD yet. One of the key concepts with AMD is importing dependency exports as local variables. The legacy code is using namespaced globals instead of local references. To switch to local references, we include the module export in our factory callback function arguments. Each dependency in the dependency array corresponds to an argument in the factory function. To have a local reference to dijit/Tooltip, we include it in the arguments:

define([
	"dojo",
	"dijit/Tooltip", 
	"dojox/gantt/GanttProjectItem", 
	"dojox/gantt/GanttResourceItem",
	"dojox/gantt/TabMenu",
	"dojo/date/locale"
], function(dojo, Tooltip, GanttProjectItem, GanttResourceItem, TabMenu, locale){

And now, to utilize the local reference, we replace all of the namespaced global references to dijit.Tooltip with our local variable Tooltip. We have now reduced global variable usage, and improved variable lookup times. In addition, if we ever needed to point to a different module to provide registry functionality, we could do by simply changing the dependency list. The remainder of the source code would not have to be changed.

Export This Module

Now, to make it possible for our converted modules to be properly used by other AMD modules, we need to export the defined function(s) of this module. In this case “dojox/gantt/GanttChart.js” creates a widget class that is assigned to the namespaced global dojox.gantt.GanttChart. We will now export this object by simply returning this class from the factory function:

... main module ...
return dojox.gantt.GanttChart;
});

Now other modules can list dojox/gantt/GanttChart in the dependencies, and locally reference the export of the module.

Global Free

To take the AMD conversion a step further, we could eliminate our dependence on the dojox global variable, and rely completely on modules for references. To do this, we can eliminate the class namespace creation argument in the dojo.declare call, and directly return the constructor created by dojo.declare.

define([...], function(dojo, Tooltip, GanttProjectItem, 
		GanttResourceItem, TabMenu, locale){
	return dojo.declare(null, {
		...
	});
});

Now the dojo.declare call will no longer create a global reference to the new class at dojox.gantt.GanttChart, it will simply return it. The module returns (exports) that class, so other modules can use dojox/gantt/GanttChart by declaring it as a dependency and utilize the module export. From another module, we would now use dojox/gantt/GanttChart like:

define(["dojox/gantt/GanttChart"], function(GanttChart){
	var myChart = new GanttChart(config, targetNode);
});

As a reminder, if you want dojox/gantt/GanttChart and all its dependencies to be AMD-based (and global-free), you would need to convert the other modules in dojox/gantt as well.

View the complete changeset to port dojox/gantt to AMD, and to make use of more recent additions to Dojo

Plugins

One of the powerful features of AMD is the plugin capability. This makes it possible to not only load standard JavaScript modules asynchronously as a dependency, but other resources, like templates and localization can be asynchronously loaded as dependencies as well. A common resource that is used by many widgets is a template loaded through dojo.cache. With AMD conversion, we can and should convert this to a dependency in the define() call. To do this, we use the dojo/text! plugin, and specify the target resource file as the suffix. For example, if our starting code had a dojo.cache() call:

dojo.declare([dijit._Widget, dijit._Templated], {
	templateString: dojo.cache("mypackage","templates/MyWidget.html");
	...

This could be converted to:

define(["dojo/text!./templates/MyWidget.html", ...], function(template){
dojo.declare([dijit._Widget, dijit._Templated], {
	templateString: template,
	...

And the MyWidget.html file can be asynchronously loaded along with other dependencies.

Localization

Likewise, you can also asynchronously load localization strings with AMD, so it is preloaded before a localization call. For example:

define["dojo/i18n", "dojo/i18n!dijit/nls/loading"], function(i18n){
	var messages = i18n.getLocalization("dijit", "loading");
	...

Baseless Dojo (AKA Nano)

One of the major changes in Dojo 1.7 is the ability to use Dojo “baseless”. You may have noticed that in the examples above we include dojo as a dependency, which in turn loads all of Dojo base. In baseless Dojo, we no longer need to load all of base if we don’t need it. Instead, we can specifically declare each Dojo module that we actually need, and use the module references. This gives us granular control over exactly what is loaded, minimizing application size. We have already seen how to directly reference modules through the factory function arguments. We can also do this for individual Dojo base modules. With dojox/gantt/GanttChart, if we removed the dojo base dependency, we would need to include several dojo base modules, for example one of them would be dojo/_base/declare:

define(["dojo/_base/declare", ...
	], function(declare, ...){
return declare(null, {...

Conversion and Upgrading

Dojo 1.7 includes many powerful new features. There are features that you can upgrade your code to leverage that are beyond this post, but converting to AMD is certainly one of the most key changes you can make to take advantage of the new module loader in 1.7. A basic conversion to making modules AMD-compatible is very simple, and you can easily choose which of the more powerful features you want to utilize (and we can help). Converting to AMD will allow you to enjoy the greater performance, interoperability, and maintainability which ultimately benefits you, your team, and your company.

Comments

  • Great post! Just wondering how declare(null, …) would work for creating widgets that are created declaratively through HTML alone. Or do widgets that are created in markup have to use a global namespace like declare(‘mynamespace.MyWidget”, …)?

  • @Scott. In 1.7, you would need to include the old namespace syntax. With Dojo 1.8 and above (1.8 is in release candidate status, with a release expected in about 10 days), this is no longer required. See http://blog.kitsonkelly.com/2012/05/dojoparser/ for more details.

  • i18n can also be loaded in shorter style:

    Instead of

    define[“dojo/i18n”, “dojo/i18n!dijit/nls/loading”], function(i18n){
    var messages = i18n.getLocalization(“dijit”, “loading”);

    you can also simply use

    define[“dojo/i18n!dijit/nls/loading”], function(messages){
    // messages is the language object, ready for use

  • Are there any coexistance issues between the old and new format? Can I keep some classes in the old format (to be compatible with older Dojo versions) and load them with the new mechanism and still benefit from asynchonous module loading?

  • @Karsten, http://dojotoolkit.org/reference-guide/loader/legacy.html goes into extensive detail on your question… short answer is yes, long answer is worth the read of that article.

  • You either need to explicitly define these dependencies for each module that you use them in, or you can create a custom convenience module that takes all of those dependencies and exposes them on one module and then use that convenience module as a dependency. Convenience modules can be dangerous because if you stop using one of the dependencies entirely and then forget to stop loading it for the convenience module, you’ll be loading extra code you don’t need.

    dojoConfig.deps just gives you a way to load modules when the loader is loaded. You still need to explicitly define your code dependencies in every module you write. AMD does not expose modules globally; this is a backwards-compatibility feature of the Dojo 1 codebase only.