PHASE 4

Dijit Widget Library

Form widgets, layout containers, dialogs, menus, themes — the complete production-ready UI toolkit in Dojo 1.17.3

Dijit Forms BorderContainer TabContainer Dialog Menu Themes
What you'll build by the end: A complete CRUD dashboard with a BorderContainer shell, TabContainer for sections, a live employee list, and a Dialog-based add/edit form with full validation — all wired to an Observable store.
1

Dijit Overview: Declarative vs Programmatic

Dijit widgets can be created two ways. Both are valid — pick the one that fits your use case.

Declarative (HTML)Programmatic (JavaScript)
Howdata-dojo-type on HTML elements + parser.parse()new Widget({...}).placeAt(...).startup()
Best forStatic layouts, server-rendered pages, quick prototypesDynamic widgets, conditional creation, data-driven UIs
DrawbackHard to parameterize, requires parser pass, slowerMore code, but full control
Production preferenceLayout shells onlyAll 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();
});
⚠ GOTCHA — Always call startup()
Widgets created programmatically MUST have 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.

Widget Property API — get() and set()

// 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 });
2

Text Input Widgets

dijit/form/TextBox

Basic text input. Adds trim, uppercase, lowercase transformations.

dijit/form/ValidationTextBox

TextBox + regex/function validation + error messages. Most-used input widget.

dijit/form/NumberTextBox

Validates numeric input. Locale-aware formatting (commas, decimals).

dijit/form/CurrencyTextBox

NumberTextBox + currency symbol + locale-aware formatting.

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
  }
});

Key Properties for All Text Inputs

PropertyTypeDescription
valuestringCurrent value — use get("value")
requiredbooleanMakes field mandatory
disabledbooleanGrey out + prevent input
readOnlybooleanDisplay only — no editing
trimbooleanStrip whitespace on blur
intermediateChangesbooleanFire onChange on every keystroke (default: only on blur)
statestring""=valid, "Incomplete"=missing required, "Error"=invalid
3

Select & ComboBox Widgets

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
});
4

Date & Time Widgets

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();
});
5

CheckBox, RadioButton, ToggleButton

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();
});
6

Textarea Widgets

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...");
});
7

dijit/form/Form — Validation & Submit

dijit/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" });
});
⚠ GOTCHA — 'name' attribute is required for getValues()
Every widget that should be included in form.getValues() output MUST have its name property set. Widgets without name are invisible to the Form.
8

BorderContainer — Page Layout

dijit/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.

BorderContainer // fills its container exactly ├── ContentPane region="top" // fixed height navbar/header ├── ContentPane region="left" // fixed width sidebar (splitter) ├── TabContainer region="center" // fills remaining space (REQUIRED) │ ├── ContentPane title="Dashboard" │ └── ContentPane title="Reports" └── ContentPane region="bottom" // fixed height status bar
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
});

BorderContainer Design Modes

DesignTop/BottomLeft/RightBest For
"headline"Span full widthFill between top and bottomApps with a top navbar + status bar
"sidebar"Fill between left and rightSpan full heightApps with a full-height sidebar

Resize and Refresh

// 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();
});
9

TabContainer & StackContainer

require([
  "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
});
10

AccordionContainer

require([
  "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"));
});
11

ContentPane — Lazy Loading

ContentPane 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();
});
12

Dialog — Modals Done Right

require([
  "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();
  }
});
⚠ GOTCHA — Dialog Memory Leaks
A Dialog stays in the Dijit registry after 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", ...).
14

Tooltip & TooltipDialog

require([
  "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();
});
15

Theme System & Customization

claro tundra soria nihilo
<!-- 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 Theme Overrides

/* 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;
}

Runtime Theme Switching

// 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");
});
16

Demo App: Full CRUD Dashboard

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 };
});

Phase 4 — Quick Reference

WidgetKey Props / MethodsWatch Out For
ValidationTextBoxregExp, validator, isValid()Set name for Form integration
FilteringSelectstore, searchAttr, queryExprValue = idProperty; .item = full object
DateTextBoxvalue is a Date object, constraints.min/maxAlways check get("value") instanceof Date
FormgetValues(), isValid(), reset()All child widgets must have name
BorderContainerdesign, region, splitter, resize()center region required; call startup()
TabContainerselectChild(), removeChild(), tabPositiondestroyRecursive removed tabs
ContentPanehref, refresh(), refreshOnShowset("content", widget) then widget.startup()
Dialogshow(), hide(), onHidedestroyRecursive in onHide for one-shot dialogs
MenutargetNodeIds, selectorContext menus need targetNodeIds
ThemesCSS on body, scoped overridesNever override Dijit CSS globally

Ready for Phase 5?

You now have the full Dijit toolkit. Next: async operations with dojo/Deferred and dojo/request — how to talk to servers without callback hell.

Continue to Phase 5: Async & Deferred →