northwind-orders-js — Northwind Orders
A minimal demo for the vanilla-grid web component using a public OData V4
endpoint. Mirrors the structure of people-cities-js
but stripped down to a single tab with no unit-conversion logic — only:
- Theme switching (8 themes: default / Material / Fiori / Carbon / Carbon Dark / Fluent / Glow / Glow Dark)
- Localization (English / Italian)
- Infinite scroll (always on, page size 100)
- Free-text search box (server-side, see below)
Toolbar
The status bar is a src/vanilla-grid-toolbar/ linked to the
orders grid (mirrors people-cities-js). The empty template exposes the search
item plus the standard auto-fit / reset / clear-filters / reload commands
(dispatched straight to the grid, no host wiring); selecting rows swaps to the
selected template, which adds export-to-Excel, a custom view command (opens
the order detail panel, enabled for a single selection only) and a
clear-selection button. The row-count line is the autonomous
<vn-grid-toolbar-status> — it self-updates from grid state (including live
selection count) via its own listeners once configureToolbarRowCountStatus()
pushes the localized strings and number formatter in; no app-side row-count
wiring is needed. A separate host-driven <vn-grid-toolbar-status-message>
carries only transient text (e.g. fetch errors), set via
ordersToolbar.setStatusMessage(). Only the exportToExcel command is
overridden (registerCommand) so the app's file naming and device-aware
headersOnly apply (theme-matched Excel styling is automatic — the grid's
exportToExcel() defaults to themeStyle: 'auto'); the override returns the
export's promise, so the toolbar handles its outcome (a cancelled export is not
an error). The view command is handled through the bubbling
vn-grid-toolbar-command event.
Search
The search box is a <vn-grid-toolbar-search> toolbar item performing a
server-side free-text search via ODataDataManager. Its presentation is
theme-driven: the expandable (Carbon-style collapsing) variant under the IBM
Carbon light/dark themes and the standard inline box otherwise, toggled at
runtime by modules/theme.js's switchTheme(). The public Northwind V4 service does not implement OData
$search (it returns HTTP 400), so the manager is configured with
searchMode: 'filter' and a searchFields list. Each keystroke (debounced)
calls gridElement.search(term), which builds a contains() disjunction across
those fields — AND-ed with any existing $filter — and reloads from page 0
(the toolbar item debounces input and sends gridElement.search(term)):
$filter=contains(ShipCity,'london') or … or contains(Customer/CompanyName,'london') …
Searched fields: ShipName, ShipCity, ShipCountry, ShipAddress,
Customer.CompanyName, Customer.ContactName, Customer.City (dotted paths are
auto-converted to OData slash notation). The total count re-resolves with the
same filter so the status line reflects the filtered total.
Hovering or focusing the search box reveals a tooltip listing these fields —
<vn-grid-toolbar-search>'s built-in search-fields tooltip, driven by
gridElement.getSearchFields(); no app code required. Each field renders as
its column's header (matched by field against the <vn-grid-column>
declarations in index.html), so the tooltip reads "Searching: Ship Name,
Ship City, Ship Country, Ship Address, Customer, Contact and Customer City"
rather than the raw OData field paths.
The active term is persisted to localStorage (persistence.searchTerm.enabled: true in initializeGrid(), opt-in) so it survives a page reload alongside sort
and column filters, and re-applies on the first data load.
Data source
Public Northwind OData service:
https://services.odata.org/V4/Northwind/Northwind.svc/Orders?$expand=Order_Details($expand=Product),Employee,Customer,Shipper
Total count is fetched from:
https://services.odata.org/V4/Northwind/Northwind.svc/Orders/$count
The grid uses ODataDataManager (shipped with vanilla-grid) to build the
$skip / $top / $expand / $orderby query string for each page. Sort,
column-filter, and search changes re-fetch page 0 automatically: each fires a
config-change on the manager and the grid (setAutoReloadOnConfigChange(true))
coalesces them into one reload — no per-sort onSortChanged reload callback.
Resizing the grid
The grid sits in a src/vanilla-resize-box/ (aria-label="Resizable orders grid"). Drag the corner handle to resize it — resizing is pointer-driven, with no keyboard path.
The handle carries role="separator" and takes its accessible name from the host's aria-label — here "Resizable orders grid — resize handle" — but is not focusable, so it adds no tab stop to the page. This app's markup needs nothing beyond the aria-label it already carries.
Layout
northwind-orders-js/
├── index.html # Single tab, vn-grid + vn-resize-box
├── app.js # Bootstrap, listeners, locale/theme wiring
├── styles.css # Layout (reused from people-cities-js)
├── themes/ # Visual app themes (reused from people-cities-js)
│ ├── app-default.css
│ ├── app-material.css
│ ├── app-fiori.css
│ ├── app-carbon.css
│ ├── app-carbon-dark.css
│ ├── app-fluent.css
│ ├── app-glow.css
│ ├── app-glow-dark.css
│ └── TEMPLATE-app-theme.css # Starting point for a new app theme
├── modules/
│ ├── index.js # Re-exports (barrel, consumed by app.js only)
│ ├── dom-refs.js # data-ref → element map + setTextById
│ ├── i18n.js # Strings + Intl helpers (en-US, it-IT)
│ ├── theme.js # Theme switcher + overlay
│ ├── orders-tab.js # ODataDataManager, grid init, order detail panel
│ └── employee-photo.js # Northwind photo decoding + Lanczos resampling
└── README.md
Run
Serve the repository root with any static server (CORS-friendly), then open:
http://localhost:<port>/samples/northwind-orders-js/
The grid connects directly to services.odata.org from the browser; no backend
is required.
Columns
| Field | Type | Notes |
|---|---|---|
OrderID |
uid | Primary key, frozen |
Customer.CompanyName |
string | Frozen |
EmployeeID |
string | renderCell: photo thumbnail + "FirstName LastName" |
Employee.Title |
string | Role |
Employee.Location |
string | renderCell: "City, Country" composite, not sortable |
Customer.ContactName |
string | |
Customer.ContactTitle |
string | |
Customer.City |
string | |
Customer.Phone |
string | Not sortable |
OrderDate |
date | |
RequiredDate |
date | |
ShippedDate |
date | |
Shipper.CompanyName |
string | Carrier |
Shipper.Phone |
string | Not sortable |
Freight |
number | |
OrderTotal |
number | renderCell: Σ(UnitPrice × Qty × (1 − Discount)), not sortable/filterable |
Order_Details.length |
number | Count of expanded line items, not sortable/filterable server-side |
ShipName |
string | |
ShipAddress |
string | |
ShipCity |
string | |
ShipRegion |
string | |
ShipPostalCode |
string | |
ShipCountry |
string | |
Order_Details |
string | renderCell: bullet list of line items (first 3 + "+N more"), not sortable |
Notes
- Selection (
selection-mode="multiple"+ checkboxes) and Excel export are wired via the toolbar's selected template; export usesregisterCommandfor app-specific file naming (theme-matched Excel styling is automatic — the grid'sexportToExcel()defaults tothemeStyle: 'auto'). - Double-clicking a row (or the toolbar's view command on a single selection) opens the order detail side panel — employee photo, shipping, dates, and line items.
- Employee photos are legacy 8-bit dithered BMPs decoded and resampled with a
pure-JS Lanczos-3 filter (see
modules/employee-photo.js) to avoid Chrome/Skia's grainy GPU downscaling of the raw bitmaps. - Locale changes trigger a full page reload, preserved across reloads via
localStorage. Theme changes apply live, no reload — every page load starts from the code-configured default theme (material), regardless of what was picked in a prior visit. - The
<vn-grid>themeattribute and the app-level<link id="appThemeLink">stay in sync viamodules/theme.js. styles.csssetsoverscroll-behavior-y: noneonhtml/body(the host-owed half of the grid's mobile touch contract — stops document-level bounce/pull-to-refresh; seedocs/vanilla-grid/00-index.md, "Mobile / touch integration").