Skip to content
Merged
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
41 changes: 41 additions & 0 deletions docs-app/src/templates/1-get-started/upgrading.gjs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Upgrading

## To 4.0

The table is a plain class now. It was a `Resource` from `ember-modify-based-class-resource`, and that package is no longer a dependency.

Two applications that each bundled a copy of `ember-modify-based-class-resource` crashed with `type may not overlap with an existing usable`. That failure is gone.

### `headlessTable` needs a parent object

Pass the object that owns the table as the first argument. The table is destroyed with that object, and it uses that object's owner.

```js
// before
class MyImplementation {
@use table = headlessTable({
columns: () => [],
data: () => [],
});
}

// after
class MyImplementation {
table = headlessTable(this, {
columns: () => [],
data: () => [],
});
}
```

The two-argument form did not change. If you already write `headlessTable(this, { ... })`, you have no work to do.

### The scroll position is no longer reset for you

The table reset the scroll container to the top every time the configuration changed. It does not do this now.

If you want that behavior, call `table.resetScrollContainer()` from your own code.

### `table.args` is gone

Read `table.config` instead. It returns the options object that you passed to `headlessTable`.
28 changes: 0 additions & 28 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,6 @@
"@embroider/addon-shim": "^1.10.2",
"@embroider/macros": "^1.19.7",
"ember-modifier": "^4.3.0",
"ember-modify-based-class-resource": "^1.1.2",
"ember-resources": "^7.0.7",
"reactiveweb": "^1.9.1",
"tracked-built-ins": "^4.1.0"
},
Expand Down
53 changes: 13 additions & 40 deletions table/src/-private/js-helper.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,16 @@
import { assert } from '@ember/debug';

import { Table } from './table.ts';

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

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

/**
* Represents a UI-less version of a table
*
* _For use for building tables in ui frameworks_.
*
* @example
* ```js
* import { use } from 'ember-resources';
* import { headlessTable } '@universal-ember/table';
*
* class MyImplementation {
* @use table = headlessTable({
* // your config here
* })
* }
* ```
*/
export function headlessTable<T = unknown>(options: TableConfig<T>): Table<T>;

/**
* Represents a UI-less version of a table
*
* _For use for building tables in ui frameworks_.
* The first argument is the object that owns the table.
* The table is destroyed with that object, and uses that object's owner.
*
* @example
* ```js
Expand All @@ -43,23 +25,14 @@ export function headlessTable<T = unknown>(options: TableConfig<T>): Table<T>;
*
*/
export function headlessTable<T = unknown>(
destroyable: object,
parent: object,
options: TableConfig<T>,
): Table<T>;

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

/**
* If any "root level" config changes, we need to throw-away everything.
* otherwise individual-property reactivity can be managed on a per-property
* "thunk"-basis
*/
return Table.from<Table<T>>(destroyable, () => options);
}

const [options] = args;

return Table.from<Table<T>>(() => options);
): Table<T> {
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<T>(parent, options);
}
122 changes: 63 additions & 59 deletions table/src/-private/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { guidFor } from '@ember/object/internals';

import { isDevelopingApp, macroCondition } from '@embroider/macros';
import { modifier } from 'ember-modifier';
import { Resource } from 'ember-modify-based-class-resource';
import { link } from 'reactiveweb/link';
import { map } from 'reactiveweb/map';

