Skip to content

feat(ember-form): add Ember adapter (@tanstack/ember-form) - #2156

Draft
NullVoxPopuli-ai-agent wants to merge 19 commits into
TanStack:mainfrom
NullVoxPopuli-ai-agent:ember-form-adapter
Draft

NullVoxPopuli-ai-agent wants to merge 19 commits into
TanStack:mainfrom
NullVoxPopuli-ai-agent:ember-form-adapter

Conversation

@NullVoxPopuli-ai-agent

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented May 8, 2026

Copy link
Copy Markdown

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.

import { createForm } from '@tanstack/ember-form';

const SignupForm = createForm({
  defaultValues: { firstName: '' },
});

<template>
  <SignupForm @onSubmit={{save}} as |tanstackForm|>
    <tanstackForm.Field @name="firstName" as |field|>
      <input
        value={{field.state.value}}
        {{on "input" (fn handleInput field)}}
      />
    </tanstackForm.Field>

    <tanstackForm.Subscribe @selector={{pickSubmit}} as |state|>
      <button type="submit" disabled={{state.cantSubmit}}>Submit</button>
    </tanstackForm.Subscribe>
  </SignupForm>
</template>

API

createForm(baseOptions) returns a component. Call it in module scope. Each arg on the component, for example @onSubmit, overrides the same key in baseOptions.

The component yields the FormApi with these additions:

  • Field renders one field. field.state is autotracked.
  • Subscribe yields a selection of the form state.
  • useSelector(selector?) reads form state in JavaScript through an autotracked current property.

Field and Subscribe are 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/ember has a quick start and seven guides, ported from the Svelte docs.

Not in this PR

  • FormGroup
  • createFormCreator and AppField
  • A changeset, and an entry in the fixed group of .changeset/config.json. Tell me which version line you want for the first release.

Notes for review

  • knip cannot read gts files, so packages/ember-form is in ignoreWorkspaces.
  • Prettier skips gts files. The other files use the root config.
  • Ember projects test in a browser, because end users do not run node or a fake DOM. The tests run in Chrome through testem, under the script test:browser.
  • Nx agents have no browser, so pr.yml has a new job, "Test (browser)", that runs test:browser on the GitHub runner, one time with the installed ember-source and one time with 7.1. test:ci includes it too, because the release workflow does not use agents. This PR changes nx.json, the root package.json, and pr.yml for that.
  • form-core writes to its stores when a field mounts. In Ember that happens during a render, so the adapter delays store notifications by one microtask. src/-private/track-store.ts has the details.

Checks

I ran these locally on the rebased branch, and all of them pass:

  • test:eslint, test:types, build, test:build
  • test:browser: 28 of 28 tests on ember-source 7.3.0 and on 7.1.0, and also through pnpm run test:browser from the root
  • test:types includes type tests that use @glint-expect-error
  • test:knip, test:sherif, test:docs

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Author

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.

Comment thread packages/ember-form/config/ember-cli-update.json Outdated
@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Author

Pushed a follow-up: createForm now exposes a closure-bound Field so consumers can write <this.form.Field @name="..." /> instead of <Field @form={{this.form}} @name="..." />. Matches svelte-form's <form.Field> ergonomics. Tests + demo + README updated; full pipeline still green (8/8 rendering tests, eslint, ember-tsc, publint, rollup build).

Comment thread packages/ember-form/src/-private/tracked-state.ts Outdated
Comment thread packages/ember-form/src/components/field.gts Outdated
Comment thread packages/ember-form/src/components/field.gts Outdated
Comment thread packages/ember-form/src/components/subscribe.gts Outdated
Comment thread packages/ember-form/README.md Outdated
Comment thread packages/ember-form/README.md Outdated
Comment thread packages/ember-form/rollup.config.mjs Outdated
Comment thread packages/ember-form/rollup.config.mjs Outdated
Comment thread packages/ember-form/rollup.config.mjs Outdated
Comment thread packages/ember-form/vite.config.mjs Outdated
Comment thread packages/ember-form/tests/integration/create-form-test.gts Outdated

