Build a robust form wizard with StackContainer navigation, per-step validation guards, dojo/store draft persistence, a custom progress indicator widget, and a Deferred final submit with success Dialog
The wizard is a self-contained Dojo module. It owns its layout (StackContainer), its state (dojo/store/Memory draft), and its navigation logic (per-step guard functions). The parent app only calls wizard.show() and listens for the WIZARD_COMPLETED topic.
| Concern | Handled by | Notes |
|---|---|---|
| Navigation | OnboardingWizard | Calls guard fn before advancing |
| Validation | Per-step Form + guard | Returns boolean; highlights invalid fields |
| Draft persistence | DraftStore module | Written on every "next", read on "back" |
| Visual progress | ProgressIndicator widget | Custom declare; prop-based rendering |
| Submit I/O | SubmitService | Returns Deferred; wizard awaits resolution |
| Completion signal | topic.publish | Parent subscribes; wizard doesn't know parent |
dijit/layout/StackContainer shows exactly one child pane at a time. The wizard calls selectChild() to advance or go back. Tabs are hidden so only the custom ProgressIndicator and Next/Back buttons drive navigation.
// app/wizard/OnboardingWizard.js
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_WidgetBase",
"dijit/layout/StackContainer",
"dijit/layout/ContentPane",
"dojo/dom-construct",
"dojo/topic",
"app/wizard/ProgressIndicator",
"app/wizard/DraftStore",
"app/wizard/SubmitService",
"app/wizard/steps/Step1Personal",
"app/wizard/steps/Step2Employment",
"app/wizard/steps/Step3Access",
"app/wizard/steps/Step4Review",
"app/topics"
], function(declare, lang, _WidgetBase, StackContainer, ContentPane,
domConstruct, topic, ProgressIndicator, DraftStore,
SubmitService, Step1, Step2, Step3, Step4, topics) {
return declare([_WidgetBase], {
// Current step index (0-based)
_step: 0,
_steps: null, // array of { pane, widget, guard }
postCreate: function() {
this._draft = DraftStore.create();
// Progress indicator
this._progress = new ProgressIndicator({
labels: ["Personal", "Employment", "Access", "Review"],
current: 0
});
domConstruct.place(this._progress.domNode, this.domNode);
// StackContainer — no tab strip
this._stack = new StackContainer({
style: "width:100%;min-height:340px;"
});
domConstruct.place(this._stack.domNode, this.domNode);
// Build steps
var s1 = new Step1({ draft: this._draft });
var s2 = new Step2({ draft: this._draft });
var s3 = new Step3({ draft: this._draft });
var s4 = new Step4({ draft: this._draft });
this._steps = [
{ widget: s1, pane: this._wrapPane("step-1", s1) },
{ widget: s2, pane: this._wrapPane("step-2", s2) },
{ widget: s3, pane: this._wrapPane("step-3", s3) },
{ widget: s4, pane: this._wrapPane("step-4", s4) }
];
this._steps.forEach(lang.hitch(this, function(s) {
this._stack.addChild(s.pane);
}));
// Navigation buttons
this._buildNavButtons();
this._stack.startup();
this._updateNav();
},
_wrapPane: function(id, widget) {
var pane = new ContentPane({ id: id });
domConstruct.place(widget.domNode, pane.containerNode);
return pane;
},
_buildNavButtons: function() {
var nav = domConstruct.create("div", {
style: "display:flex;justify-content:space-between;align-items:center;padding:.8rem 0"
}, this.domNode);
this._backBtn = domConstruct.create("button", {
textContent: "�? Back",
className: "wbtn secondary",
onclick: lang.hitch(this, "_goBack")
}, nav);
this._draftMsg = domConstruct.create("span", {
style: "font-size:.75rem;color:var(--muted)"
}, nav);
this._nextBtn = domConstruct.create("button", {
textContent: "Next →",
className: "wbtn primary",
onclick: lang.hitch(this, "_goNext")
}, nav);
},
startup: function() {
this.inherited(arguments);
this._steps.forEach(function(s) { s.widget.startup(); });
}
});
});
// Navigation methods — added to OnboardingWizard
_goNext: function() {
var current = this._steps[this._step];
if (!current.widget.validate()) {
return; // guard failed — stay on current step
}
current.widget.saveDraft(); // persist this step's values
this._step++;
if (this._step === this._steps.length - 1) {
// Entering review step — render summary
this._steps[this._step].widget.render();
this._nextBtn.textContent = "Submit";
this._nextBtn.onclick = lang.hitch(this, "_submit");
}
this._stack.selectChild(this._steps[this._step].pane);
this._progress.set("current", this._step);
this._updateNav();
},
_goBack: function() {
if (this._step === 0) { return; }
this._step--;
this._stack.selectChild(this._steps[this._step].pane);
this._steps[this._step].widget.restoreFromDraft();
this._progress.set("current", this._step);
this._updateNav();
},
_updateNav: function() {
this._backBtn.disabled = (this._step === 0);
if (this._step < this._steps.length - 1) {
this._nextBtn.textContent = "Next →";
}
}
StackContainer shows a tab strip by default (tabPosition: "none" hides it). Wizards need a linear flow with validation gates — tab strips let users skip steps freely. Always hide tabs and drive navigation through your own buttons so the guard logic cannot be bypassed.
Each step widget exposes a validate() method. The wizard calls it before selectChild(). If validation fails the step widget highlights its invalid fields and returns false — the wizard stays put.
// app/wizard/steps/WizardStepMixin.js
define(["dojo/_base/declare"], function(declare) {
return declare([], {
// Override in each step
validate: function() { return true; },
saveDraft: function() {},
restoreFromDraft: function() {},
render: function() {} // for review step
});
});
// app/wizard/steps/Step1Personal.js
define([
"dojo/_base/declare",
"dijit/_WidgetBase",
"dijit/form/ValidationTextBox",
"dijit/form/DateTextBox",
"dojo/dom-construct",
"app/wizard/steps/WizardStepMixin"
], function(declare, _WidgetBase, ValidationTextBox, DateTextBox,
domConstruct, WizardStepMixin) {
return declare([_WidgetBase, WizardStepMixin], {
draft: null, // injected by wizard
postCreate: function() {
this._name = new ValidationTextBox({
name: "name",
placeHolder: "Full name",
required: true,
style: "width:100%;margin-bottom:.8rem;"
});
this._email = new ValidationTextBox({
name: "email",
placeHolder: "Work email",
required: true,
regExp: ".+@.+\\..+",
invalidMessage: "Enter a valid email address",
style: "width:100%;margin-bottom:.8rem;"
});
this._phone = new ValidationTextBox({
name: "phone",
placeHolder: "Mobile number",
required: true,
regExp: "\\d{10}",
invalidMessage: "10-digit number required",
style: "width:100%;margin-bottom:.8rem;"
});
domConstruct.place(this._name.domNode, this.domNode);
domConstruct.place(this._email.domNode, this.domNode);
domConstruct.place(this._phone.domNode, this.domNode);
},
startup: function() {
this.inherited(arguments);
this._name.startup();
this._email.startup();
this._phone.startup();
},
// Called by wizard before advancing
validate: function() {
var valid = this._name.isValid()
&& this._email.isValid()
&& this._phone.isValid();
if (!valid) {
// Force display of validation errors
this._name.validate(true);
this._email.validate(true);
this._phone.validate(true);
}
return valid;
},
// Persist current values into the draft store
saveDraft: function() {
this.draft.put({
id: "draft",
name: this._name.get("value"),
email: this._email.get("value"),
phone: this._phone.get("value")
});
},
// Restore from draft (called on "Back")
restoreFromDraft: function() {
var d = this.draft.get("draft");
if (d) {
this._name.set("value", d.name || "");
this._email.set("value", d.email || "");
this._phone.set("value", d.phone || "");
}
}
});
});
widget.isValid() checks the current value but does NOT show the red border or error tooltip. You must call widget.validate(true) (passing true for "isFocused") to trigger the visible error state. Call isValid() to aggregate results, then call validate(true) on each invalid widget to make errors visible to the user.
// app/wizard/steps/Step2Employment.js (condensed)
define([
"dojo/_base/declare",
"dijit/_WidgetBase",
"dijit/form/ValidationTextBox",
"dijit/form/Select",
"dijit/form/DateTextBox",
"dojo/dom-construct",
"app/wizard/steps/WizardStepMixin"
], function(declare, _WidgetBase, ValidationTextBox, Select, DateTextBox,
domConstruct, WizardStepMixin) {
return declare([_WidgetBase, WizardStepMixin], {
draft: null,
postCreate: function() {
this._dept = new Select({
name: "dept",
options: ["Engineering","DevOps","HR","Marketing","Finance"]
.map(function(d) { return { value:d, label:d }; }),
style: "width:100%;margin-bottom:.8rem;"
});
this._role = new ValidationTextBox({
name: "role", placeHolder: "Job title", required: true,
style: "width:100%;margin-bottom:.8rem;"
});
this._salary = new ValidationTextBox({
name: "salary", placeHolder: "Annual salary (INR)",
required: true, regExp: "\\d+",
invalidMessage: "Numbers only",
style: "width:100%;margin-bottom:.8rem;"
});
this._startDate = new DateTextBox({
name: "startDate", required: true,
constraints: { min: new Date() },
style: "width:100%;margin-bottom:.8rem;"
});
[this._dept, this._role, this._salary, this._startDate].forEach(
function(w) { domConstruct.place(w.domNode, this.domNode); }, this);
},
validate: function() {
var valid = this._role.isValid() && this._salary.isValid() && this._startDate.isValid();
if (!valid) {
this._role.validate(true);
this._salary.validate(true);
this._startDate.validate(true);
}
return valid;
},
saveDraft: function() {
var d = this.draft.get("draft") || { id:"draft" };
d.dept = this._dept.get("value");
d.role = this._role.get("value");
d.salary = parseInt(this._salary.get("value"), 10);
d.startDate = this._startDate.get("value");
this.draft.put(d);
},
restoreFromDraft: function() {
var d = this.draft.get("draft");
if (!d) { return; }
this._dept.set("value", d.dept || "Engineering");
this._role.set("value", d.role || "");
this._salary.set("value", String(d.salary || ""));
this._startDate.set("value", d.startDate || null);
}
});
});
// Step3Access — array of CheckBox widgets
define([
"dojo/_base/declare",
"dijit/_WidgetBase",
"dijit/form/CheckBox",
"dojo/dom-construct",
"app/wizard/steps/WizardStepMixin"
], function(declare, _WidgetBase, CheckBox, domConstruct, WizardStepMixin) {
var ACCESS_LEVELS = ["VPN", "Source Control", "JIRA", "Confluence", "AWS Console"];
return declare([_WidgetBase, WizardStepMixin], {
draft: null,
_boxes: null,
postCreate: function() {
this._boxes = ACCESS_LEVELS.map(function(name) {
var cb = new CheckBox({ value: name });
var row = domConstruct.create("div", {
style: "display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;"
}, this.domNode);
domConstruct.place(cb.domNode, row);
domConstruct.create("label", { textContent: name }, row);
return { cb: cb, name: name };
}, this);
},
// Access step is always valid (at least 0 items is fine)
validate: function() { return true; },
saveDraft: function() {
var d = this.draft.get("draft") || { id:"draft" };
d.access = this._boxes
.filter(function(item) { return item.cb.get("checked"); })
.map(function(item) { return item.name; });
this.draft.put(d);
},
restoreFromDraft: function() {
var d = this.draft.get("draft");
var granted = (d && d.access) || [];
this._boxes.forEach(function(item) {
item.cb.set("checked", granted.indexOf(item.name) >= 0);
});
}
});
});
Draft data lives in a dojo/store/Memory store keyed to "draft". Every "Next" writes the current step; every "Back" reads from it. This makes forward/back navigation safe — the user never loses input by clicking Back.
// app/wizard/DraftStore.js
define(["dojo/store/Memory"], function(Memory) {
return {
create: function() {
return new Memory({
idProperty: "id",
data: [{ id: "draft" }] // pre-seed with an empty record
});
}
};
});
// Writing (called in saveDraft()):
var current = draft.get("draft") || { id: "draft" };
current.name = "Priya Sharma";
draft.put(current); // updates existing record
// Reading (called in restoreFromDraft()):
var d = draft.get("draft");
if (d && d.name) {
nameBox.set("value", d.name);
}
// Before calling SubmitService, read the complete draft
var payload = draft.get("draft");
// payload = {
// id: "draft",
// name: "Priya Sharma", email: "priya@techoral.com", phone: "9876543210",
// dept: "Engineering", role: "Senior Dev", salary: 92000,
// startDate: Date, access: ["VPN", "Source Control", "JIRA"]
// }
delete payload.id; // remove store meta key before sending to API
This in-memory store survives Back/Next navigation within a single wizard session. It does not survive page refresh. For multi-session draft persistence, serialize the store to localStorage after each put() and rehydrate it in DraftStore.create():
// Persist to localStorage on write
var origPut = draft.put.bind(draft);
draft.put = function(obj) {
var result = origPut(obj);
localStorage.setItem("wizardDraft", JSON.stringify(obj));
return result;
};
// Rehydrate on create
var saved = localStorage.getItem("wizardDraft");
var initial = saved ? JSON.parse(saved) : { id:"draft" };
initial.id = "draft";
return new Memory({ idProperty:"id", data:[initial] });
// On successful submit
draft.remove("draft");
localStorage.removeItem("wizardDraft");
// On cancel — confirm first
var dlg = new ConfirmDialog({ message: "Discard draft and exit?" });
dlg.on("execute", function() {
draft.remove("draft");
localStorage.removeItem("wizardDraft");
topic.publish(topics.WIZARD_CANCELLED);
});
The ProgressIndicator is a pure declare widget — no _TemplatedMixin needed. It accepts labels (array of step names) and current (0-based index), renders step circles, and reacts to set("current", n) via a _setCurrentAttr setter.
// app/wizard/ProgressIndicator.js
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_WidgetBase",
"dojo/dom-construct",
"dojo/dom-class",
"dojo/dom-style"
], function(declare, lang, _WidgetBase, domConstruct, domClass, domStyle) {
return declare([_WidgetBase], {
// Widget properties
labels: [], // ["Personal", "Employment", "Access", "Review"]
current: 0, // 0-based index
postCreate: function() {
domStyle.set(this.domNode, {
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "1rem 0",
gap: "0"
});
this._circles = [];
this._lines = [];
this._render();
},
_render: function() {
domConstruct.empty(this.domNode);
this._circles = [];
this._lines = [];
this.labels.forEach(lang.hitch(this, function(label, i) {
var step = domConstruct.create("div", {
style: "display:flex;flex-direction:column;align-items:center;flex:1;position:relative;"
}, this.domNode);
// Connector line (not on last step)
if (i < this.labels.length - 1) {
var line = domConstruct.create("div", {
style: "position:absolute;top:15px;left:50%;width:100%;height:2px;background:var(--border);z-index:0;"
}, step);
this._lines.push({ el: line, index: i });
}
// Step circle
var circle = domConstruct.create("div", {
style: "width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.78rem;font-weight:700;z-index:1;border:2px solid var(--border);color:var(--muted);background:var(--surface);",
textContent: i < this.current ? "✓" : String(i + 1)
}, step);
this._circles.push({ el: circle, index: i });
// Label
domConstruct.create("div", {
textContent: label,
style: "font-size:.7rem;color:var(--muted);margin-top:.3rem;white-space:nowrap;"
}, step);
// Apply initial state
this._applyState(circle, i);
}));
// Color lines up to current
this._lines.forEach(lang.hitch(this, function(item) {
if (item.index < this.current) {
item.el.style.background = "var(--purple)";
}
}));
},
_applyState: function(circle, index) {
if (index < this.current) {
// Completed
circle.style.background = "linear-gradient(135deg,#6366f1,#22d3ee)";
circle.style.borderColor = "transparent";
circle.style.color = "#fff";
} else if (index === this.current) {
// Active
circle.style.borderColor = "var(--cyan)";
circle.style.color = "var(--cyan)";
}
// Future steps: default styles already set
},
// Called when wizard does: indicator.set("current", newIndex)
_setCurrentAttr: function(value) {
this._set("current", value);
this._render(); // full re-render — small DOM, acceptable cost
}
});
});
Naming the setter _setCurrentAttr (matching the property name current) lets Dijit's _WidgetBase.set() dispatch to it automatically. When the wizard calls indicator.set("current", 2), Dijit calls _setCurrentAttr(2) — you don't need to wire this manually. Always call this._set("propertyName", value) inside the setter to actually update the stored value.
// Create once in postCreate
this._progress = new ProgressIndicator({
labels: ["Personal", "Employment", "Access", "Review"],
current: 0
});
domConstruct.place(this._progress.domNode, this.domNode, "first");
// Update on step change
this._progress.set("current", this._step); // triggers _setCurrentAttr
The Submit button calls SubmitService.submit(payload) which returns a Deferred. While pending the button shows "Saving…" and is disabled. On resolve a success Dialog appears and publishes WIZARD_COMPLETED. On reject the error is shown inline on the review step without losing any data.
// app/wizard/SubmitService.js
define(["dojo/Deferred", "dojo/request/xhr"], function(Deferred, xhr) {
return {
submit: function(payload) {
// Real implementation: return xhr.post("/api/employees", { data: JSON.stringify(payload), ... });
// For demo: simulate network delay + random 10% error
var def = new Deferred();
setTimeout(function() {
if (Math.random() < 0.1) {
def.reject({ message: "Server error — please try again." });
} else {
def.resolve({ id: Date.now(), status: "created" });
}
}, 1200);
return def.promise;
}
};
});
_submit: function() {
var payload = this._draft.get("draft");
if (!payload) { return; }
// Disable button, show loading state
this._nextBtn.disabled = true;
this._nextBtn.textContent = "Saving…";
this._errorNode && domConstruct.destroy(this._errorNode);
SubmitService.submit(payload)
.then(lang.hitch(this, function(result) {
// Success — show Dialog, publish topic, clear draft
this._draft.remove("draft");
localStorage.removeItem("wizardDraft");
var dlg = new Dialog({
title: "Onboarding Complete",
content: "<p style='color:#34d399;font-size:1.1rem'>✓ Employee created successfully!</p>"
+ "<p>Employee ID: <strong>" + result.id + "</strong></p>",
style: "width:320px"
});
dlg.on("hide", function() { dlg.destroyRecursive(); });
dlg.startup();
dlg.show();
topic.publish(topics.WIZARD_COMPLETED, { result: result });
}))
.otherwise(lang.hitch(this, function(err) {
// Error — stay on review step, show message
this._nextBtn.disabled = false;
this._nextBtn.textContent = "Retry Submit";
this._errorNode = domConstruct.create("div", {
innerHTML: "⚠ " + (err.message || "Submit failed"),
style: "color:#f87171;padding:.5rem 0;font-size:.85rem;"
}, this._nextBtn.parentNode, "before");
}));
},
// app/wizard/steps/Step4Review.js
define([
"dojo/_base/declare",
"dijit/_WidgetBase",
"dojo/dom-construct",
"app/wizard/steps/WizardStepMixin"
], function(declare, _WidgetBase, domConstruct, WizardStepMixin) {
return declare([_WidgetBase, WizardStepMixin], {
draft: null,
// render() is called by wizard just before showing step 4
render: function() {
domConstruct.empty(this.domNode);
var d = this.draft.get("draft");
if (!d) { return; }
var fields = [
{ label: "Name", value: d.name },
{ label: "Email", value: d.email },
{ label: "Phone", value: d.phone },
{ label: "Department", value: d.dept },
{ label: "Role", value: d.role },
{ label: "Salary", value: "₹" + (d.salary || 0).toLocaleString() },
{ label: "Start Date", value: d.startDate ? d.startDate.toLocaleDateString() : "—" },
{ label: "Access", value: (d.access || []).join(", ") || "None" }
];
var html = "<h5 style='color:var(--cyan);margin-top:0'>Review & Confirm</h5>";
fields.forEach(function(f) {
html += "<div style='display:flex;justify-content:space-between;padding:.4rem 0;"
+ "border-bottom:1px solid rgba(99,102,241,.1);font-size:.85rem'>"
+ "<span style='color:var(--muted)'>" + f.label + "</span>"
+ "<span style='color:var(--text);font-weight:600'>" + f.value + "</span>"
+ "</div>";
});
this.domNode.innerHTML = html;
},
// Review is read-only — always valid
validate: function() { return true; }
});
});
The wizard publishes WIZARD_COMPLETED after the success Dialog is shown. The parent app subscribes and can refresh a grid, navigate away, or update a counter — without the wizard knowing anything about the parent. This is the same pub/sub decoupling from Phase 6, applied at the macro-composition level.
The wizard integrates into any parent app with three lines. Here's the complete main.js that puts the wizard inside a Dialog so it can be opened from an "Onboard New Employee" button.
var dojoConfig = {
async: true,
parseOnLoad: false,
packages: [{ name:"app", location:"/js/app" }]
};
require([
"dojo/dom",
"dojo/on",
"dojo/topic",
"dijit/Dialog",
"app/wizard/OnboardingWizard",
"app/topics",
"dojo/domReady!"
], function(dom, on, topic, Dialog, OnboardingWizard, topics) {
var wizardDlg = null;
on(dom.byId("openWizardBtn"), "click", function() {
// Each click creates a fresh wizard + dialog
var wizard = new OnboardingWizard({});
wizard.buildRendering();
wizard.postCreate();
wizardDlg = new Dialog({
title: "New Employee Onboarding",
content: wizard.domNode,
style: "width:560px",
onHide: function() {
wizardDlg.destroyRecursive();
wizardDlg = null;
}
});
wizardDlg.startup();
wizard.startup();
wizardDlg.show();
});
// React when wizard completes
topic.subscribe(topics.WIZARD_COMPLETED, function(evt) {
console.log("New employee onboarded:", evt.result.id);
// Refresh HR Dashboard employee grid here
topic.publish("hr/employee/refresh");
});
// React when wizard is cancelled
topic.subscribe(topics.WIZARD_CANCELLED, function() {
if (wizardDlg) { wizardDlg.hide(); }
});
});
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/dojo/dijit/themes/claro/claro.css">
<script>/* dojoConfig above */</script>
<script src="/dojo/dojo/dojo.js"></script>
</head>
<body class="claro">
<button id="openWizardBtn">Onboard New Employee</button>
<script src="/js/main.js"></script>
</body>
</html>
| File | Purpose | Key API |
|---|---|---|
| OnboardingWizard.js | Root widget — navigation, state machine | declare, StackContainer, own() |
| ProgressIndicator.js | Step tracker display | declare, _setCurrentAttr |
| DraftStore.js | In-memory draft with optional localStorage | dojo/store/Memory |
| SubmitService.js | XHR submit — returns Deferred | Deferred, xhr.post |
| WizardStepMixin.js | Contract: validate/saveDraft/restoreFromDraft | declare mixin |
| Step1Personal.js | Name/email/phone fields | ValidationTextBox |
| Step2Employment.js | Dept/role/salary/startDate fields | Select, DateTextBox |
| Step3Access.js | Permission checkboxes | CheckBox array |
| Step4Review.js | Read-only summary — rendered from draft | domConstruct.empty + innerHTML |
| topics.js | WIZARD_COMPLETED, WIZARD_CANCELLED constants | plain object |
Extend the wizard with these features:
"fade-in" to the incoming ContentPane's domNode for 400ms, then remove it.// 1. Cancel with confirm Dialog
require(["dijit/ConfirmDialog"], function(ConfirmDialog) {
var dlg = new ConfirmDialog({
title: "Discard changes?",
content: "All unsaved wizard data will be lost.",
buttonOk: "Discard"
});
dlg.on("execute", function() {
draft.remove("draft");
localStorage.removeItem("wizardDraft");
topic.publish(topics.WIZARD_CANCELLED);
});
dlg.on("hide", function() { dlg.destroyRecursive(); });
dlg.show();
});
// 2. localStorage in DraftStore.create()
var saved = localStorage.getItem("wizardDraft");
var initial = saved ? JSON.parse(saved) : { id:"draft" };
initial.id = "draft";
var store = new Memory({ idProperty:"id", data:[initial] });
var origPut = store.put.bind(store);
store.put = function(obj) {
localStorage.setItem("wizardDraft", JSON.stringify(obj));
return origPut(obj);
};
return store;
// 3. Fade animation on step change
_goNext: function() {
// ... validate + saveDraft ...
this._stack.selectChild(this._steps[this._step].pane);
var pane = this._steps[this._step].pane;
domClass.add(pane.domNode, "fade-in");
setTimeout(function() { domClass.remove(pane.domNode, "fade-in"); }, 400);
}