Skip to content
Draft
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
82 changes: 82 additions & 0 deletions docs-app/public/docs/1-get-started/typescript-and-glint.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,88 @@ class Demo {
}
```

## Type-Safe Cell Components and Options

The table library supports generic type parameters for strongly-typed Cell components and custom options. This enables full type inference in Glint templates and prevents common errors.

### Basic Usage

By default, all generic parameters are set to `any` for backward compatibility:

```ts
const table = headlessTable<MyDataType>(this, {
columns: () => [...],
data: () => myData
});
```

### Advanced: Type-Safe Options

To get type safety for custom options passed to cells:

```ts
import {
headlessTable,
type ColumnConfig,
type CellContext,
} from "@universal-ember/table";

interface MyData {
id: string;
name: string;
}

interface MyOptions {
highlightColor?: string;
showBadge?: boolean;
}

interface MyCellArgs extends CellContext<MyData, MyOptions> {
// Cell components receive data, column, row, and options
}

const table = headlessTable<MyData, MyOptions, MyCellArgs>(this, {
columns: () => [
{
key: "name",
name: "Name",
Cell: MyCustomCell, // fully typed!
options: ({ row }) => ({
highlightColor: row.data.id === "special" ? "blue" : "gray",
showBadge: true,
}),
},
],
data: () => myData,
});
```

Your Cell component will now have full type inference:

```ts
import Component from '@glimmer/component';

