PHASE 7

dgrid — Advanced Data Grid

Sortable, editable, paginated, tree-capable grids connected to live dstore collections — the complete production grid toolkit

dgrid OnDemandGrid Inline Editing Selector Tree Grid dstore
What you'll build: A sortable, paginated, inline-editable employee grid with row checkboxes for bulk delete, a department badge renderer, action buttons per row, and live filtering — all connected to a dstore/Memory + Trackable collection.
Name ▲
Department
Salary
Status
Actions
Alice Chen
Engineering
$95,000
�? Active
EditDel
Bob Smith
Marketing
$72,000
�? Active
EditDel
Carol Davis
Engineering
$105,000
�? Inactive
EditDel
Dave Wilson
HR
$68,000
�? Active
EditDel
1

dgrid vs Legacy DataGrid — Why Switch

dgrid (modern)dojox/grid/DataGrid (legacy)
ArchitectureMixin-based — compose features you needMonolithic — all features always loaded
Store APIdstore (cleaner API)dojo/data (deprecated)
PerformanceVirtual scrolling, on-demand rowsRenders all rows upfront
Inline editingdgrid/editor mixinBuilt in but inflexible
Custom cellsFull DOM/widget freedomLimited formatter functions
Touch/mobileSupportedNot supported
Actively maintainedYes (1.17 era)Legacy, no new features
In existing codebases: You will encounter dojox/grid/DataGrid in legacy code. Know it well enough to read it, but write new grids with dgrid. The two grids use completely different APIs — they are not interchangeable.
2

Setup: Grid Types & Installation

<!-- dgrid CSS — REQUIRED for layout and styling -->
<link rel="stylesheet" href="dgrid/css/dgrid.css">
<link rel="stylesheet" href="dgrid/css/skins/claro.css">  <!-- match Dijit theme -->
// dojoConfig packages — dgrid and dstore side by side
var dojoConfig = {
  packages: [
    { name: "dgrid",  location: "dgrid" },
    { name: "dstore", location: "dstore" },
    { name: "put-selector", location: "put-selector" },  // dgrid dependency
    { name: "xstyle",      location: "xstyle" }          // dgrid dependency
  ]
};

Choosing the Right Grid Class

ClassWhen to UseKey Feature
dgrid/GridStatic data, small datasets (<500 rows)All rows rendered at once
dgrid/OnDemandGridLarge datasets, REST-backed storesVirtual scrolling — renders only visible rows
dgrid/ListSingle-column listsNo column headers
dgrid/OnDemandListLarge lists (e.g., feeds)Virtual scrolling + no columns
require([
  "dgrid/OnDemandGrid",
  "dstore/Memory",
  "dstore/Trackable"
], function(OnDemandGrid, Memory, Trackable) {

  // dstore/Trackable makes the store notify dgrid of changes
  var TrackableMemory = Memory.createSubclass([Trackable]);

  var store = new TrackableMemory({
    data: employees,
    idProperty: "id"
  });

  // Minimal grid — just columns + store
  var grid = new OnDemandGrid({
    collection: store,           // dstore collection (not dojo/store!)
    columns: {
      name:   "Name",
      dept:   "Department",
      salary: "Salary"
    }
  }, "gridNode");   // DOM node to replace

  grid.startup();
});
⚠ dgrid uses dstore, NOT dojo/store
dgrid 1.x requires dstore collections, not dojo/store objects. They have a similar API but are different packages. dstore/Memory vs dojo/store/Memory — the grid won't work with the wrong one. Use dstore/RequestMemory for REST-backed data.
3

Column Definitions

Columns can be defined as a simple object (shorthand) or an array of full column definition objects. The array form gives full control over ordering, widths, CSS, and behaviour.

