Skip to content

Commit 58df5fb

Browse files
johnleiderclaude
andauthored
refactor(createDataTable): drop items option for registry pattern (#236)
Co-authored-by: dev-table <noreply@anthropic.com> Co-authored-by: docs-table <noreply@anthropic.com> Co-authored-by: philosophy <noreply@anthropic.com>
1 parent a7892d2 commit 58df5fb

20 files changed

Lines changed: 1111 additions & 560 deletions

File tree

.claude/rules/composables.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Scope-specific mechanics for `packages/0/src/composables/**`. Covers naming, fac
2626
- §6.6 `useProxyModel`
2727
- §6.7 `useProxyRegistry`
2828
- §6.8 Register / unregister lifecycle contract
29+
- §6.10 Collection composables: no `items` option
2930
- §7 Events & lifecycle
3031
- §9 Errors & invariants
3132

@@ -343,6 +344,10 @@ const table = createDataTable({
343344
- The composable has exactly one correct implementation, and consumers have no reason to swap it. Example: `useHotkey` — the listener semantics are fixed.
344345
- You want to switch behavior based on a boolean flag. Use an option (`mode: 'client' | 'server'`) rather than dressing it up as an adapter. Adapters are for swapping *implementations*, not for flipping a known toggle.
345346

347+
### Collection composables: no `items` option
348+
349+
A composable that owns a collection of values exposes `register` / `onboard` / `unregister`. It never accepts an `items` option in its factory — row identity, order, and per-row state live in the registry. Followed by `createRegistry`, `createModel`, `createSelection`, `createSingle`, `createGroup`, `createStep`, `createNested`, `createSortable`, `createKanban`, `createQueue`, `createTimeline`, `createTokens`, and (after the recent refactor) `createDataTable`. Full rule and migration shape: PHILOSOPHY §6.10.
350+
346351
### `useProxyModel` and `useProxyRegistry` — cross-link
347352

348353
Both composables are covered in PHILOSOPHY §6.6 and §6.7. Repeating the when-to-use summary here for composable authors:
@@ -494,3 +499,4 @@ Pure transformers (`toRef`, `toElement`, `toValue`) are fine to call inline —
494499
- [ ] No DOM event binding inside the composable
495500
- [ ] ID generation through `useId()`
496501
- [ ] Trinity return only from `createTrinity` / `createContext` / `createPlugin`
502+
- [ ] Composable that owns a collection of values uses `register` / `onboard`, never an `items` option (PHILOSOPHY §6.10)

.claude/rules/new-feature-checklist.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,4 @@ Prefer extending an existing pattern over creating a new one.
189189
- [ ] Feature appears in `apps/docs/build/generated/api-whitelist.ts` after build
190190
- [ ] `<DocsApi />` renders on the new docs page
191191
- [ ] Maturity level matches the promotion criteria table (don't self-promote to `stable` or `mature` — those require a maintainer)
192+
- [ ] Collection composable surface uses `register` / `onboard` (no `items` option) — see PHILOSOPHY §6.10

apps/docs/src/examples/composables/create-data-table/basic/BasicTable.vue

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
import { users } from './data'
55
66
const table = createDataTable({
7-
items: users,
8-
columns,
97
pagination: { itemsPerPage: 5 },
108
})
119
12-
function sortIcon (key: string) {
13-
const dir = table.sort.direction(key)
10+
table.columns.onboard(columns)
11+
table.onboard(users.map(value => ({ id: value.id, value })))
12+
13+
function arrow (id: string) {
14+
const dir = table.sort.direction(id)
1415
if (dir === 'asc') return ''
1516
if (dir === 'desc') return ''
1617
return ''
@@ -32,13 +33,13 @@
3233
<thead>
3334
<tr class="border-b border-divider bg-surface-tint">
3435
<th
35-
v-for="col in columns"
36-
:key="col.key"
36+
v-for="col in table.columns.values()"
37+
:key="col.id"
3738
class="px-4 py-3 text-left font-medium cursor-pointer select-none hover:text-primary transition-colors"
38-
@click="table.sort.toggle(col.key)"
39+
@click="table.sort.toggle(col.id)"
3940
>
4041
{{ col.title }}
41-
<span class="ml-1 text-xs opacity-50">{{ sortIcon(col.key) }}</span>
42+
<span class="ml-1 text-xs opacity-50">{{ arrow(col.id) }}</span>
4243
</th>
4344
</tr>
4445
</thead>
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { DataTableColumn } from '@vuetify/v0'
1+
import type { DataTableColumnTicketInput } from '@vuetify/v0'
22
import type { User } from './data'
33

4-
export const columns: DataTableColumn<User>[] = [
5-
{ key: 'name', title: 'Name', sortable: true, filterable: true },
6-
{ key: 'email', title: 'Email', sortable: true, filterable: true },
7-
{ key: 'role', title: 'Role', sortable: true },
4+
export const columns: DataTableColumnTicketInput<User>[] = [
5+
{ id: 'name', title: 'Name', sortable: true, filterable: true },
6+
{ id: 'email', title: 'Email', sortable: true, filterable: true },
7+
{ id: 'role', title: 'Role', sortable: true },
88
]

apps/docs/src/examples/composables/create-data-table/features/FeaturesTable.vue

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
import { employees } from './data'
55
66
const table = createDataTable({
7-
items: employees,
8-
columns,
97
groupBy: 'department',
108
openAll: true,
119
mandate: true,
@@ -15,14 +13,17 @@
1513
pagination: { itemsPerPage: 20 },
1614
})
1715
18-
function sortIcon (key: string) {
19-
const dir = table.sort.direction(key)
16+
table.columns.onboard(columns)
17+
table.onboard(employees.map(value => ({ id: value.id, value })))
18+
19+
function arrow (id: string) {
20+
const dir = table.sort.direction(id)
2021
if (dir === 'asc') return ''
2122
if (dir === 'desc') return ''
2223
return ''
2324
}
2425
25-
function formatSalary (value: number) {
26+
function format (value: number) {
2627
return `$${value.toLocaleString()}`
2728
}
2829
</script>
@@ -68,13 +69,13 @@
6869
</th>
6970

7071
<th
71-
v-for="col in columns"
72-
:key="col.key"
72+
v-for="col in table.columns.values()"
73+
:key="col.id"
7374
class="px-4 py-3 text-left font-medium cursor-pointer select-none hover:text-primary transition-colors"
74-
@click="table.sort.toggle(col.key)"
75+
@click="table.sort.toggle(col.id)"
7576
>
7677
{{ col.title }}
77-
<span class="ml-1 text-xs opacity-50">{{ sortIcon(col.key) }}</span>
78+
<span class="ml-1 text-xs opacity-50">{{ arrow(col.id) }}</span>
7879
</th>
7980
</tr>
8081
</thead>
@@ -85,7 +86,7 @@
8586
class="bg-surface-tint cursor-pointer hover:bg-surface-variant transition-colors"
8687
@click="table.grouping.toggle(group.key)"
8788
>
88-
<td class="px-4 py-2 font-medium" :colspan="columns.length + 1">
89+
<td class="px-4 py-2 font-medium" :colspan="table.columns.size + 1">
8990
<span class="mr-2 text-xs">{{ table.grouping.isOpen(group.key) ? '−' : '+' }}</span>
9091
{{ group.key }}
9192
<span class="ml-2 text-xs opacity-50">({{ group.items.length }})</span>
@@ -111,7 +112,7 @@
111112

112113
<td class="px-4 py-3">{{ item.name }}</td>
113114
<td class="px-4 py-3">{{ item.department }}</td>
114-
<td class="px-4 py-3 font-mono">{{ formatSalary(item.salary) }}</td>
115+
<td class="px-4 py-3 font-mono">{{ format(item.salary) }}</td>
115116

116117
<td class="px-4 py-3">
117118
<span
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { DataTableColumn } from '@vuetify/v0'
1+
import type { DataTableColumnTicketInput } from '@vuetify/v0'
22
import type { Employee } from './data'
33

4-
export const columns: DataTableColumn<Employee>[] = [
5-
{ key: 'name', title: 'Name', sortable: true, filterable: true },
6-
{ key: 'department', title: 'Department', sortable: true },
4+
export const columns: DataTableColumnTicketInput<Employee>[] = [
5+
{ id: 'name', title: 'Name', sortable: true, filterable: true },
6+
{ id: 'department', title: 'Department', sortable: true },
77
{
8-
key: 'salary',
8+
id: 'salary',
99
title: 'Salary',
1010
sortable: true,
1111
filterable: true,
@@ -17,5 +17,5 @@ export const columns: DataTableColumn<Employee>[] = [
1717
return String(value).includes(query)
1818
},
1919
},
20-
{ key: 'active', title: 'Status', sortable: true },
20+
{ id: 'active', title: 'Status', sortable: true },
2121
]

apps/docs/src/examples/composables/create-data-table/server/ServerTable.vue

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,43 @@
11
<script setup lang="ts">
22
import { createDataTable, ServerDataTableAdapter } from '@vuetify/v0'
33
import { shallowRef, watch } from 'vue'
4-
import { fetchUsers } from './api'
4+
import { fetchPage } from './api'
55
import { columns } from './columns'
66
7-
import type { User } from './api'
8-
9-
const serverItems = shallowRef<User[]>([])
10-
const totalCount = shallowRef(0)
11-
const isLoading = shallowRef(false)
7+
const total = shallowRef(0)
8+
const loading = shallowRef(false)
129
1310
const table = createDataTable({
14-
items: serverItems,
15-
columns,
1611
pagination: { itemsPerPage: 5 },
17-
adapter: new ServerDataTableAdapter({
18-
total: totalCount,
19-
loading: isLoading,
20-
}),
12+
adapter: new ServerDataTableAdapter({ total, loading }),
2113
})
2214
23-
async function loadData () {
24-
isLoading.value = true
15+
table.columns.onboard(columns)
16+
17+
async function load () {
18+
loading.value = true
2519
26-
const result = await fetchUsers(
20+
const result = await fetchPage(
2721
table.query.value,
2822
table.sort.columns.value,
2923
table.pagination.page.value,
3024
table.pagination.itemsPerPage,
3125
)
3226
33-
totalCount.value = result.total
34-
serverItems.value = result.items
35-
isLoading.value = false
27+
total.value = result.total
28+
table.clear()
29+
table.onboard(result.items.map(value => ({ id: value.id, value })))
30+
loading.value = false
3631
}
3732
3833
watch(
3934
[table.query, table.sort.columns, table.pagination.page],
40-
() => loadData(),
35+
() => load(),
4136
{ immediate: true },
4237
)
4338
44-
function sortIcon (key: string) {
45-
const dir = table.sort.direction(key)
39+
function arrow (id: string) {
40+
const dir = table.sort.direction(id)
4641
if (dir === 'asc') return ''
4742
if (dir === 'desc') return ''
4843
return ''
@@ -76,13 +71,13 @@
7671
<thead>
7772
<tr class="border-b border-divider bg-surface-tint">
7873
<th
79-
v-for="col in columns"
80-
:key="col.key"
74+
v-for="col in table.columns.values()"
75+
:key="col.id"
8176
class="px-4 py-3 text-left font-medium cursor-pointer select-none hover:text-primary transition-colors"
82-
@click="table.sort.toggle(col.key)"
77+
@click="table.sort.toggle(col.id)"
8378
>
8479
{{ col.title }}
85-
<span class="ml-1 text-xs opacity-50">{{ sortIcon(col.key) }}</span>
80+
<span class="ml-1 text-xs opacity-50">{{ arrow(col.id) }}</span>
8681
</th>
8782
</tr>
8883
</thead>
@@ -109,7 +104,7 @@
109104

110105
<div class="flex items-center justify-between text-sm">
111106
<span class="opacity-60">
112-
{{ totalCount }} total
107+
{{ total }} total
113108
</span>
114109

115110
<div class="flex gap-1">

apps/docs/src/examples/composables/create-data-table/server/api.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface FetchResult {
2323
* Simulates a server API call with filtering, sorting, and pagination.
2424
* Returns only the current page of results after a short delay.
2525
*/
26-
export function fetchUsers (
26+
export function fetchPage (
2727
query: string,
2828
sorts: SortEntry[],
2929
page: number,
@@ -46,9 +46,9 @@ export function fetchUsers (
4646
if (sorts.length > 0) {
4747
const { key, direction } = sorts[0]
4848
result.sort((a, b) => {
49-
const aVal = String(a[key as keyof User])
50-
const bVal = String(b[key as keyof User])
51-
const cmp = aVal.localeCompare(bVal)
49+
const left = String(a[key as keyof User])
50+
const right = String(b[key as keyof User])
51+
const cmp = left.localeCompare(right)
5252
return direction === 'desc' ? -cmp : cmp
5353
})
5454
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { DataTableColumn } from '@vuetify/v0'
1+
import type { DataTableColumnTicketInput } from '@vuetify/v0'
22
import type { User } from './api'
33

4-
export const columns: DataTableColumn<User>[] = [
5-
{ key: 'name', title: 'Name', sortable: true, filterable: true },
6-
{ key: 'email', title: 'Email', sortable: true, filterable: true },
7-
{ key: 'department', title: 'Department', sortable: true },
4+
export const columns: DataTableColumnTicketInput<User>[] = [
5+
{ id: 'name', title: 'Name', sortable: true, filterable: true },
6+
{ id: 'email', title: 'Email', sortable: true, filterable: true },
7+
{ id: 'department', title: 'Department', sortable: true },
88
]

apps/docs/src/examples/composables/create-data-table/virtual/VirtualTable.vue

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@
22
import { createDataTable, VirtualDataTableAdapter, createVirtual } from '@vuetify/v0'
33
import { computed } from 'vue'
44
import { columns } from './columns'
5-
import { generateUsers } from './data'
5+
import { generate } from './data'
66
7-
const items = generateUsers(1000)
7+
const items = generate(1000)
88
99
const table = createDataTable({
10-
items,
11-
columns,
1210
adapter: new VirtualDataTableAdapter(),
1311
})
1412
13+
table.columns.onboard(columns)
14+
table.onboard(items.map(value => ({ id: value.id, value })))
15+
1516
const virtual = createVirtual(table.items, { itemHeight: 40 })
1617
const {
1718
element,
18-
items: virtualItems,
19+
items: visible,
1920
offset,
2021
size,
2122
scroll,
@@ -24,11 +25,11 @@
2425
const stats = computed(() => ({
2526
total: items.length,
2627
filtered: table.items.value.length,
27-
rendered: virtualItems.value.length,
28+
rendered: visible.value.length,
2829
}))
2930
30-
function sortIcon (key: string) {
31-
const dir = table.sort.direction(key)
31+
function arrow (id: string) {
32+
const dir = table.sort.direction(id)
3233
if (dir === 'asc') return ''
3334
if (dir === 'desc') return ''
3435
return ''
@@ -61,13 +62,13 @@
6162
<thead class="sticky top-0 z-10 bg-surface">
6263
<tr class="border-b border-divider bg-surface-tint">
6364
<th
64-
v-for="col in columns"
65-
:key="col.key"
65+
v-for="col in table.columns.values()"
66+
:key="col.id"
6667
class="px-4 py-3 text-left font-medium cursor-pointer select-none hover:text-primary transition-colors"
67-
@click="table.sort.toggle(col.key)"
68+
@click="table.sort.toggle(col.id)"
6869
>
6970
{{ col.title }}
70-
<span class="ml-1 text-xs opacity-50">{{ sortIcon(col.key) }}</span>
71+
<span class="ml-1 text-xs opacity-50">{{ arrow(col.id) }}</span>
7172
</th>
7273
</tr>
7374
</thead>
@@ -76,7 +77,7 @@
7677
<tr :style="{ height: `${offset}px` }" />
7778

7879
<tr
79-
v-for="item in virtualItems"
80+
v-for="item in visible"
8081
:key="item.raw.id"
8182
class="h-[40px] hover:bg-surface-tint transition-colors"
8283
>

0 commit comments

Comments
 (0)