@rivinia/table 1.0.0 → 1.0.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/CHANGELOG.md +4 -0
- package/README.md +41 -264
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
- Documentation: README restructured into docs/table/ pages; stale pre-1.0 wording removed.
|
|
6
|
+
|
|
3
7
|
## 1.0.0
|
|
4
8
|
|
|
5
9
|
- First stable release. Row identity must be unique and stable; ambiguous IDs cannot own selection or other identity-dependent state.
|
package/README.md
CHANGED
|
@@ -3,290 +3,67 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@rivinia/table)
|
|
4
4
|
[](https://www.npmjs.com/package/@rivinia/table)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
<img src="https://raw.githubusercontent.com/abdelfattahqandil21-oss/Revinia/table/v1.0.0/assets/rivinia-abstract-mark-light.svg" alt="Rivinia" width="340" />
|
|
8
|
-
</p>
|
|
6
|
+
A headless Angular table controller with typed columns, spans, selection, and optional grid UI.
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
sorting, filtering, selection and paging — plus an optional ready-made grid.
|
|
12
|
-
The core owns the data and the geometry; it never renders a cell, never knows
|
|
13
|
-
what an "action" is, and never imports a stylesheet.
|
|
14
|
-
|
|
15
|
-
- Package version: **1.0.0**
|
|
16
|
-
- Requires: **Angular `^22.0.0`** (`@angular/core` and `@angular/common`).
|
|
17
|
-
- Entry points: `@rivinia/table` (headless controller + pure helpers) and
|
|
18
|
-
`@rivinia/table/ui` (the `rv-table` grid, div-based, `role="grid"`).
|
|
19
|
-
- Side effects: **none** (`sideEffects: false`).
|
|
20
|
-
|
|
21
|
-
## Quick features
|
|
22
|
-
|
|
23
|
-
- **Headless first.** The controller is signals and pure functions — no DOM, no
|
|
24
|
-
browser needed to test the pipeline. The grid is a component you can throw
|
|
25
|
-
away and replace with your own markup.
|
|
26
|
-
- **Real spans, not CSS tricks.** `merge: 'equal'` for consecutive equal values,
|
|
27
|
-
`colSpan: n`, or a per-cell `span(ctx)` for both axes. The result is a resolved
|
|
28
|
-
matrix, so the keyboard, the ARIA and the pinned columns all follow the real
|
|
29
|
-
geometry.
|
|
30
|
-
- **One pipeline.** Filter → sort → page → selection, in that order, every step
|
|
31
|
-
a signal, so a page never recomputes more than it must.
|
|
32
|
-
- **Selection by id, never by index.** It survives sorting, filtering and
|
|
33
|
-
paging, and an action always receives the row it was rendered for.
|
|
34
|
-
`selectAllOnPage()` is page-scoped on purpose.
|
|
35
|
-
- **A shareable view.** `table.state` is the serialisable slice (sort, filters,
|
|
36
|
-
search, page, page size, selection) — put it in the URL and `setState()`
|
|
37
|
-
restores what the user sees.
|
|
38
|
-
- **The actions column is yours.** `display('actions')` reserves the column;
|
|
39
|
-
`<ng-template rvTableCell="actions">` fills it. Five icons, one link, or a
|
|
40
|
-
permission-driven set — the grid passes the row and gets out of the way.
|
|
41
|
-
- **Four bodies, not four screens.** Loading skeletons that keep the header
|
|
42
|
-
widths, an empty state, an error alert and the data, all inside one grid, all
|
|
43
|
-
live regions.
|
|
44
|
-
- **Right-to-left ready.** Every inset is logical, the header mirrors with the
|
|
45
|
-
body, labels ship in English and Arabic, and each value carries `dir="auto"`
|
|
46
|
-
so an English name stays intact in an Arabic table.
|
|
47
|
-
- **Themable end to end.** `--rv-table-*` beats `--rv-*` from `@rivinia/theme`,
|
|
48
|
-
which beats the built-in default; sizes are `rem`, so the grid follows the
|
|
49
|
-
browser's text size.
|
|
50
|
-
- **Zero runtime dependencies.** Angular peers only.
|
|
51
|
-
|
|
52
|
-
## Install
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
npm install @rivinia/table
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
or
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
pnpm add @rivinia/table
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
or
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
yarn add @rivinia/table
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
or
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
bun add @rivinia/table
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Why another table
|
|
77
|
-
|
|
78
|
-
Most Angular tables couple three things that change at different speeds: the
|
|
79
|
-
data pipeline, the DOM, and the styling. This one separates them. The pipeline
|
|
80
|
-
is signals and pure functions, so it is testable without a browser; the grid is
|
|
81
|
-
a component you can replace; and every colour, size and radius is a CSS custom
|
|
82
|
-
property read at the use site.
|
|
83
|
-
|
|
84
|
-
## Quick start
|
|
8
|
+
Package version: **1.0.1**
|
|
85
9
|
|
|
86
10
|
```ts
|
|
11
|
+
import { Component } from '@angular/core';
|
|
87
12
|
import { createTable, tableColumn } from '@rivinia/table';
|
|
88
|
-
|
|
89
|
-
@Component({
|
|
13
|
+
import { RvTable } from '@rivinia/table/ui';
|
|
14
|
+
@Component({ imports: [RvTable], template: '<rv-table aria-label="Invoices" [columns]="table.visibleColumns()" [rows]="table.pageRows()" />' })
|
|
90
15
|
export class InvoicesPage {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// column list that reads a signal re-renders. A bare array is not accepted.
|
|
94
|
-
columns: () => [
|
|
95
|
-
tableColumn<Invoice>().accessor('number', { header: 'Invoice', width: '7rem' }),
|
|
96
|
-
tableColumn<Invoice>().accessor('client', { header: 'Client', flex: 1 }),
|
|
97
|
-
tableColumn<Invoice>().accessor('amount', {
|
|
98
|
-
header: 'Amount',
|
|
99
|
-
align: 'end',
|
|
100
|
-
format: (value) => new Intl.NumberFormat('en-EG').format(value),
|
|
101
|
-
}),
|
|
102
|
-
tableColumn<Invoice>().accessor('status', { header: 'Status' }),
|
|
103
|
-
],
|
|
16
|
+
readonly table = createTable<Invoice>(() => ({
|
|
17
|
+
columns: () => [tableColumn<Invoice>().accessor('number', { header: 'Invoice' })],
|
|
104
18
|
data: () => this.invoices(),
|
|
105
|
-
pageSize: 25,
|
|
106
|
-
selection: { mode: 'multi' },
|
|
107
19
|
}));
|
|
108
|
-
|
|
109
|
-
readonly rows = this.table.pageRows;
|
|
110
|
-
readonly columns = this.table.visibleColumns;
|
|
111
20
|
}
|
|
112
21
|
```
|
|
113
22
|
|
|
114
|
-
|
|
115
|
-
from signals and computeds, so it is either a field initialiser or inside
|
|
116
|
-
`inject()`-capable code.
|
|
117
|
-
|
|
118
|
-
## Columns
|
|
119
|
-
|
|
120
|
-
A column is data, not a component. `tableColumn<T>()` has two builders:
|
|
121
|
-
`accessor(key, def?)` reads a field of the row, `display(id, def?)` has no field
|
|
122
|
-
and exists for a template or an actions column.
|
|
123
|
-
|
|
124
|
-
| Field | Meaning |
|
|
125
|
-
| -------------- | ------------------------------------------------------------------- |
|
|
126
|
-
| `header` | Visible header text. Omit for an icon-only column. |
|
|
127
|
-
| `width` | A CSS length. Fixed columns take part in `table-layout: fixed`. |
|
|
128
|
-
| `flex` | `> 0` absorbs free space; `0` stays fixed. |
|
|
129
|
-
| `align` | `'start' \| 'center' \| 'end'` — logical, so RTL needs no override. |
|
|
130
|
-
| `sortable` | Adds the three-state sort affordance and `aria-sort`. |
|
|
131
|
-
| `format` | `(value, row, locale) => string` for display only. |
|
|
132
|
-
| `comparator` | Sort with something other than the default compare. |
|
|
133
|
-
| `pinned` | `'start' \| 'end'` — stays visible during horizontal scroll. |
|
|
134
|
-
| `textOverflow` | `'nowrap'` (default), `'wrap'`, `'clamp'`. |
|
|
135
|
-
| `lines` | Clamp height, default 2. |
|
|
136
|
-
| `hidden` | Removed from the visible set without losing its definition. |
|
|
137
|
-
|
|
138
|
-
## Row and column spans
|
|
139
|
-
|
|
140
|
-
Spans are part of the core, not an add-on: pinning, keyboard navigation and
|
|
141
|
-
selection all need to know the grid's real geometry. Three ways to declare one,
|
|
142
|
-
in increasing power:
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
// 1. Row span by a shared key: consecutive rows with an equal value merge.
|
|
146
|
-
tableColumn<Invoice>().accessor('department', { header: 'Department', merge: 'equal' });
|
|
147
|
-
|
|
148
|
-
// 2. Column span: this cell covers N columns to its end.
|
|
149
|
-
tableColumn<Invoice>().accessor('note', { header: 'Notes', colSpan: 2 });
|
|
150
|
-
|
|
151
|
-
// 3. Both axes, computed per cell.
|
|
152
|
-
tableColumn<Invoice>().display('summary', {
|
|
153
|
-
span: ({ rowIndex }) => (rowIndex === 0 ? { colSpan: 5 } : null),
|
|
154
|
-
});
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Rules, enforced at `createTable()` rather than at render time:
|
|
158
|
-
|
|
159
|
-
- `span` cannot be combined with `merge` or `colSpan` — the call throws.
|
|
160
|
-
- `merge` without an accessor warns and does not merge (there is nothing to
|
|
161
|
-
compare).
|
|
162
|
-
- `merge` and `colSpan` compose into one rectangle.
|
|
163
|
-
- Overlapping spans are shrunk so the anchor wins, and the overlap is reported.
|
|
164
|
-
|
|
165
|
-
`table.spans` is a resolved `SpanModel` with `at(row, column)`,
|
|
166
|
-
`owner(row, column)` and `move(row, column, dRow, dColumn)`. Every query is
|
|
167
|
-
O(1) against a dense matrix built once per `(rows, columns)` pair, so a
|
|
168
|
-
10,000-row page does not re-walk your callbacks on scroll.
|
|
169
|
-
|
|
170
|
-
## Selection
|
|
171
|
-
|
|
172
|
-
Ids, not array positions — so a selection survives sorting, filtering and
|
|
173
|
-
paging, and an action always receives the row it was rendered for. Turn it on
|
|
174
|
-
in the options (`'single'` or `'multi'`); a table without it renders none of the
|
|
175
|
-
affordance and every method is a no-op:
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
createTable<Invoice>(() => ({
|
|
179
|
-
columns: () => this.columns,
|
|
180
|
-
data: () => this.invoices(),
|
|
181
|
-
selection: { mode: 'multi' },
|
|
182
|
-
}));
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
```ts
|
|
186
|
-
table.selection.toggle(row.id);
|
|
187
|
-
table.selection.isSelected(row.id);
|
|
188
|
-
table.selection.selectAllOnPage(); // page scope, never "everything by accident"
|
|
189
|
-
table.selection.clear();
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### Row identity contract
|
|
23
|
+
## Why @rivinia/table
|
|
193
24
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
throw an error naming the ID and source row indices; invalid values name their
|
|
198
|
-
source index. In production, rows with ambiguous IDs remain visible, but
|
|
199
|
-
selection and other identity-dependent actions are disabled for those rows.
|
|
200
|
-
The table never substitutes an array index for an invalid ID. If rows have no
|
|
201
|
-
stable `id`, provide a `getRowId` backed by a persistent domain key.
|
|
25
|
+
- The signal pipeline is headless and testable without rendering a grid.
|
|
26
|
+
- Typed columns, spans, paging, filters, sorting, and selection share one model.
|
|
27
|
+
- The optional grid renders the model and can be replaced with custom markup.
|
|
202
28
|
|
|
203
|
-
##
|
|
204
|
-
|
|
205
|
-
`page`, `pageSize`, `sorting`, `filters` and `globalSearch` are writable
|
|
206
|
-
signals; `rows` (filtered and sorted), `pageRows` (sliced) and `total` are
|
|
207
|
-
read-only computeds. `table.state` is the serialisable slice — put it in the URL
|
|
208
|
-
and the user can share what they see; `setState()` and `resetState()` restore it.
|
|
209
|
-
|
|
210
|
-
## UI entry point
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
import { RvTable } from '@rivinia/table/ui';
|
|
214
|
-
```
|
|
29
|
+
## Install
|
|
215
30
|
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
aria-label="Invoices"
|
|
219
|
-
[columns]="columns()"
|
|
220
|
-
[rows]="rows()"
|
|
221
|
-
[spans]="table.spans()"
|
|
222
|
-
[totalRows]="table.total()"
|
|
223
|
-
[(sort)]="sort"
|
|
224
|
-
[(selected)]="selected"
|
|
225
|
-
[skeletonRows]="'auto'"
|
|
226
|
-
[loading]="loading()"
|
|
227
|
-
>
|
|
228
|
-
<!-- The actions column is YOUR template. Five buttons, one button, or a
|
|
229
|
-
link — the table only reserves the column and hands you the row. -->
|
|
230
|
-
<ng-template rvTableCell="actions" let-row>
|
|
231
|
-
<button type="button" (click)="approve(row)">Approve</button>
|
|
232
|
-
</ng-template>
|
|
233
|
-
</rv-table>
|
|
31
|
+
```bash
|
|
32
|
+
npm install @rivinia/table
|
|
234
33
|
```
|
|
235
34
|
|
|
236
|
-
|
|
237
|
-
its column widths with the body, one roving `tabindex`, and cells that respect
|
|
238
|
-
`span.move()` for keyboard navigation. It ships no icons and no action
|
|
239
|
-
semantics.
|
|
240
|
-
|
|
241
|
-
Two details that are easy to miss:
|
|
242
|
-
|
|
243
|
-
- `aria-label` written on `<rv-table>` becomes the grid's accessible name; the
|
|
244
|
-
`[ariaLabel]` input is the reactive equivalent and wins over it.
|
|
245
|
-
- `[totalRows]` is what `aria-rowcount` reports when the page is smaller than
|
|
246
|
-
the data set. Without it the grid can only count what it renders, which on a
|
|
247
|
-
paged table is the page, not the table.
|
|
248
|
-
- A cell can be narrower than its content. The grid does not clip a cell by
|
|
249
|
-
default, because a floating menu inside a cell has to be able to leave it —
|
|
250
|
-
so an actions cell that does not fit paints over its neighbour. Size the
|
|
251
|
-
column (`width`) or let the content wrap; if your cells never float
|
|
252
|
-
anything, `overflow: clip` on `.rv-table-cell` is the strict fix.
|
|
253
|
-
|
|
254
|
-
## Theming
|
|
255
|
-
|
|
256
|
-
Every visual value is a token read where it is used, so a value set on an
|
|
257
|
-
ancestor wins:
|
|
35
|
+
## Quick start
|
|
258
36
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
<built-in default> last
|
|
263
|
-
```
|
|
37
|
+
1. Install the package and import `createTable` and `tableColumn`.
|
|
38
|
+
2. Create the controller in an Angular injection context.
|
|
39
|
+
3. Render `<rv-table>` with its columns and rows.
|
|
264
40
|
|
|
265
|
-
|
|
266
|
-
`1px` and focus rings `2px`.
|
|
41
|
+
## Two ways to use it
|
|
267
42
|
|
|
268
|
-
|
|
43
|
+
| Ready-made UI | Headless controller |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `@rivinia/table/ui` | `@rivinia/table` |
|
|
269
46
|
|
|
270
|
-
|
|
271
|
-
on headers and `aria-selected` on rows.
|
|
272
|
-
- With `selectable`, each row draws a **decorative** box in its first cell and
|
|
273
|
-
the state lives in `aria-selected` — one announcement per row, not two, and
|
|
274
|
-
no second tab stop inside the cell. Clicking the row or pressing Space
|
|
275
|
-
toggles it.
|
|
276
|
-
- Merged cells are one cell: a screen reader hears the value once, not once per
|
|
277
|
-
covered position.
|
|
278
|
-
- Truncated text is available to assistive technology through
|
|
279
|
-
`aria-describedby`; a visual tooltip is the consumer's choice.
|
|
280
|
-
- Loading, empty and error states are live regions (`role="status"`), never
|
|
281
|
-
silent.
|
|
47
|
+
The UI and controller are independent; use either or combine them.
|
|
282
48
|
|
|
283
|
-
##
|
|
49
|
+
## Compatibility
|
|
284
50
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
version.
|
|
51
|
+
| Angular | Node | Browser verification |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `^22.0.0` | `^22.22.3 or ^24.15.0 or >=26.0.0` | Chromium and Firefox; native Safari not verified |
|
|
289
54
|
|
|
290
|
-
##
|
|
55
|
+
## Go further
|
|
291
56
|
|
|
292
|
-
|
|
57
|
+
| Topic | Read |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| Overview | [Overview](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/overview.md) |
|
|
60
|
+
| Install | [Install](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/installation.md) |
|
|
61
|
+
| Quick start | [Quick start](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/quick-start.md) |
|
|
62
|
+
| Columns | [Columns](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/columns.md) |
|
|
63
|
+
| Row and column spans | [Row and column spans](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/spans.md) |
|
|
64
|
+
| Selection | [Selection](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/selection.md) |
|
|
65
|
+
| Paging and state | [Paging and state](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/paging-and-state.md) |
|
|
66
|
+
| Ready-made UI | [Ready-made UI](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/ready-made-ui.md) |
|
|
67
|
+
| Theming | [Theming](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/theming.md) |
|
|
68
|
+
| Accessibility | [Accessibility](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/accessibility.md) |
|
|
69
|
+
| Scope and limits | [Scope and limits](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/scope-and-limits.md) |
|