Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions table/src/-private/-type-tests/plugins-with-cell-args.test.ts
Original file line number Diff line number Diff line change
@@ -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<Person>` parameter on its own.
declare const GroupedCell: ComponentLike<
CellContext<Person> & { 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<boolean>();
expectTypeOf(isAscending(column)).toEqualTypeOf<boolean>();
expectTypeOf(isResizable(column)).toEqualTypeOf<boolean>();
expectTypeOf(isSticky(column)).toEqualTypeOf<boolean>();
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<Person>` 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<CellArgs>(
column: Column<Person, unknown, unknown, CellArgs>,
) {
return column.key;
}
takesAnyColumn(column);
3 changes: 2 additions & 1 deletion table/src/-private/interfaces/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export type PluginSubclassInstance<PluginType> = PluginType & {
* The data passed to a plugin's column APIs
*/
export interface ColumnApi<T extends Table = Table> {
column: Column<DataTypeOf<T>>;
// A plugin is handed whatever column the table has, whatever its Cell asks for.
column: Column<DataTypeOf<T>, any, any, any>;
table: T;
}

Expand Down
10 changes: 8 additions & 2 deletions table/src/-private/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,10 @@ export class Table<
}>;
columnHeader: FunctionBasedModifier<{
Element: HTMLElement;
Args: { Positional: [Column<DataType>]; Named: EmptyObject };
Args: {
Positional: [Column<DataType, ColumnMeta, Meta, CellArgs>];
Named: EmptyObject;
};
}>;
row: FunctionBasedModifier<{
Element: HTMLElement;
Expand All @@ -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<DataType>]): Destructor => {
(
element: HTMLElement,
[column]: [Column<DataType, ColumnMeta, Meta, CellArgs>],
): Destructor => {
const modifiers = this.plugins.map(
(plugin) => plugin.headerCellModifier,
);
Expand Down
51 changes: 39 additions & 12 deletions table/src/plugins/-private/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<P extends BasePlugin<any>, Data = unknown>(
column: Column<Data>,
klass: Class<P>,
) {
forColumn<
P extends BasePlugin<any>,
Data = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
>(column: Column<Data, ColumnMeta, Meta, CellArgs>, klass: Class<P>) {
return {
/**
* delete an entry on the underlying `Map` used for this column-plugin pair
Expand Down Expand Up @@ -574,8 +577,14 @@ export const meta = {
*
* Note that this requires the column instance to exist on the table.
*/
forColumn<P extends BasePlugin<any>, Data = unknown>(
column: Column<Data>,
forColumn<
P extends BasePlugin<any>,
Data = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
>(
column: Column<Data, ColumnMeta, Meta, CellArgs>,
klass: Class<P>,
): ColumnMetaFor<SignatureFrom<P>> {
const columnMeta = column.table[COLUMN_META_KEY];
Expand Down Expand Up @@ -685,8 +694,14 @@ export const meta = {
*
* For example, multiple column-focused plugins may care about width or visibility
*/
forColumn<FeatureName extends string, Data = unknown>(
column: Column<Data>,
forColumn<
FeatureName extends string,
Data = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
>(
column: Column<Data, ColumnMeta, Meta, CellArgs>,
featureName: FeatureName,
): ColumnFeatures[FeatureName] {
const { plugins } = column.table;
Expand Down Expand Up @@ -792,8 +807,14 @@ export const options = {
return fn() ?? {};
},

forColumn<P extends BasePlugin<any>, Data = unknown>(
column: Column<Data>,
forColumn<
P extends BasePlugin<any>,
Data = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
>(
column: Column<Data, ColumnMeta, Meta, CellArgs>,
klass: Class<P>,
): Partial<ColumnOptionsFor<SignatureFrom<P>>> {
const tuple = column.config.pluginOptions?.find(
Expand All @@ -819,13 +840,19 @@ function getPluginInstance<Instance>(
mapKey: Class<Instance>,
factory: () => Instance,
): Instance;
function getPluginInstance<RootKey extends Column<any> | Row<any>, Instance>(
function getPluginInstance<
RootKey extends Column<any, unknown, unknown, any> | Row<any>,
Instance,
>(
map: WeakMap<Column | Row, Map<Class<Instance>, Instance>>,
rootKey: RootKey,
mapKey: Class<Instance>,
factory: () => Instance,
): Instance;
function getPluginInstance<RootKey extends Column<any> | Row<any>, Instance>(
function getPluginInstance<
RootKey extends Column<any, unknown, unknown, any> | Row<any>,
Instance,
>(
...args:
| [FactoryMap<Instance>, Class<Instance>, () => Instance]
| [
Expand Down
67 changes: 52 additions & 15 deletions table/src/plugins/column-reordering/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <DataType = unknown>(column: Column<DataType>): void =>
meta.forColumn(column, ColumnReordering).moveLeft();
export const moveLeft = <
DataType = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
>(
column: Column<DataType, ColumnMeta, Meta, CellArgs>,
): 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 = <DataType = unknown>(column: Column<DataType>): void =>
meta.forColumn(column, ColumnReordering).moveRight();
export const moveRight = <
DataType = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
>(
column: Column<DataType, ColumnMeta, Meta, CellArgs>,
): void => meta.forColumn(column, ColumnReordering).moveRight();

/**
* Override all column positions at once.
*/
export const setColumnOrder = <DataType = unknown>(
table: Table<DataType>,
order: ColumnOrder<DataType>,
export const setColumnOrder = <
DataType = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
>(
table: Table<DataType, ColumnMeta, Meta, CellArgs>,
order: ColumnOrder<DataType, ColumnMeta, Meta, CellArgs>,
): 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.
Expand All @@ -37,23 +54,38 @@ export const setColumnOrder = <DataType = unknown>(
/**
* Ask if the column cannot move to the left
*/
export const cannotMoveLeft = <DataType = unknown>(
column: Column<DataType>,
export const cannotMoveLeft = <
DataType = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
>(
column: Column<DataType, ColumnMeta, Meta, CellArgs>,
): boolean => meta.forColumn(column, ColumnReordering).cannotMoveLeft;

/**
* Ask if the column cannot move to the right
*/
export const cannotMoveRight = <DataType = unknown>(
column: Column<DataType>,
export const cannotMoveRight = <
DataType = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
>(
column: Column<DataType, ColumnMeta, Meta, CellArgs>,
): 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 = <DataType = unknown>(
column: Column<DataType>,
export const canMoveLeft = <
DataType = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
>(
column: Column<DataType, ColumnMeta, Meta, CellArgs>,
): boolean =>
// Prefer this if available:
// meta.forColumn(column, ColumnReordering).canMoveLeft
Expand All @@ -63,8 +95,13 @@ export const canMoveLeft = <DataType = unknown>(
* Ask if the column can move to the right
* (If your plugin doesn't expose `canMoveRight`, use `!cannotMoveRight`.)
*/
export const canMoveRight = <DataType = unknown>(
column: Column<DataType>,
export const canMoveRight = <
DataType = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
>(
column: Column<DataType, ColumnMeta, Meta, CellArgs>,
): boolean =>
// Prefer this if available:
// meta.forColumn(column, ColumnReordering).canMoveRight
Expand Down
26 changes: 18 additions & 8 deletions table/src/plugins/column-reordering/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,12 @@ export class TableMeta<DataType = unknown> {
* This is also why the order of the columns is maintained via column key
*/
@tracked
columnOrder: ColumnOrder<DataType> = new ColumnOrder<DataType>({
columnOrder: ColumnOrder<DataType, any, any, any> = new ColumnOrder<
DataType,
any,
any,
any
>({
columns: () => this.allColumns,
visibleColumns: () => this.visibleColumns,
save: this.save,
Expand Down Expand Up @@ -148,7 +153,7 @@ export class TableMeta<DataType = unknown> {
/**
* Using a `ColumnOrder` instance, set the order of all columns
*/
setOrder = (order: ColumnOrder<DataType>): void => {
setOrder = (order: ColumnOrder<DataType, any, any, any>): void => {
this.columnOrder.setAll(order.map);
};

Expand All @@ -159,7 +164,7 @@ export class TableMeta<DataType = unknown> {
@action
reset(): void {
preferences.forTable(this.table, ColumnReordering).delete('order');
this.columnOrder = new ColumnOrder<DataType>({
this.columnOrder = new ColumnOrder<DataType, any, any, any>({
columns: () => this.allColumns,
visibleColumns: () => this.visibleColumns,
save: this.save,
Expand Down Expand Up @@ -223,7 +228,12 @@ export class TableMeta<DataType = unknown> {
* @private
* Used for keeping track of and updating column order
*/
export class ColumnOrder<DataType = unknown> {
export class ColumnOrder<
DataType = unknown,
ColumnMeta = unknown,
Meta = unknown,
CellArgs = unknown,
> {
/**
* This map will be empty until we re-order something.
*/
Expand All @@ -243,7 +253,7 @@ export class ColumnOrder<DataType = unknown> {
* - Provide `visibleColumns` to indicate which are visible
* - Hidden columns maintain their position when toggled
*/
columns: () => Column<DataType>[];
columns: () => Column<DataType, ColumnMeta, Meta, CellArgs>[];
/**
* Optional: Record of which columns are currently visible.
* When provided, moveLeft/moveRight will skip over hidden columns.
Expand Down Expand Up @@ -515,19 +525,19 @@ export class ColumnOrder<DataType = unknown> {
}

@cached
get orderedColumns(): Column<DataType>[] {
get orderedColumns(): Column<DataType, ColumnMeta, Meta, CellArgs>[] {
const allColumns = this.args.columns();
const columnsByKey = allColumns.reduce(
(keyMap, column) => {
keyMap[column.key] = column;
return keyMap;
},
{} as Record<string, Column<DataType>>,
{} as Record<string, Column<DataType, ColumnMeta, Meta, CellArgs>>,
);
// Use orderedMap which is reactive to preferences
const mergedOrder = this.orderedMap;

const result: Column<DataType>[] = Array.from({
const result: Column<DataType, ColumnMeta, Meta, CellArgs>[] = Array.from({
length: allColumns.length,
});

Expand Down
Loading