require(["dgrid/OnDemandGrid"], function(OnDemandGrid) {

  // ── Shorthand: object form ─────────────────────────────────────
  // key = field name in store, value = column header label
  var grid = new OnDemandGrid({
    collection: store,
    columns: {
      id:     "#",
      name:   "Full Name",
      dept:   "Department",
      salary: "Annual Salary"
    }
  }, "gridNode");

  // ── Full control: array form ───────────────────────────────────
  var grid2 = new OnDemandGrid({
    collection: store,
    columns: [
      {
        field:    "id",
        label:    "#",
        sortable: false,
        style:    "width: 50px; text-align: center;",
        className:"col-id"
      },
      {
        field:    "name",
        label:    "Full Name",
        sortable: true,         // clickable header sorts by this field
        style:    "width: 200px;"
      },
      {
        field:    "dept",
        label:    "Department",
        sortable: true,
        style:    "width: 150px;"
      },
      {
        field:    "salary",
        label:    "Annual Salary",
        sortable: true,
        style:    "width: 120px; text-align: right;",
        // formatter: transform value to display string
        formatter: function(value) {
          return "$" + value.toLocaleString();
        }
      },
      {
        field:    "hireDate",
        label:    "Hire Date",
        formatter: function(value) {
          return value ? new Date(value).toLocaleDateString("en-US") : "—";
        }
      }
    ]
  }, "gridNode2");
  grid2.startup();

  // ── Column sets — group columns (for frozen left columns) ──────
  // Use columnSets instead of columns for freezing:
  var grid3 = new OnDemandGrid({
    collection: store,
    columnSets: [
      // First set: frozen (stays visible when scrolling horizontally)
      [[
        { field: "id",   label: "#",   style: "width:50px" },
        { field: "name", label: "Name",style: "width:200px" }
      ]],
      // Second set: scrollable
      [[
        { field: "dept",   label: "Department" },
        { field: "salary", label: "Salary" },
        { field: "email",  label: "Email" },
        { field: "phone",  label: "Phone" }
      ]]
    ]
  }, "gridNode3");
});
4

Custom Cell Renderers

The renderCell function gives you complete DOM control over how a cell's content looks. It receives the row object and must return a DOM node.

require([
  "dgrid/OnDemandGrid",
  "dojo/dom-construct",
  "dojo/dom-class",
  "dojo/on"
], function(OnDemandGrid, domConstruct, domClass, on) {

  var grid = new OnDemandGrid({
    collection: store,
    columns: [

      // ── Badge renderer — colored dept label ────────────────────
      {
        field:    "dept",
        label:    "Department",
        renderCell: function(object, value, node) {
          var colors = {
            "Engineering": "cyan",
            "Marketing":   "purple",
            "HR":          "pink",
            "Finance":     "green"
          };
          var span = domConstruct.create("span", {
            textContent: value,
            className:   "dept-badge dept-" + (colors[value] || "default")
          });
          return span;
        }
      },

      // ── Status indicator ────────────────────────────────────────
      {
        field:    "active",
        label:    "Status",
        renderCell: function(object, value, node) {
          var dot = domConstruct.create("span", {
            textContent: value ? "�? Active" : "�? Inactive",
            style: "color:" + (value ? "#34d399" : "#fbbf24")
          });
          return dot;
        }
      },

      // ── Progress bar renderer ───────────────────────────────────
      {
        field:    "performance",   // 0–100
        label:    "Performance",
        renderCell: function(object, value) {
          var wrap  = domConstruct.create("div", { style: "background:#111827;border-radius:4px;overflow:hidden" });
          var fill  = domConstruct.create("div", {
            style: "width:" + (value || 0) + "%;height:8px;background:linear-gradient(90deg,#6366f1,#22d3ee);border-radius:4px;"
          }, wrap);
          return wrap;
        }
      },

      // ── Action buttons renderer ─────────────────────────────────
      {
        label:    "Actions",
        sortable: false,
        renderCell: function(object, value, node, options) {
          var wrap = domConstruct.create("div");

          var editBtn = domConstruct.create("button", {
            textContent: "Edit",
            className:   "grid-action-btn",
            "data-id":   object.id
          }, wrap);

          var delBtn = domConstruct.create("button", {
            textContent: "Delete",
            className:   "grid-action-btn grid-action-btn--danger",
            "data-id":   object.id
          }, wrap);

          // Wire events on the nodes
          on(editBtn, "click", function(e) {
            e.stopPropagation();
            topic.publish("employee/editRequested", { id: +this.dataset.id });
          });
          on(delBtn, "click", function(e) {
            e.stopPropagation();
            topic.publish("employee/deleteRequested", { id: +this.dataset.id });
          });

          return wrap;
        }
      },

      // ── Avatar initials renderer ────────────────────────────────
      {
        field:    "name",
        label:    "Employee",
        renderCell: function(object) {
          var wrap    = domConstruct.create("div", { style: "display:flex;align-items:center;gap:8px" });
          var initials = object.name.split(" ").map(function(w){ return w[0]; }).join("");
          domConstruct.create("span", {
            textContent: initials,
            style: "width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#22d3ee);" +
                   "color:#fff;font-weight:800;font-size:.78rem;display:flex;align-items:center;justify-content:center;"
          }, wrap);
          domConstruct.create("span", { textContent: object.name }, wrap);
          return wrap;
        }
      }
    ]
  }, "gridNode");
  grid.startup();
});
5

