@toclocoinc/lattice-grid 1.5.5 → 1.6.1
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/AI-SKILL.md +0 -10
- package/docs/API.html +89 -9
- package/docs/api-detail.html +982 -6
- package/lattice-grid.d.ts +510 -21
- package/lattice-grid.esm.min.js +10467 -1699
- package/lattice-grid.min.cjs +10433 -1699
- package/lattice-grid.min.css +1 -1
- package/lattice-grid.min.js +10433 -1699
- package/modules/devtools.esm.min.js +2 -2
- package/modules/react.esm.min.js +2 -2
- package/modules/svelte.esm.min.js +2 -2
- package/modules/vue.esm.min.js +2 -2
- package/modules/webcomponent.esm.min.js +10433 -1699
- package/package.json +1 -1
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.6.1</p>
|
|
441
441
|
<nav>
|
|
442
442
|
<div class="rail__group">
|
|
443
443
|
<span class="rail__label">Start here</span>
|
|
@@ -460,11 +460,22 @@
|
|
|
460
460
|
<a href="#sorting">Sorting</a>
|
|
461
461
|
<a href="#filtering">Filtering</a>
|
|
462
462
|
<a href="#grouping">Grouping and totals</a>
|
|
463
|
+
<a href="#pinned-rows">Pinned rows</a>
|
|
464
|
+
<a href="#full-width-rows">Full-width rows</a>
|
|
465
|
+
<a href="#row-reorder">Row reorder</a>
|
|
466
|
+
<a href="#row-transfer">Rows between grids</a>
|
|
467
|
+
<a href="#column-tags">Column tags</a>
|
|
468
|
+
<a href="#aligned-grids">Aligned grids</a>
|
|
469
|
+
<a href="#aggregate-safety">Aggregate safety</a>
|
|
470
|
+
<a href="#sticky-group-headings">Sticky group headings</a>
|
|
463
471
|
<a href="#sources">Large data</a>
|
|
464
472
|
</div>
|
|
465
473
|
<div class="rail__group">
|
|
466
474
|
<span class="rail__label">Interaction</span>
|
|
467
475
|
<a href="#editing">Editing</a>
|
|
476
|
+
<a href="#appearance">Grid lines and corners</a>
|
|
477
|
+
<a href="#cards">Cards, lists and feeds</a>
|
|
478
|
+
<a href="#row-form">Editing a row on a form</a>
|
|
468
479
|
<a href="#optimistic">Optimistic writes</a>
|
|
469
480
|
<a href="#tree-data">Tree data</a>
|
|
470
481
|
<a href="#master-detail">Master-detail</a>
|
|
@@ -524,13 +535,56 @@
|
|
|
524
535
|
<a href="API.html">reference tables</a> are the shorter version for when you already know.
|
|
525
536
|
</p>
|
|
526
537
|
<p class="chips">
|
|
527
|
-
<span class="chip">Version 1.
|
|
538
|
+
<span class="chip">Version 1.6.1</span>
|
|
528
539
|
<span class="chip">Zero dependencies</span>
|
|
529
540
|
<span class="chip">No build step</span>
|
|
530
541
|
</p>
|
|
531
542
|
</header>
|
|
532
543
|
|
|
533
|
-
|
|
544
|
+
|
|
545
|
+
<h2 id="messages">Translating the grid</h2>
|
|
546
|
+
<p class="section-note">Every string the grid renders or announces comes from a message catalogue. British English is the default; supply <code>messages</code> to replace some or all of it.</p>
|
|
547
|
+
|
|
548
|
+
<pre><code>import { createGrid, FR_FR } from '@toclocoinc/lattice-grid';
|
|
549
|
+
|
|
550
|
+
createGrid(element, { locale: 'fr-FR', messages: FR_FR });</code></pre>
|
|
551
|
+
|
|
552
|
+
<p>Where <code>locale</code> is not set, the grid takes the language the page declares in its <code>lang</code> attribute. Overrides merge over the default, so translating part of the interface leaves the remainder in English rather than showing raw keys, and a key that is not in the catalogue is ignored with a warning.</p>
|
|
553
|
+
|
|
554
|
+
<p>Catalogues ship for twenty-one locales: British and American English, French (France and Canada), Italian, Spanish, Brazilian Portuguese, German, Dutch, Swedish, Danish, Norwegian, Finnish, Polish, Czech, Hungarian, Romanian, Ukrainian, Greek, Japanese and Arabic. Each is an export of the package, so importing one does not reduce what is bundled. <code>EN_US</code> is a partial overlay carrying only what differs from British English and merging over it. <code>AR_SA</code> is an alias for <code>AR</code> rather than a separate catalogue — Arabic ships pan-Arabic, and a region appears in a name only where two variants exist. For anything else, <code>resolveCatalogue(tag)</code> resolves a tag to a catalogue and falls back to the base language, so <code>ar-EG</code> and <code>es-MX</code> both find one. <code>FR_CA</code> is a complete catalogue that follows Quebec usage where it differs — a column there is <em>figée</em> rather than <em>épinglée</em>.</p>
|
|
555
|
+
|
|
556
|
+
<p class="section-note">None of the translations has been reviewed by a native speaker. They are structurally complete and checked for placeholder integrity and plural coverage, but they are a starting point for review rather than finished copy.</p>
|
|
557
|
+
|
|
558
|
+
<h3>Writing your own</h3>
|
|
559
|
+
<p>A message is a string, or an object keyed by plural category when it counts something:</p>
|
|
560
|
+
|
|
561
|
+
<pre><code>{
|
|
562
|
+
'menu.sortAscending': 'Sortera stigande',
|
|
563
|
+
'count.rows': { one: '{count} rad', other: '{count} rader' },
|
|
564
|
+
}</code></pre>
|
|
565
|
+
|
|
566
|
+
<p>Placeholders are named, so word order is yours to choose. The parameter called <code>count</code> selects the plural form, using the categories the language actually has — English needs two, French treats zero as singular, Arabic has six. Numbers are formatted for the locale automatically; do not format them yourself.</p>
|
|
567
|
+
|
|
568
|
+
<p><code>MESSAGE_KEYS</code> lists every key. <code>auditCatalogue(yours)</code> returns what is missing and what is not a real key, which is the quickest way to check a translation before shipping it.</p>
|
|
569
|
+
|
|
570
|
+
<div class="why">
|
|
571
|
+
<p><strong>The grid is checked in the other direction too.</strong> <code>auditCatalogue</code> tells you a catalogue is complete — that a translator covered every key. It cannot tell you the grid only ever renders text that came from a catalogue in the first place, and a string written into the source passes every test, because the tests assert on the English the grid happens to produce.</p>
|
|
572
|
+
<p>So the build refuses one. Any literal reaching an element's text, or an announced attribute such as <code>aria-label</code>, <code>title</code> or <code>placeholder</code>, has to come from the catalogue. That is what stops a localised grid drifting back into English one plausible change at a time.</p>
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
<h3>Right-to-left</h3>
|
|
576
|
+
<p>The grid lays out right to left. Set <code>direction: 'rtl'</code> outright, or leave it unset and it follows the element's computed <code>dir</code> first and the locale second — so <code>locale: 'ar'</code> renders right to left with no further configuration, and a grid inside a page that has already declared <code>dir="rtl"</code> agrees with it.</p>
|
|
577
|
+
|
|
578
|
+
<pre><code>createGrid(element, { locale: 'ar' }); <span class="cmt">// direction follows the locale</span>
|
|
579
|
+
createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so outright</span></code></pre>
|
|
580
|
+
|
|
581
|
+
<p>The element comes before the locale because a page that has set <code>dir</code> has already made a decision about layout, and a grid inside it should not disagree on the strength of a language tag.</p>
|
|
582
|
+
|
|
583
|
+
<p>Pinned columns swap sides, the header and pinned rows follow the scroll the other way, and column resize and reorder, the fill handle, annotations, the facet band, the comment marker and menu placement all follow the writing direction rather than the physical one.</p>
|
|
584
|
+
|
|
585
|
+
<p><strong>Not yet exercised with bidirectional text.</strong> The layout is verified, but only with Latin text in a right-to-left grid. Cells, headings and editors holding actual Arabic or Hebrew — particularly mixed with Latin text or numbers — have not been tested.</p>
|
|
586
|
+
|
|
587
|
+
<h2 id="what">What it is</h2>
|
|
534
588
|
<p class="lead-in">
|
|
535
589
|
Lattice Grid renders tabular data in a browser. That sentence covers a great many products,
|
|
536
590
|
so here is what is actually different about this one.
|
|
@@ -870,7 +924,7 @@ off(); <span class="cmt">// every subscrip
|
|
|
870
924
|
</p>
|
|
871
925
|
<div class="example">
|
|
872
926
|
<p class="example__label">Which version am I running?</p>
|
|
873
|
-
<pre><code>grid.getVersion(); <span class="cmt">// '1.
|
|
927
|
+
<pre><code>grid.getVersion(); <span class="cmt">// '1.6.1'</span>
|
|
874
928
|
LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid to hand</span></code></pre>
|
|
875
929
|
</div>
|
|
876
930
|
<p class="lead-in">
|
|
@@ -980,12 +1034,89 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
|
|
|
980
1034
|
<tr><td class="name">Temporal</td><td class="desc"><code>datetime</code>, <code>time</code>, <code>duration</code></td></tr>
|
|
981
1035
|
<tr><td class="name">Network</td><td class="desc"><code>ipv4</code>, <code>ipv6</code>, <code>cidr</code>, <code>mac</code></td></tr>
|
|
982
1036
|
<tr><td class="name">Numeric bases</td><td class="desc"><code>hex</code>, <code>hex8</code>, <code>hex16</code>, <code>hex32</code>, <code>binary</code>, <code>binary8</code>, <code>octal</code></td></tr>
|
|
983
|
-
<tr><td class="name">Units</td><td class="desc"><code>bytes</code>, <code>megabytes</code>, <code>gigabytes</code>, <code>bitrate</code></td></tr>
|
|
1037
|
+
<tr><td class="name">Units — computing</td><td class="desc"><code>bytes</code>, <code>megabytes</code>, <code>gigabytes</code>, <code>bitrate</code>, <code>gigabits</code></td></tr>
|
|
1038
|
+
<tr><td class="name">Units — physical</td><td class="desc"><code>metres</code>, <code>millimetres</code>, <code>kilometres</code>, <code>grams</code>, <code>kilograms</code>, <code>tonnes</code>, <code>seconds</code>, <code>milliseconds</code>, <code>hours</code></td></tr>
|
|
1039
|
+
<tr><td class="name">Units — engineering</td><td class="desc"><code>speed</code>, <code>kph</code>, <code>mph</code>, <code>knots</code>, <code>acceleration</code>, <code>area</code>, <code>hectares</code>, <code>volume</code>, <code>cubicMetres</code>, <code>energy</code>, <code>kilowattHours</code>, <code>power</code>, <code>kilowatts</code>, <code>force</code>, <code>pressure</code>, <code>bar</code>, <code>psi</code>, <code>torque</code>, <code>density</code>, <code>flow</code>, <code>litresPerMinute</code>, <code>radians</code>, <code>degrees</code></td></tr>
|
|
1040
|
+
<tr><td class="name">Units — electrical and scientific</td><td class="desc"><code>voltage</code>, <code>current</code>, <code>resistance</code>, <code>capacitance</code>, <code>inductance</code>, <code>charge</code>, <code>conductance</code>, <code>fluxDensity</code>, <code>luminousFlux</code>, <code>illuminance</code>, <code>substance</code>, <code>absorbedDose</code>, <code>equivalentDose</code>, <code>radioactivity</code>, <code>frequency</code></td></tr>
|
|
1041
|
+
<tr><td class="name">Temperature</td><td class="desc"><code>celsius</code>, <code>fahrenheit</code>, <code>kelvin</code></td></tr>
|
|
984
1042
|
<tr><td class="name">Structured</td><td class="desc"><code>json</code>, <code>colour</code>, <code>rating</code>, <code>percent</code></td></tr>
|
|
985
1043
|
</tbody>
|
|
986
1044
|
</table>
|
|
987
1045
|
</div>
|
|
988
1046
|
|
|
1047
|
+
<h3 id="units">Units</h3>
|
|
1048
|
+
<p class="lead-in">
|
|
1049
|
+
A unit type stores a plain number in a named base unit and makes only display and input
|
|
1050
|
+
unit-aware. That is the whole design: the column still backs onto a typed array, so sorting,
|
|
1051
|
+
filtering, grouping and totalling stay ordinary arithmetic and never touch the rendered text.
|
|
1052
|
+
A gigabyte column holding <code>0.5</code> shows <code>512 MB</code>, accepts
|
|
1053
|
+
<code>512M</code> typed over it, and stores <code>0.5</code> throughout.
|
|
1054
|
+
</p>
|
|
1055
|
+
|
|
1056
|
+
<div class="example">
|
|
1057
|
+
<p class="example__label">Declared types, and one built to order</p>
|
|
1058
|
+
<pre><code>columns: [
|
|
1059
|
+
{ field: 'span', type: <span class="str">'metres'</span> }, <span class="cmt">// 1500 → "1.5 km"</span>
|
|
1060
|
+
{ field: 'load', type: <span class="str">'kilograms'</span> },
|
|
1061
|
+
{ field: 'inlet', type: <span class="str">'pressure'</span> }, <span class="cmt">// 200000 → "200 kPa"</span>
|
|
1062
|
+
{ field: 'bearing', type: <span class="str">'degrees'</span> },
|
|
1063
|
+
{ field: 'cap', type: <span class="str">'capacitance'</span> }, <span class="cmt">// 4.7e-11 → "47 pF"</span>
|
|
1064
|
+
{ field: 'inletTemp', type: <span class="str">'celsius'</span> },
|
|
1065
|
+
],
|
|
1066
|
+
|
|
1067
|
+
<span class="cmt">// or configure your own base unit and precision</span>
|
|
1068
|
+
dataTypes: {
|
|
1069
|
+
runtime: createUnitType({ system: <span class="str">'duration'</span>, unit: <span class="str">'ms'</span>, display: <span class="str">'auto'</span> }),
|
|
1070
|
+
}</code></pre>
|
|
1071
|
+
</div>
|
|
1072
|
+
|
|
1073
|
+
<p class="lead-in">
|
|
1074
|
+
Seventeen unit systems ship: <code>data</code>, <code>bitrate</code>, <code>length</code>,
|
|
1075
|
+
<code>mass</code>, <code>duration</code>, <code>speed</code>, <code>acceleration</code>,
|
|
1076
|
+
<code>area</code>, <code>volume</code>, <code>energy</code>, <code>power</code>,
|
|
1077
|
+
<code>force</code>, <code>pressure</code>, <code>torque</code>, <code>density</code>,
|
|
1078
|
+
<code>flow</code> and <code>angle</code>, plus fifteen SI-prefixed electrical and scientific
|
|
1079
|
+
quantities. <code>registerUnitSystem</code> adds one of your own.
|
|
1080
|
+
</p>
|
|
1081
|
+
|
|
1082
|
+
<div class="why">
|
|
1083
|
+
<p><strong>Ambiguous units are refused, not guessed.</strong> A US gallon and an imperial
|
|
1084
|
+
gallon differ by about a fifth, and "ton" means three different masses. Each has its own
|
|
1085
|
+
symbol — <code>gal (US)</code>, <code>ton (UK)</code> — and the bare word is claimed by all of
|
|
1086
|
+
them, so typing it is rejected rather than resolved. A <code>gal</code> silently taken as US
|
|
1087
|
+
in a UK deployment is data corruption that reads as rounding.</p>
|
|
1088
|
+
<p>The same rule catches case: <code>mV</code> and <code>MV</code> are a billion apart, so both
|
|
1089
|
+
exact spellings work and the case-folded <code>mv</code> is refused.</p>
|
|
1090
|
+
<p><strong>Customary units are accepted but never chosen.</strong> <code>display: 'auto'</code>
|
|
1091
|
+
walks the coherent SI ladder only. With the calorie, the BTU and the kilojoule all on one
|
|
1092
|
+
ladder, 4,000 J would render as <code>3.79 BTU</code> — auto picks the largest unit that fits,
|
|
1093
|
+
and the BTU happens to be larger than the kilojoule. Ask for a BTU by name and you get one.</p>
|
|
1094
|
+
</div>
|
|
1095
|
+
|
|
1096
|
+
<p class="lead-in">
|
|
1097
|
+
<code>significantFigures</code> renders to a fixed precision rather than a fixed number of
|
|
1098
|
+
decimals. Two decimals is four significant figures at <code>12.34 kB</code> and three at
|
|
1099
|
+
<code>1.54 kB</code>, so a column claims different accuracy row by row depending on nothing but
|
|
1100
|
+
which unit auto picked; significant figures are what an instrument has, and they hold across
|
|
1101
|
+
the ladder. Rounding is applied before the unit is chosen, so 999,999 bytes to three figures
|
|
1102
|
+
is <code>1.00 MB</code> rather than <code>1,000 kB</code>.
|
|
1103
|
+
</p>
|
|
1104
|
+
|
|
1105
|
+
<div class="why">
|
|
1106
|
+
<p><strong>Angles wrap, so their mean is replaced.</strong> The average of 359° and 1° is 0°,
|
|
1107
|
+
and the arithmetic answer — 180° — is a confident, plausible number pointing in exactly the
|
|
1108
|
+
wrong direction. A <code>degrees</code> or <code>radians</code> column averages by direction
|
|
1109
|
+
instead, and reports nothing where the angles cancel and there is no mean direction to give.
|
|
1110
|
+
The <em>sum</em> stays arithmetic, because a total rotation of 720° is two turns and that is a
|
|
1111
|
+
real figure.</p>
|
|
1112
|
+
<p><strong>Temperature is its own type, not a unit.</strong> Every other unit is a
|
|
1113
|
+
multiplication; Celsius to Fahrenheit carries an offset, and zero Celsius is not zero
|
|
1114
|
+
anything, so no factor converts it. <code>celsius</code>, <code>fahrenheit</code> and
|
|
1115
|
+
<code>kelvin</code> convert on input — type <code>72 F</code> into a Celsius column and it
|
|
1116
|
+
stores 22.2 — and <strong>refuse to be summed</strong>: twenty degrees plus twenty degrees is
|
|
1117
|
+
not forty degrees, and a footer saying so would be believed.</p>
|
|
1118
|
+
</div>
|
|
1119
|
+
|
|
989
1120
|
<h3>Types read from the data</h3>
|
|
990
1121
|
<p class="lead-in">
|
|
991
1122
|
A column that declares no <code>type</code> takes one from the rows. The first hundred non-empty
|
|
@@ -1646,6 +1777,412 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
|
|
|
1646
1777
|
grid.columns.pivot(['statusId']); <span class="cmt">// a column per distinct status</span></code></pre>
|
|
1647
1778
|
</div>
|
|
1648
1779
|
|
|
1780
|
+
<h2 id="pinned-rows">Pinned rows</h2>
|
|
1781
|
+
<p class="lead-in">
|
|
1782
|
+
A pinned row sits outside the scrolling body, against the header or above the status bar,
|
|
1783
|
+
and stays there while the rows scroll past it. Use one for a column-units line, a target or
|
|
1784
|
+
budget to compare against, a precomputed summary, or a note that must not scroll away.
|
|
1785
|
+
</p>
|
|
1786
|
+
|
|
1787
|
+
<div class="example">
|
|
1788
|
+
<p class="example__label">Pinning a units row</p>
|
|
1789
|
+
<pre><code>createGrid(element, {
|
|
1790
|
+
columns,
|
|
1791
|
+
rows,
|
|
1792
|
+
pinnedTopRows: [{ product: 'Units', capacity: 'MW', margin: '%' }],
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
<span class="cmt">// Or at runtime, at either edge:</span>
|
|
1796
|
+
grid.setPinnedRows([summaryLine], { edge: 'top' });
|
|
1797
|
+
grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span></code></pre>
|
|
1798
|
+
</div>
|
|
1799
|
+
|
|
1800
|
+
<p class="lead-in">
|
|
1801
|
+
The objects are yours and are rendered through the ordinary column pipeline — value getters,
|
|
1802
|
+
formatters, cell renderers and conditional formatting all run, so a pinned row looks like the
|
|
1803
|
+
data it sits against without you rebuilding any of that.
|
|
1804
|
+
</p>
|
|
1805
|
+
|
|
1806
|
+
<p class="lead-in">
|
|
1807
|
+
<strong>They are not part of the data</strong>, and that separation is the point. A pinned
|
|
1808
|
+
row is not counted by <code>rows.count()</code>, not sorted, not filtered, not grouped, not
|
|
1809
|
+
selectable, not included in a total and not exported. A units row that sorted itself into the
|
|
1810
|
+
middle of the data, or a target line that was added to the sum it is there to be compared
|
|
1811
|
+
against, would be worse than no feature at all. If you want a row that behaves like data,
|
|
1812
|
+
make it data.
|
|
1813
|
+
</p>
|
|
1814
|
+
|
|
1815
|
+
<p class="lead-in">
|
|
1816
|
+
A filter that matches nothing still leaves the pinned rows visible, which is usually what you
|
|
1817
|
+
want: an empty grid with its column-units line is readable, and an empty grid without one is
|
|
1818
|
+
not.
|
|
1819
|
+
</p>
|
|
1820
|
+
|
|
1821
|
+
<div class="table-wrap">
|
|
1822
|
+
<table>
|
|
1823
|
+
<thead><tr><th>Point</th><th>Behaviour</th></tr></thead>
|
|
1824
|
+
<tbody>
|
|
1825
|
+
<tr><td class="name">Order</td><td class="desc">Rows appear in the order of the array. At the bottom edge, the grand total comes first and your rows sit below it.</td></tr>
|
|
1826
|
+
<tr><td class="name">Height</td><td class="desc">From <code>rowHeight</code>, including the function form — which is called with the pinned row, so you can measure your own content. The body reserves exactly the strip's height, so no data row hides underneath it.</td></tr>
|
|
1827
|
+
<tr><td class="name">Updating</td><td class="desc">Pass a <em>new</em> array. Array identity is how the grid knows the rows changed; pushing into the array you passed before will not repaint.</td></tr>
|
|
1828
|
+
<tr><td class="name">Editing</td><td class="desc">A pinned row has no place in the store to write to, so it is not editable.</td></tr>
|
|
1829
|
+
</tbody>
|
|
1830
|
+
</table>
|
|
1831
|
+
</div>
|
|
1832
|
+
|
|
1833
|
+
<h2 id="full-width-rows">Full-width rows</h2>
|
|
1834
|
+
<p class="lead-in">
|
|
1835
|
+
A row drawn as a single band across every column instead of being divided into them: a
|
|
1836
|
+
section banner, an explanatory note, an empty-group message, a “load more” affordance —
|
|
1837
|
+
anything that belongs between rows and is not itself divided by the columns.
|
|
1838
|
+
</p>
|
|
1839
|
+
|
|
1840
|
+
<div class="example">
|
|
1841
|
+
<p class="example__label">A banner before each section</p>
|
|
1842
|
+
<pre><code>createGrid(element, {
|
|
1843
|
+
columns,
|
|
1844
|
+
rows, <span class="cmt">// your data, with the banners in it</span>
|
|
1845
|
+
fullWidth: {
|
|
1846
|
+
when: (row) => row.data.kind === 'section',
|
|
1847
|
+
render: ({ data }) => data.title,
|
|
1848
|
+
},
|
|
1849
|
+
});</code></pre>
|
|
1850
|
+
</div>
|
|
1851
|
+
|
|
1852
|
+
<p class="lead-in">
|
|
1853
|
+
<code>render</code> returns a string for text or a node for content, or returns nothing and
|
|
1854
|
+
writes into <code>params.element</code> itself. An HTML string is deliberately not accepted.
|
|
1855
|
+
<code>params</code> carries <code>{ row, data, index, grid, element }</code>.
|
|
1856
|
+
</p>
|
|
1857
|
+
|
|
1858
|
+
<p class="lead-in">
|
|
1859
|
+
The band <strong>holds still while the columns scroll under it</strong>, which is what a
|
|
1860
|
+
banner is for — text that scrolled sideways out of view with the columns would be a worse
|
|
1861
|
+
version of a cell. It is drawn over the pinned regions as well as the centre, so it genuinely
|
|
1862
|
+
spans every column.
|
|
1863
|
+
</p>
|
|
1864
|
+
|
|
1865
|
+
<div class="why">
|
|
1866
|
+
<p><strong>A full-width row is still one of your data rows.</strong> It is counted by
|
|
1867
|
+
<code>rows.count()</code>, sorted, filtered and exported like any other; only its
|
|
1868
|
+
presentation changes. That is the difference between this and
|
|
1869
|
+
<a href="#pinned-rows">pinned rows</a>, and it is the thing to get straight before choosing
|
|
1870
|
+
between them: <em>full-width</em> changes how a row looks, <em>pinned</em> changes whether a
|
|
1871
|
+
row is data at all.</p>
|
|
1872
|
+
<p>So if your banners must not appear in an export or a row count, they should not be in the
|
|
1873
|
+
data. If they are section headings that belong with the records they head — and should sort,
|
|
1874
|
+
filter and export alongside them — this is the right tool.</p>
|
|
1875
|
+
<p>One consequence worth stating plainly: sorting reorders banners along with everything
|
|
1876
|
+
else, because the predicate follows the row and not its position. Either do not offer sorting
|
|
1877
|
+
on such a grid, or sort on a key that keeps each section together.</p>
|
|
1878
|
+
</div>
|
|
1879
|
+
|
|
1880
|
+
<p class="lead-in">
|
|
1881
|
+
A band is exposed as a row containing one cell with <code>aria-colspan</code> covering every
|
|
1882
|
+
column, so a screen reader reads it as one wide cell rather than as a row with missing ones.
|
|
1883
|
+
No second, empty row is rendered underneath it.
|
|
1884
|
+
</p>
|
|
1885
|
+
|
|
1886
|
+
<h2 id="row-reorder">Row reorder</h2>
|
|
1887
|
+
<p class="lead-in">
|
|
1888
|
+
<code>rowReorder: true</code> puts a drag handle in the first visible column and lets a user
|
|
1889
|
+
move rows with it, or with <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>↑</kbd>/<kbd>↓</kbd>.
|
|
1890
|
+
<code>{ column: 'name' }</code> puts the handle somewhere else.
|
|
1891
|
+
</p>
|
|
1892
|
+
|
|
1893
|
+
<div class="example">
|
|
1894
|
+
<p class="example__label">Reordering a list, and saving the result</p>
|
|
1895
|
+
<pre><code>createGrid(element, {
|
|
1896
|
+
columns,
|
|
1897
|
+
rows,
|
|
1898
|
+
rowReorder: <span class="kw">true</span>,
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
grid.on('row:moved', ({ key, from, to }) => {
|
|
1902
|
+
<span class="cmt">// rows.data() is the new order in full.</span>
|
|
1903
|
+
api.saveOrder(grid.rows.data().map((r, i) => ({ id: r.id, order: i })));
|
|
1904
|
+
});</code></pre>
|
|
1905
|
+
</div>
|
|
1906
|
+
|
|
1907
|
+
<div class="why">
|
|
1908
|
+
<p><strong>The order is your data, not a view of it.</strong> A move reorders the array you
|
|
1909
|
+
gave the grid and tells you it happened; writing it somewhere permanent is yours, because
|
|
1910
|
+
only you know where the order lives. A grid that rearranged rows on screen and stopped there
|
|
1911
|
+
would look finished and lose the order on the next load — which is worse than not offering
|
|
1912
|
+
the feature.</p>
|
|
1913
|
+
<p>If the save fails, move it back: <code>grid.rows.move(key, from)</code>.</p>
|
|
1914
|
+
</div>
|
|
1915
|
+
|
|
1916
|
+
<p class="lead-in">
|
|
1917
|
+
<strong>It refuses while a sort, filter or grouping is active</strong>, and says why out loud
|
|
1918
|
+
rather than springing the row back in silence. The reason is that dropping between two
|
|
1919
|
+
visible rows says nothing about where the row belongs in the underlying array — under a
|
|
1920
|
+
filter there may be hidden rows between them, and under a sort the displayed order is
|
|
1921
|
+
something the grid computed rather than something the data says. Rather than pick an
|
|
1922
|
+
interpretation and put the row somewhere you did not ask for, the move is declined.
|
|
1923
|
+
<code>rows.move()</code> returns <code>{ moved: false, reason }</code> so you can handle it
|
|
1924
|
+
yourself.
|
|
1925
|
+
</p>
|
|
1926
|
+
|
|
1927
|
+
<h2 id="row-transfer">Moving rows between grids</h2>
|
|
1928
|
+
<p class="lead-in">
|
|
1929
|
+
A row can be dragged out of one grid and into another — a picker beside a basket, an inbox
|
|
1930
|
+
beside a queue, an available list beside an assigned one.
|
|
1931
|
+
</p>
|
|
1932
|
+
|
|
1933
|
+
<div class="example">
|
|
1934
|
+
<p class="example__label">A one-way drag, from a catalogue into a basket</p>
|
|
1935
|
+
<pre><code><span class="cmt">// Sends, never receives. rowReorder draws the handle a drag starts from.</span>
|
|
1936
|
+
createGrid(left, { columns, rows,
|
|
1937
|
+
rowReorder: <span class="kw">true</span>,
|
|
1938
|
+
rowTransfer: { receive: <span class="kw">false</span>, group: 'order' },
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
<span class="cmt">// Receives, never sends. Draws no handles at all.</span>
|
|
1942
|
+
createGrid(right, { columns, rows,
|
|
1943
|
+
rowTransfer: { send: <span class="kw">false</span>, group: 'order' },
|
|
1944
|
+
});</code></pre>
|
|
1945
|
+
</div>
|
|
1946
|
+
|
|
1947
|
+
<p class="lead-in">
|
|
1948
|
+
<code>mode: 'copy'</code> on the sending grid leaves the row where it was. <code>group</code>
|
|
1949
|
+
restricts exchange to grids sharing the same name, so two unrelated grids on a page do not
|
|
1950
|
+
accept each other's rows.
|
|
1951
|
+
</p>
|
|
1952
|
+
|
|
1953
|
+
<div class="table-wrap">
|
|
1954
|
+
<table>
|
|
1955
|
+
<thead><tr><th>Event</th><th>Fired on</th><th>Carries</th></tr></thead>
|
|
1956
|
+
<tbody>
|
|
1957
|
+
<tr><td class="name">row:received</td><td class="desc">the target</td><td class="desc"><code>{ data, at, rejected }</code></td></tr>
|
|
1958
|
+
<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>
|
|
1959
|
+
<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>
|
|
1960
|
+
</tbody>
|
|
1961
|
+
</table>
|
|
1962
|
+
</div>
|
|
1963
|
+
|
|
1964
|
+
<div class="why">
|
|
1965
|
+
<p><strong>Off by default, and both ends have to agree.</strong> Rows leaving a grid is a data
|
|
1966
|
+
change you have to want: a grid that quietly let its rows be dragged away would lose one to a
|
|
1967
|
+
mis-drag, and there is no gesture a user would think to try to get it back. A one-way
|
|
1968
|
+
relationship is a declaration on both grids rather than a convention — the sender refuses to
|
|
1969
|
+
receive, and the receiver never starts a drag.</p>
|
|
1970
|
+
<p><strong>The target adds before the source removes.</strong> If the add is refused — a
|
|
1971
|
+
duplicate key, most likely — nothing is removed, so a rejected transfer loses no data. The
|
|
1972
|
+
other order would delete a row and then discover it had nowhere to go.</p>
|
|
1973
|
+
<p><strong>The row object is cloned, not shared.</strong> Two grids holding the same object
|
|
1974
|
+
would edit each other's rows through it, which is the sort of coupling nobody goes looking for
|
|
1975
|
+
when a cell changes in a grid they were not touching.</p>
|
|
1976
|
+
</div>
|
|
1977
|
+
|
|
1978
|
+
<p class="lead-in">
|
|
1979
|
+
A grid is highlighted while a dragged row is over it only when it would actually accept the
|
|
1980
|
+
drop. Marking one that is going to refuse promises a placement that will not happen. A refusal
|
|
1981
|
+
is announced rather than left silent.
|
|
1982
|
+
</p>
|
|
1983
|
+
|
|
1984
|
+
<h2 id="column-tags">Column tags</h2>
|
|
1985
|
+
<p class="lead-in">
|
|
1986
|
+
Tag columns, then let a user show only the ones carrying a chosen tag. Sixty columns of
|
|
1987
|
+
monthly figures across five years become twelve by picking a year.
|
|
1988
|
+
</p>
|
|
1989
|
+
|
|
1990
|
+
<div class="example">
|
|
1991
|
+
<p class="example__label">Five years of months, filtered to one</p>
|
|
1992
|
+
<pre><code>createGrid(element, {
|
|
1993
|
+
columns: [
|
|
1994
|
+
{ field: 'account', title: 'Account' }, <span class="cmt">// no tags</span>
|
|
1995
|
+
{ field: 'jan24', title: '01/24', tags: ['2024', 'Q1'] },
|
|
1996
|
+
{ field: 'feb24', title: '02/24', tags: ['2024', 'Q1'] },
|
|
1997
|
+
<span class="cmt">// …</span>
|
|
1998
|
+
{ field: 'total', title: 'Total' }, <span class="cmt">// no tags</span>
|
|
1999
|
+
],
|
|
2000
|
+
rows,
|
|
2001
|
+
columnTagFilter: <span class="kw">true</span>,
|
|
2002
|
+
});</code></pre>
|
|
2003
|
+
</div>
|
|
2004
|
+
|
|
2005
|
+
<div class="why">
|
|
2006
|
+
<p><strong>Only tagged columns are ever hidden.</strong> That is the rule the whole feature
|
|
2007
|
+
turns on. A financial grid with sixty month columns also has an account name, a total and a
|
|
2008
|
+
variance, and none of those belong to a year — if filtering hid them the view would be
|
|
2009
|
+
useless, and tagging every column merely to keep it visible would be busywork. So "show 2024"
|
|
2010
|
+
does not mean "hide everything else"; it means "hide tagged columns that are not 2024".</p>
|
|
2011
|
+
<p>The same rule runs the other way: an untagged column you hid yourself stays hidden, because
|
|
2012
|
+
forcing it visible would undo a decision that has nothing to do with tags.</p>
|
|
2013
|
+
</div>
|
|
2014
|
+
|
|
2015
|
+
<p class="lead-in">
|
|
2016
|
+
A column can carry more than one tag, which gives you a second axis for free — tag each month
|
|
2017
|
+
with its year <em>and</em> its quarter, and a user can pick either. The dropdown lists tags in
|
|
2018
|
+
the order they were declared rather than alphabetically, since they are usually already in a
|
|
2019
|
+
meaningful sequence and sorting would put <code>Q10</code> before <code>Q2</code>.
|
|
2020
|
+
</p>
|
|
2021
|
+
|
|
2022
|
+
<div class="table-wrap">
|
|
2023
|
+
<table>
|
|
2024
|
+
<thead><tr><th>Member</th><th>Does</th></tr></thead>
|
|
2025
|
+
<tbody>
|
|
2026
|
+
<tr><td class="sig">columns.tags()</td><td class="desc">Every distinct tag, in declaration order.</td></tr>
|
|
2027
|
+
<tr><td class="sig">columns.showTagged(tags)</td><td class="desc">Show only the columns carrying one of these. Nothing, or an empty list, shows all. Returns the ids it hid.</td></tr>
|
|
2028
|
+
<tr><td class="sig">columns.activeTags()</td><td class="desc">What is being shown, empty when all are.</td></tr>
|
|
2029
|
+
<tr><td class="name">columns:tagged</td><td class="desc">Fired with <code>{ tags, hidden }</code>.</td></tr>
|
|
2030
|
+
</tbody>
|
|
2031
|
+
</table>
|
|
2032
|
+
</div>
|
|
2033
|
+
|
|
2034
|
+
<h3>Headings without the controls</h3>
|
|
2035
|
+
<p class="lead-in">
|
|
2036
|
+
A dense grid often wants the heading and nothing else. <code>showColumnFunctions: false</code>
|
|
2037
|
+
leaves each heading as its label, with no sort, filter or menu control — they are not drawn
|
|
2038
|
+
rather than hidden, so the label has the whole cell. Sorting, filtering and the column menu
|
|
2039
|
+
stay reachable through the API, the keyboard and the tool panel; only the furniture goes. The
|
|
2040
|
+
resize grip stays, since dragging a column wider is a view adjustment rather than a function
|
|
2041
|
+
of the column.
|
|
2042
|
+
</p>
|
|
2043
|
+
|
|
2044
|
+
<h2 id="aligned-grids">Aligned grids</h2>
|
|
2045
|
+
<p class="lead-in">
|
|
2046
|
+
Two or more grids that read as one table split into sections: a summary band above a detail
|
|
2047
|
+
grid, two datasets side by side under identical columns, a frozen top section that is
|
|
2048
|
+
genuinely different data rather than a pinned row.
|
|
2049
|
+
</p>
|
|
2050
|
+
|
|
2051
|
+
<div class="example">
|
|
2052
|
+
<p class="example__label">A summary band above a detail grid</p>
|
|
2053
|
+
<pre><code><span class="kw">const</span> summary = createGrid(top, { columns, rows: totals });
|
|
2054
|
+
<span class="kw">const</span> detail = createGrid(bottom, { columns, rows, alignedGrids: [summary] });</code></pre>
|
|
2055
|
+
</div>
|
|
2056
|
+
|
|
2057
|
+
<p class="lead-in">
|
|
2058
|
+
Declare it on the grid you create <em>last</em>, since that is the only one that can name the
|
|
2059
|
+
others. The link is peer-based once made: whichever grid the user resizes is the one the
|
|
2060
|
+
others follow.
|
|
2061
|
+
</p>
|
|
2062
|
+
|
|
2063
|
+
<div class="table-wrap">
|
|
2064
|
+
<table>
|
|
2065
|
+
<thead><tr><th>Shared</th><th>Independent</th></tr></thead>
|
|
2066
|
+
<tbody>
|
|
2067
|
+
<tr><td class="desc">Column widths and flex</td><td class="desc">Sort</td></tr>
|
|
2068
|
+
<tr><td class="desc">Column order</td><td class="desc">Filters</td></tr>
|
|
2069
|
+
<tr><td class="desc">Column visibility</td><td class="desc">Selection</td></tr>
|
|
2070
|
+
<tr><td class="desc">Pinning</td><td class="desc">Grouping and totals</td></tr>
|
|
2071
|
+
<tr><td class="desc">Horizontal scroll</td><td class="desc">Vertical scroll, and the rows themselves</td></tr>
|
|
2072
|
+
</tbody>
|
|
2073
|
+
</table>
|
|
2074
|
+
</div>
|
|
2075
|
+
|
|
2076
|
+
<div class="why">
|
|
2077
|
+
<p><strong>What is not shared is the design.</strong> If sort, filters and selection travelled
|
|
2078
|
+
too, this would not be a feature — it would be one grid with extra steps. The reason to have
|
|
2079
|
+
two is that the sections hold different data, so each keeps its own view of it.</p>
|
|
2080
|
+
<p>Vertical scroll stays independent for the same reason: the grids hold different numbers of
|
|
2081
|
+
rows, and yoking them would make the shorter one run out.</p>
|
|
2082
|
+
</div>
|
|
2083
|
+
|
|
2084
|
+
<p class="lead-in">
|
|
2085
|
+
A column one grid has and another does not is skipped rather than invented, and nothing checks
|
|
2086
|
+
that the column sets match — aligning grids with different columns is a caller error that
|
|
2087
|
+
produces a visibly wrong result rather than a silent one. Destroying any grid releases its
|
|
2088
|
+
link and leaves the rest working.
|
|
2089
|
+
</p>
|
|
2090
|
+
|
|
2091
|
+
<h2 id="aggregate-safety">Totals that a type can refuse</h2>
|
|
2092
|
+
<p class="lead-in">
|
|
2093
|
+
Some values do not add up the way plain numbers do. A data type can say which aggregates are
|
|
2094
|
+
meaningful for it, and supply its own arithmetic where the built-in one would be wrong.
|
|
2095
|
+
</p>
|
|
2096
|
+
|
|
2097
|
+
<div class="why">
|
|
2098
|
+
<p><strong>The failure this prevents is a confident wrong number.</strong> You cannot add
|
|
2099
|
+
decibels — 90 dB and 90 dB make 93 dB, not 180. The mean of a column of rates
|
|
2100
|
+
is not the mean — a 100% conversion on two visits and a 1% conversion on ten thousand average
|
|
2101
|
+
to 1.02%, not 50.5%. Both mistakes produce a plausible figure rather than an error, and a
|
|
2102
|
+
footer nobody can check gets used. A missing total gets asked about; a wrong one does not.</p>
|
|
2103
|
+
</div>
|
|
2104
|
+
|
|
2105
|
+
<div class="example">
|
|
2106
|
+
<p class="example__label">A type declaring what it supports</p>
|
|
2107
|
+
<pre><code>{
|
|
2108
|
+
base: 'number',
|
|
2109
|
+
totals: {
|
|
2110
|
+
<span class="cmt">// Anything else is refused when a column is configured, not at render.</span>
|
|
2111
|
+
supported: ['sum', 'avg', 'min', 'max', 'count', 'countValues'],
|
|
2112
|
+
<span class="cmt">// And where the built-in arithmetic is wrong, replace it.</span>
|
|
2113
|
+
implement: { sum: (values) => logDomainSum(values) },
|
|
2114
|
+
},
|
|
2115
|
+
}</code></pre>
|
|
2116
|
+
</div>
|
|
2117
|
+
|
|
2118
|
+
<p class="lead-in">
|
|
2119
|
+
A type that declares no <code>totals</code> supports everything, so nothing that shipped
|
|
2120
|
+
before this behaves differently. An <code>implement</code> function receives the values
|
|
2121
|
+
index-aligned with their rows and a context carrying <code>column</code> and
|
|
2122
|
+
<code>valueAt(colId, i)</code>, which is how a weighted mean reaches the denominators in
|
|
2123
|
+
another column.
|
|
2124
|
+
</p>
|
|
2125
|
+
|
|
2126
|
+
<h3>The types that use it</h3>
|
|
2127
|
+
<div class="table-wrap">
|
|
2128
|
+
<table>
|
|
2129
|
+
<thead><tr><th>Type</th><th>What it does differently</th></tr></thead>
|
|
2130
|
+
<tbody>
|
|
2131
|
+
<tr><td class="name">decibel</td><td class="desc">Sums and averages in the linear domain and converts back. Power scale, factor 10.</td></tr>
|
|
2132
|
+
<tr><td class="name">decibelAmplitude</td><td class="desc">The same, on the field scale — factor 20, for voltage and current.</td></tr>
|
|
2133
|
+
<tr><td class="name">ratio</td><td class="desc">Averages by weight, using the column named in <code>typeOptions.weight</code>. Refuses <code>sum</code>, since two rates do not add to a rate.</td></tr>
|
|
2134
|
+
<tr><td class="name">percentRate</td><td class="desc">As <code>ratio</code>, displayed with a percent sign.</td></tr>
|
|
2135
|
+
</tbody>
|
|
2136
|
+
</table>
|
|
2137
|
+
</div>
|
|
2138
|
+
|
|
2139
|
+
<div class="example">
|
|
2140
|
+
<p class="example__label">A conversion rate averaged properly</p>
|
|
2141
|
+
<pre><code>{ field: 'conversion', type: 'percentRate', total: 'avg',
|
|
2142
|
+
typeOptions: { weight: 'visits' } }</code></pre>
|
|
2143
|
+
</div>
|
|
2144
|
+
|
|
2145
|
+
<p class="lead-in">
|
|
2146
|
+
Without a weight column the average returns nothing rather than falling back to the
|
|
2147
|
+
unweighted mean — falling back would be the exact mistake the type exists to prevent, arrived
|
|
2148
|
+
at silently. Rows with no rate, or no weight, are left out rather than counted as zero.
|
|
2149
|
+
</p>
|
|
2150
|
+
|
|
2151
|
+
<h2 id="sticky-group-headings">Sticky group headings</h2>
|
|
2152
|
+
<p class="lead-in">
|
|
2153
|
+
Scrolling inside a group keeps that group's headings pinned above the rows, so the rows on
|
|
2154
|
+
screen always say which group they belong to. Nested groups stack, up to a cap.
|
|
2155
|
+
</p>
|
|
2156
|
+
|
|
2157
|
+
<div class="example">
|
|
2158
|
+
<p class="example__label">On by default; turn it off or change the cap</p>
|
|
2159
|
+
<pre><code>createGrid(element, { stickyGroupHeaders: <span class="kw">false</span> }); <span class="cmt">// off</span>
|
|
2160
|
+
createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack up to three</span></code></pre>
|
|
2161
|
+
</div>
|
|
2162
|
+
|
|
2163
|
+
<div class="why">
|
|
2164
|
+
<p><strong>This could not be <code>position: sticky</code>.</strong> The heading row is very
|
|
2165
|
+
often not in the page at all: the grid renders a window of rows, and a heading five hundred
|
|
2166
|
+
rows above the viewport was recycled long ago. So the pinned heading is synthesised from
|
|
2167
|
+
whichever group the top visible row belongs to, which the grid answers by binary search over
|
|
2168
|
+
an index it builds while flattening the rows — the ten-thousandth row of a group costs what
|
|
2169
|
+
the second one does.</p>
|
|
2170
|
+
<p>The cap exists because each heading costs a row of viewport. A five-level grouping without
|
|
2171
|
+
one would spend a third of the screen describing what the other two thirds contain.</p>
|
|
2172
|
+
</div>
|
|
2173
|
+
|
|
2174
|
+
<p class="lead-in">
|
|
2175
|
+
The pinned headings are hidden from assistive technology. Each is a duplicate of a row that is
|
|
2176
|
+
already in the tree, and announcing it again would report a group the reader has not moved to,
|
|
2177
|
+
and add an entry to a row count that virtualisation already makes hard to reconcile.
|
|
2178
|
+
</p>
|
|
2179
|
+
|
|
2180
|
+
<p class="lead-in">
|
|
2181
|
+
The same answer is available directly as <code>grid.rows.groupHeadings(index)</code>, which
|
|
2182
|
+
returns the enclosing group rows outermost first — for a breadcrumb, or a heading elsewhere on
|
|
2183
|
+
your page.
|
|
2184
|
+
</p>
|
|
2185
|
+
|
|
1649
2186
|
<h2 id="sources">Working with large data</h2>
|
|
1650
2187
|
<p class="lead-in">
|
|
1651
2188
|
A million rows in memory is fine — that is what the columnar store is for. Beyond that, or
|
|
@@ -1716,6 +2253,365 @@ columns: [
|
|
|
1716
2253
|
}}</code></pre>
|
|
1717
2254
|
</div>
|
|
1718
2255
|
|
|
2256
|
+
<h2 id="appearance">Grid lines and corners</h2>
|
|
2257
|
+
<p class="lead-in">
|
|
2258
|
+
Two settings that change how the grid is drawn rather than what it draws.
|
|
2259
|
+
</p>
|
|
2260
|
+
|
|
2261
|
+
<div class="example">
|
|
2262
|
+
<p class="example__label">Rules between cells, and rounded corners</p>
|
|
2263
|
+
<pre><code>createGrid(element, {
|
|
2264
|
+
columns, rows,
|
|
2265
|
+
gridLines: <span class="str">'both'</span>, <span class="cmt">// 'horizontal' (default), 'vertical', 'both', 'none'</span>
|
|
2266
|
+
cornerRadius: <span class="kw">true</span>, <span class="cmt">// or a number of pixels, or a CSS length</span>
|
|
2267
|
+
});</code></pre>
|
|
2268
|
+
</div>
|
|
2269
|
+
|
|
2270
|
+
<p class="lead-in">
|
|
2271
|
+
<code>gridLines</code> chooses which rules are drawn between cells. <code>'horizontal'</code>
|
|
2272
|
+
is the default and is what the grid has always drawn; vertical rules between body cells are
|
|
2273
|
+
additive, so the default is unchanged and nothing moves on upgrade. <code>'rows'</code> and
|
|
2274
|
+
<code>'columns'</code> are accepted as aliases. Only the rules between data are affected — the
|
|
2275
|
+
header underline and the seams beside pinned columns are structure rather than decoration, and
|
|
2276
|
+
removing them would make the pinned regions look detached.
|
|
2277
|
+
</p>
|
|
2278
|
+
|
|
2279
|
+
<p class="lead-in">
|
|
2280
|
+
<code>cornerRadius</code> rounds the grid's outer corners: <code>true</code> adopts the theme's
|
|
2281
|
+
own radius, a number is a count of pixels, and a string is used as written, so
|
|
2282
|
+
<code>'0 0 8px 8px'</code> rounds only the bottom. The body is clipped to match, so a row
|
|
2283
|
+
scrolling past a rounded corner is cut by it rather than squaring it off.
|
|
2284
|
+
</p>
|
|
2285
|
+
|
|
2286
|
+
<h2 id="cards">Cards, lists and feeds</h2>
|
|
2287
|
+
<p class="lead-in">
|
|
2288
|
+
<code>rowTemplate</code> draws each row with a layout of your own instead of dividing it into
|
|
2289
|
+
columns. A card list, a feed, a search-result list, a message list — any presentation where a
|
|
2290
|
+
record is a small piece of layout rather than a line of cells.
|
|
2291
|
+
</p>
|
|
2292
|
+
|
|
2293
|
+
<div class="example">
|
|
2294
|
+
<p class="example__label">A card list</p>
|
|
2295
|
+
<pre><code>createGrid(element, {
|
|
2296
|
+
columns, <span class="cmt">// still declared: they are the data model</span>
|
|
2297
|
+
rows,
|
|
2298
|
+
rowKey: <span class="str">'id'</span>,
|
|
2299
|
+
rowHeight: 64,
|
|
2300
|
+
rowTemplate: <span class="str">'<p class="title">{{data.name}}</p>'</span>
|
|
2301
|
+
+ <span class="str">'<p class="sub">{{data.owner}} · {{data.stage}}</p>'</span>,
|
|
2302
|
+
});</code></pre>
|
|
2303
|
+
</div>
|
|
2304
|
+
|
|
2305
|
+
<div class="why">
|
|
2306
|
+
<p><strong>The template compiles; it does not call back.</strong> There is deliberately no
|
|
2307
|
+
"here is a container, build what you like for this row" hook. That shape is easy to offer and
|
|
2308
|
+
would be used to allocate DOM per row, and at that moment the virtualisation stops paying for
|
|
2309
|
+
itself — quietly, and in a way nobody can attribute to a change. A row template is the same
|
|
2310
|
+
declarative string a cell template is: parsed once, built into real DOM the first time an
|
|
2311
|
+
element is used, and afterwards updated by writing text into the few nodes the bindings own.
|
|
2312
|
+
Scrolling ten thousand records through a hundred pooled cards allocates nothing.</p>
|
|
2313
|
+
<p><strong>Everything underneath is unchanged.</strong> Sorting, filtering, grouping,
|
|
2314
|
+
selection, permissions, redaction, saved views, undo, export and the remote source all apply
|
|
2315
|
+
exactly as they do to a table — only the drawing changes. That is the reason to build a card
|
|
2316
|
+
view on a grid rather than beside one.</p>
|
|
2317
|
+
</div>
|
|
2318
|
+
|
|
2319
|
+
<p class="lead-in">
|
|
2320
|
+
<strong>A card is still a row.</strong> It carries the same row identity a table row does, so
|
|
2321
|
+
<code>row:clicked</code> and <code>row:dblclicked</code> fire with the same payload, clicking
|
|
2322
|
+
selects, the context menu opens, and <code>rowReorder</code> works — with the card itself as
|
|
2323
|
+
the drag handle, since there is no cell to put a grip in. None of that is a second
|
|
2324
|
+
implementation; it is the same code that serves a table.
|
|
2325
|
+
</p>
|
|
2326
|
+
|
|
2327
|
+
<div class="why">
|
|
2328
|
+
<p><strong>It is announced as a list, not a grid.</strong> A card has no columns, so the
|
|
2329
|
+
<code>grid</code> role — which promises columns, <code>gridcell</code> children and a
|
|
2330
|
+
two-dimensional keyboard model — would misdescribe it completely. The layer is a
|
|
2331
|
+
<code>list</code>, each card a <code>listitem</code> carrying its position and the size of the
|
|
2332
|
+
whole set, and the column header is not drawn. <code>role</code> and <code>itemRole</code>
|
|
2333
|
+
override both, for a presentation that is really a <code>listbox</code>.</p>
|
|
2334
|
+
</div>
|
|
2335
|
+
|
|
2336
|
+
<h3>Collapsing to cards on a narrow screen</h3>
|
|
2337
|
+
<p class="lead-in">
|
|
2338
|
+
A table on a phone is a compromise however it is styled. <code>responsive</code> presents rows
|
|
2339
|
+
as cards when the grid is too narrow to be a table honestly, and returns to a table above the
|
|
2340
|
+
threshold.
|
|
2341
|
+
</p>
|
|
2342
|
+
|
|
2343
|
+
<div class="example">
|
|
2344
|
+
<p class="example__label">One grid, two presentations, chosen by width</p>
|
|
2345
|
+
<pre><code>createGrid(element, {
|
|
2346
|
+
columns, rows, rowKey: <span class="str">'id'</span>,
|
|
2347
|
+
toolPanel: <span class="kw">true</span>,
|
|
2348
|
+
responsive: {
|
|
2349
|
+
maxWidth: 640, <span class="cmt">// the default</span>
|
|
2350
|
+
rowHeight: 64,
|
|
2351
|
+
template: <span class="str">'<p class="title">{{cell.name}}</p><p>{{cell.owner}}</p>'</span>,
|
|
2352
|
+
},
|
|
2353
|
+
});</code></pre>
|
|
2354
|
+
</div>
|
|
2355
|
+
|
|
2356
|
+
<div class="why">
|
|
2357
|
+
<p><strong>Measured on the container, not the viewport.</strong> A media query is the obvious
|
|
2358
|
+
way and the wrong one: a grid inside a 400px panel on a large monitor is narrow, and a grid
|
|
2359
|
+
filling a small tablet is not. The grid already watches its own element for size changes, so
|
|
2360
|
+
the same observer answers this.</p>
|
|
2361
|
+
<p><strong>The state a user built survives the switch.</strong> Rotating a phone must not lose
|
|
2362
|
+
the sort, the filters, the selection or the scroll position, and it does not — it is one grid
|
|
2363
|
+
throughout, and only the drawing changes. An open cell editor is closed, since the cell it
|
|
2364
|
+
belonged to stops existing.</p>
|
|
2365
|
+
</div>
|
|
2366
|
+
|
|
2367
|
+
<p class="lead-in">
|
|
2368
|
+
<strong>Sorting and filtering need a home</strong> when there are no column headings to click,
|
|
2369
|
+
and the tool panel is it: set <code>toolPanel: true</code> and its rail stays available in card
|
|
2370
|
+
presentation with the columns and filter panels behind it. <strong>Export is unaffected</strong>
|
|
2371
|
+
— the columns are still the data model, so a CSV or an Excel file from a collapsed grid holds
|
|
2372
|
+
every column, including ones the card does not show.
|
|
2373
|
+
</p>
|
|
2374
|
+
|
|
2375
|
+
<p class="lead-in">
|
|
2376
|
+
A collapsed card takes <code>rowHeight</code> from the <code>responsive</code> block rather
|
|
2377
|
+
than the grid's, because a table row is too short to hold one; 64px by default. The change is
|
|
2378
|
+
announced, and the role tree follows the presentation. <code>presentation:changed</code> fires
|
|
2379
|
+
with <code>'cards'</code> or <code>'table'</code>.
|
|
2380
|
+
</p>
|
|
2381
|
+
|
|
2382
|
+
<h3>Showing what the grid shows</h3>
|
|
2383
|
+
<p class="lead-in">
|
|
2384
|
+
<code>{{cell.<em>column</em>}}</code> is the text the table puts in that cell — the column's
|
|
2385
|
+
own formatter, data type, number and date settings and lookup label, all of it.
|
|
2386
|
+
<code>{{data.<em>field</em>}}</code> is the raw value underneath.
|
|
2387
|
+
</p>
|
|
2388
|
+
|
|
2389
|
+
<div class="table-wrap">
|
|
2390
|
+
<table>
|
|
2391
|
+
<thead><tr><th>Binding</th><th>Reads</th></tr></thead>
|
|
2392
|
+
<tbody>
|
|
2393
|
+
<tr><td class="sig">{{cell.value}}</td><td class="desc">£1,250.50 — the cell's rendered text</td></tr>
|
|
2394
|
+
<tr><td class="sig">{{data.value}}</td><td class="desc">1250.5 — the stored number</td></tr>
|
|
2395
|
+
<tr><td class="sig">{{cell.stage}}</td><td class="desc">Held — a lookup's label</td></tr>
|
|
2396
|
+
<tr><td class="sig">{{data.stage}}</td><td class="desc">2 — the lookup's id</td></tr>
|
|
2397
|
+
</tbody>
|
|
2398
|
+
</table>
|
|
2399
|
+
</div>
|
|
2400
|
+
|
|
2401
|
+
<p class="lead-in">
|
|
2402
|
+
Both are wanted, which is why both exist: a card showing a value to a person wants
|
|
2403
|
+
<code>cell</code>, and a template comparing or calculating wants <code>data</code>. A lookup is
|
|
2404
|
+
the case that decides it — a card showing <code>2</code> where the table shows
|
|
2405
|
+
<code>Held</code> is not a formatting preference but a plain bug.
|
|
2406
|
+
</p>
|
|
2407
|
+
|
|
2408
|
+
<div class="why">
|
|
2409
|
+
<p><strong>A protected column cannot be read raw.</strong> Binding
|
|
2410
|
+
<code>{{data.password}}</code> on a secret or redacted column would print the value the column
|
|
2411
|
+
exists to hide, while the table beside it shows dots. Such a binding reads the masked text
|
|
2412
|
+
instead and says once that it did — a card must not become the hole a redaction closes.</p>
|
|
2413
|
+
</div>
|
|
2414
|
+
|
|
2415
|
+
<h3>Several cards on a line</h3>
|
|
2416
|
+
<p class="lead-in">
|
|
2417
|
+
By default a card takes a line to itself, which is what a feed or a search-result list wants.
|
|
2418
|
+
A gallery wants tiles, and there are two ways to ask for them because they answer different
|
|
2419
|
+
questions.
|
|
2420
|
+
</p>
|
|
2421
|
+
|
|
2422
|
+
<div class="example">
|
|
2423
|
+
<p class="example__label">A fixed count, and a size that flows</p>
|
|
2424
|
+
<pre><code>rowTemplate: { template: CARD, cardsPerRow: 3 } <span class="cmt">// always three across</span>
|
|
2425
|
+
rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as many as fit</span></code></pre>
|
|
2426
|
+
</div>
|
|
2427
|
+
|
|
2428
|
+
<p class="lead-in">
|
|
2429
|
+
<code>cardsPerRow</code> is a count, for a layout that must not reflow. <code>maxCardWidth</code>
|
|
2430
|
+
is a ceiling: the grid fits as many whole cards as it can without exceeding it, and they share
|
|
2431
|
+
the remaining space rather than leaving a ragged margin — so 900px at a 200px ceiling is four
|
|
2432
|
+
cards of 225px, and the count changes with the container. <code>gap</code> sets the space
|
|
2433
|
+
between them. Where both are given, <code>cardsPerRow</code> wins, being an instruction rather
|
|
2434
|
+
than a preference.
|
|
2435
|
+
</p>
|
|
2436
|
+
|
|
2437
|
+
<div class="why">
|
|
2438
|
+
<p><strong>The scroll height counts lines, not records.</strong> Four records on a line means
|
|
2439
|
+
the content is a quarter as tall as the row model alone would make it, and a scrollbar sized
|
|
2440
|
+
per record would be four times too long — the last several screens empty. The tiled layout
|
|
2441
|
+
works out its own window from the scroll position for the same reason: the window it would
|
|
2442
|
+
otherwise be handed counts one record per line and would leave the bottom of the screen bare.
|
|
2443
|
+
Pooling is unaffected; scrolling a tiled gallery reuses its elements exactly as a list does.</p>
|
|
2444
|
+
</div>
|
|
2445
|
+
|
|
2446
|
+
<p class="lead-in">
|
|
2447
|
+
Tiles are a fixed height, taken from <code>rowHeight</code> —
|
|
2448
|
+
<code>rowHeight: 'auto'</code> measures a rendered row and cannot describe a line holding
|
|
2449
|
+
several of different heights. Variable-height tiles flowing into the shortest column is a
|
|
2450
|
+
masonry layout, which is a different thing and is not offered.
|
|
2451
|
+
</p>
|
|
2452
|
+
|
|
2453
|
+
<p class="lead-in">
|
|
2454
|
+
Row heights work as they do everywhere else, including <code>rowHeight: 'auto'</code>, which
|
|
2455
|
+
measures the rendered card — content-driven card heights need no extra configuration. The
|
|
2456
|
+
columns are still declared and still hold the data: they are what sorting, filtering and
|
|
2457
|
+
export operate on, and what the bindings read.
|
|
2458
|
+
</p>
|
|
2459
|
+
|
|
2460
|
+
<h2 id="row-form">Editing a row on a form</h2>
|
|
2461
|
+
<p class="lead-in">
|
|
2462
|
+
Double-clicking a row opens it in a panel — a right-hand drawer or a centred dialog — with
|
|
2463
|
+
one control per field, a Save and a Cancel. It is the shape almost every application built on
|
|
2464
|
+
a grid ends up wanting, and until now the shape they had to build themselves.
|
|
2465
|
+
</p>
|
|
2466
|
+
|
|
2467
|
+
<div class="example">
|
|
2468
|
+
<p class="example__label">The grid's own columns, in a drawer</p>
|
|
2469
|
+
<pre><code>createGrid(element, {
|
|
2470
|
+
columns, rows, rowKey: <span class="str">'id'</span>,
|
|
2471
|
+
editable: <span class="kw">true</span>,
|
|
2472
|
+
rowForm: <span class="kw">true</span>,
|
|
2473
|
+
});</code></pre>
|
|
2474
|
+
</div>
|
|
2475
|
+
|
|
2476
|
+
<p class="lead-in">
|
|
2477
|
+
That is the whole of it for the common case: the form is the row, edited with the same
|
|
2478
|
+
editors, types, formats and lookups the cells use. Where the record has more to it than the
|
|
2479
|
+
grid shows, give a <code>load</code> function — and then say which fields and in what order,
|
|
2480
|
+
because nothing in the grid knows the shape of something it has never seen.
|
|
2481
|
+
</p>
|
|
2482
|
+
|
|
2483
|
+
<div class="example">
|
|
2484
|
+
<p class="example__label">A fuller record, in a dialog</p>
|
|
2485
|
+
<pre><code>rowForm: {
|
|
2486
|
+
mode: <span class="str">'dialog'</span>, <span class="cmt">// 'drawer' is the default</span>
|
|
2487
|
+
title: ({ data }) => <span class="str">'Edit '</span> + data.name,
|
|
2488
|
+
load: ({ key }) => fetch(<span class="str">`/api/orders/${key}`</span>).then((r) => r.json()),
|
|
2489
|
+
fields: [
|
|
2490
|
+
{ field: <span class="str">'name'</span>, label: <span class="str">'Name'</span> },
|
|
2491
|
+
{ field: <span class="str">'ref'</span>, label: <span class="str">'Reference'</span> }, <span class="cmt">// not a column</span>
|
|
2492
|
+
{ field: <span class="str">'notes'</span>, label: <span class="str">'Notes'</span>, editor: <span class="str">'textarea'</span> },
|
|
2493
|
+
{ field: <span class="str">'score'</span>, label: <span class="str">'Score'</span>, editor: <span class="str">'rating'</span>, props: { max: 5 } },
|
|
2494
|
+
],
|
|
2495
|
+
}</code></pre>
|
|
2496
|
+
</div>
|
|
2497
|
+
|
|
2498
|
+
<h3>Which editor a field gets</h3>
|
|
2499
|
+
<p class="lead-in">
|
|
2500
|
+
Every editor is available on a form, including your own from the module registry — the form
|
|
2501
|
+
builds its controls through the same call a cell does, so a field gets the same editor, type,
|
|
2502
|
+
formatting and lookup its column would have given it. A field named after a column borrows
|
|
2503
|
+
that column outright and needs nothing further.
|
|
2504
|
+
</p>
|
|
2505
|
+
<p class="lead-in">
|
|
2506
|
+
A field the grid has never seen — or one you want entered differently from its cell — says so
|
|
2507
|
+
on the field itself. <code>editor</code> names it, and <code>type</code>, <code>props</code>
|
|
2508
|
+
and <code>lookup</code> configure it exactly as they would on a column. Overriding the control
|
|
2509
|
+
does not change where the value goes: a field still writes back only if it maps to a column.
|
|
2510
|
+
</p>
|
|
2511
|
+
|
|
2512
|
+
<div class="why">
|
|
2513
|
+
<p><strong>A picker opens when it is asked to.</strong> A popup editor — a date, a dropdown, a
|
|
2514
|
+
tree, a colour, a code panel — <em>is</em> its panel: in a cell it opens the moment the cell
|
|
2515
|
+
does, which is right, because the user has just asked to edit that one cell. A form builds
|
|
2516
|
+
every field at once, so on a form the field shows the current value on a control and the panel
|
|
2517
|
+
opens over it when clicked. Choosing puts the panel away again and updates the control.</p>
|
|
2518
|
+
</div>
|
|
2519
|
+
|
|
2520
|
+
<div class="why">
|
|
2521
|
+
<p><strong>The panel opens before the record arrives.</strong> A click that does nothing for
|
|
2522
|
+
half a second reads as a click that was missed, and the user clicks again. So the panel
|
|
2523
|
+
appears immediately with a loading state and fills in when the record lands.</p>
|
|
2524
|
+
<p><strong>A failure keeps the panel open</strong> and offers a retry inside it. Closing would
|
|
2525
|
+
discard the intent and leave the user nothing to act on but the row they already
|
|
2526
|
+
double-clicked.</p>
|
|
2527
|
+
<p><strong>And a load that never answers is a failure too.</strong> A promise that neither
|
|
2528
|
+
resolves nor rejects is what a dropped request looks like from the page; left alone it spins
|
|
2529
|
+
until the user gives up, which reads as an application that has hung rather than a request
|
|
2530
|
+
that failed. After <code>timeout</code> milliseconds — two seconds unless you say otherwise —
|
|
2531
|
+
the form stops waiting and shows the same message and retry as any other failure. Set
|
|
2532
|
+
<code>timeout: false</code> to wait indefinitely, which is right only where your own loader
|
|
2533
|
+
already has a limit and would rather report that one. A record that turns up after the form
|
|
2534
|
+
gave up on it is discarded rather than dropped into a panel the user may have moved on
|
|
2535
|
+
from.</p>
|
|
2536
|
+
</div>
|
|
2537
|
+
|
|
2538
|
+
<p class="lead-in">
|
|
2539
|
+
The fields scroll and the heading and buttons do not, so Save stays reachable on a record with
|
|
2540
|
+
forty fields. If a validator refuses one of them, the form stays open, the field is marked, and
|
|
2541
|
+
it is scrolled into view and focused — on a long form the offending field is otherwise nowhere
|
|
2542
|
+
near the button that was just pressed.
|
|
2543
|
+
</p>
|
|
2544
|
+
|
|
2545
|
+
<p class="lead-in">
|
|
2546
|
+
Save collects the changed fields, writes the ones that map to columns, and announces the lot.
|
|
2547
|
+
Where the record actually lives is not something the grid can know, so persisting is yours: a
|
|
2548
|
+
field that came from <code>load</code> and is not a column is reported in
|
|
2549
|
+
<code>unmapped</code> and not written, since inventing a column for it would put data in the
|
|
2550
|
+
grid that the grid was never asked to show. Save is disabled while there is nothing to save.
|
|
2551
|
+
</p>
|
|
2552
|
+
|
|
2553
|
+
<div class="table-wrap">
|
|
2554
|
+
<table>
|
|
2555
|
+
<thead><tr><th>Member</th><th>Does</th></tr></thead>
|
|
2556
|
+
<tbody>
|
|
2557
|
+
<tr><td class="sig">form.open(key)</td><td class="desc">Open a row by key. Returns false if there is no such row.</td></tr>
|
|
2558
|
+
<tr><td class="sig">form.close()</td><td class="desc">Close without saving.</td></tr>
|
|
2559
|
+
<tr><td class="sig">form.save()</td><td class="desc">Commit the fields and close. Returns false if a validator refused, or if there is nothing to save.</td></tr>
|
|
2560
|
+
<tr><td class="sig">form.isOpen()</td><td class="desc">Whether the panel is showing.</td></tr>
|
|
2561
|
+
<tr><td class="name">form:opened</td><td class="desc">Fired with <code>{ key, row }</code>.</td></tr>
|
|
2562
|
+
<tr><td class="name">form:saved</td><td class="desc">Fired with <code>{ key, values, changed, unmapped }</code>.</td></tr>
|
|
2563
|
+
<tr><td class="name">form:closed</td><td class="desc">Fired with <code>{ key }</code>.</td></tr>
|
|
2564
|
+
<tr><td class="name">form:error</td><td class="desc">Fired with <code>{ key, error, timedOut }</code> when a load fails or runs out of time.</td></tr>
|
|
2565
|
+
</tbody>
|
|
2566
|
+
</table>
|
|
2567
|
+
</div>
|
|
2568
|
+
|
|
2569
|
+
<div class="why">
|
|
2570
|
+
<p><strong>The form takes the double click.</strong> On an editable grid that gesture also
|
|
2571
|
+
opens a cell editor, and the two cannot both own it — a form that quietly did nothing where a
|
|
2572
|
+
cell happened to be editable would be worse than no form. So where <code>rowForm</code> is
|
|
2573
|
+
configured, double-clicking a row opens the form and the cell editor stays reachable by
|
|
2574
|
+
Enter or by typing into the cell. Set <code>trigger: false</code> to leave opening entirely to
|
|
2575
|
+
<code>form.open()</code> and keep double-click for cells.</p>
|
|
2576
|
+
</div>
|
|
2577
|
+
|
|
2578
|
+
<h3>Putting the form in your own element</h3>
|
|
2579
|
+
<p class="lead-in">
|
|
2580
|
+
A drawer and a dialog both sit over the grid. Give <code>container</code> an element of your
|
|
2581
|
+
own and the form is built there instead — a sidebar beside the grid, a panel below it, a
|
|
2582
|
+
column in a layout you already have. It fills what it is given, so the size and position are
|
|
2583
|
+
yours.
|
|
2584
|
+
</p>
|
|
2585
|
+
|
|
2586
|
+
<div class="example">
|
|
2587
|
+
<p class="example__label">A sidebar the application owns</p>
|
|
2588
|
+
<pre><code>createGrid(element, {
|
|
2589
|
+
columns, rows, rowKey: <span class="str">'id'</span>, editable: <span class="kw">true</span>,
|
|
2590
|
+
rowForm: { container: <span class="str">'#record-panel'</span> }, <span class="cmt">// or the element itself, or a function</span>
|
|
2591
|
+
});</code></pre>
|
|
2592
|
+
</div>
|
|
2593
|
+
|
|
2594
|
+
<p class="lead-in">
|
|
2595
|
+
A selector is resolved when the form <em>opens</em>, not when the grid is configured, because
|
|
2596
|
+
a grid is routinely built before the layout around it exists. A container that cannot be found
|
|
2597
|
+
falls back to opening over the grid — better a form in the wrong place than a double-click
|
|
2598
|
+
that appears to do nothing.
|
|
2599
|
+
</p>
|
|
2600
|
+
|
|
2601
|
+
<div class="why">
|
|
2602
|
+
<p><strong>A form in your own container is not modal.</strong> It sits beside the grid rather
|
|
2603
|
+
than over it, so it takes nothing away: it is announced as a region rather than a dialog, and
|
|
2604
|
+
Tab moves out of it into the rest of your page instead of being trapped. Claiming otherwise
|
|
2605
|
+
would tell a screen reader user the page had gone away when it plainly has not. Escape still
|
|
2606
|
+
closes it, and it still takes focus when it opens.</p>
|
|
2607
|
+
</div>
|
|
2608
|
+
|
|
2609
|
+
<p class="lead-in">
|
|
2610
|
+
Over the grid, the panel is a modal dialog: it takes focus when it opens, traps Tab while it
|
|
2611
|
+
is showing, closes on Escape, and returns focus to whatever had it before. Its width can be
|
|
2612
|
+
set with <code>width</code>.
|
|
2613
|
+
</p>
|
|
2614
|
+
|
|
1719
2615
|
<h2 id="optimistic">Optimistic writes and rollback</h2>
|
|
1720
2616
|
<p class="lead-in">
|
|
1721
2617
|
The grid has always written optimistically without calling it that: an edit lands in the
|
|
@@ -2796,6 +3692,7 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
|
|
|
2796
3692
|
<tr><td class="name"><kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>H</kbd></td><td class="desc">Move focus to the column header</td></tr>
|
|
2797
3693
|
<tr><td class="name"><kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>P</kbd></td><td class="desc">Move focus to the tool panel</td></tr>
|
|
2798
3694
|
<tr><td class="name"><kbd>Shift</kbd>+<kbd>F10</kbd> <span class="sep">or</span> <kbd>ContextMenu</kbd></td><td class="desc">Open the context menu for the focused cell</td></tr>
|
|
3695
|
+
<tr><td class="name"><kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>ArrowUp</kbd> <span class="sep">or</span> <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>ArrowDown</kbd></td><td class="desc">Move the focused row, when row reorder is enabled</td></tr>
|
|
2799
3696
|
<tr><th colspan="2">On a column heading</th></tr>
|
|
2800
3697
|
<tr><td class="name"><kbd>ArrowLeft</kbd> <span class="sep">or</span> <kbd>ArrowRight</kbd></td><td class="desc">Move between headings</td></tr>
|
|
2801
3698
|
<tr><td class="name"><kbd>Ctrl</kbd>+<kbd>ArrowLeft</kbd> <span class="sep">or</span> <kbd>Ctrl</kbd>+<kbd>ArrowRight</kbd></td><td class="desc">First / last heading</td></tr>
|
|
@@ -3050,6 +3947,13 @@ grid.edit.pasteInto(text); <span class="cmt">// Excel's t
|
|
|
3050
3947
|
</div>
|
|
3051
3948
|
|
|
3052
3949
|
<h2 id="keyboard">Keyboard</h2>
|
|
3950
|
+
<p class="lead-in">
|
|
3951
|
+
Press <kbd>?</kbd> in the grid to see this list in the product. The overlay is generated from
|
|
3952
|
+
the same bindings the grid implements, so it cannot drift from them, and it shows
|
|
3953
|
+
<kbd>Cmd</kbd> rather than <kbd>Ctrl</kbd> on a Mac — the grid reads either, so that is what
|
|
3954
|
+
you will actually press. <kbd>Escape</kbd> closes it and focus returns where it was. Set
|
|
3955
|
+
<code>shortcuts: false</code> if you want <kbd>?</kbd> for something else.
|
|
3956
|
+
</p>
|
|
3053
3957
|
<div class="table-wrap">
|
|
3054
3958
|
<table>
|
|
3055
3959
|
<thead><tr><th>Keys</th><th>Does</th></tr></thead>
|
|
@@ -3324,6 +4228,33 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
|
|
|
3324
4228
|
a missing <code>return</code> is a typo and deleting the whole menu is a harsh reading of
|
|
3325
4229
|
one.</p>
|
|
3326
4230
|
</div>
|
|
4231
|
+
<p class="lead-in">
|
|
4232
|
+
<code>columnMenu</code> takes the same function form, for both routes into a column's menu:
|
|
4233
|
+
the header's 3-dot button and a right-click on the heading. Its <code>params</code> is
|
|
4234
|
+
<code>{ colId, column, grid }</code>, and the same rules apply — spread the defaults, return
|
|
4235
|
+
an empty array to suppress, return nothing to leave them alone.
|
|
4236
|
+
</p>
|
|
4237
|
+
<div class="example">
|
|
4238
|
+
<p class="example__label">An item that appears on some columns and not others</p>
|
|
4239
|
+
<pre><code>createGrid(el, {
|
|
4240
|
+
columns: [{ field: 'jan', title: 'Jan', context: { month: 1 } }],
|
|
4241
|
+
columnMenu: (params, defaults) => {
|
|
4242
|
+
<span class="cmt">// Your own keys are on the definition you wrote.</span>
|
|
4243
|
+
<span class="kw">const</span> month = params.column.def.context?.month;
|
|
4244
|
+
<span class="kw">if</span> (!month) <span class="kw">return</span> defaults;
|
|
4245
|
+
<span class="kw">return</span> [...defaults, { separator: <span class="kw">true</span> },
|
|
4246
|
+
{ name: 'Select quarter', action: () => selectQuarter(month) }];
|
|
4247
|
+
},
|
|
4248
|
+
});</code></pre>
|
|
4249
|
+
</div>
|
|
4250
|
+
<div class="why">
|
|
4251
|
+
<p><strong>Your properties are on <code>column.def</code>, not on the column itself.</strong>
|
|
4252
|
+
<code>column</code> is the grid's resolved interpretation of your definition and carries only
|
|
4253
|
+
keys the grid understands; <code>column.def</code> is the object you wrote, untouched. Keeping
|
|
4254
|
+
them apart means an application property can never collide with one the grid adds in a later
|
|
4255
|
+
version, and you do not have to maintain a lookup table keyed by column id alongside the
|
|
4256
|
+
columns themselves.</p>
|
|
4257
|
+
</div>
|
|
3327
4258
|
<div class="example">
|
|
3328
4259
|
<p class="example__label">A button of your own on the rail</p>
|
|
3329
4260
|
<pre><code>createGrid(el, {
|
|
@@ -3478,6 +4409,49 @@ grid.highlight.clear();</code></pre>
|
|
|
3478
4409
|
named view must not do.</p>
|
|
3479
4410
|
</div>
|
|
3480
4411
|
|
|
4412
|
+
<h3>When the columns change underneath a saved view</h3>
|
|
4413
|
+
<p class="lead-in">
|
|
4414
|
+
A saved view is user data written months ago against a column set that has since moved on. A
|
|
4415
|
+
release adds columns, renames one, drops another; the views people saved must survive it.
|
|
4416
|
+
</p>
|
|
4417
|
+
|
|
4418
|
+
<div class="table-wrap">
|
|
4419
|
+
<table>
|
|
4420
|
+
<thead><tr><th>What changed</th><th>What a saved view does</th></tr></thead>
|
|
4421
|
+
<tbody>
|
|
4422
|
+
<tr><td class="name">A column was added</td><td class="desc"><strong>It appears</strong>, in the state its definition declares, placed after every column the view names. A view is not a whitelist — it says nothing about columns it has never seen, and silence is not an instruction to hide.</td></tr>
|
|
4423
|
+
<tr><td class="name">A column was added, and should not appear yet</td><td class="desc">Declare it <code>layout: { hidden: true }</code>. The view does not mention it, so nothing overrides that, and it stays hidden until the user shows it.</td></tr>
|
|
4424
|
+
<tr><td class="name">A column was removed</td><td class="desc">The entries naming it are skipped and reported; the rest of the view applies. A sort or grouping on the missing column is dropped rather than left pointing at nothing.</td></tr>
|
|
4425
|
+
<tr><td class="name">A column was renamed</td><td class="desc">That is a removal and an addition. The old id is skipped, the new column appears at the end, and any width or pinning the user had set is lost with the old id.</td></tr>
|
|
4426
|
+
</tbody>
|
|
4427
|
+
</table>
|
|
4428
|
+
</div>
|
|
4429
|
+
|
|
4430
|
+
<div class="why">
|
|
4431
|
+
<p><strong>Applying a view never throws and never refuses.</strong> It returns a report —
|
|
4432
|
+
<code>{ applied, skipped }</code> — naming each thing it could not use and why. Refusing the
|
|
4433
|
+
whole view because one column has gone would lose a layout the user built deliberately, and
|
|
4434
|
+
throwing during a page load would lose the page. So a view degrades to as much of itself as
|
|
4435
|
+
still makes sense, and the host decides whether the user needs telling.</p>
|
|
4436
|
+
</div>
|
|
4437
|
+
|
|
4438
|
+
<div class="example">
|
|
4439
|
+
<p class="example__label">Telling the user their view has aged</p>
|
|
4440
|
+
<pre><code><span class="kw">const</span> report = grid.state.apply(saved.state);
|
|
4441
|
+
<span class="kw">if</span> (report.skipped.length) {
|
|
4442
|
+
<span class="cmt">// e.g. [{ key: 'columns.legacyRef', reason: 'unknown column' }]</span>
|
|
4443
|
+
notify(<span class="str">`This view was saved against an older layout; ${report.skipped.length} setting(s) no longer apply.`</span>);
|
|
4444
|
+
}</code></pre>
|
|
4445
|
+
</div>
|
|
4446
|
+
|
|
4447
|
+
<p class="lead-in">
|
|
4448
|
+
The consequence worth planning for is the first one: <strong>a column added in a new release
|
|
4449
|
+
is visible to everyone, including users with a saved view.</strong> That is usually what you
|
|
4450
|
+
want — a new field nobody can see is a field nobody uses — but if a release adds several at
|
|
4451
|
+
once, every saved view gains them all at the right-hand end. Ship them hidden if that is not
|
|
4452
|
+
the introduction you want.
|
|
4453
|
+
</p>
|
|
4454
|
+
|
|
3481
4455
|
<h3>Persisting them</h3>
|
|
3482
4456
|
<p class="lead-in">
|
|
3483
4457
|
<strong>The grid makes no network calls.</strong> It tells you what happened and you decide
|
|
@@ -3767,6 +4741,8 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
|
|
|
3767
4741
|
<tr><td class="name">sort:changed / filter:changed</td><td class="type">{ sort } / { filters }</td><td class="desc">Reflecting the view in the URL.</td></tr>
|
|
3768
4742
|
<tr><td class="name">history:changed</td><td class="type">{ canUndo, canRedo, undo, redo }</td><td class="desc">Driving your own undo button. Emitted <em>after</em> the entry is pushed, so the label is right.</td></tr>
|
|
3769
4743
|
<tr><td class="name">history:applied</td><td class="type">{ direction, step }</td><td class="desc">An action was undone or redone, with which and what. <code>history:changed</code> also fires when a new action is pushed, so it cannot distinguish the two.</td></tr>
|
|
4744
|
+
<tr><td class="name">form:saved</td><td class="type">{ key, values, changed, unmapped }</td><td class="desc">Persisting a row edited on a form. <code>unmapped</code> names the fields that are not columns, which the grid reports rather than writes.</td></tr>
|
|
4745
|
+
<tr><td class="name">form:error</td><td class="type">{ key, error, timedOut }</td><td class="desc">A row form's <code>load</code> failed or ran out of time. The panel stays open with a retry.</td></tr>
|
|
3770
4746
|
<tr><td class="name">view:saved / :removed</td><td class="type">{ view, views }</td><td class="desc">Persisting saved views to a server.</td></tr>
|
|
3771
4747
|
<tr><td class="name">render:done</td><td class="type">{ first, last }</td><td class="desc">Decorating cells from outside. The cell layer rewrites class names on every paint, so anything added before this is erased.</td></tr>
|
|
3772
4748
|
</tbody>
|
|
@@ -3874,7 +4850,7 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'<
|
|
|
3874
4850
|
|
|
3875
4851
|
<footer>
|
|
3876
4852
|
<p>
|
|
3877
|
-
Lattice Grid 1.
|
|
4853
|
+
Lattice Grid 1.6.1 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
|
|
3878
4854
|
Written against the shipped source. Where this guide and the code disagree, the code wins —
|
|
3879
4855
|
please <a href="https://www.latticegrid.dev">tell us</a>.
|
|
3880
4856
|
</p>
|