@toclocoinc/lattice-grid 1.55.0 → 1.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/API.html +389 -42
- package/docs/api-detail.html +373 -8
- package/lattice-grid.d.ts +525 -26
- package/lattice-grid.esm.min.js +1943 -920
- package/lattice-grid.min.cjs +1942 -920
- package/lattice-grid.min.css +1 -1
- package/lattice-grid.min.js +1942 -920
- package/modules/ai.esm.min.js +6 -4
- package/modules/ai.min.cjs +6 -4
- package/modules/ai.min.js +6 -4
- package/modules/angular.esm.min.js +7 -4
- package/modules/angular.min.cjs +7 -4
- package/modules/angular.min.js +7 -4
- package/modules/chart-alluvial.esm.min.js +1 -1
- package/modules/chart-arc.esm.min.js +1 -1
- package/modules/chart-bubblemap.esm.min.js +1 -1
- package/modules/chart-bump.esm.min.js +1 -1
- package/modules/chart-calendar.esm.min.js +1 -1
- package/modules/chart-decomposition.esm.min.js +1 -1
- package/modules/chart-diverging.esm.min.js +1 -1
- package/modules/chart-dumbbell.esm.min.js +1 -1
- package/modules/chart-fan.esm.min.js +1 -1
- package/modules/chart-hexbin.esm.min.js +1 -1
- package/modules/chart-hexmap.esm.min.js +1 -1
- package/modules/chart-icicle.esm.min.js +1 -1
- package/modules/chart-parallel.esm.min.js +1 -1
- package/modules/chart-ridgeline.esm.min.js +1 -1
- package/modules/chart-roc.esm.min.js +1 -1
- package/modules/chart-slope.esm.min.js +1 -1
- package/modules/chart-splom.esm.min.js +1 -1
- package/modules/chart-waffle.esm.min.js +1 -1
- package/modules/charts.esm.min.js +15 -10
- package/modules/charts.min.cjs +15 -10
- package/modules/charts.min.js +15 -10
- package/modules/data-router.esm.min.js +37 -4
- package/modules/data-router.min.cjs +37 -4
- package/modules/data-router.min.js +37 -4
- package/modules/devtools.esm.min.js +2 -2
- package/modules/devtools.min.cjs +2 -2
- package/modules/devtools.min.js +2 -2
- package/modules/dhtmlx-compat.esm.min.js +4 -4
- package/modules/dhtmlx-compat.min.cjs +4 -4
- package/modules/dhtmlx-compat.min.js +4 -4
- package/modules/gantt.esm.min.js +4 -4
- package/modules/gantt.min.cjs +4 -4
- package/modules/gantt.min.js +4 -4
- package/modules/htmx.esm.min.js +1939 -920
- package/modules/htmx.min.cjs +1939 -920
- package/modules/htmx.min.js +1939 -920
- package/modules/kanban.esm.min.js +106 -25
- package/modules/kanban.min.cjs +106 -25
- package/modules/kanban.min.js +106 -25
- package/modules/kpi.esm.min.js +44 -7
- package/modules/kpi.min.cjs +44 -7
- package/modules/kpi.min.js +44 -7
- package/modules/layout.esm.min.js +4 -4
- package/modules/layout.min.cjs +4 -4
- package/modules/layout.min.js +4 -4
- package/modules/mock-socket.esm.min.js +2 -2
- package/modules/mock-socket.min.cjs +2 -2
- package/modules/mock-socket.min.js +2 -2
- package/modules/react.esm.min.js +7 -4
- package/modules/react.min.cjs +7 -4
- package/modules/react.min.js +7 -4
- package/modules/svelte.esm.min.js +7 -4
- package/modules/svelte.min.cjs +7 -4
- package/modules/svelte.min.js +7 -4
- package/modules/tabs.esm.min.js +4 -4
- package/modules/tabs.min.cjs +4 -4
- package/modules/tabs.min.js +4 -4
- package/modules/vue.esm.min.js +7 -4
- package/modules/vue.min.cjs +7 -4
- package/modules/vue.min.js +7 -4
- package/modules/webcomponent.esm.min.js +1942 -920
- package/modules/webcomponent.min.cjs +1942 -920
- package/modules/webcomponent.min.js +1942 -920
- package/package.json +3 -2
package/docs/api-detail.html
CHANGED
|
@@ -437,7 +437,7 @@
|
|
|
437
437
|
<div class="shell">
|
|
438
438
|
<aside class="rail">
|
|
439
439
|
<p class="rail__brand">Lattice Grid</p>
|
|
440
|
-
<p class="rail__sub">Developer guide · v1.
|
|
440
|
+
<p class="rail__sub">Developer guide · v1.57.0</p>
|
|
441
441
|
<nav>
|
|
442
442
|
<div class="rail__group">
|
|
443
443
|
<span class="rail__label">Start here</span>
|
|
@@ -553,7 +553,7 @@
|
|
|
553
553
|
<a href="API.html">reference tables</a> are the shorter version for when you already know.
|
|
554
554
|
</p>
|
|
555
555
|
<p class="chips">
|
|
556
|
-
<span class="chip">Version 1.
|
|
556
|
+
<span class="chip">Version 1.57.0</span>
|
|
557
557
|
<span class="chip">Zero dependencies</span>
|
|
558
558
|
<span class="chip">No build step</span>
|
|
559
559
|
</p>
|
|
@@ -1278,7 +1278,7 @@ off(); <span class="cmt">// every subscrip
|
|
|
1278
1278
|
</p>
|
|
1279
1279
|
<div class="example">
|
|
1280
1280
|
<p class="example__label">Which version am I running?</p>
|
|
1281
|
-
<pre><code>grid.getVersion(); <span class="cmt">// '1.
|
|
1281
|
+
<pre><code>grid.getVersion(); <span class="cmt">// '1.57.0'</span>
|
|
1282
1282
|
LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid to hand</span></code></pre>
|
|
1283
1283
|
</div>
|
|
1284
1284
|
<p class="lead-in">
|
|
@@ -1286,6 +1286,46 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
|
|
|
1286
1286
|
"the grid on this page", and whoever reads it has a grid rather than the module it was built
|
|
1287
1287
|
from.
|
|
1288
1288
|
</p>
|
|
1289
|
+
|
|
1290
|
+
<h3 id="headless-vs-dom">A headless grid is not a smaller grid, it is a grid without a renderer</h3>
|
|
1291
|
+
<p class="lead-in">
|
|
1292
|
+
<code>createGrid</code> builds this same core and attaches the DOM renderer to it;
|
|
1293
|
+
<code>createHeadlessGrid</code> stops one step earlier. Everything the core owns — data, state,
|
|
1294
|
+
sort, filter, group, total, pivot, formulas and computed columns, editing and optimistic
|
|
1295
|
+
write-back, export, and every event — runs unchanged with no renderer attached, which is why
|
|
1296
|
+
most of the examples in this guide that call <code>createHeadlessGrid</code> are settling real
|
|
1297
|
+
API questions, not toy snippets. What it does not have is the renderer: no layout, no
|
|
1298
|
+
measurement, no scrolling geometry, no focus, and <code>grid.element</code> is <code>null</code>.
|
|
1299
|
+
</p>
|
|
1300
|
+
<div class="why">
|
|
1301
|
+
<p><strong>A grid mounted where it has no rendered box paints only a handful of rows, not the
|
|
1302
|
+
whole dataset — by design, not as a bug.</strong> The visible row window is computed from the
|
|
1303
|
+
container's own height; a container that is detached from the document, or sits under a
|
|
1304
|
+
<code>display: none</code> ancestor, measures zero, and the virtualiser falls back to a small
|
|
1305
|
+
band around the top of the data (the overscan margin, five rows with the default configuration)
|
|
1306
|
+
rather than nothing at all. A harness that builds a grid off in a hidden or unattached container
|
|
1307
|
+
and then asserts on what is visible reads far fewer rows than it loaded and looks broken; giving
|
|
1308
|
+
the container a real, measured box before asserting is what fixes it. Note that this is about
|
|
1309
|
+
the container having <em>no computed size</em>, not about being visually off-screen: a
|
|
1310
|
+
container positioned outside the browser's visible area with real, explicit dimensions (for
|
|
1311
|
+
example <code>position: fixed; left: -9999px</code> with a width and height) still gets a real
|
|
1312
|
+
box and renders fully.</p>
|
|
1313
|
+
<p><strong>The in-repo test DOM (<code>testdom.js</code>) is a stub, not a browser, and says so
|
|
1314
|
+
in its own header comment.</strong> It implements exactly the surface the renderer touches —
|
|
1315
|
+
element creation, attributes, <code>classList</code>, <code>style</code>, children,
|
|
1316
|
+
<code>textContent</code>, event dispatch, injected geometry, and shims for
|
|
1317
|
+
<code>ResizeObserver</code> and <code>requestAnimationFrame</code> — which is enough to drive
|
|
1318
|
+
the renderer's logic in Node for most of the suite. It does not compute a cascade or a layout,
|
|
1319
|
+
so a class or style change that a stylesheet then overrides, or a box that depends on CSS rather
|
|
1320
|
+
than on the geometry a test injected, is invisible to it; those need the real-browser tests
|
|
1321
|
+
(<code>test/*-browser.test.js</code>) that drive an actual headless Chrome instead. It also does
|
|
1322
|
+
not model real focus semantics — its <code>focus()</code> simply records which element is
|
|
1323
|
+
"focused" with no check for visibility, tab order or focusability. Since BACKLOG-0001181,
|
|
1324
|
+
<code>classList.add</code> does throw on a token containing a space, the same
|
|
1325
|
+
<code>InvalidCharacterError</code> a real <code>DOMTokenList</code> throws, closing the specific
|
|
1326
|
+
gap where a two-word class name passed every headless test and then blanked a real page.</p>
|
|
1327
|
+
</div>
|
|
1328
|
+
|
|
1289
1329
|
<h2 id="columns-guide">Defining columns</h2>
|
|
1290
1330
|
<p class="lead-in">
|
|
1291
1331
|
A column is an object with a <code>field</code> (the path to read from your data) and
|
|
@@ -1346,6 +1386,55 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
|
|
|
1346
1386
|
A cycle is caught at compile time with the full path named, rather than becoming a stack
|
|
1347
1387
|
overflow at render time.</p>
|
|
1348
1388
|
</div>
|
|
1389
|
+
<p>When a computed value is re-run, stated plainly:</p>
|
|
1390
|
+
<ul>
|
|
1391
|
+
<li>A pure compute (the default) runs at ingest and is cached. It runs again when its row is
|
|
1392
|
+
replaced through <code>rows.apply({ update })</code> or the data through
|
|
1393
|
+
<code>rows.load()</code> — unconditionally, since a value derived from data that is
|
|
1394
|
+
gone is stale by definition; when the grid a derived grid follows changes; and when you ask
|
|
1395
|
+
with <code>rows.refresh({ rows, columns, force: true })</code>. A sort, a filter, a state
|
|
1396
|
+
restore or an edit to a column outside its <code>deps</code> does not re-run it. An in-place
|
|
1397
|
+
cell edit to one of its <code>deps</code> does not currently re-run it either.</li>
|
|
1398
|
+
<li>Naming a column re-runs what depends on it: <code>refresh({ columns: ['p'], force: true })</code>
|
|
1399
|
+
recomputes a <code>q</code> whose <code>deps</code> include <code>p</code>;
|
|
1400
|
+
<code>refresh({ columns: ['q'] })</code> does not recompute <code>p</code>.</li>
|
|
1401
|
+
<li><code>pure: false</code> guarantees the compute is re-evaluated on every read and every
|
|
1402
|
+
paint. It is never served from a cache.</li>
|
|
1403
|
+
<li>Whenever a compute re-runs, <code>rows.text()</code> and the painted cell show the new
|
|
1404
|
+
result, and so do a sort or filter on the column: every cache the grid keeps for that cell
|
|
1405
|
+
— the one behind the text and the paint, and the one sort and filter handles read
|
|
1406
|
+
— is invalidated together.</li>
|
|
1407
|
+
<li>Without <code>force</code>, a targeted <code>refresh({ rows, columns })</code> behaves
|
|
1408
|
+
differently by store mode, and which one you get flips at <code>columnarBelow</code>. On a
|
|
1409
|
+
grid below that row count the named cell is re-run on its next read; on a columnar one the
|
|
1410
|
+
stored value stands until you pass <code>force: true</code>. This is behaviour to plan for,
|
|
1411
|
+
not a tuning detail: the same call on the same data recomputes or does not purely according
|
|
1412
|
+
to how many rows arrived. Pass <code>force: true</code> when you want the same answer
|
|
1413
|
+
whatever the row count.</li>
|
|
1414
|
+
</ul>
|
|
1415
|
+
<div class="example">
|
|
1416
|
+
<p class="example__label">An answer that arrives later</p>
|
|
1417
|
+
<pre><code><span class="cmt">// A lookup the grid cannot see: show a placeholder, fill the table, then</span>
|
|
1418
|
+
<span class="cmt">// tell the grid which cells to recompute. The compute stays pure, so it is</span>
|
|
1419
|
+
<span class="cmt">// not re-run on every paint — only when you say the answer changed.</span>
|
|
1420
|
+
const names = new Map();
|
|
1421
|
+
const column = {
|
|
1422
|
+
id: 'owner', title: 'Owner',
|
|
1423
|
+
value: {
|
|
1424
|
+
deps: ['ownerId'],
|
|
1425
|
+
compute: (deps) => names.get(deps.ownerId) ?? 'Loading…',
|
|
1426
|
+
},
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1429
|
+
const missing = [...new Set(grid.rows.data().map((r) => r.ownerId))].filter((id) => !names.has(id));
|
|
1430
|
+
const resolved = await fetchNames(missing); <span class="cmt">// { id: name }</span>
|
|
1431
|
+
for (const id of missing) names.set(id, resolved[id]);
|
|
1432
|
+
const rows = grid.rows.data().filter((r) => missing.includes(r.ownerId)).map((r) => r.id);
|
|
1433
|
+
grid.rows.refresh({ rows, columns: ['owner'], force: true });
|
|
1434
|
+
|
|
1435
|
+
<span class="cmt">// Or declare the column `pure: false` and it re-reads `names` on every</span>
|
|
1436
|
+
<span class="cmt">// paint; then a plain grid.rows.refresh() after the fetch is enough.</span></code></pre>
|
|
1437
|
+
</div>
|
|
1349
1438
|
|
|
1350
1439
|
<h3>Sizing and pinning</h3>
|
|
1351
1440
|
<div class="example">
|
|
@@ -2105,6 +2194,59 @@ grid.filters.quick(''); <span class="cmt">// clear</span></code></pre>
|
|
|
2105
2194
|
<code>notBlank</code> work everywhere.
|
|
2106
2195
|
</p>
|
|
2107
2196
|
|
|
2197
|
+
<h3 id="where-guide">Filters your application owns: <code>where</code></h3>
|
|
2198
|
+
<p class="lead-in">
|
|
2199
|
+
A condition tree can only test what is in a column. Plenty of real filters cannot be written
|
|
2200
|
+
that way — whether this user may see the row, whether you hold an exchange rate for its
|
|
2201
|
+
currency, whether it came back from your last search call. Those go in as <strong>named
|
|
2202
|
+
predicates</strong>, and they compose with everything above.
|
|
2203
|
+
</p>
|
|
2204
|
+
|
|
2205
|
+
<div class="example">
|
|
2206
|
+
<p class="example__label">A permission filter and a toggle, side by side</p>
|
|
2207
|
+
<pre><code>grid.filters.where('visibleToMe', row => row.owner === me, { pinned: true });
|
|
2208
|
+
grid.filters.where('rateKnown', row => rates.has(row.ccy), { deps: ['ccy'] });
|
|
2209
|
+
|
|
2210
|
+
grid.filters.where(); <span class="cmt">// ['visibleToMe', 'rateKnown']</span>
|
|
2211
|
+
grid.filters.where('rateKnown', null); <span class="cmt">// remove just that one</span>
|
|
2212
|
+
grid.filters.reapply('rateKnown'); <span class="cmt">// the rate table arrived late</span></code></pre>
|
|
2213
|
+
</div>
|
|
2214
|
+
|
|
2215
|
+
<div class="why">
|
|
2216
|
+
<p>There is deliberately no "a filter is present" flag. That flag is a second piece of state
|
|
2217
|
+
describing the first, and the two drift: the classic symptom is a grid that filters while the
|
|
2218
|
+
UI insists it is not, or insists it is filtering while every row passes. Here, registering a
|
|
2219
|
+
predicate is what puts it in force, and removing it is what takes it out.</p>
|
|
2220
|
+
</div>
|
|
2221
|
+
|
|
2222
|
+
<p class="lead-in">
|
|
2223
|
+
Three options shape one. <code>deps</code> names the columns the predicate reads, exactly as
|
|
2224
|
+
<code>value.deps</code> does for a computed column: the verdict is then cached per row and
|
|
2225
|
+
re-run when one of <em>those</em> columns changes on that row, not when an unrelated one does.
|
|
2226
|
+
Leave it off and the predicate is assumed to read the whole row, so it runs every pass and can
|
|
2227
|
+
never be stale. <code>pinned</code> makes a predicate survive
|
|
2228
|
+
<code>filters.clear()</code>, which is what you want for permissions and tenant scoping and
|
|
2229
|
+
not much else. <code>condition</code> gives the predicate a declarative twin that is pushed to
|
|
2230
|
+
the source while the function stays as the residual, so a pushdown engine narrows the fetch
|
|
2231
|
+
instead of your code filtering a page.
|
|
2232
|
+
</p>
|
|
2233
|
+
|
|
2234
|
+
<div class="why">
|
|
2235
|
+
<p><strong>Migrating from AG Grid's external filter:</strong> its three pieces become two.
|
|
2236
|
+
<code>isExternalFilterPresent()</code> goes away, because registration is presence.
|
|
2237
|
+
<code>doesExternalFilterPass(node)</code> becomes the predicate itself.
|
|
2238
|
+
<code>onFilterChanged()</code> becomes <code>deps</code> where the grid can watch the change
|
|
2239
|
+
for you, and <code>reapply(name?)</code> where it cannot.</p>
|
|
2240
|
+
</div>
|
|
2241
|
+
|
|
2242
|
+
<div class="why">
|
|
2243
|
+
<p><strong>What travels in a saved view is the name, not the function.</strong>
|
|
2244
|
+
<code>state.get()</code> carries <code>where: string[]</code>; your predicates are your code
|
|
2245
|
+
and the grid will not pretend it can serialise them. Applying a view that names a predicate
|
|
2246
|
+
you have not registered <em>reports</em> the skip instead of quietly showing a wider row set,
|
|
2247
|
+
and never removes a predicate the view did not mention.</p>
|
|
2248
|
+
</div>
|
|
2249
|
+
|
|
2108
2250
|
<h2 id="grouping">Grouping, totals and pivot</h2>
|
|
2109
2251
|
<div class="example">
|
|
2110
2252
|
<p class="example__label">Group by one or more columns</p>
|
|
@@ -2613,13 +2755,94 @@ createGrid(right, { columns, rows,
|
|
|
2613
2755
|
<table>
|
|
2614
2756
|
<thead><tr><th>Event</th><th>Fired on</th><th>Carries</th></tr></thead>
|
|
2615
2757
|
<tbody>
|
|
2616
|
-
<tr><td class="name">
|
|
2758
|
+
<tr><td class="name">beforeRowReceive</td><td class="desc">the target, before the insert</td><td class="desc"><code>{ data, at, overKey, source }</code>, cancellable</td></tr>
|
|
2759
|
+
<tr><td class="name">rowReceive:cancelled</td><td class="desc">the target, on a veto</td><td class="desc"><code>{ data, at, overKey, source, reason }</code></td></tr>
|
|
2760
|
+
<tr><td class="name">row:received</td><td class="desc">the target</td><td class="desc"><code>{ data, at, overKey, rejected }</code></td></tr>
|
|
2617
2761
|
<tr><td class="name">row:sent</td><td class="desc">the source, on a move</td><td class="desc"><code>{ key, data, mode }</code></td></tr>
|
|
2618
2762
|
<tr><td class="name">row:copied</td><td class="desc">the source, on a copy</td><td class="desc"><code>{ key, data, mode }</code></td></tr>
|
|
2619
2763
|
</tbody>
|
|
2620
2764
|
</table>
|
|
2621
2765
|
</div>
|
|
2622
2766
|
|
|
2767
|
+
<p class="lead-in">
|
|
2768
|
+
Those events all describe a drag that has <em>settled</em>. To follow one while it is
|
|
2769
|
+
happening — to highlight a candidate row, drive your own drop indicator, or update a side
|
|
2770
|
+
panel as the row travels — there are four more, and they all fire on the grid the drag
|
|
2771
|
+
<strong>started</strong> in, for a same-grid reorder and a cross-grid transfer alike. A drag is
|
|
2772
|
+
one gesture with one owner, and the source is the only grid present for the whole of it, where
|
|
2773
|
+
the pointer may cross several others or none; <code>over</code> names whichever grid the event
|
|
2774
|
+
is about, so one subscription can decorate any of them.
|
|
2775
|
+
</p>
|
|
2776
|
+
|
|
2777
|
+
<div class="table-wrap">
|
|
2778
|
+
<table>
|
|
2779
|
+
<thead><tr><th>Event</th><th>Fires when</th><th>Carries</th></tr></thead>
|
|
2780
|
+
<tbody>
|
|
2781
|
+
<tr><td class="name">rowDrag:started</td><td class="desc">the press passed the drag threshold</td><td class="desc"><code>{ key, data, over, at, overKey }</code></td></tr>
|
|
2782
|
+
<tr><td class="name">rowDrag:moved</td><td class="desc">the pointer is over a candidate position; <strong>at most once per animation frame</strong></td><td class="desc"><code>{ key, data, over, at, overKey }</code></td></tr>
|
|
2783
|
+
<tr><td class="name">rowDrag:left</td><td class="desc">the pointer left a grid; <code>over</code> is the grid it left</td><td class="desc"><code>{ key, data, over, at: null, overKey: null }</code></td></tr>
|
|
2784
|
+
<tr><td class="name">rowDrag:ended</td><td class="desc">the gesture ended, drop or no drop</td><td class="desc"><code>{ key, data, over, at, overKey, dropped }</code></td></tr>
|
|
2785
|
+
</tbody>
|
|
2786
|
+
</table>
|
|
2787
|
+
</div>
|
|
2788
|
+
|
|
2789
|
+
<div class="example">
|
|
2790
|
+
<p class="example__label">Highlight the row a drag is hovering, and clean up however it ends</p>
|
|
2791
|
+
<pre><code>backlog.on('rowDrag:moved', (e) => {
|
|
2792
|
+
<span class="cmt">// e.over is the grid under the pointer, null over none.</span>
|
|
2793
|
+
paintCandidate(e.over, e.overKey); <span class="cmt">// overKey is null where there is no row to name</span>
|
|
2794
|
+
});
|
|
2795
|
+
backlog.on('rowDrag:left', (e) => clearCandidate(e.over));
|
|
2796
|
+
backlog.on('rowDrag:ended', (e) => {
|
|
2797
|
+
clearCandidate(e.over); <span class="cmt">// always fires, even released off every grid</span>
|
|
2798
|
+
<span class="kw">if</span> (!e.dropped) toast('Nothing moved');
|
|
2799
|
+
});</code></pre>
|
|
2800
|
+
</div>
|
|
2801
|
+
|
|
2802
|
+
<div class="why">
|
|
2803
|
+
<p><strong>Notifications, not gates.</strong> None of the four is cancellable and none carries
|
|
2804
|
+
<code>preventDefault</code>. The drop is already vetoable twice over — <code>beforeRowMove</code>
|
|
2805
|
+
for a reorder, <code>beforeRowReceive</code> for a drop into another grid — and a third veto on
|
|
2806
|
+
the same gesture would be a third place to look when a drop does not happen.</p>
|
|
2807
|
+
<p><strong><code>rowDrag:moved</code> is coalesced to one event per animation frame</strong>,
|
|
2808
|
+
carrying that frame's latest pointer position. A pointer produces several hundred moves a
|
|
2809
|
+
second and a handler that draws cannot usefully run faster than the display, so the rate is
|
|
2810
|
+
capped at the display's rather than the pointer's. The other three fire on the transition
|
|
2811
|
+
itself, and no <code>rowDrag:moved</code> is ever delivered after <code>rowDrag:ended</code>.</p>
|
|
2812
|
+
<p><strong>Read, measure and draw in these handlers; do not mutate.</strong> The drag resolves
|
|
2813
|
+
where it would land against the display order, so changing rows, sort, filters or grouping
|
|
2814
|
+
mid-gesture moves the ground under the drop — and <code>data</code> is the source row's own
|
|
2815
|
+
object rather than a copy, so writing to it edits a row that is still in the grid without
|
|
2816
|
+
announcing the change. Work that changes the grid belongs in <code>beforeRowReceive</code>,
|
|
2817
|
+
which is asked before the insert, or in the settled events afterwards.</p>
|
|
2818
|
+
<p><strong>The end is always reported.</strong> Exactly one <code>rowDrag:ended</code> follows
|
|
2819
|
+
every <code>rowDrag:started</code>, including a release outside every grid, where
|
|
2820
|
+
<code>over</code> is null. A press that never passes the drag threshold is a click and raises
|
|
2821
|
+
none of them; a grid destroyed mid-drag raises no <code>rowDrag:ended</code>.</p>
|
|
2822
|
+
</div>
|
|
2823
|
+
|
|
2824
|
+
<p class="lead-in">
|
|
2825
|
+
A drop can mean something other than a move. Dragging a backlog row onto a row in another
|
|
2826
|
+
grid often means <em>assign this to that</em>: the host wants to know which row it landed on,
|
|
2827
|
+
record the relationship, and keep the row where it was. <code>beforeRowReceive</code> fires on
|
|
2828
|
+
the receiving grid before the insert, naming the row under the pointer as <code>overKey</code>
|
|
2829
|
+
(null past the last row, on empty space, on the header or on a pinned row) and the grid it came
|
|
2830
|
+
from as <code>source</code>. <code>preventDefault(reason)</code> stops the insert, and the source
|
|
2831
|
+
grid is untouched: the row stays, and neither <code>row:sent</code> nor <code>row:copied</code>
|
|
2832
|
+
fires. The handler may be <code>async</code>, as every before-event may; a drop whose row under
|
|
2833
|
+
the pointer, or source row, is gone by the time it settles is cancelled as <code>'stale'</code>.
|
|
2834
|
+
</p>
|
|
2835
|
+
|
|
2836
|
+
<div class="example">
|
|
2837
|
+
<p class="example__label">Assign on drop, rather than move</p>
|
|
2838
|
+
<pre><code>assigned.on('beforeRowReceive', (e) => {
|
|
2839
|
+
<span class="kw">if</span> (e.overKey === <span class="kw">null</span>) <span class="kw">return</span>; <span class="cmt">// dropped on no row: let it move</span>
|
|
2840
|
+
e.preventDefault('assigned'); <span class="cmt">// the backlog row stays in the backlog</span>
|
|
2841
|
+
assign(e.data.id, e.overKey); <span class="cmt">// the relationship is the change</span>
|
|
2842
|
+
});
|
|
2843
|
+
assigned.on('rowReceive:cancelled', (e) => console.log(e.reason)); <span class="cmt">// 'assigned'</span></code></pre>
|
|
2844
|
+
</div>
|
|
2845
|
+
|
|
2623
2846
|
<div class="why">
|
|
2624
2847
|
<p><strong>Off by default, and both ends have to agree.</strong> Rows leaving a grid is a data
|
|
2625
2848
|
change you have to want: a grid that quietly let its rows be dragged away would lose one to a
|
|
@@ -3342,6 +3565,15 @@ createGrid(right, {
|
|
|
3342
3565
|
hundred updates against a two hundred thousand row source cost under 300 ms in total. Set
|
|
3343
3566
|
<code>refresh</code> to <code>live</code>, <code>manual</code> or a number of milliseconds to
|
|
3344
3567
|
change the coalescing; <code>idle</code> is the default and settles to a frame.</p>
|
|
3568
|
+
<p><strong><code>manual</code> means the host says when, and <code>rows.load()</code> is how it
|
|
3569
|
+
says it.</strong> A <code>manual</code> derived grid never re-derives on its own: the source can
|
|
3570
|
+
filter, edit and tick underneath it and the panel keeps showing what it last derived. Call
|
|
3571
|
+
<code>rows.load()</code> <em>on the derived grid</em>, with no argument, and it re-reads its
|
|
3572
|
+
<code>from</code> there and then and replaces its rows; call it again whenever you want the next
|
|
3573
|
+
reading. A derived grid takes its rows from <code>from</code>, so anything passed to
|
|
3574
|
+
<code>load</code> is not used. The derived grid's own sort and filters stay as they were. Press
|
|
3575
|
+
it as often as you like — the source is left exactly as one press leaves it, and
|
|
3576
|
+
destroying the panel leaves nothing of it attached there.</p>
|
|
3345
3577
|
<p><strong>Derived grids are read-only.</strong> There is one copy of the data and it lives in
|
|
3346
3578
|
the source. Write there and the derived grid follows: an edit through the source's editing
|
|
3347
3579
|
API (<code>edit.setCells</code>, an inline commit, a fill, a paste, an undo, a redo or an
|
|
@@ -3358,6 +3590,59 @@ createGrid(right, {
|
|
|
3358
3590
|
the source writes onto every row it produces: the group value, the profiled column, or the
|
|
3359
3591
|
source row's own key when nothing is grouped. Set <code>rowKey</code> only to override it.</p>
|
|
3360
3592
|
</div>
|
|
3593
|
+
<div class="example" id="derived-manual-refresh">
|
|
3594
|
+
<p class="example__label">A frozen panel, refreshed on a button press, executed</p>
|
|
3595
|
+
<pre data-run="js" data-expect="20 20 10 10 5; steady; released" data-covers="config:refresh method:rows method:diagnostics"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
3596
|
+
|
|
3597
|
+
<span class="kw">const</span> detail = createHeadlessGrid({
|
|
3598
|
+
rowKey: 'id',
|
|
3599
|
+
columns: [{ field: 'region' }, { field: 'capacity', type: 'number' }],
|
|
3600
|
+
rows: Array.from({ length: <span class="num">20</span> }, (_, i) => ({ id: i, region: i % <span class="num">2</span> ? 'North' : 'South', capacity: i })),
|
|
3601
|
+
});
|
|
3602
|
+
detail.rows.count();
|
|
3603
|
+
|
|
3604
|
+
<span class="cmt">// What the detail grid is listening with before any panel is attached.</span>
|
|
3605
|
+
<span class="kw">const</span> listening = () => Object.values(detail.diagnostics.events()).reduce((a, b) => a + b, <span class="num">0</span>);
|
|
3606
|
+
<span class="kw">const</span> alone = listening();
|
|
3607
|
+
|
|
3608
|
+
<span class="cmt">// A summary that derives once, then waits to be told.</span>
|
|
3609
|
+
<span class="kw">const</span> panel = createHeadlessGrid({
|
|
3610
|
+
columns: [{ field: 'region' }, { field: 'sites', type: 'number' }],
|
|
3611
|
+
source: { mode: 'derived', from: detail, groupBy: 'region',
|
|
3612
|
+
select: { sites: { fn: 'count' } }, refresh: 'manual' },
|
|
3613
|
+
});
|
|
3614
|
+
|
|
3615
|
+
<span class="cmt">// The Refresh button. In a page this is your <button>; here any EventTarget will do.</span>
|
|
3616
|
+
<span class="kw">const</span> button = <span class="kw">new</span> EventTarget();
|
|
3617
|
+
button.addEventListener('click', () => panel.rows.load()); <span class="cmt">// no argument</span>
|
|
3618
|
+
|
|
3619
|
+
<span class="kw">const</span> sites = () => {
|
|
3620
|
+
<span class="kw">let</span> n = <span class="num">0</span>;
|
|
3621
|
+
panel.rows.forEach((row) => { n += panel.rows.value(row.key, 'sites'); });
|
|
3622
|
+
<span class="kw">return</span> n;
|
|
3623
|
+
};
|
|
3624
|
+
<span class="cmt">// Longer than any automatic refresh would take to land, so "frozen" is a finding.</span>
|
|
3625
|
+
<span class="kw">const</span> settle = () => <span class="kw">new</span> Promise((done) => setTimeout(done, <span class="num">50</span>));
|
|
3626
|
+
|
|
3627
|
+
<span class="kw">const</span> attached = listening();
|
|
3628
|
+
<span class="kw">const</span> seen = [sites()]; <span class="cmt">// 20</span>
|
|
3629
|
+
detail.filters.set({ col: 'capacity', op: 'lt', value: <span class="num">10</span> });
|
|
3630
|
+
<span class="kw">await</span> settle(); seen.push(sites()); <span class="cmt">// 20: the source moved, the panel did not</span>
|
|
3631
|
+
button.dispatchEvent(<span class="kw">new</span> Event('click'));
|
|
3632
|
+
seen.push(sites()); <span class="cmt">// 10: re-derived on the press</span>
|
|
3633
|
+
detail.filters.set({ col: 'capacity', op: 'lt', value: <span class="num">5</span> });
|
|
3634
|
+
<span class="kw">await</span> settle(); seen.push(sites()); <span class="cmt">// 10: frozen again</span>
|
|
3635
|
+
button.dispatchEvent(<span class="kw">new</span> Event('click'));
|
|
3636
|
+
seen.push(sites()); <span class="cmt">// 5</span>
|
|
3637
|
+
|
|
3638
|
+
<span class="cmt">// Pressing it any number of times leaves the detail grid as one press does,</span>
|
|
3639
|
+
<span class="cmt">// and destroying the panel leaves nothing of it behind there.</span>
|
|
3640
|
+
<span class="kw">const</span> steady = listening() === attached ? 'steady' : 'grew';
|
|
3641
|
+
panel.destroy();
|
|
3642
|
+
<span class="kw">const</span> released = listening() === alone ? 'released' : 'left behind';
|
|
3643
|
+
detail.destroy();
|
|
3644
|
+
<span class="kw">return</span> `${seen.join(' ')}; ${steady}; ${released}`;</code></pre>
|
|
3645
|
+
</div>
|
|
3361
3646
|
|
|
3362
3647
|
<h3>Other shapes</h3>
|
|
3363
3648
|
<div class="table-wrap">
|
|
@@ -3561,7 +3846,11 @@ createGrid(right, {
|
|
|
3561
3846
|
next frame; a <strong>number</strong> is a debounce in milliseconds; <code>'live'</code>
|
|
3562
3847
|
derives on every change and is the one to avoid for an expensive analysis over a ticking feed;
|
|
3563
3848
|
<code>'manual'</code> stops automatic derivation entirely, leaving the host to drive the
|
|
3564
|
-
source
|
|
3849
|
+
source: call <code>rows.load()</code> on the derived grid, with no argument, whenever the
|
|
3850
|
+
analysis should be brought up to date — from a Refresh button, when its tab is shown, or
|
|
3851
|
+
on a timer of your own. Each call re-derives once, at the cost in the table above;
|
|
3852
|
+
<a href="#derived-manual-refresh">a frozen panel refreshed on a button press</a> is executed
|
|
3853
|
+
above. Below a few tens of thousands of rows none of this matters.</p>
|
|
3565
3854
|
|
|
3566
3855
|
<h2 id="cross-filter">Cross-filtering</h2>
|
|
3567
3856
|
<p class="lead-in">
|
|
@@ -4610,6 +4899,35 @@ grid.selection.extendRange(34, 'cap'); <span class="cmt">// grows the block jus
|
|
|
4610
4899
|
Selected rows carry <code>aria-selected</code> and the class
|
|
4611
4900
|
<code>lat-row--selected</code>, which the theme styles.
|
|
4612
4901
|
</p>
|
|
4902
|
+
<div class="example">
|
|
4903
|
+
<p class="example__label">A row with its own click action</p>
|
|
4904
|
+
<pre><code>selection: { mode: 'multiple', checkbox: <span class="kw">true</span>, checkboxOnly: <span class="kw">true</span> }</code></pre>
|
|
4905
|
+
</div>
|
|
4906
|
+
<p class="lead-in">
|
|
4907
|
+
<code>checkboxOnly: true</code> restricts row selection to the checkbox column:
|
|
4908
|
+
clicking the checkbox selects or deselects the row, and clicking anywhere else in the
|
|
4909
|
+
row does neither. This is for a host that binds its own action — typically opening a
|
|
4910
|
+
record's detail view — to a plain click on the row: without it, that click also
|
|
4911
|
+
selects the row, and a bulk action run afterwards operates on rows the user never
|
|
4912
|
+
chose to select. The same restriction applies to the keyboard: Space still toggles
|
|
4913
|
+
selection while focus is on the checkbox cell, and does nothing elsewhere. Cell ranges
|
|
4914
|
+
and the fill handle are unaffected either way. Off by default, so a plain click still
|
|
4915
|
+
selects a row exactly as it always has.
|
|
4916
|
+
</p>
|
|
4917
|
+
<p class="lead-in">
|
|
4918
|
+
<code>checkboxOnly</code> only narrows which gesture may change selection; it does not
|
|
4919
|
+
grant selection where <code>mode: 'none'</code> has already refused it, and it composes
|
|
4920
|
+
normally with <code>mode: 'single'</code> — the checkbox remains the only way to change
|
|
4921
|
+
which one row is selected.
|
|
4922
|
+
</p>
|
|
4923
|
+
<p class="lead-in">
|
|
4924
|
+
With <code>mode: 'single'</code>, checking a row's box selects that row and replaces
|
|
4925
|
+
whichever one was selected before; unchecking the selected row clears the selection.
|
|
4926
|
+
<code>grid.selection.set(keys)</code> itself keeps only the <em>first</em> key of whatever
|
|
4927
|
+
array it is given when <code>mode</code> is <code>'single'</code> — that contract is
|
|
4928
|
+
unchanged (BACKLOG-0001233) — so the checkbox column never builds a two-key array to hand
|
|
4929
|
+
it; it sets the one key it just toggled.
|
|
4930
|
+
</p>
|
|
4613
4931
|
|
|
4614
4932
|
<div class="why">
|
|
4615
4933
|
<p><strong>Ctrl+Shift+Arrow is the keyboard form of ctrl-dragging.</strong> The first press
|
|
@@ -4743,6 +5061,15 @@ createChart({
|
|
|
4743
5061
|
chart stops moving, even though time is still passing — and the silence is usually the
|
|
4744
5062
|
thing worth seeing. <code>maxAge</code> is a span of wall clock, so it means the same thing
|
|
4745
5063
|
whatever the feed is doing.</p>
|
|
5064
|
+
<p><strong>An empty reduction is a gap, not a zero.</strong> <code>fn: 'avg'</code> above
|
|
5065
|
+
— and <code>sum</code>, <code>mean</code>, <code>min</code>, <code>max</code>,
|
|
5066
|
+
<code>first</code> and <code>last</code> beside it — read as <code>null</code> when a
|
|
5067
|
+
bucket carries no rows to reduce (BACKLOG-0001088), so a producer that has stopped sending is
|
|
5068
|
+
drawn as a break in the line rather than a value dropping to zero, which would read as a real
|
|
5069
|
+
observation nobody made. <code>count</code> and <code>countValues</code> are the deliberate
|
|
5070
|
+
exception: they are already honest at zero, a tally of rows or of values actually present, so
|
|
5071
|
+
reach for <code>countValues</code> when the reading you actually want is “how many
|
|
5072
|
+
arrived” and zero has to be drawn as zero rather than as a gap.</p>
|
|
4746
5073
|
<p><strong>Two bounds, one eviction path.</strong> <code>maxAge</code> and
|
|
4747
5074
|
<code>maxRows</code> are independent and compose: both are applied on the same pass and
|
|
4748
5075
|
whichever bites first is simply the one that drops rows. Neither is silently ignored.
|
|
@@ -4801,6 +5128,16 @@ createChart({
|
|
|
4801
5128
|
that far ahead the chart shows its empty state, with the same warning.
|
|
4802
5129
|
<code>chart.data().windowed</code> counts the readings dropped at either edge of the
|
|
4803
5130
|
window. The fix for the warning is the producer’s clock, not a wider window.</p>
|
|
5131
|
+
<p><strong>The other edge: a producer that has simply stopped.</strong> The case above is a
|
|
5132
|
+
clock running fast; the opposite is a feed that has gone quiet for longer than the window
|
|
5133
|
+
(BACKLOG-0001088) — every reading is older than the span, so every mark would fall to
|
|
5134
|
+
the left of the domain, off the plot, while the axes and legend keep drawing as if the chart
|
|
5135
|
+
were healthy. Rather than draw that, the chart shows its empty state and warns once <strong>per
|
|
5136
|
+
chart instance</strong>, naming the span and how old the newest reading actually is, so a dead
|
|
5137
|
+
feed reads as “no data” rather than as a chart that quietly stopped moving. Two
|
|
5138
|
+
charts bound to the same stale column each get their own warning — the key is scoped to
|
|
5139
|
+
the chart, not just the column, so a dashboard of tiled charts sharing one timestamp column
|
|
5140
|
+
does not lose the second warning to the first.</p>
|
|
4804
5141
|
<p><strong>Idle costs nothing.</strong> Both halves advance on a plain interval — a
|
|
4805
5142
|
quarter of the window, clamped to between 50 ms and one second — and never on an
|
|
4806
5143
|
animation frame. The source’s wake returns after a single number comparison unless a
|
|
@@ -7061,7 +7398,7 @@ grid.import.apply(preview);</code></pre>
|
|
|
7061
7398
|
<tr><td class="name">deriveRangeSpec</td><td class="desc">Decide what a chart of a range should be without drawing it: the type, the category column, the measures, and a spec ready for <code>createChart</code>.</td></tr>
|
|
7062
7399
|
<tr><td class="name">regressionPlots</td><td class="desc">Turn a fitted regression model into diagnostic chart specs ready for <code>createChart</code>: the fit line with its confidence band, residuals-vs-fitted, a QQ plot of the residuals, and a multicollinearity correlogram with the model’s VIF. The plots that need a per-row or per-coefficient quantity the grid has no column for (scale-location, residuals-vs-leverage, the coefficient forest) are returned as a null spec carrying the reason rather than dropped.</td></tr>
|
|
7063
7400
|
<tr><td class="name">createDataRouter</td><td class="desc">Split one arriving stream or dataset across many grids by what each record is — a property or a predicate — driving each grid through the public keyed <code>rows.apply</code> path so a snapshot is a diff, a delta is applied in place, a moved partition moves the row rather than duplicating it, and an unmatched record is counted, sunk and never dropped. By default routing is first-match-wins (<code>overlap: false</code>); to fan one partition value to several viewers at once (a grid <em>and</em> a KPI panel <em>and</em> a chart off one feed) create the router with <code>overlap: true</code> — with the default, a second viewer on the same value receives nothing and the router emits a one-time dev warning naming the clash. v2 adds cross-grid selection filtering: <code>link(source, target, relation)</code> makes a selection in one grid filter what another receives — by a key map or a predicate function, multi-select as an IN set, debounced — re-pushed through the same keyed-diff path so the target stays dumb. v5 adds wedge-conversion primitives: <code>subscribe(value, handler)</code> routes a slice to any non-grid view (KPI tile, detail pane, map, form) as the same keyed diff a grid gets; <code>alert(value, condition, handler)</code> evaluates a condition over a slice and emits (edge-triggered, debounced) rather than rendering; and <code>configure(spec)</code> (or <code>createDataRouter({ config })</code>) takes the whole routing graph as one declarative data spec that desugars to the imperative API and composes with it. v3 also adds per-route reshaping — <code>transform</code>/<code>filter</code>/<code>sort</code> and <code>rollup</code> ({ groupBy, aggregate }) summaries — a relationship graph (<code>relate(edges)</code>: multi-hop, several-into-one AND, and mutual edges) that scales v2's pairwise <code>link</code>, and stream hygiene: a <code>seq</code>/version orders and de-duplicates a feed (stale/duplicate deltas dropped, counted in <code>dropped</code>), <code>push</code> with a <code>batch</code>/<code>coalesce</code> buffers a high-frequency feed (<code>flushStream</code> for a deterministic point), and <code>lastSeq</code>/<code>checkpoint</code>/<code>seenThrough</code> resume precisely after a dropped socket. v4 adds time-travel: <code>buffer({ window, max })</code> records the ordered stream into a bounded ring over a moving base, so <code>scrubTo</code> reconstructs a past point, <code>replay</code> (with <code>pause</code>/<code>resume</code>) walks a range, and <code>live</code> returns to the head — every state pushed by the same keyed diff, <code>traveling</code>/<code>buffered</code> reporting the state. v6 adds cross-tab sync: <code>broadcast({ channel })</code> mirrors the ordered, de-duplicated deltas to other tabs/windows over a BroadcastChannel with no echo loop, a popped-out grid joining the same feed with no second socket and resyncing mid-stream via the reconnect path. v7 adds query-slice routing: <code>query(adapter, request)</code> sources the router from a DFQL/DuckDB (or any pushdown) adapter, partitioning one result across the routes; a route-level <code>where</code> is pushed down where the adapter's capabilities allow and the residual finished client-side, with <code>lastQueryPlan</code> reporting the split. v8 adds write-back: a <code>writable</code> route captures the grid's committed edits off its public edit surface and routes them to <code>onWrite(change, ctx)</code> (per-route or router-global), reverting on reject, re-entering an accepted write as a delta, and surfacing a last-write-wins <code>onConflict</code>; a derived route cannot be writable. v9 adds fan-in: <code>addSource(feed, { map, key })</code> returns a per-feed handle (<code>load</code>/<code>apply</code>/<code>push</code>/<code>remove</code>) whose rows are normalized and namespaced so many feeds merge into one keyed store without id collisions, <code>removeSource</code> dropping exactly a feed's rows and <code>sources</code> listing them. v10 adds observability: <code>metrics()</code> is a cheap snapshot of per-route/per-source counts and throughput plus the global unrouted/dropped/buffered/lag figures, <code>on('metrics')</code> drives a periodic emit (off unless a listener is registered), and <code>mountDevtools(el)</code> renders a live panel from the module's own DOM file. Detaches its grids on <code>destroy</code>; the host owns them.</td></tr>
|
|
7064
|
-
<tr><td class="name">createKanban</td><td class="desc">A board (kanban) view of rows as cards, grouped into columns by a configurable property (a status, a stage, a state field), with per-column card count and an optional points sum, a WIP over-limit flag, configured columns shown even when empty, granular readonly (whole board / per column / per card), field-mapped card templates that reuse the grid’s own column formatters when a grid is bound, and the core pointer events (<code>card:click</code>, <code>card:dblclick</code>, <code>card:contextmenu</code>). It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, board)</code> and drive a kanban beside a grid and a chart off one feed. Accessibility is built in from the start — a labelled group of labelled column lists, cards in a roving-tabindex focus ring with arrow-key navigation, and a polite live region. Every structural property is named in config (<code>columnProperty</code>, <code>pointsProperty</code>, <code>orderProperty</code>, <code>swimlaneProperty</code>, <code>sprintProperty</code>, <code>epicProperty</code>) so it maps DemandFlow and any customer schema without code change. Pass <code>null</code> as the element for a headless board that computes the same model without a DOM. Cards drag between columns (writing the column property) and within a column into a position (writing the order property with fractional ranking), and the same move is keyboard-accessible — <kbd>Space</kbd> to grab, arrows to choose a target, <kbd>Space</kbd>/<kbd>Enter</kbd> to drop, <kbd>Escape</kbd> to cancel — announced on the live region; multi-select drags every selected card. A move calls <code>onBeforeMove(card, from, to, index)</code> first (return <code>false</code> to veto) and then persists through the grid’s shipped write-back path — grid-bound via <code>grid.edit.setCells</code> (the same public edit-commit path inline editing uses, so the grid’s pipeline owns optimistic apply / confirm / revert), standalone with a revert when <code>onCardMove</code> rejects — emitting <code>card:move</code>. A configurable per-card <code>contextMenu</code> replaces the <code>card:contextmenu</code> event when present. With <code>swimlanes: true</code> it renders a 2D lane×column grid grouped by <code>swimlaneProperty</code> (per-lane count/points, columns aligned across lanes, a cross-lane drag writing the swimlane property); columns and lanes collapse (state surviving a keyed-diff update), columns reorder by header drag, and <code>setQuickFilter</code>/<code>setFilter</code>/<code>facets</code> drive search and faceting. <code>setSprint</code>/<code>showBacklog</code>/<code>sprints</code> give a sprint view, switcher and backlog; <code>setEpic</code>/<code>epicRollup</code>/<code>rollup</code> give an epic view and rollups (count, points, progress toward the <code>done</code> columns). A card can pop out a nested grid of its children (an epic's stories, a story's tasks, recursively) via a <code>childrenProperty</code> and/or <code>loadChildren(card)</code>: the child is a full composed <code>createGrid</code> (or, with <code>asBoard</code>, a nested board) opened in a drawer/modal/inline container — reuse by composition, no grid-core coupling — emitting <code>card:expand</code>/<code>card:drill</code>. Live updates arrive through the same keyed-diff contract a grid uses, so a Data Router drives the board directly (<code>attach(board, predicate)</code>); a live <code>rows.apply</code> re-renders preserving scroll, focus, selection, collapse and any open pop-out. <code>virtualize</code> renders only a scroll window of a tall column; <code>getState</code>/<code>setState</code> (and <code>config.state</code>) save and restore collapse, order, filter and sprint/epic selection; <code>setLoading</code>/<code>setError</code> give loading and error states. The move is fully keyboard-driven — Space to grab, arrows for column/position, Alt+Up/Down across swimlanes, Space/Enter to drop, Escape to cancel — announced on a live region. A field opted in with <code>card: { title: { field, edit: true } }</code> edits inline (double-click or <code>editCard</code>): grid-bound through the grid's own field editor via its public edit path, standalone through a host editor factory or a default input with an <code>onCardEdit</code> revert; a per-column add-card (<code>config.addCard</code>/<code>onAddCard</code>, or <code>grid.edit.addRow</code>) creates a card and opens it in edit. The module imports nothing from the grid's DOM package.</td></tr>
|
|
7401
|
+
<tr><td class="name">createKanban</td><td class="desc">A board (kanban) view of rows as cards, grouped into columns by a configurable property (a status, a stage, a state field), with per-column card count and an optional points sum, a WIP over-limit flag, configured columns shown even when empty, granular readonly (whole board / per column / per card), field-mapped card templates that reuse the grid’s own column formatters when a grid is bound, and the core pointer events (<code>card:click</code>, <code>card:dblclick</code>, <code>card:contextmenu</code>). It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, board)</code> and drive a kanban beside a grid and a chart off one feed. Accessibility is built in from the start — a labelled group of labelled column lists, cards in a roving-tabindex focus ring with arrow-key navigation, and a polite live region. Every structural property is named in config (<code>columnProperty</code>, <code>pointsProperty</code>, <code>orderProperty</code>, <code>swimlaneProperty</code>, <code>sprintProperty</code>, <code>epicProperty</code>) so it maps DemandFlow and any customer schema without code change. Pass <code>null</code> as the element for a headless board that computes the same model without a DOM. Cards drag between columns (writing the column property) and within a column into a position (writing the order property with fractional ranking), and the same move is keyboard-accessible — <kbd>Space</kbd> to grab, arrows to choose a target, <kbd>Space</kbd>/<kbd>Enter</kbd> to drop, <kbd>Escape</kbd> to cancel — announced on the live region; multi-select drags every selected card. A move calls <code>onBeforeMove(card, from, to, index)</code> first (return <code>false</code> to veto) and then persists through the grid’s shipped write-back path — grid-bound via <code>grid.edit.setCells</code> (the same public edit-commit path inline editing uses, so the grid’s pipeline owns optimistic apply / confirm / revert), standalone with a revert when <code>onCardMove</code> rejects — emitting <code>card:move</code>. A configurable per-card <code>contextMenu</code> replaces the <code>card:contextmenu</code> event when present. With <code>swimlanes: true</code> it renders a 2D lane×column grid grouped by <code>swimlaneProperty</code> (per-lane count/points, columns aligned across lanes, a cross-lane drag writing the swimlane property); columns and lanes collapse (state surviving a keyed-diff update), columns reorder by header drag, and <code>setQuickFilter</code>/<code>setFilter</code>/<code>facets</code> drive search and faceting. <code>setSprint</code>/<code>showBacklog</code>/<code>sprints</code> give a sprint view, switcher and backlog; <code>setEpic</code>/<code>epicRollup</code>/<code>rollup</code> give an epic view and rollups (count, points, progress toward the <code>done</code> columns). A card can pop out a nested grid of its children (an epic's stories, a story's tasks, recursively) via a <code>childrenProperty</code> and/or <code>loadChildren(card)</code>: the child is a full composed <code>createGrid</code> (or, with <code>asBoard</code>, a nested board) opened in a drawer/modal/inline container — reuse by composition, no grid-core coupling — emitting <code>card:expand</code>/<code>card:drill</code>. Live updates arrive through the same keyed-diff contract a grid uses, so a Data Router drives the board directly (<code>attach(board, predicate)</code>); on a standalone board (not <code>grid</code>-bound, which ignores <code>rows.apply</code> and routes to the bound grid instead) <code>rows.apply({ update })</code> merges each patch into the row already stored under its key, matching the grid's own <code>rows.apply({ update })</code> — a field the patch omits is preserved, and a patch that does change the grouping property still moves the card — while <code>add</code> sets the row outright; a live <code>rows.apply</code> re-renders preserving scroll, focus, selection, collapse and any open pop-out. <code>virtualize</code> renders only a scroll window of a tall column; <code>getState</code>/<code>setState</code> (and <code>config.state</code>) save and restore collapse, order, filter and sprint/epic selection; <code>setLoading</code>/<code>setError</code> give loading and error states. The move is fully keyboard-driven — Space to grab, arrows for column/position, Alt+Up/Down across swimlanes, Space/Enter to drop, Escape to cancel — announced on a live region. A field opted in with <code>card: { title: { field, edit: true } }</code> edits inline (double-click or <code>editCard</code>): grid-bound through the grid's own field editor via its public edit path, standalone through a host editor factory or a default input with an <code>onCardEdit</code> revert; a per-column add-card (<code>config.addCard</code>/<code>onAddCard</code>, or <code>grid.edit.addRow</code>) creates a card and opens it in edit. The module imports nothing from the grid's DOM package.</td></tr>
|
|
7065
7402
|
<tr><td class="name">createKPI</td><td class="desc">A KPI / stat-tile view (module <code>kpi</code>) of a dataset as a panel of aggregate tiles — each tile a <code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countDistinct</code> or a <code>custom</code> reducer over the routed rows, with an optional <code>filter</code> predicate, number <code>format</code> (<code>number</code>/<code>currency</code>/<code>percent</code>/<code>compact</code>), a <code>baseline</code> for a delta, semantic threshold bands (<code>thresholds</code> with two cut points and a direction, or an explicit <code>bands</code> list, giving a <code>good</code>/<code>warn</code>/<code>critical</code> status kept separate from any accent), and an optional <code>sparkline</code> series. It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, kpi)</code> and drive a KPI panel beside a grid, a kanban and a chart off one feed. Updates are incremental — a delta adjusts each tile's running accumulator by only the rows it carries (an add contributes, a remove reverses, an update reverses-then-contributes), the two bounded exceptions being a <code>min</code>/<code>max</code> whose current extreme is removed (a rescan of that tile's own value multiset) and a <code>custom</code> reducer (recomputed over the filtered store, an arbitrary function having no inverse). Each tile is a labelled <code><figure></code>, focusable and keyboard-activatable, its value announced, the sparkline respecting <code>prefers-reduced-motion</code>; a <code>tile:click</code> event (also from the keyboard) lets a host drill down or filter a routed grid. Pass <code>null</code> as the element for a headless panel that computes the same tile model without a DOM. Not a dashboard layout engine (that is the parked dashboard generator) and no charting beyond the minimal sparkline (that is the charts module).</td></tr>
|
|
7066
7403
|
<tr><td class="name">createAI</td><td class="desc">Create an AI narrative / insights controller (module <code>ai</code>) over a live grid. It produces a short, plain-language narrative of the grid's <em>computed</em> figures — a per-KPI / per-chart / per-column <code>explain</code>, or an <code>insights</code> panel over the current filtered view. The grid makes no AI call of its own: <code>createAI</code> imports no provider SDK, holds no key, and makes no network request; it calls one host callback, <code>ask({ system, messages, prompt, tools?, schema?, signal })</code> — your model, your key, your privacy decision — the same philosophy as the data adapters. Two grounding paths feed one guard: where the provider offers tool-use the model is given a curated read-only tool set (<code>getSchema</code>, <code>getProfile</code>, <code>getStatistics</code>, <code>getForecast</code>, <code>runQuery</code>) and the grid's own engine computes what it asks for; otherwise the module builds a facts packet from <code>grid.statistics</code>/the profile/forecasts/view counts and passes it in the prompt. Every figure in the narrative is reconciled against the values the engine produced this render — an ungrounded number is stripped before display (the number-reconciliation guard), so a hallucinated figure never reaches the user. The prompt is constrained to narrate-only; the layer is read-only and never mutates data. <code>redact</code> (a column id, a list, or a predicate) and <code>maxRows</code> bound what the module hands <code>ask()</code>, and the module sends nothing itself; an <code>ask()</code> error surfaces a friendly message and the grid stays fully usable, AI being additive rather than load-bearing. Complementary to <code>grid.ai</code> (the intent/plan skill layer): pass no <code>ask</code> and it adopts the grid's configured <code>ai.ask</code>, running the facts-packet path over it. Pass a headless grid for a DOM-free narrative; <code>facts(target)</code> returns the exact grounded packet without calling <code>ask()</code>. UMD global <code>LatticeGridAI</code>.</td></tr>
|
|
7067
7404
|
<tr><td class="name">createTabs</td><td class="desc">Create a tabbed grid (module <code>tabs</code>): a <code>role="tablist"</code> strip above a stack of <code>role="tabpanel"</code> regions, each hosting its own, independently-configured <code>createGrid</code> instance — "configure each tab as per a normal grid" rather than one grid whose state is swapped (<code>ColumnModel#applyState</code> only repositions/hides/resizes existing columns by id; it carries no field, type or row data, so a state-swap only works when every tab shares one schema). <code>createGrid</code> is injected (<code>createTabs(el, { createGrid, tabs })</code>), the same pattern the React/Vue/Svelte adapters use, so the module imports no engine code and adds nothing to a page that does not load it. A tab that names <code>from: '<tabId>'</code> gets a <code>source: { mode: 'derived', from: <the parent tab’s live grid>, where, group, join, … }</code> wired for it automatically — reusing the shipped derived-source mechanism rather than a new config-inheritance one — and activating a derived tab materialises its whole ancestor chain first; a cyclic <code>from</code> graph is refused (naming the exact cycle) when <code>createTabs</code> is called, not at first click. A tab’s grid mounts on first activation and then stays alive, hidden, so its scroll/selection/filters/sort/grouping/expansion — and an open cell/row editor, left exactly as it was, uncommitted and undiscarded — survive a switch natively; <code>destroy()</code> tears every mounted tab down. The strip is a real tablist with <code>aria-selected</code>, a roving <code>tabindex</code>, and manual-activation keyboard handling (arrows/Home/End move focus, Enter/Space or a click activates). Events: <code>tab:changed</code>, a cancellable <code>beforeTabChange</code> paired with <code>tabChange:cancelled</code>. UMD global <code>LatticeGridTabs</code>.</td></tr>
|
|
@@ -7218,6 +7555,10 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
|
|
|
7218
7555
|
<tr><td class="name">row:moved</td><td class="desc">A row was dragged to a new position.</td></tr>
|
|
7219
7556
|
<tr><td class="name">row:received</td><td class="desc">A row arrived from a source.</td></tr>
|
|
7220
7557
|
<tr><td class="name">row:sent</td><td class="desc">A row was written back to a source.</td></tr>
|
|
7558
|
+
<tr><td class="name">rowDrag:started</td><td class="desc">A row drag began. Fires on the grid the drag started in, as do rowDrag:moved, rowDrag:left and rowDrag:ended.</td></tr>
|
|
7559
|
+
<tr><td class="name">rowDrag:moved</td><td class="desc">The drag is over a candidate position. Coalesced to one event per animation frame, carrying that frame's latest pointer position.</td></tr>
|
|
7560
|
+
<tr><td class="name">rowDrag:left</td><td class="desc">The pointer left a grid; over names the grid it left. Emitted on the transition, not on a frame.</td></tr>
|
|
7561
|
+
<tr><td class="name">rowDrag:ended</td><td class="desc">The gesture ended, whether or not a drop followed, including a release outside every grid. dropped says whether the release is being acted on. Notifications only — none of the four is cancellable.</td></tr>
|
|
7221
7562
|
<tr><td class="name">rows:changed</td><td class="desc">The row set changed. See what a change firing promises: identified means the three arrays name the rows that moved, companion marks a duplicate announcement of a change already made with identity, and a firing with neither is a real change of unknown extent.</td></tr>
|
|
7222
7563
|
<tr><td class="name">rows:deferred</td><td class="desc">Updates were held rather than applied, because an edit is in flight.</td></tr>
|
|
7223
7564
|
<tr><td class="name">rows:paused</td><td class="desc">A live feed was paused; updates queue from here.</td></tr>
|
|
@@ -7287,7 +7628,7 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
|
|
|
7287
7628
|
<tr><td class="name">filter:changed</td><td class="desc">The condition tree or the quick filter changed.</td></tr>
|
|
7288
7629
|
<tr><td class="name">page:changed</td><td class="desc">Fired after the rows have moved, whether the page changed by API or by the pager control.</td></tr>
|
|
7289
7630
|
<tr><td class="name">sort:changed</td><td class="desc">The full sort entry list.</td></tr>
|
|
7290
|
-
<tr><td class="name">state:changed</td><td class="desc">report lists anything a restore could not apply.</td></tr>
|
|
7631
|
+
<tr><td class="name">state:changed</td><td class="desc">Every state change, whether a user gesture or a programmatic call — including a named filters.where predicate registered, replaced, removed or reapplied (BACKLOG-0001235) — announced exactly once. cause is 'user', 'apply' or 'reset'; sections names the GridState keys that moved; report lists anything a restore could not apply. A save layer subscribes to this one event and ignores cause 'reset'.</td></tr>
|
|
7291
7632
|
<tr><td class="name">state:reset</td><td class="desc">The grid was returned to its baseline.</td></tr>
|
|
7292
7633
|
<tr><td class="name">timeline:attached</td><td class="desc">A time brush was connected to the grid.</td></tr>
|
|
7293
7634
|
<tr><td class="name">timeline:detached</td><td class="desc">The brush was removed.</td></tr>
|
|
@@ -7352,6 +7693,7 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
|
|
|
7352
7693
|
<tr><td class="name">beforeDelete</td><td class="desc">Before an optimistic row delete applies — the canonical confirm-before-delete hook. Paired with delete:cancelled.</td></tr>
|
|
7353
7694
|
<tr><td class="name">beforeRowMove</td><td class="desc">Before a row reorder applies. Paired with rowMove:cancelled.</td></tr>
|
|
7354
7695
|
<tr><td class="name">beforeGroup</td><td class="desc">Before a group/tree expand or collapse applies. Paired with group:cancelled.</td></tr>
|
|
7696
|
+
<tr><td class="name">beforeRowReceive</td><td class="desc">Before a row dragged from another grid is inserted into this one; fires on the receiving grid and names the row under the pointer (overKey). A veto leaves the source grid untouched. Paired with rowReceive:cancelled.</td></tr>
|
|
7355
7697
|
<tr><td class="name">edit:cancelled</td><td class="desc">A beforeEdit was vetoed; reason is 'stale' when a live delta moved the cell during an async gate.</td></tr>
|
|
7356
7698
|
<tr><td class="name">sort:cancelled</td><td class="desc">A beforeSort was vetoed.</td></tr>
|
|
7357
7699
|
<tr><td class="name">filter:cancelled</td><td class="desc">A beforeFilter was vetoed.</td></tr>
|
|
@@ -7363,6 +7705,7 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
|
|
|
7363
7705
|
<tr><td class="name">delete:cancelled</td><td class="desc">A beforeDelete was vetoed; reason is 'stale' when the row was already gone.</td></tr>
|
|
7364
7706
|
<tr><td class="name">rowMove:cancelled</td><td class="desc">A beforeRowMove was vetoed; reason is 'stale' when the row had moved.</td></tr>
|
|
7365
7707
|
<tr><td class="name">group:cancelled</td><td class="desc">A beforeGroup was vetoed.</td></tr>
|
|
7708
|
+
<tr><td class="name">rowReceive:cancelled</td><td class="desc">A beforeRowReceive was vetoed; nothing was inserted and the source still holds the row. reason is 'stale' when the row under the pointer or the source row was gone by the time an async handler settled.</td></tr>
|
|
7366
7709
|
<tr><td class="name">export:request</td><td class="desc">A remote export was requested. Past-tense notification.</td></tr>
|
|
7367
7710
|
<tr><td class="name">export:done</td><td class="desc">A remote export completed. Past-tense notification.</td></tr>
|
|
7368
7711
|
<tr><td class="name">shortcuts:opened</td><td class="desc">The keyboard-shortcuts help overlay opened. Past-tense notification.</td></tr>
|
|
@@ -7534,9 +7877,31 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'<
|
|
|
7534
7877
|
});</code></pre>
|
|
7535
7878
|
</div>
|
|
7536
7879
|
|
|
7880
|
+
<h3 id="wrapping-creategrid-recipe">House-wide defaults, without patching <code>createGrid</code></h3>
|
|
7881
|
+
<p class="lead-in">
|
|
7882
|
+
<code>createGrid</code> is exported through a getter with no setter, so
|
|
7883
|
+
<code>LatticeGrid.createGrid = myWrapper</code> does not replace it — silently in a plain
|
|
7884
|
+
script, with a <code>TypeError</code> in a module (see the <a
|
|
7885
|
+
href="API.html#wrapping-creategrid">reference</a> for the exact descriptor). Own the seam
|
|
7886
|
+
yourself instead: one module that every call site imports from.
|
|
7887
|
+
</p>
|
|
7888
|
+
<div class="example">
|
|
7889
|
+
<pre><code><span class="cmt">// lattice.js</span>
|
|
7890
|
+
<span class="kw">import</span> { createGrid <span class="kw">as</span> baseCreateGrid } <span class="kw">from</span> '@toclocoinc/lattice-grid';
|
|
7891
|
+
|
|
7892
|
+
<span class="kw">export function</span> createGrid(element, config) {
|
|
7893
|
+
<span class="kw">return</span> baseCreateGrid(element, { theme: 'house', locale: 'en-GB', ...config });
|
|
7894
|
+
}</code></pre>
|
|
7895
|
+
</div>
|
|
7896
|
+
<p class="lead-in">
|
|
7897
|
+
There is no shipped <code>defaults()</code> call that does this for you today (a separate card,
|
|
7898
|
+
BACKLOG-0001187, is considering one) — a wrapping module you own and every call site imports is
|
|
7899
|
+
the supported pattern until then.
|
|
7900
|
+
</p>
|
|
7901
|
+
|
|
7537
7902
|
<footer>
|
|
7538
7903
|
<p>
|
|
7539
|
-
Lattice Grid 1.
|
|
7904
|
+
Lattice Grid 1.57.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
|
|
7540
7905
|
Written against the shipped source. Where this guide and the code disagree, the code wins,
|
|
7541
7906
|
please <a href="https://www.latticegrid.dev">tell us</a>.
|
|
7542
7907
|
</p>
|