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:

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.

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