Skip to content

feat: createDataGrid composable#174

Open
johnleider wants to merge 38 commits into
masterfrom
feat/create-data-grid
Open

feat: createDataGrid composable#174
johnleider wants to merge 38 commits into
masterfrom
feat/create-data-grid

Conversation

@johnleider
Copy link
Copy Markdown
Member

@johnleider johnleider commented Mar 31, 2026

Summary

Adds createDataGrid, a headless data grid composable layered on top of createDataTable. Adds column layout (sizing, pinning, resizing, reordering), cell editing with validation, row ordering, row spanning, and adapter-based virtualization.

What's included

createDataGrid composable

  • Column layout — percentage-based sizing (Splitter-compatible), tri-region pinning (left / scrollable / right), delta-based resizing, reordering via internal registry + group
  • Cell editing — edit/commit/cancel lifecycle, per-column validation, dirty tracking
  • Row ordering — ID-based order state with move and reset
  • Row spanning — computed span map with hidden-cell tracking
  • Grid adaptersClientGridAdapter, ServerGridAdapter, VirtualGridAdapter; row ordering inserts between sort and pagination
  • Trinity patterncreateDataGridContext, createDataGridPlugin-style helpers, useDataGrid

Docs

  • Reference page at composables/data/create-data-grid with frontmatter, intro, usage, examples
  • Four example apps under apps/docs/src/examples/composables/create-data-grid/ (basic, editing, pinned, spanning)

Master-alignment touch-ups (this rebase)

  • Rebased onto current master (was diverged with conflicts).
  • Renamed DataTableAdapterInterface references to DataTableAdapter; updated ServerAdapter / ServerAdapterOptions re-exports to ServerDataTableAdapter / ServerDataTableAdapterOptions after the master adapter rename.
  • Added reactive: true to the layout registry — template-iterable pinned / columns refs were silently non-reactive without it (fix follows the convention codified in ff4d6c4).
  • Replaced raw === undefined checks with isUndefined() guard.
  • Added @example JSDoc blocks to createDataGrid, createDataGridContext, and useDataGrid so they surface in <DocsApi />.

CI

  • ✅ lint, typecheck, test (5873 / 5891 passing, 18 skipped), build, repo-integrity, CodeQL, pkg-pr-new
  • ⚠️ codecov/patch reports 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-check fails — pre-existing global link-checker flakiness (npmjs.com 403s, W3C 404s), affecting every open PR.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Mar 31, 2026

Open in StackBlitz

commit: b9f3d65

@johnleider johnleider marked this pull request as draft March 31, 2026 14:23
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
@johnleider johnleider force-pushed the feat/create-data-grid branch from 08d1b2b to 5fd3a60 Compare April 12, 2026 01:11
johnleider added 25 commits May 12, 2026 09:01
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.
@johnleider johnleider force-pushed the feat/create-data-grid branch from 5fd3a60 to 6630729 Compare May 12, 2026 14:10
- 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.
@johnleider johnleider marked this pull request as ready for review May 12, 2026 14:31
johnleider and others added 6 commits May 12, 2026 09:52
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant