Skip to content

Remove ember-modify-based-class-resource, derive from data instead - #193

Merged
NullVoxPopuli merged 2 commits into
universal-ember:mainfrom
NullVoxPopuli-ai-agent:remove-class-resource
Aug 19, 2026
Merged

Remove ember-modify-based-class-resource, derive from data instead#193
NullVoxPopuli merged 2 commits into
universal-ember:mainfrom
NullVoxPopuli-ai-agent:remove-class-resource

Conversation

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown

Removes ember-modify-based-class-resource from @universal-ember/table. The Table is a plain class now, and every value is derived from the config that it was constructed with.

This unblocks josemarluedke/frontile#415. Two bundled copies of ember-modify-based-class-resource register the same usable type twice, which throws type may not overlap with an existing usable and blanks the page. Apps no longer need the direct-dependency or Vite-alias workaround.

What changed

  • Table no longer extends Resource. The constructor takes (parent, config) and calls link(this, parent) from reactiveweb/link, which gives the table the parent's owner and destroys it with the parent.
  • modify() is gone. preferences is a cached getter, so a preferences thunk that reads tracked data restores again on its own.
  • Plugin instances survive a preferences change now. Before, any consumed arg change reassigned args and rebuilt every plugin.
  • ember-modify-based-class-resource and ember-resources are dropped from dependencies. Nothing in src imports either one. reactiveweb brings ember-resources itself.

Breaking changes

Before After
@use table = headlessTable({ ... }) table = headlessTable(this, { ... })
table.args.named table.config
scroll container reset on config change call table.resetScrollContainer()

The two-argument headlessTable(this, { ... }) form is unchanged, so most consumers need no code change. frontile is one of them. The one-argument form asserts in development with a message that says what to do.

Docs: docs-app/src/templates/1-get-started/upgrading.gjs.md.

Verification

  • pnpm turbo --filter test-app test: 187 pass, 0 fail. This covers the multi-table shared-preferences reactivity test, plus a new test for an owner that is assigned after construction.
  • pnpm turbo lint: 4 packages pass, types and format included.
  • pnpm turbo build: dist/-private/table.js imports reactiveweb only.

Please label this breaking.

🤖 Generated with Claude Code

The Table was a class-based Resource. Two bundled copies of
ember-modify-based-class-resource register the same usable type twice, which
throws "type may not overlap with an existing usable" and blanks the page.
The table needs lifetime linking, not resources, so it is a plain class now:
`link(this, parent)` gives it the parent's owner and destruction, and every
value is derived from the config it was constructed with.

Preferences move from the `modify` hook to a cached getter, so a `preferences`
thunk that reads tracked data restores again on its own. Plugin instances now
survive that, instead of being thrown away whenever any consumed arg changed.

Breaking:
- `headlessTable` requires a parent object. The `@use headlessTable({...})`
  form is gone.
- `table.args` is gone. Read `table.config`.
- The scroll container is no longer reset when the config changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread table/src/-private/table.ts Outdated
*/
scrollContainerElement?: HTMLElement;
get #owner(): Owner | undefined {
const existing = getOwner(this);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

link sets up teh owner, so we can getOwner + assert here, and return the owner, and no more

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Moved the assert into the owner getter, so plugins just reads it. Kept a fallback to the parent, because link alone does not cover every case.

A parent can receive its owner after the table is created. A class field initializes before setOwner runs on a manually constructed object, so link runs while getOwner(parent) is still undefined. 41 setups in the test suite do this:

ctx = new DefaultOptions(); // the table is built in a class field here
setOwner(ctx, this.owner);

With getOwner(this) + assert and nothing else, that suite goes from 187 pass / 0 fail to 24 pass / 45 fail, every failure being The Table does not have an owner.

So the getter now is:

get #owner(): Owner {
  let owner = getOwner(this);

  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;
}

Back to 187 pass / 0 fail. If you would rather drop the late-owner support and make those setups call setOwner before they build the table, say so and I will change the tests instead.

The plugins getter no longer asserts. The owner getter does, because that is
where the lookup happens.

`link` cannot be the only source of the owner. A parent can receive its owner
after the table is created, since a class field initializes before `setOwner`
runs on a manually constructed object. 41 test setups do exactly that.
Reading the owner from the parent on first use covers them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@NullVoxPopuli
NullVoxPopuli merged commit de9b531 into universal-ember:main Aug 19, 2026
10 of 14 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants