Repository navigation
v5.0.3 - Safeword - 2026-11-09
Thanks for the reports and contributions guys 🫶
Enjoy!
Changed
- WASM is opt-in — the default grid uses the TypeScript engine. Core no longer pulls the WASM runtime into every app build. If you relied on automatic WASM loading, explicitly enable it with the new
@ornery/ui-grid-core/wasmentry. The existing explicit module and asset-base routes remain available. - Coordinated 5.0.3 packages — Angular, Core, Vanilla, React, and Solid ship together. Internal package ranges now use
^5.0.3; manifests, lockfile metadata, packed exports, and publishing checks are synchronized.
Keep the default TypeScript engine without any WASM setup. To opt in, install @ornery/ui-grid-wasm and call this before mounting your grids:
import { enableUiGridWasmEngine } from '@ornery/ui-grid-core/wasm';
await enableUiGridWasmEngine();Added
- React customization slots —
headerRenderers,filterRenderers,expandableRowRenderer, andemptyStateRendererjoin cell renderers. - Theming hooks that match the docs —
header-cell,filter-cell,body-cell,header-label,header-actions, andpin-menuparts are rendered.--ui-grid-body-cell-align,--ui-grid-pin-menu-direction, andgrid.addStyles()give you more control over the layout and cascade. - Grouping value overrides —
groupingValue(row)lets a column group by a derived value. - Column resize events — subscribe with
api.core.on.columnSizeChanged((columnName, width) => { ... }).
Fixed
- External and empty pagination — page two now describes page two, and an empty result reads
0 of 0with both navigation buttons disabled. Initial renders and updates share the formatter. ChangedpaginationCurrentPagevalues are honored without resetting interactive navigation when unchanged options are passed again. - Pinned layout — the selection column stays with left-pinned columns, custom filter cells follow resized pinned neighbors, and open unpinned pin choosers stay below pinned headers. Header/filter strips compensate for the body's scrollbar width.
- Virtual row heights — virtual rows use
rowHeightinstead of shrinking to their content and leaving a blank band at the bottom. - Grouping accessors and labels — grouping respects
fieldandvalueGetter; group rows show the column's display name and the singular row suffix for one row. - React slot lifecycles — filters survive patch renders; header, filter, and expanded-detail payloads refresh; empty-state components retain their local state. Slots added alongside data updates no longer render blank.
- Next.js builds and SSR — URL-based WASM imports no longer break Turbopack, and the React wrapper waits until client mount to load browser-only custom elements.
- WASM opt-in recovery — enabling WASM while an unconfigured initialization is still pending no longer reuses the failed attempt.
- Template escaping and release packaging — filter/group values are escaped, local lockfile versions stay in sync, and compatibility packaging uses the current vanilla browser bundle.
Usage examples
Server pagination, including an empty result
The server supplies the current page's rows; totalItems is the total across all pages. No client-side slice is applied to those rows.
import { mountVanillaUiGrid } from '@ornery/ui-grid-vanilla';
const grid = await mountVanillaUiGrid(document.querySelector('#app')!, {
id: 'server-page',
columnDefs: [{ name: 'name' }],
rowIdentity: (row) => String(row['id']),
enablePagination: true,
useExternalPagination: true,
paginationPageSize: 10,
paginationCurrentPage: 2,
totalItems: 12,
data: [
{ id: '11', name: 'Eleven' },
{ id: '12', name: 'Twelve' },
],
});
// Pager: 11-12 of 12
// A server-side filter returns no matches:
grid.options = {
...grid.options,
data: [],
totalItems: 0,
paginationCurrentPage: 1,
};
// Pager: 0 of 0; previous and next are disabled.React filters, headers, details, and empty states
Use the grid API to connect custom filter controls to filtering. In Next.js, put this component behind a client boundary; its server prerender no longer evaluates HTMLElement.
'use client';
import { useRef } from 'react';
import { UiGrid, type UiGridApi } from '@ornery/ui-grid-react';
export function OrdersGrid() {
const api = useRef<UiGridApi | null>(null);
return (
<UiGrid
options={{
id: 'orders',
data: [{ id: '1', name: 'First order', status: 'Open' }],
rowIdentity: (row) => String(row['id']),
columnDefs: [
{ name: 'name', displayName: 'Order' },
{ name: 'status', displayName: 'Status' },
],
enableFiltering: true,
enableExpandable: true,
}}
onRegisterApi={(registered) => { api.current = registered; }}
headerRenderers={{
status: ({ column }) => <strong>{column.displayName}</strong>,
}}
filterRenderers={{
status: ({ value, disabled }) => (
<select
aria-label="Filter status"
value={value}
disabled={disabled}
onChange={(event) => api.current?.core.setFilter('status', event.target.value)}
>
<option value="">All statuses</option>
<option value="Open">Open</option>
<option value="Closed">Closed</option>
</select>
),
}}
expandableRowRenderer={({ row }) => <p>Details for {String(row['name'])}</p>}
emptyStateRenderer={({ heading }) => <p>{heading}</p>}
/>
);
}Group nested and derived values
Use field for a nested value, or groupingValue when the grouping key is derived. The example renders “Status: OPEN” with the correct row count.
import { mountVanillaUiGrid } from '@ornery/ui-grid-vanilla';
await mountVanillaUiGrid(document.querySelector('#app')!, {
id: 'grouped-orders',
data: [{ id: '1', payment: { status: 'open' } }],
rowIdentity: (row) => String(row['id']),
enableGrouping: true,
grouping: { groupBy: ['status'] },
columnDefs: [{
name: 'status',
displayName: 'Status',
field: 'payment.status',
groupingValue: (row) =>
String((row['payment'] as { status: string }).status).toUpperCase(),
}],
});Pinning, fixed virtual rows, and theming
The selection column is pinned automatically when a data column is pinned left. Use rowHeight for virtual row sizing and the new alignment token to center shorter cell content.
import { mountVanillaUiGrid } from '@ornery/ui-grid-vanilla';
const grid = await mountVanillaUiGrid(document.querySelector('#app')!, {
id: 'pinned-orders',
data: Array.from({ length: 500 }, (_, index) => ({
id: String(index), name: `Order ${index + 1}`, status: 'Open',
})),
rowIdentity: (row) => String(row['id']),
columnDefs: [
{ name: 'name', width: '220px', pinnedLeft: true },
{ name: 'status', width: '400px' },
],
enablePinning: true,
enableFiltering: true,
enableRowSelection: true,
enableRowHeaderSelection: true,
enableVirtualization: true,
rowHeight: 60,
});
grid.style.height = '420px';
grid.style.setProperty('--ui-grid-body-cell-align', 'center');
grid.style.setProperty('--ui-grid-pin-menu-direction', 'column');
// App styles come after the grid's adopted stylesheets.
const removeStyles = grid.addStyles('.header-label { font-weight: 700; }');
// Call removeStyles() when you no longer want this override.Or style the exposed parts from your application's stylesheet:
ui-grid-element::part(body-cell) {
color: #334155;
}All 566 unit tests passed, along with the adapter integration checks, production/package builds, Chrome layout checks, and a packed Next.js 16.4.6 build. Thanks to @jay23606 for the empty-pager and pin-menu fixes.
Full changelog: v5.0.2...v5.0.3