@NullVoxPopuli NullVoxPopuli left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

lets of updates needed -- be sure to update the docs in this repo as well

NullVoxPopuli-ai-agent pushed a commit to NullVoxPopuli-ai-agent/form that referenced this pull request May 8, 2026
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>
@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Author

Pushed a review-response commit (6c06223). Summary against your inline notes:

Mechanical cleanup

  • Deleted config/ and .env.development
  • rollup.config.mjs: removed addon.appReexports, addon.hbs, addon.keepAssets, plus the empty ember-addon.app-js block in package.json
  • vite.config.mjs + babel.config.cjs: stripped all ENABLE_COMPAT_BUILD branches; dropped @embroider/compat and ember-template-lint from devDeps; dropped the lint:hbs* scripts
  • @ember/test-waiters was unused — removed

Reactivity

  • Deleted src/-private/tracked-state.ts. createForm, <Field>, and <Subscribe> now use trackedObject (imported from @glimmer/validator, where @ember/reactive/collections re-exports it from — that path isn't yet in @embroider/addon-dev's virtual-peer list, so the underlying import avoids the resolver warning). Min ember-source peer is now ^6.8.0.
  • <Field> args are reactive: a @cached _syncArgs getter calls api.update(...) whenever any this.args.* changes, mirroring svelte-form's $effect.pre. Read in the template so the autotracking entanglement actually fires.
  • Added a comment on the Object.defineProperty(api, 'state', ...) shadowing inside <Field> explaining why we replace the prototype getter (it's intentionally unconventional).
  • <Subscribe>'s signature is now parameterized over the form generics; the any[]s in FormState<...> are gone.

Tests

  • Split create-form-test.gts into 4: create-form-test, field-test, subscribe-test, and a new field-reactive-args-test that swaps @validators at runtime and asserts the new validator runs (covers the @cached _syncArgs flow). 10/10 pass.

Docs

  • Added docs/framework/ember/quick-start.md and 7 guides (basic-concepts, validation, dynamic-validation, async-initial-values, arrays, linked-fields, form-composition), all ported faithfully from svelte's. Wired into docs/config.json for both Getting Started and Guides; pnpm test:docs (link verifier) passes.
  • README: compat updated to ember-source 6.8+, dropped the @glimmer/component peer mention, added a link to the new docs.

Notes for your re-review

  • form-composition.md lacks an App-form section — createFormCreator doesn't exist on the Ember adapter yet, and I called that out near the top of the guide and used Glimmer wrapper components instead. Happy to add createFormCreator in a follow-up if you want it as part of this PR.
  • async-initial-values.md demonstrates reactiveweb's trackedFunction for the data-loading example. If you'd rather a different primitive (warp-drive, Ember Data, route models), it's a one-snippet swap.
  • I added NODE_ENV=development to the test:lib script so @embroider/macros runs in runtime mode; without it, setTesting(true) in tests/test-helper.js fails at compile time once the compat plugin is gone.

Full pipeline still green: 10/10 rendering tests, eslint, ember-tsc, publint --strict, rollup build, sherif, and pnpm test:docs.

Comment thread docs/framework/ember/guides/arrays.md Outdated
Comment thread docs/framework/ember/guides/basic-concepts.md Outdated
Comment thread docs/framework/ember/guides/basic-concepts.md Outdated
@crutchcorn

Copy link
Copy Markdown
Member

@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? :)

@NullVoxPopuli

Copy link
Copy Markdown

bah, looks like I have to take over. Agent is refusing to do work

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Author

Pushed (force-with-lease, replaced the WIP commit): bumped to ember-source@^7.1.0-alpha.5 and stripped all on/fn imports. The alpha is what does it — 7.0.0-beta.1's strict-mode pass still rejects unbound on/fn, but the alpha's template build transforms compile them into scope automatically. eslint config also got on/fn/hash/array/concat/get declared as readonly globals for .gts/.gjs so no-undef stays quiet. All five pipeline targets green (10/10 tests, eslint, ember-tsc, publint --strict, rollup build) plus sherif and test:docs.

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Author

