diff --git a/table/src/-private/-type-tests/plugins-with-cell-args.test.ts b/table/src/-private/-type-tests/plugins-with-cell-args.test.ts new file mode 100644 index 0000000..70f5c1f --- /dev/null +++ b/table/src/-private/-type-tests/plugins-with-cell-args.test.ts @@ -0,0 +1,80 @@ +import { expectTypeOf } from 'expect-type'; + +import { headlessTable } from '../../index.ts'; +import { + ColumnOrder, + moveLeft, + moveRight, + orderedColumnsFor, + setColumnOrder, +} from '../../plugins/column-reordering/index.ts'; +import { isResizable } from '../../plugins/column-resizing/index.ts'; +import { + hide, + isVisible, + show, +} from '../../plugins/column-visibility/index.ts'; +import { isAscending, sort } from '../../plugins/data-sorting/index.ts'; +import { isSticky } from '../../plugins/sticky-columns/index.ts'; +import { columns, meta } from '../../plugins/index.ts'; +import { ColumnVisibility } from '../../plugins/column-visibility/index.ts'; + +import type { CellContext, Column } from '../../index.ts'; +import type { ComponentLike } from '@glint/template'; + +interface Person { + name: string; +} +declare const people: Person[]; + +///////////////////////////////////////////// +// The plugins take a column whose Cell asks for args. +// The args sit in the args of `Cell`, so a column carrying them +// does not fit a `Column` parameter on its own. +declare const GroupedCell: ComponentLike< + CellContext & { groupBy: 'day' | 'week' } +>; + +const table = headlessTable( + {}, + { + columns: () => [ + { key: 'name', Cell: GroupedCell, meta: { align: 'left' } }, + ], + data: () => people, + }, +); + +const column = table.columns[0]!; + +expectTypeOf(isVisible(column)).toEqualTypeOf(); +expectTypeOf(isAscending(column)).toEqualTypeOf(); +expectTypeOf(isResizable(column)).toEqualTypeOf(); +expectTypeOf(isSticky(column)).toEqualTypeOf(); +hide(column); +show(column); +sort(column); +moveLeft(column); +moveRight(column); +meta.forColumn(column, ColumnVisibility); + +// an order is built from the columns of the table it orders +setColumnOrder(table, new ColumnOrder({ columns: () => [column] })); + +// the column list helpers keep the args, and the meta with them +expectTypeOf(columns.for(table)[0]!.meta?.align).toEqualTypeOf< + 'left' | undefined +>(); +expectTypeOf(orderedColumnsFor(table)[0]!.meta?.align).toEqualTypeOf< + 'left' | undefined +>(); + +// Code of your own that reads a column takes the args as a parameter. +// `Column` alone cannot accept it: the args are in the args of `Cell`, +// so a column that requires them is not a column that does not. +function takesAnyColumn( + column: Column, +) { + return column.key; +} +takesAnyColumn(column); diff --git a/table/src/-private/interfaces/plugins.ts b/table/src/-private/interfaces/plugins.ts index 8cca8d4..eb4ad31 100644 --- a/table/src/-private/interfaces/plugins.ts +++ b/table/src/-private/interfaces/plugins.ts @@ -31,7 +31,8 @@ export type PluginSubclassInstance = PluginType & { * The data passed to a plugin's column APIs */ export interface ColumnApi { - column: Column>; + // A plugin is handed whatever column the table has, whatever its Cell asks for. + column: Column, any, any, any>; table: T; } diff --git a/table/src/-private/table.ts b/table/src/-private/table.ts index 63e0faf..562681c 100644 --- a/table/src/-private/table.ts +++ b/table/src/-private/table.ts @@ -198,7 +198,10 @@ export class Table< }>; columnHeader: FunctionBasedModifier<{ Element: HTMLElement; - Args: { Positional: [Column]; Named: EmptyObject }; + Args: { + Positional: [Column]; + Named: EmptyObject; + }; }>; row: FunctionBasedModifier<{ Element: HTMLElement; @@ -225,7 +228,10 @@ 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]: [Column], + ): Destructor => { const modifiers = this.plugins.map( (plugin) => plugin.headerCellModifier, ); diff --git a/table/src/plugins/-private/base.ts b/table/src/plugins/-private/base.ts index e0dec7a..4ef12b6 100644 --- a/table/src/plugins/-private/base.ts +++ b/table/src/plugins/-private/base.ts @@ -164,10 +164,13 @@ export const preferences = { * (though, if other plugins can guess how the underlying plugin access * works, they can access this data, too. No security guaranteed) */ - forColumn

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

