dojo/_base/declareDojo's class system — single and multiple inheritance, the 8 widget lifecycle hooks, templating, and memory-safe teardown
declare?this.inherited(arguments)postCreate vs startupdestroy vs destroyRecursive_TemplatedMixin_WidgetsInTemplateMixindeclare?dojo/_base/declare is Dojo's answer to the boilerplate of raw JavaScript prototype inheritance. Instead of manually wiring up prototype chains, copying constructors, and setting up super calls, you describe your class as a plain object literal and let declare handle the rest.
| Raw JS Prototype | declare() equivalent |
|---|---|
|
|
Beyond cleaner syntax, declare gives you:
this.inherited() — call any ancestor's implementation of the current method without knowing the class namedeclaredClass — every instance knows its class name as a string, useful for debuggingdijit/_Widget, all 8 lifecycle methods are called automatically in order// declare(superclass, properties)
// superclass: a single class, or null for a base class
// properties: object literal — methods + default property values
var Shape = declare(null, {
color: "black",
sides: 0,
constructor: function(props) {
// props is the argument passed to new Shape({...})
// declare automatically mixes props into 'this'
// so you usually don't need to do anything here
// unless you need initialization logic
console.log("Shape created:", this.color);
},
describe: function() {
return "A " + this.color + " shape with " + this.sides + " sides";
},
area: function() {
return 0; // subclasses override this
}
});
var Rectangle = declare(Shape, {
sides: 4,
width: 0,
height: 0,
area: function() {
return this.width * this.height;
},
describe: function() {
// Call Shape.describe() then add our info
var base = this.inherited(arguments);
return base + " (" + this.width + "x" + this.height + ")";
}
});
var r = new Rectangle({ color: "red", width: 5, height: 3 });
console.log(r.describe()); // "A red shape with 4 sides (5x3)"
console.log(r.area()); // 15
console.log(r instanceof Rectangle); // true
console.log(r instanceof Shape); // true
console.log(r.declaredClass); // "Object" (unnamed) or "myapp/Rectangle" if named
constructor Hook// constructor() fires BEFORE the DOM exists
// Use it ONLY for:
// - initializing per-instance objects/arrays (avoid prototype trap)
// - storing constructor arguments for later use
// Do NOT use it for:
// - DOM queries (DOM doesn't exist yet)
// - calling methods that need the widget to be placed in the page
var MyWidget = declare([_Widget], {
constructor: function(params, srcNodeRef) {
// params: config object passed to new MyWidget({...})
// srcNodeRef: DOM node or id to replace (optional)
// Safe in constructor: initialize per-instance data
this._handles = [];
this._data = [];
// NOT safe in constructor: this.domNode doesn't exist yet
// this.domNode.style.color = "red"; �? will throw
}
});
When the first argument to declare is an array, the first element is the true superclass and the rest are mixins. Mixins contribute methods and properties but are not treated as full superclasses for instanceof checks.
// Define reusable mixin behaviors
var Serializable = declare(null, {
serialize: function() {
return JSON.stringify(this._getData());
},
_getData: function() {
return {}; // override in subclass
}
});
var Validatable = declare(null, {
isValid: function() {
return this._validate();
},
_validate: function() {
return true; // override in subclass
},
getErrors: function() {
return [];
}
});
// Combine: UserModel inherits from BaseModel and gets both mixins
var UserModel = declare([BaseModel, Serializable, Validatable], {
name: "",
email: "",
_getData: function() {
return { name: this.name, email: this.email };
},
_validate: function() {
return this.name.length > 0 && this.email.indexOf("@") > -1;
}
});
var user = new UserModel({ name: "Alice", email: "alice@example.com" });
console.log(user.isValid()); // true
console.log(user.serialize()); // '{"name":"Alice","email":"alice@example.com"}'
When multiple classes in the mixin chain define the same method, declare uses C3 linearization to determine which runs first. The rule is: left-to-right, depth-first, with the declared class's own method winning.
var A = declare(null, { greet: function() { return "A"; } });
var B = declare([A], { greet: function() { return "B+" + this.inherited(arguments); } });
var C = declare([A], { greet: function() { return "C+" + this.inherited(arguments); } });
var D = declare([B,C], { greet: function() { return "D+" + this.inherited(arguments); } });
var d = new D();
console.log(d.greet());
// "D+B+C+A"
// Order: D → B → C → A (C3 linearization)
this.inherited(arguments) in its overridden methods — even if it doesn't expect a parent. This ensures the full chain runs when the class is mixed into another.
| Mixin | What It Adds | Requires |
|---|---|---|
dijit/_Widget | Base lifecycle, domNode, startup/destroy | — |
dijit/_TemplatedMixin | HTML template processing, attach points/events | _Widget |
dijit/_WidgetsInTemplateMixin | Parse nested declarative widgets in template | _TemplatedMixin |
dijit/_CssStateMixin | Hover/active/focused CSS classes automatically | _Widget |
dijit/_FocusMixin | Focus/blur lifecycle hooks | _Widget |
dijit/_Container | addChild/removeChild API | _Widget |
dijit/_Contained | getParent() method | _Widget |
this.inherited(arguments)this.inherited(arguments) calls the same-named method in the nearest ancestor class. It uses the live arguments object to identify which method is currently running and passes the same arguments up the chain.
// Basic usage — pass arguments straight through
var Base = declare(null, {
init: function(config) {
this.config = config;
console.log("Base.init");
}
});
var Middle = declare(Base, {
init: function(config) {
console.log("Middle.init — before super");
this.inherited(arguments); // calls Base.init(config)
console.log("Middle.init — after super");
}
});
var Child = declare(Middle, {
init: function(config) {
console.log("Child.init");
this.inherited(arguments); // calls Middle.init(config) → Base.init(config)
}
});
new Child().init({ debug: true });
// Child.init
// Middle.init — before super
// Base.init
// Middle.init — after super
// Pass modified arguments to the parent
var MyButton = declare([dijit.form.Button], {
onClick: function(e) {
// Enrich the event before passing to parent handler
e.widgetSource = this.id;
this.inherited(arguments); // Button.onClick(e) — with our enriched event
}
});
// Or pass entirely different arguments:
var MyWidget = declare([_Widget], {
resize: function(newSize) {
// Double the size before passing to parent
this.inherited(arguments, [{ w: newSize.w * 2, h: newSize.h * 2 }]);
// ↑ second arg overrides what gets passed up
}
});
=>) do not have their own arguments object. If you use an arrow function as a widget method, this.inherited(arguments) will fail or call the wrong ancestor. Always use function() {} syntax for widget methods.
// Safe pattern when you're not sure if a parent implements the method
var result = this.inherited(arguments);
// If no parent has this method, inherited() returns undefined — no error thrown
// Alternatively, check explicitly:
if (this.inherited) {
this.inherited(arguments);
}
Every dijit/_Widget subclass goes through a precise sequence of lifecycle hooks. Understanding which hook fires when — and what the DOM state is at each point — is the single most important skill in Dojo widget development.
this.domNode is setthis.inherited(arguments) first so the template is processed before you manipulate nodes.this.domNode exists but widget is NOT yet in the documentstartup() hasn't run yet.preserveDom=true, the DOM node is kept. Always call this.inherited(arguments).destroy(). Called after all child widgets destroyed.// Complete widget with all lifecycle hooks annotated
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_Widget",
"dijit/_TemplatedMixin",
"dojo/on",
"dojo/topic",
"dojo/text!./templates/MyWidget.html"
], function(declare, lang, _Widget, _TemplatedMixin, on, topic, template) {
return declare([_Widget, _TemplatedMixin], {
templateString: template,
// Default properties (on prototype — safe for primitives)
label: "Default Label",
selected: false,
// ── Hook 1: constructor ──────────────────────────────────
constructor: function(params, srcNodeRef) {
this._handles = []; // per-instance array — SAFE
this._cache = {}; // per-instance object — SAFE
},
// ── Hook 2: postMixInProperties ─────────────────────────
postMixInProperties: function() {
this.inherited(arguments);
// label is now set from params — compute derived value for template
this.ariaLabel = this.label + " button";
},
// ── Hook 3: buildRendering ───────────────────────────────
buildRendering: function() {
this.inherited(arguments);
// domNode is now set — template is parsed
// attach points (data-dojo-attach-point) are wired
this.domNode.setAttribute("role", "button");
},
// ── Hook 4: postCreate ───────────────────────────────────
postCreate: function() {
this.inherited(arguments);
// Wire events — store handles for cleanup
this._handles.push(
on(this.domNode, "click", lang.hitch(this, "_onClick"))
);
this._handles.push(
topic.subscribe("app/reset", lang.hitch(this, "_onReset"))
);
},
// ── Hook 6: startup ──────────────────────────────────────
startup: function() {
if (this._started) return; // guard — startup should only run once
this.inherited(arguments);
// Safe to use offsetWidth here
this._initialWidth = this.domNode.offsetWidth;
},
// ── Private methods ──────────────────────────────────────
_onClick: function(e) {
this.set("selected", !this.selected);
this.emit("change", { selected: this.selected });
},
_onReset: function() {
this.set("selected", false);
},
// ── Hook 7: destroy ──────────────────────────────────────
destroy: function(preserveDom) {
// Remove ALL stored handles
this._handles.forEach(function(h) { h.remove(); });
this._handles = [];
this._cache = null;
this.inherited(arguments); // MUST call — cleans up dijit registry
}
});
});
postCreate vs startupThis is the most common source of layout bugs in Dojo applications. The rule is simple but easy to forget:
postCreate() | startup() | |
|---|---|---|
| DOM exists? | Yes — this.domNode is ready | Yes — widget is in the live document |
| In document? | No — widget not yet placed | Yes — after placeAt() |
| offsetWidth valid? | No — always 0 | Yes — browser has laid out |
| Child widgets started? | No | Yes — startup() cascades to children |
| Use for | Event wiring, initial DOM setup | Sizing, child widget calls, scroll setup |
// WRONG — checking size in postCreate
postCreate: function() {
this.inherited(arguments);
var w = this.domNode.offsetWidth; // always 0 — widget not in page yet
this._setColumnWidths(w); // bug: calculates with 0
},
// CORRECT — size-dependent logic in startup
postCreate: function() {
this.inherited(arguments);
on(this.domNode, "click", lang.hitch(this, "_onClick")); // event: OK here
},
startup: function() {
if (this._started) return;
this.inherited(arguments);
var w = this.domNode.offsetWidth; // correct — widget is in the page
this._setColumnWidths(w);
}
startup(). You must call it after placeAt():new MyWidget({...}).placeAt("container").startup();BorderContainer, TabContainer, etc. calls startup(), it cascades to all child widgets. But when YOU place a widget manually, you must call startup() yourself.
destroy vs destroyRecursiveFailing to destroy widgets is the most common source of memory leaks in large Dojo applications. Every widget you create programmatically that is later removed from the page must be explicitly destroyed.
| Method | What It Does | When to Use |
|---|---|---|
destroy() |
Destroys this widget only. Child widgets remain alive. | Leaf widgets with no children. |
destroyRecursive() |
Destroys this widget AND all child widgets in its subtree. | Container widgets (BorderContainer, TabContainer, any widget with children). Use this by default. |
destroyDescendants() |
Destroys child widgets only — keeps this widget alive. | Refreshing a container's contents without destroying the container itself. |
// Common scenario: replace a tab's content
function refreshTab(tabContainer) {
var tab = dijit.byId("dataTab");
// Destroy old content BEFORE adding new content
tab.destroyDescendants(); // keeps the tab, destroys its children
// Or destroy the entire tab and recreate:
// tab.destroyRecursive();
// tabContainer.addChild(new ContentPane({...}));
}
// Scenario: a dialog that creates widgets inside
var dlg = new Dialog({...});
dlg.show();
// When done — destroyRecursive to get widgets inside the dialog too
dlg.on("hide", function() {
dlg.destroyRecursive();
});
// Registry check — after destroy, widget should not be in registry
var widget = new MyWidget({id: "myW"}).placeAt("body").startup();
widget.destroyRecursive();
console.log(dijit.byId("myW")); // undefined — correctly removed from registry
container.innerHTML = ""; — removes DOM but leaves widgets alive in the registry.on(node, "click", handler) without storing and removing the handle.topic.subscribe(...) without storing and calling handle.remove() in destroy.
_TemplatedMixindijit/_TemplatedMixin lets you define your widget's DOM structure as an HTML string (usually loaded with dojo/text!). It processes the template, substitutes ${property} values, and wires up attach points and attach events automatically.
<!-- myapp/templates/UserCard.html -->
<!-- Rules:
1. Must have exactly ONE root element
2. ${prop} = one-time substitution at create time (not reactive)
3. data-dojo-attach-point = creates this.nameRef property on the widget
4. data-dojo-attach-event = auto-wires event to widget method
-->
<div class="user-card ${extraClass}"
data-dojo-attach-point="containerNode">
<div class="user-avatar"
style="background-color: ${avatarColor}">
<span data-dojo-attach-point="initialsNode">${initials}</span>
</div>
<div class="user-info">
<h3 data-dojo-attach-point="nameNode">${name}</h3>
<p data-dojo-attach-point="roleNode">${role}</p>
</div>
<button data-dojo-attach-point="editBtn"
data-dojo-attach-event="onclick:_onEdit">
Edit
</button>
</div>
// myapp/widgets/UserCard.js
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_Widget",
"dijit/_TemplatedMixin",
"dojo/text!./templates/UserCard.html"
], function(declare, lang, _Widget, _TemplatedMixin, template) {
return declare([_Widget, _TemplatedMixin], {
templateString: template,
// These match ${...} substitutions in the template
name: "Unknown User",
role: "viewer",
extraClass: "",
avatarColor: "#6366f1",
initials: "?",
postMixInProperties: function() {
this.inherited(arguments);
// Compute initials before template renders
if (this.name && this.name !== "Unknown User") {
this.initials = this.name
.split(" ")
.map(function(w) { return w[0].toUpperCase(); })
.join("");
}
},
postCreate: function() {
this.inherited(arguments);
// attach points are now available as widget properties:
// this.containerNode — the root div
// this.initialsNode — the span inside avatar
// this.nameNode — the h3
// this.roleNode — the p
// this.editBtn — the button
// (data-dojo-attach-event already wired _onEdit to button click)
if (this.role === "admin") {
this.containerNode.classList.add("is-admin");
}
},
// Called by data-dojo-attach-event
_onEdit: function(e) {
e.stopPropagation();
this.emit("edit", { user: { name: this.name, role: this.role } });
},
// Reactive property update — called when set("name", newValue)
_setNameAttr: function(name) {
this._set("name", name); // updates this.name
if (this.nameNode) { // guard: may not exist if called before postCreate
this.nameNode.textContent = name;
}
}
});
});
${prop} Substitution vs _setXxxAttr| Mechanism | When It Runs | Reactive? |
|---|---|---|
${name} in template | Once, at widget creation time | No — static |
_setNameAttr method | Every time widget.set("name", v) is called | Yes — live updates |
Use ${prop} for values that never change after creation (CSS classes, ARIA roles). Use _setXxxAttr for values that the app will update at runtime.
_WidgetsInTemplateMixinWhen your template contains declarative Dojo widgets (elements with data-dojo-type), add _WidgetsInTemplateMixin. It runs the parser on your template so nested widgets are instantiated as part of your widget's creation.
<!-- templates/SearchBar.html -->
<div class="search-bar">
<!-- Nested Dijit widget inside this widget's template -->
<input data-dojo-type="dijit/form/ValidationTextBox"
data-dojo-props="placeholder:'Search...', required:true"
data-dojo-attach-point="inputWidget" />
<button data-dojo-type="dijit/form/Button"
data-dojo-props="label:'Go', iconClass:'dijitIconSearch'"
data-dojo-attach-point="searchBtn"
data-dojo-attach-event="onClick:_onSearch">
</button>
</div>
define([
"dojo/_base/declare",
"dijit/_Widget",
"dijit/_TemplatedMixin",
"dijit/_WidgetsInTemplateMixin", // �? ADD this mixin
"dojo/text!./templates/SearchBar.html",
"dijit/form/ValidationTextBox", // �? MUST require nested widget types
"dijit/form/Button"
], function(declare, _Widget, _TemplatedMixin, _WidgetsInTemplateMixin,
template, ValidationTextBox, Button) {
return declare([_Widget, _TemplatedMixin, _WidgetsInTemplateMixin], {
templateString: template,
postCreate: function() {
this.inherited(arguments);
// this.inputWidget — the ValidationTextBox INSTANCE (not a DOM node)
// this.searchBtn — the Button INSTANCE
},
startup: function() {
if (this._started) return;
this.inherited(arguments);
// Start nested widgets (required for layout widgets inside template)
this.inputWidget.startup();
this.searchBtn.startup();
},
_onSearch: function() {
if (this.inputWidget.isValid()) {
this.emit("search", { query: this.inputWidget.get("value") });
}
},
destroy: function(preserveDom) {
// destroyRecursive handles nested widgets automatically
// but if you stored extra references, clean them up here
this.inherited(arguments);
}
});
});
require() those modules in your define() dependencies array. Otherwise the parser won't find them when it processes the template.
A real-world migration showing the same "ExpandablePanel" component — first as a jQuery plugin, then rewritten as a proper Dijit widget.
// jQuery version — problems: no lifecycle, no cleanup, no registry,
// no proper events, no property system
$.fn.expandablePanel = function(options) {
var settings = $.extend({ title: "Panel", collapsed: false }, options);
return this.each(function() {
var $el = $(this);
var $header = $("<div class='panel-header'>" + settings.title + "</div>");
var $body = $("<div class='panel-body'>").append($el.children());
$el.empty().append($header).append($body);
if (settings.collapsed) $body.hide();
$header.on("click", function() {
$body.toggle();
$el.trigger("toggle", [$body.is(":visible")]);
});
// No cleanup. No way to find this "widget" later. No property updates.
});
};
// Dijit version — full lifecycle, registry, events, property system
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_Widget",
"dijit/_TemplatedMixin",
"dojo/on",
"dojo/dom-class",
"dojo/text!./templates/ExpandablePanel.html"
], function(declare, lang, _Widget, _TemplatedMixin, on, domClass, template) {
return declare([_Widget, _TemplatedMixin], {
templateString: template,
// Public API — settable via constructor or set()
title: "Panel",
collapsed: false,
postCreate: function() {
this.inherited(arguments);
this._handles.push(
on(this.headerNode, "click", lang.hitch(this, "_onHeaderClick"))
);
if (this.collapsed) {
domClass.add(this.domNode, "is-collapsed");
}
},
constructor: function() {
this._handles = [];
},
_onHeaderClick: function() {
this.set("collapsed", !this.collapsed);
},
// Reactive setter — fires when set("collapsed", value) is called
_setCollapsedAttr: function(val) {
this._set("collapsed", val);
domClass.toggle(this.domNode, "is-collapsed", val);
// Emit a Dojo event (listeners use widget.on("toggle", ...))
this.emit("toggle", { collapsed: val });
},
// Reactive setter for title
_setTitleAttr: { node: "titleNode", type: "innerHTML" },
// shorthand: automatically sets titleNode.innerHTML = value
destroy: function(preserveDom) {
this._handles.forEach(function(h) { h.remove(); });
this.inherited(arguments);
}
});
});
// Usage — clean, discoverable, proper lifecycle
var panel = new ExpandablePanel({
title: "Server Stats",
collapsed: false
}).placeAt("sidebarArea");
panel.startup();
// Listen to events cleanly
panel.on("toggle", function(e) {
console.log("Panel is now", e.collapsed ? "collapsed" : "expanded");
});
// Later: update properties reactively
panel.set("title", "Updated Title");
panel.set("collapsed", true);
// Find it later from anywhere
var samePanel = dijit.byId(panel.id);
// When done — proper cleanup
panel.destroyRecursive();
Build a ProfileCard widget that logs a message at every lifecycle hook so you can see the exact order. It should have:
selected state and emits a select eventdestroy()_setSelectedAttr that visually updates the card's appearance reactively// templates/ProfileCard.html
// <div class="profile-card" data-dojo-attach-point="containerNode">
// <div class="avatar" data-dojo-attach-point="avatarNode">${initials}</div>
// <div class="info">
// <h4 data-dojo-attach-point="nameNode">${name}</h4>
// <p data-dojo-attach-point="roleNode">${role}</p>
// </div>
// <button data-dojo-attach-point="selectBtn"
// data-dojo-attach-event="onclick:_onSelect">Select</button>
// </div>
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_Widget",
"dijit/_TemplatedMixin",
"dojo/dom-class",
"dojo/text!./templates/ProfileCard.html"
], function(declare, lang, _Widget, _TemplatedMixin, domClass, template) {
return declare([_Widget, _TemplatedMixin], {
templateString: template,
name: "Unknown",
role: "viewer",
selected: false,
initials: "?",
constructor: function() {
console.log("[1] constructor — no DOM yet");
this._handles = [];
},
postMixInProperties: function() {
this.inherited(arguments);
this.initials = this.name.split(" ").map(function(w){ return w[0]; }).join("");
console.log("[2] postMixInProperties — initials:", this.initials);
},
buildRendering: function() {
this.inherited(arguments);
console.log("[3] buildRendering — domNode:", this.domNode.tagName);
},
postCreate: function() {
this.inherited(arguments);
console.log("[4] postCreate — nameNode:", this.nameNode.textContent);
console.log(" offsetWidth:", this.domNode.offsetWidth); // 0 — not in page
},
startup: function() {
if (this._started) return;
this.inherited(arguments);
console.log("[6] startup — offsetWidth:", this.domNode.offsetWidth); // real value
},
_onSelect: function() {
this.set("selected", !this.selected);
},
_setSelectedAttr: function(val) {
this._set("selected", val);
domClass.toggle(this.containerNode, "is-selected", val);
this.selectBtn.textContent = val ? "Deselect" : "Select";
this.emit("select", { selected: val, name: this.name });
},
destroy: function(preserveDom) {
console.log("[7] destroy — cleaning up", this._handles.length, "handles");
this._handles.forEach(function(h) { h.remove(); });
this.inherited(arguments);
}
});
});
| Concept | Key Rule |
|---|---|
declare(null, {...}) | Base class — no superclass |
declare(Super, {...}) | Single inheritance |
declare([Super, M1, M2], {...}) | Multiple inheritance — first is base, rest are mixins |
this.inherited(arguments) | Call parent's same method — always pass arguments (never an array) |
| Objects/arrays on prototype | Initialize in constructor(), never on the class body |
postCreate | Event wiring — DOM exists but widget NOT in page |
startup | Size logic — widget IS in page. MUST call manually after placeAt() |
destroy() | Leaf widgets. Always call this.inherited(arguments) |
destroyRecursive() | Container widgets. Use by default. |
${prop} | One-time substitution at creation time — not reactive |
_setXxxAttr | Reactive setter — fires on every widget.set("xxx", v) |
data-dojo-attach-point | Creates this.nodeRef on widget |
data-dojo-attach-event | Auto-wires DOM event to widget method |
_WidgetsInTemplateMixin | Required when template has data-dojo-type — must also require() nested types |
Time: 7–9 hrs
Pages: 20–25
Exercises: 1
Case studies: 1