northwind-orders-angular — Northwind Orders (Angular)
An Angular port of northwind-orders-js — feature-
identical (public Northwind OData V4 endpoint, ODataDataManager, server-side
free-text search, toolbar, order detail panel, employee-photo decoding,
8 themes, EN/IT i18n, resizable container, Excel export, multi-selection),
all TypeScript. The northwind-orders-angular ↔ northwind-orders-js relationship is the same as github-repos-react ↔ github-repos-js: a
framework port of a proven plain-JS app, demonstrating that the
zero-dependency components drop into a framework app consumed as-is via
classic script tags.
Built with the current stable Angular CLI major (see package.json /
package-lock.json): standalone components, signals, zoneless change
detection, no NgModules.
The Angular wrapper layer
- Directives, not wrapper components —
src/app/vanilla/attaches typed inputs/outputs and an imperative API surface directly to the native custom elements via attribute directives with element selectors (selector: 'vn-grid'). No extra host element lands in the DOM (nothing sits between<vn-resize-box>and<vn-grid>, or between the grid and its<vn-grid-column>light-DOM children), and template reference variables viaexportAsgive parents the imperative API (#ordersGrid="vnGrid"). This is deliberately different from wikipedia-pages-vue's Vue wrapper components and github-repos-react's React ref-handle wrappers — each port demonstrates its framework's idiomatic bridge. VnGridDirective—[dataManager]routed through the element's imperativesetDataManager()(there is nodataManagerDOM property; a plain property binding would be a silent no-op), typed outputs bridging the grid's CustomEvents (loaded,selectionChanged,rowDblclick,persistenceReady), and delegating methods for the imperative API (initializeGrid,loadRowsAsync,setTheme,refresh,exportToExcel, …) plus anelementescape hatch.VnGridToolbarDirective— owns the template-content problem. Angular builds DOM imperatively, so children authored inside a native<template>land inchildNodes, not.content— the toolbar would clone them as empty. The directive takes theempty/selectedstate templates as HTML strings ([emptyTemplate]/[selectedTemplate], module-level constants inorders-tab.component.ts), creates realHTMLTemplateElements, and assignsinnerHTML.[gridId]sets thegridattribute only after the templates are appended; the toolbar re-reads its state templates on relink, so the late-appended templates are picked up (the element is already connected when Angular instantiates the directive).<vn-resize-box>needs no wrapper at all — it is purely declarative (attributes only) and adopts children appended after connect on its own;CUSTOM_ELEMENTS_SCHEMAcovers it.- Zoneless change detection (
provideZonelessChangeDetection()) — Zone.js would patchaddEventListenerglobally and re-run change detection on the grid's high-frequency internal events (scroll, pointer). Zoneless + signals means only the wrapper directives' explicit event bridges and the app's own signal writes schedule change detection. - Bootstrap gate —
src/main.tsawaitswindow.VanillaGridReadybeforebootstrapApplication(), so everyvn-*element is upgraded andwindow.ODataDataManagerexists before the first template instantiates. The components are loaded as classic script tags fromvendor/(synced from the sibling component folders byscripts/sync-vendor.mjsvia theprestart/prebuildnpm hooks — the Angular builder refuses asset paths outside the workspace root).
What it shows (beyond the wrapper layer)
- Built-in
ODataDataManagerfrom TypeScript — unlike github-repos-react's customDataManagersubclass, northwind-orders-angular uses the grid's shipped OData manager (new window.ODataDataManager({...}), fully typed via the hand-authored.d.ts): structured$expandquery,$skip/$topinfinite scroll (page size 100), server-side$orderbysort, and server-side free-text search. - Toolbar search item — the empty/selected templates include
<vn-grid-toolbar-search delay="600">(github-repos-react's toolbar has no search). The search is server-side: the public Northwind V4 service does not implement OData$search(HTTP 400), so the manager runs insearchMode: 'filter'— each (debounced) term builds acontains()disjunction acrossShipName,ShipCity,ShipCountry,ShipAddress,Customer.CompanyName,Customer.ContactName,Customer.Cityand reloads from page 0. The term is persisted tolocalStorage(persistence.searchTerm.enabled: true, opt-in) alongside sort + column filters. - Theme-driven search presentation —
ThemeServicetoggles theexpandable(Carbon-style collapsing) attribute on every toolbar search box, in both the inert<template>sources and any mounted element, for the IBM Carbon light/dark themes only (northwind-orders-js'sapplyToolbarSearchExpandable()). - Server-side column filters — the OData manager implements the grid's
column-filter contract, so the header-menu filter UI stays enabled
(github-repos-react disables it) and the app pushes localized filter-panel messages
(operator labels, Apply/Clear) via
I18nService.getGridFilterMessages(). - Employee photo pipeline in TypeScript —
src/app/utils/employee-photo.tsports northwind-orders-js's decoder: Northwind's legacy 8-bit dithered BMPs (wrapped in a 78-byte OLE header) are decoded and resampled to clean 96×96 PNGs with a pure-TS Lanczos-3 filter, used by both the Employee cell renderer and the detail panel's hero photo (a signal +effect()handles the async data-URL resolution).
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.
Endpoint
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 with
the same $filter, so the toolbar status reflects the filtered total. The
grid connects directly to services.odata.org from the browser; no backend
is required.
Toolbar
Same shape as northwind-orders-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, disabled-when="!hasSingleSelection") routed through
the directive's command output, and a clear-selection button. The
row-count line is the autonomous <vn-grid-toolbar-status>; transient fetch
errors go through the host-driven <vn-grid-toolbar-status-message>. Only
exportToExcel is overridden (registerCommand) for the app's file naming
and device-aware headersOnly (theme-matched Excel styling is automatic); the
override returns the export's promise, so the toolbar handles its outcome (a
cancelled export is not an error).
Columns
Identical to northwind-orders-js's 24-column set (see northwind-orders-js's README), including the custom cell renderers: Employee (photo + full name), Emp. Location ("City, Country"), Order Total (Σ UnitPrice × Qty × (1 − Discount), currency-formatted), and Order Details (first 3 line items + "+N more…", hard-clipped to the 70 px row height).
Run
npm install # first time only
npm start # syncs vendor/ then serves on http://localhost:4200/
Manual verification checklist:
- Grid loads the first Northwind page (100 rows) and infinite-scrolls in 100-row pages; the status line shows "Loaded N/total".
- Server-side sort round-trips (restarts at page 0 with
$orderby); column filters and toolbar search build$filterserver-side; the search term survives a page reload. - Toolbar: status counts, search, auto-fit, reset, clear-filters (enabled only with filter/sort), reload, export (selected scope, theme-styled file), view (single selection only), clear selection.
- Row double-click opens the order detail panel (employee photo, shipping, dates, line items); theme switch is live across all 8 themes with the Carbon themes swapping the search box to its expandable variant; EN/IT toggle reloads localized.
npm run buildoutput works when served from a subdirectory (relativebaseHref), andnode build.js(repo root) integrates it intodist/with shared component paths.
Build
npm run build # syncs vendor/ then ng build → dist/northwind-orders-angular/browser/
ng build type-checks the whole app (templates included, strictTemplates).
The repo-level node build.js delegates to this build, copies the output to
dist/samples/northwind-orders-angular/, rewrites the vendor/ script paths to the shared
../../vanilla-components/latest/ folder, minifies the public CSS, and drops
the copied vendor/ folder — the same post-process contract wikipedia-pages-vue and github-repos-react get.
Project layout
northwind-orders-angular/
├── angular.json # vendor assets mapping; baseHref "./"; no bundled styles
├── package.json # @angular/* only — no other runtime deps
├── scripts/sync-vendor.mjs # copies the 3 component sources → vendor/ (prestart/prebuild)
├── public/
│ ├── styles.css # northwind-orders-js's stylesheet, verbatim
│ └── themes/app-*.css # the 8 app themes + TEMPLATE, verbatim from northwind-orders-js
├── src/
│ ├── index.html # favicon ("NOA" badge), overlay boot script, vendor script tags
│ ├── main.ts # await VanillaGridReady → bootstrapApplication (zoneless)
│ ├── types/
│ │ ├── global.d.ts # triple-slash refs to the 3 component .d.ts files
│ │ └── order.ts # NorthwindOrder row interfaces (expanded navigation props)
│ └── app/
│ ├── app.component.ts|html # shell: header, theme/language selectors, tab, overlay handoff
│ ├── app.config.ts # provideZonelessChangeDetection()
│ ├── vanilla/
│ │ ├── vn-grid.directive.ts
│ │ └── vn-grid-toolbar.directive.ts
│ ├── services/
│ │ ├── i18n.service.ts # signals port of northwind-orders-js's modules/i18n.js (incl. filter-panel bundle)
│ │ └── theme.service.ts # live theme swap + Carbon expandable-search toggle
│ ├── utils/
│ │ └── employee-photo.ts # BMP/OLE decode + Lanczos-3 resample (port of northwind-orders-js's module)
│ └── components/
│ ├── orders-tab.component.ts|html # toolbar + resize-box + grid + columns + wiring
│ ├── order-detail-panel.component.ts|html
│ └── theme-switch-overlay.component.ts
└── README.md
Behavior notes (ported 1:1 from northwind-orders-js)
- Theme switching — live, no reload, no persistence.
ThemeServiceswaps the app-theme<link href>(with the shared cache-bust token) and the search variant;AppComponentthen calls the grid directive'ssetTheme()+refresh(). The boot theme is hard-coded in markup (material). - Locale — EN/IT; changing persists (
northwind-orders-angular:selectedLocale) and reloads the page (simplest way to re-init the grid's localized formatting), with theoverlayReason/skipOverlaysessionStorage dance kept. The pre-bootstrap overlay inindex.htmlcovers theVanillaGridReadywait;AppComponentswaps it for the Angular-rendered overlay on first render. - Persistence —
storage-mode="local": column layout, sort, column filters, and (opt-in) the search term survive reloads; the toolbar's reset command (clearPersistedSettings) clears them. public/styles.csssetsoverscroll-behavior-y: noneonhtml/body(the host-owned half of the grid's mobile touch contract — stops document-level bounce/pull-to-refresh; seedocs/vanilla-grid/00-index.md, "Mobile / touch integration").