tabler-react-2 0.1.153 → 0.1.154

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.
@@ -2,6 +2,10 @@
2
2
  title: Changelog
3
3
  ---
4
4
 
5
+ # 0.1.153
6
+
7
+ - Support className and arbitrary props on the `Checkbox` component.
8
+
5
9
  # 0.1.152
6
10
 
7
11
  - Fix rendering of `invalid` prop on `Input` components with `prependedText` or `appendedText`.
@@ -0,0 +1,367 @@
1
+ ---
2
+ title: Tables v2
3
+ ---
4
+
5
+ import { Excerpt } from "../../components/Excerpt.jsx";
6
+ import { TableV2 } from "../../components/LoadableTabler.jsx";
7
+ import congressPeople from "../../data/congressPeople.json";
8
+
9
+ Tables v2 is a new data table built on TanStack Table v8 with Tabler styling. It is fully controlled: you pass the current page of `data`, the total row count, and the active `sorting`. This design fits server-side pagination and ordering out of the box.
10
+
11
+ > Note: live previews on this page may require a package version that exports `TableV2`. If you don’t see live examples, the code snippets still illustrate usage.
12
+
13
+ ## Signature
14
+
15
+ ```jsx
16
+ import { TableV2 } from "tabler-react-2";
17
+
18
+ <TableV2 {...props} />;
19
+ ```
20
+
21
+ ### Core props
22
+
23
+ | Prop | Required | Type | Description |
24
+ | ---------------------- | -------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
25
+ | `columns` | Yes | TanStack `ColumnDef[]` | Column definitions using `accessorKey`, `header`, and optional `cell` renderers. |
26
+ | `data` | Yes | `any[]` | Current page rows only. |
27
+ | `totalRows` | Yes | `number` | Total available row count (for page count and range text). |
28
+ | `page` | Yes | `number` | Current page (1-based). |
29
+ | `size` | Yes | `number` | Current page size. |
30
+ | `onPageChange` | Yes | `(page:number) => void` | Called with next 1-based page. |
31
+ | `onSizeChange` | Yes | `(size:number) => void` | Called with next page size. Typically also reset page to 1. |
32
+ | `sorting` | Yes | `{ id:string, desc?:boolean }[]` | Active ordering (TanStack format). Empty array for “no sort”. |
33
+ | `onSortingChange` | Yes | `(next) => void` | Called when the user clicks a sortable header. |
34
+ | `loading` | No | `boolean` | Shows a small spinner and disables pager while loading. |
35
+ | `headerSticky` | No | `boolean` | Makes the header stick to the top of the card. |
36
+ | `emptyState` | No | `string \| () => ReactNode` | What to render when there are no rows. |
37
+ | `getRowId` | No | `(row) => string` | Supply when your rows need a stable custom ID. |
38
+ | `rowSelection` | No | `Record<string, boolean>` | Controlled selection map. |
39
+ | `onRowSelectionChange` | No | `(updater) => void` | Controlled selection callback. |
40
+ | `renderToolbarLeft` | No | `({ page,size,totalRows,sorting }) => ReactNode` | Custom left content in the card header. |
41
+ | `renderToolbarRight` | No | same | Custom right content in the card header. |
42
+
43
+ ## Basic usage (client or server data)
44
+
45
+ The table is controlled; keep `page`, `size`, and `sorting` in your component state. The example below uses in-memory data, but the same shape works with server APIs.
46
+
47
+ ```jsx
48
+ import { useMemo, useState } from "react";
49
+ import { TableV2 } from "tabler-react-2";
50
+
51
+ const allRows = congressPeople; // e.g., from your API
52
+
53
+ export default function Demo() {
54
+ const [page, setPage] = useState(1);
55
+ const [size, setSize] = useState(5);
56
+ const [sorting, setSorting] = useState([]); // [{ id:'name', desc:false }]
57
+
58
+ const columns = useMemo(
59
+ () => [
60
+ { accessorKey: "name", header: "Name" },
61
+ { accessorKey: "party", header: "Party" },
62
+ { accessorKey: "region.state", header: "State" },
63
+ {
64
+ accessorKey: "email",
65
+ header: "Email",
66
+ cell: ({ getValue }) => (
67
+ <a className="text-reset" href={`mailto:${getValue()}`}>
68
+ {getValue()}
69
+ </a>
70
+ ),
71
+ },
72
+ ],
73
+ []
74
+ );
75
+
76
+ // client-side slice just for the demo
77
+ const ordered = useMemo(() => {
78
+ if (!sorting.length) return allRows;
79
+ const { id, desc } = sorting[0];
80
+ const val = (row) => id.split(".").reduce((a, k) => a?.[k], row);
81
+ const cmp = (a, b) => (a === b ? 0 : a > b ? 1 : -1);
82
+ const sorted = [...allRows].sort((a, b) => cmp(val(a), val(b)));
83
+ return desc ? sorted.reverse() : sorted;
84
+ }, [sorting]);
85
+
86
+ const start = (page - 1) * size;
87
+ const pageData = ordered.slice(start, start + size);
88
+
89
+ return (
90
+ <TableV2
91
+ columns={columns}
92
+ data={pageData}
93
+ totalRows={allRows.length}
94
+ page={page}
95
+ size={size}
96
+ onPageChange={setPage}
97
+ onSizeChange={(n) => {
98
+ setPage(1);
99
+ setSize(n);
100
+ }}
101
+ sorting={sorting}
102
+ onSortingChange={(next) => {
103
+ setPage(1);
104
+ setSorting(next);
105
+ }}
106
+ />
107
+ );
108
+ }
109
+ ```
110
+
111
+ <Excerpt>
112
+ {(() => {
113
+ const React = require("react");
114
+ const { useMemo, useState } = React;
115
+
116
+ // Local, fully functional demo state (client-side slice)
117
+ const allRows = congressPeople;
118
+ const [page, setPage] = useState(1);
119
+ const [size, setSize] = useState(10);
120
+ const [sorting, setSorting] = useState([]); // [{ id:'name', desc:false }]
121
+
122
+ const ordered = useMemo(() => {
123
+ if (!sorting.length) return allRows;
124
+ const { id, desc } = sorting[0];
125
+ const val = (row) => id.split(".").reduce((a, k) => a?.[k], row);
126
+ const cmp = (a, b) => (a === b ? 0 : a > b ? 1 : -1);
127
+ const sorted = [...allRows].sort((a, b) => cmp(val(a), val(b)));
128
+ return desc ? sorted.reverse() : sorted;
129
+ }, [sorting]);
130
+
131
+ const start = (page - 1) * size;
132
+ const pageData = ordered.slice(start, start + size);
133
+
134
+ return (
135
+ <TableV2
136
+ columns={[
137
+ { accessorKey: "name", header: "Name" },
138
+ { accessorKey: "party", header: "Party" },
139
+ { accessorKey: "region.state", header: "State" },
140
+ {
141
+ accessorKey: "email",
142
+ header: "Email",
143
+ cell: ({ getValue }) => (
144
+ <a className="text-reset" href={`mailto:${getValue()}`}>
145
+ {getValue()}
146
+ </a>
147
+ ),
148
+ },
149
+ ]}
150
+ data={pageData}
151
+ totalRows={allRows.length}
152
+ page={page}
153
+ size={size}
154
+ onPageChange={setPage}
155
+ onSizeChange={(n) => {
156
+ setPage(1);
157
+ setSize(n);
158
+ }}
159
+ sorting={sorting}
160
+ onSortingChange={(next) => {
161
+ setPage(1);
162
+ setSorting(next);
163
+ }}
164
+ loading={false}
165
+ />
166
+ );
167
+ })()}
168
+
169
+ </Excerpt>
170
+
171
+ ## Server-side workflow
172
+
173
+ When fetching from an API, keep the same controlled state, but fetch rows for the current `page`, `size`, and `sorting`.
174
+
175
+ ```jsx
176
+ const [page, setPage] = useState(1);
177
+ const [size, setSize] = useState(25);
178
+ const [sorting, setSorting] = useState([]);
179
+ const [rows, setRows] = useState([]);
180
+ const [total, setTotal] = useState(0);
181
+ const [loading, setLoading] = useState(false);
182
+
183
+ useEffect(() => {
184
+ let isMounted = true;
185
+ (async () => {
186
+ setLoading(true);
187
+ try {
188
+ const res = await fetchPeople({ page, size, sorting });
189
+ if (!isMounted) return;
190
+ setRows(res.items);
191
+ setTotal(res.total);
192
+ } finally {
193
+ setLoading(false);
194
+ }
195
+ })();
196
+ return () => {
197
+ isMounted = false;
198
+ };
199
+ }, [page, size, sorting]);
200
+
201
+ <TableV2
202
+ columns={columns}
203
+ data={rows}
204
+ totalRows={total}
205
+ page={page}
206
+ size={size}
207
+ onPageChange={setPage}
208
+ onSizeChange={(n) => {
209
+ setPage(1);
210
+ setSize(n);
211
+ }}
212
+ sorting={sorting}
213
+ onSortingChange={(next) => {
214
+ setPage(1);
215
+ setSorting(next);
216
+ }}
217
+ loading={loading}
218
+ />;
219
+ ```
220
+
221
+ ### Live async demo (simulated API)
222
+
223
+ The preview below simulates a server by sorting and slicing in a faux `fetch` with a 1s delay.
224
+
225
+ <Excerpt>
226
+ {(() => {
227
+ const React = require("react");
228
+ const { useEffect, useMemo, useState } = React;
229
+
230
+ // Simulated API that delays 1s and then returns sorted, paged rows
231
+ const fetchPeople = ({ page, size, sorting }) =>
232
+ new Promise((resolve) => {
233
+ setTimeout(() => {
234
+ const all = congressPeople;
235
+ const total = all.length;
236
+
237
+ // server-side order
238
+ let ordered = all;
239
+ if (sorting?.length) {
240
+ const { id, desc } = sorting[0];
241
+ const val = (row) => id.split(".").reduce((a, k) => a?.[k], row);
242
+ const cmp = (a, b) => (a === b ? 0 : a > b ? 1 : -1);
243
+ const sorted = [...all].sort((a, b) => cmp(val(a), val(b)));
244
+ ordered = desc ? sorted.reverse() : sorted;
245
+ }
246
+
247
+ // server-side page
248
+ const start = (page - 1) * size;
249
+ const items = ordered.slice(start, start + size);
250
+ resolve({ items, total });
251
+ }, 1000);
252
+ });
253
+
254
+ const columns = useMemo(
255
+ () => [
256
+ { accessorKey: "name", header: "Name" },
257
+ { accessorKey: "party", header: "Party" },
258
+ { accessorKey: "region.state", header: "State" },
259
+ {
260
+ accessorKey: "email",
261
+ header: "Email",
262
+ cell: ({ getValue }) => (
263
+ <a className="text-reset" href={`mailto:${getValue()}`}>
264
+ {getValue()}
265
+ </a>
266
+ ),
267
+ },
268
+ ],
269
+ []
270
+ );
271
+
272
+ const [page, setPage] = useState(1);
273
+ const [size, setSize] = useState(10);
274
+ const [sorting, setSorting] = useState([]);
275
+ const [rows, setRows] = useState([]);
276
+ const [total, setTotal] = useState(0);
277
+ const [loading, setLoading] = useState(false);
278
+
279
+ useEffect(() => {
280
+ let isMounted = true;
281
+ setLoading(true);
282
+ fetchPeople({ page, size, sorting }).then((res) => {
283
+ if (!isMounted) return;
284
+ setRows(res.items);
285
+ setTotal(res.total);
286
+ setLoading(false);
287
+ });
288
+ return () => {
289
+ isMounted = false;
290
+ };
291
+ }, [page, size, sorting]);
292
+
293
+ return (
294
+ <TableV2
295
+ columns={columns}
296
+ data={rows}
297
+ totalRows={total}
298
+ page={page}
299
+ size={size}
300
+ onPageChange={setPage}
301
+ onSizeChange={(n) => {
302
+ setPage(1);
303
+ setSize(n);
304
+ }}
305
+ sorting={sorting}
306
+ onSortingChange={(next) => {
307
+ setPage(1);
308
+ setSorting(next);
309
+ }}
310
+ loading={loading}
311
+ />
312
+ );
313
+ })()}
314
+
315
+ </Excerpt>
316
+
317
+ ## Extras
318
+
319
+ - Sticky header: pass `headerSticky` to keep headers visible while scrolling.
320
+ - Toolbars: use `renderToolbarLeft`/`renderToolbarRight` to add filters or actions.
321
+ - Row selection: control with `rowSelection` and `onRowSelectionChange`.
322
+
323
+ ### Sticky header example
324
+
325
+ ```jsx
326
+ <TableV2 {...commonProps} headerSticky />
327
+ ```
328
+
329
+ ### Toolbar example
330
+
331
+ ```jsx
332
+ <TableV2
333
+ {...commonProps}
334
+ renderToolbarLeft={() => (
335
+ <input className="form-control form-control-sm" placeholder="Search" />
336
+ )}
337
+ renderToolbarRight={() => (
338
+ <button className="btn btn-sm btn-primary">Add</button>
339
+ )}
340
+ />
341
+ ```
342
+
343
+ ### Row selection (minimal)
344
+
345
+ ```jsx
346
+ const [rowSelection, setRowSelection] = useState({});
347
+
348
+ <TableV2
349
+ {...commonProps}
350
+ rowSelection={rowSelection}
351
+ onRowSelectionChange={setRowSelection}
352
+ columns={[
353
+ {
354
+ id: "select",
355
+ header: () => null,
356
+ cell: ({ row }) => (
357
+ <input
358
+ type="checkbox"
359
+ checked={row.getIsSelected()}
360
+ onChange={row.getToggleSelectedHandler()}
361
+ />
362
+ ),
363
+ },
364
+ ...columns,
365
+ ]}
366
+ />;
367
+ ```
@@ -454,3 +454,231 @@ const customSortFn = (a, b) => {
454
454
  ```
455
455
 
456
456
  > **Note**: Sorting only works for the data in the table. If, for example, you have thousands of rows and are only rendering a few, you will need to handle the sorting yourself, outside of the table. Incorporating internal pagination that supports sorting is a planned feature but not yet implemented.
457
+
458
+ ### External ordering
459
+
460
+ You can control ordering from the parent (e.g., server-side sort) and still use the table’s sortable headers. Provide the props below. When controlled, the table will not sort your `data`; it assumes you already passed rows in the desired order.
461
+
462
+ - orderBy: accessor of the active column
463
+ - order: either `"asc"` or `"desc"`
464
+ - onSetOrder: called with `(orderBy, order)` when the user clicks a header
465
+
466
+ <Excerpt>
467
+ {(() => {
468
+ const React = require("react");
469
+ const [page, setPage] = React.useState(1);
470
+ const [size, setSize] = React.useState(5);
471
+ const [orderBy, setOrderBy] = React.useState("name");
472
+ const [order, setOrder] = React.useState("asc");
473
+ const total = congressPeople.length;
474
+
475
+ const sorted = [...congressPeople].sort((a, b) => {
476
+ const av = orderBy.split('.').reduce((acc, k) => acc?.[k], a);
477
+ const bv = orderBy.split('.').reduce((acc, k) => acc?.[k], b);
478
+ if (av === bv) return 0;
479
+ const cmp = av > bv ? 1 : -1;
480
+ return order === 'asc' ? cmp : -cmp;
481
+ });
482
+ const start = (page - 1) * size;
483
+ const pageData = sorted.slice(start, start + size);
484
+
485
+ return (
486
+ <Table
487
+ columns={[
488
+ { label: "Name", accessor: "name", sortable: true },
489
+ { label: "Party", accessor: "party", sortable: true },
490
+ { label: "State", accessor: "region.state", sortable: true },
491
+ ]}
492
+ data={pageData}
493
+ showPagination
494
+ page={page}
495
+ size={size}
496
+ totalRows={total}
497
+ orderBy={orderBy}
498
+ order={order}
499
+ onSetOrder={(by, dir) => { setOrderBy(by); setOrder(dir); }}
500
+ onSetPage={setPage}
501
+ onSetSize={(n) => { setPage(1); setSize(n); }}
502
+ />
503
+ );
504
+ })()}
505
+ </Excerpt>
506
+
507
+ ```jsx
508
+ // Parent controls ordering and pagination
509
+ const [orderBy, setOrderBy] = useState('name');
510
+ const [order, setOrder] = useState('asc');
511
+ const [page, setPage] = useState(1);
512
+ const [size, setSize] = useState(10);
513
+
514
+ const ordered = [...allRows].sort((a, b) => {
515
+ const av = orderBy.split('.').reduce((acc, k) => acc?.[k], a);
516
+ const bv = orderBy.split('.').reduce((acc, k) => acc?.[k], b);
517
+ if (av === bv) return 0;
518
+ const cmp = av > bv ? 1 : -1;
519
+ return order === 'asc' ? cmp : -cmp;
520
+ });
521
+
522
+ const start = (page - 1) * size;
523
+ const currentData = ordered.slice(start, start + size);
524
+
525
+ <Table
526
+ columns={columns}
527
+ data={currentData}
528
+ showPagination
529
+ page={page}
530
+ size={size}
531
+ totalRows={allRows.length}
532
+ orderBy={orderBy}
533
+ order={order}
534
+ onSetOrder={(by, dir) => { setOrderBy(by); setOrder(dir); }}
535
+ onSetPage={setPage}
536
+ onSetSize={(n) => { setPage(1); setSize(n); }}
537
+ />;
538
+ ```
539
+
540
+ ## Pagination
541
+
542
+ The `Table` supports built-in pagination UI. Enable it with `showPagination`.
543
+
544
+ - showPagination: toggles the pagination controls
545
+ - defaultRowsPerPage: initial rows per page for internal mode
546
+ - pageSizeOptions: selectable page sizes (default `[10, 25, 50, 100]`)
547
+
548
+ <Excerpt>
549
+ <Table
550
+ columns={[
551
+ { label: "Name", accessor: "name" },
552
+ { label: "Party", accessor: "party" },
553
+ { label: "State", accessor: "region.state" },
554
+ ]}
555
+ data={congressPeople}
556
+ showPagination
557
+ defaultRowsPerPage={10}
558
+ />
559
+ </Excerpt>
560
+
561
+ ```jsx
562
+ <Table
563
+ columns={columns}
564
+ data={rows}
565
+ showPagination
566
+ defaultRowsPerPage={10}
567
+ pageSizeOptions={[10, 25, 50, 100]}
568
+ />
569
+ ```
570
+
571
+ ### External pagination
572
+
573
+ You can also fully control pagination from the parent (e.g., server-side paging) while keeping the same UI. Provide the controlled props and callbacks below. When any controlled pagination prop is provided, the table does not slice your `data` and instead assumes `data` already contains the current page.
574
+
575
+ - page: current page (1-based)
576
+ - size: current page size
577
+ - totalRows: total available row count used for page count and the “Showing … of …” text
578
+ - onSetPage: called with next page (1-based)
579
+ - onSetSize: called with next page size
580
+
581
+ Aliases are supported for compatibility: `rowsPerPage`, `onPageChange`, `onRowsPerPageChange`.
582
+
583
+ <Excerpt>
584
+ {(() => {
585
+ // Simulated external pagination
586
+ const React = require("react");
587
+ const [page, setPage] = React.useState(1);
588
+ const [rpp, setRpp] = React.useState(4);
589
+ const total = congressPeople.length;
590
+ const start = (page - 1) * rpp;
591
+ const pageData = congressPeople.slice(start, start + rpp);
592
+ return (
593
+ <Table
594
+ columns={[
595
+ { label: "Name", accessor: "name" },
596
+ { label: "Party", accessor: "party" },
597
+ { label: "State", accessor: "region.state" },
598
+ ]}
599
+ data={pageData}
600
+ showPagination
601
+ page={page}
602
+ size={rpp}
603
+ totalRows={total}
604
+ onSetPage={setPage}
605
+ onSetSize={(n) => {
606
+ setPage(1);
607
+ setRpp(n);
608
+ }}
609
+ />
610
+ );
611
+ })()}
612
+ </Excerpt>
613
+
614
+ ```jsx
615
+ // Parent controls the table
616
+ const [page, setPage] = useState(1);
617
+ const [rowsPerPage, setRowsPerPage] = useState(10);
618
+ const totalRows = allRows.length; // from your API
619
+
620
+ // Only pass the current slice to the table
621
+ const start = (page - 1) * rowsPerPage;
622
+ const currentData = allRows.slice(start, start + rowsPerPage);
623
+
624
+ <Table
625
+ columns={columns}
626
+ data={currentData}
627
+ showPagination
628
+ page={page}
629
+ size={rowsPerPage}
630
+ totalRows={totalRows}
631
+ onSetPage={setPage}
632
+ onSetSize={(n) => { setPage(1); setRowsPerPage(n); }}
633
+ />;
634
+ ```
635
+
636
+ If you don’t supply these controlled props, the table falls back to its internal pagination state, preserving backwards compatibility.
637
+
638
+ ## useTable hook
639
+
640
+ For async workflows, use the helper hook to centralize loading, pagination, count, and ordering while preserving the Table’s controlled API.
641
+
642
+ ```jsx
643
+ import { useEffect, useState } from 'react';
644
+ import { Table, useTable } from 'tabler-react-2';
645
+
646
+ function PeopleTable() {
647
+ const [rows, setRows] = useState([]);
648
+ const { tableProps, setLoading, setPage, setCount, setOrder } = useTable({
649
+ autoLoading: false,
650
+ onPageChange: async (page, size, sort) => {
651
+ setLoading(true);
652
+ try {
653
+ // fetch your data here using page, size, and sort
654
+ const res = await fetchPeople({ page, size, sort });
655
+ setRows(res.items);
656
+ setCount(res.total);
657
+ } finally {
658
+ setLoading(false);
659
+ }
660
+ },
661
+ onSortChange: async (by, dir) => {
662
+ // optionally kick off a fetch based on new sort
663
+ // setOrder(by, dir) is already called for you
664
+ },
665
+ });
666
+
667
+ useEffect(() => {
668
+ // initial load
669
+ setPage(1);
670
+ }, [setPage]);
671
+
672
+ return <Table {...tableProps} columns={[
673
+ { label: 'Name', accessor: 'name', sortable: true },
674
+ { label: 'Party', accessor: 'party', sortable: true },
675
+ { label: 'State', accessor: 'region.state', sortable: true },
676
+ ]} data={rows} />;
677
+ }
678
+ ```
679
+
680
+ The hook returns:
681
+
682
+ - tableProps: spread onto `<Table />` to enable controlled pagination, ordering, and loading
683
+ - setLoading, setPage, setCount: imperative helpers for async flows
684
+ - Also available: page, size, count, orderBy, order, setSize, setOrder
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "tabler-react-2",
3
- "version": "0.1.153",
3
+ "version": "0.1.154",
4
4
  "description": "A react implementation of Tabler ui",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
7
7
  "test": "cd demo && yarn start",
8
8
  "build:css": "postcss src/**/*.css --dir dist",
9
- "build": "babel src --out-dir dist && npm run build:css"
9
+ "build": "babel src --out-dir dist && npm run build:css",
10
+ "docs:vanilla": "yarn --cwd docs start",
11
+ "docs": "cpulimit -l 20 -- yarn --cwd docs start"
10
12
  },
11
13
  "author": "Jack Crane",
12
14
  "license": "MIT",
@@ -15,6 +17,8 @@
15
17
  "@babel/core": "^7.24.8",
16
18
  "@babel/preset-env": "^7.24.8",
17
19
  "@babel/preset-react": "^7.24.7",
20
+ "@emotion/react": "^11.14.0",
21
+ "@tanstack/react-table": "^8.21.3",
18
22
  "classnames": "^2.5.1",
19
23
  "prop-types": "^15.8.1",
20
24
  "react": "^18.3.1",