diff --git a/table/src/-private/-type-tests/column-options.test.ts b/table/src/-private/-type-tests/column-options.test.ts new file mode 100644 index 00000000..f1f44bee --- /dev/null +++ b/table/src/-private/-type-tests/column-options.test.ts @@ -0,0 +1,102 @@ +import { expectTypeOf } from 'expect-type'; + +import { column, headlessTable } from '../../index.ts'; + +import type { + CellArgs, + CellContext, + CellOptions, + ColumnConfig, +} from '../../index.ts'; +import type { ComponentLike } from '@glint/template'; + +interface Person { + name: string; + age: number; +} + +declare const UnitCell: ComponentLike>; +declare const PlainCell: ComponentLike>; +declare const people: Person[]; + +const col = column(); + +///////////////////////////////////////////// +// Each column checks its own Cell against its own options +expectTypeOf( + col({ key: 'age', Cell: UnitCell, options: () => ({ unit: 'years' }) }), +).toEqualTypeOf>(); + +// @ts-expect-error wrong option type +col({ key: 'age', Cell: UnitCell, options: () => ({ unit: 42 }) }); + +// @ts-expect-error missing option +col({ key: 'age', Cell: UnitCell, options: () => ({}) }); + +// @ts-expect-error Cell is for other row data +column<{ other: number }>()({ key: 'other', Cell: UnitCell }); + +///////////////////////////////////////////// +// Columns without `column()` keep working, and T is still inferred +const table = headlessTable( + {}, + { + columns: () => [ + { key: 'name', Cell: PlainCell }, + { key: 'name2', options: () => ({ anything: 1 }) }, + col({ key: 'age', Cell: UnitCell, options: () => ({ unit: 'years' }) }), + ], + data: () => people, + }, +); + +expectTypeOf(table.rows[0]!.data).toEqualTypeOf(); + +///////////////////////////////////////////// +// Column meta is declared once per app, and needs no row +declare module '../../index.ts' { + interface ColumnMeta { + align?: 'left' | 'right'; + exportValue?: (data: T) => string; + } +} + +const aligned = headlessTable( + {}, + { + columns: () => [ + { + key: 'age', + meta: { align: 'right' as const, exportValue: (person) => person.name }, + }, + ], + data: () => people, + }, +); + +expectTypeOf(aligned.columns[0]!.meta?.align).toEqualTypeOf< + 'left' | 'right' | undefined +>(); + +headlessTable( + {}, + { + // @ts-expect-error not one of the declared alignments + columns: () => [{ key: 'age', meta: { align: 'middle' } }], + data: () => people, + }, +); + +///////////////////////////////////////////// +// `column.Cell` can be rendered with or without `@options` +type PlainCellArgs = + NonNullable<(typeof table.columns)[0]['Cell']> extends ComponentLike< + infer Args + > + ? Args + : never; + +expectTypeOf<{ + row: (typeof table.rows)[0]; + column: (typeof table.columns)[0]; +}>().toMatchTypeOf(); diff --git a/table/src/-private/-type-tests/table-types.test.ts b/table/src/-private/-type-tests/table-types.test.ts new file mode 100644 index 00000000..f2798a9b --- /dev/null +++ b/table/src/-private/-type-tests/table-types.test.ts @@ -0,0 +1,290 @@ +import { expectTypeOf } from 'expect-type'; + +import { headlessTable, tableTypes } from '../../index.ts'; +import { + ColumnVisibility, + isVisible, +} from '../../plugins/column-visibility/index.ts'; +import { DataSorting, sort } from '../../plugins/data-sorting/index.ts'; +import { columns, meta } from '../../plugins/index.ts'; + +import type { CellContext, Column, ColumnConfig, Table } from '../../index.ts'; +import type { ComponentLike } from '@glint/template'; + +interface Person { + name: string; + age: number; +} +declare const people: Person[]; + +///////////////////////////////////////////// +// A table declares its own column meta and table meta +interface ReportColumnMeta { + align?: 'left' | 'right'; + exportWidth?: number; +} +interface ReportTableMeta { + updateCell: (data: Person, key: string, value: unknown) => void; +} + +interface ReportTypes { + columnMeta: ReportColumnMeta; + tableMeta: ReportTableMeta; +} +const types = tableTypes(); + +const report = headlessTable( + {}, + { + types, + columns: () => [ + { key: 'name', meta: { align: 'left' } }, + { key: 'age', meta: { align: 'right', exportWidth: 12 } }, + ], + data: () => people, + meta: { updateCell: () => {} }, + plugins: [ColumnVisibility, DataSorting], + }, +); + +// the row type is still inferred +expectTypeOf(report.rows[0]!.data).toEqualTypeOf(); + +// meta is typed where a column is read, with no declaration merging +expectTypeOf(report.columns[0]!.meta).toEqualTypeOf< + ReportColumnMeta | undefined +>(); +expectTypeOf(report.columns[0]!.table.config.meta!.updateCell).toBeFunction(); + +// helpers that return columns keep the type +expectTypeOf(columns.for(report)[0]!.meta).toEqualTypeOf< + ReportColumnMeta | undefined +>(); +expectTypeOf(columns.next(report.columns[0]!)!.meta).toEqualTypeOf< + ReportColumnMeta | undefined +>(); + +// plugin helpers take the typed table and column +expectTypeOf(isVisible(report.columns[0]!)).toEqualTypeOf(); +sort(report.columns[0]!); +meta.forColumn(report.columns[0]!, ColumnVisibility); + +// code that knows nothing about these types accepts the table and its columns +function takesAnyColumn(column: Column) { + return column.key; +} +function takesAnyTable(table: Table) { + return table.columns.length; +} +takesAnyColumn(report.columns[0]!); +takesAnyTable(report); + +// shared code can ask for the meta it needs, and still use the whole table +function exportWidths< + T, + TableTypes extends { columnMeta: { exportWidth?: number } }, +>(table: Table) { + return columns.for(table).map((column) => column.meta?.exportWidth); +} +expectTypeOf(exportWidths(report)).toEqualTypeOf<(number | undefined)[]>(); + +// shared code declares the meta it reads; it fits every table that has it +function exportWidthOf(column: { meta?: { exportWidth?: number } }) { + return column.meta?.exportWidth; +} +expectTypeOf(exportWidthOf(report.columns[0]!)).toEqualTypeOf< + number | undefined +>(); + +headlessTable( + {}, + { + types, + // @ts-expect-error not a key of this table's column meta + columns: () => [{ key: 'name', meta: { colour: 'red' } }], + data: () => people, + meta: { updateCell: () => {} }, + }, +); + +headlessTable( + {}, + { + types, + columns: () => [{ key: 'name' }], + data: () => people, + // @ts-expect-error this table's meta must have `updateCell` + meta: {}, + }, +); + +///////////////////////////////////////////// +// Another table in the same program has another shape +const other = headlessTable( + {}, + { + types: tableTypes<{ columnMeta: { sticky?: boolean } }>(), + columns: () => [{ key: 'name', meta: { sticky: true } }], + data: () => people, + }, +); + +expectTypeOf(other.columns[0]!.meta).toEqualTypeOf< + { sticky?: boolean } | undefined +>(); + +// @ts-expect-error `exportWidth` is not in this table's column meta +exportWidthOf(other.columns[0]!); +// @ts-expect-error `exportWidth` is not in this table's column meta +exportWidths(other); + +///////////////////////////////////////////// +// Inside `value`, `options` and a `Cell`, the column has this table's meta +declare const AlignedCell: ComponentLike>; + +headlessTable( + {}, + { + types, + columns: () => [ + { + key: 'age', + Cell: AlignedCell, + value: ({ column }) => { + expectTypeOf(column.meta?.align).toEqualTypeOf< + 'left' | 'right' | undefined + >(); + + return column.meta?.exportWidth ?? 0; + }, + options: ({ column }) => ({ + wide: (column.meta?.exportWidth ?? 0) > 10, + }), + }, + ], + data: () => people, + meta: { updateCell: () => {} }, + }, +); + +type AlignedCellArgs = + typeof AlignedCell extends ComponentLike ? Args : never; + +expectTypeOf().toEqualTypeOf< + ReportColumnMeta | undefined +>(); + +///////////////////////////////////////////// +// A column list written elsewhere names the types it was written for. +// `types` alone decides the table's types: an untyped list cannot widen them. +const typedList: ColumnConfig[] = [ + { key: 'name', meta: { align: 'left' } }, +]; +const fromList = headlessTable( + {}, + { + types, + columns: () => typedList, + data: () => people, + meta: { updateCell: () => {} }, + }, +); + +expectTypeOf(fromList.columns[0]!.meta).toEqualTypeOf< + ReportColumnMeta | undefined +>(); + +const untypedList: ColumnConfig[] = [{ key: 'name' }]; +const stillTyped = headlessTable( + {}, + { + types, + columns: () => untypedList, + data: () => people, + meta: { updateCell: () => {} }, + }, +); + +expectTypeOf(stillTyped.columns[0]!.meta).toEqualTypeOf< + ReportColumnMeta | undefined +>(); + +///////////////////////////////////////////// +// Meta that nothing declares is an error, with and without `types` +headlessTable( + {}, + { + types: tableTypes<{ tableMeta: ReportTableMeta }>(), + // @ts-expect-error these types declare no column meta + columns: () => [{ key: 'name', meta: { undeclared: 1 } }], + data: () => people, + meta: { updateCell: () => {} }, + }, +); + +///////////////////////////////////////////// +// Code written before `types` existed keeps working: bare annotations accept +// columns, configs and tables, from tables with and without `types` +const plain = headlessTable( + {}, + { + columns: () => [{ key: 'name' }], + data: () => people, + }, +); + +export const bareRowColumns: Column[] = [ + ...plain.columns, + ...report.columns, +]; +export const bareConfigs: ColumnConfig[] = [{ key: 'name' }]; +export const bareTables: Table[] = [plain, report]; + +function legacyHelper(columns: Column[]) { + return columns.map((column) => column.key); +} +legacyHelper([...plain.columns]); +legacyHelper([...report.columns]); + +///////////////////////////////////////////// +// A table can give its cells args of its own, passed where the cell is rendered: +// +interface ListTypes { + cellArgs: { dateRange: [Date, Date]; onUpdate: (person: Person) => void }; +} + +declare const DateCell: ComponentLike< + CellContext & ListTypes['cellArgs'] +>; +declare const UntypedCell: ComponentLike>; + +const list = headlessTable( + {}, + { + types: tableTypes(), + columns: () => [ + { key: 'name', Cell: DateCell }, + { key: 'plain', Cell: UntypedCell }, + ], + data: () => people, + plugins: [ColumnVisibility], + }, +); + +type Args = + NonNullable<(typeof list.columns)[0]['Cell']> extends ComponentLike + ? A + : never; +expectTypeOf().toEqualTypeOf<[Date, Date]>(); + +// typed columns and tables still fit code that knows nothing about these types +expectTypeOf(isVisible(list.columns[0]!)).toEqualTypeOf(); +expectTypeOf(columns.for(list)).toBeArray(); +function anyColumn(column: Column) { + return column.key; +} +function anyTable(table: Table) { + return table.columns.length; +} +anyColumn(list.columns[0]!); +anyTable(list); diff --git a/table/src/-private/column-helper.ts b/table/src/-private/column-helper.ts new file mode 100644 index 00000000..c913b5c0 --- /dev/null +++ b/table/src/-private/column-helper.ts @@ -0,0 +1,61 @@ +import type { Column } from './column.ts'; +import type { TableTypeSlots } from './types.ts'; +import type { CellContext, CellOptions, ColumnConfig } from './interfaces'; +import type { Row } from './row.ts'; +import type { ComponentLike } from '@glint/template'; + +/** + * The args of a `Cell` that is given `@options`, when rendered as + * `` + */ +export interface CellArgs< + T, + Options extends CellOptions = CellOptions, + TableTypes extends TableTypeSlots = TableTypeSlots, +> { + column: Column; + row: Row; + options: Options; +} + +/** + * A column config where the `Cell` and the `options` must agree. + */ +export type TypedColumnConfig< + T, + Options extends CellOptions, + TableTypes extends TableTypeSlots = TableTypeSlots, +> = Omit, 'Cell' | 'options'> & { + Cell?: ComponentLike>; + options?: (context: CellContext) => Options; +}; + +/** + * Checks each column's `Cell` against that column's own `options`. + * + * ```ts + * const col = column(); + * + * headlessTable(this, { + * columns: () => [ + * col({ key: 'name' }), + * col({ key: 'age', Cell: UnitCell, options: () => ({ unit: 'years' }) }), + * ], + * }); + * ``` + * + * `T` is given first and `Options` is inferred per call, + * because TypeScript cannot infer only some of a function's type arguments. + */ +export function column< + T, + TableTypes extends TableTypeSlots = TableTypeSlots, +>() { + return ( + config: TypedColumnConfig, + ): ColumnConfig => { + // `Options` is only known per column, and a list of columns has one element type. + // The check has happened by here, so the list can hold the general type. + return config as unknown as ColumnConfig; + }; +} diff --git a/table/src/-private/column.ts b/table/src/-private/column.ts index b28810c9..efa042fc 100644 --- a/table/src/-private/column.ts +++ b/table/src/-private/column.ts @@ -3,8 +3,9 @@ import { isEmpty } from '@ember/utils'; import type { Row } from './row'; import type { Table } from './table'; -import type { ComponentLike, ContentValue } from '@glint/template'; -import type { CellContext, ColumnConfig } from './interfaces'; +import type { ContentValue } from '@glint/template'; +import type { TableTypeSlots } from './types.ts'; +import type { ColumnConfig } from './interfaces'; const DEFAULT_VALUE = '--'; const DEFAULT_VALUE_KEY = 'defaultValue'; @@ -12,8 +13,11 @@ const DEFAULT_OPTIONS = { [DEFAULT_VALUE_KEY]: DEFAULT_VALUE, }; -export class Column { - get Cell(): ComponentLike> | undefined { +export class Column< + T = unknown, + TableTypes extends TableTypeSlots = TableTypeSlots, +> { + get Cell(): ColumnConfig['Cell'] { return this.config.Cell; } @@ -25,9 +29,13 @@ export class Column { return this.config.name; } + get meta(): ColumnConfig['meta'] { + return this.config.meta; + } + constructor( - public table: Table, - public config: ColumnConfig, + public table: Table, + public config: ColumnConfig, ) {} @action diff --git a/table/src/-private/interfaces/column.ts b/table/src/-private/interfaces/column.ts index 7b893d0a..34f20fe1 100644 --- a/table/src/-private/interfaces/column.ts +++ b/table/src/-private/interfaces/column.ts @@ -1,12 +1,22 @@ import type { BasePlugin, Plugin } from '../../plugins'; import type { Column } from '../column'; +import type { + ExtractCellArgs, + ExtractColumnMeta, + TableTypeSlots, +} from '../types.ts'; import type { Row } from '../row'; import type { ColumnOptionsFor, SignatureFrom } from './plugins'; import type { Constructor } from '../private-types'; import type { ComponentLike, ContentValue } from '@glint/template'; -export interface CellContext { - column: Column; +declare const rowType: unique symbol; + +export interface CellContext< + T, + TableTypes extends TableTypeSlots = TableTypeSlots, +> { + column: Column; row: Row; } @@ -14,6 +24,25 @@ type ColumnPluginOption

