From 8b1b85454ca66b01a32354d59bdce987f54e9d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Wed, 23 Sep 2026 01:36:37 +0200 Subject: [PATCH] Name the column of any table, and keep Column checked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 60dafd9 makes `CellArgs` default to `any`, so that a column whose Cell asks for args fits a `Column` parameter. It also makes the Cell of every column written by hand unchecked: `Column['Cell']` is `ComponentLike`, which takes any args and requires none, not even `@row` and `@column`. On 4.0.0 it was `ComponentLike>`. The two readings of `Column` — "a column I will render" and "a column I only read" — cannot both hold. This gives the second one its own name, so the first keeps its checks. `AnyColumn` and `AnyTable` are what the plugin helpers, `meta.forColumn`, the header modifier and the resize handle take. The default goes back to `unknown`. Cowritten by Claude --- .../-private/-type-tests/any-column.test.ts | 58 +++++++++++++++++++ .../-private/-type-tests/cell-args.test.ts | 13 ++++- table/src/-private/any.ts | 32 ++++++++++ table/src/-private/column.ts | 2 +- table/src/-private/table.ts | 9 +-- table/src/index.ts | 1 + table/src/plugins/-private/base.ts | 15 ++--- .../src/plugins/column-reordering/helpers.ts | 21 ++++--- table/src/plugins/column-resizing/handle.ts | 8 ++- table/src/plugins/column-resizing/helpers.ts | 11 ++-- .../src/plugins/column-visibility/helpers.ts | 9 +-- table/src/plugins/data-sorting/helpers.ts | 17 +++--- table/src/plugins/sticky-columns/helpers.ts | 7 ++- 13 files changed, 156 insertions(+), 47 deletions(-) create mode 100644 table/src/-private/-type-tests/any-column.test.ts create mode 100644 table/src/-private/any.ts diff --git a/table/src/-private/-type-tests/any-column.test.ts b/table/src/-private/-type-tests/any-column.test.ts new file mode 100644 index 0000000..c546494 --- /dev/null +++ b/table/src/-private/-type-tests/any-column.test.ts @@ -0,0 +1,58 @@ +import { expectTypeOf } from 'expect-type'; + +import { headlessTable } from '../../index.ts'; +import { isVisible, hide } from '../../plugins/column-visibility/index.ts'; +import { sort } from '../../plugins/data-sorting/index.ts'; + +import type { AnyColumn, AnyTable, CellContext, Column } from '../../index.ts'; +import type { ComponentLike } from '@glint/template'; + +interface Person { + name: string; +} +declare const people: Person[]; + +declare const GroupedCell: ComponentLike< + CellContext & { groupBy: 'day' | 'week' } +>; + +const table = headlessTable( + {}, + { + columns: () => [{ key: 'name', Cell: GroupedCell, meta: { width: 120 } }], + data: () => people, + }, +); + +///////////////////////////////////////////// +// `Column` keeps a Cell that is checked: a Cell of a column written by hand +// still takes `@row` and `@column`, and nothing else. +expectTypeOf['Cell']>>().toEqualTypeOf< + ComponentLike> +>(); + +///////////////////////////////////////////// +// `AnyColumn` is the column of any table: what its Cell takes is not known here. +expectTypeOf['Cell']>>().toEqualTypeOf< + ComponentLike +>(); + +// Code that reads a column takes any column, whatever its Cells ask for. +function keyOf(column: AnyColumn) { + return column.key; +} +function widthOf(column: AnyColumn) { + return column.meta?.width; +} +function columnCount(table: AnyTable) { + return table.columns.length; +} + +keyOf(table.columns[0]!); +widthOf(table.columns[0]!); +columnCount(table); + +// and so do the plugins +isVisible(table.columns[0]!); +hide(table.columns[0]!); +sort(table.columns[0]!); diff --git a/table/src/-private/-type-tests/cell-args.test.ts b/table/src/-private/-type-tests/cell-args.test.ts index 31e9c5c..f78ff3f 100644 --- a/table/src/-private/-type-tests/cell-args.test.ts +++ b/table/src/-private/-type-tests/cell-args.test.ts @@ -13,7 +13,14 @@ import { import { DataSorting, sort } from '../../plugins/data-sorting/index.ts'; import { meta } from '../../plugins/index.ts'; -import type { CellContext, Column, ColumnConfig, Table } from '../../index.ts'; +import type { + AnyColumn, + AnyTable, + CellContext, + Column, + ColumnConfig, + Table, +} from '../../index.ts'; import type { ComponentLike } from '@glint/template'; interface Person { @@ -168,10 +175,10 @@ expectTypeOf>().toEqualTypeOf< ///////////////////////////////////////////// // A column whose Cell takes args fits code that knows nothing about them -function takesAnyColumn(column: Column) { +function takesAnyColumn(column: AnyColumn) { return column.key; } -function takesAnyTable(table: Table) { +function takesAnyTable(table: AnyTable) { return table.columns.length; } takesAnyColumn(grouped.columns[0]!); diff --git a/table/src/-private/any.ts b/table/src/-private/any.ts new file mode 100644 index 0000000..cc075fe --- /dev/null +++ b/table/src/-private/any.ts @@ -0,0 +1,32 @@ +import type { Column } from './column.ts'; +import type { Table } from './table.ts'; + +/** + * A column of any table, for code that reads a column without rendering its Cell. + * + * The args of a `Cell` are the args of a function, so a column that carries them + * is not a column that carries none. Code that only reads `key`, `name` or `meta` + * would have to name those args and then ignore them. This says "any args" instead. + * + * ```ts + * function widthOf(column: AnyColumn) { + * return column.meta?.width; + * } + * ``` + */ +export type AnyColumn = Column< + T, + ColumnMeta, + any, + any +>; + +/** + * A table of any shape, the counterpart of `AnyColumn`. + */ +export type AnyTable = Table< + T, + ColumnMeta, + any, + any +>; diff --git a/table/src/-private/column.ts b/table/src/-private/column.ts index 92c9884..a591936 100644 --- a/table/src/-private/column.ts +++ b/table/src/-private/column.ts @@ -24,7 +24,7 @@ export class Column< T = unknown, ColumnMeta = unknown, Meta = unknown, - CellArgs = any, + CellArgs = unknown, > { get Cell(): | ComponentLike & CellArgs> diff --git a/table/src/-private/table.ts b/table/src/-private/table.ts index 58e90ee..79d8794 100644 --- a/table/src/-private/table.ts +++ b/table/src/-private/table.ts @@ -19,6 +19,7 @@ import { composeFunctionModifiers } from './utils.ts'; import type { BasePlugin, Plugin } from '../plugins/index.ts'; import type { Class } from './private-types.ts'; +import type { AnyColumn } from './any.ts'; import type { ColumnConfig, Destructor, TableConfig } from './interfaces'; import type Owner from '@ember/owner'; import { compatOwner } from './ember-compat.ts'; @@ -55,7 +56,7 @@ export interface Table< DataType = unknown, ColumnMeta = unknown, Meta = unknown, - CellArgs = any, + CellArgs = unknown, > { /** * @private @@ -87,7 +88,7 @@ export class Table< DataType = unknown, ColumnMeta = unknown, Meta = unknown, - CellArgs = any, + CellArgs = unknown, > { /** * @private @@ -198,7 +199,7 @@ export class Table< }>; columnHeader: FunctionBasedModifier<{ Element: HTMLElement; - Args: { Positional: [Column]; Named: EmptyObject }; + Args: { Positional: [AnyColumn]; Named: EmptyObject }; }>; row: FunctionBasedModifier<{ Element: HTMLElement; @@ -225,7 +226,7 @@ export class Table< // With curried+composed modifiers, only the plugin's headerModifier // that has tracked changes would run, leaving the other modifiers alone columnHeader: modifier( - (element: HTMLElement, [column]: [Column]): Destructor => { + (element: HTMLElement, [column]: [AnyColumn]): Destructor => { const modifiers = this.plugins.map( (plugin) => plugin.headerCellModifier, ); diff --git a/table/src/index.ts b/table/src/index.ts index af8b79b..475894c 100644 --- a/table/src/index.ts +++ b/table/src/index.ts @@ -11,6 +11,7 @@ export { deserializeSorts, serializeSorts } from './utils.ts'; * Public Types *******************************/ export type { Column } from './-private/column.ts'; +export type { AnyColumn, AnyTable } from './-private/any.ts'; export type { CellContext, ColumnConfig, diff --git a/table/src/plugins/-private/base.ts b/table/src/plugins/-private/base.ts index e0dec7a..bf2bf36 100644 --- a/table/src/plugins/-private/base.ts +++ b/table/src/plugins/-private/base.ts @@ -9,6 +9,7 @@ import { import { normalizePluginsConfig } from './utils.ts'; import type { Table } from '../../-private/table.ts'; +import type { AnyColumn, AnyTable } from '../../-private/any.ts'; import type { ColumnReordering } from '../column-reordering'; import type { ColumnVisibility } from '../column-visibility'; import type { Class, Constructor } from '../../-private/private-types.ts'; @@ -165,7 +166,7 @@ export const preferences = { * works, they can access this data, too. No security guaranteed) */ forColumn

