mvc-kit 2.12.0 → 2.12.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/agent-config/bin/postinstall.mjs +5 -3
- package/agent-config/bin/setup.mjs +3 -4
- package/agent-config/claude-code/agents/mvc-kit-architect.md +14 -0
- package/agent-config/claude-code/skills/guide/api-reference.md +24 -2
- package/agent-config/lib/install-claude.mjs +10 -33
- package/dist/Model.cjs +9 -1
- package/dist/Model.cjs.map +1 -1
- package/dist/Model.d.ts +1 -1
- package/dist/Model.d.ts.map +1 -1
- package/dist/Model.js +9 -1
- package/dist/Model.js.map +1 -1
- package/dist/ViewModel.cjs +9 -1
- package/dist/ViewModel.cjs.map +1 -1
- package/dist/ViewModel.d.ts +1 -1
- package/dist/ViewModel.d.ts.map +1 -1
- package/dist/ViewModel.js +9 -1
- package/dist/ViewModel.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +3 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +3 -0
- package/dist/mvc-kit.js.map +1 -1
- package/dist/produceDraft.cjs +105 -0
- package/dist/produceDraft.cjs.map +1 -0
- package/dist/produceDraft.d.ts +19 -0
- package/dist/produceDraft.d.ts.map +1 -0
- package/dist/produceDraft.js +105 -0
- package/dist/produceDraft.js.map +1 -0
- package/package.json +4 -2
- package/src/Channel.md +408 -0
- package/src/Channel.test.ts +957 -0
- package/src/Channel.ts +429 -0
- package/src/Collection.md +533 -0
- package/src/Collection.test.ts +1559 -0
- package/src/Collection.ts +653 -0
- package/src/Controller.md +306 -0
- package/src/Controller.test.ts +380 -0
- package/src/Controller.ts +90 -0
- package/src/EventBus.md +308 -0
- package/src/EventBus.test.ts +295 -0
- package/src/EventBus.ts +110 -0
- package/src/Feed.md +218 -0
- package/src/Feed.test.ts +442 -0
- package/src/Feed.ts +101 -0
- package/src/Model.md +524 -0
- package/src/Model.test.ts +642 -0
- package/src/Model.ts +260 -0
- package/src/Pagination.md +168 -0
- package/src/Pagination.test.ts +244 -0
- package/src/Pagination.ts +92 -0
- package/src/Pending.md +380 -0
- package/src/Pending.test.ts +1719 -0
- package/src/Pending.ts +390 -0
- package/src/PersistentCollection.md +183 -0
- package/src/PersistentCollection.test.ts +649 -0
- package/src/PersistentCollection.ts +375 -0
- package/src/Resource.ViewModel.test.ts +503 -0
- package/src/Resource.md +239 -0
- package/src/Resource.test.ts +786 -0
- package/src/Resource.ts +231 -0
- package/src/Selection.md +155 -0
- package/src/Selection.test.ts +326 -0
- package/src/Selection.ts +117 -0
- package/src/Service.md +440 -0
- package/src/Service.test.ts +241 -0
- package/src/Service.ts +72 -0
- package/src/Sorting.md +170 -0
- package/src/Sorting.test.ts +334 -0
- package/src/Sorting.ts +135 -0
- package/src/Trackable.md +166 -0
- package/src/Trackable.test.ts +236 -0
- package/src/Trackable.ts +129 -0
- package/src/ViewModel.async.test.ts +813 -0
- package/src/ViewModel.derived.test.ts +1583 -0
- package/src/ViewModel.md +1111 -0
- package/src/ViewModel.test.ts +1236 -0
- package/src/ViewModel.ts +800 -0
- package/src/bindPublicMethods.test.ts +126 -0
- package/src/bindPublicMethods.ts +48 -0
- package/src/env.d.ts +5 -0
- package/src/errors.test.ts +155 -0
- package/src/errors.ts +133 -0
- package/src/index.ts +49 -0
- package/src/produceDraft.md +90 -0
- package/src/produceDraft.test.ts +394 -0
- package/src/produceDraft.ts +168 -0
- package/src/react/components/CardList.md +97 -0
- package/src/react/components/CardList.test.tsx +142 -0
- package/src/react/components/CardList.tsx +68 -0
- package/src/react/components/DataTable.md +179 -0
- package/src/react/components/DataTable.test.tsx +599 -0
- package/src/react/components/DataTable.tsx +267 -0
- package/src/react/components/InfiniteScroll.md +116 -0
- package/src/react/components/InfiniteScroll.test.tsx +218 -0
- package/src/react/components/InfiniteScroll.tsx +70 -0
- package/src/react/components/types.ts +90 -0
- package/src/react/derived.test.tsx +261 -0
- package/src/react/guards.ts +24 -0
- package/src/react/index.ts +40 -0
- package/src/react/provider.test.tsx +143 -0
- package/src/react/provider.tsx +55 -0
- package/src/react/strict-mode.test.tsx +266 -0
- package/src/react/types.ts +25 -0
- package/src/react/use-event-bus.md +214 -0
- package/src/react/use-event-bus.test.tsx +168 -0
- package/src/react/use-event-bus.ts +40 -0
- package/src/react/use-instance.md +204 -0
- package/src/react/use-instance.test.tsx +350 -0
- package/src/react/use-instance.ts +60 -0
- package/src/react/use-local.md +457 -0
- package/src/react/use-local.rapid-remount.test.tsx +503 -0
- package/src/react/use-local.test.tsx +692 -0
- package/src/react/use-local.ts +165 -0
- package/src/react/use-model.md +364 -0
- package/src/react/use-model.test.tsx +394 -0
- package/src/react/use-model.ts +161 -0
- package/src/react/use-singleton.md +415 -0
- package/src/react/use-singleton.test.tsx +296 -0
- package/src/react/use-singleton.ts +69 -0
- package/src/react/use-subscribe-only.ts +39 -0
- package/src/react/use-teardown.md +169 -0
- package/src/react/use-teardown.test.tsx +86 -0
- package/src/react/use-teardown.ts +27 -0
- package/src/react-native/NativeCollection.test.ts +250 -0
- package/src/react-native/NativeCollection.ts +138 -0
- package/src/react-native/index.ts +1 -0
- package/src/singleton.md +310 -0
- package/src/singleton.test.ts +204 -0
- package/src/singleton.ts +70 -0
- package/src/types.ts +70 -0
- package/src/walkPrototypeChain.ts +22 -0
- package/src/web/IndexedDBCollection.test.ts +235 -0
- package/src/web/IndexedDBCollection.ts +66 -0
- package/src/web/WebStorageCollection.test.ts +214 -0
- package/src/web/WebStorageCollection.ts +116 -0
- package/src/web/idb.ts +184 -0
- package/src/web/index.ts +2 -0
- package/src/wrapAsyncMethods.ts +249 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { CardList } from './CardList';
|
|
7
|
+
|
|
8
|
+
interface Item {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const items: Item[] = [
|
|
14
|
+
{ id: '1', name: 'Alice' },
|
|
15
|
+
{ id: '2', name: 'Bob' },
|
|
16
|
+
{ id: '3', name: 'Charlie' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
describe('CardList', () => {
|
|
20
|
+
it('renders items', () => {
|
|
21
|
+
render(
|
|
22
|
+
<CardList
|
|
23
|
+
items={items}
|
|
24
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
25
|
+
/>,
|
|
26
|
+
);
|
|
27
|
+
expect(screen.getByText('Alice')).toBeDefined();
|
|
28
|
+
expect(screen.getByText('Bob')).toBeDefined();
|
|
29
|
+
expect(screen.getByText('Charlie')).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders as list layout by default', () => {
|
|
33
|
+
const { container } = render(
|
|
34
|
+
<CardList
|
|
35
|
+
items={items}
|
|
36
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
37
|
+
/>,
|
|
38
|
+
);
|
|
39
|
+
const ul = container.querySelector('[data-component="card-list"]')!;
|
|
40
|
+
expect(ul.getAttribute('data-layout')).toBe('list');
|
|
41
|
+
expect(ul.getAttribute('role')).toBe('list');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders as grid layout with CSS custom properties', () => {
|
|
45
|
+
const { container } = render(
|
|
46
|
+
<CardList
|
|
47
|
+
items={items}
|
|
48
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
49
|
+
layout="grid"
|
|
50
|
+
columns={4}
|
|
51
|
+
gap="2rem"
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
54
|
+
const ul = container.querySelector('[data-component="card-list"]')!;
|
|
55
|
+
expect(ul.getAttribute('data-layout')).toBe('grid');
|
|
56
|
+
expect((ul as HTMLElement).style.display).toBe('grid');
|
|
57
|
+
expect((ul as HTMLElement).style.gridTemplateColumns).toContain('4');
|
|
58
|
+
expect((ul as HTMLElement).style.gap).toContain('2rem');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('uses custom keyOf', () => {
|
|
62
|
+
const { container } = render(
|
|
63
|
+
<CardList
|
|
64
|
+
items={[{ uid: 'x', label: 'X' }]}
|
|
65
|
+
keyOf={(item: any) => item.uid}
|
|
66
|
+
renderItem={(item: any) => <span>{item.label}</span>}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
69
|
+
expect(screen.getByText('X')).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('renders empty state', () => {
|
|
73
|
+
render(
|
|
74
|
+
<CardList
|
|
75
|
+
items={[]}
|
|
76
|
+
renderItem={() => <span>item</span>}
|
|
77
|
+
renderEmpty={() => <p>No items found</p>}
|
|
78
|
+
/>,
|
|
79
|
+
);
|
|
80
|
+
expect(screen.getByText('No items found')).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('renders loading state', () => {
|
|
84
|
+
render(
|
|
85
|
+
<CardList
|
|
86
|
+
items={items}
|
|
87
|
+
loading={true}
|
|
88
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
89
|
+
renderLoading={() => <p>Loading data...</p>}
|
|
90
|
+
/>,
|
|
91
|
+
);
|
|
92
|
+
expect(screen.getByText('Loading data...')).toBeDefined();
|
|
93
|
+
expect(screen.queryByText('Alice')).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('renders error state', () => {
|
|
97
|
+
render(
|
|
98
|
+
<CardList
|
|
99
|
+
items={items}
|
|
100
|
+
error="Something went wrong"
|
|
101
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
102
|
+
renderError={(err) => <p>Error: {err}</p>}
|
|
103
|
+
/>,
|
|
104
|
+
);
|
|
105
|
+
expect(screen.getByText('Error: Something went wrong')).toBeDefined();
|
|
106
|
+
expect(screen.queryByText('Alice')).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('applies className', () => {
|
|
110
|
+
const { container } = render(
|
|
111
|
+
<CardList
|
|
112
|
+
items={items}
|
|
113
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
114
|
+
className="my-list"
|
|
115
|
+
/>,
|
|
116
|
+
);
|
|
117
|
+
expect(container.querySelector('.my-list')).not.toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('applies aria-label', () => {
|
|
121
|
+
const { container } = render(
|
|
122
|
+
<CardList
|
|
123
|
+
items={items}
|
|
124
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
125
|
+
aria-label="User list"
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
const ul = container.querySelector('[data-component="card-list"]')!;
|
|
129
|
+
expect(ul.getAttribute('aria-label')).toBe('User list');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('passes index to renderItem', () => {
|
|
133
|
+
render(
|
|
134
|
+
<CardList
|
|
135
|
+
items={items}
|
|
136
|
+
renderItem={(item, index) => <span>{index}: {item.name}</span>}
|
|
137
|
+
/>,
|
|
138
|
+
);
|
|
139
|
+
expect(screen.getByText('0: Alice')).toBeDefined();
|
|
140
|
+
expect(screen.getByText('2: Charlie')).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AsyncStateProps } from './types';
|
|
3
|
+
|
|
4
|
+
/** Props for the CardList headless component. */
|
|
5
|
+
export interface CardListProps<T> extends AsyncStateProps {
|
|
6
|
+
items: T[];
|
|
7
|
+
renderItem: (item: T, index: number) => ReactNode;
|
|
8
|
+
keyOf?: (item: T) => string | number;
|
|
9
|
+
layout?: 'list' | 'grid';
|
|
10
|
+
columns?: number;
|
|
11
|
+
gap?: string;
|
|
12
|
+
renderEmpty?: () => ReactNode;
|
|
13
|
+
renderLoading?: () => ReactNode;
|
|
14
|
+
renderError?: (error: string) => ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
'aria-label'?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const defaultKeyOf = (item: any) => item.id;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Headless list/grid component with render-prop items.
|
|
23
|
+
* Renders a semantic `<ul>` with optional CSS grid layout.
|
|
24
|
+
*/
|
|
25
|
+
export function CardList<T>({
|
|
26
|
+
items,
|
|
27
|
+
renderItem,
|
|
28
|
+
keyOf = defaultKeyOf,
|
|
29
|
+
layout = 'list',
|
|
30
|
+
columns = 3,
|
|
31
|
+
gap = '1rem',
|
|
32
|
+
loading,
|
|
33
|
+
error,
|
|
34
|
+
renderEmpty,
|
|
35
|
+
renderLoading,
|
|
36
|
+
renderError,
|
|
37
|
+
className,
|
|
38
|
+
'aria-label': ariaLabel,
|
|
39
|
+
}: CardListProps<T>) {
|
|
40
|
+
if (loading && renderLoading) return <>{renderLoading()}</>;
|
|
41
|
+
if (error && renderError) return <>{renderError(error)}</>;
|
|
42
|
+
if (items.length === 0 && renderEmpty) return <>{renderEmpty()}</>;
|
|
43
|
+
|
|
44
|
+
const style = layout === 'grid'
|
|
45
|
+
? {
|
|
46
|
+
display: 'grid',
|
|
47
|
+
gridTemplateColumns: `repeat(var(--card-list-columns, ${columns}), 1fr)`,
|
|
48
|
+
gap: `var(--card-list-gap, ${gap})`,
|
|
49
|
+
} as const
|
|
50
|
+
: undefined;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<ul
|
|
54
|
+
role="list"
|
|
55
|
+
data-component="card-list"
|
|
56
|
+
data-layout={layout}
|
|
57
|
+
className={className}
|
|
58
|
+
aria-label={ariaLabel}
|
|
59
|
+
style={style}
|
|
60
|
+
>
|
|
61
|
+
{items.map((item, index) => (
|
|
62
|
+
<li key={keyOf(item)} data-index={index}>
|
|
63
|
+
{renderItem(item, index)}
|
|
64
|
+
</li>
|
|
65
|
+
))}
|
|
66
|
+
</ul>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# DataTable
|
|
2
|
+
|
|
3
|
+
Headless, unstyled table component with sort headers, row selection, and pagination support.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Use DataTable for tabular data displays. It renders semantic HTML (`<table role="grid">`) with data attributes for styling hooks. No CSS included — bring your own styles.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
Pass helpers directly — DataTable detects them via duck-typing:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { DataTable } from 'mvc-kit/react';
|
|
19
|
+
import type { Column } from 'mvc-kit/react';
|
|
20
|
+
|
|
21
|
+
const columns: Column<User>[] = [
|
|
22
|
+
{ key: 'name', header: 'Name', render: u => u.name, sortable: true },
|
|
23
|
+
{ key: 'email', header: 'Email', render: u => u.email },
|
|
24
|
+
{ key: 'role', header: 'Role', render: u => u.role, align: 'center' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function UsersTable() {
|
|
28
|
+
const [state, vm] = useLocal(UsersVM, { search: '' });
|
|
29
|
+
return (
|
|
30
|
+
<DataTable
|
|
31
|
+
items={vm.items}
|
|
32
|
+
columns={columns}
|
|
33
|
+
sort={vm.sorting}
|
|
34
|
+
selection={vm.selection}
|
|
35
|
+
pagination={vm.pagination}
|
|
36
|
+
paginationTotal={vm.filteredCount}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Props
|
|
45
|
+
|
|
46
|
+
### Required
|
|
47
|
+
|
|
48
|
+
| Prop | Type | Description |
|
|
49
|
+
|------|------|-------------|
|
|
50
|
+
| `items` | `T[]` | Array of data items to render |
|
|
51
|
+
| `columns` | `Column<T>[]` | Column definitions |
|
|
52
|
+
|
|
53
|
+
### Column Definition
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
interface Column<T> {
|
|
57
|
+
key: string; // Unique identifier
|
|
58
|
+
header: ReactNode; // Header content
|
|
59
|
+
render: (item: T, index: number) => ReactNode; // Cell renderer
|
|
60
|
+
sortable?: boolean; // Enable sort header
|
|
61
|
+
width?: string; // Column width CSS
|
|
62
|
+
align?: 'left' | 'center' | 'right'; // Alignment
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Optional
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Default | Description |
|
|
69
|
+
|------|------|---------|-------------|
|
|
70
|
+
| `keyOf` | `(item: T) => string \| number` | `item => item.id` | Key extractor |
|
|
71
|
+
| `pageSize` | `number` | — | Enables client-side pagination |
|
|
72
|
+
| `sort` | `SortDescriptor[] \| SortingHelper` | — | Sort descriptors or Sorting helper |
|
|
73
|
+
| `onSort` | `(key: string) => void` | — | Sort header click handler (not needed with SortingHelper) |
|
|
74
|
+
| `selection` | `SelectionState \| SelectionHelper` | — | Selection state or Selection helper |
|
|
75
|
+
| `pagination` | `PaginationState \| PaginationHelper` | — | Pagination state or Pagination helper |
|
|
76
|
+
| `paginationTotal` | `number` | — | Total item count (required with PaginationHelper) |
|
|
77
|
+
| `loading` | `boolean` | — | Loading state |
|
|
78
|
+
| `error` | `string \| null` | — | Error message |
|
|
79
|
+
| `className` | `string` | — | Container class |
|
|
80
|
+
| `aria-label` | `string` | — | Table accessibility label |
|
|
81
|
+
|
|
82
|
+
### Render Slots
|
|
83
|
+
|
|
84
|
+
| Prop | Type | Description |
|
|
85
|
+
|------|------|-------------|
|
|
86
|
+
| `renderEmpty` | `() => ReactNode` | Empty state |
|
|
87
|
+
| `renderLoading` | `() => ReactNode` | Loading state |
|
|
88
|
+
| `renderError` | `(error: string) => ReactNode` | Error state |
|
|
89
|
+
| `renderSortIndicator` | `(props: SortHeaderProps) => ReactNode` | Custom sort indicator |
|
|
90
|
+
| `renderRow` | `(item, index, cells) => ReactNode` | Custom row wrapper |
|
|
91
|
+
| `renderPagination` | `(info: PaginationInfo) => ReactNode` | Pagination controls |
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Sort Headers
|
|
96
|
+
|
|
97
|
+
Pass a `Sorting` helper directly, or use the object-literal form with `sort` + `onSort`:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
// Helper (recommended)
|
|
101
|
+
sort={vm.sorting}
|
|
102
|
+
|
|
103
|
+
// Object-literal (custom usage)
|
|
104
|
+
sort={vm.sorting.sorts}
|
|
105
|
+
onSort={key => vm.sorting.toggle(key)}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
When sort is provided, sortable columns render `<button>` elements with `aria-sort` attributes:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
renderSortIndicator={({ active, direction }) => (
|
|
112
|
+
<span>{active ? (direction === 'asc' ? ' ↑' : ' ↓') : ''}</span>
|
|
113
|
+
)}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Selection
|
|
119
|
+
|
|
120
|
+
Pass a `Selection` helper directly, or use the object-literal form:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// Helper (recommended) — DataTable passes allKeys to toggleAll automatically
|
|
124
|
+
selection={vm.selection}
|
|
125
|
+
|
|
126
|
+
// Object-literal (custom usage)
|
|
127
|
+
selection={{
|
|
128
|
+
selected: vm.selection.selected,
|
|
129
|
+
onToggle: id => vm.selection.toggle(id),
|
|
130
|
+
onToggleAll: allKeys => vm.selection.toggleAll(allKeys),
|
|
131
|
+
}}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
DataTable computes `allKeys` from the visible items and passes them to `onToggleAll()` in both paths — you never need to reconstruct the key set yourself.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Pagination
|
|
139
|
+
|
|
140
|
+
Pass a `Pagination` helper directly (with `paginationTotal`), or use the object-literal form:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
// Helper (recommended)
|
|
144
|
+
pagination={vm.pagination}
|
|
145
|
+
paginationTotal={vm.filteredCount}
|
|
146
|
+
|
|
147
|
+
// Object-literal (custom usage)
|
|
148
|
+
pagination={{
|
|
149
|
+
page: vm.pagination.page,
|
|
150
|
+
total: vm.filteredCount,
|
|
151
|
+
onPageChange: p => vm.pagination.setPage(p),
|
|
152
|
+
}}
|
|
153
|
+
pageSize={vm.pagination.pageSize}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Uncontrolled** (with `pageSize` only): Shows first page of items.
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
renderPagination={info => (
|
|
160
|
+
<nav>
|
|
161
|
+
<button disabled={!info.hasPrev} onClick={info.goPrev}>Prev</button>
|
|
162
|
+
<span>Page {info.page} of {info.pageCount}</span>
|
|
163
|
+
<button disabled={!info.hasNext} onClick={info.goNext}>Next</button>
|
|
164
|
+
</nav>
|
|
165
|
+
)}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Data Attributes
|
|
171
|
+
|
|
172
|
+
| Attribute | Element | Description |
|
|
173
|
+
|-----------|---------|-------------|
|
|
174
|
+
| `data-component="data-table"` | Container `<div>` | Component identifier |
|
|
175
|
+
| `data-sortable` | `<th>` | Present on sortable columns |
|
|
176
|
+
| `data-sorted` | `<th>` | Present on actively sorted columns |
|
|
177
|
+
| `data-selected` | `<tr>` | Present on selected rows |
|
|
178
|
+
| `data-align` | `<th>`, `<td>` | Column alignment value |
|
|
179
|
+
| `data-column="select"` | `<th>`, `<td>` | Selection checkbox column |
|