Form widgets, layout containers, dialogs, menus, themes — the complete production-ready UI toolkit in Dojo 1.17.3
dijit/form/Form — Validation & SubmitBorderContainer — Page LayoutTabContainer & StackContainerAccordionContainerContentPane — Lazy LoadingDialog — Modals Done RightMenu & DropDownMenuTooltip & TooltipDialogBorderContainer shell, TabContainer for sections, a live employee list, and a Dialog-based add/edit form with full validation — all wired to an Observable store.
Dijit widgets can be created two ways. Both are valid — pick the one that fits your use case.
| Declarative (HTML) | Programmatic (JavaScript) | |
|---|---|---|
| How | data-dojo-type on HTML elements + parser.parse() | new Widget({...}).placeAt(...).startup() |
| Best for | Static layouts, server-rendered pages, quick prototypes | Dynamic widgets, conditional creation, data-driven UIs |
| Drawback | Hard to parameterize, requires parser pass, slower | More code, but full control |
| Production preference | Layout shells only | All dynamic content |
<!-- Declarative — requires parser.parse() to activate -->
<input data-dojo-type="dijit/form/ValidationTextBox"
data-dojo-props="required:true, label:'Name'"
id="nameField" />
<script>
require(["dojo/parser", "dijit/form/ValidationTextBox", "dojo/domReady!"],
function(parser) {
parser.parse(); // activates all data-dojo-type elements
});
</script>
// Programmatic — full control, no parser needed
require(["dijit/form/ValidationTextBox", "dojo/domReady!"],
function(ValidationTextBox) {
var nameField = new ValidationTextBox({
required: true,
label: "Name",
placeholder: "Enter your name",
trim: true
}, "nameFieldNode"); // replace existing DOM node
nameField.startup();
// OR: place into a container node
var nameField2 = new ValidationTextBox({ required: true });
nameField2.placeAt("formContainer", "last");
nameField2.startup();
});
startup() called after placeAt(). Forgetting it causes layout widgets to render with zero height and form widgets to behave unexpectedly. The one exception: if a parent container widget calls startup(), it cascades down to children.
// All Dijit widgets use get()/set() for property access
var widget = dijit.byId("myInput");
// Read
var val = widget.get("value");
var isValid = widget.get("state") === ""; // "" = valid, "Error" = invalid
var isDisab = widget.get("disabled");
// Write — triggers _setXxxAttr, updates DOM reactively
widget.set("value", "new value");
widget.set("disabled", true);
widget.set("placeholder", "Type here...");
widget.set("style", "width: 300px");
// Batch set
widget.set({ value: "Alice", disabled: false, required: true });
require([
"dijit/form/ValidationTextBox",
"dijit/form/NumberTextBox",
"dijit/form/CurrencyTextBox"
], function(ValidationTextBox, NumberTextBox, CurrencyTextBox) {
// Email field with regex validation
var emailField = new ValidationTextBox({
name: "email",
label: "Email Address",
placeholder: "you@company.com",
required: true,
trim: true, // strip leading/trailing whitespace on blur
regExp: "[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}",
invalidMessage: "Please enter a valid email address",
missingMessage: "Email is required"
}).placeAt("emailContainer");
emailField.startup();
// Custom validator function (more powerful than regex)
var usernameField = new ValidationTextBox({
name: "username",
label: "Username",
validator: function(value, constraints) {
// Return true = valid, false = invalid
return /^[a-z0-9_]{3,20}$/.test(value);
},
invalidMessage: "3–20 lowercase letters, numbers, or underscores only"
}).placeAt("usernameContainer");
usernameField.startup();
// Salary field — numeric with constraints
var salaryField = new NumberTextBox({
name: "salary",
label: "Annual Salary",
constraints: { min: 0, max: 1000000, places: 0 },
invalidMessage: "Enter a salary between 0 and 1,000,000"
}).placeAt("salaryContainer");
salaryField.startup();
// Reading/writing values
var email = emailField.get("value");
emailField.set("value", "alice@example.com");
// Checking validity
if (emailField.isValid()) {
console.log("Email OK:", emailField.get("value"));
} else {
emailField.focus(); // bring user back to invalid field
}
});
| Property | Type | Description |
|---|---|---|
value | string | Current value — use get("value") |
required | boolean | Makes field mandatory |
disabled | boolean | Grey out + prevent input |
readOnly | boolean | Display only — no editing |
trim | boolean | Strip whitespace on blur |
intermediateChanges | boolean | Fire onChange on every keystroke (default: only on blur) |
state | string | ""=valid, "Incomplete"=missing required, "Error"=invalid |
require([
"dijit/form/Select",
"dijit/form/FilteringSelect",
"dijit/form/ComboBox",
"dojo/store/Memory"
], function(Select, FilteringSelect, ComboBox, Memory) {
// ── dijit/form/Select — fixed option list ────────────────────
var deptSelect = new Select({
name: "department",
label: "Department",
options: [
{ label: "— Select —", value: "", selected: true },
{ label: "Engineering",value: "engineering" },
{ label: "Marketing", value: "marketing" },
{ label: "HR", value: "hr" }
],
onChange: function(val) {
console.log("Selected dept:", val);
}
}).placeAt("deptContainer");
deptSelect.startup();
// ── dijit/form/FilteringSelect — store-backed, type-to-filter ─
// User MUST choose from the list (validates against store)
var deptStore = new Memory({
data: [
{ id: "eng", name: "Engineering" },
{ id: "mkt", name: "Marketing" },
{ id: "hr", name: "HR" },
{ id: "fin", name: "Finance" }
]
});
var deptFilter = new FilteringSelect({
name: "department",
label: "Department",
store: deptStore,
searchAttr: "name", // which field to search/display
labelAttr: "name", // display field
required: true,
autoComplete: true,
pageSize: 10, // show max 10 results in dropdown
queryExpr: "*\${0}*", // substring match (default is prefix only)
invalidMessage:"Please select a valid department"
}).placeAt("deptFilterContainer");
deptFilter.startup();
// Get selected item's full store object:
var item = deptFilter.item; // the selected store object (not just the value)
var id = deptFilter.get("value"); // the idProperty value
// ── dijit/form/ComboBox — store-backed, free text allowed ────
// User CAN type values not in the list (no validation against store)
var tagInput = new ComboBox({
name: "tag",
store: new Memory({ data: [
{ id: "js", name: "JavaScript" },
{ id: "java", name: "Java" },
{ id: "python", name: "Python" }
]}),
searchAttr: "name",
pageSize: 20
}).placeAt("tagContainer");
tagInput.startup();
// Dynamically update Select options
deptSelect.addOption({ label: "Design", value: "design" });
deptSelect.removeOption("hr");
deptSelect.updateOption({ value: "engineering", label: "Eng & Ops" });
// Dynamically update FilteringSelect store
deptStore.add({ id: "ops", name: "Operations" });
deptFilter.reset(); // refresh dropdown from store
});
require([
"dijit/form/DateTextBox",
"dijit/form/TimeTextBox",
"dijit/Calendar"
], function(DateTextBox, TimeTextBox, Calendar) {
// DateTextBox — locale-aware date picker
var hireDateField = new DateTextBox({
name: "hireDate",
label: "Hire Date",
required: true,
value: new Date(), // default to today
constraints: {
min: new Date(2000, 0, 1), // Jan 1 2000
max: new Date(), // today
datePattern: "yyyy-MM-dd" // display format
},
onChange: function(date) {
// 'date' is a JavaScript Date object (not a string)
console.log("Selected date:", date.toISOString());
}
}).placeAt("hireDateContainer");
hireDateField.startup();
// Get value as Date object
var d = hireDateField.get("value"); // Date object or null
console.log(d instanceof Date); // true
// TimeTextBox — time picker
var startTimeField = new TimeTextBox({
name: "startTime",
constraints: {
timePattern: "HH:mm", // 24-hour
clickableIncrement: "T00:30:00",// 30-minute steps
visibleIncrement: "T00:30:00",
visibleRange: "T04:00:00" // show 4-hour window
}
}).placeAt("startTimeContainer");
startTimeField.startup();
// Standalone Calendar widget (useful in custom date pickers)
var cal = new Calendar({
value: new Date(),
onChange: function(date) {
hireDateField.set("value", date);
}
}).placeAt("calendarContainer");
cal.startup();
});
require([
"dijit/form/CheckBox",
"dijit/form/RadioButton",
"dijit/form/ToggleButton"
], function(CheckBox, RadioButton, ToggleButton) {
// CheckBox
var activeCheck = new CheckBox({
name: "active",
value: "yes", // value submitted when checked
checked: true,
label: "Active employee",
onChange: function(checked) {
console.log("Active:", checked); // boolean
}
}).placeAt("activeContainer");
activeCheck.startup();
var isChecked = activeCheck.get("checked"); // boolean
activeCheck.set("checked", false);
// RadioButton group — use the same 'name', different 'value'
["full-time", "part-time", "contract"].forEach(function(type) {
var rb = new RadioButton({
name: "employmentType",
value: type,
checked: type === "full-time",
label: type.replace("-", " ").replace(/\b\w/g, function(l){ return l.toUpperCase(); })
}).placeAt("typeContainer");
rb.startup();
});
// ToggleButton — button that stays pressed
var boldBtn = new ToggleButton({
showLabel: true,
checked: false,
label: "Bold",
iconClass: "dijitEditorIcon dijitEditorIconBold",
onChange: function(checked) {
applyBold(checked);
}
}).placeAt("toolbarContainer");
boldBtn.startup();
});
require([
"dijit/form/Textarea",
"dijit/form/SimpleTextarea"
], function(Textarea, SimpleTextarea) {
// Textarea — auto-grows as content is typed
var notesField = new Textarea({
name: "notes",
value: "",
style: "width: 100%; min-height: 80px;",
placeHolder: "Add notes about this employee..."
}).placeAt("notesContainer");
notesField.startup();
// SimpleTextarea — fixed size, scrollable
var descField = new SimpleTextarea({
name: "description",
rows: 5,
cols: 60,
style: "width: 100%; resize: vertical;"
}).placeAt("descContainer");
descField.startup();
// Reading value
var notes = notesField.get("value");
notesField.set("value", "Updated notes text...");
});
dijit/form/Form — Validation & Submitdijit/form/Form wraps a set of Dijit form widgets and provides collective validation, value collection, and submit handling.
require([
"dijit/form/Form",
"dijit/form/ValidationTextBox",
"dijit/form/Select",
"dijit/form/DateTextBox",
"dijit/form/Button",
"dojo/dom"
], function(Form, ValidationTextBox, Select, DateTextBox, Button, dom) {
// Form wraps the <form> element
var employeeForm = new Form({
// onSubmit fires when the submit button is clicked
// and ALL fields pass validation
onSubmit: function(e) {
e.preventDefault();
if (!this.validate()) return false; // double-check
// getValues() returns { fieldName: value, ... }
var data = this.getValues();
console.log("Form data:", data);
saveEmployee(data);
return false; // prevent native form submit
}
}, "employeeFormNode"); // wraps the <form id="employeeFormNode"> element
// Add widgets to the form's DOM — they auto-register with the Form
var nameWidget = new ValidationTextBox({
name: "name", // MUST set 'name' for Form.getValues() to include it
required: true,
label: "Full Name"
}).placeAt("employeeFormNode");
nameWidget.startup();
var deptWidget = new Select({
name: "dept",
options: [
{ label: "Engineering", value: "eng" },
{ label: "Marketing", value: "mkt" }
]
}).placeAt("employeeFormNode");
deptWidget.startup();
// Submit button
var submitBtn = new Button({
type: "submit",
label: "Save Employee"
}).placeAt("employeeFormNode");
submitBtn.startup();
employeeForm.startup();
// Manual validation check (without submitting)
if (employeeForm.isValid()) {
console.log("All fields valid");
}
// Get all values programmatically
var vals = employeeForm.getValues();
// { name: "Alice", dept: "eng", ... }
// Reset all fields to defaults
employeeForm.reset();
// Set multiple field values at once
employeeForm.setValues({ name: "Bob Smith", dept: "mkt" });
});
form.getValues() output MUST have its name property set. Widgets without name are invisible to the Form.
BorderContainer — Page Layoutdijit/layout/BorderContainer divides available space into up to 5 regions (top, bottom, left, right, center). It is the foundation of every Dojo desktop-style application layout.
require([
"dijit/layout/BorderContainer",
"dijit/layout/TabContainer",
"dijit/layout/ContentPane",
"dojo/dom-construct",
"dojo/domReady!"
], function(BorderContainer, TabContainer, ContentPane, domConstruct) {
// Full-page layout — CSS REQUIRED:
// html, body { height: 100%; margin: 0; overflow: hidden; }
var layout = new BorderContainer({
design: "headline", // "headline": top/bottom span full width
// "sidebar": left/right span full height
style: "width:100%; height:100vh;"
});
// ── Top region — fixed height ──────────────────────────────
var header = new ContentPane({
region: "top",
style: "height: 60px; padding: 0 20px; line-height: 60px;",
content: "<strong>My Dojo App</strong>"
});
// ── Left region — resizable sidebar ───────────────────────
var sidebar = new ContentPane({
region: "left",
style: "width: 220px;",
splitter: true, // user can drag to resize
minSize: 150, // minimum width when dragging
maxSize: 400,
content: "<nav>Navigation here</nav>"
});
// ── Center region — REQUIRED, fills remaining space ───────
var tabs = new TabContainer({
region: "center",
tabPosition: "top" // "top", "bottom", "left", "right"
});
tabs.addChild(new ContentPane({ title: "Dashboard", content: "Welcome!" }));
tabs.addChild(new ContentPane({ title: "Employees", content: "..." }));
tabs.addChild(new ContentPane({ title: "Reports", content: "..." }));
// ── Bottom region — status bar ─────────────────────────────
var statusBar = new ContentPane({
region: "bottom",
style: "height: 28px; padding: 4px 12px; font-size: 0.82rem;",
content: "Ready"
});
// Assemble
layout.addChild(header);
layout.addChild(sidebar);
layout.addChild(tabs);
layout.addChild(statusBar);
layout.placeAt(document.body);
layout.startup(); // CRITICAL — triggers all child startups
});
| Design | Top/Bottom | Left/Right | Best For |
|---|---|---|---|
"headline" | Span full width | Fill between top and bottom | Apps with a top navbar + status bar |
"sidebar" | Fill between left and right | Span full height | Apps with a full-height sidebar |
// After dynamic show/hide, call resize() to recalculate all regions
layout.resize();
// After adding a new child dynamically
layout.addChild(newPane);
layout.layout(); // re-run layout algorithm
// Listen to resize events (e.g., when splitter is dragged)
layout.on("layoutDone", function() {
updateChartSizes();
});
TabContainer & StackContainerrequire([
"dijit/layout/TabContainer",
"dijit/layout/StackContainer",
"dijit/layout/ContentPane"
], function(TabContainer, StackContainer, ContentPane) {
// TabContainer — shows tabs, one pane visible at a time
var tabs = new TabContainer({
tabPosition: "top",
tabStrip: true,
useMenu: true, // "more tabs" overflow menu when too many tabs
useSlider: false,
closable: false // per-tab close button (can also set per pane)
}).placeAt("tabsArea");
// Add tabs
var dashTab = new ContentPane({
title: "Dashboard",
iconClass:"dijitIconFunction",
content: "<div id='dashContent'></div>"
});
var empTab = new ContentPane({
title: "Employees",
closable: true, // this tab can be closed by user
onClose: function() {
return confirm("Close Employees tab?"); // return false to cancel close
}
});
tabs.addChild(dashTab);
tabs.addChild(empTab);
tabs.startup();
// Switch tab programmatically
tabs.selectChild(empTab);
// Remove a tab
tabs.removeChild(empTab);
empTab.destroyRecursive(); // always destroy after removing
// Event: tab selected
tabs.watch("selectedChildWidget", function(prop, old, newTab) {
console.log("Switched to tab:", newTab.get("title"));
});
// ── StackContainer — same as TabContainer but NO tab strip ──
// Use when you build your own navigation (wizard, custom nav)
var wizard = new StackContainer().placeAt("wizardArea");
var step1 = new ContentPane({ title: "Step 1", content: "..." });
var step2 = new ContentPane({ title: "Step 2", content: "..." });
var step3 = new ContentPane({ title: "Step 3", content: "..." });
wizard.addChild(step1);
wizard.addChild(step2);
wizard.addChild(step3);
wizard.startup();
// Navigate steps
wizard.forward(); // go to next child
wizard.back(); // go to previous child
wizard.selectChild(step3); // jump to specific step
});
AccordionContainerrequire([
"dijit/layout/AccordionContainer",
"dijit/layout/ContentPane"
], function(AccordionContainer, ContentPane) {
var accordion = new AccordionContainer({
duration: 200 // animation duration in ms (0 = instant)
}).placeAt("accordionArea");
accordion.addChild(new ContentPane({
title: "Account Settings",
content: "<div>Account form here</div>"
}));
accordion.addChild(new ContentPane({
title: "Notification Preferences",
selected: true, // open by default
content: "<div>Notifications here</div>"
}));
accordion.addChild(new ContentPane({
title: "Security",
content: "<div>Security settings</div>"
}));
accordion.startup();
// Open a panel programmatically
accordion.selectChild(accordion.getChildren()[0]);
// Which panel is open?
var open = accordion.get("selectedChildWidget");
console.log(open.get("title"));
});
ContentPane — Lazy LoadingContentPane is the workhorse container. Beyond holding static content, it can lazy-load remote HTML via href, making it ideal for large apps where you don't want to load all content upfront.
require(["dijit/layout/ContentPane"], function(ContentPane) {
// Static content
var pane = new ContentPane({
content: "<h2>Hello</h2><p>Static content</p>"
}).placeAt("container");
pane.startup();
// Remote content via href — loaded when pane becomes visible
var remotePane = new ContentPane({
href: "/partials/employee-list.html",
loadingMessage: "<div class='loading'>Loading...</div>",
errorMessage: "<div class='error'>Failed to load content</div>",
refreshOnShow: false, // re-fetch every time pane is shown (default: false)
parseOnLoad: true // run dojo/parser on loaded content (if it has data-dojo-type)
}).placeAt("remoteContainer");
remotePane.startup();
// Reload content programmatically
remotePane.set("href", "/partials/employee-list.html");
// or force a reload of the same URL:
remotePane.refresh();
// Events during loading
remotePane.on("load", function() { console.log("Content loaded"); });
remotePane.on("error", function(e){ console.error("Load error:", e); });
// Setting content dynamically after creation
pane.set("content", "<p>Updated content at " + new Date() + "</p>");
// Setting innerHTML via a DOM node or widget
var myWidget = new SomeWidget({...});
pane.set("content", myWidget); // widget placed inside ContentPane
myWidget.startup();
});
Dialog — Modals Done Rightrequire([
"dijit/Dialog",
"dijit/form/ValidationTextBox",
"dijit/form/Button",
"dojo/dom-construct"
], function(Dialog, ValidationTextBox, Button, domConstruct) {
// ── Basic Dialog ──────────────────────────────────────────────
var dlg = new Dialog({
title: "Add Employee",
style: "width: 480px",
content: "<p>Dialog content here</p>"
});
dlg.show();
// ── Dialog with programmatic content ─────────────────────────
function createEmployeeDialog(employee) {
var formNode = domConstruct.create("div");
var nameField = new ValidationTextBox({
name: "name",
label: "Full Name",
required: true,
value: employee ? employee.name : ""
}).placeAt(formNode);
nameField.startup();
var saveBtn = new Button({
label: "Save",
onClick: function() {
if (nameField.isValid()) {
saveEmployee({ name: nameField.get("value") });
dlg2.hide();
}
}
}).placeAt(formNode);
saveBtn.startup();
var cancelBtn = new Button({
label: "Cancel",
onClick: function() { dlg2.hide(); }
}).placeAt(formNode);
cancelBtn.startup();
var dlg2 = new Dialog({
title: employee ? "Edit Employee" : "Add Employee",
style: "width: 480px",
content: formNode,
onHide: function() {
// CRITICAL: destroy dialog when hidden to prevent memory leaks
// Only if it's a one-shot dialog (not reused)
dlg2.destroyRecursive();
}
});
return dlg2;
}
// Open from a button click
on(dom.byId("addBtn"), "click", function() {
createEmployeeDialog(null).show();
});
// ── Confirmation Dialog (blocking pattern) ────────────────────
function confirmDelete(employeeName, onConfirm) {
var dlg = new Dialog({
title: "Confirm Delete",
style: "width: 360px",
content: domConstruct.toDom(
"<p>Delete <strong>" + employeeName + "</strong>? This cannot be undone.</p>" +
"<div style='text-align:right'>" +
"<button id='dlgCancel'>Cancel</button> " +
"<button id='dlgConfirm' class='danger'>Delete</button>" +
"</div>"
)
});
on(dlg.domNode, on.selector("#dlgConfirm", "click"), function() {
dlg.hide();
onConfirm();
});
on(dlg.domNode, on.selector("#dlgCancel", "click"), function() { dlg.hide(); });
dlg.on("hide", function() { dlg.destroyRecursive(); });
dlg.show();
}
});
hide(). If you create a new Dialog each time a button is clicked without destroying the old one, you accumulate hundreds of dialog instances. Always call dlg.destroyRecursive() in the onHide callback for one-shot dialogs, OR reuse a single dialog instance and update its content via dlg.set("content", ...).
Tooltip & TooltipDialogrequire([
"dijit/Tooltip",
"dijit/TooltipDialog",
"dijit/form/DropDownButton"
], function(Tooltip, TooltipDialog, DropDownButton) {
// ── Simple Tooltip ────────────────────────────────────────────
new Tooltip({
connectId: ["helpIcon"], // DOM node ids that trigger tooltip
label: "<strong>Tip:</strong> Hover to learn more",
position: ["above", "below"], // preferred positions in order
showDelay: 300, // ms before showing
hideDelay: 100
});
// Attach to multiple targets with different labels
query("[data-tooltip]").forEach(function(node) {
new Tooltip({
connectId: [node],
label: domAttr.get(node, "data-tooltip")
});
});
// ── TooltipDialog — rich popup with widgets ───────────────────
// Like a Dialog but positioned near its trigger (not centered on screen)
var quickEditDialog = new TooltipDialog({
style: "width: 280px; padding: 12px;",
content: "<div id='quickEditContent'></div>"
});
var quickEditBtn = new DropDownButton({
label: "Quick Edit",
dropDown: quickEditDialog,
onOpen: function() {
// Populate dialog when it opens
var nameField = new ValidationTextBox({
value: currentEmployee.name
}).placeAt("quickEditContent");
nameField.startup();
}
}).placeAt("toolbarArea");
quickEditBtn.startup();
});
<!-- Switch themes by changing the class on <body> -->
<link rel="stylesheet" href="dijit/themes/claro/claro.css">
<body class="claro"> <!-- or: tundra, soria, nihilo -->
/* Safe: scoped to your app container — won't break Dijit internals */
.myapp .dijitTextBox {
border-radius: 6px;
font-size: 0.95rem;
}
.myapp .dijitButton .dijitButtonNode {
background: linear-gradient(135deg, #6366f1, #22d3ee);
border: none;
color: #fff;
border-radius: 50px;
padding: 6px 20px;
}
.myapp .dijitButton .dijitButtonNode:hover {
opacity: 0.88;
}
/* Override validation error color */
.myapp .dijitValidationTextBoxError .dijitValidationContainer {
background-color: rgba(239, 68, 68, 0.1);
border-color: #ef4444;
}
// Switch theme at runtime — load new CSS then swap body class
require(["dojo/dom-class", "dojo/dom-construct"], function(domClass, domConstruct) {
function switchTheme(themeName) {
// Load the new theme CSS
var link = domConstruct.create("link", {
rel: "stylesheet",
href: "dijit/themes/" + themeName + "/" + themeName + ".css"
}, document.head);
link.onload = function() {
// Swap body class
["claro", "tundra", "soria", "nihilo"].forEach(function(t) {
domClass.remove(document.body, t);
});
domClass.add(document.body, themeName);
};
}
switchTheme("soria");
});
Combines BorderContainer, TabContainer, an Observable store-backed employee list, and a Dialog-based add/edit form into a complete working dashboard.
// CRUDDashboard.js — the root application widget
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_Widget",
"dijit/layout/BorderContainer",
"dijit/layout/TabContainer",
"dijit/layout/ContentPane",
"dijit/form/Button",
"dijit/Dialog",
"dijit/form/Form",
"dijit/form/ValidationTextBox",
"dijit/form/Select",
"dojo/store/Memory",
"dojo/store/Observable",
"dojo/dom-construct",
"dojo/on",
"dojo/domReady!"
], function(
declare, lang,
_Widget,
BorderContainer, TabContainer, ContentPane,
Button, Dialog, Form,
ValidationTextBox, Select,
Memory, Observable,
domConstruct, on
) {
// ── Store ──────────────────────────────────────────────────────
var store = Observable(new Memory({
data: [
{ 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 }
]
}));
var nextId = 4;
// ── Employee List Renderer ─────────────────────────────────────
function buildEmployeeList(container) {
domConstruct.empty(container);
var tbody = domConstruct.create("tbody", {}, domConstruct.create("table",
{ className: "data-table", style: "width:100%" }, container
));
domConstruct.create("thead", {
innerHTML: "<tr><th>Name</th><th>Dept</th><th>Salary</th><th></th></tr>"
}, container.querySelector("table"), "first");
var results = store.query({}, { sort: [{ attribute: "name" }] });
results.forEach(function(emp) {
appendRow(emp, tbody);
});
// Live updates
return results.observe(function(emp, from, to) {
var row = tbody.querySelector("[data-id='" + emp.id + "']");
if (from >= 0 && row) domConstruct.destroy(row);
if (to >= 0) appendRow(emp, tbody, to);
}, true);
}
function appendRow(emp, tbody, atIndex) {
var tr = domConstruct.create("tr", {
"data-id": emp.id,
innerHTML:
"<td>" + emp.name + "</td>" +
"<td>" + emp.dept + "</td>" +
"<td>$" + emp.salary.toLocaleString() + "</td>" +
"<td><button class='edit-btn' data-id='" + emp.id + "'>Edit</button> " +
"<button class='del-btn' data-id='" + emp.id + "'>Delete</button></td>"
});
if (atIndex !== undefined && atIndex < tbody.children.length) {
domConstruct.place(tr, tbody.children[atIndex], "before");
} else {
domConstruct.place(tr, tbody, "last");
}
}
// ── Employee Form Dialog ───────────────────────────────────────
function openEmployeeDialog(employee) {
var formNode = domConstruct.create("div", { style: "padding:12px" });
var nameF = new ValidationTextBox({
name: "name", label: "Full Name", required: true,
value: employee ? employee.name : ""
}).placeAt(formNode); nameF.startup();
var deptF = new Select({
name: "dept", label: "Department",
options: [
{ label: "Engineering", value: "Engineering" },
{ label: "Marketing", value: "Marketing" },
{ label: "HR", value: "HR" }
],
value: employee ? employee.dept : "Engineering"
}).placeAt(formNode); deptF.startup();
var saveB = new Button({
label: "Save",
onClick: function() {
if (!nameF.isValid()) { nameF.focus(); return; }
var data = {
id: employee ? employee.id : nextId++,
name: nameF.get("value"),
dept: deptF.get("value"),
salary: employee ? employee.salary : 0
};
store.put(data); // Observable notifies list automatically
dlg.hide();
}
}).placeAt(formNode); saveB.startup();
new Button({ label: "Cancel", onClick: function() { dlg.hide(); }
}).placeAt(formNode).startup();
var dlg = new Dialog({
title: employee ? "Edit Employee" : "Add Employee",
content: formNode,
onHide: function() { dlg.destroyRecursive(); }
});
dlg.show();
}
// ── Layout Assembly ────────────────────────────────────────────
var layout = new BorderContainer({ style: "width:100%;height:100vh;" });
// Header
layout.addChild(new ContentPane({
region: "top", style: "height:56px; padding: 0 16px; line-height:56px;",
content: "<strong>Employee Manager</strong>"
}));
// Center — tabs
var tabs = new TabContainer({ region: "center" });
// Employee tab
var empContentNode = domConstruct.create("div");
var addBtn = new Button({ label: "Add Employee",
onClick: function() { openEmployeeDialog(null); }
}).placeAt(empContentNode); addBtn.startup();
var listNode = domConstruct.create("div", {}, empContentNode);
var observeHandle = buildEmployeeList(listNode);
// Wire edit/delete via delegation
on(listNode, on.selector(".edit-btn", "click"), function() {
var emp = store.get(+domAttr.get(this, "data-id"));
openEmployeeDialog(emp);
});
on(listNode, on.selector(".del-btn", "click"), function() {
store.remove(+domAttr.get(this, "data-id"));
});
tabs.addChild(new ContentPane({ title: "Employees", content: empContentNode }));
tabs.addChild(new ContentPane({ title: "Reports", content: "<p>Reports here</p>" }));
layout.addChild(tabs);
// Status bar
layout.addChild(new ContentPane({
region: "bottom", style: "height:28px; padding:4px 12px; font-size:.82rem;",
content: "Ready — " + store.query({}).length + " employees"
}));
layout.placeAt(document.body);
layout.startup();
return { store: store, layout: layout };
});
| Widget | Key Props / Methods | Watch Out For |
|---|---|---|
ValidationTextBox | regExp, validator, isValid() | Set name for Form integration |
FilteringSelect | store, searchAttr, queryExpr | Value = idProperty; .item = full object |
DateTextBox | value is a Date object, constraints.min/max | Always check get("value") instanceof Date |
Form | getValues(), isValid(), reset() | All child widgets must have name |
BorderContainer | design, region, splitter, resize() | center region required; call startup() |
TabContainer | selectChild(), removeChild(), tabPosition | destroyRecursive removed tabs |
ContentPane | href, refresh(), refreshOnShow | set("content", widget) then widget.startup() |
Dialog | show(), hide(), onHide | destroyRecursive in onHide for one-shot dialogs |
Menu | targetNodeIds, selector | Context menus need targetNodeIds |
| Themes | CSS on body, scoped overrides | Never override Dijit CSS globally |
Time: 12–15 hrs
Pages: 35–45
Demo apps: 1
Widgets covered: 20+