feat(ember-form): add Ember adapter (@tanstack/ember-form) - #2156
NullVoxPopuli-ai-agent wants to merge 19 commits into
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
cc @NullVoxPopuli — drafted per your direction (Ember 6 v2 addon, gjs/gts only, modeled on svelte-form). Couldn't add you as a reviewer programmatically due to fork permissions; please assign yourself when convenient. |
|
Pushed a follow-up: |
NullVoxPopuli
left a comment
There was a problem hiding this comment.
lets of updates needed -- be sure to update the docs in this repo as well
Per review on TanStack#2156: * drop scaffold cruft: config/, .env.development, ENABLE_COMPAT_BUILD branches, addon.appReexports/hbs/keepAssets, ember-template-lint, @embroider/compat, @ember/test-waiters * replace TrackedValue helper with @glimmer/validator#trackedObject (the same primitive @ember/reactive/collections re-exports). Bumps minimum ember-source peer to ^6.8.0 and removes -private/tracked-state.ts. * make Field args reactive: a @cached _syncArgs getter calls api.update() whenever any this.args.* changes, mirroring svelte-form's $effect.pre * add why-comment on Object.defineProperty(api, 'state', ...) explaining the intentional shadowing of FieldApi#state for tracked reads * parameterize SubscribeSignature generics so consumers no longer see any-typed selector params * split tests by concept: create-form, field, subscribe, and field-reactive-args (which exercises the new @cached args sync) * docs/framework/ember/: full port of svelte-form's quick-start + 7 guides (basic-concepts, validation, dynamic-validation, async-initial-values, arrays, linked-fields, form-composition), wired into docs/config.json * README: bump compat to ember-source 6.8+, drop @glimmer/component peer note, link to the new docs NODE_ENV=development is set on test:lib so @embroider/macros runs in runtime mode (required for setTesting in test-helper.js). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed a review-response commit (6c06223). Summary against your inline notes: Mechanical cleanup
Reactivity
Tests
Docs
Notes for your re-review
Full pipeline still green: 10/10 rendering tests, eslint, ember-tsc, publint --strict, rollup build, sherif, and |
|
@NullVoxPopuli given the noise on this PR, can you message me via DMs on Bsky/Discord when this is ready for review or when you have clarifying questions? :) |
|
bah, looks like I have to take over. Agent is refusing to do work |
73ecdf2 to
3887537
Compare
|
Pushed (force-with-lease, replaced the WIP commit): bumped to |
|
Pushed ( Inline review notes
Self-cleanup
Pipeline still green: eslint, ember-tsc, 10/10 testem tests, publint --strict, rollup build, sherif, |
|
Pushed ( Inline review notes (latest pass)
Before: class App extends Component {
form = createForm(this, { defaultValues: {...}, onSubmit: ... });
<template>
<this.form.Field @name="x" as |field|>...</this.form.Field>
</template>
}After: const MyForm = createForm({ defaultValues: {...} });
class App extends Component {
handleSubmit = ({ value }) => { ... };
<template>
<MyForm @onSubmit={{this.handleSubmit}} as |Form|>
<Form.Field @name="x" as |field|>...</Form.Field>
</MyForm>
</template>
}The yielded Updates
Pipeline: eslint, ember-tsc, 10/10 testem tests, publint --strict, rollup build, sherif, and Notes for re-review
|
|
Pushed ( Block-param naming Per your steer, the convention is now:
Applied across: Resolved threads Pipeline green on default heap: eslint, ember-tsc, 10/10 testem tests, publint --strict, rollup build, sherif, |
Introduces @tanstack/ember-form, an Ember v2 addon (Ember 6+, gjs/gts) wrapping @tanstack/form-core via Glimmer's @Tracked autotracking. - createForm(parent, opts): returns FormApi with reactive useStore(selector) - <Field @Form @name>: yields a FieldApi whose .state is autotracked - <Subscribe @Form @selector>: yields a reactive slice of form state - All tests pass (test:lib testem-on-Chrome) plus test:eslint, test:types, test:build (publint), and build (rollup + ember-tsc declarations). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an ergonomic shorthand: createForm now exposes a Field component bound to the owning form via lexical scope, so consumers can write <this.form.Field @name="firstName" as |field|> ... </this.form.Field> instead of repeating @Form={{this.form}} everywhere. Mirrors svelte-form's <form.Field> shape. Also updates the demo-app + README to use the bound form, and adds a rendering test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per review on TanStack#2156: * drop scaffold cruft: config/, .env.development, ENABLE_COMPAT_BUILD branches, addon.appReexports/hbs/keepAssets, ember-template-lint, @embroider/compat, @ember/test-waiters * replace TrackedValue helper with @glimmer/validator#trackedObject (the same primitive @ember/reactive/collections re-exports). Bumps minimum ember-source peer to ^6.8.0 and removes -private/tracked-state.ts. * make Field args reactive: a @cached _syncArgs getter calls api.update() whenever any this.args.* changes, mirroring svelte-form's $effect.pre * add why-comment on Object.defineProperty(api, 'state', ...) explaining the intentional shadowing of FieldApi#state for tracked reads * parameterize SubscribeSignature generics so consumers no longer see any-typed selector params * split tests by concept: create-form, field, subscribe, and field-reactive-args (which exercises the new @cached args sync) * docs/framework/ember/: full port of svelte-form's quick-start + 7 guides (basic-concepts, validation, dynamic-validation, async-initial-values, arrays, linked-fields, form-composition), wired into docs/config.json * README: bump compat to ember-source 6.8+, drop @glimmer/component peer note, link to the new docs NODE_ENV=development is set on test:lib so @embroider/macros runs in runtime mode (required for setTesting in test-helper.js). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… imports
Per review request. ember-source 7.x's template build transforms compile
`on`/`fn` (and other built-in keywords) directly into the template scope,
so they no longer need explicit imports.
- ember-source peer + dev → ^7.1.0-alpha.5
- babel-plugin-ember-template-compilation → ^3.1.0 (matches the 7-beta
@ember/app-blueprint shape)
- All `import { on } from '@ember/modifier'` and `import { fn } from
'@ember/helper'` removed from src, tests, demo-app, README, and the 8
docs files
- eslint.config.mjs: declare `on`, `fn`, `hash`, `array`, `concat`, `get`
as readonly globals for `.gts`/`.gjs` (template-implicit, not JS-implicit,
but easier than per-file overrides)
All five pipeline targets pass: test:eslint, test:types (ember-tsc),
test:lib (10/10 rendering tests under testem+Chrome), test:build (publint
--strict), build (rollup + ember-tsc declarations). Sherif and pnpm
test:docs also green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inline review notes (from latest pass on 3887537): - field.gts: expand the why-comment on Object.defineProperty(api,'state',...) so it spells out (a) why the shadow is needed (template autotracking needs to hit a tracked proxy, not the store), and (b) why a subclass isn't an option (form-core constructs FieldApi instances internally). - tests/integration: actually split create-form-test.gts by concept into handle-submit-test, use-store-test, and bound-field-test (the previous "split" left those three concepts in one file). - docs/arrays.md: drop the unused `|person i|` block param from the basic usage example (only the full example needs the index for nameAt). - docs/basic-concepts.md: make each `formOpts` / `Person` example block self-contained instead of relying on context from a prior block. Self-review cleanup: - Drop `import { hash } from '@ember/helper'` everywhere — ember-source 7.1's build transforms add it to template scope alongside `on`/`fn`. Updated docs/README/demo + tests verified the drop. quick-start.md's "you need to import on/fn" note is replaced with the actual behavior. - README compatibility line bumped to "ember-source 7.1+" (matches the peer + dev range), and a short note about the build transforms. - create-form.gts: update stale "@Tracked via TrackedValue" doc comment (we use trackedObject now) and drop the obsolete "Typed via `as never`" note on makeBoundField. Example uses the closure-bound `<this.form.Field>` shorthand to mirror the recommended shape. - package.json: drop unused devDeps (concurrently, @embroider/core, prettier-plugin-ember-template-tag, prettier, @ember/app-tsconfig), redundant `lint`/`lint:fix`/`lint:format`/`format` blueprint scripts, and the `ember: { edition: octane }` block (only relevant to classic). - Delete `.editorconfig`, `CONTRIBUTING.md`, `LICENSE.md` — duplicate or out-of-date scaffold leftovers. Trim `.gitignore` to v2-addon essentials. - eslint.config.mjs: drop the giant intro comment block. - tests/test-helper.js: drop the `import.meta.glob('./services/...')` placeholder comment. Pipeline still green: test:eslint, test:types (ember-tsc), test:lib (10/10 rendering tests under testem+Chrome), test:build (publint --strict), build (rollup + ember-tsc declarations), sherif, and test:docs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per review note on docs/arrays.md:72 — createForm shouldn't need a
destroyable `this`. It's now a module-scope factory that returns a
Glimmer component, and the component's args override the base options.
Before:
class App extends Component {
form = createForm(this, { defaultValues: {...}, onSubmit: ... });
<template>
<this.form.Field @name="x" as |field|>...</this.form.Field>
</template>
}
After:
const MyForm = createForm({ defaultValues: {...} });
class App extends Component {
handleSubmit = ({ value }) => { ... };
<template>
<MyForm @onsubmit={{this.handleSubmit}} as |Form|>
<Form.Field @name="x" as |field|>...</Form.Field>
</MyForm>
</template>
}
The yielded `Form` is the FormApi extended with `Field` (closure-bound)
and `useStore`. We capitalize the block param to keep it from visually
clashing with the HTML `<form>` element. Per-instance args (`@onSubmit`,
`@validators`, ...) override the matching base option via api.update()
on each render.
Also fixed `docs/arrays.md:36` (leading blank lines in code snippets)
that the reviewer flagged at the same time.
Updates included:
- src/create-form.gts: rewrite as a factory; `makeFormComponent` builds
a typed Glimmer component class with merged-options-on-render via
`_syncOptions`.
- 6 test files rewritten for the new API; all 10 tests still pass.
- demo-app/templates/application.gts ported.
- README + all 8 docs (quick-start + 7 guides) ported by a parallel
docs-port pass.
Pipeline: eslint, ember-tsc, 10/10 testem tests, publint --strict,
rollup build, sherif, and test:docs all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per review (docs/arrays.md:15): name the yielded form block param
`tanstackForm` by default, and `f` in examples that include a literal
HTML `<form>` element.
Why: in Glimmer strict mode a block param shadows same-named HTML
elements. A param named `form` makes `<form>` resolve to the lexical
binding, so Glimmer tries to render the FormApi object as a component
("Expected a dynamic component definition ... which was: FormApi").
Capitalized `Form` avoids that but doesn't match tanstack's lowercase
`form` convention; `tanstackForm` is descriptive and collision-free,
and the short `f` keeps `<form>`-wrapped examples tight.
- src/create-form.gts: doc-comment examples + the note on the bound
`Field` updated to the convention.
- tests: all 6 files use `tanstackForm`; handle-submit-test uses `f`
with a real `<form {{on "submit"}}>` element (also exercises the
shadowing-safe path). 10/10 pass on default heap.
- demo-app + README: `<form>`-wrapped, so `f`; README API section
documents the rule.
- All 8 docs ported: `tanstackForm` by default, `f` in the code blocks
that contain an HTML `<form>` element (quick-start, arrays full
example, async-initial-values, dynamic-validation AgeForm).
Pipeline: eslint, ember-tsc, 10/10 testem tests, publint --strict,
rollup build, sherif, test:docs all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stylistic pass after cross-checking my code against NVP-authored repos (NullVoxPopuli/limber, universal-ember/ember-primitives): - createForm return type is now `ComponentLike<FormComponentSignature<...>>` (matches `ember-primitives/src/load.gts`), replacing my earlier `abstract new (owner, args) => Component<...>` workaround. Added `@glint/template` to devDeps for the type-only import. - rollup.config.mjs: addon.publicEntrypoints excludes `-private/**` — matches the convention NVP uses for non-exported components. - subscribe.gts: tagged SubscribeSignature with @public JSDoc. Decisions I deliberately did *not* take from the review: - Keep the `Object.defineProperty(api, 'state', ...)` shadow in field.gts. Form-core's FieldApi has internal methods that read `this.state` — shadowing on the instance is the only way to make those internal reads hit the tracked mirror. A `@cached get state()` on the wrapper would only catch template reads, not form-core's own reads. - Keep `as unknown as TSelected` (double cast) in subscribe.gts; safer than `as any` and the type only matters at the boundary. Pipeline: eslint, ember-tsc, 10/10 testem tests, publint --strict, rollup build, sherif, test:docs all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The autofix workflow runs `pnpm format` on each pull request. The package used semicolons, which the root config removes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…owner
Types
- `<tanstackForm.Field>` yielded `unknown`, and `@form` was `AnyFormApi`,
so no template had types. `Field` and `Subscribe` now infer from `@form`
and `@name`.
- `test:types` covers tests and the demo app. It found that
`{{on "click" f.reset}}` passes the event to `reset` as the new values.
Reactivity
- `useStore` added a subscription per call, and only the destruction of
the form removed it. `useSelector` replaces it (upstream renamed the
API) and reads through the one subscription that the form owns.
- `field.state` returns the live store state. The previous copy in a
`trackedObject` could lag behind the store.
- Store notifications wait for a microtask. form-core writes to its stores
when a field mounts, which is during a render, and autotracking throws if
a `Subscribe` before that field already read the state.
- The separate `{{this._syncArgs}}` render step is gone. The yielded value
applies the args when it is read.
API
- The form yields `Subscribe`, the same as the other adapters.
- The yielded `Field` is a subclass with the form set, so new field
options need no change in this package.
Build
- Tests import `src`. They imported `dist`, which does not exist when nx
runs `test:lib` on a clean checkout.
- knip cannot read gts, so it skips this package.
- ember-source 7.1 is stable. The peer range no longer names an alpha.
Docs
- The async guide uses `getPromiseState` and no longer suggests
`{{didUpdate}}`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
0da50ff to
7e39a41
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub. |
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
The quick start had only a template-only example. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A name in scope hides the HTML element of the same name in a strict-mode template. That applies to a module-scope `const` and to a block param, and the docs covered only the block param, in three places. One note in basic-concepts now covers both, and the quick start and the arrays guide link to it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
New browser tests: - `swapValues` on an array of the same length - array subfields - `form.reset()` - a changed `@defaultValues`, with and without user input - a removed field, and its `onUnmount` listener - `onBlur`, and `onChange` on an untouched field - `onChangeAsync`, with and without debounce - `onChangeListenTo` `tests/types/templates.gts` holds type tests. `@glint-expect-error` marks each template that must not type-check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nx agents install a browser only when the root package.json lists Playwright or Cypress, so `test:lib` for this package cannot pass there. The script is now `test:browser`. The PR workflow runs it in its own job on ubuntu-latest, which has Chrome. `test:ci` includes it, because the release workflow does not use agents. The README has a Development section that says why the tests use a browser. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`trackStore` had one tag, so every store change invalidated every reader. It now keeps one tag per state key in a `trackedObject`, which compares with `Object.is`. A key that keeps its value does not invalidate its readers. A selector that reads only `state.isSubmitting` no longer runs again on each keystroke. A new test covers that. Reads still return the live store value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`trackedObject` is the newest API that the package uses, and 6.8 has it. The templates of the package use no keywords. The tests import `on`, `fn`, and `hash`, because 6.8 needs the imports. All 28 tests pass on ember-source 6.8.4 and on 7.3.0. The browser job in pr.yml runs both. The docs keep the 7.1 syntax and say which imports older versions need. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
form-core creates every state key up front and never adds one, so a plain object with one getter per key does the same work as the Proxy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reverts b8acc5c. The docs and the tests rely on `on`, `fn`, and `hash` in template scope, which ember-source 7.1 added. The peer range is now `>= 7.1.0`. The browser job in pr.yml runs the tests on the installed version and on 7.1.0. All 28 tests pass on both. The eslint globals for the keywords stay removed. eslint-plugin-ember 13.5 and ember-eslint-parser 0.14 know the keywords. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| directory: packages | ||
| env: | ||
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | ||
| test-browser: |
There was a problem hiding this comment.
we test in the browser, becaues thats where our users are
(the typical (end-)user is not running jsdom in node)
Adds
@tanstack/ember-form, an adapter for Ember on top of@tanstack/form-core.It supports ember-source 7.1 or later, with gjs and gts only. Development uses ember-source 7.3. The package is a v2 addon.
API
createForm(baseOptions)returns a component. Call it in module scope. Each arg on the component, for example@onSubmit, overrides the same key inbaseOptions.The component yields the
FormApiwith these additions:Fieldrenders one field.field.stateis autotracked.Subscribeyields a selection of the form state.useSelector(selector?)reads form state in JavaScript through an autotrackedcurrentproperty.FieldandSubscribeare also exports that take@form.Types infer from the form and from
@name, so templates get the same checks as the other adapters.Docs
docs/framework/emberhas a quick start and seven guides, ported from the Svelte docs.Not in this PR
FormGroupcreateFormCreatorandAppFieldfixedgroup of.changeset/config.json. Tell me which version line you want for the first release.Notes for review
packages/ember-formis inignoreWorkspaces.test:browser.pr.ymlhas a new job, "Test (browser)", that runstest:browseron the GitHub runner, one time with the installed ember-source and one time with 7.1.test:ciincludes it too, because the release workflow does not use agents. This PR changesnx.json, the rootpackage.json, andpr.ymlfor that.src/-private/track-store.tshas the details.Checks
I ran these locally on the rebased branch, and all of them pass:
test:eslint,test:types,build,test:buildtest:browser: 28 of 28 tests on ember-source 7.3.0 and on 7.1.0, and also throughpnpm run test:browserfrom the roottest:typesincludes type tests that use@glint-expect-errortest:knip,test:sherif,test:docs🤖 Generated with Claude Code