Pushed (7335723d) — addressed all the inline review notes from the latest pass + a self-cleanup sweep.

Inline review notes

  • field.gts why-comment on Object.defineProperty(api,'state',...): expanded to spell out (a) why the shadow is necessary (autotracking can only hit a tracked proxy, not the bare store) and (b) why a subclass isn't an option (form-core constructs FieldApi instances internally, including for sub-fields).
  • create-form-test.gts: actually split by concept this time — handle-submit-test, use-store-test, bound-field-test (the prior commit left all three concepts in one file).
  • docs/arrays.md: dropped the unused i block param from the basic example; the full example below still uses it for nameAt.
  • docs/basic-concepts.md: each formOpts / Person code block is now self-contained instead of leaning on context from earlier blocks.

Self-cleanup

  • Dropped import { hash } from '@ember/helper' everywhere — ember-source 7.1's build transforms compile hash into template scope alongside on/fn. Quick-start now describes this accurately instead of telling readers to import them.
  • README compatibility line bumped to "ember-source 7.1+".
  • create-form.gts: fixed stale doc-comments ("@Tracked via TrackedValue", "Typed via as never") and updated the inline example to use <this.form.Field>.
  • package.json: dropped unused devDeps (concurrently, @embroider/core, prettier-plugin-ember-template-tag, prettier, @ember/app-tsconfig), the redundant blueprint scripts (lint/lint:fix/lint:format/format), and the ember: { edition: octane } block.
  • Deleted .editorconfig, CONTRIBUTING.md, LICENSE.md — duplicates or out-of-date scaffold leftovers. Trimmed .gitignore to v2-addon essentials.
  • Minor: dropped the giant comment block in eslint.config.mjs, the import.meta.glob placeholder in tests/test-helper.js.

Pipeline still green: eslint, ember-tsc, 10/10 testem tests, publint --strict, rollup build, sherif, pnpm test:docs.

Comment thread docs/framework/ember/guides/arrays.md Outdated
Comment thread docs/framework/ember/guides/arrays.md Outdated
@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Author

Pushed (8f0a3e22): addressed both new comments.

Inline review notes (latest pass)

  • docs/arrays.md:36 — code snippets no longer have leading blank lines after the fence (whole-doc cleanup, several spots).
  • docs/arrays.md:72 — refactored createForm from a this-taking resource into a module-scope component factory. Same direction you sketched: pass base options at module scope, the returned component takes per-instance args (most importantly @onSubmit) as overrides, and yields the FormApi to its block.

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 extended FormApi (with Field and useStore). Block param is capitalized so <Form.Field> doesn't visually clash with the HTML <form> element. Per-instance args (@onSubmit, @validators, ...) merge over base options and call api.update(...) on each render — same reactive-args semantics as the previous Field-level _syncArgs.

Updates

  • src/create-form.gts — rewrite as factory (makeFormComponent builds a typed Glimmer component class).
  • 6 test files rewritten for the new API; all 10 still pass.
  • demo-app/templates/application.gts ported.
  • README + all 8 docs (quick-start + 7 guides) ported in one pass.

Pipeline: eslint, ember-tsc, 10/10 testem tests, publint --strict, rollup build, sherif, and pnpm test:docs all green.

