dojo/dom, dojo/on, dojo/query, NodeList, dojo/store — the complete toolkit for working with the DOM and live data
dojo/dom — Core DOM Utilitiesdojo/dom-class — CSS Class Manipulationdojo/dom-style — Style Reads & Writesdojo/dom-construct — Create & Placedojo/on — Event Bindingdojo/on vs Legacy dojo.connectdojo/query + NodeListdojo/store/Memory — CRUDdojo/store/Observable — Live Queriesdojo/Stateful — Property Watchingdojo/dom — Core DOM UtilitiesDojo wraps basic DOM operations in cross-browser modules. In modern browsers (IE9+) you could use native APIs directly, but using Dojo's wrappers keeps your code testable and consistent across the codebase.
Same as document.getElementById. Accepts string or DOM node (pass-through).
Returns true if node a is inside node b.
Enable/disable text selection on a node. Used in drag handles.
dojo/dom-attr — cross-browser attribute & property access.
require([
"dojo/dom",
"dojo/dom-attr",
"dojo/dom-geometry"
], function(dom, domAttr, domGeom) {
// byId — accepts string id or existing DOM node (safe pass-through)
var el = dom.byId("myPanel");
// dom-attr: get/set HTML attributes and DOM properties
domAttr.set(el, "aria-expanded", "true");
domAttr.set(el, { "data-role": "panel", tabIndex: 0 });
var role = domAttr.get(el, "data-role"); // "panel"
domAttr.remove(el, "hidden");
// dom-geometry: position and size (layout-safe)
var pos = domGeom.position(el); // { x, y, w, h }
var box = domGeom.getContentBox(el); // { l, t, w, h } — content area
var mbox = domGeom.getMarginBox(el); // includes margin
console.log("Element is", box.w, "x", box.h, "at", pos.x, pos.y);
});
dom.byId("myWidget") returns the DOM node.dijit.byId("myWidget") returns the widget instance..set() on a DOM node fails; calling .style on a widget instance returns undefined.
dojo/dom-class — CSS Class Manipulationrequire(["dojo/dom-class", "dojo/dom"], function(domClass, dom) {
var el = dom.byId("statusBar");
// add / remove / contains
domClass.add(el, "is-active");
domClass.remove(el, "is-loading");
var hasError = domClass.contains(el, "has-error"); // boolean
// toggle — add if absent, remove if present
domClass.toggle(el, "is-expanded");
// toggle with explicit force value (like classList.toggle(cls, bool))
domClass.toggle(el, "is-selected", true); // always add
domClass.toggle(el, "is-selected", false); // always remove
// replace — swap one class for another atomically
domClass.replace(el, "status-success", "status-error");
// Add multiple classes at once
domClass.add(el, ["card", "card-dark", "elevated"]);
});
Using dojo/dom-class instead of raw className manipulation is important because:
dojo/dom-style — Style Reads & Writesrequire(["dojo/dom-style", "dojo/dom"], function(domStyle, dom) {
var el = dom.byId("panel");
// Set a single style
domStyle.set(el, "display", "none");
// Set multiple styles at once (object form)
domStyle.set(el, {
opacity: 0.8,
transform: "translateX(0)",
transition: "transform 0.3s ease"
});
// Get a COMPUTED style value (not the inline style — the actual rendered value)
var opacity = domStyle.get(el, "opacity");
var display = domStyle.get(el, "display"); // "block", "flex", etc.
console.log("opacity:", opacity); // 0.8
console.log("display:", display); // computed value, not inline value
});
el.style only reads/writes inline styles. domStyle.get() reads the computed style — the actual rendered value after CSS cascade. Always use domStyle.get() when you need to know what the element actually looks like on screen.
// For plain DOM nodes:
domStyle.set(el, "display", "none");
domStyle.set(el, "display", "block");
// For Dijit widgets — NEVER manipulate domNode.style directly for visibility
// The widget may need to recalculate layout when shown
var widget = dijit.byId("myPanel");
widget.set("style", "display:none");
// or better: use the widget's own hide/show if it has one
// e.g., dijit/layout/ContentPane has no built-in hide, use domStyle on its domNode
// but ALWAYS call layout() on parent containers after showing a layout widget
dojo/dom-construct — Create & Placerequire(["dojo/dom-construct", "dojo/dom"], function(domConstruct, dom) {
// create(tag, attrs, refNode, position)
var li = domConstruct.create("li", {
innerHTML: "New Item",
className: "list-item",
"data-id": "42"
}, dom.byId("myList"), "last");
// position: "last"(default), "first", "before", "after", "replace", "only"
// place — move an existing node
domConstruct.place(li, dom.byId("otherList"), "first");
// toDom — parse an HTML string into a DOM node/fragment
var frag = domConstruct.toDom(
"<div class='card'><h3>Title</h3><p>Body text</p></div>"
);
domConstruct.place(frag, dom.byId("container"), "last");
// destroy — remove node AND its children from DOM (and memory)
domConstruct.destroy(li);
// empty — remove all children but keep the node itself
domConstruct.empty(dom.byId("myList"));
});
// Build a card list from data — use a DocumentFragment for performance
var data = [
{ id: 1, name: "Alice", dept: "Engineering" },
{ id: 2, name: "Bob", dept: "Marketing" }
];
var container = dom.byId("cardContainer");
var frag = document.createDocumentFragment();
data.forEach(function(person) {
var card = domConstruct.toDom(
'<div class="person-card" data-id="' + person.id + '">' +
'<strong>' + person.name + '</strong>' +
'<span class="dept">' + person.dept + '</span>' +
'</div>'
);
frag.appendChild(card);
});
// Single DOM insertion — one reflow instead of N
container.appendChild(frag);
dojo/on — Event Bindingdojo/on is the modern Dojo event system. It wraps addEventListener with cross-browser normalization and returns a handle object you can remove later.
require([
"dojo/on",
"dojo/dom",
"dojo/_base/lang"
], function(on, dom, lang) {
var btn = dom.byId("submitBtn");
// Basic binding — returns a handle
var handle = on(btn, "click", function(e) {
e.preventDefault();
console.log("clicked");
});
// Remove listener — ALWAYS store handles in widgets
handle.remove();
// Multiple events on one node
var handles = [
on(btn, "click", lang.hitch(widget, "_onClick")),
on(btn, "mouseover", lang.hitch(widget, "_onHover")),
on(btn, "keydown", lang.hitch(widget, "_onKey"))
];
// Remove all at once
handles.forEach(function(h) { h.remove(); });
// Once — fires exactly once, then auto-removes
on.once(btn, "click", function(e) {
initializeOnFirstClick();
});
// Emit a synthetic event
on.emit(btn, "click", { bubbles: true, cancelable: true });
});
// WRONG — attaching a listener to every list item
// If the list has 500 items → 500 event listeners
query(".list-item").forEach(function(item) {
on(item, "click", handler); // expensive, breaks when items are added
});
// CORRECT — single delegated listener on the parent
// Works for items added dynamically; one listener for all items
var list = dom.byId("employeeList");
on(list, on.selector(".list-item", "click"), function(e) {
// 'this' is the matched .list-item element
var id = domAttr.get(this, "data-id");
loadEmployee(id);
});
// Selector-based delegation syntax:
// on(parent, on.selector(cssSelector, eventName), handler)
// Widgets emit custom events via this.emit()
// Listen to them the same way as DOM events
var myCard = new ProfileCard({ name: "Alice" }).placeAt("app");
myCard.startup();
// Listen to a widget's custom event
myCard.on("select", function(e) {
console.log("Selected:", e.name, "→", e.selected);
});
// Emit from inside the widget:
// this.emit("select", { name: this.name, selected: true });
// The event bubbles up through the DOM like a native event
dojo/on vs Legacy dojo.connectLegacy Dojo code uses dojo.connect() and dojo.disconnect(). You will encounter these in older codebases. Understand the differences so you can safely migrate or interop with them.
dojo/on (modern) | dojo.connect (legacy) | |
|---|---|---|
| Target | DOM nodes, widgets, any event emitter | Any object method — DOM or non-DOM |
| Cleanup | handle.remove() | dojo.disconnect(handle) |
| Delegation | Yes — on.selector() | No |
| Once | Yes — on.once() | No |
| Scope of "this" | Event target (DOM) or emitter | Explicitly specified or global |
| Non-DOM method hooks | No | Yes — can hook into any object method |
// Legacy dojo.connect — hooking any object method (not just DOM events)
var handle = dojo.connect(myStore, "onSet", function(item, attr, oldVal, newVal) {
console.log("Store item changed:", attr, oldVal, "→", newVal);
});
dojo.disconnect(handle);
// Modern equivalent using dojo/aspect (for method hooking)
require(["dojo/aspect"], function(aspect) {
var handle = aspect.after(myStore, "put", function(result) {
console.log("Store.put completed:", result);
return result; // must return in aspect.after
});
handle.remove();
});
// Rule: use dojo/on for events, dojo/aspect for method interception
Every dojo/on call, every topic.subscribe(), every store.query().observe() returns a handle. Failing to remove these handles when a widget is destroyed is the primary source of memory leaks in Dojo apps.
// Pattern 1: _handles array (most common in widgets)
var MyWidget = declare([_Widget], {
constructor: function() {
this._handles = [];
},
postCreate: function() {
this.inherited(arguments);
this._handles.push(
on(this.domNode, "click", lang.hitch(this, "_onClick")),
on(window, "resize", lang.hitch(this, "_onResize")),
topic.subscribe("data/changed", lang.hitch(this, "_onDataChanged"))
);
},
destroy: function(preserveDom) {
this._handles.forEach(function(h) { h.remove(); });
this._handles = [];
this.inherited(arguments);
}
});
// Pattern 2: own() — built into dijit/_Widget
// Automatically removes handles when widget is destroyed
// No need for manual _handles array
var MyWidget = declare([_Widget], {
postCreate: function() {
this.inherited(arguments);
// own() returns the handle AND registers it for auto-cleanup
this.own(
on(this.domNode, "click", lang.hitch(this, "_onClick")),
topic.subscribe("app/reset", lang.hitch(this, "_reset"))
);
// When destroyRecursive() is called, all own()'d handles are removed
}
// No destroy() override needed for these handles!
});
this.own() for handles inside widgets — it's cleaner and safer than maintaining a manual _handles array. Use the manual array pattern only when you need to remove specific handles before the widget is destroyed.
dojo/promise/all style// When you want to remove a GROUP of handles together
require(["dojo/_base/lang"], function(lang) {
function makeHandleGroup(handles) {
return {
remove: function() {
handles.forEach(function(h) { h.remove(); });
handles = [];
}
};
}
var searchHandles = makeHandleGroup([
on(searchInput, "input", onSearchInput),
on(searchInput, "keydown", onSearchKeydown),
on(clearBtn, "click", onClearClick)
]);
// Later — remove the whole group atomically
searchHandles.remove();
});
dojo/query + NodeListdojo/query returns a NodeList — an array-like collection of DOM nodes with chainable methods for batch operations. Think of it as Dojo's jQuery-style DOM selector.
require(["dojo/query"], function(query) {
// Basic CSS selector → NodeList
var items = query(".list-item");
console.log(items.length); // number of matched nodes
// Scoped query — search within a subtree
var container = dom.byId("userList");
var activeItems = query(".list-item.is-active", container);
// NodeList is array-like — use forEach, filter, map
items.forEach(function(node, index) {
console.log(index, node.textContent);
});
// filter returns a new NodeList
var highlighted = items.filter(function(node) {
return domClass.contains(node, "highlight");
});
// at(index) — returns NodeList with single element
var first = items.at(0);
// Chaining (requires NodeList-dom plugin loaded)
query(".card")
.addClass("animated")
.style("opacity", "0")
.forEach(function(node) { fadeIn(node); });
});
// dojo/query supports the full CSS3 selector spec
query("table.data-table > tbody > tr:nth-child(odd)").addClass("alt-row");
query("input[type='checkbox']:checked").forEach(collectSelected);
query(".panel:not(.collapsed) .panel-body").style("display", "block");
query("[data-role='admin']").addClass("admin-highlight");
// Attribute selectors
query("[data-status='pending']").addClass("needs-attention");
query("[data-id]").forEach(function(node) {
console.log(domAttr.get(node, "data-id"));
});
NodeList methods are extended by requiring plugin modules. Each plugin adds a new set of methods to every NodeList in your module.
| Plugin Module | Methods Added | Use For |
|---|---|---|
dojo/NodeList-dom | addClass, removeClass, toggleClass, style, attr, place, orphan, adopt, empty, destroy | DOM manipulation on query results |
dojo/NodeList-traverse | children, parent, parents, siblings, next, prev, first, last, closest | Navigating the DOM tree |
dojo/NodeList-manipulate | innerHTML, text, val, append, prepend, before, after, wrap, wrapAll, clone | Content manipulation |
dojo/NodeList-events | on, connect | Event binding on query results |
dojo/NodeList-fx | fadeIn, fadeOut, slideTo, wipeIn, wipeOut | Animations on query results |
// Load plugins once — methods available on all NodeLists in this module
require([
"dojo/query",
"dojo/NodeList-dom",
"dojo/NodeList-traverse",
"dojo/NodeList-manipulate"
], function(query) {
// NodeList-dom: addClass, style, attr
query(".card")
.addClass("visible")
.style({ opacity: 1, transform: "translateY(0)" });
// NodeList-traverse: parent, children, siblings
query(".selected-row")
.parent() // → table tbody
.children(".data-row") // → all sibling rows
.removeClass("highlight");
query(".error-input")
.closest(".form-group") // → containing form group
.addClass("has-error");
// NodeList-manipulate: text, append, clone
query(".status-badge").text("Loading...");
query(".template-row")
.clone() // copy the template row
.attr("data-id", newItem.id)
.find(".cell-name")
.text(newItem.name)
.end() // back to the cloned row
.place(dom.byId("tbody"), "last");
});
dojo/store/Memory — CRUD Operationsdojo/store/Memory is an in-memory data store implementing the dojo/store API. It provides a consistent interface for querying and modifying data — the same API that dgrid, dijit/form/Select, and other widgets consume.
require(["dojo/store/Memory"], function(Memory) {
var store = new Memory({
idProperty: "id", // which field is the unique key (default: "id")
data: [
{ id: 1, name: "Alice Chen", dept: "Engineering", salary: 95000, active: true },
{ id: 2, name: "Bob Smith", dept: "Marketing", salary: 72000, active: true },
{ id: 3, name: "Carol Davis", dept: "Engineering", salary: 105000, active: false },
{ id: 4, name: "Dave Wilson", dept: "HR", salary: 68000, active: true }
]
});
// ── READ ────────────────────────────────────────────────────────
var alice = store.get(1); // { id:1, name:"Alice Chen", ... }
var all = store.query({}); // all records
var eng = store.query({ dept: "Engineering" }); // filtered
var active = store.query({ active: true });
// Query with sort
var sorted = store.query({}, {
sort: [{ attribute: "salary", descending: true }]
});
// Query with pagination
var page1 = store.query({}, { start: 0, count: 10 });
var page2 = store.query({}, { start: 10, count: 10 });
// Custom filter function
var highEarners = store.query(function(item) {
return item.salary > 90000 && item.active;
});
// forEach on query results
eng.forEach(function(emp) {
console.log(emp.name, emp.salary);
});
// ── CREATE ───────────────────────────────────────────────────────
store.add({
id: 5, name: "Eve Martinez", dept: "Engineering", salary: 88000, active: true
});
// add() throws if id already exists; put() upserts
// ── UPDATE ───────────────────────────────────────────────────────
store.put({ id: 1, name: "Alice Chen", dept: "Engineering", salary: 98000, active: true });
// put() replaces the entire object — include ALL fields
// Partial update pattern (fetch → merge → put)
var alice = store.get(1);
alice.salary = 98000;
store.put(alice);
// ── DELETE ───────────────────────────────────────────────────────
store.remove(2); // removes item with id 2
console.log("Total count:", store.query({}).total);
});
// SimpleQueryEngine (default) supports: exact match, regex, function
// For advanced queries, replace the queryEngine:
require(["dojo/store/Memory", "dojo/store/util/SimpleQueryEngine"],
function(Memory, SimpleQueryEngine) {
var store = new Memory({
data: employees,
queryEngine: function(query, options) {
// Custom engine: support partial string match on 'name'
return SimpleQueryEngine(function(item) {
if (typeof query === "string") {
return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1;
}
return SimpleQueryEngine(query)(item);
}, options);
}
});
// Now string queries do partial name match
store.query("ali"); // matches "Alice Chen"
});
dojo/store/Observable — Live Query ResultsObservable wraps any store and makes query results live. When you call observe() on a query result, your callback fires automatically whenever an item matching the query is added, updated, or removed.
require([
"dojo/store/Memory",
"dojo/store/Observable"
], function(Memory, Observable) {
// Wrap store with Observable
var store = Observable(new Memory({ data: employees }));
// Query returns a live result set
var engQuery = store.query({ dept: "Engineering" });
// observe(callback, includeObjectUpdates)
var observeHandle = engQuery.observe(function(item, removedFrom, insertedAt) {
//
// removedFrom: index where item was REMOVED (-1 if not removed)
// insertedAt: index where item was INSERTED (-1 if not inserted)
//
// Case 1: New item added to query results
if (removedFrom === -1 && insertedAt >= 0) {
console.log("Added to Engineering:", item.name, "at index", insertedAt);
renderNewRow(item, insertedAt);
}
// Case 2: Item removed from query results (deleted or dept changed)
else if (removedFrom >= 0 && insertedAt === -1) {
console.log("Removed from Engineering:", item.name);
removeRow(item.id);
}
// Case 3: Item updated and still in query results (moved position)
else if (removedFrom >= 0 && insertedAt >= 0) {
console.log("Updated in Engineering:", item.name);
updateRow(item);
}
}, true); // true = also notify on property changes within matched items
// Mutations trigger the observe callback automatically:
store.add({ id: 5, name: "Eve", dept: "Engineering", salary: 88000 });
// → callback fires: removedFrom=-1, insertedAt=2
store.put({ id: 1, name: "Alice Chen", dept: "Marketing", salary: 95000 });
// → callback fires: removedFrom=0, insertedAt=-1 (moved OUT of Engineering)
store.remove(3);
// → callback fires for Carol if she was in Engineering
// Always clean up the observe handle
observeHandle.remove();
});
observe() to your render function and your list always reflects the store state automatically.
dojo/Stateful — Property Watchingdojo/Stateful is a mixin that adds a watch() method to any object. When a watched property changes via set(), all watchers are notified. This is Dojo's observable object pattern — used by all Dijit widgets.
require(["dojo/Stateful", "dojo/_base/declare"], function(Stateful, declare) {
// Simple Stateful object
var model = new Stateful({
name: "Alice",
status: "active",
score: 0
});
// watch(propName, callback) — fires when property changes
var nameWatcher = model.watch("name", function(propName, oldVal, newVal) {
console.log("name changed:", oldVal, "→", newVal);
document.title = newVal + " | Dashboard";
});
var scoreWatcher = model.watch("score", function(prop, old, newVal) {
if (newVal > 100) {
model.set("status", "expert"); // triggers status watchers too
}
});
// watch all properties
var allWatcher = model.watch(function(propName, oldVal, newVal) {
console.log("[model]", propName, ":", oldVal, "→", newVal);
});
// Trigger watchers via set()
model.set("name", "Alice Chen"); // → name changed: Alice → Alice Chen
model.set("score", 150); // → score watcher fires, status set
// get() — read current value
console.log(model.get("status")); // "expert"
// Remove individual watchers
nameWatcher.remove();
scoreWatcher.remove();
allWatcher.remove();
// Using Stateful in a declare class
var UserModel = declare([Stateful], {
name: "",
email: "",
role: "viewer",
constructor: function(props) {
// set() triggers watchers even in constructor
this.set(props || {});
},
isAdmin: function() {
return this.get("role") === "admin";
}
});
var user = new UserModel({ name: "Bob", role: "admin" });
user.watch("role", function(prop, old, newVal) {
console.log("Role changed to:", newVal);
updatePermissions(newVal);
});
user.set("role", "viewer"); // → watcher fires
});
| Mechanism | Scope | Use For |
|---|---|---|
Stateful.watch() | Single object property | Model/ViewModel property changes |
Observable.observe() | Store query results (collection) | Live list/grid updates |
dojo/topic.subscribe() | App-wide named channel | Cross-module communication |
widget._setXxxAttr | Widget property | Reactive widget rendering |
This demo wires a search input directly to an Observable store query. The table updates in real time — no manual DOM scanning, no timeouts, no jQuery.
<!-- index.html -->
<input id="searchInput" type="text" placeholder="Filter by name or department...">
<div id="statsBar"></div>
<table>
<thead><tr><th>Name</th><th>Dept</th><th>Salary</th></tr></thead>
<tbody id="employeeTableBody"></tbody>
</table>
// employeeTable.js
define([
"dojo/store/Memory",
"dojo/store/Observable",
"dojo/dom",
"dojo/dom-construct",
"dojo/dom-class",
"dojo/on",
"dojo/_base/lang",
"dojo/domReady!"
], function(Memory, Observable, dom, domConstruct, domClass, on, lang) {
var employees = [
{ id: 1, name: "Alice Chen", dept: "Engineering", salary: 95000 },
{ id: 2, name: "Bob Smith", dept: "Marketing", salary: 72000 },
{ id: 3, name: "Carol Davis", dept: "Engineering", salary: 105000 },
{ id: 4, name: "Dave Wilson", dept: "HR", salary: 68000 },
{ id: 5, name: "Eve Martinez", dept: "Engineering", salary: 88000 },
{ id: 6, name: "Frank Lee", dept: "Marketing", salary: 76000 }
];
var store = Observable(new Memory({ data: employees }));
var tbody = dom.byId("employeeTableBody");
var statsBar = dom.byId("statsBar");
var currentQuery = null; // holds the active observe handle
var currentFilter = "";
// ── Render ────────────────────────────────────────────────
function renderRow(emp) {
return domConstruct.create("tr", {
"data-id": emp.id,
innerHTML:
"<td>" + emp.name + "</td>" +
"<td><span class='dept-badge dept-" + emp.dept.toLowerCase() + "'>"
+ emp.dept + "</span></td>" +
"<td>$" + emp.salary.toLocaleString() + "</td>"
});
}
function updateStats(results) {
statsBar.textContent =
results.length + " employee" + (results.length !== 1 ? "s" : "") + " shown";
}
// ── Query + Observe ───────────────────────────────────────
function applyFilter(text) {
// Clean up previous observe handle
if (currentQuery) { currentQuery.remove(); }
domConstruct.empty(tbody);
// Build query function for partial match on name or dept
var queryFn = text
? function(item) {
var t = text.toLowerCase();
return item.name.toLowerCase().indexOf(t) > -1 ||
item.dept.toLowerCase().indexOf(t) > -1;
}
: {}; // empty object = return all
var results = store.query(queryFn, { sort: [{ attribute: "name" }] });
// Initial render
results.forEach(function(emp) {
domConstruct.place(renderRow(emp), tbody, "last");
});
updateStats(results);
// Live updates via observe
currentQuery = results.observe(function(item, removedFrom, insertedAt) {
var existingRow = tbody.querySelector("[data-id='" + item.id + "']");
if (removedFrom >= 0 && existingRow) {
// Briefly highlight then remove
domClass.add(existingRow, "row-removing");
setTimeout(function() { domConstruct.destroy(existingRow); }, 300);
}
if (insertedAt >= 0) {
var newRow = renderRow(item);
if (insertedAt >= tbody.children.length) {
domConstruct.place(newRow, tbody, "last");
} else {
domConstruct.place(newRow, tbody.children[insertedAt], "before");
}
domClass.add(newRow, "row-inserting");
setTimeout(function() { domClass.remove(newRow, "row-inserting"); }, 300);
}
updateStats(store.query(queryFn));
}, true);
}
// ── Wire search input ─────────────────────────────────────
var searchHandle = on(dom.byId("searchInput"), "input", lang.hitch(null, function(e) {
applyFilter(e.target.value.trim());
}));
// ── Initial render ────────────────────────────────────────
applyFilter("");
// ── Demo: simulate live data change after 3 seconds ──────
setTimeout(function() {
// Update Alice's salary — observe() fires, row updates live
var alice = store.get(1);
alice.salary = 102000;
store.put(alice);
}, 3000);
// Expose store for console debugging
return { store: store, applyFilter: applyFilter };
});
store.put() fires, the observe() callback updates the DOM. This one-directional data flow prevents sync bugs and makes the code easy to test.
| Module | Key Methods | Remember |
|---|---|---|
dojo/dom | byId, isDescendant | dom.byId ≠ dijit.byId |
dojo/dom-class | add, remove, toggle, contains, replace | Use over raw className |
dojo/dom-style | get, set | get() returns computed, not inline |
dojo/dom-construct | create, place, toDom, destroy, empty | Batch with DocumentFragment |
dojo/on | on(), on.once(), on.selector(), on.emit() | Always store and remove handles |
dojo/aspect | before, after, around | Method interception (not DOM events) |
dojo/query | CSS selector → NodeList | Load NodeList-* plugins for chaining |
dojo/store/Memory | get, query, add, put, remove | put() replaces entire object |
dojo/store/Observable | observe(fn, includeUpdates) | removedFrom=-1 → insert; insertedAt=-1 → delete |
dojo/Stateful | get, set, watch | Use set() to trigger watchers; direct assign bypasses them |
Time: 8–10 hrs
Pages: 22–28
Demo apps: 1
Exercises: 1