dgrid/editor — Inline Editing

The dgrid/editor mixin wraps column definitions to make cells editable. It supports native HTML inputs ("text", "number", "checkbox", "select") and Dijit widgets.

require([
  "dgrid/OnDemandGrid",
  "dgrid/editor",
  "dijit/form/ValidationTextBox",
  "dijit/form/Select",
  "dijit/form/NumberSpinner",
  "dstore/Memory",
  "dstore/Trackable"
], function(OnDemandGrid, editor, ValidationTextBox, Select, NumberSpinner,
            Memory, Trackable) {

  var TrackableMemory = Memory.createSubclass([Trackable]);
  var store = new TrackableMemory({ data: employees });

  var grid = new OnDemandGrid({
    collection: store,
    columns: [
      // ── Text input — inline editable ─────────────────────────
      editor({
        field:    "name",
        label:    "Full Name",
        autoSave: true,          // save on every change (not just blur)
        editor:   "text"         // native 
      }),

      // ── Select dropdown editor ────────────────────────────────
      editor({
        field:    "dept",
        label:    "Department",
        editor:   Select,        // Dijit widget as editor
        editorArgs: {
          options: [
            { label: "Engineering", value: "Engineering" },
            { label: "Marketing",   value: "Marketing" },
            { label: "HR",          value: "HR" },
            { label: "Finance",     value: "Finance" }
          ]
        },
        autoSave:    true,
        editOn:      "click"     // activate editor on click (default: dblclick for non-autoSave)
      }),

      // ── Number editor with constraints ────────────────────────
      editor({
        field:    "salary",
        label:    "Salary",
        editor:   NumberSpinner,
        editorArgs: {
          constraints: { min: 0, max: 500000, places: 0 }
        },
        autoSave: false,         // save only on blur/Enter
        editOn:   "click"
      }),

      // ── Checkbox editor ───────────────────────────────────────
      editor({
        field:    "active",
        label:    "Active",
        editor:   "checkbox",    // native 
        autoSave: true           // saves immediately on toggle
      }),

      // ── Validation text box editor ────────────────────────────
      editor({
        field:  "email",
        label:  "Email",
        editor: ValidationTextBox,
        editorArgs: {
          regExp: "\\S+@\\S+\\.\\S+",
          invalidMessage: "Invalid email"
        },
        autoSave: false,
        editOn:   "dblclick"     // activate only on double-click
      })
    ]
  }, "gridNode");

  grid.startup();

  // Listen to cell value changes
  grid.on("dgrid-datachange", function(e) {
    // e.cell       — the cell object
    // e.oldValue   — previous value
    // e.value      — new value
    // e.cell.row.data — full row data object
    console.log("Changed:", e.cell.column.field, e.oldValue, "→", e.value);

    // Persist to server
    var updated = lang.mixin({}, e.cell.row.data, {
      [e.cell.column.field]: e.value
    });
    api.update("employees", updated.id, updated)
      .otherwise(function(err) {
        // Revert on server error
        e.cell.row.data[e.cell.column.field] = e.oldValue;
        grid.refresh();
      });
  });
});
6