, - ) { + forColumn< + P extends BasePlugin, + Data = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, + >(column: Column, klass: Class

) { return { /** * delete an entry on the underlying `Map` used for this column-plugin pair @@ -574,8 +577,14 @@ export const meta = { * * Note that this requires the column instance to exist on the table. */ - forColumn

, Data = unknown>( - column: Column, + forColumn< + P extends BasePlugin, + Data = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, + >( + column: Column, klass: Class

, ): ColumnMetaFor> { const columnMeta = column.table[COLUMN_META_KEY]; @@ -685,8 +694,14 @@ export const meta = { * * For example, multiple column-focused plugins may care about width or visibility */ - forColumn( - column: Column, + forColumn< + FeatureName extends string, + Data = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, + >( + column: Column, featureName: FeatureName, ): ColumnFeatures[FeatureName] { const { plugins } = column.table; @@ -792,8 +807,14 @@ export const options = { return fn() ?? {}; }, - forColumn

, Data = unknown>( - column: Column, + forColumn< + P extends BasePlugin, + Data = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, + >( + column: Column, klass: Class

, ): Partial>> { const tuple = column.config.pluginOptions?.find( @@ -819,13 +840,19 @@ function getPluginInstance( mapKey: Class, factory: () => Instance, ): Instance; -function getPluginInstance | Row, Instance>( +function getPluginInstance< + RootKey extends Column | Row, + Instance, +>( map: WeakMap, Instance>>, rootKey: RootKey, mapKey: Class, factory: () => Instance, ): Instance; -function getPluginInstance | Row, Instance>( +function getPluginInstance< + RootKey extends Column | 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..fb54558 100644 --- a/table/src/plugins/column-reordering/helpers.ts +++ b/table/src/plugins/column-reordering/helpers.ts @@ -8,22 +8,39 @@ import type { Column, Table } from '../../index.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 = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, +): 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 = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, +): void => meta.forColumn(column, ColumnReordering).moveRight(); /** * Override all column positions at once. */ -export const setColumnOrder = ( - table: Table, - order: ColumnOrder, +export const setColumnOrder = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + table: Table, + order: ColumnOrder, ): void => { // Note: The meta.forTable API doesn't preserve the DataType generic from the table parameter. // We use a type assertion here to match the expected types. @@ -37,23 +54,38 @@ export const setColumnOrder = ( /** * Ask if the column cannot move to the left */ -export const cannotMoveLeft = ( - column: Column, +export const cannotMoveLeft = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => meta.forColumn(column, ColumnReordering).cannotMoveLeft; /** * Ask if the column cannot move to the right */ -export const cannotMoveRight = ( - column: Column, +export const cannotMoveRight = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => meta.forColumn(column, ColumnReordering).cannotMoveRight; /** * Ask if the column can move to the left * (If your plugin doesn't expose `canMoveLeft`, use `!cannotMoveLeft`.) */ -export const canMoveLeft = ( - column: Column, +export const canMoveLeft = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => // Prefer this if available: // meta.forColumn(column, ColumnReordering).canMoveLeft @@ -63,8 +95,13 @@ export const canMoveLeft = ( * Ask if the column can move to the right * (If your plugin doesn't expose `canMoveRight`, use `!cannotMoveRight`.) */ -export const canMoveRight = ( - column: Column, +export const canMoveRight = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => // Prefer this if available: // meta.forColumn(column, ColumnReordering).canMoveRight diff --git a/table/src/plugins/column-reordering/plugin.ts b/table/src/plugins/column-reordering/plugin.ts index a66c44a..8c21f39 100644 --- a/table/src/plugins/column-reordering/plugin.ts +++ b/table/src/plugins/column-reordering/plugin.ts @@ -119,7 +119,12 @@ export class TableMeta { * This is also why the order of the columns is maintained via column key */ @tracked - columnOrder: ColumnOrder = new ColumnOrder({ + columnOrder: ColumnOrder = new ColumnOrder< + DataType, + any, + any, + any + >({ columns: () => this.allColumns, visibleColumns: () => this.visibleColumns, save: this.save, @@ -148,7 +153,7 @@ export class TableMeta { /** * Using a `ColumnOrder` instance, set the order of all columns */ - setOrder = (order: ColumnOrder): void => { + setOrder = (order: ColumnOrder): void => { this.columnOrder.setAll(order.map); }; @@ -159,7 +164,7 @@ export class TableMeta { @action reset(): void { preferences.forTable(this.table, ColumnReordering).delete('order'); - this.columnOrder = new ColumnOrder({ + this.columnOrder = new ColumnOrder({ columns: () => this.allColumns, visibleColumns: () => this.visibleColumns, save: this.save, @@ -223,7 +228,12 @@ export class TableMeta { * @private * Used for keeping track of and updating column order */ -export class ColumnOrder { +export class ColumnOrder< + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +> { /** * This map will be empty until we re-order something. */ @@ -243,7 +253,7 @@ export class ColumnOrder { * - Provide `visibleColumns` to indicate which are visible * - Hidden columns maintain their position when toggled */ - columns: () => Column[]; + columns: () => Column[]; /** * Optional: Record of which columns are currently visible. * When provided, moveLeft/moveRight will skip over hidden columns. @@ -515,19 +525,19 @@ export class ColumnOrder { } @cached - get orderedColumns(): Column[] { + get orderedColumns(): Column[] { const allColumns = this.args.columns(); const columnsByKey = allColumns.reduce( (keyMap, column) => { keyMap[column.key] = column; return keyMap; }, - {} as Record>, + {} as Record>, ); // Use orderedMap which is reactive to preferences const mergedOrder = this.orderedMap; - const result: Column[] = Array.from({ + const result: Column[] = Array.from({ length: allColumns.length, }); diff --git a/table/src/plugins/column-resizing/handle.ts b/table/src/plugins/column-resizing/handle.ts index 5897b1c..af50b71 100644 --- a/table/src/plugins/column-resizing/handle.ts +++ b/table/src/plugins/column-resizing/handle.ts @@ -25,10 +25,10 @@ const waiter = buildWaiter('ColumnResizing#ResizeHandle'); */ class ResizeHandle extends Modifier<{ - Args: { Positional: [Column] }; + Args: { Positional: [Column] }; }> { declare dragHandle: HTMLElement; - declare column: Column; + declare column: Column; declare meta: ColumnMeta; // Pointer @@ -47,7 +47,7 @@ class ResizeHandle extends Modifier<{ token?: unknown; isSetup = false; - modify(element: Element, [column]: [Column]): void { + modify(element: Element, [column]: [Column]): 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..8f74d10 100644 --- a/table/src/plugins/column-resizing/helpers.ts +++ b/table/src/plugins/column-resizing/helpers.ts @@ -9,8 +9,13 @@ import type { Column } from '../../index.ts'; * Is the column resizable? * This checks if resizing is enabled for this specific column, */ -export const isResizable = ( - column: Column, +export const isResizable = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => meta.forColumn(column, ColumnResizing).isResizable; /** @@ -23,15 +28,25 @@ export const isResizable = ( * be marked as isResizing, because this is a user-scoped question: * "Is the user directly resizing this column?" */ -export const isResizing = ( - column: Column, +export const isResizing = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => meta.forColumn(column, ColumnResizing).isResizing; /** * Does the column have room to shrink? */ -export const canShrink = ( - column: Column, +export const canShrink = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean | 0 => meta.forColumn(column, ColumnResizing).canShrink; /** @@ -41,8 +56,13 @@ export const canShrink = ( * - if resizing is enabled for the whole table * - or if we're asking about the first column (resize handles may only be "between" columns) */ -export const hasResizeHandle = ( - column: Column, +export const hasResizeHandle = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => meta.forColumn(column, ColumnResizing).hasResizeHandle; /** @@ -53,8 +73,13 @@ 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, +export const styleStringFor = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): 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..0e90f09 100644 --- a/table/src/plugins/column-visibility/helpers.ts +++ b/table/src/plugins/column-visibility/helpers.ts @@ -6,25 +6,47 @@ import type { Column } from '../../index.ts'; /** * Hide a column */ -export const hide = (column: Column): void => - meta.forColumn(column, ColumnVisibility).hide(); +export const hide = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, +): void => meta.forColumn(column, ColumnVisibility).hide(); /** * Show a column */ -export const show = (column: Column): void => - meta.forColumn(column, ColumnVisibility).show(); +export const show = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, +): void => meta.forColumn(column, ColumnVisibility).show(); /** * Ask if a column is presently supposed to be visible */ -export const isVisible = ( - column: Column, +export const isVisible = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => meta.forColumn(column, ColumnVisibility).isVisible; /** * Ask if a column is presently supposed to be hidden */ -export const isHidden = ( - column: Column, +export const isHidden = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): 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..070e862 100644 --- a/table/src/plugins/data-sorting/helpers.ts +++ b/table/src/plugins/data-sorting/helpers.ts @@ -7,36 +7,61 @@ import type { SortDirection } from './types.ts'; /** * Query a specific column's current sort direction */ -export const sortDirection = ( - column: Column, +export const sortDirection = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): SortDirection => meta.forColumn(column, Sorting).sortDirection; /** * Ask if a column is sortable */ -export const isSortable = ( - column: Column, +export const isSortable = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => meta.forColumn(column, Sorting).isSortable; /** * Ask if a column is ascending */ -export const isAscending = ( - column: Column, +export const isAscending = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => meta.forColumn(column, Sorting).isAscending; /** * Ask if a column is sorted descending */ -export const isDescending = ( - column: Column, +export const isDescending = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => meta.forColumn(column, Sorting).isDescending; /** * Ask if a column is not sorted */ -export const isUnsorted = ( - column: Column, +export const isUnsorted = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => meta.forColumn(column, Sorting).isUnsorted; /** @@ -46,19 +71,35 @@ export const isUnsorted = ( * Ascending => None => Descending * ⬑ ---------- <= ---------- ↲ */ -export const sort = (column: Column): void => - meta.forTable(column.table, Sorting).handleSort(column); +export const sort = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, +): void => meta.forTable(column.table, Sorting).handleSort(column); /** * Toggle a column between descending and not unsorted states */ -export const sortDescending = ( - column: Column, +export const sortDescending = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): void => meta.forTable(column.table, Sorting).toggleDescending(column); /** * Toggle a column between ascending and not unsorted states */ -export const sortAscending = ( - column: Column, +export const sortAscending = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): void => meta.forTable(column.table, Sorting).toggleAscending(column); diff --git a/table/src/plugins/data-sorting/plugin.ts b/table/src/plugins/data-sorting/plugin.ts index fb17ed2..447750c 100644 --- a/table/src/plugins/data-sorting/plugin.ts +++ b/table/src/plugins/data-sorting/plugin.ts @@ -160,7 +160,12 @@ export class TableMeta { } @action - handleSort(column: Column): void { + handleSort< + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, + >(column: Column): void { const columnMeta = meta.forColumn(column, Sorting); if (!columnMeta.sortProperty) { @@ -187,7 +192,12 @@ export class TableMeta { } @action - toggleAscending(column: Column): void { + toggleAscending< + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, + >(column: Column): void { const columnMeta = meta.forColumn(column, Sorting); if (!columnMeta.sortProperty) { @@ -204,7 +214,12 @@ export class TableMeta { } @action - toggleDescending(column: Column): void { + toggleDescending< + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, + >(column: Column): void { const columnMeta = meta.forColumn(column, Sorting); if (!columnMeta.sortProperty) { diff --git a/table/src/plugins/sticky-columns/helpers.ts b/table/src/plugins/sticky-columns/helpers.ts index 41f51b1..216c30b 100644 --- a/table/src/plugins/sticky-columns/helpers.ts +++ b/table/src/plugins/sticky-columns/helpers.ts @@ -5,12 +5,22 @@ import { StickyColumns } from './plugin.ts'; import type { Column } from '../../index.ts'; -export const isSticky = ( - column: Column, +export const isSticky = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): boolean => meta.forColumn(column, StickyColumns).isSticky; -export const styleFor = ( - column: Column, +export const styleFor = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): Partial => meta.forColumn(column, StickyColumns).style; /** @@ -24,8 +34,13 @@ export const styleFor = ( * In the mean time, we'll need to append style strings, which is more work * for consumers, but is a reasonable trade-off for now. */ -export const styleStringFor = ( - column: Column, +export const styleStringFor = < + DataType = unknown, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + column: Column, ): ReturnType => { const columnMeta = meta.forColumn(column, StickyColumns); diff --git a/test-app/tests/integration/cells-test.gts b/test-app/tests/integration/cells-test.gts index 0631ed2..6eeb423 100644 --- a/test-app/tests/integration/cells-test.gts +++ b/test-app/tests/integration/cells-test.gts @@ -4,6 +4,7 @@ import { module, test } from "qunit"; import { setupRenderingTest } from "ember-qunit"; import { headlessTable } from "@universal-ember/table"; +import { resizeHandle } from "@universal-ember/table/plugins/column-resizing"; import type { TOC } from "@ember/component/template-only"; import type { CellContext } from "@universal-ember/table"; @@ -77,6 +78,13 @@ module("Cells", function (hooks) { * Never rendered: these templates only exist for the type checks. */ const TypeChecks: TOC<{ Args: { table: Context["table"] } }> =