@guiexpert/table 1.1.107 → 1.1.109
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 +11 -0
- package/index.cjs +28 -28
- package/index.d.ts +2 -0
- package/index.js +2687 -2011
- package/lib/table/data/tablemodel/areamodel/area-model-object-array.d.ts +43 -0
- package/lib/table/data/tablemodel/areamodel/area-model.if.d.ts +14 -0
- package/lib/table/data/tablemodel/areamodel/col-and-rowspan-model.d.ts +12 -0
- package/lib/table/focus/focus-model.d.ts +12 -0
- package/lib/table/focus/focus-model.if.d.ts +11 -0
- package/lib/table/live-append/append-rows-options.if.d.ts +101 -0
- package/lib/table/live-append/row-index-shift.d.ts +71 -0
- package/lib/table/mouse-handler.d.ts +57 -0
- package/lib/table/render-scope.d.ts +31 -0
- package/lib/table/selection/selection-model.d.ts +17 -0
- package/lib/table/selection/selection-model.if.d.ts +13 -0
- package/lib/table/selection/selection-service.d.ts +8 -0
- package/lib/table/table-api.d.ts +82 -4
- package/lib/table/table-scope.d.ts +51 -0
- package/llms.txt +572 -0
- package/package.json +4 -1
package/llms.txt
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
# GuiExpert Table (@guiexpert/table)
|
|
2
|
+
|
|
3
|
+
> A framework-agnostic, high-performance data grid for the browser: virtual scrolling in both directions (millions of
|
|
4
|
+
> rows), zero runtime dependencies, full control over HTML and CSS, MIT-licensed (also for commercial use). One core
|
|
5
|
+
> (`@guiexpert/table`, plain TypeScript) with wrappers for Angular 14–22, React, Preact, Vue 3, Svelte 4/5, Solid and a
|
|
6
|
+
> Web Component.
|
|
7
|
+
|
|
8
|
+
This file is a usage guide for AI assistants and people: **recipes** (the shortest correct code for typical tasks),
|
|
9
|
+
**pitfalls** (symptom → cause → fix), quick answers and a feature reference. Every `ts` snippet below is compiled
|
|
10
|
+
against the real API (`pnpm check:llms`); names are exact – including the misspelled ones (`showCheckboxWihoutExtraColumn`,
|
|
11
|
+
`isHeaderVisibe`, `emmitDataKey`, `createCoverGrisDefaultOptions`), do not "correct" them. If something is not in this
|
|
12
|
+
file or in [instructions.md](https://github.com/guiexperttable/mono/blob/main/instructions.md), do not invent it –
|
|
13
|
+
the code wins (API: https://gui.expert/api).
|
|
14
|
+
|
|
15
|
+
## Packages
|
|
16
|
+
|
|
17
|
+
- `@guiexpert/table` – the core (plain JS/TS). Stylesheet `@guiexpert/table/css/main.css`.
|
|
18
|
+
- `@guiexpert/react-table`, `@guiexpert/preact-table`, `@guiexpert/solid-table`, `@guiexpert/svelte-table` (Svelte 4 and 5),
|
|
19
|
+
`@guiexpert/vue3-table`, `@guiexpert/webcomponent-table` – wrappers with the same version as the core (1.1.x / Svelte 5.1.x).
|
|
20
|
+
- `@guiexpert/angular-table@NN.x` – one build per Angular major 14 … 22 (`@guiexpert/angular-table@22` for Angular 22);
|
|
21
|
+
Angular 22+ also has `TableSignalComponent` (`<guiexpert-table-signal>`).
|
|
22
|
+
- `@guiexpert/demo-table-models` – demo data and model factories (not needed in apps).
|
|
23
|
+
|
|
24
|
+
Repository: https://github.com/guiexperttable/mono · License: MIT (current releases; future releases may be
|
|
25
|
+
dual-licensed, see https://gui.expert/license/).
|
|
26
|
+
|
|
27
|
+
## How to use – recipes
|
|
28
|
+
|
|
29
|
+
Concepts in one paragraph: a **table model** (`TableModelIf`, built by `TableFactory.createTableModel(...)`) holds three
|
|
30
|
+
**area models** (header, body, footer) and the column definitions; **table options** (`TableOptionsIf`, always
|
|
31
|
+
`{ ...new TableOptions(), ... }`) switch features on; a **`TableScope`** renders the model into a host `div` and gives you
|
|
32
|
+
the **`TableApi`** (`tableScope.getApi()`, in wrappers the `tableReady` event). Events go to an **`EventListenerIf`**
|
|
33
|
+
(extend `EventAdapter`). Indices in events and API calls are **display indices** (after filtering and sorting).
|
|
34
|
+
|
|
35
|
+
### R1 – First table (plain TypeScript)
|
|
36
|
+
|
|
37
|
+
How do I show an array of objects?
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import "@guiexpert/table/css/main.css"; // or <link> to https://cdn.jsdelivr.net/npm/@guiexpert/table/css/main.css
|
|
41
|
+
import { EventAdapter, SimpleDomService, TableFactory, TableOptions, TableScope } from "@guiexpert/table";
|
|
42
|
+
|
|
43
|
+
interface Order { id: number; customer: string; total: number }
|
|
44
|
+
const orders: Order[] = [{ id: 1, customer: "Smith & Co", total: 120.5 }, { id: 2, customer: "Jones", total: 99 }];
|
|
45
|
+
|
|
46
|
+
const host = document.querySelector<HTMLDivElement>(".table-div")!; // needs a fixed height, e.g. height: 500px
|
|
47
|
+
const tableModel = TableFactory.createTableModel({ rows: orders, properties: ["id", "customer", "total"] });
|
|
48
|
+
const tableOptions = { ...new TableOptions(), hoverColumnVisible: false };
|
|
49
|
+
const tableScope = new TableScope(host, tableModel, new SimpleDomService(), tableOptions, new EventAdapter());
|
|
50
|
+
tableScope.firstInit();
|
|
51
|
+
const tableApi = tableScope.getApi();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- The host `div` **must have a height** (`.table-div { height: 500px }` or a flex/grid parent) – otherwise nothing is visible.
|
|
55
|
+
- Values are shown as text by default (no renderer needed). Column labels, widths, renderers:
|
|
56
|
+
`columnDefs: [ColumnDef.create({ property: "total", headerLabel: "Total", width: px120 })]`
|
|
57
|
+
instead of `properties`. Sizes: `px0 … px500` or `new Size(2, "weight")` / `new Size(30, "%")`.
|
|
58
|
+
- Remove or replace the table: `tableApi.destroy()` first (wrappers do it themselves). → instructions.md 2, 4.1, 5.1–5.2
|
|
59
|
+
|
|
60
|
+
### R2 – The same table in a framework (and how to get the TableApi)
|
|
61
|
+
|
|
62
|
+
Every wrapper takes `tableModel` + `tableOptions` and reports the `TableApi` once (`tableReady`). **A new model or
|
|
63
|
+
options object re-creates the table** (state, scroll position, selection gone) – create them once (outside the
|
|
64
|
+
component, `useMemo`, a constant / `shallowRef`, a class field) and change data through the `TableApi`.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// React (Preact and Solid: same props)
|
|
68
|
+
import { GuiexpertTable } from "@guiexpert/react-table";
|
|
69
|
+
import { GeMouseEvent, TableApi, TableFactory, TableOptions } from "@guiexpert/table";
|
|
70
|
+
import { useMemo, useRef } from "react";
|
|
71
|
+
|
|
72
|
+
export function Orders({ orders }: { orders: { id: number; total: number }[] }) {
|
|
73
|
+
const api = useRef<TableApi>();
|
|
74
|
+
const tableModel = useMemo(() => TableFactory.createTableModel({ rows: orders, properties: ["id", "total"] }), [orders]);
|
|
75
|
+
const tableOptions = useMemo(() => new TableOptions(), []);
|
|
76
|
+
return <div className="table-div">
|
|
77
|
+
<GuiexpertTable tableModel={tableModel} tableOptions={tableOptions}
|
|
78
|
+
tableReady={(a: TableApi) => api.current = a}
|
|
79
|
+
mouseClicked={(e: GeMouseEvent) => console.log(e.rowIndex)} />
|
|
80
|
+
</div>;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- Callback props may be inline functions – the wrapper always calls the latest one; only `tableModel`/`tableOptions`
|
|
85
|
+
identity matters. React/Preact/Solid clicks are not debounced. The component fills its parent (100 % × 100 %): give
|
|
86
|
+
the parent a height.
|
|
87
|
+
- **Vue 3**: `<GuiexpertTable :tableModel :tableOptions @tableReady="api = $event" @mouseClicked="…" />`; model as a
|
|
88
|
+
constant or `shallowRef` (a deep `ref()` wraps it in a proxy). Clicks are debounced (`debounceMouseClickDelay`, 150 ms, `0` = immediate).
|
|
89
|
+
- **Angular** (14–22): `<guiexpert-table [tableModel]="model" [tableOptions]="options" (tableReady)="api = $event" (mouseClicked)="…">`
|
|
90
|
+
(`TableComponent`, standalone; `(mouseDraggingEnded)`, `(contextMenu)` – not `(contextmenu)`). **Angular 22+**:
|
|
91
|
+
`<guiexpert-table-signal [tableModel]="model" [(tableApi)]="api" />` with `api = signal<TableApi | undefined>(undefined)`
|
|
92
|
+
(no `tableReady`, zoneless). Guide: `packages/angular-table/instructions.md`.
|
|
93
|
+
- **Svelte**: `<GuiexpertTable {tableModel} {tableOptions} on:tableReady={e => api = e.detail} />` or (runes)
|
|
94
|
+
`onTableReady={a => api = a}`.
|
|
95
|
+
- **Web Component**: `customElements.define("guiexpert-table", GuiexpertTable)` yourself, then
|
|
96
|
+
`el.data = new TableModelAndOptions(tableModel, tableOptions)`; events are `CustomEvent`s on the element (`e.detail`).
|
|
97
|
+
→ instructions.md 4.2–4.8
|
|
98
|
+
|
|
99
|
+
### R3 – Mouse events (click, double click, context menu)
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { EventAdapter, GeMouseEvent, SimpleDomService, TableFactory, TableOptions, TableScope } from "@guiexpert/table";
|
|
103
|
+
|
|
104
|
+
interface Order { id: number; customer: string }
|
|
105
|
+
declare const host: HTMLDivElement;
|
|
106
|
+
declare const orders: Order[];
|
|
107
|
+
|
|
108
|
+
const listener = new EventAdapter();
|
|
109
|
+
listener.onMouseClicked = (evt: GeMouseEvent) => {
|
|
110
|
+
if (evt.areaIdent !== "body") return; // "header" | "body" | "footer"
|
|
111
|
+
const order = tableApi.getBodyModel().getRowByIndex(evt.rowIndex) as Order; // the DISPLAYED row (filter/sort applied)
|
|
112
|
+
if (evt.clickCount === 2) console.log("open", order.id); else console.log("select", order.id);
|
|
113
|
+
};
|
|
114
|
+
listener.onContextmenu = (evt: GeMouseEvent) => {
|
|
115
|
+
evt.originalEvent?.preventDefault(); // no browser menu – show your own menu
|
|
116
|
+
console.log("menu at", evt.originalEvent?.clientX, evt.originalEvent?.clientY, "row", evt.rowIndex);
|
|
117
|
+
};
|
|
118
|
+
const tableModel = TableFactory.createTableModel({ rows: orders, properties: ["id", "customer"] });
|
|
119
|
+
const tableScope = new TableScope(host, tableModel, new SimpleDomService(), new TableOptions(), listener);
|
|
120
|
+
tableScope.firstInit();
|
|
121
|
+
const tableApi = tableScope.getApi();
|
|
122
|
+
const unsubscribe = tableApi.subscribe({ onMouseClicked: e => console.log("second listener", e.rowIndex) });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- `rowIndex`/`columnIndex` are display/model indices – never use `orders[evt.rowIndex]` (wrong after sorting or
|
|
126
|
+
filtering). `getRowByIndex()` returns `any` (cast it); `undefined` outside the rows. Column by name:
|
|
127
|
+
`tableApi.getColumnIndex("customer")`. Without a selection model a click still fires `onMouseClicked` and moves the
|
|
128
|
+
focus frame – it only selects nothing.
|
|
129
|
+
- A double click arrives as a click with `clickCount: 1`, then one with `clickCount: 2`. On a header cell a double click
|
|
130
|
+
sorts (sortable columns), on an editable body cell it opens the editor.
|
|
131
|
+
- Since 1.1.108 a click is reported even if your code renders between `mousedown` and `mouseup` (live data).
|
|
132
|
+
- There is no built-in context menu – `onContextmenu` + your own HTML. `onMouseMoved` fires per mouse move (keep it cheap).
|
|
133
|
+
→ instructions.md 6.5, training "events-and-context-menu"
|
|
134
|
+
|
|
135
|
+
### R4 – Keyboard: built-in keys and your own keys
|
|
136
|
+
|
|
137
|
+
Built in (WAI-ARIA grid): arrows, Home/End, Ctrl/⌘+Home/End, Page Up/Down, Enter (`ACTIVATE_CELL`), F2 (edit),
|
|
138
|
+
Ctrl/⌘+A, Ctrl/⌘+C – macOS uses ⌘. There is **no keyboard event in `EventListenerIf`**: react to your own keys with a
|
|
139
|
+
`keydown` listener on the host and read the focused cell with `getActiveCell()`.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { ShortcutActionIdMapping, TableApi, TableOptions, TableOptionsIf } from "@guiexpert/table";
|
|
143
|
+
|
|
144
|
+
declare const host: HTMLDivElement; // the element you passed to TableScope (in wrappers: the component's element)
|
|
145
|
+
declare const tableApi: TableApi;
|
|
146
|
+
|
|
147
|
+
host.addEventListener("keydown", evt => { // the Delete key removes the focused row
|
|
148
|
+
if (evt.key !== "Delete") return;
|
|
149
|
+
const cell = tableApi.getActiveCell(); // [row, column] of the focused body cell, or undefined
|
|
150
|
+
if (!cell) return;
|
|
151
|
+
const row = tableApi.getBodyModel().getRowByIndex(cell[0]);
|
|
152
|
+
tableApi.removeRows([row]);
|
|
153
|
+
tableApi.repaintHard();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const shortcutActionIdMapping: ShortcutActionIdMapping = { "ctrl c": "COPY_2_CLIPBOARD", "f2": "START_EDITING" };
|
|
157
|
+
const tableOptions: TableOptionsIf = { ...new TableOptions(), shortcutActionIdMapping }; // spellings are harmonized
|
|
158
|
+
tableApi.triggerAction("NAVIGATE_DOWN"); // run an action from code
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- Every table has a cell focus by default (an own `FocusModel("cell")`): a click or the arrow keys move it, so
|
|
162
|
+
`getActiveCell()` works without configuration. To keep a reference, pass `getFocusModel: () => focusModel`
|
|
163
|
+
(`new FocusModel("cell")`) and read `focusModel.getFocus()`; `onFocusChanged` reports moves.
|
|
164
|
+
- Your `keydown` listener and the table's own handler both run (both listen on the host); the table does not stop the
|
|
165
|
+
event. Enter also triggers the built-in `ACTIVATE_CELL` (toggles a row checkbox / tree node, else nothing); with
|
|
166
|
+
`editing` (Excel-style) Delete also clears the selected cells – remap or disable those keys
|
|
167
|
+
(`shortcutActionIdMapping`, `shortcutActionsDisabled: true` switches all built-in keys off).
|
|
168
|
+
- `removeRows(rows, predicate?)` works on a `TableFactory.createTableModel({ rows })` body: O(n) (filters all rows into a
|
|
169
|
+
new array), returns nothing and does not repaint – call `repaintHard()`. Selection and focus indices are not
|
|
170
|
+
adjusted (the focus stays at the same index = the next row) – clear or re-set the selection afterwards.
|
|
171
|
+
- Your own shortcut that the browser also uses (Ctrl/⌘+S): `evt.preventDefault()` in your `keydown` listener; check
|
|
172
|
+
`evt.ctrlKey || evt.metaKey` (macOS).
|
|
173
|
+
- The table is **one tab stop**; controls inside cells must have `tabindex="-1"`.
|
|
174
|
+
→ instructions.md 6.4, 6.14
|
|
175
|
+
|
|
176
|
+
### R5 – Row selection and the selected rows
|
|
177
|
+
|
|
178
|
+
The default selection type is `"none"` – nothing is selected until you pass a selection model.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { CellRange, SelectionModel, TableApi, TableOptions, TableOptionsIf } from "@guiexpert/table";
|
|
182
|
+
|
|
183
|
+
interface Order { id: number; total: number }
|
|
184
|
+
declare const tableApi: TableApi;
|
|
185
|
+
|
|
186
|
+
const selectionModel = new SelectionModel("row", "multi"); // "none" | "cell" | "row" | "column" | "range"; "single" | "multi"
|
|
187
|
+
const tableOptions: TableOptionsIf = { ...new TableOptions(), getSelectionModel: () => selectionModel };
|
|
188
|
+
|
|
189
|
+
function selectedOrders(): Order[] { // the selected row OBJECTS
|
|
190
|
+
const body = tableApi.getBodyModel();
|
|
191
|
+
return selectionModel.getMergedRowIndices(body.getRowCount()).map(i => body.getRowByIndex(i) as Order);
|
|
192
|
+
}
|
|
193
|
+
const sumOfSelected = () => selectedOrders().reduce((sum, o) => sum + o.total, 0);
|
|
194
|
+
|
|
195
|
+
selectionModel.addSelection(CellRange.singleRow(3)); // select from code (repaint to show it)
|
|
196
|
+
tableApi.repaint();
|
|
197
|
+
tableApi.clearSelection(); // clears and repaints
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- React to changes: `listener.onSelectionChanged = (sm: SelectionModelIf) => …` (wrappers: `selectionChanged`) – fires
|
|
201
|
+
after mouse and keyboard changes, the model is already up to date (reading your own `selectionModel` is the same).
|
|
202
|
+
Mouse: click, Shift+click (range), Ctrl/⌘+click (toggle), drag (`"range"`). Focus (the frame) ≠ selection.
|
|
203
|
+
- `getMergedRowIndices()` returns a new sorted array of display indices – O(selected rows); for "select all" on huge
|
|
204
|
+
tables check `selectionModel.isAllSelected()` first.
|
|
205
|
+
- `tableApi.getSelectionAggregates()` = count/sum/avg/min/max of the selected **numeric cells** (all columns) – for one
|
|
206
|
+
column use the row objects as above.
|
|
207
|
+
- Indices: `externalFilterChanged()` and `setRows()` clear the selection; sorting keeps the indices (not the rows).
|
|
208
|
+
`appendRows()` keeps the selection at its rows. Restore by row id: state API part `selection` (`getState`/`applyState`).
|
|
209
|
+
- Checkboxes instead: `TableFactory.createTableModel({ rows, properties, rowCheckboxVisible: true })` + `onCheckboxChanged`.
|
|
210
|
+
→ instructions.md 6.4, 6.6, 6.11
|
|
211
|
+
|
|
212
|
+
### R6 – Change data: the right update
|
|
213
|
+
|
|
214
|
+
| What changed | Call | Cost |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| a few cell values | `tableApi.updateCells([new TableCellUpdateEvent("body", row, col, value, cssClasses?)])` | only these cells |
|
|
217
|
+
| values inside your row objects (same row count) | `tableApi.repaint()` | one render |
|
|
218
|
+
| row count or row heights (own area model) | `tableApi.repaintHard()` / `repaintHard({ py })` | one render |
|
|
219
|
+
| rows keep coming (log, stream) | `tableApi.appendRows(rows, { follow, maxRows })` (R7) | O(k), one render |
|
|
220
|
+
| a new data set | `tableApi.setRows(rows)` + `tableApi.repaintHard()` (+ `externalFilterChanged()` with a filter) | O(n) |
|
|
221
|
+
| new columns / structure | a new table model | rebuild – avoid for data |
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { TableApi, TableCellUpdateEvent } from "@guiexpert/table";
|
|
225
|
+
|
|
226
|
+
declare const tableApi: TableApi;
|
|
227
|
+
const changes = new Map<number, number>(); // display row → new price, collected from a WebSocket
|
|
228
|
+
|
|
229
|
+
function flush() { // once per animation frame, not once per message
|
|
230
|
+
const col = tableApi.getColumnIndex("price");
|
|
231
|
+
tableApi.updateCells([...changes].map(([row, price]) =>
|
|
232
|
+
new TableCellUpdateEvent("body", row, col, price, { "ge-table-color-error": price < 0 })));
|
|
233
|
+
changes.clear();
|
|
234
|
+
requestAnimationFrame(flush);
|
|
235
|
+
}
|
|
236
|
+
requestAnimationFrame(flush);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- `updateCells` takes the **displayed** row index and writes into that row object (after sorting/filtering this is not
|
|
240
|
+
the index in your array). A totals footer follows automatically. `updateCells` with a body from
|
|
241
|
+
`createTableModel({ rows })` updates **your** objects – read the old value before the call.
|
|
242
|
+
- `cssClasses` (`{ className: boolean }`, optional) adds (`true`) or removes (`false`) classes on the **current** cell
|
|
243
|
+
element only – a flash: it stays until the cell is drawn again (scrolling, a repaint), then it is gone. Set both
|
|
244
|
+
directions on every update (`{ "tick-up": up, "tick-down": !up }`), otherwise an old class stays. Colors that must
|
|
245
|
+
survive scrolling belong into the model (`getCustomClassesAt`, see the heatmap showcases). Your CSS, e.g.
|
|
246
|
+
`.tick-up { color: #15803d } .tick-down { color: #be123c }` (no prefix needed). A ticker:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { TableApi, TableCellUpdateEvent } from "@guiexpert/table";
|
|
250
|
+
|
|
251
|
+
interface Quote { symbol: string; price: number }
|
|
252
|
+
declare const tableApi: TableApi;
|
|
253
|
+
|
|
254
|
+
setInterval(() => { // 50 random prices every 100 ms – one updateCells call
|
|
255
|
+
const col = tableApi.getColumnIndex("price");
|
|
256
|
+
const body = tableApi.getBodyModel();
|
|
257
|
+
const events: TableCellUpdateEvent[] = [];
|
|
258
|
+
for (let n = 0; n < 50; n++) {
|
|
259
|
+
const row = Math.floor(Math.random() * body.getRowCount()); // a displayed row
|
|
260
|
+
const quote = body.getRowByIndex(row) as Quote;
|
|
261
|
+
const price = Math.round(quote.price * (1 + (Math.random() - 0.5) / 50) * 100) / 100;
|
|
262
|
+
const up = price >= quote.price;
|
|
263
|
+
events.push(new TableCellUpdateEvent("body", row, col, price, { "tick-up": up, "tick-down": !up }));
|
|
264
|
+
}
|
|
265
|
+
tableApi.updateCells(events); // only these cells are redrawn
|
|
266
|
+
}, 100);
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Showcase "crypto", training chapter "live-data".
|
|
270
|
+
→ instructions.md 6.9, 6.13, showcases "fps", "crypto"
|
|
271
|
+
|
|
272
|
+
### R7 – A rolling log: rows that keep coming (since 1.1.108)
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import { TableApi } from "@guiexpert/table";
|
|
276
|
+
|
|
277
|
+
interface LogLine { time: string; level: string; text: string }
|
|
278
|
+
declare const tableApi: TableApi;
|
|
279
|
+
const pending: LogLine[] = [];
|
|
280
|
+
let frozen = false, newBelow = 0, scheduled = false;
|
|
281
|
+
|
|
282
|
+
function onMessage(line: LogLine) { // e.g. WebSocket: buffer, append once per frame
|
|
283
|
+
pending.push(line);
|
|
284
|
+
if (!scheduled) { scheduled = true; requestAnimationFrame(flush); }
|
|
285
|
+
}
|
|
286
|
+
function flush() {
|
|
287
|
+
scheduled = false;
|
|
288
|
+
const result = tableApi.appendRows(pending.splice(0), { follow: frozen ? false : "auto", maxRows: 1_000_000 });
|
|
289
|
+
if (result) newBelow = result.following ? 0 : newBelow + result.shown; // "+N new lines"
|
|
290
|
+
}
|
|
291
|
+
function setFrozen(f: boolean) { frozen = f; } // "Freeze scroll" button
|
|
292
|
+
function scrollToEnd() { frozen = false; newBelow = 0; tableApi.repaintHard({ py: Number.MAX_SAFE_INTEGER }); }
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
- `appendRows` (body from `TableFactory.createTableModel({ rows })`): pushes into **your** array (no copy), runs the
|
|
296
|
+
filter only for the new rows, merges them into a sorted view, keeps selection/focus/anchor at their rows, renders once
|
|
297
|
+
– and not at all if only rows below the view came in. `follow: "auto"` = stay at the end if the view was at the end
|
|
298
|
+
(an empty table counts as "at the end"), otherwise keep the first visible row; `false` = always keep the first
|
|
299
|
+
visible row (also at the end, also while the ring buffer drops older rows – unless that row itself leaves); while the
|
|
300
|
+
mouse button is held, the rows under the pointer stay. After `scrollToEnd()` the view is at the end, so `"auto"`
|
|
301
|
+
follows again.
|
|
302
|
+
- Result `{ added, shown, removed, following }`: `shown` = new rows that passed the filter (use it for "+N new lines"),
|
|
303
|
+
`removed` = rows dropped by `maxRows`. A body that cannot append (tree, row groups, pivot) → `false`.
|
|
304
|
+
- `maxRows`: the oldest rows leave in blocks of ≥ 1 % (row count 99–100 % of `maxRows`). Measured (Chromium, Mac): ~3–4 ms
|
|
305
|
+
per update of 500 rows incl. render, the same for 100,000 and 2,000,000 rows; ~0.5 ms for a reader in the middle.
|
|
306
|
+
- Own area model: append yourself (a loop – `push(...lines)` overflows above ~100,000), then
|
|
307
|
+
`tableApi.rowsAppended(count, { follow: "auto", removedAtStart })`.
|
|
308
|
+
- Clicks: read the line in the handler (`getRowByIndex(evt.rowIndex)`) – a stored index points to another line after
|
|
309
|
+
the next update. Keys: R4. Pause the intake = keep buffering, call `appendRows` later once.
|
|
310
|
+
- Don't: `addRows()` per tick (copies all rows, resets filter and sort order), a new model per tick, `repaintHard()`
|
|
311
|
+
without a position. Showcase "logtail", `apps/table-demo/log-tail.html`. → instructions.md 6.9
|
|
312
|
+
|
|
313
|
+
### R8 – Millions of rows (own area model)
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
import { AreaModel, ColumnDef, TableFactory, px120 } from "@guiexpert/table";
|
|
317
|
+
|
|
318
|
+
class MillionRows extends AreaModel { // values on demand – no object per row
|
|
319
|
+
constructor(private readonly count: number) { super(); this.areaIdent = "body"; }
|
|
320
|
+
override getRowCount(): number { return this.count; }
|
|
321
|
+
override getRowHeight(): number { return 24; }
|
|
322
|
+
override getConstantRowHeight(): number { return 24; } // all rows equal → O(1) positions
|
|
323
|
+
override getValueAt(row: number, col: number): string { return col === 0 ? String(row) : `cell ${row}/${col}`; }
|
|
324
|
+
}
|
|
325
|
+
const tableModel = TableFactory.createTableModel({
|
|
326
|
+
bodyAreaModel: new MillionRows(10_000_000),
|
|
327
|
+
columnDefs: ["#", "A", "B"].map((label, i) => new ColumnDef("c" + i, label, px120)),
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
- Variable heights: return them from `getRowHeight(i)` and leave out `getConstantRowHeight()` (prefix sums, O(log n)).
|
|
332
|
+
The scroll height comes from the model (since 1.1.107). Showcase "large" (2 million rows). → instructions.md 5.5
|
|
333
|
+
|
|
334
|
+
### R9 – Sorting and filtering
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { ColumnDef, TableApi, TableOptions, TableOptionsIf, TrueFn } from "@guiexpert/table";
|
|
338
|
+
|
|
339
|
+
interface Order { id: number; customer: string; total: number }
|
|
340
|
+
declare const tableApi: TableApi;
|
|
341
|
+
let search = "";
|
|
342
|
+
|
|
343
|
+
const columnDefs = [
|
|
344
|
+
ColumnDef.create({ property: "customer", headerLabel: "Customer", sortable: TrueFn }),
|
|
345
|
+
ColumnDef.create({ property: "total", headerLabel: "Total", sortable: TrueFn,
|
|
346
|
+
sortComparator: (a: number, b: number) => a - b }),
|
|
347
|
+
];
|
|
348
|
+
const tableOptions: TableOptionsIf = {
|
|
349
|
+
...new TableOptions(),
|
|
350
|
+
externalFilterFunction: (order: Order) => order.customer.toLowerCase().includes(search),
|
|
351
|
+
};
|
|
352
|
+
function onSearch(text: string) { search = text.toLowerCase(); tableApi.externalFilterChanged(); }
|
|
353
|
+
tableApi.sortByColumn("total", "desc"); // like a double click on the header
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
- The header sorts on **double click** (not single click). After changing data: `tableApi.reSort()`.
|
|
357
|
+
- `externalFilterChanged()` filters all rows, clears the selection and scrolls to the top. There is no built-in filter
|
|
358
|
+
UI – build inputs yourself. → instructions.md 6.2, 6.3
|
|
359
|
+
|
|
360
|
+
### R10 – Own cell renderer
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import { AreaIdent, AreaModelIf, CellRendererIf, ColumnDef, DomServiceIf, RendererCleanupFnType } from "@guiexpert/table";
|
|
364
|
+
|
|
365
|
+
class StatusRenderer implements CellRendererIf {
|
|
366
|
+
render(cellDiv: HTMLDivElement, rowIndex: number, columnIndex: number, areaIdent: AreaIdent,
|
|
367
|
+
areaModel: AreaModelIf, cellValue: unknown, domService: DomServiceIf): RendererCleanupFnType | undefined {
|
|
368
|
+
const badge = domService.createElement<HTMLSpanElement>("span");
|
|
369
|
+
domService.addClass(badge, "status-" + String(cellValue));
|
|
370
|
+
domService.appendChild(badge, domService.createText(String(cellValue))); // text – never data in innerHTML
|
|
371
|
+
domService.appendChild(cellDiv, badge);
|
|
372
|
+
return undefined; // or a cleanup function
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const column = ColumnDef.create({ property: "status", headerLabel: "Status", bodyRenderer: new StatusRenderer() });
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
- Built in: `NumberCellRenderer`, `ProgressBarCellRenderer`, `StarRatingCellRenderer`, `DateToLocaleDateCellRenderer`,
|
|
379
|
+
`TrueFalseCellRenderer`, `CheckboxCellRenderer`, … Framework components as cells: `ComponentRendererWrapper` (React,
|
|
380
|
+
Preact, Vue, Svelte, Solid), Angular `RenderWrapperFactory.create(MyComponent, cdr)`.
|
|
381
|
+
- If you must build HTML: `escapeHtml(value)`. Buttons/inputs in a cell: `tabindex="-1"`. → instructions.md 6.1, 6.14
|
|
382
|
+
|
|
383
|
+
### R11 – Editing
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
import { ColumnDef, EventAdapter, GeModelChangeEvent, TableOptions, TableOptionsIf, TrueFn } from "@guiexpert/table";
|
|
387
|
+
|
|
388
|
+
const columns = [ColumnDef.create({ property: "qty", headerLabel: "Qty", editable: TrueFn,
|
|
389
|
+
validate: (value: unknown) => typeof value === "number" && value >= 0 ? undefined : "must be ≥ 0" })];
|
|
390
|
+
const tableOptions: TableOptionsIf = { ...new TableOptions(), editing: true }; // Excel-style: type over, Enter, Tab, Delete
|
|
391
|
+
const listener = new EventAdapter();
|
|
392
|
+
listener.onModelChanged = (evt: GeModelChangeEvent) => {
|
|
393
|
+
evt.changes?.forEach(c => console.log(c.property, c.oldValue, "→", c.newValue, "row", c.row)); // save here
|
|
394
|
+
};
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
- Without `editing`: double click / F2 opens the classic in-cell editor (`editable`). "Collect, then save":
|
|
398
|
+
`batchEdit: true` + `commitBatchEdit()`. Paste from Excel: `clipboard: true`; undo: `undo: true`.
|
|
399
|
+
→ instructions.md 6.7, 6.17, 6.20–6.22
|
|
400
|
+
|
|
401
|
+
### R12 – Columns: fixed, hidden, width, index
|
|
402
|
+
|
|
403
|
+
- Fixed columns belong to the **model**: `TableFactory.createTableModel({ rows, properties, fixedLeftColumnCount: 1, fixedRightColumnCount: 1 })`.
|
|
404
|
+
- `tableApi.setColumnVisible("total", false)`, `tableApi.setColumnWidth(index, px)`, `tableApi.autoResizeColumns()`.
|
|
405
|
+
- Row numbers (`rowNumbersVisible: true`), a checkbox column or a row-group column shift all column indices by one –
|
|
406
|
+
always `tableApi.getColumnIndex(property)`. Header groups: `instructions.md` 5.2, demo "headergroup".
|
|
407
|
+
|
|
408
|
+
### R13 – Look and feel
|
|
409
|
+
|
|
410
|
+
- CSS variables `--ge-table-*` (`packages/table/css/table-color-vars.css`), dark mode with `<html data-theme="dark">`.
|
|
411
|
+
- Hide scroll bars: `.my-table .ge-table-scroll-viewport { scrollbar-width: none; }` (no option needed).
|
|
412
|
+
- Right-to-left: `tableOptions.direction: "rtl"` (or `"auto"`). Borders, hover: `verticalBorderVisible`,
|
|
413
|
+
`hoverRowVisible`, `hoverColumnVisible`. → instructions.md 6.12, 6.19
|
|
414
|
+
|
|
415
|
+
### R14 – Remember the state (sorting, widths, scroll position, selection)
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
import { AutoRestoreOptions, TableApi, TableOptions, TableOptionsIf } from "@guiexpert/table";
|
|
419
|
+
|
|
420
|
+
declare const tableApi: TableApi;
|
|
421
|
+
const tableOptions: TableOptionsIf = {
|
|
422
|
+
...new TableOptions(),
|
|
423
|
+
autoRestoreOptions: {
|
|
424
|
+
...new AutoRestoreOptions<{ id: number }>(),
|
|
425
|
+
getStorageKeyFn: () => "orders-table", // REQUIRED – without a key nothing is stored
|
|
426
|
+
getRowId: row => row.id, // selection/checked rows by id, not by index
|
|
427
|
+
autoRestoreSortingState: true, autoRestoreColumnLayout: true, autoRestoreScrollPosition: true,
|
|
428
|
+
autoRestoreSelectedState: true,
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
const state = tableApi.getState(); // JSON for your server; later tableApi.applyState(state)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
→ instructions.md 6.11
|
|
435
|
+
|
|
436
|
+
### R15 – Export
|
|
437
|
+
|
|
438
|
+
`await tableApi.copyToClipboard()` (selection, tab-separated) and `tableApi.downloadExcel("orders.xlsx", "author")`
|
|
439
|
+
(own XLSX writer, the **current view**: filtered and sorted rows + header + footer). All rows or other data:
|
|
440
|
+
`new ExcelService().downloadExcel(matrix, …)`. → instructions.md 6.10
|
|
441
|
+
|
|
442
|
+
### R16 – Lifecycle
|
|
443
|
+
|
|
444
|
+
Plain TS: call `tableApi.destroy()` before you replace the model or remove the host (listeners, ResizeObserver, renderer
|
|
445
|
+
cleanups). Wrappers do it on unmount and on a new model/options object. Never create a second `TableScope` on the same
|
|
446
|
+
host without destroying the first.
|
|
447
|
+
|
|
448
|
+
### R17 – Not built in (and how to do it)
|
|
449
|
+
|
|
450
|
+
- **Filter UI**: inputs of your own + `externalFilterFunction` + `externalFilterChanged()` (R9).
|
|
451
|
+
- **Context menu**: `onContextmenu` + your own HTML menu (R3); example `apps/table-demo/spreadsheet.html`.
|
|
452
|
+
- **Pagination**: not needed for size (virtual scrolling); if wanted, `setRows(page)` + `repaintHard()`.
|
|
453
|
+
- **Server-side row model** (fetch rows on demand): an own `AreaModel` whose `getValueAt` reads a cache and requests
|
|
454
|
+
missing blocks; `repaint()` when a block arrives (R8).
|
|
455
|
+
- **Drag between tables**: row drag works inside one table (`rowDrag: true`); between tables use `onRowDragEnd` +
|
|
456
|
+
`rowDragOptions.managed: false` and move the data yourself.
|
|
457
|
+
|
|
458
|
+
More (short): tree + checkboxes `TreeFactory` (instructions.md 6.6), master/detail `masterDetail` (6.18), row grouping
|
|
459
|
+
`RowGroupFactory` (5.6), pivot `PivotFactory` (5.7), calculated columns `valueGetter`/`formula` (5.2, 5.9), spreadsheet
|
|
460
|
+
`SpreadsheetFactory.create` (6.25), cover grid (5.10), totals footer `footerAggregates` (5.4), row numbers (5.2).
|
|
461
|
+
|
|
462
|
+
## Pitfalls
|
|
463
|
+
|
|
464
|
+
Symptom → cause → fix.
|
|
465
|
+
|
|
466
|
+
- **The table is empty / invisible** → the host has no height or the CSS is missing → give the host a fixed height and
|
|
467
|
+
load `@guiexpert/table/css/main.css`.
|
|
468
|
+
- **The table is rebuilt on every render; selection, scroll position and sorting are lost** → a new model or options
|
|
469
|
+
object per render (inline in JSX/templates, a deep `ref()`, a computed that creates a new object) → create them once;
|
|
470
|
+
change data through the `TableApi` (R6).
|
|
471
|
+
- **Clicks select nothing / no `onSelectionChanged`** → selection type defaults to `"none"` → pass
|
|
472
|
+
`getSelectionModel: () => new SelectionModel("row", "multi")`.
|
|
473
|
+
- **The wrong row is used after sorting/filtering** → `rowIndex` is the displayed index → `getRowByIndex(rowIndex)`,
|
|
474
|
+
never `myRows[rowIndex]`.
|
|
475
|
+
- **The wrong column** → row numbers/checkbox/group column shift indices → `tableApi.getColumnIndex(property)`.
|
|
476
|
+
- **Single click on the header does not sort** → it sorts on double click → or `tableApi.sortByColumn(...)`.
|
|
477
|
+
- **Selection gone after filtering; view jumps to the top** → `externalFilterChanged()` does that by design → restore via
|
|
478
|
+
the state API (`getState(["selection"])` / `applyState`) with `getRowId`.
|
|
479
|
+
- **`repaint()` after adding rows shows nothing new / wrong scroll bar** → the row count changed → `repaintHard()`
|
|
480
|
+
(or `appendRows`).
|
|
481
|
+
- **Slow live updates** → a new model, `setRows` or `addRows` per tick → `updateCells` (values) or `appendRows` (new
|
|
482
|
+
rows), once per animation frame.
|
|
483
|
+
- **`Maximum call stack size exceeded`** → `array.push(...rows)` with ~100,000+ rows → a `for` loop (or `appendRows`).
|
|
484
|
+
- **Clicks lost while the table updates** (before 1.1.108) → a render between `mousedown` and `mouseup` → update to
|
|
485
|
+
1.1.108; older: render only when the visible part changed.
|
|
486
|
+
- **Context menu right after creating the table is ignored** → the first 50 ms are swallowed → normal; Angular: use
|
|
487
|
+
`(contextMenu)` – `(contextmenu)` fires twice (also the native event).
|
|
488
|
+
- **Clicks arrive late in Angular/Vue** → `debounceMouseClickDelay` (150 ms) → `0` for immediate clicks (a double click
|
|
489
|
+
then also delivers the single click first).
|
|
490
|
+
- **Own model with 20 px rows shows a scroll bar / wrong height** (before 1.1.107) → `defaultRowHeights` won → update,
|
|
491
|
+
or set `defaultRowHeights.body` to your height. Since 1.1.107 the model's `getRowHeight()` wins.
|
|
492
|
+
- **Shortcut works on Windows, not on the Mac** → checked `ctrlKey` → the table uses ⌘ on macOS; map both or use the
|
|
493
|
+
table's actions.
|
|
494
|
+
- **Second tab stop / keyboard focus lost in a cell** → focusable elements in a cell → `tabindex="-1"`.
|
|
495
|
+
- **XSS from cell values** → data in `innerHTML` of a renderer → `textContent` / `domService.createText` / `escapeHtml`.
|
|
496
|
+
- **State is not stored** → `autoRestoreOptions` without `getStorageKeyFn` (a console warning says so) → add the key.
|
|
497
|
+
- **`instanceof AreaModelTree` is false** → two copies of the core in the bundle → `isAreaModelTree(x)`, `isTreeRow(x)`;
|
|
498
|
+
dedupe `@guiexpert/table`.
|
|
499
|
+
- **TypeScript: unknown option** → "corrected" a misspelled API → use `showCheckboxWihoutExtraColumn`, `isHeaderVisibe`,
|
|
500
|
+
`emmitDataKey`, `createCoverGrisDefaultOptions` as they are.
|
|
501
|
+
- **`AsyncBodyAreaModelObjectArray` cannot be imported** → unfinished, not exported → own `AreaModel` (R8, R17).
|
|
502
|
+
- **Invented APIs** – these do NOT exist: `getSelectedRows()`, `onKeyDown`, `api.addRow()`, `api.refresh()`,
|
|
503
|
+
`setData()`, `gridOptions`, `rowData`, `columnDefs` as a table option (it is a model parameter). Use R4–R7.
|
|
504
|
+
|
|
505
|
+
## Quick answers (FAQ)
|
|
506
|
+
Full answers: instructions.md section 6.26 (https://github.com/guiexperttable/mono/blob/main/instructions.md#626-faq--quick-answers).
|
|
507
|
+
- Hide the scroll bars: CSS `.my-table .ge-table-scroll-viewport { scrollbar-width: none; }` – the table uses the full width/height, wheel, touch, keyboard and the API still scroll. No table option needed (`overflowY` is only "auto" | "scroll"). Thin/colored: `scrollbar-width: thin; scrollbar-color: …`.
|
|
508
|
+
- Scroll bar although all rows fit (own AreaModel): fixed in 1.1.107 – the scroll height comes from the model's `getConstantRowHeight()` / `getRowHeight()`. Older versions: set `defaultRowHeights.body` to the model's row height.
|
|
509
|
+
- Rows appended continuously (live log, ticker) and keep the position or follow the end: `tableApi.repaintHard({ py })` – one render pass (since 1.1.107); follow the end with `py: Number.MAX_SAFE_INTEGER`.
|
|
510
|
+
- Clicks lost while the app renders (live tail, `repaint()`/`repaintHard()` between mousedown and mouseup): fixed in 1.1.108 – the table reports `mouseClicked` (and the double click) for the pressed cell itself, once, with the current index of that row object.
|
|
511
|
+
- Rows that keep coming (log, stream, ticker of trades): `tableApi.appendRows(rows, { follow: "auto", maxRows })` (since 1.1.108) – in place, O(k), one render, filter/sort only for new rows, selection/focus stay; freeze = `follow: false`, jump to the end = `repaintHard({ py: Number.MAX_SAFE_INTEGER })`. Own AreaModel: `rowsAppended(count, { removedAtStart })`. Not `addRows()`.
|
|
512
|
+
- `repaint()` = values changed, same row count; `repaintHard()` = row count or heights changed; `updateCells()` = single cells.
|
|
513
|
+
- Variable row heights: implement `getRowHeight(i)`; `getConstantRowHeight()` only if all rows are equally high. `defaultRowHeights` only applies to models the factory builds or whose `getRowHeight()` returns 0.
|
|
514
|
+
- Empty table: the host div needs a fixed height and the stylesheet `@guiexpert/table/css/main.css`.
|
|
515
|
+
|
|
516
|
+
## Feature reference
|
|
517
|
+
|
|
518
|
+
What the table can do – one line per feature, details in instructions.md (each line names the options and API).
|
|
519
|
+
|
|
520
|
+
- Virtual scrolling for handling large datasets
|
|
521
|
+
- Excellent performance for large tables
|
|
522
|
+
- No third-party dependencies
|
|
523
|
+
- UI-agnostic design
|
|
524
|
+
- Column interactions (resize, reorder)
|
|
525
|
+
- Row sorting
|
|
526
|
+
- Row, column, and range selection
|
|
527
|
+
- Single and multi-selection
|
|
528
|
+
- Row and column spanning
|
|
529
|
+
- Fixed columns (left and right)
|
|
530
|
+
- Tree table (hierarchical view)
|
|
531
|
+
- Accessibility: WAI-ARIA grid/treegrid for screen readers (aria-rowcount/rowindex with virtual scrolling, aria-activedescendant focus, aria-sort, aria-selected, live-region announcements; options ariaLabel, ariaLabelledBy, ariaDescribedBy, ariaTexts; tableApi.announce()) and keyboard navigation (arrows, Home/End, Ctrl+Home/End, Page Up/Down, Enter, header focus) – since 1.1.91
|
|
532
|
+
- Overlays: "Loading…" with a CSS spinner (tableApi.showLoadingOverlay(text?), hideOverlay(), getOverlayState(); `loading` prop in all framework wrappers, no table re-creation) and automatic "No rows to show" / "No matching rows" (tableOptions.overlayOptions: texts, autoNoRows, blockInteractionWhileLoading, loadingDelayInMs, getOverlayElement; CSS variables --ge-table-overlay-*, --ge-table-spinner-*) – since 1.1.92
|
|
533
|
+
- Row numbers: TableFactory.createTableModel({ rows, rowNumbersVisible: true | { mode: "display" | "original", format, headerLabel, exported, minWidthInPx } }) or new RowNumberColumnDef() as first column; fixed left, width follows the largest number, role="rowheader", click selects the row, "#" selects all, not in copy/Excel unless exported; column indices shift by one – tableApi.getColumnIndex(property) – since 1.1.93
|
|
534
|
+
- Aggregation / total rows: TableFactory.createTableModel({ rows, columnDefs, footerAggregates: { total: "sum" } | [{ label, aggregates }] | { rows, labelProperty, rowHeight, source: "filtered" | "all", getRows } }) builds an AreaModelAggregateFooter (keys = column properties; aggregators "sum" | "count" | "avg" | "min" | "max" | "countDistinct" | "first" | "last" or own AggregatorIf { init, add, merge, result }; spec { aggregator, value: path | (row) => unknown, format }); follows filter and sorting, tree = filtered leaves, recomputed after filter/sort/edit/updateCells/tableApi.repaint() (AreaModelIf.invalidateDerivedValues?), never while scrolling; label cell role="rowheader"; Excel gets numbers. Helpers aggregate(rows, value, aggregator), aggregateColumns(rows, specs), Aggregators, formatAggregateValue. Selection: tableApi.getSelectionAggregates() → { count, numericCount, sum, avg, min, max, capped } (SelectionAggregator.maxCells) – since 1.1.94
|
|
535
|
+
- Row grouping: RowGroupFactory.createTableModel<T>({ rows, columnDefs, groupBy: ["region", (row) => key, { value, name, label, format, order: "asc" | "desc" | "insertion" | fn }], aggregations: { umsatz: "sum" }, footerAggregates?, groupColumn?: { headerLabel, width, showCount, label, fixed }, hideGroupedColumns?, expanded?: boolean | level | fn, emptyLabel? }) → body AreaModelRowGroups (a tree: isAreaModelTree true, isAreaModelRowGroups true) with a fixed RowGroupColumnDef ("Nord (3)"), grouped columns hidden, total row; group = merge of sub-group states (avg stays correct); externalFilterFunction gets the row objects, aggregates/counts follow the filter; sort by aggregate / group key, tableApi.sort(fn) sorts rows inside groups; edits recompute only the changed path (invalidateDerivedValues(change?: DerivedValuesChangeIf)), a changed grouped value moves the row; tableApi.setRowGroupBy(levels) / getRowGroupBy() regroup without a new model (expanded state by stable RowGroup.id kept); model: getGroups(), findGroup(path), getRowGroup(rowIndex), getDetailRow(rowIndex), getTotals(), getCheckedRows(), expandToLevel(n), setRows(), regroup(); engine without table: groupRows(rows, groupBy), aggregateRowGroups(groups, specs, accept?); isRowGroup, isGroupRow, isRowGroupColumnDef; CSS ge-table-row-group*, --ge-table-row-group-* – since 1.1.94
|
|
536
|
+
- Pivot tables: PivotFactory.createTableModel<T>({ rows, rowFields: RowGroupField[] (like groupBy), columnFields?: RowGroupField[], values: ("path" | fn | { value, aggregator?: "sum" (default) | "avg" | … | AggregatorIf, name?, label?, format? })[], subtotals?, columnSubtotals? (default: ≥ 2 column fields, collapsible column groups), totalColumn?: "fixed" (default) | true | false, totalRow?, maxColumns? (1000, more keys are cut off with a warning, totals still count all rows), expanded?, emptyLabel?, emptyText? (empty combination = undefined, not 0), totalLabel?, groupColumn?, valueColumnWidth?, rowNumbersVisible? }) → PivotTableModel (a TableModel) with body AreaModelPivot (a tree of RowGroup rows, isAreaModelTree true), header AreaModelCellGroups, footer AreaModelPivotFooter; subtotals/totals merged from aggregation states (avg correct); externalFilterFunction gets the source objects (filter before the aggregation), columns stay; sort by a value column / key; tableApi.setPivotConfig(changes, rows?) / getPivotConfig() pivot again without a new model; body: getResult() (PivotResult: rowGroups, columnGroups, columnLeaves, values, truncated, getValue(row?, column?, value?), getCount, getSourceRows), getColumnInfo(c), getColumnProperty(path, value?), getColumnIndex(path), getCellSourceRows(r, c), getRowGroup(r); engine without table: pivotRows(rows, config, accept?); isPivotTableModel, isAreaModelPivot, isPivotColumnDef; CSS ge-table-pivot-*, --ge-table-pivot-* – since 1.1.94
|
|
537
|
+
- Calculated columns, „Show values as“, formulas (since 1.1.95, body of object-array / tree / row-group models): ColumnDef.create({ property, valueGetter?: (row, ctx: ValueGetterContextIf { property, getValue(row, p), hasColumn(p), isCalculated(p), aggregate(p, aggregator) }) => unknown, valueSetter?: (row, value) => boolean | void (without it a calculated column is never editable), showValuesAs?: "percentOfColumnTotal" | "percentOfGrandTotal" | "percentOfParentTotal" | "percentOfRowTotal" | "differenceFromPrevious" | "runningTotal" | "rank" | { mode, fractionDigits?, format? }, formula?: "[price] * [quantity] * 1.19", formulaOptions?: { functions } }); one evaluation point (getValueByT) → sorting (memo per pass, errors last), copy, Excel, footerAggregates and RowGroupFactory aggregations/groupBy use computed values; filter: tableApi.getRowValue(row, property); reference of shares = filtered rows, sorting by raw value, Excel: shares as number with format 0.0% (ExcelFormattedValueIf { value, numberFormat }, ExcelService.supportsNumberFormats); pivot: values: [{ value, showValuesAs: "percentOfRowTotal" }] (4 percent modes, sorts by the share), AreaModelPivot.getShownValue(group, info); tableApi.setShowValuesAs(property, mode | undefined) / getShowValuesAs(property); formulas: compileFormula(source, { functions }?) → CompiledFormula { source, ast, error, fields, usesAggregates, evaluate(row, ctx?) } – Pratt parser + closures, no eval / new Function (CSP-safe), fields = own properties only ([constructor] → #NAME?), operators = <> < <= > >= & + - * / ^ (−2^2 = −4), TRUE/FALSE, texts "…"/'…', functions ROUND ABS FLOOR CEILING MOD SQRT POWER IF IFERROR AND OR NOT CONCAT LEN UPPER LOWER TRIM ISBLANK, SUM/AVG/AVERAGE/MIN/MAX/COUNT([field]) = column aggregate over the filtered rows (cached); errors FormulaError { code: "#ERROR!" | "#NAME?" | "#VALUE!" | "#DIV/0!" | "#NUM!" | "#CIRC!", message } (isFormulaError, CSS ge-table-cell-error, --ge-table-cell-error-color, tooltip = message), never thrown; createFormulaValueGetter, FORMULA_FUNCTION_NAMES; tableApi.setColumnFormula(property, source, options?) → FormulaError | undefined; helpers calculateShowValuesAs(values, mode), formatShowValuesAs(value, config), resolveShowValuesAs, isPercentShowValuesAs, sumOfNumbers, SHOW_VALUES_AS_MODES – no cell references (=B2), no formula editor UI
|
|
538
|
+
- Row drag (since 1.1.95): ColumnDef.rowDrag: true | (row, rowIndex) => boolean (drag handle in that column) or TableFactory.createTableModel({ rows, rowDrag: true }) (first data column); Pointer Events (mouse, touch, pen), threshold, ghost (text, never HTML), drop marker, auto-scroll at the body edges (quadratic, computed rows – works with virtual scrolling and capped scroll heights), Escape / pointercancel cancel; tableOptions.rowDragOptions: { managed: true (default; table moves the rows in place via the optional AreaModelIf.moveRows/canMoveRows – AreaModelObjectArray, AreaModelArrayOfArrays, AreaModelTree; blocked while a column is sorted asc/desc or a filter hides rows: handles dimmed, announcement) | false (only events, no blocking), multiRow (selected rows travel together), treeDropInside (tree: upper/lower quarter = sibling, middle = child; no cycles), dragThresholdInPx, autoScrollZoneInPx, autoScrollMaxSpeedInPx, ghostText(rows) }; events EventListenerIf.onRowDragStart/onRowDragMove/onRowDragEnd(GeRowDragEvent { type: "start" | "move" | "end" | "cancel", source: "pointer" | "keyboard", rows, rowIndices, target?: RowDropTargetIf { rowIndex, position: "before" | "after" | "inside" }, toIndex, outside, managed, moved, originalEvent, preventDefault() }), wrappers rowDragStart/rowDragMove/rowDragEnd; keyboard ActionId MOVE_ROW_UP/MOVE_ROW_DOWN (Ctrl+Alt+Up/Down, aria-keyshortcuts on the handle cell, announcement "Row moved to position 7 of 20"); tableApi.moveRows(rowIndices, target | index), isRowDragging(), cancelRowDrag(); helpers moveArrayItems, getInsertBeforeIndex, calcMoveToIndex, moveTreeRows, canMoveTreeRows; row grouping / pivot: managed not possible (use managed: false); CSS ge-table-row-drag-*, ge-table-row-drop-indicator, --ge-table-row-drag-* / --ge-table-row-drop-*
|
|
539
|
+
- Master/detail (since 1.1.97): tableOptions.masterDetail: { detailRenderer: (container, row, ctx: DetailRenderContextIf) => cleanup? | DetailRendererIf, detailHeight: number | "auto" (default, ResizeObserver) | (row, rowIndex) => …, estimatedDetailHeight (200), isMaster(row, rowIndex), toggleColumn (index | property, default: first data column behind row numbers/checkbox), maxRenderedDetails (50, LRU of hidden contents), getRowId } – only flat body models (tree/row groups/pivot: ignored + warning). Detail rows live in one layer div ge-table-detail-layer over the body (full width incl. fixed columns, does not scroll horizontally, never cleared by the virtual rendering); heights are extras of the body RowLayout (prefix sums, also for variable row heights: O(log n) per frame). Content created when first visible, hidden (content-visibility) when scrolled out – state and scroll positions stay –, destroyed on collapse, LRU, refreshDetail, destroy (cleanup fn, ctx.signal aborted, nested tables destroyed). ctx: row, rowIndex, tableApi, recreated, signal, createTable({ tableModel, tableOptions?, eventListener?, height? }) → TableApi of a nested table. Keyed by row object or getRowId (survives sort/filter/setRows). API: setDetailExpanded(rowIndex | row, expanded), toggleDetail, isDetailExpanded, getExpandedDetailRows, expandAllDetails / collapseAllDetails (no event per row), refreshDetail, getDetailElement; getBodyRowTop / getBodyRowIndexAtY on the scope. Events EventListenerIf.onDetailExpandedChanged(GeDetailEvent { row, rowIndex, expanded, source: "mouse" | "keyboard" | "api" | "state" }), wrappers detailExpandedChanged; events from a detail row are ignored by the outer table; mouse wheel over a detail scrolls the outer table if nothing inside scrolls. ARIA: host role treegrid, master rows aria-expanded, detail rows role row + gridcell aria-colspan in an own rowgroup (aria-owns); Enter toggles, Tab enters the detail, Shift+Tab back. State part "details" (detailRowIds, autoRestoreDetailState). Copy/Excel without details. Framework detail components: DetailComponentRendererWrapper (React, Preact, Vue, Svelte 4/5, Solid; props row, rowIndex, tableApi, context), Angular DetailRenderWrapperFactory + DetailComponentRendererIf.setDetail(row, ctx). CSS ge-table-detail-*, --ge-table-detail-bg/-border/-toggle-color/-toggle-hover-bg
|
|
540
|
+
- Right-to-left (since 1.1.98): tableOptions.direction: "ltr" (default) | "rtl" | "auto" (getComputedStyle(host).direction before the table sets dir; read once when built – new options object = rebuild). The host gets dir="rtl"/"ltr" (restored by destroy()). All positions are computed from the start of the row; ConvenienceDomService.setDirection("rtl") writes them as CSS right instead of left → the whole table is mirrored. "left"/"right" in API names mean start/end: fixedLeftColumnCount = start (right in RTL), fixedRightColumnCount = end, NAVIGATE_LEFT = backwards, FillDirection "right" = forwards, ge-table-text-align-left/-right = start/end (new: -start/-end). Positions count from the start: scrollToPixel(x, y), GeScrollEvent.scrollLeft, state scroll.left (≥ 0); browser scrollLeft is 0…negative in RTL → tableScope.getViewportScrollLeft()/setViewportScrollLeft(x), helpers getLogicalScrollLeft, setLogicalScrollLeft, toPhysicalScrollLeft, canScrollHorizontally, toLogicalX, resolveTableDirection, mirrorArrowKeys. Keyboard: ←/→ swapped before the shortcut mapping is looked up. Resize handle at the left edge (dragging left widens). Excel: ExcelExportOptionsIf { rightToLeft } (4th parameter of ExcelService.downloadExcel / 3rd of createExcelBlob), set by tableApi.downloadExcel for RTL tables. CSS on logical properties (padding-inline-start, text-align: start) – pixel-identical in LTR; RTL-only rules .ge-table[dir="rtl"] (expanded tree arrow, detail toggle, pending marker). Nested master/detail tables inherit the direction. api.getDirection(): "ltr" | "rtl"
|
|
541
|
+
- Fill handle and batch edit (since 1.1.96): tableOptions.fillHandle: true | { direction: "both" | "vertical" | "horizontal", fillValue(ctx: FillValueContextIf) => unknown (undefined = default), maxCells (100000), dragThresholdInPx, autoScrollZoneInPx, autoScrollMaxSpeedInPx } – square at the bottom right corner of the last range of a SelectionModel("range"), Pointer Events, preview, auto-scroll, Escape; series rules computeFillValues(sourceValues, count, backward?, mode: "auto" | "alternate" | "copy"): one number copied, several numbers linear (uneven: best-fit line), dates +1 day or +n months (month ends stay; Date stays Date, ISO text stays text), "Item 9" → "Item 10" (leading zeros kept), others repeat; ⌥/Alt (or Ctrl) switches series/copy; computeFillChanges / getFillRange; only editable cells of shown columns (calculated columns need valueSetter), editInputPipe + ColumnDef.validate(value, row) => message; keyboard ActionId FILL_DOWN (Ctrl/⌘+D) / FILL_RIGHT (Ctrl/⌘+R), consumed only with fillHandle and a selected range; tableApi.fillRange(source: CellRange, direction, count, mode?), isFilling(), cancelFill(). Batch edit: tableOptions.batchEdit: true | { blockCommitOnInvalid } (always on) or tableApi.startBatchEdit(); values are shown (class ge-table-cell-pending, invalid ge-table-cell-invalid + aria-invalid + title) but not written; keyed by row object + property (sorting/filtering keep them), back to the original = no change; sorting/filter/totals/copy/Excel use written data; tableApi.getPendingChanges(): CellChangeIf[] { row, rowIndex (-1 = filtered out), columnIndex, property, oldValue, newValue, error? }, hasPendingChanges(), isBatchEditing(), commitBatchEdit(): boolean (false while invalid), cancelBatchEdit(); EventListenerIf.onBatchEditChanged(GeBatchEditEvent { type: "start" | "change" | "commit" | "cancel", changes, pendingCount, invalidCount, pendingChanges }), wrappers batchEditChanged. One write path tableApi.applyCellChanges([{ rowIndex, column: index | property, value }], source?) → CellChangeIf[]; onModelChanged once per edit / fill / commit with GeModelChangeEvent.changes + source ("edit" | "fill" | "commit" | "api"). CSS ge-table-fill-handle, ge-table-fill-preview, --ge-table-fill-*, --ge-table-cell-pending-*, --ge-table-cell-invalid-*. Paste: tableOptions.clipboard, undo: tableOptions.undo (since 1.1.99).
|
|
542
|
+
- Excel-style editing (since 1.1.99): tableOptions.editing: true | { startOnType, enterKey: "edit" | "moveDown", enterMoves, tabMoves, arrowKeysCommit, clearKeys, ctrlEnterFillsSelection, emptyValue, defaultEditable, maxClearCells } – type to overwrite, F2, Enter/Tab move, Escape cancels, Delete clears (source "clear"), Ctrl+Enter fills the selection, Ctrl+arrow to the data edge, Shift+arrow extends; one editor container in the host (CellEditorService), ColumnDef.editor "text" | "number" | "date" | factory, IME. api.startEditing/stopEditing/isEditing/getEditingCell/clearCells, api.subscribe(listener) → unsubscribe, event onEditingChanged (GeEditEvent).
|
|
543
|
+
- Clipboard (since 1.1.99): tableOptions.clipboard: true | { copyHeaders, copyValue: "text" | "raw", html, pasteAddsRows, createRow, maxPasteCells, processCopy, processPaste } – Ctrl+C/X/V via the browser's copy/cut/paste events, TSV + HTML table (Excel quoting), a single value fills the selection, only editable cells, editInputPipe + validate; cut = one operation (sources "paste", "cut"). api.copySelection(), cutSelection(), pasteText(text), getSelectionAsText(), cancelCut(). Input pipes: sheetInputPipe(locale), createNumberInputPipe, createDateInputPipe, booleanInputPipe.
|
|
544
|
+
- Undo/redo (since 1.1.99): tableOptions.undo: true | { maxSteps, maxCells, onConflict: "skip" | "overwrite", recordApiChanges } – Ctrl+Z / Ctrl+Y (Cmd+Shift+Z) for edit, fill, paste, cut, clear, batch edits and structure changes; rows found by object (sorting/filter safe). api.undo(), redo(), canUndo(), canRedo(), clearUndoHistory(), getUndoState(), batchUndoable(label, fn); event onUndoStackChanged (GeUndoEvent: canUndo, canRedo, undoLabel, redoLabel).
|
|
545
|
+
- Cell formulas with A1 references (since 1.1.99): tableOptions.cellFormulas: true | { columns, functions } (ColumnDef.cellFormulas: false = free text) – texts with "=" in the data are formulas ("'=" = text): A1, $A$1, B2:D9, A:A, 70 Excel functions (FORMULA_FUNCTION_INFO; SUM, AVERAGE, IF, IFS, VLOOKUP, XLOOKUP, INDEX/MATCH, SUMIFS, COUNTIFS, TEXT, DATE, EOMONTH …), errors #DIV/0! #REF! #NAME? #VALUE! #N/A #NUM! #CIRC!; dependency graph, only dependents recalculate; own parser, no eval. Formula input: colored references, pointing with arrows/mouse, F4 toggles $, function list (combobox) and hint. Fill/copy shift relative references; Excel export with real formulas: downloadExcel(name, author, { cellFormulas: true }). api.getCellInput/getCellValue/setCellInput/getCellAddress/getCellByAddress/evaluateFormula/getPrecedents/getDependents/recalculate/getSheetEngine. DOM-free: SheetEngine.fromArrays(data), shiftFormula, adjustFormula.
|
|
546
|
+
- Spreadsheet mode (since 1.1.99): SpreadsheetFactory.create({ data: unknown[][], rowCount (100), columnCount (26), columnWidth, rowHeight, rowNumbers, locale, functions, tableOptions }) → { tableModel: SpreadsheetTableModel, tableOptions } – column letters (AreaModelSheetHeader), row numbers, sparse AreaModelSheet; preset tableOptions.spreadsheet: true | { editing, clipboard, undo, cellFormulas, fillHandle } (false = off) for any flat table (TableFactory columnLetters: true). FormulaBar(container, api) – address box + formula input. api.getActiveCell(), goToAddress("B2:C5"), insertRows(at, count), deleteRows, insertColumns, deleteColumns (references adjusted, one undo step, source "structure"), getSheetData() (inputs for JSON).
|
|
547
|
+
- Angular signal component (since 1.1.100, Angular 22 and later): TableSignalComponent, selector guiexpert-table-signal, from @guiexpert/angular-table@22.x – inputs (input()) tableModel (TableModelIf | undefined), tableOptions (default new TableOptions()), loading, debounceMouseClickDelay (150, 0 = synchronous); tableApi = model<TableApi | undefined>() → [(tableApi)]="api" with api = signal<TableApi | undefined>(undefined), (tableApiChange), viewChild(TableSignalComponent)?.tableApi(); set when the table is built, switches directly to the new API on a new model/options, undefined after destroy, written from outside = replaced. Outputs (output()): no tableReady (the API comes as the signal tableApi / tableApiChange), mouseClicked, mouseMoved, mouseDragging, mouseDraggingEnded, contextMenu (camelCase – TableComponent's (contextmenu) also receives the native MouseEvent first), scroll, modelChanged, selectionChanged, focusChanged, checkboxChanged, stateChanged, rowDragStart/Move/End, batchEditChanged, detailExpandedChanged, editingChanged, undoStackChanged – events of the table (mouse, keyboard, scroll …) are emitted outside the Angular zone (write signals in handlers). effect() instead of ngOnChanges, no RxJS, zoneless (provideZonelessChangeDetection()), SSR: no table on the server. TableComponent (<guiexpert-table>) unchanged for Angular 14 – 22+.
|
|
548
|
+
- Live data / log tail (since 1.1.108): tableApi.appendRows(rows, { follow?: "auto" (default: stays at the end if it was at the end, else the first visible row stays) | true (always the end) | false (freeze), maxRows?: ring buffer – oldest rows removed from the start in blocks of ≥ 1 % }) → AppendRowsResultIf { added, shown, removed, following } | false (tree, row groups, pivot, spreadsheet, cell formulas) – AreaModelObjectArray body only, IN PLACE (pushes to your array, no copy), O(k): externalFilterFunction only for the new rows, sorted column / sort(fn) → merged in (binary search, behind equal rows); selection, focus, Shift+arrow anchor and checked rows stay at their rows (RowIndexShift, SelectionModel/FocusModel.adjustForRowChanges – no onSelectionChanged unless a selected row left the buffer); one render pass, none when only rows below the view came in (cells not rebuilt). Held mouse button: "auto" keeps the rows under the pointer and follows again after release. Own AreaModel: append yourself, then tableApi.rowsAppended(count, { follow, removedAtStart }). Not addRows() (copies all rows, resets filter and sort). Showcase logtail, demo apps/table-demo/log-tail.html, LogTailController + createLogTailModelAndOptions in @guiexpert/demo-table-models.
|
|
549
|
+
- Custom filtering
|
|
550
|
+
- In-place cell editing
|
|
551
|
+
- User-defined key and mouse events
|
|
552
|
+
- Customizable look and feel via CSS variables
|
|
553
|
+
- State persistence: column layout (width, order, visibility), sorting, scroll position, expanded tree nodes, checked and selected rows, header groups, custom data – in localStorage (tableOptions.autoRestoreOptions: autoRestoreColumnLayout, autoRestoreSortingState, autoRestoreScrollPosition, autoRestoreCollapsedExpandedState, autoRestoreCheckedState, autoRestoreSelectedState, autoRestoreHeaderGroupState, autoRestoreCustomState; storage: StateStorageIf) or on your server (tableApi.getState(parts?), applyState(state, parts?), resetState(), setCustomState(), tableOptions.initialState, EventListenerIf.onStateChanged(GeStateChangeEvent), `stateChanged` event in all wrappers); columns by property, rows by getRowId – since 1.1.92
|
|
554
|
+
- Customizable cell contents via renderers
|
|
555
|
+
|
|
556
|
+
## Documentation and links
|
|
557
|
+
|
|
558
|
+
- Developer guide (all frameworks, checked against the code): https://github.com/guiexperttable/mono/blob/main/instructions.md
|
|
559
|
+
- Angular guide (14–22, signals): https://github.com/guiexperttable/mono/blob/main/packages/angular-table/instructions.md
|
|
560
|
+
- API reference (TypeDoc): https://gui.expert/api
|
|
561
|
+
- Training (40 chapters, step by step, live demos): https://gui.expert/training/
|
|
562
|
+
- What's new (features since October 2026 with code): https://gui.expert/whats-new/
|
|
563
|
+
- Showcases: https://gui.expert/showcase/ (large, logtail, crypto, treetable, pivot, spreadsheet, masterdetail, rtl …)
|
|
564
|
+
- Demos in the repo: apps/table-demo (plain TS), apps/*-table-demo (frameworks), apps/angular-table-demo
|
|
565
|
+
|
|
566
|
+
## Notes for LLMs
|
|
567
|
+
|
|
568
|
+
- Prefer the recipes above; for anything else read instructions.md (it is compiled against the code) – the code wins.
|
|
569
|
+
- Do not invent APIs (see the last pitfall); if a feature is listed as "not built in" (R17), say so and show the way
|
|
570
|
+
with the existing API.
|
|
571
|
+
- Plain TypeScript works in every framework; the wrappers only add props/events and lifecycle.
|
|
572
|
+
- License: MIT for current releases ("free, also for commercial use"); do not quote prices.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guiexpert/table",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.109",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dr. Marc Kronberg <info@gui.expert> (https://gui.expert)",
|
|
6
6
|
"repository": {
|
|
@@ -71,6 +71,9 @@
|
|
|
71
71
|
"test:unit": "vitest run --project unit",
|
|
72
72
|
"test:browser": "vitest run --project browser --project browser-scrollbars",
|
|
73
73
|
"test:browser:scrollbars": "GE_ALL_SCROLLBARS=1 vitest run --project browser-scrollbars",
|
|
74
|
+
"test:browser:webkit": "GE_WEBKIT=1 vitest run --project browser-webkit",
|
|
75
|
+
"test:perf": "vitest run --config perf/vitest.perf.config.ts",
|
|
76
|
+
"perf:baseline": "node perf/update-baseline.mjs",
|
|
74
77
|
"test:watch": "vitest --project unit",
|
|
75
78
|
"test:coverage": "vitest run --coverage",
|
|
76
79
|
"test:types": "tsc -p tsconfig.spec.json --noEmit",
|