dgrid/selector — Row & Cell Selection

require([
  "dgrid/OnDemandGrid",
  "dgrid/selector",
  "dgrid/Selection"
], function(OnDemandGrid, selector, Selection) {

  // Mix in Selection for selection state management
  var SelectableGrid = declare([OnDemandGrid, Selection]);

  var grid = new SelectableGrid({
    collection:      store,
    selectionMode:   "multiple",   // "none","single","multiple","extended"
    allowSelectAll:  true,         // header checkbox to select all visible rows

    columns: [
      // Add checkbox column as first column
      selector({ label: "" }),     // renders a checkbox; no field needed

      { field: "name",   label: "Name" },
      { field: "dept",   label: "Department" },
      { field: "salary", label: "Salary" }
    ]
  }, "gridNode");
  grid.startup();

  // ── Reading selection ──────────────────────────────────────────
  // grid.selection is an object: { rowId: true, rowId2: true, ... }
  function getSelectedIds() {
    return Object.keys(grid.selection).filter(function(id) {
      return grid.selection[id];
    });
  }

  function getSelectedObjects() {
    return getSelectedIds().map(function(id) {
      return store.getSync(id);
    });
  }

  // ── Bulk action button ─────────────────────────────────────────
  on(dom.byId("bulkDeleteBtn"), "click", function() {
    var ids = getSelectedIds();
    if (!ids.length) { alert("Select at least one row"); return; }

    if (!confirm("Delete " + ids.length + " employee(s)?")) return;

    // Delete all selected from store — Trackable notifies grid
    ids.forEach(function(id) { store.remove(id); });
    grid.clearSelection();
  });

  // ── Selection events ───────────────────────────────────────────
  grid.on("dgrid-select", function(e) {
    // e.rows = array of selected row objects
    console.log("Selected rows:", e.rows.map(function(r){ return r.data.name; }));
    updateBulkActionsBar(getSelectedIds().length);
  });

  grid.on("dgrid-deselect", function(e) {
    console.log("Deselected:", e.rows.length, "rows");
    updateBulkActionsBar(getSelectedIds().length);
  });

  // ── Programmatic selection ─────────────────────────────────────
  grid.select(rowId);            // select by store id
  grid.deselect(rowId);
  grid.selectAll();
  grid.clearSelection();
});
7

dgrid/tree — Hierarchical Grid

require([
  "dgrid/OnDemandGrid",
  "dgrid/tree",
  "dstore/Memory",
  "dstore/Trackable"
], function(OnDemandGrid, tree, Memory, Trackable) {

  // Hierarchical data — children array or parentId reference
  var deptData = [
    { id: "eng",  name: "Engineering", type: "dept",  hasChildren: true },
    { id: "mkt",  name: "Marketing",   type: "dept",  hasChildren: true },
    { id: "e1",   name: "Alice Chen",  type: "emp",   parent: "eng",  salary: 95000 },
    { id: "e2",   name: "Carol Davis", type: "emp",   parent: "eng",  salary: 105000 },
    { id: "e3",   name: "Bob Smith",   type: "emp",   parent: "mkt",  salary: 72000 }
  ];

  var TrackableMemory = Memory.createSubclass([Trackable]);
  var store = new TrackableMemory({
    data:       deptData,
    idProperty: "id"
  });

  // getChildren — how to find children of a row
  store.getChildren = function(parent) {
    return this.filter({ parent: parent.id });
  };
  // mayHaveChildren — whether to show expand arrow
  store.mayHaveChildren = function(item) {
    return item.hasChildren;
  };

  var grid = new OnDemandGrid({
    collection: store,
    columns: [
      // tree() wraps the first column to add expand/collapse
      tree({
        field:    "name",
        label:    "Name / Department",
        style:    "width: 220px;"
      }),
      {
        field:    "salary",
        label:    "Salary",
        formatter: function(v) { return v ? "$" + v.toLocaleString() : "—"; }
      }
    ]
  }, "treeGridNode");
  grid.startup();

  // Expand/collapse programmatically
  grid.expand(store.get("eng"), true);   // true = expand, false = collapse
  grid.expand(store.get("mkt"), true);
});
8

