feat(ember-form): add Ember adapter (@tanstack/ember-form)#2156
feat(ember-form): add Ember adapter (@tanstack/ember-form)#2156NullVoxPopuli-ai-agent wants to merge 3 commits intoTanStack:mainfrom
Conversation
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>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. |
There was a problem hiding this comment.
let's delete this and the config folder -- for this project, keeping up with the blueprint upstream doesn't matter
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>
|
Pushed a follow-up: |
There was a problem hiding this comment.
we should not use this. use trackedObject (which means our minimum supported ember version is 6.8, which is fine)
| this.#api = new FieldApi({ form, name, ...rest } as never); | ||
|
|
||
| const stateBox = new TrackedValue(this.#api.store.state); | ||
| Object.defineProperty(this.#api, 'state', { |
There was a problem hiding this comment.
this is going to need a "why" comment about why this is here -- as this is a sort of unconventional thing to do on someone else's API
| ) { | ||
| super(owner as never, args); | ||
|
|
||
| const { form, name, ...rest } = args; |
There was a problem hiding this comment.
do the arguments hear not need to be reactive?
| selector?: ( | ||
| state: FormState< | ||
| TParentData, | ||
| any, |
|
|
||
| ## Compatibility | ||
|
|
||
| - Ember.js v6.0 or above (gjs/gts only) |
There was a problem hiding this comment.
this should really be ember-source 6.8+
ember.js isn't a package (that's relevant anyway)
| ## Compatibility | ||
|
|
||
| - Ember.js v6.0 or above (gjs/gts only) | ||
| - `@glimmer/component` v2 |
There was a problem hiding this comment.
we don't need to specify this, because package manager peer errors will tell the user
| plugins: [ | ||
| addon.publicEntrypoints(['**/*.js', 'index.js']), | ||
|
|
||
| addon.appReexports([ |
| configFile: babelConfig, | ||
| }), | ||
|
|
||
| addon.hbs(), |
|
|
||
| addon.hbs(), | ||
| addon.gjs(), | ||
| addon.keepAssets(['**/*.css']), |
| import { babel } from '@rollup/plugin-babel'; | ||
|
|
||
| // For scenario testing | ||
| const isCompat = Boolean(process.env.ENABLE_COMPAT_BUILD); |
There was a problem hiding this comment.
we are not going to support / test against compat builds -- remove this (and everything else related to ENABLE_COMPAT_BUILD)
There was a problem hiding this comment.
this test file should be split in to multiple test files, based on the concept being tested
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 |
|
|
||
| <template> | ||
| <this.form.Field @name="people" @mode="array" as |field|> | ||
| {{#each field.state.value as |person i|}} |
There was a problem hiding this comment.
why specify i? do we expect users to care about the index?
|
|
||
| export default class MyForm extends Component { | ||
| form = createForm(this, { | ||
| ...formOpts, |
| } | ||
| ``` | ||
|
|
||
| > Why pass `this`? `createForm` mounts the underlying `FormApi` immediately and registers a destructor against the parent so that store subscriptions are cleaned up when the component is torn down. Passing `this` ties the form's lifetime to your component. |
|
@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? :) |
Summary
Adds
@tanstack/ember-form, a new framework adapter for Ember (6.x, gjs/gts only, v2 addon) wrapping@tanstack/form-core.createForm(parent, options)— Returns the underlyingFormApiextended withuseStore(selector?). The form is mounted immediately and unmounted via@ember/destroyable'sregisterDestructoragainst the supplied parent (typicallythisfrom a@glimmer/component).<Field @form @name [@validators] [@defaultValue] …>— Constructs aFieldApi, manages its mount lifecycle, and yields it. The yieldedfield.stateis autotracked, so reads in templates rerender on store changes.<Subscribe @form [@selector]>— Yields a reactive slice of the form's store, recomputed on subscription updates.form.useStore(selector?)— Imperative variant (e.g. for@cachedgetters).Modeled closely on
@tanstack/svelte-form's API, swapping Svelte runes for Glimmer's@tracked.Implementation notes
.tsfor plain modules,.gtsfor components). Builds via the standard Embroider v2 addon scaffold (@embroider/addon-dev/rollup+ babel +ember-tscfor.d.ts)..statereactive surface is implemented with a smallTrackedValue<T>(a@tracked current: Tbox). On the field's store subscription firing, the box'scurrentis reassigned, which dirties the autotracking tag and schedules a rerender of templates that read it. The same primitive backsuseStoreand<Subscribe>.ember-source's provision of@glimmer/trackingrather than the standalone@glimmer/tracking@1.x, whoseconsume()is a no-op stub that breaks autotracking under modern Ember.Testing
pnpm --filter @tanstack/ember-form▸test:eslinttest:typesember-tsc --noEmittest:libtest:buildbuildTest coverage (Chrome via testem):
<Subscribe>reactivity across selectorsuseStoreselector reactivityhandleSubmit→onSubmit({ value })Open items / questions
test:librequires Chrome (testem).ubuntu-latestships Chrome so it should work in the existing Nx pipeline; happy to swap to a headless setup that mirrors how other adapters run their browser tests if that's preferred.@tanstack/ember-formto.changeset/config.json#fixed— adding it would auto-bump the package to1.xto match the others on first publish. Let me know which path you'd like.form.Field: Svelte exposes<form.Field>via closure-binding; the Ember version requires@form={{this.form}}explicitly. I can add a closure-bound variant if you'd like to mirror the Svelte ergonomics.() => optsso option changes propagate. Ember'screateFormcurrently captures options once; users can callform.update(opts)to re-apply. A@cachedautotracking variant is a possible follow-up.Marking this draft and would love your feedback on shape, ergonomics, and CI integration.
Test plan
pnpm startfrompackages/ember-formruns the demo-app🤖 Generated with Claude Code