class MyCustomCell extends Component<MyCellArgs> {
get color() {
return this.args.options?.highlightColor ?? 'gray';
}

<template>
<span style="color: {{this.color}}">
{{@row.data.name}}
{{#if @options.showBadge}}<span class="badge">!</span>{{/if}}
</span>
</template>
}
```

### CellContext Types

The library provides two context types:

- **`CellConfigContext<T, OptionsType>`**: Used when defining column configurations. Has optional fields (`column?`, `row?`, `options?`) for user convenience.
- **`CellContext<T, OptionsType>`**: Used for Cell component signatures. Has required fields since they're always provided at runtime.

## In Templates

[Glint][docs-glint] can be a great choice to help ensure that your code is as bug-free as possible.
Expand Down
17 changes: 11 additions & 6 deletions table/src/-private/column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import { isEmpty } from '@ember/utils';
import type { Row } from './row';
import type { Table } from './table';
import type { ContentValue } from '@glint/template';
import type { ColumnConfig } from './interfaces';
import type {
ColumnConfig,
CellOptions,
CellConfigContext,
} from './interfaces';

const DEFAULT_VALUE = '--';
const DEFAULT_VALUE_KEY = 'defaultValue';
const DEFAULT_OPTIONS = {
[DEFAULT_VALUE_KEY]: DEFAULT_VALUE,
};

export class Column<T = unknown> {
export class Column<T = unknown, OptionsType = any, CellArgs = any> {
get Cell() {
return this.config.Cell;
}
Expand All @@ -27,7 +31,7 @@ export class Column<T = unknown> {

constructor(
public table: Table<T>,
public config: ColumnConfig<T>,
public config: ColumnConfig<T, OptionsType, CellArgs>,
) {}

@action
Expand All @@ -52,11 +56,12 @@ export class Column<T = unknown> {
}

private getDefaultValue(row: Row<T>) {
return this.getOptionsForRow(row)[DEFAULT_VALUE_KEY];
const options = this.getOptionsForRow(row) as any;
return options[DEFAULT_VALUE_KEY];
}

@action
getOptionsForRow(row: Row<T>) {
getOptionsForRow(row?: Row<T>): OptionsType & CellOptions {
const configuredDefault = this.table.config.defaultCellValue;
const defaults = {
[DEFAULT_VALUE_KEY]:
Expand All @@ -66,6 +71,6 @@ export class Column<T = unknown> {
return {
...defaults,
...this.config.options?.({ column: this, row }),
};
} as OptionsType & CellOptions;
}
}
29 changes: 21 additions & 8 deletions table/src/-private/interfaces/column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ import type { ColumnOptionsFor, SignatureFrom } from './plugins';
import type { Constructor } from '../private-types';
import type { ComponentLike, ContentValue } from '@glint/template';

export interface CellContext<T> {
column: Column<T>;
row: Row<T>;
// Configuration context (for defining column options) - optional fields for user convenience
export interface CellConfigContext<DataType = unknown, OptionsType = unknown> {
column?: Column<DataType, OptionsType>;
row?: Row<DataType>;
options?: OptionsType & CellOptions;
}

// Runtime context (for Cell components) - required fields since they're always provided
export interface CellContext<DataType, OptionsType = unknown> {
column: Column<DataType, OptionsType>;
row: Row<DataType>;
options?: OptionsType & CellOptions;
}

type ColumnPluginOption<P = Plugin> = P extends BasePlugin
Expand All @@ -21,7 +30,11 @@ export type CellOptions = {
defaultValue?: string;
} & Record<string, unknown>;

export interface ColumnConfig<T = unknown> {
export interface ColumnConfig<
DataType = unknown,
OptionsType = unknown,
CellArgs = unknown,
> {
/**
* the `key` is required for preferences storage, as well as
* managing uniqueness of the columns in an easy-to-understand way.
Expand All @@ -37,14 +50,14 @@ export interface ColumnConfig<T = unknown> {
/**
* Optionally provide a function to determine the value of a row at this column
*/
value?: (context: CellContext<T>) => ContentValue;
value?(context: CellConfigContext<DataType>): 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<CellContext<T>>;
Cell?: ComponentLike<CellArgs>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T previously allowed for inferred types - does this still work?


/**
* The name or title of the column, shown in the column heading / th
Expand All @@ -54,7 +67,7 @@ export interface ColumnConfig<T = unknown> {
/**
* Bag of extra properties to pass to Cell via `@options`, if desired
*/
options?: (context: CellContext<T>) => CellOptions;
options?(context: CellConfigContext<DataType>): OptionsType;

/**
* Each plugin may provide column options, and provides similar syntax to how
Expand All @@ -70,4 +83,4 @@ export interface ColumnConfig<T = unknown> {
pluginOptions?: ColumnPluginOption[];
}

export type ColumnKey<T> = NonNullable<ColumnConfig<T>['key']>;
export type ColumnKey<DataType> = NonNullable<ColumnConfig<DataType>['key']>;
6 changes: 3 additions & 3 deletions table/src/-private/interfaces/table.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Plugins } from '../../plugins/-private/utils';
import type { ColumnConfig } from './column';
import type { ColumnConfig, CellOptions, CellContext } from './column';
import type { Pagination } from './pagination';
import type { PreferencesAdapter } from './preferences';
import type { Selection } from './selection';
Expand All @@ -9,13 +9,13 @@ export interface TableMeta {
totalRowsSelectedCount?: number;
}

export interface TableConfig<DataType> {
export interface TableConfig<DataType, OptionsType = any, CellArgs = any> {
/**
* 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<DataType>[];
columns: () => ColumnConfig<DataType, OptionsType, CellArgs>[];
/**
* The data to render, as described via the `columns` option.
*
Expand Down
29 changes: 18 additions & 11 deletions table/src/-private/js-helper.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Table } from './table.ts';

import type { TableConfig } from './interfaces';
import type { TableConfig, CellContext } from './interfaces';

type Args<T> =
| [destroyable: object, options: TableConfig<T>]
| [options: TableConfig<T>];
type Args<DataType, OptionsType = any, CellArgs = any> =
| [destroyable: object, options: TableConfig<DataType, OptionsType, CellArgs>]
| [options: TableConfig<DataType, OptionsType, CellArgs>];

/**
* Represents a UI-less version of a table
Expand All @@ -23,7 +23,9 @@ type Args<T> =
* }
* ```
*/
export function headlessTable<T = unknown>(options: TableConfig<T>): Table<T>;
export function headlessTable<DataType = unknown, OptionsType = any, CellArgs = any>(
options: TableConfig<DataType, OptionsType, CellArgs>,
): Table<DataType, OptionsType, CellArgs>;

/**
* Represents a UI-less version of a table
Expand All @@ -42,12 +44,14 @@ export function headlessTable<T = unknown>(options: TableConfig<T>): Table<T>;
* ```
*
*/
export function headlessTable<T = unknown>(
export function headlessTable<DataType = unknown, OptionsType = any, CellArgs = any>(
destroyable: object,
options: TableConfig<T>,
): Table<T>;
options: TableConfig<DataType, OptionsType, CellArgs>,
): Table<DataType, OptionsType, CellArgs>;

export function headlessTable<T = unknown>(...args: Args<T>): Table<T> {
export function headlessTable<DataType = unknown, OptionsType = any, CellArgs = any>(
...args: Args<DataType, OptionsType, CellArgs>
): Table<DataType, OptionsType, CellArgs> {
if (args.length === 2) {
const [destroyable, options] = args;

Expand All @@ -56,10 +60,13 @@ export function headlessTable<T = unknown>(...args: Args<T>): Table<T> {
* otherwise individual-property reactivity can be managed on a per-property
* "thunk"-basis
*/
return Table.from<Table<T>>(destroyable, () => options);
return Table.from<Table<DataType, OptionsType, CellArgs>>(
destroyable,
() => options,
);
}

const [options] = args;

return Table.from<Table<T>>(() => options);
return Table.from<Table<DataType, OptionsType, CellArgs>>(() => options);
}
32 changes: 23 additions & 9 deletions table/src/-private/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { composeFunctionModifiers } from './utils.ts';
import type { BasePlugin, Plugin } from '../plugins/index.ts';
import type { Class } from './private-types.ts';
import type { Destructor, TableConfig } from './interfaces';
import type { CellOptions, CellContext } from './interfaces/column.ts';
import { compatOwner } from './ember-compat.ts';

const getOwner = compatOwner.getOwner;
Expand All @@ -30,8 +31,8 @@ const DEFAULT_COLUMN_CONFIG = {
minWidth: 128,
};

interface Signature<DataType> {
Named: TableConfig<DataType>;
interface Signature<DataType, OptionsType = any, CellArgs = any> {
Named: TableConfig<DataType, OptionsType>;
}

/**
Expand All @@ -54,7 +55,11 @@ const attachContainer = (element: Element, table: Table) => {
table.scrollContainerElement = element;
};

export class Table<DataType = unknown> extends Resource<Signature<DataType>> {
export class Table<
DataType = unknown,
OptionsType = any,
CellArgs = any,
> extends Resource<Signature<DataType, OptionsType, CellArgs>> {
/**
* @private
*/
Expand All @@ -66,11 +71,14 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {
/**
* @private
*/
[COLUMN_META_KEY] = new WeakMap<Column, Map<Class<unknown>, any>>();
[COLUMN_META_KEY] = new WeakMap<
Column<DataType, OptionsType, CellArgs>,
Map<Class<unknown>, any>
>();
/**
* @private
*/
[ROW_META_KEY] = new WeakMap<Row, Map<Class<unknown>, any>>();
[ROW_META_KEY] = new WeakMap<Row<DataType>, Map<Class<unknown>, any>>();

/**
* @private
Expand Down Expand Up @@ -110,7 +118,10 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {
/**
* @private
*/
modify(_: [] | undefined, named: Signature<DataType>['Named']) {
modify(
_: [] | undefined,
named: Signature<DataType, OptionsType, CellArgs>['Named'],
) {
this.args = { named };

// only set the preferences once
Expand Down Expand Up @@ -157,7 +168,10 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {
// 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, OptionsType, CellArgs>],
): Destructor => {
const modifiers = this.plugins.map(
(plugin) => plugin.headerCellModifier,
);
Expand Down Expand Up @@ -251,7 +265,7 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {

return dataFn() ?? [];
},
map: (datum) => new Row(this, datum),
map: (datum) => new Row<DataType>(this, datum),
});

columns = map(this, {
Expand Down Expand Up @@ -286,7 +300,7 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {
return result;
},
map: (config) => {
return new Column<DataType>(this, {
return new Column<DataType, OptionsType, CellArgs>(this, {
...DEFAULT_COLUMN_CONFIG,
...config,
});
Expand Down
1 change: 1 addition & 0 deletions table/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { deserializeSorts, serializeSorts } from './utils.ts';
*******************************/
export type { Column } from './-private/column.ts';
export type {
CellContext,
ColumnConfig,
ColumnKey,
Pagination,
Expand Down
Loading
Loading