Skip to content

Commit dc3a339

Browse files
committed
feat(createDataGrid): add main factory with trinity pattern
Wires together column layout, cell editing, row ordering, and row spanning on top of createDataTable with a ClientGridAdapter. Exports createDataGrid, createDataGridContext, and useDataGrid following the trinity pattern. Also fixes a pre-existing TypeScript error in spanning.ts (Array.from unknown type).
1 parent 1bc189b commit dc3a339

3 files changed

Lines changed: 242 additions & 1 deletion

File tree

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/**
2+
* @module createDataGrid
3+
*
4+
* @remarks
5+
* Main factory that wires together column layout, cell editing, row ordering,
6+
* and row spanning on top of a createDataTable pipeline. Uses a ClientGridAdapter
7+
* so row ordering is applied post-sort, pre-pagination.
8+
*
9+
* Follows the trinity pattern for dependency injection.
10+
*/
11+
12+
// Composables
13+
import { createContext, useContext } from '#v0/composables/createContext'
14+
import { createDataTable } from '#v0/composables/createDataTable'
15+
import { extractLeaves, resolveHeaders } from '#v0/composables/createDataTable/columns'
16+
import { createTrinity } from '#v0/composables/createTrinity'
17+
18+
// Adapters
19+
import { ClientGridAdapter } from './adapters'
20+
21+
// Utilities
22+
import { toRef, watch } from 'vue'
23+
24+
// Types
25+
import type { DataTableAdapterInterface, DataTableContext } from '#v0/composables/createDataTable'
26+
import type { InternalHeader } from '#v0/composables/createDataTable/columns'
27+
import type { FilterOptions } from '#v0/composables/createFilter'
28+
import type { PaginationOptions } from '#v0/composables/createPagination'
29+
import type { ContextTrinity } from '#v0/composables/createTrinity'
30+
import type { VirtualOptions } from '#v0/composables/createVirtual'
31+
import type { ID } from '#v0/types'
32+
import type { CellEditing } from './editing'
33+
import type { ColumnLayout, GridColumnDef } from './layout'
34+
import type { RowSpanningOptions, SpanEntry } from './spanning'
35+
import type { App, ComputedRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
36+
37+
// Grid modules
38+
import { createCellEditing } from './editing'
39+
import { createColumnLayout } from './layout'
40+
import { createRowOrdering } from './ordering'
41+
import { createRowSpanning } from './spanning'
42+
43+
export type { ColumnLayout, GridColumnDef, PinnedRegion, PinPosition, ResolvedColumn } from './layout'
44+
export type { ActiveCell, CellEditing, CellEditingOptions, EditableColumn } from './editing'
45+
export type { RowOrdering } from './ordering'
46+
export type { RowSpanningOptions, SpanEntry } from './spanning'
47+
export { ClientGridAdapter, ServerGridAdapter, VirtualGridAdapter } from './adapters'
48+
export type { ServerGridAdapterOptions } from './adapters'
49+
50+
export interface DataGridColumn<T extends Record<string, unknown> = Record<string, unknown>> extends GridColumnDef {
51+
readonly key: string
52+
readonly title?: string
53+
readonly sortable?: boolean
54+
readonly filterable?: boolean
55+
readonly sort?: (a: unknown, b: unknown) => number
56+
readonly filter?: (value: unknown, query: string) => boolean
57+
readonly editable?: boolean | ((item: T) => boolean)
58+
readonly editor?: 'text' | 'number' | 'boolean'
59+
readonly validate?: (value: unknown, item?: T) => boolean | string
60+
readonly span?: (item: T) => number
61+
readonly children?: readonly DataGridColumn<T>[]
62+
}
63+
64+
export interface DataGridOptions<T extends Record<string, unknown>> {
65+
items: MaybeRefOrGetter<T[]>
66+
columns: readonly DataGridColumn<T>[]
67+
itemValue?: string
68+
adapter?: DataTableAdapterInterface<T>
69+
filter?: Omit<FilterOptions, 'keys'>
70+
pagination?: Omit<PaginationOptions, 'size'>
71+
sortMultiple?: boolean
72+
pinning?: { left?: string[], right?: string[] }
73+
resizing?: boolean | { min?: number, max?: number }
74+
reordering?: boolean
75+
editing?: {
76+
columns?: string[]
77+
onEdit?: (row: ID, column: string, value: unknown, item: T) => void
78+
}
79+
rowReordering?: boolean
80+
preserveRowOrder?: boolean
81+
rowSpanning?: (item: T, column: string) => number
82+
virtualization?: VirtualOptions
83+
}
84+
85+
export interface DataGridContext<T extends Record<string, unknown>> extends DataTableContext<T> {
86+
layout: ColumnLayout
87+
rows: {
88+
order: Readonly<ShallowRef<ID[]>>
89+
move: (fromIndex: number, toIndex: number) => void
90+
reset: () => void
91+
}
92+
editing: CellEditing
93+
headers: Readonly<Ref<InternalHeader[][]>>
94+
spans: ComputedRef<Map<ID, Map<string, SpanEntry>>>
95+
virtual: null
96+
}
97+
98+
export interface DataGridContextOptions<T extends Record<string, unknown>> extends DataGridOptions<T> {
99+
namespace?: string
100+
}
101+
102+
/**
103+
* Creates a data grid instance with layout, editing, row ordering, and spanning
104+
* layered on top of the createDataTable pipeline.
105+
*
106+
* @param options Data grid options
107+
* @returns Data grid context
108+
*/
109+
export function createDataGrid<T extends Record<string, unknown>> (
110+
options: DataGridOptions<T>,
111+
): DataGridContext<T> {
112+
const {
113+
items,
114+
columns,
115+
itemValue = 'id',
116+
adapter: customAdapter,
117+
filter,
118+
pagination,
119+
sortMultiple,
120+
editing: editingOptions,
121+
preserveRowOrder = false,
122+
rowSpanning,
123+
} = options
124+
125+
// 1. Extract leaves from possibly nested column definitions
126+
const leaves = extractLeaves(columns)
127+
128+
// 2. Create row ordering state
129+
const ordering = createRowOrdering()
130+
131+
// 3. Create adapter: use ClientGridAdapter (closes over ordering) unless custom provided
132+
const adapter = customAdapter ?? new ClientGridAdapter<T>(ordering.order, itemValue)
133+
134+
// 4. Create the data table with the grid adapter
135+
const table = createDataTable<T>({
136+
items,
137+
columns,
138+
itemValue: itemValue as never,
139+
filter,
140+
pagination,
141+
sortMultiple,
142+
adapter,
143+
})
144+
145+
// 5. Watch sort changes to reset row order (unless preserveRowOrder)
146+
if (!preserveRowOrder) {
147+
watch(table.sort.columns, () => {
148+
ordering.reset()
149+
})
150+
}
151+
152+
// 6. Create column layout
153+
const layout = createColumnLayout(columns)
154+
155+
// 7. Resolve headers (toRef wrapping resolveHeaders)
156+
const headers = toRef(() => resolveHeaders(columns))
157+
158+
// 8. Create cell editing
159+
const editableColumns = leaves
160+
.filter(col => col.editable !== undefined || col.validate !== undefined)
161+
.map(col => ({
162+
key: col.key,
163+
editable: col.editable as boolean | ((item: unknown) => boolean) | undefined,
164+
validate: col.validate as ((value: unknown, item?: unknown) => boolean | string) | undefined,
165+
}))
166+
167+
const editing = createCellEditing({
168+
columns: editableColumns,
169+
onEdit: editingOptions?.onEdit
170+
? (row, column, value) => {
171+
const item = table.allItems.value.find(
172+
i => (i[itemValue] as ID) === row,
173+
) as T | undefined
174+
editingOptions.onEdit!(row, column, value, item as T)
175+
}
176+
: undefined,
177+
})
178+
179+
// 9. Create row spanning
180+
const columnKeys = leaves.map(col => col.key)
181+
182+
const spanOptions: RowSpanningOptions<T> = {
183+
items: table.items as Ref<readonly T[]>,
184+
columns: columnKeys,
185+
itemKey: itemValue,
186+
rowSpanning,
187+
}
188+
189+
const spans = createRowSpanning<T>(spanOptions)
190+
191+
// 10. Return merged context (table + grid features)
192+
return {
193+
...table,
194+
layout,
195+
rows: {
196+
order: ordering.order,
197+
move: ordering.move,
198+
reset: ordering.reset,
199+
},
200+
editing,
201+
headers,
202+
spans,
203+
virtual: null,
204+
}
205+
}
206+
207+
/**
208+
* Creates a data grid context with dependency injection support.
209+
*
210+
* @param options Data grid context options including namespace
211+
* @returns A trinity tuple: [useDataGrid, provideDataGrid, defaultContext]
212+
*/
213+
export function createDataGridContext<T extends Record<string, unknown>> (
214+
_options: DataGridContextOptions<T>,
215+
): ContextTrinity<DataGridContext<T>> {
216+
const { namespace = 'v0:data-grid', ...options } = _options
217+
const [useDataGridContext, _provideDataGridContext] = createContext<DataGridContext<T>>(namespace)
218+
const context = createDataGrid(options)
219+
220+
function provideDataGridContext (
221+
_context: DataGridContext<T> = context,
222+
app?: App,
223+
): DataGridContext<T> {
224+
return _provideDataGridContext(_context, app)
225+
}
226+
227+
return createTrinity<DataGridContext<T>>(useDataGridContext, provideDataGridContext, context)
228+
}
229+
230+
/**
231+
* Returns the current data grid context from dependency injection.
232+
*
233+
* @param namespace The namespace for the data grid context. @default 'v0:data-grid'
234+
* @returns The current data grid context
235+
*/
236+
export function useDataGrid<T extends Record<string, unknown>> (
237+
namespace = 'v0:data-grid',
238+
): DataGridContext<T> {
239+
return useContext<DataGridContext<T>>(namespace)
240+
}

packages/0/src/composables/createDataGrid/spanning.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function createRowSpanning<T extends Record<string, unknown>> (
4040

4141
// Track which cells are covered by a span from a previous row
4242
// covered[colIndex] = number of remaining rows to skip
43-
const covered = Array.from({ length: columns.length }).fill(0)
43+
const covered = Array.from<number>({ length: columns.length }).fill(0)
4444

4545
for (let row = 0; row < list.length; row++) {
4646
const item = list[row]

packages/0/src/composables/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Composables
22
export * from './createBreadcrumbs'
33
export * from './createContext'
4+
export * from './createDataGrid'
45
export * from './createDataTable'
56
export * from './createPlugin'
67
export * from './createTrinity'

0 commit comments

Comments
 (0)