, Data = unknown>( - column: Column, + column: AnyColumn, klass: Class

, ) { return { @@ -328,7 +329,7 @@ function columnsFor< } function resolveColumns( - table: Table, + table: AnyTable, requester?: Plugin, ): Column[] { assert( @@ -575,7 +576,7 @@ export const meta = { * Note that this requires the column instance to exist on the table. */ forColumn

, Data = unknown>( - column: Column, + column: AnyColumn, klass: Class

, ): ColumnMetaFor> { const columnMeta = column.table[COLUMN_META_KEY]; @@ -686,7 +687,7 @@ export const meta = { * For example, multiple column-focused plugins may care about width or visibility */ forColumn( - column: Column, + column: AnyColumn, featureName: FeatureName, ): ColumnFeatures[FeatureName] { const { plugins } = column.table; @@ -793,7 +794,7 @@ export const options = { }, forColumn

, Data = unknown>( - column: Column, + column: AnyColumn, klass: Class

, ): Partial>> { const tuple = column.config.pluginOptions?.find( @@ -819,13 +820,13 @@ function getPluginInstance( mapKey: Class, factory: () => Instance, ): Instance; -function getPluginInstance | Row, Instance>( +function getPluginInstance | Row, Instance>( map: WeakMap, Instance>>, rootKey: RootKey, mapKey: Class, factory: () => Instance, ): Instance; -function getPluginInstance | Row, Instance>( +function getPluginInstance | Row, Instance>( ...args: | [FactoryMap, Class, () => Instance] | [ diff --git a/table/src/plugins/column-reordering/helpers.ts b/table/src/plugins/column-reordering/helpers.ts index da9d675..50c7f23 100644 --- a/table/src/plugins/column-reordering/helpers.ts +++ b/table/src/plugins/column-reordering/helpers.ts @@ -3,26 +3,29 @@ import { ColumnReordering } from './plugin.ts'; import type { ColumnOrder, TableMeta } from './plugin.ts'; import type { Column, Table } from '../../index.ts'; +import type { AnyColumn, AnyTable } from '../../-private/any.ts'; /** * Move the column one position to the left. * If the column is first, nothing will happen. */ -export const moveLeft = (column: Column): void => - meta.forColumn(column, ColumnReordering).moveLeft(); +export const moveLeft = ( + column: AnyColumn, +): void => meta.forColumn(column, ColumnReordering).moveLeft(); /** * Move the column one position to the right. * If the column is last, nothing will happen. */ -export const moveRight = (column: Column): void => - meta.forColumn(column, ColumnReordering).moveRight(); +export const moveRight = ( + column: AnyColumn, +): void => meta.forColumn(column, ColumnReordering).moveRight(); /** * Override all column positions at once. */ export const setColumnOrder = ( - table: Table, + table: AnyTable, order: ColumnOrder, ): void => { // Note: The meta.forTable API doesn't preserve the DataType generic from the table parameter. @@ -38,14 +41,14 @@ export const setColumnOrder = ( * Ask if the column cannot move to the left */ export const cannotMoveLeft = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, ColumnReordering).cannotMoveLeft; /** * Ask if the column cannot move to the right */ export const cannotMoveRight = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, ColumnReordering).cannotMoveRight; /** @@ -53,7 +56,7 @@ export const cannotMoveRight = ( * (If your plugin doesn't expose `canMoveLeft`, use `!cannotMoveLeft`.) */ export const canMoveLeft = ( - column: Column, + column: AnyColumn, ): boolean => // Prefer this if available: // meta.forColumn(column, ColumnReordering).canMoveLeft @@ -64,7 +67,7 @@ export const canMoveLeft = ( * (If your plugin doesn't expose `canMoveRight`, use `!cannotMoveRight`.) */ export const canMoveRight = ( - column: Column, + column: AnyColumn, ): boolean => // Prefer this if available: // meta.forColumn(column, ColumnReordering).canMoveRight diff --git a/table/src/plugins/column-resizing/handle.ts b/table/src/plugins/column-resizing/handle.ts index 5897b1c..4babf56 100644 --- a/table/src/plugins/column-resizing/handle.ts +++ b/table/src/plugins/column-resizing/handle.ts @@ -6,6 +6,8 @@ import Modifier from 'ember-modifier'; import { meta } from '../-private/base.ts'; import { ColumnResizing } from './plugin.ts'; +import type { AnyColumn } from '../../-private/any.ts'; + import type { ColumnMeta } from './plugin.ts'; import type { Column } from '../../-private/column.ts'; @@ -25,10 +27,10 @@ const waiter = buildWaiter('ColumnResizing#ResizeHandle'); */ class ResizeHandle extends Modifier<{ - Args: { Positional: [Column] }; + Args: { Positional: [AnyColumn] }; }> { declare dragHandle: HTMLElement; - declare column: Column; + declare column: AnyColumn; declare meta: ColumnMeta; // Pointer @@ -47,7 +49,7 @@ class ResizeHandle extends Modifier<{ token?: unknown; isSetup = false; - modify(element: Element, [column]: [Column]): void { + modify(element: Element, [column]: [AnyColumn]): void { this.column = column; this.meta = meta.forColumn(column, ColumnResizing); this.dragHandle = element as HTMLElement; diff --git a/table/src/plugins/column-resizing/helpers.ts b/table/src/plugins/column-resizing/helpers.ts index 9876612..c3411ef 100644 --- a/table/src/plugins/column-resizing/helpers.ts +++ b/table/src/plugins/column-resizing/helpers.ts @@ -4,13 +4,14 @@ import { meta } from '../-private/base.ts'; import { ColumnResizing } from './plugin.ts'; import type { Column } from '../../index.ts'; +import type { AnyColumn, AnyTable } from '../../-private/any.ts'; /** * Is the column resizable? * This checks if resizing is enabled for this specific column, */ export const isResizable = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, ColumnResizing).isResizable; /** @@ -24,14 +25,14 @@ export const isResizable = ( * "Is the user directly resizing this column?" */ export const isResizing = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, ColumnResizing).isResizing; /** * Does the column have room to shrink? */ export const canShrink = ( - column: Column, + column: AnyColumn, ): boolean | 0 => meta.forColumn(column, ColumnResizing).canShrink; /** @@ -42,7 +43,7 @@ export const canShrink = ( * - or if we're asking about the first column (resize handles may only be "between" columns) */ export const hasResizeHandle = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, ColumnResizing).hasResizeHandle; /** @@ -54,7 +55,7 @@ export const hasResizeHandle = ( * This utility is meant to be applied to the `style` attribute of a particular td-like element. */ export const styleStringFor = ( - column: Column, + column: AnyColumn, ): ReturnType => { const columnMeta = meta.forColumn(column, ColumnResizing); diff --git a/table/src/plugins/column-visibility/helpers.ts b/table/src/plugins/column-visibility/helpers.ts index 48b0c4c..d57a5aa 100644 --- a/table/src/plugins/column-visibility/helpers.ts +++ b/table/src/plugins/column-visibility/helpers.ts @@ -2,29 +2,30 @@ import { meta } from '../-private/base.ts'; import { ColumnVisibility } from './plugin.ts'; import type { Column } from '../../index.ts'; +import type { AnyColumn, AnyTable } from '../../-private/any.ts'; /** * Hide a column */ -export const hide = (column: Column): void => +export const hide = (column: AnyColumn): void => meta.forColumn(column, ColumnVisibility).hide(); /** * Show a column */ -export const show = (column: Column): void => +export const show = (column: AnyColumn): void => meta.forColumn(column, ColumnVisibility).show(); /** * Ask if a column is presently supposed to be visible */ export const isVisible = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, ColumnVisibility).isVisible; /** * Ask if a column is presently supposed to be hidden */ export const isHidden = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, ColumnVisibility).isHidden; diff --git a/table/src/plugins/data-sorting/helpers.ts b/table/src/plugins/data-sorting/helpers.ts index 9d02036..434a5dd 100644 --- a/table/src/plugins/data-sorting/helpers.ts +++ b/table/src/plugins/data-sorting/helpers.ts @@ -2,41 +2,42 @@ import { meta } from '../-private/base.ts'; import { Sorting } from './plugin.ts'; import type { Column } from '../../index.ts'; +import type { AnyColumn, AnyTable } from '../../-private/any.ts'; import type { SortDirection } from './types.ts'; /** * Query a specific column's current sort direction */ export const sortDirection = ( - column: Column, + column: AnyColumn, ): SortDirection => meta.forColumn(column, Sorting).sortDirection; /** * Ask if a column is sortable */ export const isSortable = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, Sorting).isSortable; /** * Ask if a column is ascending */ export const isAscending = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, Sorting).isAscending; /** * Ask if a column is sorted descending */ export const isDescending = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, Sorting).isDescending; /** * Ask if a column is not sorted */ export const isUnsorted = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, Sorting).isUnsorted; /** @@ -46,19 +47,19 @@ export const isUnsorted = ( * Ascending => None => Descending * ⬑ ---------- <= ---------- ↲ */ -export const sort = (column: Column): void => +export const sort = (column: AnyColumn): void => meta.forTable(column.table, Sorting).handleSort(column); /** * Toggle a column between descending and not unsorted states */ export const sortDescending = ( - column: Column, + column: AnyColumn, ): void => meta.forTable(column.table, Sorting).toggleDescending(column); /** * Toggle a column between ascending and not unsorted states */ export const sortAscending = ( - column: Column, + column: AnyColumn, ): void => meta.forTable(column.table, Sorting).toggleAscending(column); diff --git a/table/src/plugins/sticky-columns/helpers.ts b/table/src/plugins/sticky-columns/helpers.ts index 41f51b1..87d86de 100644 --- a/table/src/plugins/sticky-columns/helpers.ts +++ b/table/src/plugins/sticky-columns/helpers.ts @@ -4,13 +4,14 @@ import { meta } from '../-private/base.ts'; import { StickyColumns } from './plugin.ts'; import type { Column } from '../../index.ts'; +import type { AnyColumn, AnyTable } from '../../-private/any.ts'; export const isSticky = ( - column: Column, + column: AnyColumn, ): boolean => meta.forColumn(column, StickyColumns).isSticky; export const styleFor = ( - column: Column, + column: AnyColumn, ): Partial => meta.forColumn(column, StickyColumns).style; /** @@ -25,7 +26,7 @@ export const styleFor = ( * for consumers, but is a reasonable trade-off for now. */ export const styleStringFor = ( - column: Column, + column: AnyColumn, ): ReturnType => { const columnMeta = meta.forColumn(column, StickyColumns);