= P extends BasePlugin ? [Constructor

, () => ColumnOptionsFor>] : [P | Constructor

, () => unknown]; +/** + * What an app knows about a column, apart from how to render its cells, + * for example alignment, or a width to use in an export. + * + * Empty by default. Apps declare their own keys: + * + * ```ts + * declare module '@universal-ember/table' { + * interface ColumnMeta { + * align?: 'left' | 'right'; + * exportValue?: (data: T) => string; + * } + * } + * ``` + * + * `T` is the type of each row's data. A declaration must use the same name for it. + */ +export interface ColumnMeta {} + export type CellOptions = { /** * when no value is present for a given set of data for the given column config @@ -21,7 +50,10 @@ export type CellOptions = { defaultValue?: string; } & Record; -export interface ColumnConfig { +export interface ColumnConfig< + T = unknown, + TableTypes extends TableTypeSlots = TableTypeSlots, +> { /** * the `key` is required for preferences storage, as well as * managing uniqueness of the columns in an easy-to-understand way. @@ -37,24 +69,34 @@ export interface ColumnConfig { /** * Optionally provide a function to determine the value of a row at this column */ - value?: (context: CellContext) => ContentValue; + value?: (context: CellContext) => ContentValue; /** * Recommended property to use for custom components for each cell per column. * Out-of-the-box, this property isn't used, but the provided type may be * a convenience for consumers of the headless table */ - Cell?: ComponentLike>; + Cell?: ComponentLike< + CellContext & { + options?: CellOptions; + } & ExtractCellArgs + >; /** * The name or title of the column, shown in the column heading / th */ name?: string; + /** + * Static information about the column, read as `column.meta`. + * Unlike `options`, it needs no row. + */ + meta?: ExtractColumnMeta; + /** * Bag of extra properties to pass to Cell via `@options`, if desired */ - options?: (context: CellContext) => CellOptions; + options?: (context: CellContext) => CellOptions; /** * Each plugin may provide column options, and provides similar syntax to how @@ -68,6 +110,15 @@ export interface ColumnConfig { * ``` */ pluginOptions?: ColumnPluginOption[]; + + /** + * Type-only, never set. + * + * The row type is otherwise used in callback parameters only. With this mention, + * a list annotated as `ColumnConfig[]` always gives the table the row type `unknown`. + * Without it, TypeScript's result depends on which types it compared first. + */ + readonly [rowType]?: T; } export type ColumnKey = NonNullable['key']>; diff --git a/table/src/-private/interfaces/table.ts b/table/src/-private/interfaces/table.ts index b22aa327..80fcf3c1 100644 --- a/table/src/-private/interfaces/table.ts +++ b/table/src/-private/interfaces/table.ts @@ -1,5 +1,6 @@ import type { Plugins } from '../../plugins/-private/utils'; import type { ColumnConfig } from './column'; +import type { ExtractTableMeta, TableTypeSlots } from '../types.ts'; import type { Pagination } from './pagination'; import type { PreferencesAdapter } from './preferences'; import type { Selection } from './selection'; @@ -9,13 +10,21 @@ export interface TableMeta { totalRowsSelectedCount?: number; } -export interface TableConfig { +export interface TableConfig< + DataType, + TableTypes extends TableTypeSlots = TableTypeSlots, +> { + /** + * The types this table declares for itself, see `tableTypes`. + */ + types?: TableTypes; + /** * Configuration describing how the table will crawl through `data` * and render it. Within this `columns` config, there will also be opportunities * to set the behavior of columns when rendered */ - columns: () => ColumnConfig[]; + columns: () => ColumnConfig>[]; /** * The data to render, as described via the `columns` option. * @@ -87,7 +96,7 @@ export interface TableConfig { onRowSelectionChange?: (selection: DataType | undefined) => void; // Uncategorized - meta?: TableMeta; + meta?: ExtractTableMeta>; pagination?: Pagination; /** diff --git a/table/src/-private/js-helper.ts b/table/src/-private/js-helper.ts index de3fffc6..3d7c7c5e 100644 --- a/table/src/-private/js-helper.ts +++ b/table/src/-private/js-helper.ts @@ -2,6 +2,7 @@ import { assert } from '@ember/debug'; import { Table } from './table.ts'; +import type { TableTypeSlots } from './types.ts'; import type { TableConfig } from './interfaces'; /** @@ -24,15 +25,15 @@ import type { TableConfig } from './interfaces'; * ``` * */ -export function headlessTable( - parent: object, - options: TableConfig, -): Table { +export function headlessTable< + T = unknown, + TableTypes extends TableTypeSlots = TableTypeSlots, +>(parent: object, options: TableConfig): Table { assert( `headlessTable requires a parent object as the first argument, usually \`this\`. ` + `The single-argument form was removed, because the table is no longer a Resource.`, options, ); - return new Table(parent, options); + return new Table(parent, options); } diff --git a/table/src/-private/table.ts b/table/src/-private/table.ts index 8ae789e9..8318acbc 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 { TableTypeSlots } from './types.ts'; import type { ColumnConfig, Destructor, TableConfig } from './interfaces'; import type Owner from '@ember/owner'; import { compatOwner } from './ember-compat.ts'; @@ -51,7 +52,10 @@ const attachContainer = (element: Element, table: Table) => { * Symbol-keyed fields live on this interface, * because `isolatedDeclarations` cannot emit computed class members. */ -export interface Table { +export interface Table< + DataType = unknown, + TableTypes extends TableTypeSlots = TableTypeSlots, +> { /** * @private */ @@ -71,7 +75,10 @@ export interface Table { } // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging -export class Table { +export class Table< + DataType = unknown, + TableTypes extends TableTypeSlots = TableTypeSlots, +> { /** * @private * @@ -95,9 +102,9 @@ export class Table { scrollContainerElement?: HTMLElement; #parent: object; - #config: TableConfig; + #config: TableConfig; - constructor(parent: object, config: TableConfig) { + constructor(parent: object, config: TableConfig) { this.#parent = parent; this.#config = config; this[TABLE_KEY] = guidFor(this); @@ -142,7 +149,7 @@ export class Table { * * used by other private APIs */ - get config(): TableConfig { + get config(): TableConfig { return this.#config; } @@ -181,7 +188,7 @@ export class Table { }>; columnHeader: FunctionBasedModifier<{ Element: HTMLElement; - Args: { Positional: [Column]; Named: EmptyObject }; + Args: { Positional: [Column]; Named: EmptyObject }; }>; row: FunctionBasedModifier<{ Element: HTMLElement; @@ -208,7 +215,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, ); @@ -288,7 +298,10 @@ export class Table { map: (datum) => new Row(this, datum), }); - columns: MappedArray[], Column> = map(this, { + columns: MappedArray< + ColumnConfig[], + Column + > = map(this, { data: () => { const configFn = this.#config.columns; @@ -320,7 +333,7 @@ export class Table { return result; }, map: (config) => { - return new Column(this, { + return new Column(this, { ...DEFAULT_COLUMN_CONFIG, ...config, }); diff --git a/table/src/-private/types.ts b/table/src/-private/types.ts new file mode 100644 index 00000000..b24745de --- /dev/null +++ b/table/src/-private/types.ts @@ -0,0 +1,79 @@ +import type { ColumnMeta } from './interfaces/column.ts'; +import type { TableMeta } from './interfaces/table.ts'; + +/** + * The types a table declares for itself. + * + * ```ts + * interface ReportTypes { + * columnMeta: ReportColumnMeta; + * tableMeta: ReportTableMeta; + * cellArgs: { dateRange: DateRange }; + * } + * + * headlessTable(this, { types: tableTypes(), columns, data }); + * + * const columns: ColumnConfig[] = []; + * ``` + * + * Only tables created with these types get them, so two tables in one app + * can declare different shapes. The slot names are those of TanStack Table. + */ +export interface TableTypeSlots { + columnMeta?: object; + tableMeta?: object; + cellArgs?: object; +} + +/** + * Carries a table's types into its config. Only the type is used. + * + * It is a call because TypeScript infers a type argument from a value: + * writing `headlessTable(...)` would stop `Person` being inferred. + */ +export function tableTypes(): TableTypes { + return {} as TableTypes; +} + +declare const undeclared: unique symbol; + +/** + * The column meta of a table that declares none. + * It has one key nobody can write, so that writing or reading any other key is an error. + * An empty interface would accept every key. + */ +export interface NoColumnMeta { + readonly [undeclared]?: never; +} + +/** + * The type of `column.meta`: the table's `columnMeta` when it declares one, + * else the app-wide `ColumnMeta` interface, else `NoColumnMeta`. + */ +export type ExtractColumnMeta = TableTypes extends { + columnMeta: object; +} + ? TableTypes['columnMeta'] + : keyof ColumnMeta extends never + ? NoColumnMeta + : ColumnMeta; + +/** + * The type of `table.config.meta`: the table's `tableMeta` slot when it declares one, + * together with the keys this library defines itself. + */ +export type ExtractTableMeta = TableTypes extends { + tableMeta: object; +} + ? TableMeta & TableTypes['tableMeta'] + : TableMeta; + +/** + * The args a table gives its cells besides `@row`, `@column` and `@options`: + * the table's `cellArgs` when it declares them. + */ +export type ExtractCellArgs = TableTypes extends { + cellArgs: object; +} + ? TableTypes['cellArgs'] + : unknown; diff --git a/table/src/index.ts b/table/src/index.ts index e4182017..7702492a 100644 --- a/table/src/index.ts +++ b/table/src/index.ts @@ -1,6 +1,8 @@ /******************************** * Public API *******************************/ +export { column } from './-private/column-helper.ts'; +export { tableTypes } from './-private/types.ts'; export { headlessTable, headlessTable as table } from './-private/js-helper.ts'; // Utilities @@ -11,8 +13,18 @@ export { deserializeSorts, serializeSorts } from './utils.ts'; * Public Types *******************************/ export type { Column } from './-private/column.ts'; +export type { CellArgs, TypedColumnConfig } from './-private/column-helper.ts'; export type { + ExtractCellArgs, + ExtractColumnMeta, + ExtractTableMeta, + TableTypeSlots, +} from './-private/types.ts'; +export type { + CellContext, + CellOptions, ColumnConfig, + ColumnMeta, ColumnKey, Pagination, PreferencesAdapter, diff --git a/table/src/plugins/-private/base.ts b/table/src/plugins/-private/base.ts index d0c66948..f430d8fe 100644 --- a/table/src/plugins/-private/base.ts +++ b/table/src/plugins/-private/base.ts @@ -8,6 +8,7 @@ import { } from '../../-private/table.ts'; import { normalizePluginsConfig } from './utils.ts'; +import type { TableTypeSlots } from '../../-private/types.ts'; import type { Table } from '../../-private/table.ts'; import type { ColumnReordering } from '../column-reordering'; import type { ColumnVisibility } from '../column-visibility'; @@ -309,10 +310,13 @@ export const preferences = { * This works recursively up the plugin tree up until a plugin has no requirements, and then * all columns from the table are returned. */ -function columnsFor( - table: Table, +function columnsFor< + DataType = any, + TableTypes extends TableTypeSlots = TableTypeSlots, +>( + table: Table, requester?: Plugin, -): Column[] { +): Column[] { assert( `First argument passed to columns.for must be an instance of Table`, table[TABLE_KEY], @@ -432,10 +436,10 @@ export const columns = { * If a plugin class is provided, the hierarchy of column list modifications * will be respected. */ - next: ( - current: Column, + next: ( + current: Column, requester?: Plugin, - ): Column | undefined => { + ): Column | undefined => { const columns = requester ? columnsFor(current.table, requester) : columnsFor(current.table); @@ -464,10 +468,13 @@ export const columns = { * If a plugin class is provided, the hierarchy of column list modifications * will be respected. */ - previous: ( - current: Column, + previous: < + Data = unknown, + TableTypes extends TableTypeSlots = TableTypeSlots, + >( + current: Column, requester?: Plugin, - ): Column | undefined => { + ): Column | undefined => { const columns = requester ? columnsFor(current.table, requester) : columnsFor(current.table); @@ -494,10 +501,10 @@ export const columns = { * if a plugin class is provided, the hierarchy of column list modifications * will be respected. */ - before: ( - current: Column, + before: ( + current: Column, requester?: Plugin, - ): Column[] => { + ): Column[] => { const columns = requester ? columnsFor(current.table, requester) : columnsFor(current.table); @@ -513,10 +520,10 @@ export const columns = { * if a plugin class is provided, the hierarchy of column list modifications * will be respected. */ - after: ( - current: Column, + after: ( + current: Column, requester?: Plugin, - ): Column[] => { + ): Column[] => { const columns = requester ? columnsFor(current.table, requester) : columnsFor(current.table); diff --git a/table/src/plugins/column-reordering/helpers.ts b/table/src/plugins/column-reordering/helpers.ts index cfc5d1b9..3458858c 100644 --- a/table/src/plugins/column-reordering/helpers.ts +++ b/table/src/plugins/column-reordering/helpers.ts @@ -2,6 +2,7 @@ import { meta } from '../-private/base.ts'; import { ColumnReordering } from './plugin.ts'; import type { ColumnOrder, TableMeta } from './plugin.ts'; +import type { TableTypeSlots } from '../../-private/types.ts'; import type { Column, Table } from '../../index.ts'; /** @@ -88,9 +89,12 @@ export const canMoveRight = ( * // Use the ordered columns for rendering or other operations * ``` */ -export const orderedColumnsFor = ( - table: Table, -): Column[] => { +export const orderedColumnsFor = < + DataType = unknown, + TableTypes extends TableTypeSlots = TableTypeSlots, +>( + table: Table, +): Column[] => { // Note: The meta.forTable API doesn't preserve the DataType generic from the table parameter. // This is a limitation of the current plugin meta system architecture. // We use a type assertion here because we know the columns come from the same table. diff --git a/test-app/tests/unit/public-api-test.ts b/test-app/tests/unit/public-api-test.ts index e3d3186c..edf61272 100644 --- a/test-app/tests/unit/public-api-test.ts +++ b/test-app/tests/unit/public-api-test.ts @@ -24,6 +24,8 @@ module('Public API', function () { [ 'headlessTable', 'table', + 'column', + 'tableTypes', 'TablePreferences', 'deserializeSorts', 'serializeSorts',