diff --git a/.changeset/config.json b/.changeset/config.json
index 0cfd51439c..7d93c0c8ca 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -1,19 +1,19 @@
{
- "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
+ "$schema": "https://unpkg.com/@changesets/config@4.0.1/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "TanStack/form", "disableThanks": true }
],
"commit": false,
"access": "public",
- "baseBranch": "main",
+ "baseBranch": "alpha",
+ "privatePackages": { "version": false, "tag": false },
"updateInternalDependencies": "patch",
"fixed": [
[
"@tanstack/form-core",
"@tanstack/react-form",
"@tanstack/react-form-start",
- "@tanstack/react-form-remix",
"@tanstack/react-form-nextjs",
"@tanstack/angular-form",
"@tanstack/vue-form",
@@ -27,8 +27,5 @@
]
],
"linked": [],
- "ignore": [],
- "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
- "onlyUpdatePeerDependentsWhenOutOfRange": true
- }
+ "ignore": []
}
diff --git a/.changeset/flat-rats-like.md b/.changeset/flat-rats-like.md
new file mode 100644
index 0000000000..4c118e362b
--- /dev/null
+++ b/.changeset/flat-rats-like.md
@@ -0,0 +1,13 @@
+---
+'@tanstack/form-core': patch
+'@tanstack/form-devtools': patch
+'@tanstack/preact-form': patch
+'@tanstack/react-form': patch
+---
+
+BREAKING: `runOnSubmit` has been renamed in favor of `skipOnSubmit`. That way, it's easier to explain what happens when you omit the property.
+
+Migration:
+
+- rename `runOnSubmit` to `skipOnSubmit`
+- invert the condition you returned.
diff --git a/.changeset/pre.json b/.changeset/pre.json
new file mode 100644
index 0000000000..9a89b8a38f
--- /dev/null
+++ b/.changeset/pre.json
@@ -0,0 +1,4 @@
+{
+ "mode": "pre",
+ "tag": "alpha"
+}
diff --git a/.changeset/pre/brave-lists-replace.md b/.changeset/pre/brave-lists-replace.md
new file mode 100644
index 0000000000..cf377dbac3
--- /dev/null
+++ b/.changeset/pre/brave-lists-replace.md
@@ -0,0 +1,6 @@
+---
+'@tanstack/form-core': minor
+'@tanstack/vue-form': minor
+---
+
+Restore `field.replaceValue()` and add `form.replaceFieldValue()` for replacing an array element in place
diff --git a/.changeset/pre/cyan-papayas-search.md b/.changeset/pre/cyan-papayas-search.md
new file mode 100644
index 0000000000..d3ac150b55
--- /dev/null
+++ b/.changeset/pre/cyan-papayas-search.md
@@ -0,0 +1,10 @@
+---
+'@tanstack/preact-form': minor
+'@tanstack/svelte-form': minor
+'@tanstack/react-form': minor
+'@tanstack/solid-form': minor
+'@tanstack/form-core': minor
+'@tanstack/vue-form': minor
+---
+
+Feature: Specify default options for `createFormHook`
diff --git a/.changeset/pre/fast-yaks-kneel.md b/.changeset/pre/fast-yaks-kneel.md
new file mode 100644
index 0000000000..d46f669297
--- /dev/null
+++ b/.changeset/pre/fast-yaks-kneel.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/form-core': patch
+---
+
+`onSubmit` now has access to `createErrorMap`
diff --git a/.changeset/pre/forty-eyes-fall.md b/.changeset/pre/forty-eyes-fall.md
new file mode 100644
index 0000000000..103c0c7d8d
--- /dev/null
+++ b/.changeset/pre/forty-eyes-fall.md
@@ -0,0 +1,18 @@
+---
+'@tanstack/angular-form': patch
+'@tanstack/preact-form': patch
+'@tanstack/svelte-form': patch
+'@tanstack/react-form': patch
+'@tanstack/solid-form': patch
+'@tanstack/form-core': patch
+'@tanstack/lit-form': patch
+'@tanstack/vue-form': patch
+---
+
+Refactor: Adapter `formOptions`/`appFormOptions` no longer shim the core types and runtime.
+
+BREAKING: `formOptions.looseSchema` and `formOptions.strictSchema` now require a schema as
+first parameter. This locks down inference to get the best type safety out of it vs. the options object alone.
+
+Fix: `formOptions.looseSchema` now allows `defaultValues` to omit properties instead of
+requiring them to be explicitly undefined.
diff --git a/.changeset/pre/gentle-forms-default.md b/.changeset/pre/gentle-forms-default.md
new file mode 100644
index 0000000000..0566f72803
--- /dev/null
+++ b/.changeset/pre/gentle-forms-default.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/react-form': minor
+---
+
+Add default form, field, and form group options to `createFormHook`.
diff --git a/.changeset/pre/grumpy-seals-dig.md b/.changeset/pre/grumpy-seals-dig.md
new file mode 100644
index 0000000000..2c2655ac35
--- /dev/null
+++ b/.changeset/pre/grumpy-seals-dig.md
@@ -0,0 +1,21 @@
+---
+'@tanstack/angular-form': minor
+'@tanstack/form-core': minor
+'@tanstack/preact-form': minor
+'@tanstack/react-form': minor
+---
+
+BREAKING: `schemaOutputs` no longer exists. It is superceded by `validatorOutputs`. This is only a name change, the schema's result is the same as before
+
+`validatorOutputs` now allows a callback validator to specify data that the submit function should know. Schemas do this
+automatically, but now you can do it manually as well:
+`run: ({ createOutput }) => createOutput('This is not a validation error and onSubmit can use this')`
+
+The current map is:
+
+| Validator | `onSubmit: ({ validatorOutputs }) => ...` |
+| -------------------------- | ----------------------------------------- |
+| Schema | Schema's output data |
+| `createOutput(X)` | `X` |
+| Default callback validator | `undefined` |
+| Validation error | N/A, `onSubmit` doesn't run |
diff --git a/.changeset/pre/legal-animals-design.md b/.changeset/pre/legal-animals-design.md
new file mode 100644
index 0000000000..5a049e15ed
--- /dev/null
+++ b/.changeset/pre/legal-animals-design.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/form-core': patch
+---
+
+Docs: Add JSDoc to code and API sections to website
diff --git a/.changeset/pre/odd-comics-push.md b/.changeset/pre/odd-comics-push.md
new file mode 100644
index 0000000000..25de60ef11
--- /dev/null
+++ b/.changeset/pre/odd-comics-push.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/form-core': patch
+---
+
+Fix: Schema output type now properly guards against dynamically enabled validators
diff --git a/.changeset/pre/polite-planets-deny.md b/.changeset/pre/polite-planets-deny.md
new file mode 100644
index 0000000000..436951ea8a
--- /dev/null
+++ b/.changeset/pre/polite-planets-deny.md
@@ -0,0 +1,12 @@
+---
+'@tanstack/angular-form': patch
+'@tanstack/preact-form': patch
+'@tanstack/svelte-form': patch
+'@tanstack/react-form': patch
+'@tanstack/solid-form': patch
+'@tanstack/form-core': patch
+'@tanstack/lit-form': patch
+'@tanstack/vue-form': patch
+---
+
+Fix: Subscription selectors now infer tuple return values without requiring `as const`.
diff --git a/.changeset/pre/quiet-arrays-share.md b/.changeset/pre/quiet-arrays-share.md
new file mode 100644
index 0000000000..497c7165c3
--- /dev/null
+++ b/.changeset/pre/quiet-arrays-share.md
@@ -0,0 +1,6 @@
+---
+'@tanstack/react-form': patch
+'@tanstack/preact-form': patch
+---
+
+Provide the field context inside App Form `ArrayField`, so registered field components render there instead of throwing.
diff --git a/.changeset/pre/seven-trains-play.md b/.changeset/pre/seven-trains-play.md
new file mode 100644
index 0000000000..61940dd422
--- /dev/null
+++ b/.changeset/pre/seven-trains-play.md
@@ -0,0 +1,11 @@
+---
+'@tanstack/preact-form': minor
+'@tanstack/svelte-form': minor
+'@tanstack/react-form': minor
+'@tanstack/solid-form': minor
+'@tanstack/form-core': minor
+'@tanstack/lit-form': minor
+'@tanstack/vue-form': minor
+---
+
+Feat: Field groups may now omit identity bindings like `fields={{ a: 'a' }}`
diff --git a/.changeset/pre/shy-hairs-follow.md b/.changeset/pre/shy-hairs-follow.md
new file mode 100644
index 0000000000..0adaaceb15
--- /dev/null
+++ b/.changeset/pre/shy-hairs-follow.md
@@ -0,0 +1,6 @@
+---
+'@tanstack/form-devtools': patch
+'@tanstack/form-core': patch
+---
+
+Refactor: Use stable validator identity instead of index
diff --git a/.changeset/pre/silent-tools-cry.md b/.changeset/pre/silent-tools-cry.md
new file mode 100644
index 0000000000..5fba16a46c
--- /dev/null
+++ b/.changeset/pre/silent-tools-cry.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/vue-form': patch
+---
+
+Keep Vue field values and validation state reactive in child components, and keep slot and injected field handlers connected after resets and field name changes.
diff --git a/.changeset/pre/stable-listeners-rest.md b/.changeset/pre/stable-listeners-rest.md
new file mode 100644
index 0000000000..9eab863e92
--- /dev/null
+++ b/.changeset/pre/stable-listeners-rest.md
@@ -0,0 +1,6 @@
+---
+'@tanstack/form-core': patch
+'@tanstack/form-devtools': patch
+---
+
+Refactor: Use stable listener identity instead of index
diff --git a/.changeset/pre/tasty-humans-joke.md b/.changeset/pre/tasty-humans-joke.md
new file mode 100644
index 0000000000..82ab346146
--- /dev/null
+++ b/.changeset/pre/tasty-humans-joke.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/form-core': patch
+---
+
+Refactor: Store Form Groups on trie nodes instead of the form instance
diff --git a/.changeset/pre/tender-keys-own.md b/.changeset/pre/tender-keys-own.md
new file mode 100644
index 0000000000..1f6fb90bf0
--- /dev/null
+++ b/.changeset/pre/tender-keys-own.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/react-form': minor
+---
+
+Feat: Field and Form components can now be nested to create namespaces.
diff --git a/.changeset/pre/v2-alpha.md b/.changeset/pre/v2-alpha.md
new file mode 100644
index 0000000000..6134b1a958
--- /dev/null
+++ b/.changeset/pre/v2-alpha.md
@@ -0,0 +1,17 @@
+---
+'@tanstack/angular-form': major
+'@tanstack/form-core': major
+'@tanstack/form-devtools': major
+'@tanstack/lit-form': major
+'@tanstack/preact-form': major
+'@tanstack/react-form': major
+'@tanstack/react-form-devtools': major
+'@tanstack/react-form-nextjs': major
+'@tanstack/react-form-start': major
+'@tanstack/solid-form': major
+'@tanstack/solid-form-devtools': major
+'@tanstack/svelte-form': major
+'@tanstack/vue-form': major
+---
+
+Release the first alpha of TanStack Form v2.
diff --git a/.coderabbit.yaml b/.coderabbit.yaml
new file mode 100644
index 0000000000..dedfba43b4
--- /dev/null
+++ b/.coderabbit.yaml
@@ -0,0 +1,19 @@
+# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
+
+reviews:
+ request_changes_workflow: false
+ slop_detection:
+ enabled: true
+ auto_review:
+ base_branches:
+ - alpha
+ - main
+ path_filters:
+ - '!docs/reference/**'
+ - '!docs/framework/*/reference/**'
+
+knowledge_base:
+ code_guidelines:
+ enabled: true
+ filePatterns:
+ - 'CONTRIBUTING.md'
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index aac7579c4b..205aedde06 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -5,7 +5,8 @@
## ā Checklist
- [ ] I have followed the steps in the [Contributing guide](https://github.com/TanStack/form/blob/main/CONTRIBUTING.md).
-- [ ] I have tested this code locally with `pnpm test:pr`.
+- [ ] I have tested code changes locally with `pnpm test:pr`, or these tests do not apply to this pull request.
+- [ ] I fully understand the code in this pull request, including any code generated with AI assistance.
## š Release Impact
diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml
index 5a0bd9c854..2067bc3f98 100644
--- a/.github/workflows/autofix.yml
+++ b/.github/workflows/autofix.yml
@@ -26,17 +26,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Tools
- uses: TanStack/config/.github/setup@99d23c6212f36eff17d30a21e58da77f55a4cfe9 # main
+ uses: TanStack/config/.github/setup@466151b2d479656b38942cd84a0a3dce29d48de3 # main
- name: Fix formatting
run: pnpm format
- name: Generate Docs
if: ${{ github.event_name == 'push' || github.event.inputs.generate-docs == true }}
run: pnpm generate-docs
- name: Apply fixes
- uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8
+ uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4
with:
commit-message: 'ci: apply automated fixes and generate docs'
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 49c36c6dc2..03bb049d7b 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -19,16 +19,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Start Nx Agents
run: npx nx-cloud start-ci-run --distribute-on=".nx/workflows/dynamic-changesets.yaml"
- name: Setup Tools
- uses: TanStack/config/.github/setup@99d23c6212f36eff17d30a21e58da77f55a4cfe9 # main
+ uses: TanStack/config/.github/setup@466151b2d479656b38942cd84a0a3dce29d48de3 # main
- name: Get base and head commits for `nx affected`
- uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4.4.0
+ uses: nrwl/nx-set-shas@afb73a62d26e41464e9254689e1fd6122ee683c1 # v5.0.1
with:
main-branch-name: main
- name: Run Checks
@@ -37,7 +37,7 @@ jobs:
if: ${{ always() }}
run: npx nx-cloud stop-all-agents
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
+ uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
directory: packages
env:
@@ -47,11 +47,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Tools
- uses: TanStack/config/.github/setup@99d23c6212f36eff17d30a21e58da77f55a4cfe9 # main
+ uses: TanStack/config/.github/setup@466151b2d479656b38942cd84a0a3dce29d48de3 # main
- name: Build Packages
run: pnpm run build:all
- name: Publish Previews
@@ -64,10 +64,10 @@ jobs:
pull-requests: write
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Tools
- uses: TanStack/config/.github/setup@99d23c6212f36eff17d30a21e58da77f55a4cfe9 # main
+ uses: TanStack/config/.github/setup@466151b2d479656b38942cd84a0a3dce29d48de3 # main
- name: Changeset Preview
- uses: TanStack/config/.github/changeset-preview@99d23c6212f36eff17d30a21e58da77f55a4cfe9 # main
+ uses: TanStack/config/.github/changeset-preview@466151b2d479656b38942cd84a0a3dce29d48de3 # main
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 698e7e2a19..d7835ed68e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,7 +6,7 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.ref }}
- cancel-in-progress: true
+ cancel-in-progress: false
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
@@ -23,24 +23,29 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: true # changesets/action pushes version/release changes
- name: Setup Tools
- uses: TanStack/config/.github/setup@99d23c6212f36eff17d30a21e58da77f55a4cfe9 # main
+ uses: TanStack/config/.github/setup@466151b2d479656b38942cd84a0a3dce29d48de3 # main
- name: Run Tests
run: pnpm run test:ci
+ - name: Verify Release Mode
+ env:
+ RELEASE_BRANCH: ${{ github.ref_name }}
+ run: pnpm run changeset:verify-release
- name: Run Changesets (version or publish)
id: changesets
- uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1.7.0
+ uses: changesets/action@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2
with:
- version: pnpm run changeset:version
- publish: pnpm run changeset:publish
- commit: 'ci: Version Packages'
- title: 'ci: Version Packages'
+ pr-base-branch: ${{ github.ref_name }}
+ version-script: pnpm run changeset:version
+ publish-script: pnpm run changeset:publish
+ commit-message: 'ci: Version Packages'
+ pr-title: 'ci: Version Packages'
- name: Comment on PRs about release
if: steps.changesets.outputs.published == 'true'
- uses: TanStack/config/.github/comment-on-release@99d23c6212f36eff17d30a21e58da77f55a4cfe9 # main
+ uses: TanStack/config/.github/comment-on-release@466151b2d479656b38942cd84a0a3dce29d48de3 # main
with:
- published-packages: ${{ steps.changesets.outputs.publishedPackages }}
+ published-packages: ${{ steps.changesets.outputs.published-packages }}
diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml
index 1d4088db88..c05cda9222 100644
--- a/.github/workflows/zizmor.yml
+++ b/.github/workflows/zizmor.yml
@@ -14,12 +14,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run zizmor
- uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
+ uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
with:
advanced-security: false
annotations: true
diff --git a/.gitignore b/.gitignore
index 885eed7cc3..4fc6f1f9fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,12 @@ stats.json
stats.html
*.cpuprofile
.vscode/settings.json
+.idea
+.cursor
+.claude
+.codex
+AGENTS.md
+.agents
*.log
.cache
@@ -43,3 +49,5 @@ stats.html
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
tsconfig.vitest-temp.json
+
+.nx/migrate-runs
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000000..af9e354350
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,10 @@
+# Agent Instructions
+
+Before analyzing or changing this repository, read and follow
+[`CONTRIBUTING.md`](./CONTRIBUTING.md). Its contribution, development, testing,
+pull request, and changeset requirements apply to AI-assisted work as well as
+human-authored work.
+
+Keep every change focused on one topic. Understand and verify all generated
+code, run the required checks, and provide a concise pull request description
+that follows the repository template.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 27b46aacae..a970e6b1fa 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,12 +6,23 @@ If you have questions about implementation details, help or support, then please
## Reporting Issues
-If you have found what you think is a bug, please [file an issue](https://github.com/TanStack/form/issues/new/choose). **PLEASE NOTE:** Issues that are identified as implementation questions or non-issues will be immediately closed and redirected to [GitHub Discussions](https://github.com/TanStack/form/discussions)
+If you have found what you think is a bug, first search the [open and closed issues](https://github.com/TanStack/form/issues?q=is%3Aissue) to make sure it has not already been reported. If you cannot find an existing report, use the [bug report template](https://github.com/TanStack/form/issues/new?template=bug_report.yml). **PLEASE NOTE:** Issues that are identified as implementation questions or non-issues will be immediately closed and redirected to [GitHub Discussions](https://github.com/TanStack/form/discussions).
## Suggesting new features
If you are here to suggest a feature, first create an issue if it does not already exist. From there, we will discuss use-cases for the feature and then finally discuss how it could be implemented.
+## Pull Request Guidelines
+
+Every pull request must follow the [TanStack Form pull request template](.github/pull_request_template.md). Complete its description and checklist without removing or bypassing the required sections.
+
+- Search the [open and closed pull requests](https://github.com/TanStack/form/pulls?q=is%3Apr) before starting work to avoid duplicating an existing contribution.
+- Keep each pull request focused on one change or topic. Pull requests that combine unrelated changes will be closed with a request to split them into separately reviewable contributions.
+- Write a concise description that clearly explains what changed and why. Follow the sections in the pull request template; a long, unstructured description makes a contribution harder to review.
+- You may use AI tools to help generate code, but you remain responsible for understanding, testing, and verifying every submitted change. Do not submit unreviewed, low-quality, or irrelevant generated code.
+- Do not mass-submit unrelated or low-quality AI-generated pull requests. We treat that behavior as spam and may close the pull requests, block the contributor, and report the GitHub account.
+- Every change that affects a published package must include a changeset. Create it with `pnpm changeset`; documentation, CI, and development-only changes do not require one.
+
## Development
If you have been assigned to fix an issue or develop a new feature, please follow these steps to get started:
diff --git a/README.md b/README.md
index 6bd9001654..e8b928f2fb 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,19 @@
-
+
+
+
+
+
@@ -29,8 +43,9 @@
-
+
### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
+
# TanStack Form
diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml
new file mode 100644
index 0000000000..470362969c
--- /dev/null
+++ b/_artifacts/domain_map.yaml
@@ -0,0 +1,1007 @@
+schema_version: 1
+status: reviewed
+review_state: phase_5_finalized
+generated_from:
+ command: pnpm intent:scaffold
+ workflow: '@tanstack/intent domain-discovery'
+ phase: 5
+repository:
+ root: /home/luca/repos/tanstack/form-v2
+ package_manager: pnpm@11.8.0
+ monorepo: true
+ workspace_sources:
+ - package.json
+ - pnpm-workspace.yaml
+ - packages/*/package.json
+ - docs/**/*.md
+ - packages/*/src/**/*.ts
+ - packages/*/tests/**/*.ts
+ - packages/*/tests/**/*.tsx
+ - examples/react/**/*.tsx
+maintainer_guidance:
+ adapter_first: true
+ primary_user_entrypoint: '@tanstack/react-form'
+ secondary_user_entrypoint: '@tanstack/solid-form'
+ core_role: 'Concept and implementation source, not a user workflow entrypoint.'
+ primary_adapter_status:
+ react: 'Most complete adapter; use as primary examples and skill target.'
+ solid: 'Important adapter but missing critical planned pieces; avoid Solid-first coverage for now.'
+ schema_modes:
+ default_form_options:
+ summary: 'DefaultValues-first inference.'
+ use_when:
+ - 'The form type should be inferred from defaultValues.'
+ - 'Validation is mostly callback-based and callbacks should receive a value type derived from defaultValues.'
+ properties:
+ - 'defaultValues is taken at face value.'
+ - 'The inferred defaultValues type is used for validators and callbacks.'
+ - 'This mode is often the simplest choice when no schema should own the form type.'
+ strict_schema:
+ summary: 'Schema-source pipeline mode.'
+ use_when:
+ - 'The schema is a transforming pipeline from input type to output type.'
+ - 'The schema should be the source for form typing.'
+ properties:
+ - 'defaultValues must align with the schema input type and should produce type errors when they do not.'
+ - 'Callbacks may not have useful inference unless a schema is present in the validator array.'
+ - 'onSubmit should read the transform output from validatorOutputs instead of raw value.'
+ loose_schema:
+ summary: 'Schema-source ruleset mode with editable defaults.'
+ use_when:
+ - 'The schema is a ruleset for confirming valid output, but the editable form state needs nullable or undefined starting values.'
+ - 'A schema such as z.date() needs a starting value like null before the user has chosen a date.'
+ properties:
+ - 'The input is looser while the parsed submit output is still the schema output type.'
+ - 'defaultValues must still align with the schema shape, but properties may also be null or undefined where editing requires it.'
+ - 'Callbacks may not have useful inference unless a schema is present in the validator array.'
+ - 'onSubmit should read the transform output from validatorOutputs instead of raw value.'
+ schema_ecosystem:
+ standard_schema:
+ role: 'Most common integration shape.'
+ note: 'Standard Schema is an interoperability interface, not a pipeline-vs-ruleset classification system.'
+ source: 'https://standardschema.dev/'
+ pipeline_vs_ruleset_heuristic:
+ status: 'maintainer commentary'
+ meaning: 'A mental model for deciding TanStack Form option mode: is the schema written as a pipeline of constraints/transforms, or as a firewall that only lets already-valid values through?'
+ caution: 'This is not part of Standard Schema and should not be presented as a Standard Schema concept.'
+ validation_libraries_as_of_2026_06_25:
+ - name: Zod
+ standard_schema_support: 'Supported; Zod docs recommend Standard Schema for schema-agnostic black-box validation and say Zod is included among compatible libraries.'
+ general_leaning: 'ruleset/firewall-leaning by default'
+ tanstack_form_bias: 'Often looseSchema for editable forms, with strictSchema only when a Zod schema is deliberately authored as a transform pipeline.'
+ source: 'https://zod.dev/library-authors'
+ - name: Valibot
+ standard_schema_support: 'Implements Standard Schema v1.'
+ general_leaning: 'pipeline-leaning'
+ tanstack_form_bias: 'Often strictSchema when the schema is authored as an input-to-output pipeline.'
+ source: 'https://valibot.dev/guides/integrate-valibot/'
+ - name: ArkType
+ standard_schema_support: 'Supports and co-authored Standard Schema; can consume Standard Schema validators as definitions.'
+ general_leaning: 'mixed; structural firewall constraints with morph/pipe support for pipeline-style transforms'
+ tanstack_form_bias: 'Choose by the actual schema intent rather than the library name.'
+ source: 'https://arktype.io/docs/integrations'
+ - name: Effect Schema
+ standard_schema_support: 'Can generate Standard Schema V1 via Schema.standardSchemaV1 for schemas without dependencies.'
+ general_leaning: 'pipeline/codec-leaning because schemas distinguish decoded Type from Encoded input and support decoding/encoding.'
+ tanstack_form_bias: 'Often strictSchema when the Effect schema is used as an encoded-to-decoded boundary.'
+ source: 'https://effect.website/docs/schema/standard-schema/'
+ query_shape:
+ async_defaults: 'Incoming values are often maybe undefined while data loads.'
+ default_values_plan: 'Do not pass query data alone as form defaults; either gate mounted fields/form until data is ready, or coalesce query data with a static shape-complete emptyFormValues object. Neither strategy is preferred globally.'
+ visual_language: 'Gating fields or the whole form changes the visual UI language because the form appears only after data is ready; an emptyFormValues fallback can render the form immediately with loading or placeholder state.'
+ submit: 'Use an async promise for endpoint calls so isSubmitting reflects the request. Catch submit errors only when they can produce validation errors that must be returned as values. Transient thrown/rejected errors may remain thrown; they set isSubmitSuccessful to false while external query/framework state preserves the error.'
+ hard_do_nots:
+ - id: no_type_assertions
+ rule: 'Do not type assert to make TanStack Form code compile; preserve inference and fix the mismatched API boundary.'
+ bad_patterns:
+ - 'name={`users[${index}].name` as const}'
+ - '{(field: any) => ...}'
+ - 'field: { value: string }'
+ preferred_patterns:
+ - 'Let field names infer without as const.'
+ - 'Let form.Field render-prop children infer their field type.'
+ - 'Use FieldWithValue or AnyFieldApi when a reusable component needs an explicit field prop type.'
+ - id: validation_errors_are_not_transient_errors
+ rule: 'Do not store query, network, or framework transient failures as form validation errors.'
+ reason: 'Validation errors from validators or onSubmit block the user from submitting; transient errors should use the query or framework error state.'
+ preferred_patterns:
+ - 'Use an async promise for endpoint calls so isSubmitting reflects the request.'
+ - 'Return createValidationError or parseIssues only for validation-shaped server responses.'
+ - 'Keep retryable network/server failures in the query or framework layer; they may throw or reject as transient failures.'
+ - id: validation_errors_are_values
+ rule: 'Do not throw validation errors from validators or onSubmit.'
+ reason: 'Validation errors must be returned values. Thrown/rejected errors are submit failures, not validation state.'
+ preferred_patterns:
+ - 'Return createValidationError(...) from onSubmit for validation failures.'
+ - 'Return parseIssues(...) when converting Standard Schema issues.'
+ - 'Let transient endpoint failures reject if the surrounding query/framework preserves the error state.'
+ - id: no_react_form_type_unions
+ rule: 'Do not type shared sections as unions of multiple ReactFormType forms.'
+ bad_patterns:
+ - 'form: ReactFormType | ReactFormType'
+ reason: 'TypeScript cannot make useful sense of different form API unions for field names and methods.'
+ preferred_patterns:
+ - 'Use ReactFormType for splitting one known form into sections.'
+ - 'Use field groups when a reusable section should bind into multiple different forms or paths.'
+ excluded_from_current_skills:
+ - devtools_as_entrypoint
+ - migration_guidance
+ - schema_metadata_extraction
+ - non_validation_error_state_ownership
+ - ui_library_behavior_itself
+ - solid_first_examples
+domain_boundaries:
+ in_scope:
+ - 'React adapter-first form setup.'
+ - 'Core concepts explained through adapter APIs.'
+ - 'Standard Schema-compatible validation and submit parsing.'
+ - 'Choosing default, strictSchema, and looseSchema option modes.'
+ - 'Async default values and async submit with query-like libraries.'
+ - 'Error visibility policy callbacks.'
+ - 'Reusable split-form types and reusable validation or visibility policy modules.'
+ - 'Interpreting TypeScript errors caused by form/schema composition.'
+ - 'Authoring realistic examples that demonstrate intended usage.'
+ out_of_scope:
+ - 'Using @tanstack/form-core directly in app examples.'
+ - 'Devtools workflows while devtools are still changing.'
+ - 'Migration from v1 or other form libraries.'
+ - 'Deriving UI metadata from Standard Schemas.'
+ - 'Owning transient server/network error state inside TanStack Form.'
+ - 'Teaching a specific UI library as part of TanStack Form itself.'
+packages:
+ - name: '@tanstack/form-core'
+ role: concept_source
+ public_user_entrypoint: false
+ relevant_concepts:
+ - form_options
+ - strict_schema
+ - loose_schema
+ - standard_schema_parsing
+ - validation_errors
+ - error_visibility
+ - create_error_visibility
+ - create_validator
+ - create_validators
+ - async_defaults
+ - array_helpers
+ - array_field_versioning
+ - reset_semantics
+ sources:
+ - packages/form-core/src/utils.public.ts
+ - packages/form-core/src/validation.public.ts
+ - packages/form-core/src/standardSchema.public.ts
+ - packages/form-core/src/standardSchema.lib.ts
+ - packages/form-core/src/FormApi/FormApi.public.ts
+ - packages/form-core/src/FormApi/FormApi.lib.ts
+ - packages/form-core/src/FormApi/array-methods.lib.ts
+ - packages/form-core/src/FieldApi/fieldState.lib.ts
+ - packages/form-core/tests/validation.test.ts
+ - packages/form-core/tests/validation.test-d.ts
+ - packages/form-core/tests/FormApi/default-value.spec.ts
+ - packages/form-core/tests/FormApi/submission-handling.spec.ts
+ - packages/form-core/tests/FormApi/field-array-methods.spec.ts
+ - name: '@tanstack/react-form'
+ role: primary_user_entrypoint
+ public_user_entrypoint: true
+ relevant_concepts:
+ - use_form
+ - form_field
+ - array_field
+ - form_subscribe
+ - app_form
+ - app_form_options
+ - create_form_hook
+ - field_groups
+ - field_subscription
+ - array_field_subscription
+ - react_form_type
+ - field_with_value
+ - any_field_api
+ sources:
+ - packages/react-form/src/ReactForm/useForm.public.ts
+ - packages/react-form/src/ReactForm/ReactFormApi.lib.tsx
+ - packages/react-form/src/ReactForm/Components.public.ts
+ - packages/react-form/src/ReactForm/fieldSubscriptions.lib.ts
+ - packages/react-form/src/ReactForm/useField.lib.ts
+ - packages/react-form/src/AppForm/appFormOptions.public.ts
+ - packages/react-form/src/AppForm/createFormHook.public.ts
+ - packages/react-form/src/AppForm/getFormHookHelpers.public.ts
+ - packages/react-form/src/FieldGroup/withFields.public.ts
+ - packages/react-form/src/ReactForm/formType.public.ts
+ - packages/react-form/tests/useForm.spec.tsx
+ - packages/react-form/tests/submit-return.test-d.tsx
+ - packages/react-form/tests/FormGroup.test-d.tsx
+ - packages/react-form/tests/FieldGroupApi.test-d.tsx
+ - name: '@tanstack/solid-form'
+ role: secondary_adapter
+ public_user_entrypoint: true
+ current_skill_status: deferred
+ note: 'Use as conceptual parity context only until planned Solid adapter pieces land.'
+ - name: '@tanstack/form-devtools'
+ role: devtools
+ public_user_entrypoint: false
+ current_skill_status: excluded
+ note: 'Devtools are in development and should not be treated as an entrypoint yet.'
+domains:
+ - id: react-adapter-composition
+ title: React Adapter Composition
+ description: 'How users compose headless React forms, reusable field components, app form contexts, field groups, arrays, and UI-library bindings through @tanstack/react-form.'
+ package_focus:
+ - '@tanstack/react-form'
+ - '@tanstack/form-core'
+ core_questions:
+ - 'Should this be a one-off useForm example, an app-form setup, or a field-group composition?'
+ - 'Which pieces need to be reusable without over-constraining submit types?'
+ - 'Is this reducing nesting in one known form, reducing field JSX boilerplate, or reusing a section across forms?'
+ - 'Should split child components receive a form prop typed with ReactFormType derived from shared options, or should the section be a field group?'
+ - 'Does the example need field arrays or reorder behavior?'
+ related_skills:
+ - react-form-composition-setup
+ - examples-authoring
+ - type-error-debugging
+ - id: schema-driven-forms
+ title: Schema Driven Forms
+ description: 'How Standard Schema-compatible schemas influence form option mode, validation, field error routing, submit output, and TypeScript inference.'
+ package_focus:
+ - '@tanstack/react-form'
+ - '@tanstack/form-core'
+ core_questions:
+ - 'Is the schema a pipeline from input A to output B, or a ruleset for confirming B?'
+ - 'Should submit logic read raw value or validatorOutputs?'
+ - 'Do defaults need to be nullable or maybe undefined while editing?'
+ related_skills:
+ - choosing-form-options-modes
+ - schema-driven-forms
+ - type-error-debugging
+ - id: async-data-and-submit
+ title: Async Data And Submit
+ description: 'How query-like loading data and mutation-like submit functions interact with default values, touched fields, validation errors, and transient errors.'
+ package_focus:
+ - '@tanstack/react-form'
+ - '@tanstack/form-core'
+ core_questions:
+ - 'Can incoming data be undefined at first render?'
+ - 'Should later defaultValues update untouched fields while preserving touched fields?'
+ - 'Is an onSubmit failure transient or validation-related?'
+ related_skills:
+ - query-backed-forms
+ - error-visibility-workflows
+ - examples-authoring
+ - id: validation-and-error-visibility
+ title: Validation And Error Visibility
+ description: 'How validators produce errors, how Standard Schema issues are routed, and how visible errors are filtered by form-level or field-level callbacks.'
+ package_focus:
+ - '@tanstack/react-form'
+ - '@tanstack/form-core'
+ core_questions:
+ - 'When should an error become visible?'
+ - 'Should the visibility policy or validator trigger policy be extracted into a named reusable helper?'
+ - 'Is visibility form-wide, field-specific, or group-scoped?'
+ - 'Should submit errors be parsed from Standard Schema issues or manually created?'
+ related_skills:
+ - error-visibility-workflows
+ - schema-driven-forms
+ - query-backed-forms
+ - id: diagnostics-and-examples
+ title: Diagnostics And Examples
+ description: "How to turn the library's type surface and examples into agent-readable, realistic user journeys."
+ package_focus:
+ - '@tanstack/react-form'
+ - '@tanstack/form-core'
+ core_questions:
+ - 'What type boundary is producing the error?'
+ - 'Is the example demonstrating intended usage rather than only core behavior?'
+ - 'Which realistic integration should the example model without claiming ownership of external UI or query state?'
+ related_skills:
+ - type-error-debugging
+ - examples-authoring
+skill_candidates:
+ - id: react-form-composition-setup
+ title: React Form Composition Setup
+ status: reviewed
+ priority: high
+ domain: react-adapter-composition
+ purpose: 'Help users build adapter-first React forms, reusable components, app-form setups, field groups, and UI-library bindings.'
+ entrypoints:
+ - '@tanstack/react-form/useForm'
+ - '@tanstack/react-form/ReactFormType'
+ - '@tanstack/react-form/FieldWithValue'
+ - '@tanstack/react-form/AnyFieldApi'
+ - '@tanstack/react-form/formOptions'
+ - '@tanstack/react-form/createFormHook'
+ - 'form.Field'
+ - 'form.ArrayField'
+ - 'form.Subscribe'
+ - 'form.FormGroup'
+ sources:
+ - packages/react-form/src/ReactForm/Components.public.ts
+ - packages/react-form/src/ReactForm/fieldSubscriptions.lib.ts
+ - packages/react-form/src/AppForm/createFormHook.public.ts
+ - packages/react-form/src/FieldGroup/withFields.public.ts
+ - packages/react-form/tests/submit-return.test-d.tsx
+ - packages/form-core/src/FormApi/FormApiArrayMethods.types.public.ts
+ - packages/form-core/src/FieldApi/fieldState.lib.ts
+ - examples/react/array/src/index.tsx
+ - examples/react/basic-splitting-form/src/index.tsx
+ - examples/react/ui-integration-shadcn/src/components/form/app-form.ts
+ teaches:
+ - 'Prefer adapter APIs over constructing core APIs directly.'
+ - 'Use render-prop field APIs and subscriptions instead of copying form state into component state.'
+ - 'Treat useForm and useAppForm as stable form creation hooks, not reactive state reads.'
+ - 'Use useSelector or form.Subscribe when component output must react to form state.'
+ - 'Keep reusable shared options submit-agnostic when child components should accept multiple submit implementations.'
+ - 'Use ReactFormType as one option for splitting a known form into child components that receive a form prop.'
+ - 'Do not union multiple ReactFormType forms for shared sections; use field groups when a section must bind into multiple different forms or paths.'
+ - 'Use AppForm to reduce repeated field JSX boilerplate; use split form sections to reduce component nesting; use field groups for reusable sections across forms or paths.'
+ - 'Let field names and form.Field children infer; use FieldWithValue or AnyFieldApi for explicit reusable field component props.'
+ - 'form.Field may bind an array value; it behaves immutably like other fields, so any element change changes the array value and rerenders whole-array consumers.'
+ - 'Use form.ArrayField when an array is a rendering boundary for child fields or list items; ArrayField subscriptions follow array shape/version changes instead of treating value as the whole render unit.'
+ - 'Use field groups for reusable sections with virtual-to-real field bindings.'
+ failure_modes:
+ - name: type_asserting_form_inference
+ symptom: 'Code adds as const to field names, annotates render-prop children as any, or uses as any to force form code to compile.'
+ correction: 'Remove the assertion and let names, children, and form helpers infer the types.'
+ evidence:
+ - maintainer_guidance.hard_do_nots
+ - packages/react-form/src/ReactForm/Components.public.ts
+ - name: shimmed_field_prop_type
+ symptom: 'A reusable component accepts a hand-written field shape like field: { value: string }.'
+ correction: 'Use inferred render-prop types, FieldWithValue, or AnyFieldApi instead of a structural shim.'
+ evidence:
+ - examples/react/basic-splitting-form/src/StringField.tsx
+ - examples/react/basic-splitting-form/src/FieldError.tsx
+ - name: expecting_hook_reactivity
+ symptom: 'A component reads form.state or derived flags from the stable useForm/useAppForm return and does not re-render when form state changes.'
+ correction: 'Use useSelector with form.atom or a field/group atom, or use form.Subscribe with an explicit selector.'
+ evidence:
+ - packages/react-form/src/ReactForm/ReactFormApi.lib.tsx
+ - packages/react-form/src/Subscribe.public.tsx
+ - packages/react-form/src/ReactForm/fieldSubscriptions.lib.ts
+ - examples/react/ui-integration-dnd-kit/src/twoLists/index.tsx
+ - name: core_as_user_entrypoint
+ symptom: 'Example imports @tanstack/form-core or constructs FormApi in React application code.'
+ correction: 'Use @tanstack/react-form APIs; treat core as concept and implementation source.'
+ evidence:
+ - maintainer_guidance.core_role
+ - packages/react-form/src/ReactForm/useForm.public.ts
+ - name: app_components_outside_provider
+ symptom: 'App field or form components throw because they are rendered outside form.Field or form.AppForm.'
+ correction: 'Wrap custom form components in the app-form provider pattern created by createFormHook.'
+ evidence:
+ - packages/react-form/src/AppForm/contexts.lib.ts
+ - examples/react/ui-integration-shadcn/src/components/form/app-form.ts
+ - name: over_specific_shared_form_type
+ symptom: 'A reusable child form prop rejects a parent form once onSubmit is added.'
+ correction: 'Keep shared options without onSubmit when reusable children should stay submit-agnostic.'
+ evidence:
+ - packages/react-form/src/ReactForm/formType.public.ts
+ - packages/react-form/tests/submit-return.test-d.tsx
+ - name: split_form_without_react_form_type
+ symptom: 'A split form section hand-writes a large form prop type or loses field-name inference.'
+ correction: 'Export shared form options and type the prop as ReactFormType.'
+ evidence:
+ - examples/react/basic-splitting-form/src/FormSection.tsx
+ - docs/framework/react/reference/type-aliases/ReactFormType.md
+ - name: react_form_type_union_shared_section
+ symptom: 'A shared section accepts a union like ReactFormType | ReactFormType and TypeScript cannot resolve field names or methods.'
+ correction: 'Use field groups for reusable sections across forms, or use ReactFormType only for one known shared options type.'
+ evidence:
+ - maintainer_guidance.hard_do_nots
+ - packages/react-form/src/FieldGroup/withFields.public.ts
+ - name: field_used_for_rendering_array_children
+ symptom: 'A Field bound to an array value is used as the boundary for rendering item fields, and item updates rerender the whole array-value consumer.'
+ correction: 'Use ArrayField at the list boundary, then render item Fields for the values that should update independently.'
+ evidence:
+ - examples/react/array/src/index.tsx
+ - packages/react-form/src/ReactForm/fieldSubscriptions.lib.ts
+ - packages/form-core/src/FieldApi/fieldState.lib.ts
+ - name: array_field_assumed_required_for_any_array_value
+ symptom: 'Code reaches for ArrayField only because the field value is an array, even though the array is consumed as one immutable value.'
+ correction: 'Use Field when the array is a single field value; reserve ArrayField for rendering child fields or list items.'
+ evidence:
+ - packages/react-form/src/ReactForm/Components.public.ts
+ - packages/react-form/src/ReactForm/fieldSubscriptions.lib.ts
+ - name: raw_array_mutation
+ symptom: 'Array item values and field meta fall out of sync after splice, reorder, or manual setFieldValue.'
+ correction: 'Use pushFieldValue, insertFieldValue, removeFieldValue, swapFieldValues, moveFieldValue, clearFieldValues, or filterFieldValues.'
+ evidence:
+ - packages/form-core/src/FormApi/array-methods.lib.ts
+ - packages/form-core/src/FormApi/FormApiArrayMethods.types.public.ts
+ - packages/form-core/tests/FormApi/field-array-methods.spec.ts
+ - id: reusable-form-policies
+ title: Reusable Form Policies
+ status: reviewed
+ priority: high
+ domain: validation-and-error-visibility
+ purpose: 'Help users extract split-form types, error visibility callbacks, and validator configs into named reusable helpers when the behavior deserves semantic intent.'
+ entrypoints:
+ - '@tanstack/react-form/ReactFormType'
+ - 'createErrorVisibility'
+ - 'createValidator'
+ - 'createValidators'
+ - 'shared formOptions'
+ - 'shared appFormOptions'
+ sources:
+ - examples/react/basic-splitting-form/src/FormSection.tsx
+ - examples/react/basic-splitting-form/src/sharedForm.ts
+ - packages/react-form/tests/submit-return.test-d.tsx
+ - packages/form-core/tests/validation.test.ts
+ - packages/form-core/tests/validation.test-d.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ teaches:
+ - 'Use ReactFormType as the helper type for split form sections that receive a form prop.'
+ - 'ReactFormType splitting reduces nesting in one known form; AppForm reduces repeated field JSX; field groups cover sections reused across different forms or paths.'
+ - 'Prefer form-level errorVisibility callbacks over repeated local showErrors checks.'
+ - 'Use helper functions when a callback or validator config deserves a semantic name that preserves intent.'
+ - 'Use comments or JSDoc on named helpers to explain workflow behavior, trigger timing, debounce, bailIfInvalid, skipOnSubmit, or rewardEarlyPunishLate policy.'
+ - 'Keep reusable visibility policies value-agnostic unless they are intentionally tied to one form shape; use inline callbacks when typed values are required.'
+ failure_modes:
+ - name: split_form_type_written_by_hand
+ symptom: 'A split form section loses field-name inference because its form prop type is hand-written.'
+ correction: 'Export shared options and type the prop as ReactFormType.'
+ evidence:
+ - examples/react/basic-splitting-form/src/FormSection.tsx
+ - packages/react-form/tests/submit-return.test-d.tsx
+ - name: reusable_section_uses_react_form_type_union
+ symptom: 'A shared section accepts FormA | FormB and TypeScript cannot narrow field names or methods.'
+ correction: 'Use field groups for shared sections that need to work across different forms or paths.'
+ evidence:
+ - maintainer_guidance.hard_do_nots
+ - packages/react-form/src/FieldGroup/withFields.public.ts
+ - name: scattered_show_errors_policy
+ symptom: 'Each field repeats local showErrors logic such as field.meta.isTouched && field.meta.isInvalid.'
+ correction: 'Move default visible-error behavior into form-level errorVisibility, or extract a named createErrorVisibility helper.'
+ evidence:
+ - packages/form-core/tests/validation.test-d.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ - name: duplicated_validator_config_without_intent
+ symptom: 'Trigger, debounce, skipOnSubmit, or bailIfInvalid config is copied without explaining the intended workflow.'
+ correction: 'Extract a named createValidator/createValidators helper and document the behavior intent.'
+ evidence:
+ - packages/form-core/tests/validation.test.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ - name: reusable_visibility_reads_concrete_values
+ symptom: 'A reusable createErrorVisibility callback tries to read a concrete property such as state.values.name.'
+ correction: 'Keep reusable policies value-agnostic, or use an inline errorVisibility callback when concrete typed values are needed.'
+ evidence:
+ - packages/form-core/tests/validation.test-d.ts
+ - id: choosing-form-options-modes
+ title: Choosing Form Options Modes
+ status: reviewed
+ priority: high
+ domain: schema-driven-forms
+ purpose: 'Help users choose default formOptions, strictSchema, or looseSchema based on whether the schema is a transform pipeline or a validation ruleset.'
+ entrypoints:
+ - '@tanstack/react-form/formOptions'
+ - '@tanstack/react-form/formOptions.strictSchema'
+ - '@tanstack/react-form/formOptions.looseSchema'
+ - '@tanstack/react-form/appFormOptions'
+ - '@tanstack/react-form/appFormOptions.strictSchema'
+ - '@tanstack/react-form/appFormOptions.looseSchema'
+ sources:
+ - packages/form-core/src/utils.public.ts
+ - packages/react-form/src/AppForm/appFormOptions.public.ts
+ - packages/form-core/tests/validation.test.ts
+ - packages/form-core/tests/validation.test-d.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ teaches:
+ - 'The option helpers are runtime identity helpers with type-level meaning.'
+ - 'Default formOptions infers from defaultValues; those values are taken at face value and drive validator callback value types.'
+ - 'strictSchema uses the schema as the typing source for a transforming input-to-output pipeline.'
+ - 'strictSchema defaultValues must align with the schema input type and should fail type-checking when they do not.'
+ - 'looseSchema uses the schema as a ruleset while allowing editable default values that include null or undefined where needed.'
+ - 'looseSchema defaultValues must still align with the schema shape, but the input can be looser while output remains schema output.'
+ - 'In schema modes, callbacks may not infer usefully unless a schema is present in the validator array.'
+ - 'In schema modes, onSubmit should read validatorOutputs instead of raw value.'
+ - 'Use Standard Schema-capable libraries as heuristic examples only: Zod generally leans ruleset/firewall, Valibot generally leans pipeline, ArkType is mixed, and Effect Schema often leans pipeline/codec.'
+ failure_modes:
+ - name: expecting_option_helper_runtime_behavior
+ symptom: 'User expects strictSchema or looseSchema to add validation without validators.'
+ correction: 'Explain that the helper shapes inference; validation still comes from configured validators.'
+ evidence:
+ - packages/form-core/src/utils.public.ts
+ - packages/form-core/tests/validation.test.ts
+ - name: schema_expected_to_type_default_form_options
+ symptom: 'User chooses default formOptions but expects a schema to own the form data type.'
+ correction: 'Use default formOptions for defaultValues-first inference, or choose a schema mode when the schema should be the typing source.'
+ evidence:
+ - maintainer_guidance.schema_modes
+ - packages/form-core/src/utils.public.ts
+ - name: strict_ruleset_schema_blocks_editing
+ symptom: 'Defaults or field values cannot represent empty, null, or partial UI states because strict schema typing is too narrow.'
+ correction: 'Use looseSchema or default formOptions when the schema is a ruleset validating final form shape.'
+ evidence:
+ - maintainer_guidance.schema_modes
+ - packages/form-core/src/utils.public.ts
+ - name: loose_schema_shape_mismatch
+ symptom: "Loose schema defaults include null or undefined but no longer match the schema's object shape or field structure."
+ correction: 'Keep the defaultValues shape aligned with the schema while allowing null or undefined where editable state needs it.'
+ evidence:
+ - maintainer_guidance.schema_modes
+ - packages/form-core/src/utils.public.ts
+ - name: schema_mode_without_schema_validator
+ symptom: 'Callbacks in strictSchema or looseSchema mode do not infer the expected value type.'
+ correction: 'Put a schema in the validator array so the helper has a schema source to infer from.'
+ evidence:
+ - maintainer_guidance.schema_modes
+ - packages/form-core/tests/validation.test-d.ts
+ - name: loose_pipeline_schema_loses_transform_intent
+ symptom: 'Submit logic treats raw form value as if it were the transformed schema output.'
+ correction: 'Use strictSchema for pipeline schemas and read the transformed value from validatorOutputs.'
+ evidence:
+ - packages/form-core/tests/validation.test.ts
+ - packages/form-core/tests/validation.test-d.ts
+ - name: on_submit_reads_value_in_schema_mode
+ symptom: 'onSubmit reads value even though strictSchema or looseSchema is being used.'
+ correction: 'Read the relevant validatorOutputs entry, because both schema modes preserve raw form value separately from parsed output.'
+ evidence:
+ - packages/form-core/tests/validation.test.ts
+ - packages/form-core/src/FormApi/FormApi.public.ts
+ - name: confusing_zod_input_with_pipeline_input
+ symptom: 'A Zod schema is treated as a reliable A-to-B pipeline because z.input differs from z.output.'
+ correction: 'Start from Zod-as-ruleset guidance unless the specific schema is deliberately written as a transform pipeline.'
+ evidence:
+ - maintainer_guidance.schema_ecosystem
+ - examples/react/ui-integration-shadcn/src/app/booking/schema.ts
+ - id: schema-driven-forms
+ title: Schema Driven Forms
+ status: reviewed
+ priority: high
+ domain: schema-driven-forms
+ purpose: 'Help users turn Standard Schema-compatible schemas into form validators, field errors, typed submit output, and realistic examples.'
+ entrypoints:
+ - 'validators'
+ - 'FormSubmitContext.validatorOutputs'
+ - 'parseIssues'
+ - 'createValidationError'
+ - 'Standard Schema validators'
+ sources:
+ - packages/form-core/src/standardSchema.public.ts
+ - packages/form-core/src/standardSchema.lib.ts
+ - packages/form-core/src/FormApi/FormApi.public.ts
+ - packages/form-core/tests/validation.test.ts
+ - packages/form-core/tests/validation.test-d.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/schema.ts
+ - examples/react/field-groups/src/index.tsx
+ teaches:
+ - 'Use Standard Schema-compatible validators in form or field validators.'
+ - 'Extract reusable validator trigger configs with createValidator or createValidators when the policy is complex enough to deserve a semantic name.'
+ - 'Read parsed schema output from onSubmit context.validatorOutputs.'
+ - 'Return validation errors as values through createValidationError or parseIssues; do not throw them.'
+ - 'Let Standard Schema issue paths route field errors instead of hand-building brittle path maps.'
+ failure_modes:
+ - name: assuming_schema_output_replaces_value
+ symptom: 'onSubmit reads value expecting transformed/coerced data.'
+ correction: 'Use validatorOutputs for parsed schema output; value remains form state.'
+ evidence:
+ - packages/form-core/src/FormApi/FormApi.public.ts
+ - packages/form-core/tests/validation.test.ts
+ - name: returning_raw_schema_error
+ symptom: 'A submit validator returns or throws raw Zod/Standard Schema errors and the form does not show intended field errors.'
+ correction: 'Return parseIssues for Standard Schema issues or createValidationError for explicit form/field errors; validation errors must be values, not thrown errors.'
+ evidence:
+ - packages/form-core/src/FormApi/FormApi.public.ts
+ - packages/form-core/tests/FormApi/submission-handling.spec.ts
+ - name: manual_path_mapping
+ symptom: 'Nested and array validation errors are routed to the wrong field names.'
+ correction: 'Use Standard Schema parsing so paths like user.name and users[0].name are produced consistently.'
+ evidence:
+ - packages/form-core/src/standardSchema.lib.ts
+ - packages/form-core/tests/validation.test.ts
+ - name: duplicated_validator_policy
+ symptom: 'The same triggers, debounce, skipOnSubmit, or bailIfInvalid choices are copied across validators with no explanation.'
+ correction: 'Extract a named createValidator/createValidators helper when the policy has semantic meaning, and document why that behavior exists.'
+ evidence:
+ - packages/form-core/tests/validation.test.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ - name: schema_metadata_expectation
+ symptom: 'Example tries to infer required labels, descriptions, or UI decorations from Standard Schema metadata.'
+ correction: 'Keep UI metadata explicit or schema-library-specific; Standard Schema does not standardize metadata.'
+ evidence:
+ - maintainer_guidance.excluded_from_current_skills
+ - id: query-backed-forms
+ title: Query Backed Forms
+ status: reviewed
+ priority: high
+ domain: async-data-and-submit
+ purpose: 'Help users combine forms with query-like async defaults and mutation-like submit calls while separating transient failures from validation errors.'
+ entrypoints:
+ - 'useForm defaultValues'
+ - 'onSubmit'
+ - 'form.reset'
+ - 'createValidationError'
+ - 'parseIssues'
+ sources:
+ - packages/react-form/tests/useForm.spec.tsx
+ - packages/form-core/src/FormApi/FormApi.lib.ts
+ - packages/form-core/src/FormApi/FormApi.public.ts
+ - packages/form-core/tests/FormApi/default-value.spec.ts
+ - packages/form-core/tests/FormApi/submission-handling.spec.ts
+ teaches:
+ - 'Maybe-undefined incoming data requires an explicit default-values plan, not direct query data alone.'
+ - 'Approach 1: gate the fields, or the whole form, until the query is ready so mounted fields start with correct data.'
+ - 'Approach 2: define a static shape-complete emptyFormValues object and pass defaultValues as queryData ?? emptyFormValues.'
+ - 'Neither approach is preferred globally; choose based on the desired loading and visual UI language.'
+ - 'Gating fields changes the UI because fields mount only after data is ready, while an emptyFormValues fallback can show the form immediately.'
+ - 'Let updated defaultValues populate untouched fields while preserving user-touched fields.'
+ - 'Use an async promise for endpoint calls inside onSubmit so isSubmitting reflects the request.'
+ - 'Catch submit errors only when the endpoint can return validation errors that should be converted into createValidationError or parseIssues values.'
+ - 'Transient submit errors can throw or reject; they set isSubmitSuccessful to false while query/framework state should preserve the error details.'
+ - 'Remember that errors stored from validators or onSubmit block submission, so transient errors should not be converted into validation errors.'
+ - 'Use reset semantics deliberately when replacing values and default baselines.'
+ failure_modes:
+ - name: undefined_data_as_final_shape
+ symptom: 'Initial query data is undefined, so fields or schema types collapse into invalid permanent defaults.'
+ correction: 'Either gate mounting until data is ready, or coalesce with a static shape-complete emptyFormValues object.'
+ evidence:
+ - packages/react-form/tests/useForm.spec.tsx
+ - maintainer_guidance.query_shape
+ - name: query_data_without_fallback_plan
+ symptom: 'defaultValues is fed directly from query.data even though query.data can be undefined.'
+ correction: 'Use query.data ?? emptyFormValues, or delay mounting fields/form until the query has populated data.'
+ evidence:
+ - maintainer_guidance.query_shape
+ - packages/react-form/tests/useForm.spec.tsx
+ - name: ignoring_loading_visual_language
+ symptom: 'An example gates fields without accounting for the different loading UI, or renders empty fallback values without communicating that data is still loading.'
+ correction: 'Choose the default-values plan together with the intended visual loading language.'
+ evidence:
+ - maintainer_guidance.query_shape
+ - name: overwriting_touched_fields
+ symptom: 'Late async defaults replace user edits.'
+ correction: "Use the form's defaultValues update behavior, which preserves touched fields and updates untouched fields."
+ evidence:
+ - packages/form-core/src/FormApi/FormApi.lib.ts
+ - packages/react-form/tests/useForm.spec.tsx
+ - name: transient_error_as_validation_error
+ symptom: 'Network/server outage becomes a visible field validation error.'
+ correction: 'Keep transient error state in the query or framework layer; return createValidationError or parseIssues only for validation failures, because stored validation errors block submission.'
+ evidence:
+ - packages/form-core/tests/FormApi/submission-handling.spec.ts
+ - maintainer_guidance.query_shape
+ - name: non_async_endpoint_call
+ symptom: 'onSubmit starts an endpoint call without returning or awaiting its promise, so isSubmitting does not cover the request.'
+ correction: 'Return or await the async endpoint promise from onSubmit.'
+ evidence:
+ - packages/form-core/tests/FormApi/submission-handling.spec.ts
+ - packages/form-core/src/FormApi/handleSubmit.lib.ts
+ - name: thrown_validation_error
+ symptom: 'onSubmit throws or rejects with validation information and expects field errors.'
+ correction: 'Convert validation failures into returned createValidationError or parseIssues values; thrown/rejected errors are submit failures.'
+ evidence:
+ - packages/form-core/tests/FormApi/submission-handling.spec.ts
+ - packages/form-core/src/FormApi/FormApi.public.ts
+ - name: rejected_submit_expected_to_show_field_errors
+ symptom: 'onSubmit throws or rejects a transient failure and the user expects field errors to appear.'
+ correction: 'Let transient failures remain transient and use query/framework error state; only validation failures should be converted into returned validation values.'
+ evidence:
+ - packages/form-core/tests/FormApi/submission-handling.spec.ts
+ - packages/form-core/src/FormApi/FormApi.public.ts
+ - id: error-visibility-workflows
+ title: Error Visibility Workflows
+ status: reviewed
+ priority: high
+ domain: validation-and-error-visibility
+ purpose: 'Help users implement visible-error policies such as show-after-blur, show-after-submit, or custom workflows.'
+ entrypoints:
+ - 'errorVisibility'
+ - 'createErrorVisibility'
+ - 'createValidator'
+ - 'createValidators'
+ - 'fieldState.meta'
+ - 'state.submissionAttempts'
+ - 'form.FormGroup'
+ sources:
+ - packages/form-core/src/validation.public.ts
+ - packages/form-core/src/FieldApi/fieldState.lib.ts
+ - packages/form-core/src/utils.public.ts
+ - packages/form-core/tests/validation.test-d.ts
+ - packages/form-core/tests/FormApi/validation.spec.ts
+ - packages/form-core/tests/FormGroupApi/FormGroupApi.spec.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ teaches:
+ - 'errorVisibility is a callback, not a string preset.'
+ - 'Use form-level errorVisibility to control default visible-error behavior instead of scattering local checks like field.meta.isTouched && field.meta.isInvalid.'
+ - 'Reusable policies can use createErrorVisibility but only receive unknown form values.'
+ - 'Helper functions mainly serve to give a semantic name to a callback or validator config.'
+ - 'Named reusable policies are good places for JSDoc or comments that explain why errors appear at that moment.'
+ - 'Reusable validator configs are useful when they explain trigger timing, bailIfInvalid, debounce, skipOnSubmit, or rewardEarlyPunishLate behavior.'
+ - 'Inline callbacks can use typed form values from the consuming form.'
+ - 'Visibility callbacks see pre-filter meta, not visible errors or validity.'
+ - 'Group submission attempts are scoped to the nearest group.'
+ failure_modes:
+ - name: string_preset
+ symptom: "User writes errorVisibility: 'touched' or errorVisibility: 'always'."
+ correction: 'Use a callback or createErrorVisibility helper.'
+ evidence:
+ - packages/form-core/tests/validation.test-d.ts
+ - packages/form-core/src/validation.public.ts
+ - name: scattered_local_error_display
+ symptom: 'Each field repeats local showErrors expressions like field.meta.isTouched && field.meta.isInvalid.'
+ correction: 'Move default visible-error behavior into form-level errorVisibility.'
+ evidence:
+ - maintainer_guidance
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ - name: checking_visible_errors_inside_visibility
+ symptom: 'Visibility callback tries to inspect fieldState.meta.errors or validity.'
+ correction: 'Base visibility on pre-filter meta such as isBlurred, isTouched, isDirty, or submissionAttempts.'
+ evidence:
+ - packages/form-core/tests/validation.test-d.ts
+ - packages/form-core/src/FieldApi/fieldState.lib.ts
+ - name: reusable_policy_expects_typed_values
+ symptom: 'A createErrorVisibility callback tries to read state.values.name with a concrete form type.'
+ correction: 'Use inline errorVisibility when typed values are required, or keep reusable policies value-agnostic.'
+ evidence:
+ - packages/form-core/tests/validation.test-d.ts
+ - name: undocumented_policy_intent
+ symptom: 'A complex visibility or validator trigger policy is copied inline, making agents preserve the code but miss the workflow intent.'
+ correction: 'Extract a named helper and use comments or JSDoc to document the intended user behavior.'
+ evidence:
+ - packages/form-core/tests/validation.test.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ - name: group_submit_scope_surprise
+ symptom: 'A wizard section expects form-level submit attempts and group-level submit attempts to be interchangeable.'
+ correction: 'Account for nearest group scoping when using state.submissionAttempts in grouped forms.'
+ evidence:
+ - packages/form-core/tests/FormGroupApi/FormGroupApi.spec.ts
+ - id: type-error-debugging
+ title: Type Error Debugging
+ status: reviewed
+ priority: medium
+ domain: diagnostics-and-examples
+ purpose: 'Help users interpret large TypeScript errors from schemas, option modes, field groups, reusable forms, and adapter APIs.'
+ entrypoints:
+ - 'ReactFormType'
+ - 'FieldWithValue'
+ - 'AnyFieldApi'
+ - 'formOptions'
+ - 'appFormOptions'
+ - 'createErrorVisibility'
+ - 'createValidators'
+ - 'fieldComponent.strict'
+ - 'fieldComponent.loose'
+ - 'fieldBrand.strict'
+ - 'fieldBrand.loose'
+ - 'getFieldGroupHelpers'
+ - 'DeepKeys'
+ - 'DeepValue'
+ sources:
+ - packages/form-core/tests/validation.test-d.ts
+ - packages/form-core/tests/deep-keys.test-d.ts
+ - packages/react-form/tests/submit-return.test-d.tsx
+ - packages/react-form/tests/FormGroup.test-d.tsx
+ - packages/react-form/tests/FieldGroupApi.test-d.tsx
+ - packages/react-form/src/AppForm/getFormHookHelpers.public.ts
+ - packages/react-form/src/FieldGroup/withFields.public.ts
+ teaches:
+ - 'Do not use type assertions to silence TanStack Form type errors; preserve inference and fix the mismatched boundary.'
+ - 'Classify the error by boundary: option mode, schema output, submit return, reusable child form prop, error visibility state, validator helper arity, form group scope, field group virtual path or binding, field component brand, deep-key path, or public API surface.'
+ - 'Treat ReactFormType unions in shared sections as a composition issue; use field groups instead of asserting through the union.'
+ - 'Separate schema strict/loose from field component strict/loose; they solve different problems.'
+ - 'For long errors, find the first concrete path, value, validator, or submit-return mismatch and ignore expanded generic machinery around it.'
+ - 'Reusable createErrorVisibility callbacks are form-agnostic; unknown values usually mean the policy should stay generic or become an inline callback.'
+ - 'createValidators type errors usually mean the count of run functions does not match the count of option configs.'
+ - 'FormGroup type errors usually mean a root-form path or value was used where group-scoped paths or values are expected.'
+ - 'Field-group type errors usually mean real paths were used where virtual paths are expected, a binding map is incomplete, or strict/loose slot compatibility was chosen incorrectly.'
+ - 'Deep-key path errors usually point to path shape; arrays use bracket segments and array helpers only accept paths whose value includes an array.'
+ - 'Use the first meaningful generic mismatch instead of the full expanded error wall.'
+ - 'Check whether the code is trying to use private/internal shape rather than public atom-based APIs.'
+ failure_modes:
+ - name: assertion_hides_form_inference
+ symptom: 'A user adds as const, as any, or a render-prop annotation like field: any to make an error disappear.'
+ correction: 'Remove the assertion and follow the inferred API boundary; use FieldWithValue or AnyFieldApi for reusable field props.'
+ evidence:
+ - maintainer_guidance.hard_do_nots
+ - examples/react/basic-splitting-form/src/StringField.tsx
+ - examples/react/basic-splitting-form/src/FieldError.tsx
+ - name: react_form_type_union
+ symptom: 'A shared section types its form prop as a union of multiple ReactFormType aliases.'
+ correction: 'Use field groups for shared sections across forms; ReactFormType is for splitting a known form.'
+ evidence:
+ - maintainer_guidance.hard_do_nots
+ - packages/react-form/src/FieldGroup/withFields.public.ts
+ - name: reusable_error_visibility_unknown_values
+ symptom: 'A createErrorVisibility helper tries to read a concrete property such as state.values.name.'
+ correction: 'Keep reusable policies value-agnostic, or use an inline callback when concrete form values are needed.'
+ evidence:
+ - packages/form-core/tests/validation.test-d.ts
+ - name: create_validators_arity_mismatch
+ symptom: 'createValidators reports a long tuple or overload error around missing or extra run functions.'
+ correction: 'Pass exactly one run function for each option config.'
+ evidence:
+ - packages/form-core/tests/validation.test-d.ts
+ - name: form_group_scope_mismatch
+ symptom: 'A FormGroup validator or field name complains about missing root fields or invalid paths.'
+ correction: 'Use group-scoped values and paths inside the group; reach through group.form only when root form state is intentionally needed.'
+ evidence:
+ - packages/react-form/tests/FormGroup.test-d.tsx
+ - name: field_group_virtual_path_mismatch
+ symptom: 'A field group method, watchFields entry, Field, or ArrayField rejects a real form path.'
+ correction: 'Use the virtual paths defined by defineFields inside the field group; map them to real paths only through withFields bindings.'
+ evidence:
+ - packages/react-form/tests/FieldGroupApi.test-d.tsx
+ - name: field_group_binding_map_mismatch
+ symptom: 'withFields rejects a binding map as incomplete, wrong, or as the internal defineFields object.'
+ correction: 'Provide every defined field binding, pass real form paths as bindings, and include the required form prop.'
+ evidence:
+ - packages/react-form/tests/FieldGroupApi.test-d.tsx
+ - name: deep_key_path_shape_mismatch
+ symptom: 'A field name or array helper rejects a path involving arrays, optional arrays, readonly arrays, or nullish data.'
+ correction: 'Check the DeepKeys path shape, including bracket array segments like users[0].name and array-valued path constraints.'
+ evidence:
+ - packages/form-core/tests/deep-keys.test-d.ts
+ - name: schema_strict_loose_vs_field_strict_loose
+ symptom: 'User assumes fieldComponent.strict/loose means strictSchema/looseSchema.'
+ correction: 'Explain schema option modes separately from field component value-compatibility branding.'
+ evidence:
+ - packages/react-form/src/AppForm/getFormHookHelpers.public.ts
+ - packages/form-core/src/utils.public.ts
+ - name: submit_specific_type_leaks_to_reusable_child
+ symptom: 'A reusable child rejects a form because TOnSubmit differs.'
+ correction: 'Use shared options without onSubmit or type the child around the intended ReactFormType boundary.'
+ evidence:
+ - packages/react-form/tests/submit-return.test-d.tsx
+ - name: strict_field_group_binding_mismatch
+ symptom: 'A field group strict slot rejects a real field path whose value is only assignable in one direction.'
+ correction: 'Use a loose field slot when the reusable group should accept wider compatible field values.'
+ evidence:
+ - packages/react-form/tests/FieldGroupApi.test-d.tsx
+ - packages/react-form/src/FieldGroup/withFields.public.ts
+ - name: public_api_store_confusion
+ symptom: 'Type error mentions missing store because example uses form.store or field.store.'
+ correction: 'Use public atom-based APIs and adapter subscriptions.'
+ evidence:
+ - packages/react-form/tests/FormGroup.test-d.tsx
+ - packages/react-form/src/ReactForm/fieldSubscriptions.lib.ts
+ - id: examples-authoring
+ title: Examples Authoring
+ status: reviewed
+ priority: high
+ domain: diagnostics-and-examples
+ purpose: 'Help Codex scaffold substantial maintainer examples for testing, future documentation, and user reference.'
+ entrypoints:
+ - 'examples/react/basic'
+ - 'examples/react/array'
+ - 'examples/react/basic-splitting-form'
+ - 'examples/react/field-groups'
+ - 'examples/react/ui-integration-shadcn'
+ - 'examples/react/ui-integration-dnd-kit'
+ sources:
+ - examples/react/basic/src/index.tsx
+ - examples/react/array/src/index.tsx
+ - examples/react/basic-splitting-form/src/index.tsx
+ - examples/react/field-groups/src/index.tsx
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ - examples/react/ui-integration-shadcn/src/app/booking/booking-form.tsx
+ - examples/react/ui-integration-dnd-kit/src/singleList/index.tsx
+ - examples/react/ui-integration-dnd-kit/src/twoLists/index.tsx
+ teaches:
+ - 'Examples are scaffolding targets for maintainers in this private repository, and future users experience them as reference examples.'
+ - 'Examples should model user journeys, not just newly added core behavior.'
+ - 'Use React adapter APIs and realistic composition patterns.'
+ - 'When an example renders arrays of fields, use ArrayField at the list boundary and Field for item properties; when an array is one immutable value, Field is acceptable.'
+ - 'Show schema mode choice, async defaults, submit handling, and error visibility together when that reflects a real task.'
+ - 'Use useSelector or form.Subscribe for reactive form state reads; the form hook return is stable by design.'
+ - 'Keep UI library and query-library ownership boundaries honest.'
+ failure_modes:
+ - name: barren_core_test_example
+ symptom: 'Example only proves a core feature works and does not teach how a user should compose a real form.'
+ correction: 'Write examples around realistic adapter-first user journeys.'
+ evidence:
+ - maintainer_guidance.adapter_first
+ - examples/react/basic/src/index.tsx
+ - examples/react/ui-integration-shadcn/src/app/booking/booking-form.tsx
+ - name: dnd_multi_list_index_model
+ symptom: 'Multi-list drag-and-drop stores items as a single indexed array and races React with the DnD library.'
+ correction: 'Use stable item IDs with separate order arrays for cross-list movement.'
+ evidence:
+ - examples/react/ui-integration-dnd-kit/src/twoLists/index.tsx
+ - name: native_reset_surprise
+ symptom: 'A reset button triggers browser-native reset instead of form.reset semantics.'
+ correction: 'Prevent default button behavior and call form.reset intentionally.'
+ evidence:
+ - examples/react/basic/src/index.tsx
+ - packages/form-core/tests/FormApi/default-value.spec.ts
+ - name: submit_button_not_subscribed
+ symptom: 'Submit button does not reflect canSubmit or isSubmitting reliably.'
+ correction: 'Use form.Subscribe selectors for submit-related state.'
+ evidence:
+ - examples/react/basic/src/index.tsx
+ - packages/react-form/src/ReactForm/Components.public.ts
+gaps:
+ - id: missing-tanstack-query-example
+ skill: query-backed-forms
+ status: open
+ question: 'No checked-in query-backed example was available in this filesystem snapshot.'
+ context: 'The IDE referenced examples/react/tanstack-query/src/App.tsx, but examples/react/tanstack-query was absent. Query-backed guidance is therefore sourced from tests and maintainer interview until a realistic example is scaffolded.'
+evidence_constraints:
+ - id: docs-are-mostly-reference
+ status: noted
+ note: 'The docs directory is largely generated API reference; conceptual documentation is sparse.'
+ implication: 'Generated skills should lean on source, tests, examples, and maintainer interview for current intent.'
+ - id: no-private-issue-signal
+ status: resolved
+ note: 'Maintainer clarified private issues are developer TODOs/plans and not useful user-problem evidence.'
+ implication: 'Do not mine private issues for skill priorities in this repository.'
+ - id: examples-are-maintainer-scaffolds
+ status: resolved
+ note: "Examples are needed so maintainers can test scenarios and expand future documentation; intent skills should improve Codex's ability to scaffold them."
+ implication: 'Do not treat example selection as a public roadmap priority question. Future users primarily consume examples as references.'
+tensions:
+ - id: schema_strictness_vs_editable_defaults
+ skills:
+ - choosing-form-options-modes
+ - schema-driven-forms
+ - type-error-debugging
+ description: 'Schema-source typing improves parsed output confidence, but editable form defaults may need nullable or undefined intermediate values.'
+ implication: 'Agents may choose strictSchema for a ruleset-style form and then fight nullish defaults with assertions, or choose looseSchema for a true transform pipeline and read raw value.'
+ - id: query_loading_ui_vs_form_initialization
+ skills:
+ - query-backed-forms
+ - examples-authoring
+ description: 'Gating a form until query data arrives gives correct initial state, while fallback empty values let the UI render immediately.'
+ implication: 'Agents may pass maybe-undefined query data directly or choose a loading strategy without matching the visual workflow.'
+ - id: validation_state_vs_transient_state
+ skills:
+ - query-backed-forms
+ - schema-driven-forms
+ - error-visibility-workflows
+ description: 'Validation errors are form state that can block submission; transient query/network/framework failures belong outside validation state.'
+ implication: 'Agents may convert retryable endpoint failures into visible field errors and accidentally block the next submit.'
+ - id: reuse_vs_inference_boundaries
+ skills:
+ - react-form-composition-setup
+ - reusable-form-policies
+ - type-error-debugging
+ - examples-authoring
+ description: 'Reusable sections improve examples and app code, but the wrong abstraction boundary breaks inference.'
+ implication: 'Agents may assert field names, hand-write field props, or union ReactFormType forms instead of using inferred children, helper types, or field groups.'
+cross_references:
+ - from: react-form-composition-setup
+ to: examples-authoring
+ reason: 'Examples should model adapter-first composition choices instead of core-level API proofs.'
+ - from: react-form-composition-setup
+ to: reusable-form-policies
+ reason: 'Split sections, AppForm, and field groups determine which reusable helper type or policy boundary is appropriate.'
+ - from: reusable-form-policies
+ to: react-form-composition-setup
+ reason: 'Reusable policies should not replace the larger composition choice between split forms, AppForm, and field groups.'
+ - from: reusable-form-policies
+ to: error-visibility-workflows
+ reason: 'Named visibility helpers should preserve the user workflow that controls when errors appear.'
+ - from: reusable-form-policies
+ to: type-error-debugging
+ reason: 'Reusable helper boundaries often explain long ReactFormType, createErrorVisibility, and createValidators errors.'
+ - from: examples-authoring
+ to: react-form-composition-setup
+ reason: 'Example scaffolds need the Field, ArrayField, Subscribe, AppForm, ReactFormType, and field-group boundaries.'
+ - from: choosing-form-options-modes
+ to: schema-driven-forms
+ reason: 'Schema validation and submit output depend on the selected option mode.'
+ - from: choosing-form-options-modes
+ to: type-error-debugging
+ reason: 'Many large type errors originate at the default, strictSchema, or looseSchema boundary.'
+ - from: schema-driven-forms
+ to: query-backed-forms
+ reason: 'Query-backed submit handlers often need to convert server validation responses into returned validation values.'
+ - from: query-backed-forms
+ to: error-visibility-workflows
+ reason: 'Async defaults and submit failures often change when users expect validation errors to become visible.'
+ - from: type-error-debugging
+ to: react-form-composition-setup
+ reason: 'Most type fixes should preserve adapter inference instead of adding assertions.'
+phase5_finalization:
+ reviewed_on: '2026-06-25'
+ resolved_interview_topics:
+ - hard_do_nots
+ - schema_mode_wording
+ - query_backed_default_value_strategies
+ - async_on_submit_errors
+ - reusable_policy_helpers
+ - app_form_vs_split_sections_vs_field_groups
+ - field_vs_array_field
+ - type_error_debugging_from_type_tests
+ - examples_as_maintainer_scaffolds
+ - standard_schema_libraries_as_heuristic_examples
+ remaining_open_gaps:
+ - missing-tanstack-query-example
diff --git a/_artifacts/skill_spec.md b/_artifacts/skill_spec.md
new file mode 100644
index 0000000000..ce1e85d5e1
--- /dev/null
+++ b/_artifacts/skill_spec.md
@@ -0,0 +1,480 @@
+# TanStack Form V2 Skill Spec
+
+Status: reviewed, Phase 5 finalized.
+
+This reviewed artifact follows the `pnpm intent:scaffold` domain-discovery workflow. It is intentionally adapter-first: user-facing guidance starts from `@tanstack/react-form`, with `@tanstack/form-core` used as the source of concepts and behavior rather than as a workflow entrypoint.
+
+## Scope
+
+In scope:
+
+- React form composition with `useForm`, `form.Field`, `form.ArrayField`, `form.Subscribe`, app-form helpers, and field groups.
+- Standard Schema-compatible schema usage.
+- Choosing between default form options, `strictSchema`, and `looseSchema`.
+- Query-like async defaults and mutation-like submit handling.
+- Error visibility callback workflows.
+- Split-form helper types and reusable validation or visibility policy modules.
+- Type error diagnosis for schema/form composition.
+- Authoring richer React examples.
+
+Out of scope for this pass:
+
+- Devtools as an entrypoint.
+- Migration guidance.
+- Schema metadata extraction, such as automatically deriving required labels.
+- TanStack Form owning transient or non-validation error state.
+- Teaching UI-library behavior as part of TanStack Form itself.
+- Solid-first coverage.
+
+## Hard Do Nots
+
+- Do not type assert to make TanStack Form code compile. The library is built around inference, and assertions are usually a sign that the code is fighting the API.
+- Do not write field names like ``name={`users[${index}].name` as const}``; `name` is inferred without the assertion.
+- Do not annotate render-prop children as `any`; field children already receive inferred field APIs.
+- Do not shim field prop types by hand, such as `{ value: string }`; use inferred render-prop types, `FieldWithValue`, or `AnyFieldApi` when a reusable component needs an explicit field prop type.
+- Do not treat validation errors and transient errors as the same thing. Errors stored from validators or `onSubmit` block the user from submitting.
+- Do not store query/network/framework transient failures as form validation errors. Most query libraries already own transient error state; use that state instead.
+- Do not throw validation errors from validators or `onSubmit`; validation errors must be returned values, such as `createValidationError` or `parseIssues`.
+- Do not type a shared section as `form: FormA | FormB` where each form is a different `ReactFormType`; TypeScript cannot make sense of that union. Use field groups for shared sections that need to work across different forms.
+
+## Proposed Skills
+
+### 1. React Form Composition Setup
+
+Purpose: Help users build adapter-first React forms, reusable field components, app-form setups, field groups, arrays, and UI-library bindings.
+
+Primary APIs:
+
+- `useForm`
+- `ReactFormType`
+- `FieldWithValue`
+- `AnyFieldApi`
+- `formOptions`
+- `createFormHook`
+- `form.Field`
+- `form.ArrayField`
+- `form.Subscribe`
+- `form.FormGroup`
+- `withFields`
+
+Core guidance:
+
+- Start from `@tanstack/react-form`, not `@tanstack/form-core`.
+- Use render-prop field APIs and subscriptions for state reads.
+- Treat `useForm` and `useAppForm` as stable form creation hooks, not reactive state reads by themselves.
+- Use `useSelector` or `form.Subscribe` when component output must update in response to form state.
+- Use app-form helpers when an application wants reusable field and form components.
+- Keep shared form options submit-agnostic when child components need to accept forms with different `onSubmit` handlers.
+- Use `ReactFormType` as one option for splitting a known form into child sections that receive the form as a prop.
+- Do not use unions like `ReactFormType | ReactFormType` for shared sections; use field groups when one section should bind into multiple different forms.
+- Use AppForm to reduce repeated field JSX boilerplate. Use split form sections to reduce component nesting. Use field groups for reusable sections that bind to different form paths or different forms.
+- Let field `name` props and render-prop children infer their types; use `FieldWithValue` or `AnyFieldApi` for explicit reusable field component props.
+- `form.Field` may bind to an array value. It treats that array immutably like any other field value, so changing one element changes the field `value` and rerenders consumers of the whole array field.
+- Use `form.ArrayField` when the array is a rendering boundary for child fields or list items, and those child fields are the reactive units the example cares about.
+- Use array helpers for array mutations and reorders.
+
+Important failure modes:
+
+- Adding `as const`, `as any`, render-prop `field: any`, or other assertions to force form code to compile.
+- Shimming field prop types by hand instead of using inferred field types, `FieldWithValue`, or `AnyFieldApi`.
+- Reading from the stable `useForm` or `useAppForm` return and expecting the component to re-render as form state changes.
+- Importing core directly in examples or application code.
+- Rendering app field components outside their required app-form or field context.
+- Making a reusable child form prop too specific by baking in an `onSubmit` type.
+- Splitting a form by hand-writing a large form prop type instead of deriving it from shared options with `ReactFormType`.
+- Typing a shared section as a union of multiple `ReactFormType` forms instead of using field groups.
+- Using `form.Field` to render a list of child fields, then being surprised that item changes rerender the whole array-value consumer.
+- Treating `form.ArrayField` as required for every array-typed value, instead of using it when the array will render child fields or list items.
+- Mutating arrays manually and desynchronizing values from field meta.
+
+Source trail:
+
+- `packages/react-form/src/ReactForm/Components.public.ts`
+- `packages/react-form/src/ReactForm/ReactFormApi.lib.tsx`
+- `packages/react-form/src/Subscribe.public.tsx`
+- `packages/react-form/src/ReactForm/fieldSubscriptions.lib.ts`
+- `packages/react-form/src/AppForm/createFormHook.public.ts`
+- `packages/react-form/src/AppForm/contexts.lib.ts`
+- `docs/reference/type-aliases/FieldWithValue.md`
+- `docs/reference/type-aliases/AnyFieldApi.md`
+- `docs/framework/react/reference/type-aliases/ReactFormType.md`
+- `packages/react-form/tests/submit-return.test-d.tsx`
+- `packages/form-core/src/FormApi/array-methods.lib.ts`
+- `packages/form-core/src/FormApi/FormApiArrayMethods.types.public.ts`
+- `packages/form-core/src/FieldApi/fieldState.lib.ts`
+- `examples/react/array/src/index.tsx`
+- `examples/react/basic-splitting-form/src/index.tsx`
+- `examples/react/basic-splitting-form/src/FormSection.tsx`
+- `examples/react/basic-splitting-form/src/StringField.tsx`
+- `examples/react/basic-splitting-form/src/FieldError.tsx`
+- `examples/react/ui-integration-shadcn/src/components/form/app-form.ts`
+
+### Cross-Cutting: Reusable Form Policies
+
+Purpose: Help agents recognize when form behavior should be extracted into a named reusable helper rather than repeated inline.
+
+Primary APIs:
+
+- `ReactFormType`
+- `createErrorVisibility`
+- `createValidator`
+- `createValidators`
+- shared `formOptions` / `appFormOptions`
+
+Core guidance:
+
+- Use `ReactFormType` as the helper type for split form sections that receive a `form` prop.
+- Treat `ReactFormType` splitting as one composition option, not a general replacement for AppForm or field groups.
+- AppForm reduces repeated field JSX boilerplate; split sections reduce nesting in a known form; field groups cover shared sections across different forms or field paths.
+- Prefer form-level `errorVisibility` callbacks over scattered local patterns like `const showErrors = field.meta.isTouched && field.meta.isInvalid`.
+- Use helper functions mainly when a callback or validator config deserves a semantic name that preserves intent.
+- Extract validator configs with `createValidator` or `createValidators` when trigger timing, debounce, `bailIfInvalid`, or `skipOnSubmit` policy is complex enough to name.
+- Use comments or JSDoc on named helpers to explain behavior intent, such as why errors wait for blur or submit, or why a validator uses a `rewardEarlyPunishLate` policy.
+- Keep reusable policies value-agnostic unless they are intentionally tied to a specific form shape; inline callbacks are better when typed values are required.
+
+Important failure modes:
+
+- A split form loses field-name inference because the child prop type was hand-written.
+- A shared section accepts `FormA | FormB` and TypeScript cannot narrow field names or form methods enough to make the section usable.
+- Error display behavior is scattered through local `showErrors` variables instead of being controlled by form-level `errorVisibility`.
+- A complex visibility workflow is duplicated inline and later examples preserve code shape but lose the user-workflow reason.
+- Validator trigger settings are copied without explanation, so agents cannot tell whether `blur`, `change`, debounce, or `bailIfInvalid` is intentional.
+- A reusable visibility function tries to read a concrete `state.values.name` even though reusable visibility callbacks are form-agnostic.
+
+Source trail:
+
+- `examples/react/basic-splitting-form/src/FormSection.tsx`
+- `examples/react/basic-splitting-form/src/sharedForm.ts`
+- `packages/react-form/tests/submit-return.test-d.tsx`
+- `packages/form-core/tests/validation.test.ts`
+- `packages/form-core/tests/validation.test-d.ts`
+- `examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx`
+
+### 2. Choosing Form Options Modes
+
+Purpose: Help users decide between default `formOptions`, `strictSchema`, and `looseSchema`.
+
+Primary APIs:
+
+- `formOptions`
+- `formOptions.strictSchema`
+- `formOptions.looseSchema`
+- `appFormOptions`
+- `appFormOptions.strictSchema`
+- `appFormOptions.looseSchema`
+
+Core guidance:
+
+- Treat the option helpers as runtime identity helpers with type-level meaning.
+- Use default `formOptions` when the form should be inferred from `defaultValues`; the default values are taken at face value and that type flows into validators and callbacks.
+- Default `formOptions` is especially useful for callback-based validation because callback `value` is already typed from the form defaults.
+- Use `strictSchema` when the schema is the source of typing and acts as a transforming pipeline from input type to output type.
+- In `strictSchema`, `defaultValues` should type-check against the schema input type. If they do not align, the defaults should produce type errors.
+- Use `looseSchema` when the schema is a ruleset for confirming valid output, but editable defaults need nullable or undefined values, such as a date field that starts as `null`.
+- In `looseSchema`, default values must still align with the schema shape, but properties may also be `null` or `undefined` where editing requires it.
+- In both schema modes, callbacks may not have useful inference unless there is a schema in the validator array for the helper to infer from.
+- In both schema modes, `onSubmit` should read the parsed transform output from `validatorOutputs` rather than raw `value`.
+- Treat pipeline-vs-ruleset as TanStack Form guidance, not a Standard Schema concept. Standard Schema only gives the shared validation/typing interface.
+- Explain the mental model as: is this schema a pipeline of constraints/transforms, or a firewall that only lets valid values through?
+- List current Standard Schema-capable libraries as examples with a general leaning, not as hard categories.
+- Zod generally leans ruleset/firewall for form work, while explicit transforms/codecs can still make a Zod schema pipeline-like.
+- Valibot generally leans pipeline because its docs center `pipe` and validation/transformation actions.
+- ArkType is mixed: structural constraints often read like a firewall, while morphs/pipes can make it pipeline-like.
+- Effect Schema often leans pipeline/codec when used as an encoded-to-decoded boundary.
+
+Important failure modes:
+
+- Expecting `strictSchema` or `looseSchema` to add runtime validation by itself.
+- Using default `formOptions` while expecting the schema to be the source of the form type.
+- Using strict schema mode for a form whose editable UI states cannot satisfy the schema type.
+- Using `looseSchema` but providing default values that no longer match the schema shape at all.
+- Using schema modes without putting a schema in the validator array, then expecting callbacks to infer from that schema.
+- Using loose schema mode for a true pipeline and then reading raw `value` as if it were parsed output.
+- Reading `value` in `onSubmit` for schema-mode forms instead of reading the corresponding `validatorOutputs` entry.
+- Treating all Zod `input` and `output` differences as a real pipeline decision.
+
+Source trail:
+
+- `packages/form-core/src/utils.public.ts`
+- `packages/react-form/src/AppForm/appFormOptions.public.ts`
+- `packages/form-core/tests/validation.test.ts`
+- `packages/form-core/tests/validation.test-d.ts`
+- `examples/react/ui-integration-shadcn/src/app/booking/schema.ts`
+- `examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx`
+- `https://standardschema.dev/`
+- `https://zod.dev/library-authors`
+- `https://valibot.dev/guides/integrate-valibot/`
+- `https://arktype.io/docs/integrations`
+- `https://effect.website/docs/schema/standard-schema/`
+
+### 3. Schema Driven Forms
+
+Purpose: Help users take a Standard Schema-compatible schema and wire it into validation, field errors, submit output, and type inference.
+
+Primary APIs:
+
+- `validators`
+- `FormSubmitContext.validatorOutputs`
+- `parseIssues`
+- `createValidationError`
+
+Core guidance:
+
+- Configure schema validation through validators.
+- Extract reusable validator trigger configs with `createValidator` or `createValidators` when the behavior is complex enough to need a semantic name.
+- Read parsed schema output from `onSubmit` context's `validatorOutputs`; raw `value` remains form state.
+- Route submit validation errors as returned values through `createValidationError` or `parseIssues`; do not throw them.
+- Let Standard Schema issue paths route nested and array field errors when possible.
+- Keep UI metadata explicit or schema-library-specific; Standard Schema does not standardize it.
+
+Important failure modes:
+
+- Expecting schema output to replace form `value`.
+- Returning or throwing raw schema errors from submit code and expecting field errors.
+- Throwing validation errors instead of returning `createValidationError` or `parseIssues` values.
+- Manually mapping nested issue paths incorrectly.
+- Duplicating validator trigger, debounce, submit, or bail policy inline without documenting why that behavior exists.
+- Trying to build general UI metadata extraction from Standard Schema.
+
+Source trail:
+
+- `packages/form-core/src/standardSchema.public.ts`
+- `packages/form-core/src/standardSchema.lib.ts`
+- `packages/form-core/src/FormApi/FormApi.public.ts`
+- `packages/form-core/tests/validation.test.ts`
+- `packages/form-core/tests/FormApi/submission-handling.spec.ts`
+- `examples/react/field-groups/src/index.tsx`
+
+### 4. Query Backed Forms
+
+Purpose: Help users combine form state with query-like async defaults and mutation-like submit calls.
+
+Primary APIs:
+
+- `useForm({ defaultValues })`
+- `onSubmit`
+- `form.reset`
+- `createValidationError`
+- `parseIssues`
+
+Core guidance:
+
+- Do not feed maybe-undefined query data into the form by itself; choose an explicit default-values plan.
+- Approach 1: do not mount the fields until the query is finished. The whole form can also be gated so the first mounted form state is populated with correct data.
+- Approach 2: define a static shape-complete `emptyFormValues` object and pass defaults as `query.data ?? emptyFormValues`.
+- Neither approach is preferred globally; choose based on the desired loading and visual UI language.
+- Gating fields changes the UI because fields or the whole form appear only after data is ready. The fallback approach can render the form immediately, often with loading or placeholder state around it.
+- Update defaults when async data arrives and rely on touched-field preservation for user edits.
+- Separate transient errors from validation errors.
+- Use an async promise for the endpoint call inside `onSubmit` so `isSubmitting` reflects the request.
+- Catch submit failures only when the endpoint can return validation errors that should be converted into returned `createValidationError` or `parseIssues` values.
+- Transient submit failures may throw or reject; the form will set `isSubmitSuccessful` to false, and query/framework state should preserve the transient error details.
+- Leave transient query, network, and framework failures in the query/framework error state instead of converting them into form validation errors.
+- Use reset options deliberately when replacing both values and default baselines.
+
+Important failure modes:
+
+- Treating initial `undefined` query data as a final form shape.
+- Passing `query.data` directly as defaults without either gated mounting or an `emptyFormValues` fallback.
+- Gating fields without accounting for the different loading UI, or rendering fallback values without communicating that query data is still loading.
+- Overwriting touched user edits when async defaults arrive.
+- Starting an endpoint call inside `onSubmit` without returning or awaiting its promise, so `isSubmitting` does not cover the request.
+- Turning transient network or server failures into validation errors and accidentally blocking future submits.
+- Throwing validation errors from `onSubmit` instead of returning validation values.
+- Treating a rejected transient `onSubmit` as something that should produce visible field errors.
+
+Source trail:
+
+- `packages/react-form/tests/useForm.spec.tsx`
+- `packages/form-core/src/FormApi/FormApi.lib.ts`
+- `packages/form-core/src/FormApi/FormApi.public.ts`
+- `packages/form-core/tests/FormApi/default-value.spec.ts`
+- `packages/form-core/tests/FormApi/submission-handling.spec.ts`
+
+Open evidence gap: the IDE referenced `examples/react/tanstack-query/src/App.tsx`, but this checkout did not contain `examples/react/tanstack-query`.
+
+### 5. Error Visibility Workflows
+
+Purpose: Help users implement policies such as "show after blur", "show after submit", or "show after blur or submit".
+
+Primary APIs:
+
+- `errorVisibility`
+- `createErrorVisibility`
+- `createValidator`
+- `createValidators`
+- `fieldState.meta`
+- `state.submissionAttempts`
+- `form.FormGroup`
+
+Core guidance:
+
+- `errorVisibility` is callback-based.
+- Use form-level `errorVisibility` to control default visible-error behavior instead of scattering local checks like `field.meta.isTouched && field.meta.isInvalid`.
+- Reusable visibility policies created with `createErrorVisibility` should be value-agnostic.
+- Use `createErrorVisibility`, `createValidator`, and `createValidators` mainly when a policy benefits from a semantic name or intent-focused comments/JSDoc.
+- Inline callbacks can use the consuming form's typed values.
+- Visibility callbacks receive pre-filter field meta, not visible errors or validity.
+- Grouped forms scope submission attempts to the nearest group.
+
+Important failure modes:
+
+- Writing string presets such as `errorVisibility: 'touched'`.
+- Repeating local `showErrors` expressions across fields instead of setting the form's default error visibility behavior.
+- Inspecting `fieldState.meta.errors` or validity inside the visibility callback.
+- Expecting reusable `createErrorVisibility` callbacks to know a concrete form value shape.
+- Copying complex visibility or validator policy inline without documenting the workflow intent.
+- Forgetting group-scoped submission attempts in wizard or section-level flows.
+
+Source trail:
+
+- `packages/form-core/src/validation.public.ts`
+- `packages/form-core/src/FieldApi/fieldState.lib.ts`
+- `packages/form-core/tests/validation.test-d.ts`
+- `packages/form-core/tests/FormApi/validation.spec.ts`
+- `packages/form-core/tests/FormGroupApi/FormGroupApi.spec.ts`
+- `examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx`
+
+### 6. Type Error Debugging
+
+Purpose: Help users interpret large TypeScript errors from schemas, option modes, field groups, reusable forms, and adapter APIs.
+
+Primary APIs and concepts:
+
+- `ReactFormType`
+- `formOptions`
+- `appFormOptions`
+- `createErrorVisibility`
+- `createValidators`
+- schema `strictSchema` / `looseSchema`
+- field component `strict` / `loose`
+- field group `strict` / `loose`
+- `DeepKeys`
+- `DeepValue`
+- public `atom` APIs
+
+Core guidance:
+
+- Do not use type assertions to silence TanStack Form type errors; preserve inference and fix the mismatched boundary.
+- Classify the error by boundary: schema option mode, schema output, submit return, reusable child form props, error visibility state, validator helper arity, form group scope, field group virtual path/binding, field component brand, deep-key path, or public/private API mismatch.
+- Treat a `ReactFormType` union in a shared section as a design issue, not a TypeScript problem to assert through; reach for field groups instead.
+- Do not confuse schema strict/loose with field component or field group strict/loose.
+- For long errors, find the first concrete path, value, validator, or submit-return mismatch and ignore the expanded generic machinery around it.
+- Reusable `createErrorVisibility` callbacks are form-agnostic; if a type error mentions `unknown` values, use an inline callback for form-specific values.
+- `createValidators` type errors often mean the number of run functions does not match the number of option configs.
+- FormGroup type errors often mean the code used a root-form path/value where the group expects group-scoped paths/values.
+- Field-group type errors often mean the code used real form paths where virtual paths are expected, passed an incomplete binding map, or chose `strict` when the real path is only loosely compatible.
+- Deep-key path errors usually point to path shape: arrays use bracket segments such as `users[${number}].name`, and array helpers only accept paths whose value includes an array.
+- Look for the first meaningful generic mismatch rather than the full expanded error wall.
+- Prefer source-backed explanation over broad TypeScript folklore.
+
+Important failure modes:
+
+- Adding assertions like `as const`, `as any`, or `(field: any)` instead of letting form names and children infer.
+- Hand-writing shim field prop types instead of using inferred props, `FieldWithValue`, or `AnyFieldApi`.
+- Typing a shared section as `ReactFormType | ReactFormType`.
+- Expecting a reusable `createErrorVisibility` callback to read concrete form values.
+- Calling `createValidators` with fewer or more run functions than option configs.
+- Using root-form field names or values inside a `FormGroup` validator or grouped field.
+- Passing real form paths into a field group API where virtual paths are required.
+- Providing an incomplete `withFields` binding map, passing the internal defined-fields object as a binding, or omitting the required `form` prop.
+- Reading `form.store` or `field.store` instead of public `atom` APIs.
+- Treating `fieldComponent.strict/loose` as schema mode selection.
+- Passing a submit-specific form type to a reusable child that should accept several submit handlers.
+- Binding a strict field group slot to a field path whose value is only loosely compatible.
+
+Source trail:
+
+- `packages/form-core/tests/validation.test-d.ts`
+- `packages/form-core/tests/deep-keys.test-d.ts`
+- `packages/react-form/tests/submit-return.test-d.tsx`
+- `packages/react-form/tests/FormGroup.test-d.tsx`
+- `packages/react-form/tests/FieldGroupApi.test-d.tsx`
+- `packages/react-form/src/AppForm/getFormHookHelpers.public.ts`
+- `packages/react-form/src/FieldGroup/withFields.public.ts`
+
+### 7. Examples Authoring
+
+Purpose: Help Codex scaffold substantial maintainer examples for testing, future documentation, and user reference.
+
+Primary example areas:
+
+- Basic form and submit handling.
+- Schema-driven form.
+- Query-backed form.
+- Error visibility workflow.
+- App-form and UI-library integration.
+- Field groups.
+- Arrays and reorder behavior.
+
+Core guidance:
+
+- Examples are scaffolding targets for maintainers in this private repository, and future users experience them as reference examples.
+- Examples should be adapter-first and user-journey-shaped.
+- Combine features when real workflows combine them, such as schema mode plus async defaults plus submit validation plus visibility policy.
+- Keep boundaries explicit: TanStack Form handles validation and form state; query/framework code owns transient errors; UI libraries own visual behavior.
+- Show reactive state reads with `useSelector` or `form.Subscribe`; do not imply the stable hook return is itself reactive.
+- Prefer realistic examples over minimal API proofs once behavior has test coverage.
+
+Important failure modes:
+
+- Writing barren examples that resemble core tests more than user workflows.
+- Modeling complex DnD multi-list state as a single indexed array and creating UI races.
+- Letting native reset behavior fight form reset semantics.
+- Reading form state from the stable hook return without a selector or subscription, then wondering why UI does not update.
+- Reading submit state outside `form.Subscribe` and missing updates.
+
+Source trail:
+
+- `examples/react/basic/src/index.tsx`
+- `examples/react/array/src/index.tsx`
+- `examples/react/basic-splitting-form/src/index.tsx`
+- `examples/react/field-groups/src/index.tsx`
+- `examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx`
+- `examples/react/ui-integration-shadcn/src/app/booking/booking-form.tsx`
+- `examples/react/ui-integration-dnd-kit/src/singleList/index.tsx`
+- `examples/react/ui-integration-dnd-kit/src/twoLists/index.tsx`
+
+## Cross-Skill Relationships
+
+- React Form Composition Setup is the base skill for all example work.
+- Reusable Form Policies sits between React Form Composition Setup, Error Visibility Workflows, and Type Error Debugging.
+- Choosing Form Options Modes feeds Schema Driven Forms and Type Error Debugging.
+- Schema Driven Forms feeds Query Backed Forms when submit returns server-side validation.
+- Error Visibility Workflows is often layered on top of Schema Driven Forms and Query Backed Forms.
+- Examples Authoring should compose the other skills into realistic maintainer scaffolds that can later serve as references.
+
+## Cross-Skill Tensions
+
+- Schema strictness vs editable defaults: strict schema typing helps submit output, but editable forms often need nullable or undefined intermediate values.
+- Query loading UI vs form initialization: gated mounting gives correct first form state, while `emptyFormValues` lets the UI render immediately.
+- Validation state vs transient state: validation errors block submit, while query/network/framework failures should remain outside form validation state.
+- Reuse vs inference boundaries: reusable sections are valuable, but assertions, hand-written field props, or `ReactFormType` unions break the intended inference model.
+
+## Remaining Gaps
+
+- Query Backed Forms: the IDE referenced `examples/react/tanstack-query/src/App.tsx`, but this checkout did not contain `examples/react/tanstack-query`. Query-backed guidance is reviewed from tests and maintainer interview, and should be exercised by scaffolding a realistic example later.
+
+## Phase 5 Finalization
+
+Resolved in the detail interview:
+
+- Hard do-nots, especially no type assertions and no transient errors as validation errors.
+- Schema mode wording for default `formOptions`, `strictSchema`, and `looseSchema`.
+- Query-backed default-value strategies and their visual loading language.
+- Async `onSubmit` behavior, including `isSubmitting`, `isSubmitSuccessful`, returned validation values, and thrown transient failures.
+- Reusable policy helpers as semantic names for complex callbacks/configs.
+- AppForm, `ReactFormType` split sections, and field groups as separate composition tools.
+- Field vs `ArrayField`, especially array-as-immutable-value versus array-as-child-field-rendering-boundary.
+- Type-error debugging categories derived from the type tests.
+- Examples authoring as maintainer scaffolding for testing and future reference examples.
+- Standard Schema libraries as heuristic examples, with pipeline-vs-firewall/ruleset clearly marked as TanStack Form guidance rather than a Standard Schema concept.
+
+Reviewed acceptance criteria:
+
+- Skills never make `@tanstack/form-core` the user-facing workflow entrypoint.
+- Skills explain schemas through adapter APIs while preserving core semantics.
+- Skills distinguish validation errors from transient async failures.
+- Skills do not promise schema metadata extraction.
+- Skills do not use devtools or migration as current entrypoints.
+- Examples guidance is realistic enough for agents to build non-barren examples.
diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml
new file mode 100644
index 0000000000..5be3cbdd3c
--- /dev/null
+++ b/_artifacts/skill_tree.yaml
@@ -0,0 +1,224 @@
+schema_version: 1
+status: reviewed
+library:
+ name: TanStack Form v2
+ version: '0.0.0'
+ repository: https://github.com/TanStack/form-v2
+ description: 'Headless form state, validation, schema, and adapter APIs for TanStack Form v2, with React as the primary skill target.'
+generated_from:
+ domain_map: _artifacts/domain_map.yaml
+ skill_spec: _artifacts/skill_spec.md
+generated_at: '2026-06-25'
+layout:
+ kind: flat
+ reason: 'The reviewed skills are task-focused and independently discoverable; no router skill is needed for 8 skills.'
+ default_package: packages/react-form
+ adapter_first: true
+skills:
+ - name: React Form Composition Setup
+ slug: react-form-composition-setup
+ type: framework
+ domain: react-adapter-composition
+ path: packages/react-form/skills/react-form-composition-setup/SKILL.md
+ package: packages/react-form
+ description: 'Use when building adapter-first React forms with useForm, form.Field, form.ArrayField, form.Subscribe, AppForm, ReactFormType, field groups, reusable field components, or UI-library bindings.'
+ sources:
+ - packages/react-form/src/ReactForm/Components.public.ts
+ - packages/react-form/src/ReactForm/fieldSubscriptions.lib.ts
+ - packages/react-form/src/AppForm/createFormHook.public.ts
+ - packages/react-form/src/FieldGroup/withFields.public.ts
+ - packages/react-form/tests/submit-return.test-d.tsx
+ - packages/form-core/src/FormApi/FormApiArrayMethods.types.public.ts
+ - packages/form-core/src/FieldApi/fieldState.lib.ts
+ - examples/react/array/src/index.tsx
+ - examples/react/basic-splitting-form/src/index.tsx
+ - examples/react/ui-integration-shadcn/src/components/form/app-form.ts
+ - name: Reusable Form Policies
+ slug: reusable-form-policies
+ type: framework
+ domain: validation-and-error-visibility
+ path: packages/react-form/skills/reusable-form-policies/SKILL.md
+ package: packages/react-form
+ description: 'Use when extracting ReactFormType split-form props, createErrorVisibility policies, createValidator/createValidators configs, shared formOptions, or appFormOptions into named reusable helpers with documented intent.'
+ requires:
+ - react-form-composition-setup
+ - error-visibility-workflows
+ sources:
+ - examples/react/basic-splitting-form/src/FormSection.tsx
+ - examples/react/basic-splitting-form/src/sharedForm.ts
+ - packages/react-form/tests/submit-return.test-d.tsx
+ - packages/form-core/tests/validation.test.ts
+ - packages/form-core/tests/validation.test-d.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ references:
+ - references/reusable-policy-boundaries.md
+ - name: Choosing Form Options Modes
+ slug: choosing-form-options-modes
+ type: framework
+ domain: schema-driven-forms
+ path: packages/react-form/skills/choosing-form-options-modes/SKILL.md
+ package: packages/react-form
+ description: 'Use when choosing between default formOptions, formOptions.strictSchema, formOptions.looseSchema, appFormOptions, and schema-source typing for React forms.'
+ sources:
+ - packages/form-core/src/utils.public.ts
+ - packages/react-form/src/AppForm/appFormOptions.public.ts
+ - packages/form-core/tests/validation.test.ts
+ - packages/form-core/tests/validation.test-d.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/schema.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ - https://standardschema.dev/
+ - https://zod.dev/library-authors
+ - https://valibot.dev/guides/integrate-valibot/
+ - https://arktype.io/docs/integrations
+ - https://effect.website/docs/schema/standard-schema/
+ references:
+ - references/schema-option-modes.md
+ - references/standard-schema-library-leanings.md
+ - name: Schema Driven Forms
+ slug: schema-driven-forms
+ type: framework
+ domain: schema-driven-forms
+ path: packages/react-form/skills/schema-driven-forms/SKILL.md
+ package: packages/react-form
+ description: 'Use when wiring Standard Schema-compatible validators, validators arrays, outputs, parseIssues, createValidationError, nested issue paths, and schema submit output into React forms.'
+ requires:
+ - choosing-form-options-modes
+ sources:
+ - packages/form-core/src/standardSchema.public.ts
+ - packages/form-core/src/standardSchema.lib.ts
+ - packages/form-core/src/FormApi/FormApi.public.ts
+ - packages/form-core/tests/validation.test.ts
+ - packages/form-core/tests/validation.test-d.ts
+ - packages/form-core/tests/FormApi/submission-handling.spec.ts
+ - examples/react/field-groups/src/index.tsx
+ references:
+ - references/schema-validation-and-submit-output.md
+ - name: Query Backed Forms
+ slug: query-backed-forms
+ type: composition
+ domain: async-data-and-submit
+ path: packages/react-form/skills/query-backed-forms/SKILL.md
+ package: packages/react-form
+ description: 'Use when combining React forms with query-like async defaults, maybe-undefined incoming data, emptyFormValues fallbacks, gated field mounting, async onSubmit, mutation calls, and transient-vs-validation errors.'
+ requires:
+ - react-form-composition-setup
+ sources:
+ - packages/react-form/tests/useForm.spec.tsx
+ - packages/form-core/src/FormApi/FormApi.lib.ts
+ - packages/form-core/src/FormApi/FormApi.public.ts
+ - packages/form-core/src/FormApi/handleSubmit.lib.ts
+ - packages/form-core/tests/FormApi/default-value.spec.ts
+ - packages/form-core/tests/FormApi/submission-handling.spec.ts
+ - maintainer_guidance.query_shape
+ references:
+ - references/query-backed-defaults-and-submit.md
+ - name: Error Visibility Workflows
+ slug: error-visibility-workflows
+ type: framework
+ domain: validation-and-error-visibility
+ path: packages/react-form/skills/error-visibility-workflows/SKILL.md
+ package: packages/react-form
+ description: 'Use when implementing errorVisibility callbacks, createErrorVisibility helpers, show-after-blur, show-after-submit, grouped submission attempts, fieldState.meta visibility, or reusable validation trigger policies.'
+ sources:
+ - packages/form-core/src/validation.public.ts
+ - packages/form-core/src/FieldApi/fieldState.lib.ts
+ - packages/form-core/src/utils.public.ts
+ - packages/form-core/tests/validation.test-d.ts
+ - packages/form-core/tests/FormApi/validation.spec.ts
+ - packages/form-core/tests/FormGroupApi/FormGroupApi.spec.ts
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ references:
+ - references/error-visibility-policies.md
+ - name: Type Error Debugging
+ slug: type-error-debugging
+ type: framework
+ domain: diagnostics-and-examples
+ path: packages/react-form/skills/type-error-debugging/SKILL.md
+ package: packages/react-form
+ description: 'Use when explaining large TypeScript errors from ReactFormType, FieldWithValue, AnyFieldApi, formOptions, appFormOptions, createErrorVisibility, createValidators, field groups, DeepKeys, DeepValue, or schema modes.'
+ requires:
+ - react-form-composition-setup
+ - choosing-form-options-modes
+ sources:
+ - packages/form-core/tests/validation.test-d.ts
+ - packages/form-core/tests/deep-keys.test-d.ts
+ - packages/react-form/tests/submit-return.test-d.tsx
+ - packages/react-form/tests/FormGroup.test-d.tsx
+ - packages/react-form/tests/FieldGroupApi.test-d.tsx
+ - packages/react-form/src/AppForm/getFormHookHelpers.public.ts
+ - packages/react-form/src/FieldGroup/withFields.public.ts
+ references:
+ - references/type-error-boundaries.md
+ - name: Examples Authoring
+ slug: examples-authoring
+ type: lifecycle
+ domain: diagnostics-and-examples
+ path: packages/react-form/skills/examples-authoring/SKILL.md
+ package: packages/react-form
+ description: 'Use when scaffolding realistic React examples for TanStack Form v2, including schema-driven forms, query-backed forms, error visibility workflows, app-form UI integrations, field groups, arrays, and reorder behavior.'
+ requires:
+ - react-form-composition-setup
+ sources:
+ - examples/react/basic/src/index.tsx
+ - examples/react/array/src/index.tsx
+ - examples/react/basic-splitting-form/src/index.tsx
+ - examples/react/field-groups/src/index.tsx
+ - examples/react/ui-integration-shadcn/src/app/booking/shared-form.tsx
+ - examples/react/ui-integration-shadcn/src/app/booking/booking-form.tsx
+ - examples/react/ui-integration-dnd-kit/src/singleList/index.tsx
+ - examples/react/ui-integration-dnd-kit/src/twoLists/index.tsx
+ references:
+ - references/example-scenarios.md
+tensions:
+ - name: schema_strictness_vs_editable_defaults
+ skills:
+ - choosing-form-options-modes
+ - schema-driven-forms
+ - type-error-debugging
+ implication: 'Generated skills must warn against fighting nullish editable defaults with assertions or reading raw value for pipeline output.'
+ - name: query_loading_ui_vs_form_initialization
+ skills:
+ - query-backed-forms
+ - examples-authoring
+ implication: 'Generated skills must tie the default-values plan to the visual loading workflow.'
+ - name: validation_state_vs_transient_state
+ skills:
+ - query-backed-forms
+ - schema-driven-forms
+ - error-visibility-workflows
+ implication: 'Generated skills must keep transient query/network failures out of form validation state.'
+ - name: reuse_vs_inference_boundaries
+ skills:
+ - react-form-composition-setup
+ - reusable-form-policies
+ - type-error-debugging
+ - examples-authoring
+ implication: 'Generated skills must prefer inference, helper types, and field groups over assertions, hand-written field shims, or ReactFormType unions.'
+cross_references:
+ - from: react-form-composition-setup
+ to: examples-authoring
+ - from: react-form-composition-setup
+ to: reusable-form-policies
+ - from: reusable-form-policies
+ to: react-form-composition-setup
+ - from: reusable-form-policies
+ to: error-visibility-workflows
+ - from: reusable-form-policies
+ to: type-error-debugging
+ - from: examples-authoring
+ to: react-form-composition-setup
+ - from: choosing-form-options-modes
+ to: schema-driven-forms
+ - from: choosing-form-options-modes
+ to: type-error-debugging
+ - from: schema-driven-forms
+ to: query-backed-forms
+ - from: query-backed-forms
+ to: error-visibility-workflows
+ - from: type-error-debugging
+ to: react-form-composition-setup
+gaps:
+ - id: missing-tanstack-query-example
+ skill: query-backed-forms
+ status: open
+ note: 'No checked-in examples/react/tanstack-query example existed during discovery; query-backed form guidance is reviewed from tests and maintainer interview.'
diff --git a/docs/arrays.md b/docs/arrays.md
new file mode 100644
index 0000000000..ef2d56ce88
--- /dev/null
+++ b/docs/arrays.md
@@ -0,0 +1,429 @@
+---
+id: arrays
+title: Arrays
+---
+
+TanStack Form fields can hold arrays just like any other value. The important distinction is whether the array is the field value itself or whether you're rendering fields for the items inside it.
+
+If one control reads and replaces the whole array, use a regular field. If you're rendering fields for individual items and need to add, remove, or reorder them, use an array field.
+
+This guide covers both cases and how to choose between them.
+
+## An array as the field value
+
+Use a regular field when one control reads and replaces the complete array. A
+multi-select, tag picker, or checkbox group can pass its next array to
+`handleChange` like any other field value:
+
+
+
+# React
+
+```tsx
+
+ {(field) => (
+ field.handleChange(roleIds)}
+ />
+ )}
+
+```
+
+# Preact
+
+```tsx
+
+ {(field) => (
+ field.handleChange(roleIds)}
+ />
+ )}
+
+```
+
+# Solid
+
+```tsx
+
+ {(field) => (
+ field().handleChange(roleIds)}
+ />
+ )}
+
+```
+
+# Angular
+
+```html
+
+
+
+```
+
+# Lit
+
+```ts
+this.form.field(
+ { name: 'selectedRoleIds' },
+ (field) => html`
+ >) =>
+ field.handleChange(event.detail)}
+ >
+ `,
+)
+```
+
+# Svelte
+
+```svelte
+
+ {#snippet children(field)}
+ field.handleChange(roleIds)}
+ />
+ {/snippet}
+
+```
+
+# Vue
+
+```vue
+
+
+
+```
+
+
+
+## Rendering fields in an array
+
+If you need to edit fields inside each array item, the previous approach has two problems.
+
+- Editing any item's value would cause the whole array to rerender.
+- Moving items within the array would not update fields, since it was only changing the array's value.
+
+
+
+# React
+
+This is where `form.ArrayField` comes in. It ensures that each item in the array is rendered independently, while still subscribing to
+item changes like swapping, removing or pushing new values.
+
+```tsx
+
+ {(array) => (
+
+ {array.value.map((person, index) => (
+
+
+ {(field) => }
+
+
+ ))}
+
+ )}
+
+```
+
+# Preact
+
+This is where `form.ArrayField` comes in. It ensures that each item in the array is rendered independently, while still subscribing to
+item changes like swapping, removing or pushing new values.
+
+```tsx
+
+ {(array) => (
+
+ {array.value.map((person, index) => (
+
+
+ {(field) => }
+
+
+ ))}
+
+ )}
+
+```
+
+# Solid
+
+This is where `form.ArrayField` comes in. It ensures that each item in the array is rendered independently, while still subscribing to
+item changes like swapping, removing or pushing new values.
+
+```tsx
+
+ {(array) => (
+
+
+ {(_person, index) => (
+
+
+ {(field) => }
+
+
+ )}
+
+
+ )}
+
+```
+
+# Angular
+
+This is where `TanStackArrayField` comes in. It ensures that each item in the array is rendered independently, while still subscribing to
+item changes like swapping, removing or pushing new values.
+
+```ts
+import { Component } from '@angular/core'
+import {
+ TanStackArrayField,
+ TanStackField,
+ injectForm,
+} from '@tanstack/angular-form'
+
+@Component({
+ selector: 'app-people-form',
+ standalone: true,
+ imports: [TanStackArrayField, TanStackField],
+ template: `
+
+
+ @for (person of people.api.value; track person.id; let index = $index) {
+
+
+
+
+
+ }
+
+
+ `,
+})
+export class PeopleFormComponent {
+ form = injectForm({
+ defaultValues: {
+ people: [{ id: 'person-1', name: '' }],
+ },
+ })
+
+ personName(index: number): `people[${number}].name` {
+ return `people[${index}].name`
+ }
+}
+```
+
+# Lit
+
+This is where `form.arrayField` comes in. It ensures that each item in the array is rendered independently, while still subscribing to
+item changes like swapping, removing or pushing new values.
+
+```ts
+this.form.arrayField(
+ { name: 'people' },
+ (array) => html`
+
+ `,
+)
+```
+
+# Svelte
+
+This is where `form.ArrayField` comes in. It ensures that each item in the array is rendered independently, while still subscribing to
+item changes like swapping, removing or pushing new values.
+
+```svelte
+
+ {#snippet children(array)}
+
+ {#each array.value as person, index (person.id)}
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+ {/each}
+
+ {/snippet}
+
+```
+
+# Vue
+
+This is where `form.ArrayField` comes in. It ensures that each item in the array is rendered independently, while still subscribing to
+item changes like swapping, removing or pushing new values.
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### Keeping item identity for objects
+
+While the nested fields use the index in their `name`, use your adapter's identity mechanism to keep each object tied
+to the same rendered item.
+
+
+
+# React
+
+```tsx
+array.value.map((person) =>
-
- `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- people: [] as Array<{ name: string; age: number }>,
- },
- onSubmit({ value }) {
- alert(JSON.stringify(value))
- },
- })
-}
-```
-
-This will generate the mapped JSX every time you run `pushValue` on `field`:
-
-```angular-html
-
-```
-
-Finally, you can use a subfield like so:
-
-```angular-html
-
-
-
-
-
-```
-
-Where `getPeopleName` is a method on the class like so:
-
-```typescript
-export class AppComponent {
- getPeopleName = (idx: number) => `people[${idx}].name` as const
-
- // ...
-}
-```
-
-> While it's unfortunate that you need to use a function to get the field name, it's a requirement for how our strict TypeScript types work.
->
-> See, if we did the following:
->
-> ```angular-html
-> [tanstackField]="form"
-> [name]="'people[' + $index + '].name'"
-> >
-> ```
->
-> We'd be running into a TypeScript issue where `"one" + "two"` is `string` rather than the required `"onetwo"` type
->
-> Moreover, while Angular supports template literals in the template, they may not contain dynamic interpolation within them, such as our `$index` argument.
-
-> It's possible that we've missed something! If you can think of a better fix for this problem, [drop us a line on our GitHub discussions](https://github.com/TanStack/form/discussions).
-
-## Full Example
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
- `,
-})
-export class AppComponent {
- defaultPerson = { name: '', age: 0 }
-
- form = injectForm({
- defaultValues: {
- people: [] as Array<{ name: string; age: number }>,
- },
- onSubmit({ value }) {
- alert(JSON.stringify(value))
- },
- })
-
- getPeopleName = (idx: number) => `people[${idx}].name` as const
-
- canSubmit = injectStore(this.form, (state) => state.canSubmit)
- isSubmitting = injectStore(this.form, (state) => state.isSubmitting)
-
- handleSubmit(event: SubmitEvent) {
- event.preventDefault()
- event.stopPropagation()
- this.form.handleSubmit()
- }
-}
-```
diff --git a/docs/framework/angular/guides/async-initial-values.md b/docs/framework/angular/guides/async-initial-values.md
new file mode 100644
index 0000000000..40a40d6e4b
--- /dev/null
+++ b/docs/framework/angular/guides/async-initial-values.md
@@ -0,0 +1,159 @@
+---
+id: async-initial-values
+title: Async Initial Values
+---
+
+Forms often edit data loaded from an API. A server-state library such as
+TanStack Query should own fetching, caching, loading, and retries; TanStack
+Form should own the editable copy.
+
+There are two useful patterns for async initial values in Angular: resolve the
+data before constructing the form, or construct the form immediately with
+complete fallback values and reset it when data arrives.
+
+## Resolve data before constructing the editor
+
+Angular Router resolvers can make the record available synchronously when the
+editor component is constructed. This lets `defaultValues` start with the
+complete loaded value.
+
+```ts
+import { Component, inject } from '@angular/core'
+import { ActivatedRoute } from '@angular/router'
+import type { ResolveFn, Routes } from '@angular/router'
+import { injectForm } from '@tanstack/angular-form'
+
+type User = {
+ firstName: string
+ lastName: string
+}
+
+export const userResolver: ResolveFn = async (route) => {
+ const response = await fetch(`/api/users/${route.paramMap.get('userId')}`)
+ return response.json()
+}
+
+@Component({
+ selector: 'app-user-editor',
+ standalone: true,
+ template: ``,
+})
+export class UserEditorComponent {
+ private route = inject(ActivatedRoute)
+ private user = this.route.snapshot.data['user'] as User
+
+ form = injectForm({
+ defaultValues: this.user,
+ onSubmit: async ({ value }) => {
+ await fetch('/api/users', {
+ method: 'PUT',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify(value),
+ })
+ },
+ })
+}
+
+export const routes: Routes = [
+ {
+ path: 'users/:userId',
+ component: UserEditorComponent,
+ resolve: { user: userResolver },
+ },
+]
+```
+
+This pattern is useful when no form state is needed during loading. It gives
+the form complete data on its initial construction. If the router reuses the
+same component instance for another record, reset that form from the new
+resolved data or arrange for a fresh editor instance.
+
+## Create the form before data arrives
+
+You can instead use `injectForm` in the same component as `injectQuery`.
+Provide a static, shape-complete fallback so `defaultValues` are never
+`undefined`, then deliberately reset the untouched form when query data
+arrives.
+
+```ts
+import { Component, effect, inject } from '@angular/core'
+import { ActivatedRoute } from '@angular/router'
+import { injectQuery } from '@tanstack/angular-query-experimental'
+import { injectForm } from '@tanstack/angular-form'
+
+type User = {
+ firstName: string
+ lastName: string
+}
+
+const emptyUser: User = {
+ firstName: '',
+ lastName: '',
+}
+
+@Component({
+ selector: 'app-user-page',
+ standalone: true,
+ template: `
+ @if (userQuery.isPending()) {
+
Loadingā¦
+ } @else if (userQuery.isError()) {
+
Could not load the user.
+ } @else {
+
+ }
+ `,
+})
+export class UserPageComponent {
+ private route = inject(ActivatedRoute)
+ private userId = this.route.snapshot.paramMap.get('userId')!
+
+ userQuery = injectQuery(() => ({
+ queryKey: ['user', this.userId],
+ queryFn: async (): Promise => {
+ const response = await fetch(`/api/users/${this.userId}`)
+ return response.json()
+ },
+ }))
+
+ form = injectForm({
+ defaultValues: emptyUser,
+ onSubmit: async ({ value }) => {
+ await fetch('/api/users', {
+ method: 'PUT',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify(value),
+ })
+ },
+ })
+
+ constructor() {
+ effect(() => {
+ const user = this.userQuery.data()
+ if (user && !this.form.state.isTouched) {
+ this.form.reset(user)
+ }
+ })
+ }
+}
+```
+
+In this example, fields remain unmounted during loading, so the user cannot
+edit them before the loaded values arrive. You can render the empty form
+immediately when that better suits the UI, but then decide how incoming data
+should interact with edits.
+
+This is the approach demonstrated by the repository's
+`examples/angular/query-integration` example.
+
+## Background refetches and record changes
+
+`defaultValues` establish the form's baseline; they are not a fully controlled
+`values` prop. When background data arrives, decide at the product level
+whether to keep local edits, prompt before replacing them, or call
+`form.reset(nextValues)` deliberately. Do not overwrite in-progress input
+merely because a query refreshed.
+
+When switching to a different record, construct a fresh editor or deliberately
+reset the existing form. Otherwise, values from the previous record can remain
+in the form.
diff --git a/docs/framework/angular/guides/basic-concepts.md b/docs/framework/angular/guides/basic-concepts.md
deleted file mode 100644
index facc91c612..0000000000
--- a/docs/framework/angular/guides/basic-concepts.md
+++ /dev/null
@@ -1,363 +0,0 @@
----
-id: basic-concepts
-title: Basic Concepts and Terminology
----
-
-This page introduces the basic concepts and terminology used in the `@tanstack/angular-form` library. Familiarizing yourself with these concepts will help you better understand and work with the library.
-
-## Form Instance
-
-A Form Instance is an object that represents an individual form and provides methods and properties for working with the form. You create a form instance using the `injectForm` function. The hook accepts an object with an `onSubmit` function, which is called when the form is submitted.
-
-```typescript
-const form = injectForm({
- onSubmit: async ({ value }) => {
- // Do something with form data
- console.log(value)
- },
-})
-```
-
-## Field
-
-A Field represents a single form input element, such as a text input or a checkbox. Fields are created using the `tanstackField` directive. The directive accepts a name prop, which should match a key in the form's default values. It also exposes a `field` named instance of the directive's internals that should be used via a template variable to access the field's internals.
-
-Example:
-
-```html
-
-
-
-```
-
-## Field State
-
-Each field has its own state, which includes its current value, validation status, error messages, and other metadata. You can access a field's state using the `fieldApi.state` property.
-
-Example:
-
-```ts
-const {
- value,
- meta: { errors, isValidating },
-} = field.state
-```
-
-There are four states in the metadata that can be useful to see how the user interacts with a field:
-
-- _"isTouched"_, after the user changes the field or blurs the field
-- _"isDirty"_, after the field's value has been changed, even if it's been reverted to the default. Opposite of _"isPristine"_
-- _"isPristine"_, until the user changes the field value. Opposite of _"isDirty"_
-- _"isBlurred"_, after the field has been blurred
-
-```ts
-const { isTouched, isDirty, isPristine, isBlurred } = field.state.meta
-```
-
-
-
-## Understanding 'isDirty' in Different Libraries
-
-Non-Persistent `dirty` state
-
-- **Libraries**: React Hook Form (RHF), Formik, Final Form.
-- **Behavior**: A field is 'dirty' if its value differs from the default. Reverting to the default value makes it 'clean' again.
-
-Persistent `dirty` state
-
-- **Libraries**: Angular Form, Vue FormKit.
-- **Behavior**: A field remains 'dirty' once changed, even if reverted to the default value.
-
-We have chosen the persistent 'dirty' state model. To also support a non-persistent 'dirty' state, we introduce an additional flag:
-
-- _"isDefaultValue"_, whether the field's current value is the default value
-
-```ts
-const { isDefaultValue, isTouched } = field.state.meta
-
-// The following line will re-create the non-Persistent `dirty` functionality.
-const nonPersistentIsDirty = !isDefaultValue
-```
-
-
-
-## Field API
-
-The Field API is an object accessed in the `tanstackField.api` property when creating a field. It provides methods for working with the field's state.
-
-Example:
-
-```angular-html
-
-```
-
-## Validation
-
-`@tanstack/angular-form` provides both synchronous and asynchronous validation out of the box. Validation functions can be passed to the `tanstackField` directive using the `validators` prop.
-
-Example:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
-
- `,
-})
-export class AppComponent {
- firstNameValidator: FieldValidateFn = ({ value }) =>
- !value
- ? 'A first name is required'
- : value.length < 3
- ? 'First name must be at least 3 characters'
- : undefined
-
- firstNameAsyncValidator: FieldValidateAsyncFn = async ({
- value,
- }) => {
- await new Promise((resolve) => setTimeout(resolve, 1000))
- return value.includes('error') && 'No "error" allowed in first name'
- }
-
- form = injectForm({
- defaultValues: {
- firstName: '',
- },
- onSubmit({ value }) {
- console.log(value)
- },
- })
-}
-```
-
-## Validation with Standard Schema Libraries
-
-In addition to hand-rolled validation options, we also support the [Standard Schema](https://github.com/standard-schema/standard-schema) specification.
-
-You can define a schema using any of the libraries implementing the specification and pass it to a form or field validator.
-
-Supported libraries include:
-
-- [Zod](https://zod.dev/) (v3.24.0 or higher)
-- [Valibot](https://valibot.dev/) (v1.0.0 or higher)
-- [ArkType](https://arktype.io/) (v2.1.20 or higher)
-- [Yup](https://github.com/jquense/yup) (v1.7.0 or higher)
-
-Example:
-
-```angular-ts
-import { z } from 'zod'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
-
- `,
-})
-export class AppComponent {
- firstNameAsyncValidator = z.string().refine(
- async (value) => {
- await new Promise((resolve) => setTimeout(resolve, 1000))
- return !value.includes('error')
- },
- {
- message: "No 'error' allowed in first name",
- },
- )
-
- form = injectForm({
- defaultValues: {
- firstName: '',
- },
- onSubmit({ value }) {
- // Do something with form data
- console.log(value)
- },
- })
-
- z = z
-}
-```
-
-## Reactivity
-
-`@tanstack/angular-form` offers a way to subscribe to form and field state changes via `injectStore(this.form, selector)`.
-
-Example:
-
-```typescript
-import { injectForm, injectStore } from '@tanstack/angular-form'
-
-@Component(/*...*/)
-class AppComponent {
- form = injectForm(/*...*/)
- canSubmit = injectStore(this.form, (state) => state.canSubmit)
- isSubmitting = injectStore(this.form, (state) => state.isSubmitting)
-}
-```
-
-## Listeners
-
-`@tanstack/angular-form` allows you to react to specific triggers and "listen" to them to dispatch side effects.
-
-Example:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
- `,
-})
-
-...
-
-onCountryChange: FieldListenerFn = ({
- value,
- }) => {
- console.log(`Country changed to: ${value}, resetting province`)
- this.form.setFieldValue('province', '')
- }
-```
-
-More information can be found at [Listeners](./listeners.md)
-
-## Array Fields
-
-Array fields allow you to manage a list of values within a form, such as a list of hobbies. You can create an array field using the `tanstackField` directive.
-
-When working with array fields, you can use the fields `pushValue`, `removeValue`, `swapValues` and `moveValue` methods to add, remove, swap, and move a value from one index to another within the array, respectively. Additional helper methods such as `insertValue`, `replaceValue`, and `clearValues` are also available for inserting, replacing, and clearing array values.
-
-Example:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
- Hobbies
-
- @if (!hobbies.api.state.value.length) {
- No hobbies found
- }
- @for (_ of hobbies.api.state.value; track $index) {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
-
-
-
-
- `,
-})
-export class AppComponent {
- defaultHobby = {
- name: '',
- description: '',
- yearsOfExperience: 0,
- }
-
- getHobbyName = (idx: number) => `hobbies[${idx}].name` as const
- getHobbyDesc = (idx: number) => `hobbies[${idx}].description` as const
-
- form = injectForm({
- defaultValues: {
- hobbies: [] as Array<{
- name: string
- description: string
- yearsOfExperience: number
- }>,
- },
- onSubmit({ value }) {
- alert(JSON.stringify(value))
- },
- })
-}
-```
-
-These are the basic concepts and terminology used in the `@tanstack/angular-form` library. Understanding these concepts will help you work more effectively with the library and create complex forms with ease.
diff --git a/docs/framework/angular/guides/debugging.md b/docs/framework/angular/guides/debugging.md
new file mode 100644
index 0000000000..92590d4b88
--- /dev/null
+++ b/docs/framework/angular/guides/debugging.md
@@ -0,0 +1,99 @@
+---
+id: debugging
+title: Debugging Angular Usage
+---
+
+This page covers common Angular and TypeScript problems when using TanStack
+Form.
+
+## An input initially renders `undefined`
+
+Every value rendered by an input must exist in form-level `defaultValues` from
+the first render.
+
+```ts
+form = injectForm({
+ defaultValues: {
+ firstName: '',
+ subscribed: false,
+ },
+})
+```
+
+Do not omit `defaultValues` or rely on a field-level default. When values come
+from an API, render a loading state before mounting the editable fields or
+supply a complete fallback shape.
+
+## A field value is `unknown`
+
+Field names and values are inferred from `defaultValues`. Check that you have
+not widened the initial object to `Record`, `any`, or an
+unrelated union. Prefer a concrete value type:
+
+```ts
+type Profile = { firstName: string; age: number }
+
+const defaults: Profile = { firstName: '', age: 0 }
+form = injectForm({ defaultValues: defaults })
+```
+
+For extremely large forms, split UI into typed components or scoped form and
+field groups instead of casting each field value.
+
+## A child component rejects the form input
+
+Use `formOptions` for reusable base options and derive the input type with
+`AngularFormType`. This keeps the child compatible when the parent adds
+`onSubmit`:
+
+```ts
+import { Component, input } from '@angular/core'
+import { formOptions } from '@tanstack/angular-form'
+import type { AngularFormType } from '@tanstack/angular-form'
+
+export const profileFormOptions = formOptions({
+ defaultValues: { firstName: '', age: 0 },
+})
+
+@Component({
+ selector: 'app-profile-fields',
+ standalone: true,
+ template: ``,
+})
+export class ProfileFieldsComponent {
+ form = input.required>()
+}
+```
+
+## Async defaults do not update
+
+`injectForm` receives its initial options when the component is constructed.
+When data arrives later, call `form.reset(nextValues)` deliberately, normally
+inside an Angular `effect`, or resolve the data before constructing the form.
+Guard against replacing edits the user has already made.
+
+## `injectForm` reports a missing injection context
+
+Call `injectForm`, `injectSelector`, and `injectField` in an Angular injection
+context, such as a component field initializer or constructor. Do not call
+them later from an event handler, subscription callback, or arbitrary utility
+function.
+
+## Type instantiation is excessively deep
+
+This TypeScript error usually involves a very large recursive value shape, a
+widely inferred union, or unsupported composition. Reduce it to the smallest
+form that reproduces the problem, keep `defaultValues` concrete, and avoid
+unnecessary generic wrappers.
+
+If the minimal reproduction still fails, report it in the
+[TanStack Form issue tracker](https://github.com/TanStack/form/issues). Include
+the Angular and TypeScript versions, the form value type, and the smallest
+failing field.
+
+## Errors exist but are not displayed
+
+`field.api.errors` respects the form's `errorVisibility` policy. Inspect
+`field.api.meta.original.errors` when debugging whether validation ran but the
+issues are intentionally hidden. Also confirm that the input calls
+`field.api.handleBlur()` if the policy depends on `isBlurred`.
diff --git a/docs/framework/angular/guides/dynamic-validation.md b/docs/framework/angular/guides/dynamic-validation.md
deleted file mode 100644
index 455613002e..0000000000
--- a/docs/framework/angular/guides/dynamic-validation.md
+++ /dev/null
@@ -1,313 +0,0 @@
----
-id: dynamic-validation
-title: Dynamic Validation
----
-
-In many cases, you want to change the validation rules based depending on the state of the form or other conditions. The most popular
-example of this is when you want to validate a field differently based on whether the user has submitted the form for the first time or not.
-
-We support this through our `onDynamic` validation function.
-
-```angular-ts
-import { Component } from '@angular/core'
-import {
- TanStackField,
- injectForm,
- revalidateLogic,
-} from '@tanstack/angular-form'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: ` `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- firstName: '',
- lastName: '',
- },
- // If this is omitted, onDynamic will not be called
- validationLogic: revalidateLogic(),
- validators: {
- onDynamic: ({ value }) => {
- if (!value.firstName) {
- return { firstName: 'A first name is required' }
- }
- return undefined
- },
- },
- })
-}
-```
-
-> By default `onDynamic` is not called, so you need to pass `revalidateLogic()` to the `validationLogic` option of `injectForm`.
-
-## Revalidation Options
-
-`revalidateLogic` allows you to specify when validation should be run and change the validation rules dynamically based on the current submission state of the form.
-
-It takes two arguments:
-
-- `mode`: The mode of validation prior to the first form submission. This can be one of the following:
- - `change`: Validate on every change.
- - `blur`: Validate on blur.
- - `submit`: Validate on submit. (**default**)
-
-- `modeAfterSubmission`: The mode of validation after the form has been submitted. This can be one of the following:
- - `change`: Validate on every change. (**default**)
- - `blur`: Validate on blur.
- - `submit`: Validate on submit.
-
-You can, for example, use the following to revalidate on blur after the first submission:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: ` `,
-})
-export class AppComponent {
- form = injectForm({
- // ...
- validationLogic: revalidateLogic({
- mode: 'submit',
- modeAfterSubmission: 'blur',
- }),
- // ...
- })
-}
-```
-
-## Accessing Errors
-
-Just as you might access errors from an `onChange` or `onBlur` validation, you can access the errors from the `onDynamic` validation function using the form's error map through `injectStore`.
-
-```angular-ts
-import { Component } from '@angular/core'
-import {
- TanStackField,
- injectForm,
- injectStore,
- revalidateLogic,
-} from '@tanstack/angular-form'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
{{ formErrorMap().onDynamic?.firstName }}
`,
-})
-export class AppComponent {
- form = injectForm({
- // ...
- validationLogic: revalidateLogic(),
- validators: {
- onDynamic: ({ value }) => {
- if (!value.firstName) {
- return { firstName: 'A first name is required' }
- }
- return undefined
- },
- },
- })
-
- formErrorMap = injectStore(this.form, (state) => state.errorMap)
-}
-```
-
-## Usage with Other Validation Logic
-
-You can use `onDynamic` validation alongside other validation logic, such as `onChange` or `onBlur`.
-
-```angular-ts
-import { Component } from '@angular/core'
-import {
- TanStackField,
- injectForm,
- injectStore,
- revalidateLogic,
-} from '@tanstack/angular-form'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
{{ formErrorMap().onChange?.firstName }}
-
{{ formErrorMap().onDynamic?.lastName }}
-
- `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- firstName: '',
- lastName: '',
- },
- validationLogic: revalidateLogic(),
- validators: {
- onChange: ({ value }) => {
- if (!value.firstName) {
- return { firstName: 'A first name is required' }
- }
- return undefined
- },
- onDynamic: ({ value }) => {
- if (!value.lastName) {
- return { lastName: 'A last name is required' }
- }
- return undefined
- },
- },
- })
-
- formErrorMap = injectStore(this.form, (state) => state.errorMap)
-}
-```
-
-### Usage with Fields
-
-You can also use `onDynamic` validation with fields, just like you would with other validation logic.
-
-```angular-ts
-import { Component } from '@angular/core'
-import {
- TanStackField,
- injectForm,
- revalidateLogic,
-} from '@tanstack/angular-form'
-import type { FieldValidateFn } from '@tanstack/angular-form'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
- `,
-})
-export class AppComponent {
- ageValidator: FieldValidateFn = ({ value }) =>
- value > 18 ? undefined : 'Age must be greater than 18'
-
- form = injectForm({
- defaultValues: {
- name: '',
- age: 0,
- },
- validationLogic: revalidateLogic(),
- onSubmit({ value }) {
- alert(JSON.stringify(value))
- },
- })
-
- handleSubmit(event: SubmitEvent) {
- event.preventDefault()
- event.stopPropagation()
- this.form.handleSubmit()
- }
-}
-```
-
-### Async Validation
-
-Async validation can also be used with `onDynamic` just like with other validation logic. You can even debounce the async validation to avoid excessive calls.
-
-```angular-ts
-import { Component } from '@angular/core'
-import {
- TanStackField,
- injectForm,
- revalidateLogic,
-} from '@tanstack/angular-form'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: ` `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- username: '',
- },
- validationLogic: revalidateLogic(),
- validators: {
- onDynamicAsyncDebounceMs: 500, // Debounce the async validation by 500ms
- onDynamicAsync: async ({ value }) => {
- if (!value.username) {
- return { username: 'Username is required' }
- }
- // Simulate an async validation
- const isValid = await validateUsername(value.username)
- return isValid ? undefined : { username: 'Username is already taken' }
- },
- },
- })
-}
-```
-
-### Standard Schema Validation
-
-You can also use standard schema validation libraries like Valibot or Zod with `onDynamic` validation. This allows you to define complex validation rules that can change dynamically based on the form state.
-
-```angular-ts
-import { Component } from '@angular/core'
-import {
- TanStackField,
- injectForm,
- revalidateLogic,
-} from '@tanstack/angular-form'
-import { z } from 'zod'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: ` `,
-})
-export class AppComponent {
- schema = z.object({
- firstName: z.string().min(1, 'A first name is required'),
- lastName: z.string().min(1, 'A last name is required'),
- })
-
- form = injectForm({
- defaultValues: {
- firstName: '',
- lastName: '',
- },
- validationLogic: revalidateLogic(),
- validators: {
- onDynamic: this.schema,
- },
- })
-}
-```
diff --git a/docs/framework/angular/guides/form-composition.md b/docs/framework/angular/guides/form-composition.md
deleted file mode 100644
index 35bb092628..0000000000
--- a/docs/framework/angular/guides/form-composition.md
+++ /dev/null
@@ -1,271 +0,0 @@
----
-id: form-composition
-title: Form Composition
----
-
-A common criticism of TanStack Form is its verbosity out-of-the-box. While this _can_ be useful for educational purposes - helping enforce understanding our APIs - it's not ideal in production use cases.
-
-As a result, while basic usage of `[tanstackField]` enables the most powerful and flexible usage of TanStack Form, we provide APIs that wrap it and make your application code less verbose.
-
-## Pre-bound Field Components
-
-If you've ever used TanStack Form in Angular to bind more than one input, you'll have quickly realized how much goes into each input:
-
-```angular-ts
-import { Component } from '@angular/core'
-import { TanStackField, injectForm, injectStore } from '@tanstack/angular-form'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
- }
- `,
-})
-export class AppTextField {
- label = input.required()
- // This API requires another part to it from the parent component
- field = injectField()
-}
-```
-
-> `injectField` accepts a single generic to define the `field.state.value` type.
->
-> As a result, a numerical text field would be represented as `injectField`, for example.
-
-Now, we can use the `TanStackAppField` directive (`tanstack-app-field`) to `provide` the expected field associated with this input:
-
-```angular-ts
-import { Component } from '@angular/core'
-import {
- TanStackAppField,
- TanStackField,
- injectForm,
-} from '@tanstack/angular-form'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField, TanStackAppField, AppTextField],
- template: `
-
-
-
-
-
-
- `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- firstName: '',
- lastName: '',
- },
- onSubmit({ value }) {
- // Do something with form data
- console.log(value)
- },
- })
-}
-```
-
-> Here, the `tanstack-app-field` directive is taking the properties from `[tanstackField]` and `provide`ing them down to the `app-text-field` so that they can be more easily consumed as a component.
-
-## Breaking big forms into smaller pieces
-
-Sometimes forms get very large; it's just how it goes sometimes. While TanStack Form supports large forms well, it's never fun to work with hundreds or thousands of lines of code in a single component.
-
-To solve this, we support breaking forms into smaller pieces using the `[tanstack-with-form]` directive together with the `injectWithForm` function.
-
-The parent component owns the `form` (created via `injectForm`) and passes it down to a child component using the `tanstack-with-form` directive. The child component reads it back via `injectWithForm`, which returns a fully-typed reference to that same `FormApi` ā meaning the child can render `[tanstackField]` bindings, `tanstack-app-field` components, etc. against the parent's form without needing to redeclare any generics.
-
-First, lift your shared form options out using `formOptions` so both the parent and the child can reference the same shape:
-
-```angular-ts
-// shared-form.ts
-import { formOptions } from '@tanstack/angular-form'
-
-export const peopleFormOpts = formOptions({
- defaultValues: {
- firstName: '',
- lastName: '',
- },
-})
-```
-
-Then, build a child component that consumes the form via `injectWithForm`:
-
-```angular-ts
-// child-form.component.ts
-import { Component } from '@angular/core'
-import {
- TanStackAppField,
- TanStackField,
- injectWithForm,
-} from '@tanstack/angular-form'
-import { AppTextField } from './app-text-field.component'
-import { peopleFormOpts } from './shared-form'
-
-@Component({
- selector: 'app-child-form',
- standalone: true,
- imports: [TanStackField, TanStackAppField, AppTextField],
- template: `
-
-
- `,
-})
-export class ChildForm {
- // Spreading the form options into `injectWithForm` is what gives `withForm.form`
- // its full type inference ā no generics needed.
- withForm = injectWithForm({ ...peopleFormOpts })
-}
-```
-
-> The options passed to `injectWithForm({ ...peopleFormOpts })` are only used for type inference. They are not read at runtime; the actual `FormApi` instance comes from the ancestor `tanstack-with-form` directive.
-
-Finally, in the parent component, create the form with `injectForm` and pass it to the child via the `tanstack-with-form` directive:
-
-```angular-ts
-// app.component.ts
-import { Component } from '@angular/core'
-import { TanStackWithForm, injectForm } from '@tanstack/angular-form'
-import { ChildForm } from './child-form.component'
-import { peopleFormOpts } from './shared-form'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackWithForm, ChildForm],
- template: `
-
- `,
-})
-export class AppComponent {
- form = injectForm({
- ...peopleFormOpts,
- onSubmit({ value }) {
- console.log(value)
- },
- })
-
- handleSubmit(event: SubmitEvent) {
- event.preventDefault()
- event.stopPropagation()
- this.form.handleSubmit()
- }
-}
-```
-
-This pattern lets you split a large form across many components while keeping a single source of truth for the form's state and types. Any descendant of the `tanstack-with-form` directive can call `injectWithForm({ ...peopleFormOpts })` to gain typed access to the same form instance.
diff --git a/docs/framework/angular/guides/form-groups.md b/docs/framework/angular/guides/form-groups.md
deleted file mode 100644
index 96beebddd6..0000000000
--- a/docs/framework/angular/guides/form-groups.md
+++ /dev/null
@@ -1,302 +0,0 @@
----
-id: form-groups
-title: Form Groups
----
-
-When building a multi-stage form that has many stages, like so:
-
-
-
-It's common for each step to have its own form. However, this complicates the form submission and validation process by requiring you to add complex logic.
-
-Luckily, TanStack Form provides a way to build out sub-forms that make this kind of development trivial to implement: `[tanstackFormGroup]`.
-
-## Usage
-
-To use a form group in TanStack Form, you'll use `injectForm` to create a `form` variable, then reference it with the `tanstackFormGroup` directive like you would with `tanstackField`:
-
-```angular-ts
-import { Component } from '@angular/core'
-import { TanStackFormGroup, injectForm } from '@tanstack/angular-form'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackFormGroup],
- template: `
-
-
-
-
- `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- step1: {
- name: '',
- },
- step2: {
- age: 0,
- },
- },
- })
-}
-```
-
-This becomes much more useful when paired with external state to conditionally render a form group:
-
-```angular-ts
-import { Component, signal } from '@angular/core'
-import {
- TanStackField,
- TanStackFormGroup,
- injectForm,
-} from '@tanstack/angular-form'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField, TanStackFormGroup],
- template: `
- @if (step() === 0) {
-
-
-
- }
-
- @if (step() === 1) {
-
-
-
- }
- `,
-})
-export class AppComponent {
- step = signal(0)
- submitMeta = {} as SomeType
-
- form = injectForm({
- defaultValues: {
- step1: {
- name: '',
- },
- step2: {
- age: 0,
- },
- },
- })
-
- onStep1Submit = () => {
- // We can move the step forward when validation passes
- this.step.update((step) => step + 1)
- }
-
- onStep1SubmitInvalid = () => {
- // Or handle invalid submissions, just like a top-level form
- }
-
- onStep2Submit = () => {
- // Then, use `form.handleSubmit()` to submit the entire form
- this.form.handleSubmit()
- }
-}
-```
-
-When you split each step into its own component, pass the parent form down with [`tanstack-with-form`](./form-composition.md) and read it in the child with `injectWithForm`. The child can then use `[tanstackFormGroup]="withForm.form"` and `[tanstackField]="withForm.form"` against the same parent form instance.
-
-## Form Group Validation
-
-Form groups have a distinct validation procedure that we think makes sense for sub-forms:
-
-- Form groups can have their own validation:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackFormGroup],
- template: `
-
-
-
-
- `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- step1: { name: '' },
- },
- })
-
- groupValidator = () => 'Error'
-}
-```
-
-- Can set errors on sub-fields:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackFormGroup],
- template: `
-
- `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- step1: { name: '' },
- },
- })
-
- groupValidator = ({ value }: { value: { name: string } }) => ({
- group: value.name === 'error' ? 'Group error' : undefined,
- fields: {
- // Must use the name of the field relative to the FormGroup as the error key,
- // to stay consistent with how standard schema works with form groups
- name: value.name === 'error' ? 'Field error' : undefined,
- },
- })
-}
-```
-
-- And can even accept standard schemas:
-
-```angular-ts
-import { z } from 'zod'
-
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackFormGroup],
- template: `
-
- `,
-})
-export class AppComponent {
- step1Schema = z.object({
- name: z.string().min(2),
- })
-
- form = injectForm({
- defaultValues: {
- step1: { name: '' },
- },
- })
-}
-```
-
-> The reason we don't use the full path names for fields is so that you can compose your schemas like so:
->
-> ```ts
-> const step1Schema = z.object({
-> name: z.string().min(2),
-> })
->
-> const schema = z.object({
-> step1: step1Schema,
-> step2: step2Schema,
-> })
-> ```
->
-> And pass the `step1Schema` to a form group and `schema` to the parent form. That way, partially validated data will still flag errors if the group is bypassed.
-
-### Dynamic Group Validation
-
-If you want to use [dynamic validation (`onDynamic`)](./dynamic-validation.md) with a form group, do not rely on the `onDynamic` validator passed to `injectForm`:
-
-```ts
-form = injectForm({
- validationLogic: revalidateLogic(),
- validators: {
- // This validator will not run `onChange` when a sub-form is submitted;
- // it will only run `onChange` when the form itself is submitted.
- onDynamic: schema,
- },
-})
-```
-
-Instead, pass your sub-schema for the group to the `onDynamic` validation of the group itself:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackFormGroup],
- template: `
-
- `,
-})
-export class AppComponent {
- step1Schema = step1Schema
-
- form = injectForm({
- defaultValues: {
- step1: { name: '' },
- },
- })
-}
-```
-
-It will treat `group.api.submissionAttempts` as the way to change what validator is run before/after submit.
-
-## Form Group State
-
-Just like you're able to access `group.api.state.meta.errors`, you're also able to access the group's value using `group.api.state.value`. Likewise, here are some valuable properties you can access in the `group.api.state.meta`:
-
-- `group.api.state.meta.isFieldsValid`: `true` when the field-level validators have no errors
-- `group.api.state.meta.isGroupValid`: `true` when the group-level validators have no errors
-- `group.api.state.meta.isValid`: `true` when both the field-level and group-level validators have no errors
-- `group.api.state.meta.isSubmitting`: `true` when the group is in the process of being submitted
diff --git a/docs/framework/angular/guides/listeners.md b/docs/framework/angular/guides/listeners.md
deleted file mode 100644
index 8da6e75017..0000000000
--- a/docs/framework/angular/guides/listeners.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-id: listeners
-title: Side effects for event triggers
----
-
-For situations where you want to "affect" or "react" to triggers, there's the listener API. For example, if you, as the developer, want to reset a form field as a result of another field changing, you would use the listener API.
-
-Imagine the following user flow:
-
-- User selects a country from a drop-down.
-- User then selects a province from another drop-down.
-- User changes the selected country to a different one.
-
-In this example, when the user changes the country, the selected province needs to be reset as it's no longer valid. With the listener API, we can subscribe to the onChange event and dispatch a reset to the field "province" when the listener is fired.
-
-Events that can be "listened" to are:
-
-- `onChange`
-- `onBlur`
-- `onMount`
-- `onSubmit`
-- `onUnmount`
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
-
- `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- country: '',
- province: '',
- },
- })
-
- onCountryChange: FieldListenerFn = ({
- value,
- }) => {
- console.log(`Country changed to: ${value}, resetting province`)
- this.form.setFieldValue('province', '')
- }
-}
-```
diff --git a/docs/framework/angular/guides/submission-handling.md b/docs/framework/angular/guides/submission-handling.md
deleted file mode 100644
index b441ca7a57..0000000000
--- a/docs/framework/angular/guides/submission-handling.md
+++ /dev/null
@@ -1,106 +0,0 @@
----
-id: submission-handling
-title: Submission handling
----
-
-## Passing additional data to submission handling
-
-You may have multiple types of submission behaviour, for example, going back to another page or staying on the form.
-You can accomplish this by specifying the `onSubmitMeta` property. This meta data will be passed to the `onSubmit` function.
-
-> Note: if `form.handleSubmit()` is called without metadata, it will use the provided default.
-
-```angular-ts
-import { Component } from '@angular/core'
-import { injectForm } from '@tanstack/angular-form'
-
-type FormMeta = {
- submitAction: 'continue' | 'backToMenu' | null
-}
-
-// Metadata is not required to call form.handleSubmit().
-// Specify what values to use as default if no meta is passed
-const defaultMeta: FormMeta = {
- submitAction: null,
-}
-
-@Component({
- selector: 'app-root',
- template: `
-
- `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- data: '',
- },
- // Define what meta values to expect on submission
- onSubmitMeta: defaultMeta,
- onSubmit: async ({ value, meta }) => {
- // Do something with the values passed via handleSubmit
- console.log(`Selected action - ${meta.submitAction}`, value)
- },
- })
-
- handleSubmit(event: SubmitEvent) {
- event.preventDefault()
- event.stopPropagation()
- }
-
- handleClick(meta: FormMeta) {
- // Overwrites the default specified in onSubmitMeta
- this.form.handleSubmit(meta)
- }
-}
-```
-
-## Transforming data with Standard Schemas
-
-While Tanstack Form provides [Standard Schema support](./validation.md) for validation, it does not preserve the Schema's output data.
-
-The value passed to the `onSubmit` function will always be the input data. To receive the output data of a Standard Schema, parse it in the `onSubmit` function:
-
-```tsx
-import { z } from 'zod'
-// ...
-
-const schema = z.object({
- age: z.string().transform((age) => Number(age)),
-})
-
-// Tanstack Form uses the input type of Standard Schemas
-const defaultValues: z.input = {
- age: '13',
-}
-
-// ...
-
-@Component({
- // ...
-})
-export class AppComponent {
- form = injectForm({
- defaultValues,
- validators: {
- onChange: schema,
- },
- onSubmit: ({ value }) => {
- const inputAge: string = value.age
- // Pass it through the schema to get the transformed value
- const result = schema.parse(value)
- const outputAge: number = result.age
- },
- })
-}
-```
diff --git a/docs/framework/angular/guides/ui-libraries.md b/docs/framework/angular/guides/ui-libraries.md
new file mode 100644
index 0000000000..62d3d472ce
--- /dev/null
+++ b/docs/framework/angular/guides/ui-libraries.md
@@ -0,0 +1,131 @@
+---
+id: ui-libraries
+title: UI Libraries
+---
+
+TanStack Form is headless. UI library integration consists of adapting a
+field's value, blur callback, change callback, invalid state, and errors to the
+component's inputs and outputs.
+
+## Text inputs
+
+Wrap a UI component with `TanStackField` and pass its emitted value to
+`field.api.handleChange`:
+
+```html
+
+
+
+```
+
+```ts
+errorMessage = (error: { message: string }) => error.message
+```
+
+Pass the current `value`, not a one-time default, so the UI component remains
+driven by form state.
+
+## Checkboxes and switches
+
+Adapt the output used by the component library:
+
+```html
+
+
+
+```
+
+Some libraries emit a details object or a union such as
+`boolean | 'indeterminate'`. Normalize it at the boundary:
+
+```html
+
+```
+
+## Selects, date pickers, and custom controls
+
+The integration does not require a native event. Pass the value emitted by the
+component to `field.api.handleChange`:
+
+```html
+
+```
+
+Choose a serializable form value appropriate for your application, then adapt
+the UI library's richer object at the component boundary if necessary.
+
+## Reuse the adapter
+
+When the same wiring appears throughout the application, create an Angular
+field component that calls `injectField`:
+
+```ts
+import { ChangeDetectionStrategy, Component, input } from '@angular/core'
+import { injectField } from '@tanstack/angular-form'
+
+@Component({
+ selector: 'app-text-field',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ `,
+})
+export class TextFieldComponent {
+ label = input.required()
+ field = injectField()
+}
+```
+
+Apply `tanstack-app-field` alongside the field directive so the component can
+inject its field API:
+
+```html
+
+```
+
+Import `TanStackAppField`, `TanStackField`, and the reusable field component in
+the parent.
+
+For the helper types used when passing a form or field to an extracted
+component, see [Splitting forms](../../../splitting-forms).
+
+The repository's `examples/angular/basic-splitting-form` and
+`examples/angular/large-form` examples demonstrate this pattern with typed
+child components.
diff --git a/docs/framework/angular/guides/validation.md b/docs/framework/angular/guides/validation.md
index 11de82dbe6..60a81a4957 100644
--- a/docs/framework/angular/guides/validation.md
+++ b/docs/framework/angular/guides/validation.md
@@ -3,694 +3,315 @@ id: form-validation
title: Form and Field Validation
---
-At the core of TanStack Form's functionalities is the concept of validation. TanStack Form makes validation highly customizable:
-
-- You can control when to perform the validation (on change, on input, on blur, on submit...)
-- Validation rules can be defined at the field level or at the form level
-- Validation can be synchronous or asynchronous (for example, as a result of an API call)
-
-## When is validation performed?
-
-It's up to you! The `[tanstackField]` directive accepts some callbacks as props such as `onChange` or `onBlur`. Those callbacks are passed the current value of the field, as well as the fieldAPI object, so that you can perform the validation. If you find a validation error, simply return the error message as string and it will be available in `field.api.state.meta.errors`.
-
-Here is an example:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
-
- @if (age.api.state.meta.errors) {
- {{ age.api.state.meta.errors.join(', ') }}
- }
-
- `,
-})
-export class AppComponent {
- ageValidator: FieldValidateFn = ({ value }) =>
- value < 13 ? 'You must be 13 to make an account' : undefined
+TanStack Form supports synchronous, asynchronous, field-level, form-level, and
+Standard Schema validation. Every validator is an ordered object with a `run`
+implementation and explicit `triggers`.
+
+## Choose when validation runs
+
+Use `change` for immediate feedback and `blur` for feedback after leaving an
+input. Submission runs validators by default regardless of their configured
+change and blur triggers.
+
+```ts
+ageValidators = [
+ {
+ triggers: ['change'] as const,
+ run: ({ value }: { value: number }) =>
+ value >= 13 ? undefined : 'You must be at least 13',
+ },
+ {
+ triggers: ['blur'] as const,
+ run: ({ value }: { value: number }) =>
+ value >= 0 ? undefined : 'Age cannot be negative',
+ },
+]
+```
- // ...
-}
+```html
+
+
+
```
-In the example above, the validation is done at each keystroke (`onChange`). If, instead, we wanted the validation to be done when the field is blurred, we would change the code above like so:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
-
-
-
- @if (age.api.state.meta.errors) {
- {{ age.api.state.meta.errors.join(', ') }}
- }
-
- `,
-})
-export class AppComponent {
- ageValidator: FieldValidateFn = ({ value }) =>
- value < 13 ? 'You must be 13 to make an account' : undefined
+Additional controls include:
- // ...
-}
-```
+- `runOnMount`: run once when the form is constructed or when a field or form
+ group first mounts.
+- `skipOnSubmit`: conditionally skip submit-time execution.
+- `triggerDebounceMs`: debounce change and blur execution.
+- `bailIfInvalid`: skip this and subsequent validators when an earlier one
+ failed.
+- trigger objects with `when`: enable a trigger conditionally.
+- `watchFields`: rerun a field validator when related fields trigger it.
-So you can control when the validation is done by implementing the desired callback. You can even perform different pieces of validation at different times:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
-
-
-
- @if (!age.api.state.meta.isValid) {
- {{ age.api.state.meta.errors.join(', ') }}
- }
-
- `,
-})
-export class AppComponent {
- ageValidator: FieldValidateFn = ({ value }) =>
- value < 13 ? 'You must be 13 to make an account' : undefined
+## Control when errors are visible
- minimumAgeValidator: FieldValidateFn = ({
- value,
- }) => (value < 0 ? 'Invalid value' : undefined)
+Validation and presentation are separate. `errorVisibility` decides when
+field issues appear in `field.api.errors`:
- // ...
-}
-```
+```ts
+import { injectForm } from '@tanstack/angular-form'
-In the example above, we are validating different things on the same field at different times (at each keystroke and when blurring the field). Since `field.state.meta.errors` is an array, all the relevant errors at a given time are displayed. You can also use `field.state.meta.errorMap` to get errors based on _when_ the validation was done (onChange, onBlur etc...). More info about displaying errors below.
-
-## Displaying Errors
-
-Once you have your validation in place, you can map the errors from an array to be displayed in your UI:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
- @if (age.api.state.meta.errors) {
- {{ age.api.state.meta.errors.join(', ') }}
- }
-
- `,
+form = injectForm({
+ defaultValues: { email: '' },
+ errorVisibility: ({ fieldState, state }) =>
+ fieldState.meta.isBlurred || state.submissionAttempts > 0,
})
-export class AppComponent {
- ageValidator: FieldValidateFn = ({ value }) =>
- value < 13 ? 'You must be 13 to make an account' : undefined
-
- // ...
-}
```
-Or use the `errorMap` property to access the specific error you're looking for:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
- @if (age.api.state.meta.errorMap['onChange']) {
- {{ age.api.state.meta.errorMap['onChange'] }}
- }
-
- `,
-})
-export class AppComponent {
- ageValidator: FieldValidateFn = ({ value }) =>
- value < 13 ? 'You must be 13 to make an account' : undefined
+Use `field.api.meta.original.errors` for the unfiltered issues when debugging
+or building a custom visibility system.
- // ...
-}
-```
+## Form-level validation
-It's worth mentioning that our `errors` array and the `errorMap` matches the types returned by the validators. This means that:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
-
-
- @if (!age.api.state.meta.errorMap['onChange']?.isOldEnough) {
- The user is not old enough
- }
-
- `,
-})
-export class AppComponent {
- ageValidator: FieldValidateFn = ({ value }) =>
- value < 13 ? 'You must be 13 to make an account' : undefined
+A form validator can produce a form issue and route issues to typed field paths
+with `createErrorMap`:
- // ...
-}
-```
+```ts
+import { injectForm, injectSelector } from '@tanstack/angular-form'
-## Validation at field level vs at form level
-
-As shown above, each `[tanstackField]` accepts its own validation rules via the `onChange`, `onBlur` etc... callbacks. It is also possible to define validation rules at the form level (as opposed to field by field) by passing similar callbacks to the `injectForm()` function.
-
-Example:
-
-```angular-ts
-@Component({
- selector: 'app-root',
- standalone: true,
- imports: [TanStackField],
- template: `
-
-
-
- @if (formErrorMap().onChange) {
-
- There was an error on the form: {{ formErrorMap().onChange }}
-
- }
-
-
-
- `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- age: 0,
- },
- onSubmit({ value }) {
- console.log(value)
- },
- validators: {
- // Add validators to the form the same way you would add them to a field
- onChange({ value }) {
- if (value.age < 13) {
- return 'Must be 13 or older to sign'
+form = injectForm({
+ defaultValues: {
+ email: '',
+ phone: '',
+ },
+ validators: [
+ {
+ triggers: ['change'],
+ run: ({ value, createErrorMap }) => {
+ const errors = createErrorMap()
+
+ if (!value.email && !value.phone) {
+ errors.form = 'Provide an email address or phone number'
+ errors.fields.email = 'Email is required when phone is empty'
+ errors.fields.phone = 'Phone is required when email is empty'
}
- return undefined
+
+ return errors
},
},
- })
+ ],
+})
- // Subscribe to the form's error map so that updates to it will render
- formErrorMap = injectStore(this.form, (state) => state.errorMap)
-}
+formErrors = injectSelector(this.form, (state) => state.errors)
```
-### Setting field-level errors from the form's validators
-
-You can set errors on the fields from the form's validators. One common use case for this is validating all the fields on submit by calling a single API endpoint in the form's `onSubmitAsync` validator.
-
-```angular-ts
-@Component({
- selector: 'app-root',
- imports: [TanStackField],
- template: `
-
- `,
-})
-export class AppComponent {
- form = injectForm({
- defaultValues: {
- age: 0,
- socials: [],
- details: {
- email: '',
- },
+`injectSelector` exposes form-level issues as an Angular signal. Lower-level
+integrations can subscribe to `form.atom` directly. Field-routed issues appear
+in the corresponding `field.api.errors`.
+
+## Asynchronous validation and debouncing
+
+`run` may return a promise. Put cheap synchronous checks first and use
+`bailIfInvalid` to avoid unnecessary requests:
+
+```ts
+usernameValidators = [
+ {
+ triggers: ['change'] as const,
+ run: ({ value }: { value: string }) =>
+ value.length >= 3 ? undefined : 'Use at least 3 characters',
+ },
+ {
+ triggers: ['change'] as const,
+ triggerDebounceMs: 500,
+ bailIfInvalid: true,
+ run: async ({ value }: { value: string }) => {
+ const available = await checkUsername(value)
+ return available ? undefined : 'That username is already taken'
},
- validators: {
- onSubmitAsync: async ({ value }) => {
- // Validate the value on the server
- const hasErrors = await verifyDataOnServer(value)
- if (hasErrors) {
- return {
- form: 'Invalid data', // The `form` key is optional
- fields: {
- age: 'Must be 13 or older to sign',
- // Set errors on nested fields with the field's name
- 'socials[0].url': 'The provided URL does not exist',
- 'details.email': 'An email is required',
- },
- }
- }
+ },
+]
+```
- return null
- },
- },
- })
+Pass this array to the field with `[validators]="usernameValidators"`.
+Submit validation always runs immediately, even when a trigger is debounced.
+`field.api.meta.isValidating` and `form.state.isValidating` expose pending
+work.
- handleSubmit(event: SubmitEvent) {
- event.preventDefault()
- event.stopPropagation()
- this.form.handleSubmit()
- }
-}
-```
+## Standard Schema validation
-> Something worth mentioning is that if you have a form validation function that returns an error, that error may be overwritten by the field-specific validation.
->
-> This means that:
->
-> ```angular-ts
-> @Component({
-> selector: 'app-root',
-> standalone: true,
-> imports: [TanStackField],
-> template: `
->