Sorting — Built-in & Custom

require(["dgrid/OnDemandGrid"], function(OnDemandGrid) {

  var grid = new OnDemandGrid({
    collection: store,
    sort:        [{ property: "name", descending: false }],  // initial sort
    columns: [
      { field: "name",   label: "Name",    sortable: true },
      { field: "dept",   label: "Dept",    sortable: true },
      { field: "salary", label: "Salary",  sortable: true },
      { field: "active", label: "Active",  sortable: false }  // unsortable column
    ]
  }, "gridNode");
  grid.startup();

  // Programmatic sort
  grid.set("sort", [{ property: "salary", descending: true }]);

  // Multi-column sort (shift+click in UI also does this)
  grid.set("sort", [
    { property: "dept",   descending: false },
    { property: "salary", descending: true }
  ]);

  // ── Custom comparator ──────────────────────────────────────────
  // For client-side Memory stores, you can supply a custom sort function
  var TrackableMemory = Memory.createSubclass([Trackable]);
  var store2 = new TrackableMemory({ data: employees });

  // Sort "salary" by number but display locale-formatted
  store2.sort = function(data, queryOptions) {
    if (queryOptions && queryOptions.sort) {
      var s = queryOptions.sort[0];
      data.sort(function(a, b) {
        if (s.property === "salary") {
          return s.descending ? b.salary - a.salary : a.salary - b.salary;
        }
        var va = a[s.property] || "";
        var vb = b[s.property] || "";
        return s.descending
          ? vb.localeCompare(va)
          : va.localeCompare(vb);
      });
    }
    return data;
  };

  // Listen to sort changes
  grid.on("dgrid-sort", function(e) {
    console.log("Sorted by:", e.sort);
  });
});
9

Pagination vs On-Demand Scrolling

Pagination (dgrid/extensions/Pagination)On-Demand Scrolling (OnDemandGrid)
UXClassic page 1/2/3 controlsInfinite scroll — load more as you scroll
Best forBusiness reports, audit tablesFeeds, large browsable datasets
Memory useLow — only current page in DOMVery low — only visible rows in DOM
RESTRange headers or skip/limit paramsRange headers or skip/limit params
// ── Pagination extension ───────────────────────────────────────
require([
  "dgrid/Grid",
  "dgrid/extensions/Pagination"
], function(Grid, Pagination) {

  var PagingGrid = declare([Grid, Pagination]);

  var grid = new PagingGrid({
    collection:     store,
    rowsPerPage:    25,           // rows per page
    pagingLinks:    5,            // number of page links to show (1 2 3 ... 5)
    pagingTextBox:  true,         // show "Go to page" input box
    firstLastArrows: true,        // first/last page arrows
    previousNextArrows: true,

    columns: [
      { field: "name",   label: "Name" },
      { field: "dept",   label: "Department" },
      { field: "salary", label: "Salary" }
    ]
  }, "gridNode");
  grid.startup();

  // Navigate programmatically
  grid.gotoPage(3);
  console.log("Current page:", grid.get("currentPage"));

  // Change rows per page
  grid.set("rowsPerPage", 50);
});