Notes for re-review

  • A few doc spots assumed the new API supports a per-instance @defaultValues override (in async-initial-values.md's loader pattern). Confirmed in the code path: the constructor merges { ...baseOptions, ...this.args } and _syncOptions re-applies on arg changes, so @defaultValues={{this.resolvedData}} does work.
  • The form-composition.md "extract the form type" hint changed shape — ReturnType<typeof createForm> is now a component, not a FormApi. Doc softened to "the form component's yielded block-param type"; happy to add a canonical type expression if you have a preferred one.

Comment thread docs/framework/ember/guides/arrays.md
@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Author

Pushed (f43d0b77) — addressed the name this as form note, and resolved the 17 previously-addressed review threads.

Block-param naming
There's a real Glimmer strict-mode constraint here: a block param named form shadows the HTML <form> element. With as |form|, a <form> tag resolves to the lexical binding and Glimmer tries to render the FormApi as a component (Expected a dynamic component definition … which was: FormApi) — verified in tests.

Per your steer, the convention is now:

  • Default block param: tanstackForm — descriptive, no element collision, and reads clearly next to <tanstackForm.Field>.
  • f when the example includes a literal HTML <form> element — short alias, keeps the markup tight, sidesteps the shadow.

Applied across: src/create-form.gts doc-comments, all 6 test files, demo-app, README (with a short "Block-param naming" note explaining the rule), and all 8 docs. handle-submit-test now renders a real <form {{on "submit" (onSubmitFor f)}}> with f, so the shadowing-safe path is covered by a test.

Resolved threads
Marked the 17 prior threads resolved (config folder, trackedObject, field why-comment, reactive args, subscribe anys, README ember-source version + peer dep, rollup plugins ×3, vite compat, test-file split, arrays i, basic-concepts formOpts, "good callout", arrays leading line-breaks, createForm-as-factory). Left this naming thread open until you've had a look.

Pipeline green on default heap: eslint, ember-tsc, 10/10 testem tests, publint --strict, rollup build, sherif, pnpm test:docs.

NullVoxPopuli and others added 11 commits September 17, 2026 10:33
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>
@socket-security

socket-security Bot commented Sep 17, 2026

Copy link
Copy Markdown

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.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm data-urls is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/@embroider/addon-dev@8.4.0npm/@embroider/vite@1.7.13npm/data-urls@5.0.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/data-urls@5.0.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm rimraf is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/@embroider/addon-shim@1.10.3npm/@embroider/addon-dev@8.4.0npm/@embroider/macros@1.21.1npm/@embroider/vite@1.7.13npm/ember-source@7.3.0npm/rimraf@5.0.10

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/rimraf@5.0.10. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm rrweb-cssom is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/@embroider/addon-dev@8.4.0npm/@embroider/vite@1.7.13npm/rrweb-cssom@0.7.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/rrweb-cssom@0.7.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm rrweb-cssom is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/@embroider/addon-dev@8.4.0npm/@embroider/vite@1.7.13npm/rrweb-cssom@0.8.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/rrweb-cssom@0.8.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm underscore is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/testem@3.21.0npm/underscore@1.13.8

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/underscore@1.13.8. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Low adoption: npm ember-estree

Location: Package overview

From: pnpm-lock.yamlnpm/ember-eslint-parser@0.14.6npm/ember-estree@0.6.11

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/ember-estree@0.6.11. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Deprecated by its maintainer: npm source-map-url

Reason: See https://github.com/lydell/source-map-url#deprecated

From: pnpm-lock.yamlnpm/@embroider/addon-dev@8.4.0npm/@embroider/vite@1.7.13npm/source-map-url@0.3.0

ℹ Read more on: This package | This alert | What is a deprecated package?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Research the state of the package and determine if there are non-deprecated versions that can be used, or if it should be replaced with a new, supported solution.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/source-map-url@0.3.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Deprecated by its maintainer: npm source-map-url

Reason: See https://github.com/lydell/source-map-url#deprecated

From: pnpm-lock.yamlnpm/@embroider/vite@1.7.13npm/source-map-url@0.4.1

ℹ Read more on: This package | This alert | What is a deprecated package?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Research the state of the package and determine if there are non-deprecated versions that can be used, or if it should be replaced with a new, supported solution.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/source-map-url@0.4.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

NullVoxPopuli and others added 6 commits September 17, 2026 11:11
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>
Comment thread packages/ember-form/src/-private/track-store.ts Outdated
Comment thread docs/framework/ember/quick-start.md Outdated
Comment thread packages/ember-form/eslint.config.mjs Outdated
NullVoxPopuli and others added 2 commits September 17, 2026 19:23
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>
Comment thread .github/workflows/pr.yml
directory: packages
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-browser:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

we test in the browser, becaues thats where our users are

(the typical (end-)user is not running jsdom in node)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants