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