// ── OnDemandGrid — infinite scroll ────────────────────────────
require(["dgrid/OnDemandGrid"], function(OnDemandGrid) {

  var grid = new OnDemandGrid({
    collection:   store,
    minRowsPerPage: 25,    // load at least 25 rows each time
    maxRowsPerPage: 100,   // load at most 100 rows
    farOffRemoval: 500,    // remove rows more than 500px off screen (memory mgmt)

    columns: [
      { field: "name",   label: "Name" },
      { field: "salary", label: "Salary" }
    ]
  }, "gridNode");
  grid.startup();
});
10

Connecting to dstore

require([
  "dstore/Memory",
  "dstore/Trackable",
  "dstore/Rest",
  "dstore/Cache",
  "dstore/RequestMemory"
], function(Memory, Trackable, Rest, Cache, RequestMemory) {

  // ── Memory + Trackable (in-memory, live updates) ───────────────
  var TrackableMemory = Memory.createSubclass([Trackable]);
  var localStore = new TrackableMemory({
    data: employees,
    idProperty: "id"
  });
  // Mutations on localStore (put, add, remove) automatically update grid

  // ── REST store (server-backed) ─────────────────────────────────
  var restStore = new Rest({
    target:     "/api/employees/",
    idProperty: "id",
    // sortParam: "sort",    // query param name for sort
    // rangeStartParam: "start",
    // rangeCountParam: "count"
  });
  // dgrid requests data with Range headers: Range: items=0-24

  // ── Cached REST store (fetch once, then serve from memory) ─────
  var cachedStore = new Cache({
    masterStore: restStore,
    cacheStore:  new Memory({ idProperty: "id" })
  });

  // ── RequestMemory (load all, then work in-memory) ──────────────
  // Fetches entire collection from server once, then filters/sorts locally
  var reqMemStore = new RequestMemory({
    target: "/api/employees/"
  });
  // Good for small-medium datasets where you want client-side filtering

  // ── Assigning store to grid ────────────────────────────────────
  var grid = new OnDemandGrid({
    collection: localStore,
    columns: [....]
  }, "gridNode");
  grid.startup();

  // Swap collection at runtime (e.g., filter applied)
  var filteredStore = localStore.filter({ dept: "Engineering" });
  grid.set("collection", filteredStore);
});
11

Filtering, Refresh & Collection Swap

require([
  "dstore/Memory",
  "dstore/Trackable",
  "dgrid/OnDemandGrid",
  "dojo/dom",
  "dojo/on"
], function(Memory, Trackable, OnDemandGrid, dom, on) {

  var TrackableMemory = Memory.createSubclass([Trackable]);
  var masterStore = new TrackableMemory({ data: employees });

  var grid = new OnDemandGrid({
    collection: masterStore,
    columns: [...]
  }, "gridNode");
  grid.startup();

  // ── Filter by department ────────────────────────────────────────
  on(dom.byId("deptFilter"), "change", function() {
    var val = this.value;
    var filtered = val
      ? masterStore.filter({ dept: val })
      : masterStore;                     // no filter = show all
    grid.set("collection", filtered);
  });

  // ── Text search filter ──────────────────────────────────────────
  on(dom.byId("searchInput"), "input", function() {
    var text = this.value.toLowerCase().trim();
    var filtered = text
      ? masterStore.filter(function(emp) {
          return emp.name.toLowerCase().indexOf(text) > -1 ||
                 emp.dept.toLowerCase().indexOf(text) > -1;
        })
      : masterStore;
    grid.set("collection", filtered);
  });

  // ── Combine filters ─────────────────────────────────────────────
  function applyFilters(textSearch, deptFilter, activeOnly) {
    var col = masterStore;

    if (deptFilter) col = col.filter({ dept: deptFilter });
    if (activeOnly) col = col.filter({ active: true });
    if (textSearch) col = col.filter(function(emp) {
      return emp.name.toLowerCase().indexOf(textSearch.toLowerCase()) > -1;
    });

    grid.set("collection", col);
  }

  // ── Manual refresh ──────────────────────────────────────────────
  grid.refresh();           // re-request all data from store
  grid.refresh({ keepScrollPosition: true });  // refresh without scrolling to top

  // ── Refresh a single row ────────────────────────────────────────
  grid.refreshRow(rowId);   // re-render just one row (after an external update)

  // ── Tracking mutations (Trackable auto-updates grid) ───────────
  masterStore.put({ id: 1, name: "Alice Chen Updated", dept: "Engineering", salary: 98000 });
  // Grid row for id=1 updates automatically — no refresh needed

  masterStore.add({ id: 10, name: "New Person", dept: "HR", salary: 55000 });
  // New row appears in grid automatically

  masterStore.remove(2);
  // Row disappears from grid automatically
});
12

