How Dojo loads every dependency on demand — and how to structure your own modules the right way
require() — Loading Modulesdefine() — Creating ModulesdojoConfig — Loader ConfigurationBefore AMD, JavaScript apps loaded dependencies using plain <script> tags. This approach has three fatal flaws at scale:
<!-- Old-school script loading — 4 problems -->
<script src="utils.js"></script> <!-- 1. Order matters: must load before dependents -->
<script src="widgets.js"></script> <!-- 2. Blocks HTML parsing while downloading -->
<script src="app.js"></script> <!-- 3. Everything on window — name collisions -->
<!-- 4. No way to load only what's needed -->
// Inside widgets.js — global pollution
window.MyWidget = function() { ... };
window.MyHelper = function() { ... };
// Any other script can accidentally overwrite these
AMD (Asynchronous Module Definition) solves all four problems:
windowDojo adopted AMD with version 1.7 (2011) and it remains the module system in 1.17.3. Every Dojo module — dojo/on, dijit/form/Select, dojox/charting/Chart — is an AMD module.
require() — Loading Modulesrequire() is how you consume modules. You declare an array of module IDs you need and receive them as arguments to a callback.
// Basic form — load modules, use in callback
require([
"dojo/dom", // module IDs — forward slash separates package/module
"dojo/on",
"dijit/form/Button"
], function(dom, on, Button) {
// Arguments match the array positions exactly
// dom �? the dojo/dom module
// on �? the dojo/on module
// Button �? the dijit/form/Button constructor
var btn = new Button({
label: "Click Me",
onClick: function() {
dom.byId("output").textContent = "Clicked!";
}
}, "btnNode");
btn.startup();
});
require() — Conditional LoadingYou can call require() anywhere in your code — inside functions, event handlers, conditionals. This enables true lazy loading.
// Load a heavy charting module only when the user opens the chart tab
function onChartTabOpen() {
require(["dojox/charting/Chart", "dojox/charting/themes/Claro"],
function(Chart, theme) {
// Chart module loaded only NOW — not at app startup
var chart = new Chart("chartDiv");
chart.setTheme(theme);
chart.addPlot("default", { type: "Lines" });
chart.render();
});
}
require() — For Already-Loaded Modules// If you KNOW a module is already loaded, require returns it synchronously
// (no callback form — returns the module directly)
var dom = require("dojo/dom"); // only safe if dojo/dom was already loaded
// The safe pattern: always use the callback form
// unless you are 100% sure the module is cached
require() with a callback returns undefined — NOT the modules. The modules are only available inside the callback. Never do:var dom = require(["dojo/dom"], function(dom) { return dom; });// dom is undefined here — require is async!
| ID Form | Resolves To | Example |
|---|---|---|
package/module | package/module.js | "dojo/on" → dojo/on.js |
package/sub/module | package/sub/module.js | "dijit/form/Button" |
./sibling | Relative to current module | "./utils" from myapp/views/list |
../parent | One directory up | "../models/user" |
plugin!resource | Plugin handles the resource | "dojo/text!./tmpl.html" |
define() — Creating Modulesdefine() is how you create a module. A module file should contain exactly one define() call. Whatever you return becomes the module's exported value.
// myapp/utils/formatter.js
define([
"dojo/_base/lang" // dependencies this module needs
], function(lang) {
// Private — not accessible outside this module
var _prefix = "USD ";
// Public API — what this module exports
return {
currency: function(amount) {
return _prefix + amount.toFixed(2);
},
truncate: function(str, maxLen) {
if (str.length <= maxLen) return str;
return str.substring(0, maxLen) + "...";
}
};
});
// myapp/models/User.js
define([
"dojo/_base/declare",
"dojo/Stateful"
], function(declare, Stateful) {
// Return the constructor directly — not wrapped in an object
return declare([Stateful], {
name: "",
email: "",
role: "viewer",
constructor: function(props) {
lang.mixin(this, props);
},
isAdmin: function() {
return this.role === "admin";
}
});
});
// myapp/config/constants.js — no dependencies needed
define({
API_BASE: "/api/v2",
PAGE_SIZE: 25,
DATE_FORMAT: "yyyy-MM-dd",
THEMES: ["claro", "tundra", "soria"]
});
// Note: define(object) — shorthand when factory function isn't needed
define()| Signature | Use When |
|---|---|
define(id, deps, factory) |
Named module — only used inside build layers. Never name modules in source files. |
define(deps, factory) |
Standard form — deps array + factory function. Use this 99% of the time. |
define(value) |
Simple export — a plain object or primitive. No factory needed. |
define("myapp/utils", [...], function() {...}) in your source files. Named modules cannot be moved or renamed without breaking the loader. The build system assigns names automatically during compilation.
require vs define — When to Use Eachrequire() | define() | |
|---|---|---|
| Purpose | Consume modules | Create a module |
| Used in | App bootstrap, inline scripts, HTML | Module files only (one per file) |
| Returns | undefined (async) | Registers the module |
| File has one? | Can have many require calls | Exactly one define per file |
dojoConfig — Loader ConfigurationdojoConfig (or data-dojo-config attribute) controls how the Dojo loader finds, caches, and loads modules. It must be defined before dojo.js is loaded.
// Full dojoConfig with all common options explained
var dojoConfig = {
// ── Core loader settings ──
async: true, // REQUIRED: use AMD loader (always true for Dojo 1.7+)
parseOnLoad: false, // ALWAYS false: parse declarative widgets manually
// ── Path resolution ──
baseUrl: "/js/", // base path for all relative module IDs
// default: the directory containing dojo.js
// ── Named packages ──
packages: [
// Built-in packages (dojo, dijit, dojox) are auto-registered
// Register YOUR app code:
{ name: "myapp", location: "/js/myapp" },
{ name: "widgets", location: "/js/shared-widgets" },
{ name: "vendor", location: "/js/vendor" }
],
// ── Path aliases (for individual files) ──
paths: {
"myapp/config": "/js/config/production" // override single module path
},
// ── Locale ──
locale: "en-us", // affects dojo/i18n bundles
// ── Debug ──
isDebug: false, // set true in dev: enables console logging from loader
// ── Cache busting ──
cacheBust: "v2.1.0", // appends ?v2.1.0 to every module URL (clears browser cache)
// ── Dojo module config (per-module settings) ──
config: {
"dojo/i18n": { locale: "fr" }
}
};
// Load dojo.js AFTER dojoConfig is defined
// <script src="/js/dojo/dojo.js"></script>
<!-- Alternative: set config directly on the script tag -->
<script
data-dojo-config="async:true, parseOnLoad:false, locale:'fr'"
src="dojo/dojo.js">
</script>
<!-- This form is useful for single-page apps where JS config isn't convenient -->
// Package config controls how module IDs map to file paths
var dojoConfig = {
packages: [
{
name: "myapp", // the package prefix in require/define calls
location: "myapp", // relative to baseUrl (or absolute path)
main: "main" // default module when you require("myapp") — optional
},
{
name: "lodash",
location: "../node_modules/lodash",
main: "lodash"
}
]
};
// With the above config:
// require("myapp/views/list") → loads /js/myapp/views/list.js
// require("lodash") → loads /node_modules/lodash/lodash.js
Loader plugins extend require() to load non-JavaScript resources. They use the plugin!resource syntax — the plugin module handles loading the resource and returns a usable value.
Load any text file (HTML, SVG, CSS). Returns the raw file contents as a string. Used for widget templates.
Load a locale-aware NLS bundle. Returns the correct strings for the user's locale.
Waits for DOM to be ready. No return value — used as the last dep when you need the DOM.
Feature detection. Conditionally loads different modules based on browser capabilities.
Load a specific CSS selector engine (acme, lite). Rarely needed directly.
Platform-aware XHR — loads the right transport for browser or Node.
dojo/text! — Template Loading// myapp/widgets/UserCard.js
define([
"dojo/_base/declare",
"dijit/_Widget",
"dijit/_TemplatedMixin",
"dojo/text!./templates/UserCard.html" // �? plugin loads the HTML file
], function(declare, _Widget, _TemplatedMixin, template) {
return declare([_Widget, _TemplatedMixin], {
templateString: template, // the loaded HTML string
name: "Unknown",
role: "viewer"
});
});
<!-- myapp/widgets/templates/UserCard.html -->
<div class="user-card ${role}">
<span class="name">${name}</span>
<span data-dojo-attach-point="roleNode"></span>
</div>
dojo/i18n! — Locale-Aware Strings// myapp/nls/messages.js (root bundle)
define({ root: {
greeting: "Hello",
save: "Save",
cancel: "Cancel",
deleteMsg: "Are you sure you want to delete this?"
}, fr: true, ja: true, de: true });
// myapp/nls/fr/messages.js (French override)
define({
greeting: "Bonjour",
save: "Enregistrer",
cancel: "Annuler",
deleteMsg: "Êtes-vous sûr de vouloir supprimer ceci ?"
});
// Usage in any widget
define([
"dojo/i18n!myapp/nls/messages"
], function(strings) {
console.log(strings.greeting); // "Bonjour" if locale is fr, "Hello" otherwise
});
dojo/domReady! — DOM-Dependent Coderequire([
"dijit/registry",
"dojo/parser",
"dojo/domReady!" // trailing ! = the bang. No return value (hence no arg name needed)
], function(registry, parser) {
// DOM is guaranteed to be fully parsed here
parser.parse(); // manually parse declarative widgets
// Safe to query the DOM
var widgets = registry.findWidgets(document.body);
console.log("Widgets found:", widgets.length);
});
dojo/has! — Feature-Based Loadingdefine([
// Load touch.js on touch devices, mouse.js on desktop
"dojo/has!touch?myapp/input/touch:myapp/input/mouse"
], function(inputHandler) {
// inputHandler is the right module for this device
inputHandler.init();
});
// Syntax: "dojo/has!feature?trueModule:falseModule"
// Multiple conditions:
// "dojo/has!ie?myapp/ie-shim:dojo/has!webkit?myapp/webkit-fix:myapp/default"
A well-structured Dojo app organizes code into discrete packages. Each package is a directory with a clear responsibility. Here's the recommended structure for a production app:
project/
├── index.html
├── js/
│ ├── dojo/ �? Dojo SDK (do not edit)
│ ├── dijit/ �? Dijit SDK (do not edit)
│ ├── dojox/ �? DojoX SDK (do not edit)
│ ├── myapp/ �? Your application logic
│ │ ├── main.js �? App entry point
│ │ ├── App.js �? Root widget
│ │ ├── views/
│ │ │ ├── Dashboard.js
│ │ │ └── UserList.js
│ │ ├── models/
│ │ │ └── User.js
│ │ ├── stores/
│ │ │ └── UserStore.js
│ │ └── nls/
│ │ └── messages.js
│ ├── widgets/ �? Shared/reusable widgets (used across apps)
│ │ ├── DataGrid.js
│ │ └── templates/
│ └── utils/ �? Utility functions
│ ├── formatter.js
│ └── validator.js
// dojoConfig for the above structure
var dojoConfig = {
async: true,
parseOnLoad: false,
baseUrl: "/js/",
packages: [
{ name: "dojo", location: "dojo" },
{ name: "dijit", location: "dijit" },
{ name: "dojox", location: "dojox" },
{ name: "myapp", location: "myapp", main: "main" },
{ name: "widgets", location: "widgets" },
{ name: "utils", location: "utils" }
]
};
// Now module IDs resolve cleanly:
// require("myapp/views/Dashboard") → /js/myapp/views/Dashboard.js
// require("widgets/DataGrid") → /js/widgets/DataGrid.js
// require("utils/formatter") → /js/utils/formatter.js
// myapp/main.js — app bootstrap
define([
"myapp/App",
"dojo/domReady!"
], function(App) {
// Create root app widget and place in body
new App().placeAt(document.body).startup();
});
// index.html — single require call kicks off everything
require(["myapp/main"]);
You can create your own loader plugin to handle any resource type. A plugin is an AMD module that exposes a load() function. The loader calls it when it encounters yourPlugin!resourceId.
// myapp/plugins/json.js — a plugin to load and parse JSON files
define(["dojo/request/xhr"], function(xhr) {
return {
// Called by the AMD loader when it sees: "myapp/plugins/json!path/to/file"
load: function(resourceId, require, onLoad, config) {
if (config.isBuild) {
// During build: just signal success (actual content inlined separately)
onLoad(null);
return;
}
// During runtime: fetch and parse the JSON
xhr.get(require.toUrl(resourceId + ".json"), {
handleAs: "json"
}).then(function(data) {
onLoad(data); // pass the parsed object to the requiring module
}, function(err) {
onLoad.error(err);
});
}
};
});
// Usage: any module can now load JSON via this plugin
define([
"myapp/plugins/json!data/config"
], function(config) {
console.log(config.apiBase); // parsed JSON object, ready to use
});
dojo/text! + manual parsing for one-offs.
Loading Dojo from a CDN works for prototyping but has important limitations in production.
<!-- CDN loading (prototyping only) -->
<script>
var dojoConfig = {
async: true,
parseOnLoad: false,
// When loading from CDN, your app package must use a full path
packages: [
{ name: "myapp", location: "http://myserver.com/js/myapp" }
]
};
</script>
<script src="//ajax.googleapis.com/ajax/libs/dojo/1.17.3/dojo/dojo.js"></script>
| Issue | Detail |
|---|---|
| No build layers | CDN serves individual files. You lose the benefit of bundled layers — hundreds of separate HTTP requests. |
Cross-origin dojo/text! |
Loading templates from CDN requires CORS headers on the CDN server. |
| Version lock | CDN URL bakes the version number. Upgrading requires changing every HTML file. |
| Offline / intranet | CDN unavailable = app broken. Enterprise apps must use local copies. |
// If your app loads AMD modules from a different origin,
// the server serving those modules must return CORS headers:
// Access-Control-Allow-Origin: https://yourapp.com
//
// The Dojo loader uses XHR to fetch module text in some configurations,
// which is subject to the same-origin policy.
//
// Best practice: serve all Dojo files from the same origin as your app,
// or use the build system to bundle everything before deployment.
Build a small app with 4 modules. The ChartView module should load only when the user clicks the "Load Chart" button — demonstrating true lazy loading.
Create these files:
js/
├── myapp/
│ ├── main.js �? bootstrap
│ ├── App.js �? root widget (has a button)
│ ├── views/
│ │ └── ChartView.js �? heavy module (lazy loaded)
│ └── utils/
│ └── logger.js �? simple utility
// myapp/utils/logger.js
define(function() {
return {
log: function(msg) {
console.log("[APP] " + new Date().toISOString() + " — " + msg);
}
};
});
// myapp/views/ChartView.js (simulated heavy module)
define(["myapp/utils/logger"], function(logger) {
logger.log("ChartView module loaded");
return {
render: function(container) {
container.innerHTML = "<p style='color:cyan'>Chart rendered!</p>";
}
};
});
// myapp/App.js
define([
"dojo/_base/declare",
"dijit/_Widget",
"dojo/on",
"dojo/dom-construct",
"myapp/utils/logger"
], function(declare, _Widget, on, domConstruct, logger) {
return declare([_Widget], {
buildRendering: function() {
this.domNode = domConstruct.create("div");
this.btn = domConstruct.create("button",
{ textContent: "Load Chart", className: "btn btn-primary" },
this.domNode
);
this.chartArea = domConstruct.create("div", {}, this.domNode);
},
postCreate: function() {
var self = this;
on(this.btn, "click", function() {
// Lazy load ChartView only on click
require(["myapp/views/ChartView"], function(ChartView) {
logger.log("ChartView loaded lazily");
ChartView.render(self.chartArea);
self.btn.disabled = true;
});
});
}
});
});
// myapp/main.js
define(["myapp/App", "dojo/domReady!"], function(App) {
new App().placeAt(document.body).startup();
});
// index.html script
require(["myapp/main"]);
A complete working structure for a production-style Dojo app with three distinct packages: application, shared widgets, and utilities.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="dojo/resources/dojo.css">
<link rel="stylesheet" href="dijit/themes/claro/claro.css">
<script>
var dojoConfig = {
async: true,
parseOnLoad: false,
baseUrl: "/js/",
isDebug: true, // remove in production
packages: [
{ name: "dojo", location: "dojo" },
{ name: "dijit", location: "dijit" },
{ name: "dojox", location: "dojox" },
{ name: "app", location: "app", main: "boot" },
{ name: "widgets", location: "widgets" },
{ name: "util", location: "util" }
]
};
</script>
<script src="/js/dojo/dojo.js"></script>
</head>
<body class="claro">
<div id="root"></div>
<script>require(["app"]);</script> <!-- loads app/boot.js via main config -->
</body>
</html>
// util/format.js — pure utility, no Dojo deps
define(function() {
return {
date: function(d) { return d.toLocaleDateString("en-US"); },
currency: function(n) { return "$" + n.toFixed(2); },
initials: function(name) {
return name.split(" ").map(function(w) { return w[0]; }).join("");
}
};
});
// widgets/Avatar.js — reusable across any app
define([
"dojo/_base/declare",
"dijit/_Widget",
"dijit/_TemplatedMixin",
"dojo/text!widgets/templates/Avatar.html",
"util/format"
], function(declare, _Widget, _TemplatedMixin, template, fmt) {
return declare([_Widget, _TemplatedMixin], {
templateString: template,
name: "User",
color: "#6366f1",
_initials: "",
postMixInProperties: function() {
this._initials = fmt.initials(this.name);
}
});
});
// app/boot.js — ties everything together
define([
"dojo/_base/declare",
"dijit/_Widget",
"widgets/Avatar",
"util/format",
"dojo/dom-construct",
"dojo/domReady!"
], function(declare, _Widget, Avatar, fmt, domConstruct) {
var users = [
{ name: "Alice Chen", role: "admin" },
{ name: "Bob Smith", role: "editor" },
{ name: "Carol Davis", role: "viewer" }
];
var container = domConstruct.byId("root");
users.forEach(function(user) {
var avatar = new Avatar({ name: user.name });
avatar.placeAt(container);
avatar.startup();
});
});
app/boot.js depends on widgets/Avatar and util/format, but it has no direct knowledge of how those packages are located on disk. The package config in dojoConfig handles all path resolution. This separation makes the code portable and testable.
| Concept | Key Rule |
|---|---|
require(deps, cb) | Consume modules — never name the return variable the same as the module ID |
define(deps, factory) | Create a module — one per file, never name it explicitly |
dojoConfig | Must be defined before dojo.js loads |
async: true | Always set. Required for AMD. |
parseOnLoad: false | Always set. Call parser.parse() manually. |
dojo/text! | Load HTML templates, returns string |
dojo/i18n! | Load locale bundles, returns strings object |
dojo/domReady! | Wait for DOM, no return value — use as last dep |
dojo/has! | Feature-detect conditional module loading |
| Lazy loading | Nest require() inside event handlers for on-demand loading |
| CDN | Prototyping only. Local copy required for production builds. |
Time: 5–6 hrs
Pages: 14–18
Exercises: 1
Demo apps: 1