diff --git a/docs-app/src/templates/1-get-started/upgrading.gjs.md b/docs-app/src/templates/1-get-started/upgrading.gjs.md new file mode 100644 index 00000000..57c97332 --- /dev/null +++ b/docs-app/src/templates/1-get-started/upgrading.gjs.md @@ -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`. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a986fe9..d7eb1bf0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -253,12 +253,6 @@ importers: ember-modifier: specifier: ^4.3.0 version: 4.3.0(@babel/core@7.29.0) - ember-modify-based-class-resource: - specifier: ^1.1.2 - version: 1.1.2(24a2df5f3e45b9f9d6498ae47b2fbae9) - ember-resources: - specifier: ^7.0.7 - version: 7.0.7(972e7a9dd19717b828f7a881726685f7) reactiveweb: specifier: ^1.9.1 version: 1.9.1(e6a5f41cb5e16588d59169bb30fdd2b0) @@ -3785,15 +3779,6 @@ packages: ember-modifier@4.3.0: resolution: {integrity: sha512-O0rirSLQbGg0VJ/NqoQ4uN1bh2iAekZC/Ykma+FkjCM2ofrO38u+d8n3+AK6uVWeMJmogGX2KL+Is5fofoInJg==} - ember-modify-based-class-resource@1.1.2: - resolution: {integrity: sha512-eruTLd+aMzrXaLDInQSYv3zTcG5+0Ttn69W31JtOp6nSR3T22ET3q+6zZjCVNxmlDufSAu6lSFd9EAtKaazCdA==} - peerDependencies: - '@glimmer/component': ^1.1.2 || >= 2.0.0 - ember-resources: '>= 6.4.0' - peerDependenciesMeta: - '@glimmer/component': - optional: true - ember-page-title@9.0.3: resolution: {integrity: sha512-fedRHUsvq8tIZgOii8jTrfAyeq+la/9H5eAzhNNwEyzo7nDMmqK2SxsyBUGXprd8fOacsPabLlzlucMi/4mUpA==} engines: {node: 16.* || >= 18} @@ -12499,19 +12484,6 @@ snapshots: - '@babel/core' - supports-color - ember-modify-based-class-resource@1.1.2(24a2df5f3e45b9f9d6498ae47b2fbae9): - dependencies: - '@babel/runtime': 7.28.6 - '@embroider/addon-shim': 1.10.2 - '@embroider/macros': 1.20.1(882cf82caec78f226fcbd647b89f3bbe) - ember-resources: 7.0.7(972e7a9dd19717b828f7a881726685f7) - optionalDependencies: - '@glimmer/component': 2.0.0 - transitivePeerDependencies: - - '@babel/core' - - '@glint/template' - - supports-color - ember-page-title@9.0.3: dependencies: '@embroider/addon-shim': 1.10.2 diff --git a/table/package.json b/table/package.json index 1dbc2bc9..e7546a9a 100644 --- a/table/package.json +++ b/table/package.json @@ -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" }, diff --git a/table/src/-private/js-helper.ts b/table/src/-private/js-helper.ts index ebc3de96..de3fffc6 100644 --- a/table/src/-private/js-helper.ts +++ b/table/src/-private/js-helper.ts @@ -1,34 +1,16 @@ +import { assert } from '@ember/debug'; + import { Table } from './table.ts'; import type { TableConfig } from './interfaces'; -type Args = - | [destroyable: object, options: TableConfig] - | [options: TableConfig]; - /** * 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(options: TableConfig): Table; - -/** - * 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 @@ -43,23 +25,14 @@ export function headlessTable(options: TableConfig): Table; * */ export function headlessTable( - destroyable: object, + parent: object, options: TableConfig, -): Table; - -export function headlessTable(...args: Args): Table { - 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>(destroyable, () => options); - } - - const [options] = args; - - return Table.from>(() => options); +): 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); } diff --git a/table/src/-private/table.ts b/table/src/-private/table.ts index bceb8128..25d8653d 100644 --- a/table/src/-private/table.ts +++ b/table/src/-private/table.ts @@ -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 { @@ -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; @@ -30,18 +31,9 @@ const DEFAULT_COLUMN_CONFIG = { minWidth: 128, }; -interface Signature { - Named: TableConfig; -} - /** * 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__'); @@ -54,7 +46,7 @@ const attachContainer = (element: Element, table: Table) => { table.scrollContainerElement = element; }; -export class Table extends Resource> { +export class Table { /** * @private */ @@ -91,45 +83,74 @@ export class Table extends Resource> { /** * @private - * - * Lazy way to delay consuming arguments until they are needed. */ - @tracked declare args: { named: Signature['Named'] }; + scrollContainerElement?: HTMLElement; + + #parent: object; + #config: TableConfig; + + constructor(parent: object, config: TableConfig) { + 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['Named']) { - this.args = { named }; + get config(): TableConfig { + 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); } /** @@ -186,12 +207,10 @@ export class Table extends Resource> { * @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); @@ -202,13 +221,7 @@ export class Table extends Resource> { 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; } @@ -239,18 +252,9 @@ export class Table extends Resource> { 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 []; @@ -261,7 +265,7 @@ export class Table extends Resource> { columns = map(this, { data: () => { - const configFn = this.args.named?.columns; + const configFn = this.#config.columns; if (!configFn) return []; diff --git a/test-app/tests/unit/table-test.ts b/test-app/tests/unit/table-test.ts index 24b18d87..61c396db 100644 --- a/test-app/tests/unit/table-test.ts +++ b/test-app/tests/unit/table-test.ts @@ -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, @@ -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' },