Demo App: Full Employee Grid with Bulk Actions

// EmployeeGrid.js — complete grid module
define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dijit/_Widget",
  "dgrid/OnDemandGrid",
  "dgrid/editor",
  "dgrid/selector",
  "dgrid/Selection",
  "dstore/Memory",
  "dstore/Trackable",
  "dojo/dom",
  "dojo/dom-construct",
  "dojo/on",
  "dojo/topic"
], function(
  declare, lang, _Widget,
  OnDemandGrid, editor, selector, Selection,
  Memory, Trackable,
  dom, domConstruct, on, topic
) {

  var TrackableMemory = Memory.createSubclass([Trackable]);

  // Compose the grid class with the mixins we need
  var EditableSelectableGrid = declare([OnDemandGrid, Selection]);

  return declare([_Widget], {

    constructor: function() {
      this._store = new TrackableMemory({
        data: [],
        idProperty: "id"
      });
    },

    buildRendering: function() {
      this.domNode = domConstruct.create("div");
      this._toolbar = domConstruct.create("div", { className: "grid-toolbar" }, this.domNode);
      this._gridNode = domConstruct.create("div", { style: "height:500px;" }, this.domNode);
    },

    postCreate: function() {
      this.inherited(arguments);

      // ── Toolbar ─────────────────────────────────────────────
      var addBtn = domConstruct.create("button", {
        textContent: "+ Add Employee", className: "btn-primary"
      }, this._toolbar);

      var deleteBtn = domConstruct.create("button", {
        textContent: "Delete Selected", className: "btn-danger", disabled: true,
        id: "bulkDeleteBtn"
      }, this._toolbar);

      var searchInput = domConstruct.create("input", {
        type: "text", placeholder: "Search...", className: "search-input"
      }, this._toolbar);

      // ── Grid ─────────────────────────────────────────────────
      this._grid = new EditableSelectableGrid({
        collection:    this._store,
        selectionMode: "multiple",
        sort:          [{ property: "name", descending: false }],

        columns: [
          selector({ label: "" }),

          {
            field: "name",
            label: "Employee",
            renderCell: function(obj) {
              var wrap = domConstruct.create("div", { style: "display:flex;align-items:center;gap:8px;" });
              var initials = obj.name.split(" ").map(function(w){ return w[0]; }).join("");
              domConstruct.create("span", {
                textContent: initials,
                style: "width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#22d3ee);" +
                       "color:#fff;font-weight:800;font-size:.75rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;"
              }, wrap);
              domConstruct.create("span", { textContent: obj.name }, wrap);
              return wrap;
            }
          },

          editor({
            field: "dept", label: "Department",
            editor: "select",
            editorArgs: { options: [
              { value: "Engineering", label: "Engineering" },
              { value: "Marketing",   label: "Marketing" },
              { value: "HR",          label: "HR" }
            ]},
            autoSave: true, editOn: "click"
          }),

          editor({
            field: "salary", label: "Salary",
            editor: "number",
            formatter: function(v) { return "$" + (+v || 0).toLocaleString(); },
            autoSave: false, editOn: "dblclick"
          }),

          {
            field: "active", label: "Status",
            renderCell: function(obj) {
              var s = obj.active;
              return domConstruct.create("span", {
                textContent: s ? "�? Active" : "�? Inactive",
                style: "color:" + (s ? "#34d399" : "#fbbf24")
              });
            }
          },

          {
            label: "Actions", sortable: false,
            renderCell: function(obj) {
              var wrap = domConstruct.create("span");
              var edit = domConstruct.create("button",
                { textContent: "Edit", "data-id": obj.id, className: "grid-btn" }, wrap);
              var del  = domConstruct.create("button",
                { textContent: "Del",  "data-id": obj.id, className: "grid-btn grid-btn--red" }, wrap);

              on(edit, "click", function(e) {
                e.stopPropagation();
                topic.publish("employee/editRequested", { id: +this.dataset.id });
              });
              on(del, "click", function(e) {
                e.stopPropagation();
                if (confirm("Delete?")) {
                  store.remove(+this.dataset.id);
                }
              }.bind(del));

              return wrap;
            }
          }
        ]
      }, this._gridNode);

      // ── Wire toolbar events ───────────────────────────────────
      on(addBtn, "click", function() {
        topic.publish("employee/addRequested");
      });

      on(deleteBtn, "click", lang.hitch(this, function() {
        var ids = Object.keys(this._grid.selection)
                        .filter(function(k){ return this._grid.selection[k]; }, this);
        if (!ids.length || !confirm("Delete " + ids.length + " record(s)?")) return;
        ids.forEach(lang.hitch(this, function(id) { this._store.remove(id); }));
        this._grid.clearSelection();
      }));

      on(searchInput, "input", lang.hitch(this, function(e) {
        var text = e.target.value.trim().toLowerCase();
        this._grid.set("collection", text
          ? this._store.filter(function(emp) {
              return emp.name.toLowerCase().indexOf(text) > -1;
            })
          : this._store
        );
      }));

      // ── Enable/disable bulk delete based on selection ─────────
      this._grid.on("dgrid-select dgrid-deselect", lang.hitch(this, function() {
        var count = Object.values(this._grid.selection).filter(Boolean).length;
        deleteBtn.disabled = count === 0;
        deleteBtn.textContent = count
          ? "Delete Selected (" + count + ")"
          : "Delete Selected";
      }));

      // ── Subscribe to store changes from Dialog ─────────────────
      this.own(
        topic.subscribe("employee/saved", lang.hitch(this, function(d) {
          this._store.put(d.employee);
        })),
        topic.subscribe("employee/added", lang.hitch(this, function(d) {
          this._store.add(d.employee);
        }))
      );
    },

    startup: function() {
      if (this._started) return;
      this.inherited(arguments);
      this._grid.startup();
    },

    loadData: function(employees) {
      var self = this;
      employees.forEach(function(e) { self._store.add(e); });
    },

    destroy: function(preserveDom) {
      if (this._grid) this._grid.destroyRecursive();
      this.inherited(arguments);
    }
  });
});

Phase 7 — Quick Reference

ConceptKey Rule
Grid classUse OnDemandGrid for large/REST data; Grid for small static sets
Storedgrid needs dstore not dojo/store — always use dstore/Memory
TrackableMixin on Memory store so grid auto-updates on put/add/remove
Column shorthand{ field: label } — OK for simple grids
renderCell(obj, val, node)Returns a DOM node — full freedom over cell content
editor(colDef)Wraps column def — supports native inputs and Dijit widgets
autoSave: trueSaves on every change; false = saves on blur/Enter
dgrid-datachangeEvent fired when inline edit value changes
selector()Checkbox column — needs Selection mixin
grid.selectionObject of { id: true } pairs — iterate to get selected ids
grid.set("collection", ...)Swap data source at runtime — use for filtering
grid.refresh()Re-fetch all rows; grid.refreshRow(id) for one row

Ready for Phase 8?

Grid mastered. Next: making your app work in multiple languages and meet WCAG 2.1 AA accessibility standards.

Continue to Phase 8: i18n & Accessibility →