DataTable replaces the 20-line Table scaffold entirely: columns, sorting, filtering, pagination, selection, bulk actions, comparison layout, sticky first column, custom HTML cells, and a remote source driven by a `request` output rather than a function prop (props travel as HTML attributes, so a function arrives as its own source text). Toaster replaces the hand-rolled status div: tone icons, actions, hover pause/resume and a progress bar. Overlays audit -- Modal and Drawer declared aria-modal="true" but nothing ever moved focus into the panel, so the @keydown handler on their root never ran and closeOnEscape did nothing. Focus, focus restore, a Tab trap and a body scroll lock now live in the reactive runtime, shared by both. ContextMenu placed pointer menus by subtracting a guessed 340x420 from the viewport, which pushed every menu that was not that size away from the pointer; it now positions at the pointer and lets the anchored clamp pull it back once it can be measured. The reactive runtime size budget moves 150k -> 175k to cover anchored overlays, dialog behaviour, the toaster and the DataTable client half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
5.2 KiB
Plaintext
157 lines
5.2 KiB
Plaintext
// DataTable demo. Route: /table
|
|
//
|
|
// Two tables: one over a local array, one backed by /api/accounts.
|
|
//
|
|
// The remote table does not receive a fetch function. Component props travel
|
|
// as HTML attributes, so a function passed that way arrives as its own source
|
|
// text rather than something callable. Instead the table emits a `request`
|
|
// output and we answer it by dispatching the rows back with the same id.
|
|
page TablePage {
|
|
|
|
state localRows = [
|
|
{ id: 1, name: "Northwind", plan: "Scale", seats: 42, renewsOn: "2026-02-09" },
|
|
{ id: 2, name: "Acme Industrial", plan: "Team", seats: 8, renewsOn: "2026-01-10" },
|
|
{ id: 3, name: "Globex", plan: "Enterprise", seats: 210, renewsOn: "2025-12-24" },
|
|
{ id: 4, name: "Initech", plan: "Starter", seats: 5, renewsOn: "2026-03-02" },
|
|
{ id: 5, name: "Umbrella", plan: "Scale", seats: 96, renewsOn: "2026-01-31" }
|
|
]
|
|
|
|
state localColumns = [
|
|
{ key: "name", label: "Account", sortable: true },
|
|
{ key: "plan", label: "Plan", sortable: true },
|
|
{ key: "seats", label: "Seats", align: "end", sortable: true, type: "number" },
|
|
{ key: "renewsOn", label: "Renews", align: "end", sortable: true, type: "date" }
|
|
]
|
|
|
|
state remoteColumns = [
|
|
{ key: "name", label: "Account", sortable: true },
|
|
{ key: "owner", label: "Owner", sortable: true },
|
|
{ key: "plan", label: "Plan", align: "center" },
|
|
{ key: "seats", label: "Seats", align: "end", sortable: true, type: "number" },
|
|
{ key: "renewsOn", label: "Renews", align: "end", sortable: true, type: "date" }
|
|
]
|
|
|
|
state tableActions = []
|
|
state lastEvent = "none yet"
|
|
|
|
functions {
|
|
// The table asks for rows through its `request` output; we answer by
|
|
// dispatching the result back with the same instanceId. It cannot simply
|
|
// call a function we hand it: component props travel as HTML attributes,
|
|
// so a function would arrive as its own source text.
|
|
function loadAccounts(payload) {
|
|
var id = payload.instanceId
|
|
var search = new URLSearchParams({
|
|
page: String(payload.page),
|
|
pageSize: String(payload.pageSize),
|
|
query: payload.query || "",
|
|
sortKey: payload.sortKey || "",
|
|
sortDirection: payload.sortDirection || "asc"
|
|
})
|
|
|
|
fetch("/api/accounts?" + search.toString())
|
|
.then(function (response) {
|
|
if (!response.ok) {
|
|
throw new Error("Request failed with status " + response.status)
|
|
}
|
|
return response.json()
|
|
})
|
|
.then(function (data) {
|
|
window.dispatchEvent(new CustomEvent("wrnexus:datatable:rows", {
|
|
detail: { instanceId: id, rows: data.rows, total: data.total }
|
|
}))
|
|
})
|
|
.catch(function (error) {
|
|
window.dispatchEvent(new CustomEvent("wrnexus:datatable:error", {
|
|
detail: { instanceId: id, message: error.message }
|
|
}))
|
|
})
|
|
}
|
|
|
|
function onTableChange(payload) {
|
|
lastEvent =
|
|
"page " + payload.page + " of " + Math.max(1, Math.ceil(payload.total / payload.pageSize)) +
|
|
" · " + payload.total + " records" +
|
|
(payload.sortKey ? " · sorted by " + payload.sortKey + " " + payload.sortDirection : "")
|
|
}
|
|
|
|
function onTableAction(payload) {
|
|
toast.info(payload.id + " on " + payload.rows.length + " record(s)")
|
|
}
|
|
}
|
|
|
|
style {
|
|
.table-demo-section {
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.table-demo-note {
|
|
margin: 0;
|
|
color: var(--wire-color-text-muted);
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.table-demo-note code {
|
|
color: var(--wire-color-text);
|
|
}
|
|
}
|
|
|
|
view {
|
|
<h1>Data table</h1>
|
|
<p>
|
|
Sorting, filtering, paging and selection over a local array, and the same
|
|
component driven by an API through its <code>request</code> output.
|
|
</p>
|
|
|
|
<section class="table-demo-section">
|
|
<h2>Local array</h2>
|
|
<p class="table-demo-note">
|
|
Everything is derived in the browser. The Renews column declares
|
|
<code>type: "date"</code>, so it sorts chronologically rather than
|
|
alphabetically.
|
|
</p>
|
|
|
|
<DataTable
|
|
columns={localColumns}
|
|
rows={localRows}
|
|
actions={tableActions}
|
|
caption="Accounts"
|
|
pageSize={5}
|
|
selectable={true}
|
|
gridlines="grid"
|
|
/>
|
|
</section>
|
|
|
|
<section class="table-demo-section">
|
|
<h2>Loaded from an API</h2>
|
|
<p class="table-demo-note">
|
|
With <code>remote</code> the table emits a <code>request</code> for each
|
|
view change and waits to be handed rows back. Sorting and searching are
|
|
done by <code>/api/accounts</code> — the table never sees the other pages.
|
|
</p>
|
|
|
|
<DataTable
|
|
columns={remoteColumns}
|
|
remote={true}
|
|
@request="loadAccounts(payload)"
|
|
caption="Accounts (server side)"
|
|
description="22 records, paged by the API."
|
|
pageSize={6}
|
|
pageSizes={[6, 12, 22]}
|
|
paginationStyle="numbered"
|
|
searchPlaceholder="Search accounts"
|
|
stickyFirstColumn={true}
|
|
selectable={true}
|
|
@change="onTableChange(payload)"
|
|
@action="onTableAction(payload)"
|
|
/>
|
|
|
|
<p class="table-demo-note">Last change event: <strong>{lastEvent}</strong></p>
|
|
</section>
|
|
|
|
<Toaster position="bottom-right" />
|
|
}
|
|
}
|