import {
Expand All @@ -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 Owner from '@ember/owner';
import { compatOwner } from './ember-compat.ts';

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

interface Signature<DataType> {
Named: TableConfig<DataType>;
}

/**
* Because the table is our entry-point object to all the table behaviors,
* we need a stable way to know which table we have.
* Normally, this could be done with referential integrity / identity.
* However, due to how resources are implemented, if the consumer opts to
* not use the `@use` decorator, then proxies get involved.
* The proxies don't maintain instanceof checks, which may be a bug in
* ember-resources.
*/
export const TABLE_KEY = Symbol('__TABLE_KEY__');
export const TABLE_META_KEY = Symbol('__TABLE_META__');
Expand All @@ -54,7 +46,7 @@ const attachContainer = (element: Element, table: Table) => {
table.scrollContainerElement = element;
};

export class Table<DataType = unknown> extends Resource<Signature<DataType>> {
export class Table<DataType = unknown> {
/**
* @private
*/
Expand Down Expand Up @@ -91,45 +83,74 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {

/**
* @private
*
* Lazy way to delay consuming arguments until they are needed.
*/
@tracked declare args: { named: Signature<DataType>['Named'] };
scrollContainerElement?: HTMLElement;

#parent: object;
#config: TableConfig<DataType>;

constructor(parent: object, config: TableConfig<DataType>) {
this.#parent = parent;
this.#config = config;

/**
* The table is destroyed with the object that created it,
* and it uses that object's owner for its plugins.
*/
link(this, parent);
}

/**
* @private
*
* `link` copies the owner over, but the parent can receive its owner
* after the table is created. A class field initializes before `setOwner`
* runs on a manually constructed object, so the owner is read from the
* parent on first use.
*/
scrollContainerElement?: HTMLElement;
get #owner(): Owner {
let owner = getOwner(this);

/**
* Interact with, save, modify, etc the preferences for the table,
* plugins, columns, etc
*/
declare preferences: TablePreferences;
if (!owner) {
owner = getOwner(this.#parent);

assert(
`The Table does not have an owner. cannot create a plugin without an owner`,
owner,
);

setOwner(this, owner);
}

return owner;
}

/**
* @private
*
* used by other private APIs
*/
modify(_: [] | undefined, named: Signature<DataType>['Named']) {
this.args = { named };
get config(): TableConfig<DataType> {
return this.#config;
}

const preferences = named?.preferences;
/**
* Interact with, save, modify, etc the preferences for the table,
* plugins, columns, etc
*
* When the `preferences` config is a function, the preferences are
* restored again every time the tracked data that the function reads changes.
*/
@cached
get preferences(): TablePreferences {
const config = this.#config.preferences;
const { key = guidFor(this), adapter } =
(typeof preferences === 'function' ? preferences() : preferences) ?? {};

// only set the preferences once
if (!this.preferences) {
// TODO: when no key is present,
// use "local-storage" preferences.
// it does not make sense to use a guid in a user's preferences
this.preferences = new TablePreferences(key, adapter);
} else {
// subsequent updates to args
if (typeof preferences === 'function' && adapter) {
this.preferences.restore(adapter);
}
this.resetScrollContainer();
}
(typeof config === 'function' ? config() : config) ?? {};

// TODO: when no key is present,
// use "local-storage" preferences.
// it does not make sense to use a guid in a user's preferences
return new TablePreferences(key, adapter);
}

/**
Expand Down Expand Up @@ -186,12 +207,10 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {
* @private
*
* For all configured plugins, instantiates each one.
* If the plugins argument changes to the Table (either directly or through
* headlessTable, all state is lost and re-created)
*/
@cached
get plugins(): Plugin[] {
const plugins = normalizePluginsConfig(this.args.named?.plugins);
const plugins = normalizePluginsConfig(this.#config.plugins);

verifyPlugins(plugins);

Expand All @@ -202,13 +221,7 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {
if (typeof PluginClass === 'function') {
const plugin = new PluginClass(this);

const owner = getOwner(this);

assert(
`The Table does not have an owner. cannot create a plugin without an owner`,
owner,
);
setOwner(plugin, owner);
setOwner(plugin, this.#owner);

return plugin;
}
Expand Down Expand Up @@ -239,18 +252,9 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {
return result as unknown as Instance | undefined;
}

/**
* @private
*
* used by other private APIs
*/
get config() {
return this.args.named;
}

rows = map(this, {
data: () => {
const dataFn = this.args.named?.data;
const dataFn = this.#config.data;

if (!dataFn) return [];

Expand All @@ -261,7 +265,7 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {

columns = map(this, {
data: () => {
const configFn = this.args.named?.columns;
const configFn = this.#config.columns;

if (!configFn) return [];

Expand Down
5 changes: 2 additions & 3 deletions test-app/tests/unit/table-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

import { headlessTable } from '@universal-ember/table';
import { use } from 'ember-resources';

import type {
ColumnConfig,
Expand All @@ -29,9 +28,9 @@ function withTestDefaults(
module('Unit | -private | table', function (hooks) {
setupTest(hooks);

test('supports @use', async function (assert) {
test('supports an owner that is assigned after construction', async function (assert) {
class TestObject {
@use table = headlessTable({
table = headlessTable(this, {
columns: () => [
{ key: 'firstName', name: 'First name' },
{ key: 'lastName', name: 'Last name' },
Expand Down
Loading