Skip to content

Commit 0ff07b2

Browse files
julien-fclaude
andauthored
feat: replace defaultVisibleColumns/defaultPageSize with initialViewState (#21)
DataTableOptions supported construction-time defaults for visible columns and page size, but not sort/filters/grouping — so a consumer wanting a default sort had to call setViewState() as a separate priming step right after construction, and resetView had no way to restore that default sort since it wasn't a construction-time option the library knew about. Replace defaultVisibleColumns/defaultPageSize with a single initialViewState: TableViewState option, covering every view concern (visible columns, column order, sort, filters, grouping, page/pageSize, search) in one shape that resolveViewState now treats uniformly as the fallback for any field a view omits. Since resetView/setViewState({}) and construction-time seeding both go through resolveViewState, initialViewState is now simultaneously "what a fresh table starts at" and "what a reset restores" — closing the whole class of bug described in the issue. Grouping a column via initialViewState.groupBy with no matching sorts entry gets one auto-inserted (syncGroupSorts), mirroring what interactive group.toggle already does, so a deterministic nesting order doesn't require also hand-writing the matching sort. This only applies when groupBy itself falls back to initialViewState (construction, reset, or a setViewState call that leaves view.groupBy unset) — an explicit view.groupBy (e.g. restoring a stored/shared view) is exempt, preserving setViewState's existing contract. getViewState()'s own omission logic (buildViewStateSnapshot) now deep- compares every field against what resolveViewState({}, columns, initialViewState) resolves to, rather than checking "is this field non-empty" — this also fixes a latent bug where visibleCols was compared against every column instead of the actual construction default. Breaking change (pre-1.0, no compat shim, same precedent as the Namespaced TableState migration): defaultVisibleColumns/defaultPageSize are removed from every adapter's options. Updated all four adapters (react/vue/solid/vanilla), their READMEs, CLAUDE.md, and all four demo apps accordingly, and added core/adapter test coverage for the new option and the resetView-restores-default-sort scenario. Closes #20 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 2272189 commit 0ff07b2

35 files changed

Lines changed: 887 additions & 361 deletions

CLAUDE.md

Lines changed: 11 additions & 9 deletions
Large diffs are not rendered by default.

demo/react/src/App.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ const TIER_COLORS = {
287287
}
288288

289289
const COLUMNS: ColumnDef<Employee>[] = [
290-
// sortable: false + filterable: false — no sort/filter UI; hidden by default via defaultVisibleColumns
290+
// sortable: false + filterable: false — no sort/filter UI; hidden by default via initialViewState.visibleCols
291291
{ key: 'id', label: 'ID', type: 'number', width: 60, sortable: false, filterable: false },
292292
{ key: 'name', label: 'Name', type: 'string', width: 160 },
293293
// groupable + render: JSX cell; renderFilterLabel: custom chip in filter dropdown
@@ -444,7 +444,7 @@ const DEFAULT_VISIBLE = [
444444
]
445445

446446
// Row selection/click only need a couple of columns to make their point — a narrower
447-
// defaultVisibleColumns keeps each section visually distinct instead of repeating the same
447+
// initialViewState.visibleCols keeps each section visually distinct instead of repeating the same
448448
// 10-column table. The persisted table keeps more, since reordering needs several columns to
449449
// be meaningful.
450450
const SELECTION_VISIBLE = ['name', 'department', 'salary']
@@ -627,9 +627,8 @@ function EmployeeCards() {
627627
// sync between them.
628628
function PersistedTable({ labels }: { labels?: Partial<DataTableLabels> }) {
629629
const table = useTableState(SAMPLE_DATA, COLUMNS, {
630-
defaultVisibleColumns: PERSISTED_VISIBLE,
630+
initialViewState: { visibleCols: PERSISTED_VISIBLE, pageSize: 5 },
631631
labels,
632-
defaultPageSize: 5,
633632
})
634633
const { reset } = usePersistence(table, VIEW_KEYS.persisted)
635634
return (
@@ -645,9 +644,8 @@ function PersistedTable({ labels }: { labels?: Partial<DataTableLabels> }) {
645644
// resetView — nothing about the table's own features changes.
646645
function FullTable({ labels }: { labels?: Partial<DataTableLabels> }) {
647646
const table = useTableState(SAMPLE_DATA, COLUMNS, {
648-
defaultVisibleColumns: DEFAULT_VISIBLE,
647+
initialViewState: { visibleCols: DEFAULT_VISIBLE, pageSize: 5 },
649648
labels,
650-
defaultPageSize: 5,
651649
})
652650
usePersistedView(table, VIEW_KEYS.full.storageKey)
653651
useUrlView(table, { paramName: VIEW_KEYS.full.paramName })
@@ -667,9 +665,8 @@ function SelectionTable({
667665
onSelectionChange: (rows: Employee[]) => void
668666
}) {
669667
const table = useTableState(SAMPLE_DATA, COLUMNS, {
670-
defaultVisibleColumns: SELECTION_VISIBLE,
668+
initialViewState: { visibleCols: SELECTION_VISIBLE, pageSize: 5 },
671669
labels,
672-
defaultPageSize: 5,
673670
})
674671
usePersistedView(table, VIEW_KEYS.selection.storageKey)
675672
useUrlView(table, { paramName: VIEW_KEYS.selection.paramName })
@@ -696,9 +693,8 @@ function ClickTable({
696693
onRowClick: (row: Employee) => void
697694
}) {
698695
const table = useTableState(SAMPLE_DATA, COLUMNS, {
699-
defaultVisibleColumns: CLICK_VISIBLE,
696+
initialViewState: { visibleCols: CLICK_VISIBLE, pageSize: 5 },
700697
labels,
701-
defaultPageSize: 5,
702698
})
703699
usePersistedView(table, VIEW_KEYS.click.storageKey)
704700
useUrlView(table, { paramName: VIEW_KEYS.click.paramName })
@@ -719,7 +715,7 @@ function ClickTable({
719715
// No `labels` prop — matches the huge-dataset table's pre-existing behavior of always using the
720716
// default English labels regardless of the page's locale switcher.
721717
function HugeTable() {
722-
const table = useTableState(HUGE_DATA, HUGE_COLUMNS, { defaultPageSize: 100 })
718+
const table = useTableState(HUGE_DATA, HUGE_COLUMNS, { initialViewState: { pageSize: 100 } })
723719
usePersistedView(table, VIEW_KEYS.huge.storageKey)
724720
useUrlView(table, { paramName: VIEW_KEYS.huge.paramName })
725721
return (

demo/solid/src/App.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ function muted(text: string): Node {
296296
}
297297

298298
const COLUMNS: ColumnDef<Employee>[] = [
299-
// sortable: false + filterable: false — no sort/filter UI; hidden by default via defaultVisibleColumns
299+
// sortable: false + filterable: false — no sort/filter UI; hidden by default via initialViewState.visibleCols
300300
{ key: 'id', label: 'ID', type: 'number', width: 60, sortable: false, filterable: false },
301301
{ key: 'name', label: 'Name', type: 'string', width: 160 },
302302
// groupable + render: a real DOM node badge (this package's ColumnDef.render returns a Node, not
@@ -444,7 +444,7 @@ const DEFAULT_VISIBLE = [
444444
]
445445

446446
// Row selection/click only need a couple of columns to make their point — a narrower
447-
// defaultVisibleColumns keeps each section visually distinct instead of repeating the same
447+
// initialViewState.visibleCols keeps each section visually distinct instead of repeating the same
448448
// 10-column table. The persisted table keeps more, since reordering needs several columns to
449449
// be meaningful.
450450
const SELECTION_VISIBLE = ['name', 'department', 'salary']
@@ -643,9 +643,8 @@ function PersistedTable(props: { labels?: Partial<DataTableLabels> }) {
643643
// options passed as an accessor (rather than a plain object) so `labels` stays live — see the
644644
// "locale switcher" note in App() below.
645645
const table = createTableState(SAMPLE_DATA, COLUMNS, () => ({
646-
defaultVisibleColumns: PERSISTED_VISIBLE,
646+
initialViewState: { visibleCols: PERSISTED_VISIBLE, pageSize: 5 },
647647
labels: props.labels,
648-
defaultPageSize: 5,
649648
}))
650649
const { reset } = usePersistence(table, VIEW_KEYS.persisted)
651650
return (
@@ -661,9 +660,8 @@ function PersistedTable(props: { labels?: Partial<DataTableLabels> }) {
661660
// usePersistedView/useUrlView/resetView — nothing about the table's own features changes.
662661
function FullTable(props: { labels?: Partial<DataTableLabels> }) {
663662
const table = createTableState(SAMPLE_DATA, COLUMNS, () => ({
664-
defaultVisibleColumns: DEFAULT_VISIBLE,
663+
initialViewState: { visibleCols: DEFAULT_VISIBLE, pageSize: 5 },
665664
labels: props.labels,
666-
defaultPageSize: 5,
667665
}))
668666
usePersistedView(table, VIEW_KEYS.full.storageKey)
669667
useUrlView(table, { paramName: VIEW_KEYS.full.paramName })
@@ -680,9 +678,8 @@ function SelectionTable(props: {
680678
onSelectionChange: (rows: Employee[]) => void
681679
}) {
682680
const table = createTableState(SAMPLE_DATA, COLUMNS, () => ({
683-
defaultVisibleColumns: SELECTION_VISIBLE,
681+
initialViewState: { visibleCols: SELECTION_VISIBLE, pageSize: 5 },
684682
labels: props.labels,
685-
defaultPageSize: 5,
686683
}))
687684
usePersistedView(table, VIEW_KEYS.selection.storageKey)
688685
useUrlView(table, { paramName: VIEW_KEYS.selection.paramName })
@@ -704,9 +701,8 @@ function ClickTable(props: {
704701
onRowClick: (row: Employee) => void
705702
}) {
706703
const table = createTableState(SAMPLE_DATA, COLUMNS, () => ({
707-
defaultVisibleColumns: CLICK_VISIBLE,
704+
initialViewState: { visibleCols: CLICK_VISIBLE, pageSize: 5 },
708705
labels: props.labels,
709-
defaultPageSize: 5,
710706
}))
711707
usePersistedView(table, VIEW_KEYS.click.storageKey)
712708
useUrlView(table, { paramName: VIEW_KEYS.click.paramName })
@@ -721,7 +717,7 @@ function ClickTable(props: {
721717
// No `labels` prop — matches the huge-dataset table's pre-existing behavior in the other demos of
722718
// always using the default English labels regardless of the page's locale switcher.
723719
function HugeTable() {
724-
const table = createTableState(HUGE_DATA, HUGE_COLUMNS, { defaultPageSize: 100 })
720+
const table = createTableState(HUGE_DATA, HUGE_COLUMNS, { initialViewState: { pageSize: 100 } })
725721
usePersistedView(table, VIEW_KEYS.huge.storageKey)
726722
useUrlView(table, { paramName: VIEW_KEYS.huge.paramName })
727723
return (

demo/vanilla/src/main.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ const DEFAULT_VISIBLE = [
424424
]
425425

426426
// Each secondary section only needs a couple of columns to make its point — a narrower
427-
// defaultVisibleColumns keeps each table visually distinct instead of repeating the same
427+
// initialViewState.visibleCols keeps each table visually distinct instead of repeating the same
428428
// 10-column table everywhere. The persisted table keeps more, since reordering needs several
429429
// columns to be meaningful.
430430
const SELECTION_VISIBLE = ['name', 'department', 'salary']
@@ -711,8 +711,7 @@ const table1 = createDataTable<Employee>(document.getElementById('table1')!, {
711711
data: SAMPLE_DATA,
712712
columns: COLUMNS,
713713
rowKey: 'id',
714-
defaultVisibleColumns: DEFAULT_VISIBLE,
715-
defaultPageSize: 5,
714+
initialViewState: { visibleCols: DEFAULT_VISIBLE, pageSize: 5 },
716715
labels: LOCALES[currentLocale],
717716
})
718717
wireViewPersistence(table1, 'full')
@@ -725,8 +724,7 @@ const table2 = createDataTable<Employee>(document.getElementById('table2')!, {
725724
data: SAMPLE_DATA,
726725
columns: COLUMNS,
727726
rowKey: 'id',
728-
defaultVisibleColumns: SELECTION_VISIBLE,
729-
defaultPageSize: 5,
727+
initialViewState: { visibleCols: SELECTION_VISIBLE, pageSize: 5 },
730728
labels: LOCALES[currentLocale],
731729
selectable: true,
732730
onSelectionChange(rows) {
@@ -753,8 +751,7 @@ const tableClick = createDataTable<Employee>(document.getElementById('table-clic
753751
data: SAMPLE_DATA,
754752
columns: COLUMNS,
755753
rowKey: 'id',
756-
defaultVisibleColumns: CLICK_VISIBLE,
757-
defaultPageSize: 5,
754+
initialViewState: { visibleCols: CLICK_VISIBLE, pageSize: 5 },
758755
labels: LOCALES[currentLocale],
759756
onRowClick(row) {
760757
clickBanner.style.display = 'block'
@@ -769,8 +766,7 @@ const tablePersist = createDataTable<Employee>(document.getElementById('table-pe
769766
data: SAMPLE_DATA,
770767
columns: COLUMNS,
771768
rowKey: 'id',
772-
defaultVisibleColumns: PERSISTED_VISIBLE,
773-
defaultPageSize: 5,
769+
initialViewState: { visibleCols: PERSISTED_VISIBLE, pageSize: 5 },
774770
labels: LOCALES[currentLocale],
775771
})
776772
wireViewPersistence(tablePersist, 'persisted')
@@ -783,7 +779,7 @@ const table3 = createDataTable<Employee>(document.getElementById('table3')!, {
783779
data: dynamicData,
784780
columns: COLUMNS,
785781
rowKey: 'id',
786-
defaultVisibleColumns: DYNAMIC_VISIBLE,
782+
initialViewState: { visibleCols: DYNAMIC_VISIBLE },
787783
labels: LOCALES[currentLocale],
788784
})
789785
wireViewPersistence(table3, 'dynamic')
@@ -819,7 +815,7 @@ const tableHuge = createDataTable(document.getElementById('table-huge')!, {
819815
data: HUGE_DATA,
820816
columns: HUGE_COLUMNS,
821817
rowKey: 'id',
822-
defaultPageSize: 100,
818+
initialViewState: { pageSize: 100 },
823819
})
824820
wireViewPersistence(tableHuge, 'huge')
825821

demo/vue/src/App.vue

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ const TIER_COLORS = {
289289
}
290290
291291
const COLUMNS: ColumnDef<Employee>[] = [
292-
// sortable: false + filterable: false — no sort/filter UI; hidden by default via defaultVisibleColumns
292+
// sortable: false + filterable: false — no sort/filter UI; hidden by default via initialViewState.visibleCols
293293
{ key: 'id', label: 'ID', type: 'number', width: 60, sortable: false, filterable: false },
294294
{ key: 'name', label: 'Name', type: 'string', width: 160 },
295295
// groupable: true — slot #cell-department / #filter-department / #group-department override rendering
@@ -417,7 +417,7 @@ const DEFAULT_VISIBLE = [
417417
]
418418
419419
// Row selection/click only need a couple of columns to make their point — a narrower
420-
// defaultVisibleColumns keeps each section visually distinct instead of repeating the same
420+
// initialViewState.visibleCols keeps each section visually distinct instead of repeating the same
421421
// 10-column table. The persisted table keeps more, since reordering needs several columns to
422422
// be meaningful.
423423
const SELECTION_VISIBLE = ['name', 'department', 'salary']
@@ -540,8 +540,7 @@ useUrlView(table, { paramName: VIEW_KEYS.custom.paramName })
540540
// VIEW_KEYS.persisted object feeds all three, so its storageKey/paramName can't drift out of
541541
// sync between them.
542542
const persistedTable = useTableState(SAMPLE_DATA, COLUMNS, () => ({
543-
defaultVisibleColumns: PERSISTED_VISIBLE,
544-
defaultPageSize: 5,
543+
initialViewState: { visibleCols: PERSISTED_VISIBLE, pageSize: 5 },
545544
labels: currentLocale.value,
546545
}))
547546
const { reset: resetPersistedTable } = usePersistence(persistedTable, VIEW_KEYS.persisted)
@@ -550,32 +549,31 @@ const { reset: resetPersistedTable } = usePersistence(persistedTable, VIEW_KEYS.
550549
// wired the same way — useTableState + DataTableView instead of <DataTable> — purely so each
551550
// can also reach usePersistedView/useUrlView/resetView; nothing about their own features changes.
552551
const fullTable = useTableState(SAMPLE_DATA, COLUMNS, () => ({
553-
defaultVisibleColumns: DEFAULT_VISIBLE,
554-
defaultPageSize: 5,
552+
initialViewState: { visibleCols: DEFAULT_VISIBLE, pageSize: 5 },
555553
labels: currentLocale.value,
556554
}))
557555
usePersistedView(fullTable, VIEW_KEYS.full.storageKey)
558556
useUrlView(fullTable, { paramName: VIEW_KEYS.full.paramName })
559557
560558
const selectionTable = useTableState(SAMPLE_DATA, COLUMNS, () => ({
561-
defaultVisibleColumns: SELECTION_VISIBLE,
562-
defaultPageSize: 5,
559+
initialViewState: { visibleCols: SELECTION_VISIBLE, pageSize: 5 },
563560
labels: currentLocale.value,
564561
}))
565562
usePersistedView(selectionTable, VIEW_KEYS.selection.storageKey)
566563
useUrlView(selectionTable, { paramName: VIEW_KEYS.selection.paramName })
567564
568565
const clickTable = useTableState(SAMPLE_DATA, COLUMNS, () => ({
569-
defaultVisibleColumns: CLICK_VISIBLE,
570-
defaultPageSize: 5,
566+
initialViewState: { visibleCols: CLICK_VISIBLE, pageSize: 5 },
571567
labels: currentLocale.value,
572568
}))
573569
usePersistedView(clickTable, VIEW_KEYS.click.storageKey)
574570
useUrlView(clickTable, { paramName: VIEW_KEYS.click.paramName })
575571
576572
// No `labels` option — matches the huge-dataset table's pre-existing behavior of always using
577573
// the default English labels regardless of the page's locale switcher.
578-
const hugeTable = useTableState(HUGE_DATA, HUGE_COLUMNS, () => ({ defaultPageSize: 100 }))
574+
const hugeTable = useTableState(HUGE_DATA, HUGE_COLUMNS, () => ({
575+
initialViewState: { pageSize: 100 },
576+
}))
579577
usePersistedView(hugeTable, VIEW_KEYS.huge.storageKey)
580578
useUrlView(hugeTable, { paramName: VIEW_KEYS.huge.paramName })
581579

0 commit comments

Comments
 (0)