feat: createDataGrid composable#174
Open
johnleider wants to merge 38 commits into
Open
Conversation
|
commit: |
johnleider
pushed a commit
that referenced
this pull request
Apr 12, 2026
Builds on createDataTable with column layout (sizing, pinning, resizing, reordering), cell editing (commit/cancel + per-column validation), ID-based row ordering, and a row spanning map. Inherits the full table pipeline (filter/sort/paginate/select/expand/group) via spread. Refactors createDataTable to support recursive column trees: relaxes DataTableColumn.key from keyof T to string, adds children, and exposes leaves + 2D headers via new extractLeaves/computeDepth/resolveHeaders utilities. Closes #174
08d1b2b to
5fd3a60
Compare
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).
- Extract shared applyOrder helper, eliminating duplicate ordering logic between ClientGridAdapter and VirtualGridAdapter - Fix O(n*m) order.includes() → Set-based O(n) lookup in adapters and ordering.apply() - Simplify layout.ts columns computed to reuse pinned regions instead of double-computing resolved() - Fix editable column filter to match editing.ts guard - Remove unused RowSpanningOptions import and numbered comments - Clean up test mock imports
…ance - Fix validate(false) silently passing as valid in cell editing - Evaluate editable functions with item context via itemLookup - Pass item to validate for context-aware validation - Type-safe itemValue using KeysOfType<T, ID> instead of string - Guard onEdit callback against undefined item - Delegate ordering.apply() to applyOrder to eliminate duplication - Add same-index and bounds guards to move/reorder - Remove redundant headers from DataGridContext - Use #v0/ path aliases in grid adapters - Use isFunction type guard instead of typeof - Add missing JSDoc to exported functions and barrel files
Prefer single-word names per style convention: - customSorts → sorts, customColumnFilters → filters - openGroupKeys → opened - editableColumns → editable - itemLookup → lookup - rowDirty → entry, cellMap → cells - fromIndex/toIndex → from/to - orderedItems → ordered - splitRegions → split, regionIndex → index
- Usage section with basic grid example (search, sort, pagination, column sizing) - Adapters section covering ClientGridAdapter, ServerGridAdapter, VirtualGridAdapter - Features: column layout, cell editing, row ordering, row spanning, nested columns - Reactivity table - Pinned grid example with resize handles and pin/unpin controls - Editable grid example with inline validation and edit log - Spanning grid example with merged department cells
- Basic: project tracker with status pills, priority colors, progress bars - Pinned: financial spreadsheet with 10 columns, sticky freeze lines, resize handles - Editing: inventory editor with focus ring, inline validation, edit history - Spanning: team schedule with department spanning and availability status - Sort indicators use MDI icons instead of unicode arrows - Each example has a distinct real-world purpose that justifies grid over table
- Fix search binding (grid.search is a function, not a ref) - Fix sort toggle (ResolvedColumn has no sort() method) - Fix sort indicator (ResolvedColumn has no sorted property) - Add table-fixed class so percentage column widths are respected
Vue re-renders destroy the handle div mid-drag, losing pointer capture. Move pointermove/pointerup to document listeners that persist through re-renders. Cache table ref at drag start. Also widen handle from 1px to 8px for easier grabbing.
- useClickOutside cancels edit when clicking outside the active cell - useHotkey handles Escape globally (works even with input focused) - Both scoped via useToggleScope to only activate while editing
… columns - Pin button appears on hover, toggles between pinned left and unpinned - Resize handle hidden on last column in each pin region (no neighbor = no-op) - canResize() checks per-region position instead of global column index
…iators - Column pinning: project name pinned left with sticky positioning - Column resizing: drag handles between headers - Cell editing: click project name or budget to edit inline - Row reordering: up/down chevrons to move rows - Uses useClickOutside, useHotkey, useToggleScope, useEventListener
Categories: initialization, column layout, cell editing, row ordering, row spanning, computed access, full pipeline, adapter comparison. Tests 1K and 10K datasets per benchmark standards.
Change validate return type from `boolean | string` to `string | true` to match the FormValidationRule convention used by createForm and createValidation. Removes the dead `isString` fallback branch since non-true results are guaranteed to be strings by the type system.
- Column collection and ordering via createRegistry (replaces manual Maps) - Pin state via createGroup tri-state (selected=left, mixed=right) - Matches how createDataTable uses createGroup for sort direction - Align validate signature with v0 Rule pattern (string | true) - Sizes remain as shallowReactive Map (too hot for registry tickets)
…rison - Add search/sort pipeline benchmarks matching createDataTable names - Add 10K dataset variants for computed access - Separate "Search + sort + paginate" (comparable) from "Search + sort + paginate + layout" (grid-specific) - Use deterministic data generation in both files - Remove adapter comparison with `as any` casts
- Create one Intl.Collator per sort computation instead of per comparison (eliminates ~130K Collator instantiations for 10K item sort) - Pre-split nested key paths once outside comparator - Fast-path for simple keys (no dots) skips getNestedValue entirely - Layout resize() reads cached pinned.value instead of re-resolving
createDataTable on master renamed ServerAdapter -> ServerDataTableAdapter, ServerAdapterOptions -> ServerDataTableAdapterOptions, and dropped the separate DataTableAdapterInterface in favor of using the DataTableAdapter abstract class as both value and type. Update the rebased createDataGrid to use the new names, run lint:fix to reorder Grid module imports above Utilities, and apply the new sibling-blank-line rule to the basic example.
5fd3a60 to
6630729
Compare
- layout.ts: pass reactive: true to internal createRegistry so columns/pinned refs propagate through to template watchers and v-for renders. Without reactive: true, template effects subscribed to layout.columns.value never re-ran on pin/move because the registry's values() snapshot was non-reactive (ff4d6c4 codified this convention for plugin-shaped factories exposing iteration to templates). - layout.ts: replace raw === undefined checks with isUndefined() guard from #v0/utilities, matching the project-wide sweep on master. - index.ts: add @example JSDoc blocks to createDataGrid, createDataGridContext, and useDataGrid; add @see link in the module block to surface the docs page in the rendered API reference.
Page structure now matches the peer convention used by createDataTable, createKanban, and createSortable: Usage → Architecture (new) → Adapters → Recipes (renamed from Features) → Reactivity → Examples. The new Architecture section explains how the composable layers createDataTable with grid-specific modules (layout, editing, ordering, spanning) and includes a mermaid hierarchy diagram. Examples polish: - spanning: added header bar, status summary chips, member avatars with hashed colors, centered day columns, hover row tint, footer summary - editing: added inventory stats bar (item count, total value, low stock badge), edited-cell count chip, clear-log button, editable/sortable icon hints in headers, edited-cell pulse indicator, empty-state hint - pinned: added market overview header with gainers/losers/volume, filter input + reset moved to right, three-state pin button cycle (none → left → right → none), footer with pin region breakdown
Page section order moved to: Usage → Architecture → Reactivity → Adapters → Examples → Recipes. Reactivity now sits next to Architecture so the surface inventory is visible before adapter strategy, and Recipes are after Examples so readers get full visual demos first and reach for code-only snippets when extending. All four examples (basic, pinned, editing, spanning) had no per-column minSize, so the composable's 2% default applied — verified in Playwright that dragging a resize handle could crush a neighboring column to ~18px (clear readability bug). Set minSize per column based on column content + pin-icon affordance so resizes now stop at a sensible floor and pinned columns render their full title. Also widened the pinned example's min-w from 900 to 1100px and rebalanced column sizes so the Ticker column has room for "GOOGL" plus the pin icon, and Sector (right-pinned) no longer clips on narrow viewports.
Spanning table had overflow-hidden on the container, so when the 7-column grid exceeded the docs container width the day columns got clipped instead of scrolling. Switch container to overflow-x-auto and pin the table to min-w-[720px] so the schedule is always readable end-to-end.
layout.resize() picked the next column within the same region (left, scrollable, or right), so the trailing column of any region had no neighbor and silently no-op'd. Pinned columns sitting alone in their region were effectively un-resizable. Resize now walks the columns in display order so any column except the last picks up its right-hand neighbor, including across region boundaries. In the pinned example: - canResize() drops its region-walk and uses display order to match. - The table now ships overflow:visible inline so position:sticky cells can react to scroll on [data-grid]. Without it, computed overflow on the table fell back to hidden and made the table its own scroll containing block — pinned columns scrolled away with the rest of the row. Spanning example: day-column status labels collapsed to colored dots with title tooltips so the chip width stops dominating the column.
createDataTable predated the registry-pattern convention used by every
other collection composable in v0 (createRegistry, createModel,
createSelection, createSortable, createKanban, …). The items option
put row identity outside the composable's registry, forced
MaybeRefOrGetter plumbing, and split ownership between the caller's
array and the internal state.
Drop the items option. createDataTable now owns an internal
createRegistry({ events: true, reactive: true }) and spreads its
surface into the returned context, so consumers register rows via the
inherited register / onboard / unregister / clear / upsert methods.
Row identity comes from the ticket id at register time. The itemValue
option goes away with it; consumers pass id: value.id when they want a
domain-stable identifier (selection, expansion, grouping all key off
the ticket id, which equals the caller-supplied id by convention).
Adapters are unchanged — they still receive context.items / allItems
refs that project ticket.value through the pipeline.
Migration shape:
// before
createDataTable({ items: data, ... })
// after
const table = createDataTable({ ... })
table.onboard(data.map(value => ({ id: value.id, value })))
A reactive items source is no longer auto-synced; callers drive
register / unregister, or watch and do clear() + onboard() (see the
ServerDataTableAdapter prose for the canonical server pattern).
Codified in PHILOSOPHY §6.10 "Collection composables: no items option"
plus a checklist line in composables.md and new-feature-checklist.md.
The PHILOSOPHY entry lists createDataGrid as also compliant in
anticipation of the follow-up grid PR that adopts the same surface
inherited via spread.
Co-authored-by: dev-table <noreply@anthropic.com>
Co-authored-by: docs-table <noreply@anthropic.com>
Co-authored-by: philosophy <noreply@anthropic.com>
createDataGrid spreads createDataTable's context, so it inherits the
new register / onboard / unregister / clear surface as soon as the
table refactor lands. Drop the grid's own items / itemValue options
to match — the surface now lives on the registry the table exposes.
Internals:
- DataGridContext extends DataTableContext (no second registry).
- lookup() resolves via table.get(row)?.value instead of scanning
allItems.
- Explicit get size () { return table.size } after the ...table spread
since spread snapshots the registry's size getter as a literal.
- ClientGridAdapter / VirtualGridAdapter itemKey defaults to 'id' so
callers no longer need to supply it.
All four examples (BasicGrid, PinnedGrid, EditableGrid, SpanningGrid)
and the docs page move to:
const grid = createDataGrid({ ... })
grid.onboard(rows.map(value => ({ id: value.id, value })))
The grid's own rows: { order, move, reset } namespace (post-sort row
ordering) is unchanged — that's a separate ID[] state, not row
identity.
Co-authored-by: dev-grid <noreply@anthropic.com>
Early commits on this branch landed before master picked up the filter-aware DocsFaq / DocsFaqItem wrappers and standardised the section heading on '## FAQ'. The grid PR shouldn't carry those reverts. Restore the master version of: - apps/docs/src/components/docs/DocsFaq.vue - apps/docs/src/components/docs/DocsFaqItem.vue - 10 docs pages where the '## FAQ' heading had drifted to '## Questions' or '## Frequently Asked Questions'
Per style.md / PHILOSOPHY §3.3 single-word preference. Keeps the explicit on<Action>, is*, and can* conventions; trims everything else: - editing: formatPrice → money, totalValue → total, lowStock → low - pinned: formatVolume → volume, formatCap → cap, numericKeys → numeric, stats.gainers/losers/volume → up/down/vol, pinnedSummary → summary - spanning: dotClass → dot, avatarColor → tint, dayColumns → days - basic: progressColor → tint
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
createDataGrid, a headless data grid composable layered on top ofcreateDataTable. Adds column layout (sizing, pinning, resizing, reordering), cell editing with validation, row ordering, row spanning, and adapter-based virtualization.What's included
createDataGrid composable
moveandresetClientGridAdapter,ServerGridAdapter,VirtualGridAdapter; row ordering inserts between sort and paginationcreateDataGridContext,createDataGridPlugin-style helpers,useDataGridDocs
composables/data/create-data-gridwith frontmatter, intro, usage, examplesapps/docs/src/examples/composables/create-data-grid/(basic, editing, pinned, spanning)Master-alignment touch-ups (this rebase)
DataTableAdapterInterfacereferences toDataTableAdapter; updatedServerAdapter/ServerAdapterOptionsre-exports toServerDataTableAdapter/ServerDataTableAdapterOptionsafter the master adapter rename.reactive: trueto the layout registry — template-iterablepinned/columnsrefs were silently non-reactive without it (fix follows the convention codified in ff4d6c4).=== undefinedchecks withisUndefined()guard.@exampleJSDoc blocks tocreateDataGrid,createDataGridContext, anduseDataGridso they surface in<DocsApi />.CI
codecov/patchreports 88.97% on the new files —VirtualGridAdapter(16 lines) accounts for most of the gap; a follow-up will add coverage when the virtual surface lands its example.docs-checkfails — pre-existing global link-checker flakiness (npmjs.com 403s, W3C 404s), affecting every open PR.