diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 000000000..67493148e --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": [ + "@svitejs/changesets-changelog-github.amrom.workers.devpact", + { "repo": "TanStack/form" } + ], + "commit": false, + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "fixed": [], + "linked": [], + "ignore": [] +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 201938c3f..8559873e8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,4 +1,4 @@ -name: '🐛 Bug report' +name: 🐛 Bug Report description: Report a reproducible bug or regression body: - type: markdown @@ -124,7 +124,7 @@ body: description: | If you are using TypeScript, please let us know the exact version of TypeScript you were using when the issue occurred. placeholder: | - e.g. v4.5.4 + e.g. v5.8.3 - type: textarea id: additional attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a3b3aae94..204102040 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,11 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - - name: Feature Requests & Questions + - name: 🤔 Feature Requests & Questions url: https://github.com/TanStack/form/discussions about: Please ask and answer questions here. - - name: Community Chat - url: https://discord.com/invite/WrRKjPJ - about: A dedicated discord server hosted by Tanner Linsley + - name: 💬 Community Chat + url: https://discord.gg/mQd7egN + about: A dedicated discord server hosted by TanStack + - name: 🦋 TanStack Bluesky + url: https://bsky.app/profile/tanstack.com + about: Stay up to date with new releases of our libraries diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..aac7579c4 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## 🎯 Changes + + + +## ✅ 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`. + +## 🚀 Release Impact + +- [ ] This change affects published code, and I have generated a [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). +- [ ] This change is docs/CI/dev-only (no release). diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 1e4919a07..7d710ce0d 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Setup Tools uses: tanstack/config/.github/setup@main - name: Fix formatting @@ -35,6 +35,6 @@ jobs: if: ${{ github.event_name == 'push' || github.event.inputs.generate-docs == true }} run: pnpm docs:generate - name: Apply fixes - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef + uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 with: commit-message: 'ci: apply automated fixes and generate docs' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index d7677ce90..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: ci - -on: - workflow_dispatch: - inputs: - tag: - description: override release tag - required: false - push: - branches: [main, alpha, beta] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.number || github.ref }} - cancel-in-progress: true - -env: - NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - -permissions: - contents: write - id-token: write - -jobs: - test-and-publish: - name: Test & Publish - if: github.repository == 'TanStack/form' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - 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@main - - name: Run Tests - run: pnpm run test:ci --parallel=3 - - name: Stop Nx Agents - if: ${{ always() }} - run: npx nx-cloud stop-all-agents - - name: Publish - run: | - git config --global user.name 'Tanner Linsley' - git config --global user.email 'tannerlinsley@users.noreply.github.com' - npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}" - pnpm run cipublish - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - TAG: ${{ inputs.tag }} - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - directory: packages - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d59e223cf..c3858fce3 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,4 +1,4 @@ -name: pr +name: PR on: pull_request: @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5.0.0 with: fetch-depth: 0 - name: Start Nx Agents @@ -31,7 +31,7 @@ jobs: - name: Setup Tools uses: tanstack/config/.github/setup@main - name: Get base and head commits for `nx affected` - uses: nrwl/nx-set-shas@v4 + uses: nrwl/nx-set-shas@v4.3.3 with: main-branch-name: main - name: Run Checks @@ -50,7 +50,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5.0.0 with: fetch-depth: 0 - name: Setup Tools @@ -59,3 +59,15 @@ jobs: run: pnpm run build:all - name: Publish Previews run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*' + provenance: + name: Provenance + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5.0.0 + with: + fetch-depth: 0 + - name: Check Provenance + uses: danielroe/provenance-action@v0.1.1 + with: + fail-on-downgrade: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..0858f39cd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: Release + +on: + push: + branches: [main, alpha, beta, rc] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + +permissions: + contents: write + id-token: write + pull-requests: write + +jobs: + release: + name: Release + if: github.repository_owner == 'TanStack' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5.0.0 + with: + fetch-depth: 0 + - name: Setup Tools + uses: tanstack/config/.github/setup@main + - name: Run Tests + run: pnpm run test:ci + - name: Run Changesets (version or publish) + uses: changesets/action@v1.5.3 + with: + version: pnpm run changeset:version + publish: pnpm run changeset:publish + commit: 'ci: Version Packages' + title: 'ci: Version Packages' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.npmrc b/.npmrc index 84aee8d99..268c392d3 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1 @@ -link-workspace-packages=true -prefer-workspace-packages=true provenance=true diff --git a/.nvmrc b/.nvmrc index 1d9b7831b..b40402760 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.12.0 +24.8.0 diff --git a/README.md b/README.md index 8dc580b4b..dcfb1e770 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,107 @@ - - -![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png) - -Powerful and type-safe form state management for the web. TS/JS, React Form, Solid Form, Angular Form, Lit Form and Vue Form. - - - #TanStack - - - - - - - - semantic-release - - Join the discussion on Github -Best of JS - - - - - Gitpod Ready-to-Code - +
+ +
-Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Query](https://github.com/TanStack/query), [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger) +
-## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more! +
+ + NPM downloads for @tanstack/form-core + + + Star TanStack Form on GitHub + + + Minified + gzipped bundle size of @tanstack/form-core + +
+
+ + Semantic Release Enabled + + + TanStack Form featured on Best of JS + + + Follow @TanStack + +
+ +
+ ### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/) +
+ +# TanStack Form + +A headless form library for managing complex form state with full control over fields, validation, and workflows across any framework. + +- Framework‑agnostic & headless — bring your own UI +- Fully typed with TypeScript +- Reactive hooks & extensible modular architecture +- Sync & async validation with debouncing and nested fields + +### Read the docs → + +
+ +## Get Involved + +- We welcome issues and pull requests! +- Participate in [GitHub discussions](https://github.com/TanStack/form/discussions) +- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ) +- See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions + +## Partners + + + + + + +
+ + + + + CodeRabbit + + + + + + + + Cloudflare + + +
+ +
+Form & you? +

+We're looking for TanStack Form Partners to join our mission! Partner with us to push the boundaries of TanStack Form and build amazing things together. +

+LET'S CHAT +
+ + + +## Explore the TanStack Ecosystem + +- TanStack Config – Tooling for JS/TS packages +- TanStack DB – Reactive sync client store +- TanStack DevTools – Unified devtools panel +- TanStack Pacer – Debouncing, throttling, batching
+- TanStack Query – Async state & caching +- TanStack Ranger – Range & slider primitives +- TanStack Router – Type‑safe routing, caching & URL state +- TanStack Start – Full‑stack SSR & streaming +- TanStack Store – Reactive data store +- TanStack Table – Headless datagrids +- TanStack Virtual – Virtualized rendering + +… and more at TanStack.com » - + diff --git a/codecov.yml b/codecov.yml index 6cc665ac5..7700cbe30 100755 --- a/codecov.yml +++ b/codecov.yml @@ -6,3 +6,6 @@ coverage: default: target: auto threshold: 1% +ignore: + - 'packages/form-devtools' + - 'packages/react-form-devtools' diff --git a/docs/assets/field-states-extended.png b/docs/assets/field-states-extended.png index 033fe6e68..1ecc677af 100644 Binary files a/docs/assets/field-states-extended.png and b/docs/assets/field-states-extended.png differ diff --git a/docs/assets/field-states.png b/docs/assets/field-states.png index a78ed16f9..869f41f5c 100644 Binary files a/docs/assets/field-states.png and b/docs/assets/field-states.png differ diff --git a/docs/assets/react_form_composability.svg b/docs/assets/react_form_composability.svg index fd2f7ae14..9761825c5 100644 --- a/docs/assets/react_form_composability.svg +++ b/docs/assets/react_form_composability.svg @@ -1,3 +1,3 @@ -

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

Do you need to reuse state (like defaultValues)?

Use 'formOptions()

Do you need to custom validation functions?

Wrap 'useForm' hook into custom app hook

Do you need to reuse custom UI components?

Do you need access to the 'field'?

Do you need to reuse whole subsections of your form?

Use 'createFormHooks''s 'fieldComponents' (EG: 'TextInput' and 'NumberInput')

Use 'createFormsHook''s 'formComponents' (EG: 'SubmitButton')

Use 'withForm' from 'createFromHook'

Use 'form.Subscribe' and 'form.Field'

\ No newline at end of file +

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

Do you need to reuse state (like defaultValues)?

Use 'formOptions()

Do you need to custom validation functions?

Wrap 'useForm' hook into custom app hook

Do you need to reuse custom UI components?

Do you need access to the 'field'?

Do you need to reuse whole subsections of your form?

Use 'createFormHooks''s 'fieldComponents' (EG: 'TextInput' and 'NumberInput')

Use 'createFormsHook''s 'formComponents' (EG: 'SubmitButton')

Use 'withForm' from 'createFormHook'

Use 'form.Subscribe' and 'form.Field'

diff --git a/docs/config.json b/docs/config.json index 795815914..42228fec6 100644 --- a/docs/config.json +++ b/docs/config.json @@ -102,6 +102,10 @@ "label": "Form Validation", "to": "framework/react/guides/validation" }, + { + "label": "Dynamic Validation", + "to": "framework/react/guides/dynamic-validation" + }, { "label": "Async Initial Values", "to": "framework/react/guides/async-initial-values" @@ -134,6 +138,10 @@ "label": "UI Libraries", "to": "framework/react/guides/ui-libraries" }, + { + "label": "Focus Management", + "to": "framework/react/guides/focus-management" + }, { "label": "Form Composition", "to": "framework/react/guides/form-composition" @@ -149,6 +157,10 @@ { "label": "Debugging", "to": "framework/react/guides/debugging" + }, + { + "label": "Devtools", + "to": "framework/react/guides/devtools" } ] }, @@ -163,6 +175,10 @@ "label": "Form Validation", "to": "framework/vue/guides/validation" }, + { + "label": "Dynamic Validation", + "to": "framework/vue/guides/dynamic-validation" + }, { "label": "Async Initial Values", "to": "framework/vue/guides/async-initial-values" @@ -188,9 +204,17 @@ "label": "Form Validation", "to": "framework/angular/guides/validation" }, + { + "label": "Dynamic Validation", + "to": "framework/angular/guides/dynamic-validation" + }, { "label": "Arrays", "to": "framework/angular/guides/arrays" + }, + { + "label": "Form Composition", + "to": "framework/angular/guides/form-composition" } ] }, @@ -205,6 +229,10 @@ "label": "Form Validation", "to": "framework/solid/guides/validation" }, + { + "label": "Dynamic Validation", + "to": "framework/solid/guides/dynamic-validation" + }, { "label": "Async Initial Values", "to": "framework/solid/guides/async-initial-values" @@ -216,6 +244,10 @@ { "label": "Linked Fields", "to": "framework/solid/guides/linked-fields" + }, + { + "label": "Form Composition", + "to": "framework/solid/guides/form-composition" } ] }, @@ -226,6 +258,14 @@ "label": "Basic Concepts", "to": "framework/lit/guides/basic-concepts" }, + { + "label": "Form Validation", + "to": "framework/lit/guides/validation" + }, + { + "label": "Dynamic Validation", + "to": "framework/lit/guides/dynamic-validation" + }, { "label": "Arrays", "to": "framework/lit/guides/arrays" @@ -243,6 +283,10 @@ "label": "Form Validation", "to": "framework/svelte/guides/validation" }, + { + "label": "Dynamic Validation", + "to": "framework/svelte/guides/dynamic-validation" + }, { "label": "Async Initial Values", "to": "framework/svelte/guides/async-initial-values" @@ -515,6 +559,10 @@ "label": "Form Composition", "to": "framework/react/examples/large-form" }, + { + "label": "Dynamic Validation", + "to": "framework/react/examples/dynamic" + }, { "label": "TanStack Query Integration", "to": "framework/react/examples/query-integration" @@ -542,6 +590,10 @@ { "label": "Field Errors From Form Validators", "to": "framework/react/examples/field-errors-from-form-validators" + }, + { + "label": "Devtools", + "to": "framework/react/examples/devtools" } ] }, @@ -555,6 +607,10 @@ { "label": "Arrays", "to": "framework/vue/examples/array" + }, + { + "label": "Standard Schema", + "to": "framework/vue/examples/standard-schema" } ] }, @@ -568,6 +624,14 @@ { "label": "Arrays", "to": "framework/angular/examples/array" + }, + { + "label": "Form Composition", + "to": "framework/angular/examples/large-form" + }, + { + "label": "Standard Schema", + "to": "framework/angular/examples/standard-schema" } ] }, @@ -581,6 +645,14 @@ { "label": "Arrays", "to": "framework/solid/examples/array" + }, + { + "label": "Form Composition", + "to": "framework/solid/examples/large-form" + }, + { + "label": "Standard Schema", + "to": "framework/solid/examples/standard-schema" } ] }, @@ -591,9 +663,17 @@ "label": "Simple", "to": "framework/lit/examples/simple" }, + { + "label": "Array", + "to": "framework/lit/examples/array" + }, { "label": "UI Libraries", "to": "framework/lit/examples/ui-libraries" + }, + { + "label": "Standard Schema", + "to": "framework/lit/examples/standard-schema" } ] }, @@ -607,6 +687,10 @@ { "label": "Arrays", "to": "framework/svelte/examples/array" + }, + { + "label": "Standard Schema", + "to": "framework/svelte/examples/standard-schema" } ] } diff --git a/docs/framework/angular/guides/basic-concepts.md b/docs/framework/angular/guides/basic-concepts.md index 551d34b12..8527075e5 100644 --- a/docs/framework/angular/guides/basic-concepts.md +++ b/docs/framework/angular/guides/basic-concepts.md @@ -40,21 +40,51 @@ Each field has its own state, which includes its current value, validation statu Example: -```tsx +```ts const { value, meta: { errors, isValidating }, } = field.state ``` -There are three field states can be very useful to see how the user interacts with a field. A field is _"touched"_ when the user clicks/tabs into it, _"pristine"_ until the user changes value in it, and _"dirty"_ after the value has been changed. You can check these states via the `isTouched`, `isPristine` and `isDirty` flags, as seen below. +There are four states in the metadata that can be useful to see how the user interacts with a field: -```tsx -const { isTouched, isPristine, isDirty } = field.state.meta +- _"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 ``` ![Field states](https://raw.githubusercontent.com/TanStack/form/main/docs/assets/field-states.png) +## 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 states extended](https://raw.githubusercontent.com/TanStack/form/main/docs/assets/field-states-extended.png) + ## 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. @@ -125,9 +155,10 @@ You can define a schema using any of the libraries implementing the specificatio Supported libraries include: -- [Zod](https://zod.dev/) -- [Valibot](https://valibot.dev/) -- [ArkType](https://arktype.io/) +- [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: @@ -228,7 +259,7 @@ onCountryChange: FieldListenerFn = ({ } ``` -More information can be found at [Listeners](./listeners.md) +More information can be found at [Listeners](../listeners.md) ## Array Fields diff --git a/docs/framework/angular/guides/dynamic-validation.md b/docs/framework/angular/guides/dynamic-validation.md new file mode 100644 index 000000000..0ecff7434 --- /dev/null +++ b/docs/framework/angular/guides/dynamic-validation.md @@ -0,0 +1,297 @@ +--- +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: ` +
+ + + @if (age.api.state.meta.errorMap.onDynamic) { +

+ {{ age.api.state.meta.errorMap.onDynamic }} +

+ } +
+ +
+ `, +}) +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 new file mode 100644 index 000000000..9bb32c7dd --- /dev/null +++ b/docs/framework/angular/guides/form-composition.md @@ -0,0 +1,178 @@ +--- +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: ` +
+ + + + @if (firstName.api.state.meta.isTouched) { + @for (error of firstName.api.state.meta.errors; track $index) { +
+ {{ error }} +
+ } + } + @if (firstName.api.state.meta.isValidating) { +

Validating...

+ } +
+
+
+ + + + @if (lastName.api.state.meta.isTouched) { + @for (error of lastName.api.state.meta.errors; track $index) { +
+ {{ error }} +
+ } + } + @if (lastName.api.state.meta.isValidating) { +

Validating...

+ } +
+
+ `, +}) +export class AppComponent { + form = injectForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + onSubmit({ value }) { + // Do something with form data + console.log(value) + }, + }) +} +``` + +This is functionally correct, but introduces a _lot_ of repeated templating behavior over and over. Instead, let's move the error handling, label to input binding, and other repeated logic into a component: + +```angular-ts +import {injectField} from '@tanstack/angular-form' + +@Component({ + selector: 'app-text-field', + standalone: true, + template: ` + + + @if (field.api.state.meta.isTouched) { + @for (error of field.api.state.meta.errors; track $index) { +
+ {{ error }} +
+ } + } + @if (field.api.state.meta.isValidating) { +

Validating...

+ } + `, +}) +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. diff --git a/docs/framework/angular/guides/submission-handling.md b/docs/framework/angular/guides/submission-handling.md index e5fae2477..4f503f0cd 100644 --- a/docs/framework/angular/guides/submission-handling.md +++ b/docs/framework/angular/guides/submission-handling.md @@ -65,7 +65,7 @@ export class AppComponent { ## 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. +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: diff --git a/docs/framework/angular/guides/validation.md b/docs/framework/angular/guides/validation.md index 60e3a2dc8..85f384105 100644 --- a/docs/framework/angular/guides/validation.md +++ b/docs/framework/angular/guides/validation.md @@ -554,7 +554,7 @@ TanStack Form natively supports all libraries following the [Standard Schema spe _Note:_ make sure to use the latest version of the schema libraries as older versions might not support Standard Schema yet. -> Validation will not provide you with transformed values. See [submission handling](./submission-handling.md) for more information. +> Validation will not provide you with transformed values. See [submission handling](../submission-handling.md) for more information. To use schemas from these libraries you can pass them to the `validators` props as you would do with a custom function: diff --git a/docs/framework/angular/reference/classes/tanstackappfield.md b/docs/framework/angular/reference/classes/tanstackappfield.md new file mode 100644 index 000000000..c91b07bf7 --- /dev/null +++ b/docs/framework/angular/reference/classes/tanstackappfield.md @@ -0,0 +1,336 @@ +--- +id: TanStackAppField +title: TanStackAppField +--- + + + +# Class: TanStackAppField\ + +Defined in: [app-field.ts:20](https://github.com/TanStack/form/blob/main/packages/angular-form/src/app-field.ts#L20) + +## Extends + +- [`TanStackField`](../tanstackfield.md)\<`TParentData`, `TName`, `TData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnDynamic`, `TFormOnDynamicAsync`, `TFormOnServer`, `TSubmitMeta`\> + +## Type Parameters + +• **TParentData** + +• **TName** *extends* `DeepKeys`\<`TParentData`\> + +• **TData** *extends* `DeepValue`\<`TParentData`, `TName`\> + +• **TOnMount** *extends* `undefined` \| `FieldValidateOrFn`\<`TParentData`, `TName`, `TData`\> + +• **TOnChange** *extends* `undefined` \| `FieldValidateOrFn`\<`TParentData`, `TName`, `TData`\> + +• **TOnChangeAsync** *extends* `undefined` \| `FieldAsyncValidateOrFn`\<`TParentData`, `TName`, `TData`\> + +• **TOnBlur** *extends* `undefined` \| `FieldValidateOrFn`\<`TParentData`, `TName`, `TData`\> + +• **TOnBlurAsync** *extends* `undefined` \| `FieldAsyncValidateOrFn`\<`TParentData`, `TName`, `TData`\> + +• **TOnSubmit** *extends* `undefined` \| `FieldValidateOrFn`\<`TParentData`, `TName`, `TData`\> + +• **TOnSubmitAsync** *extends* `undefined` \| `FieldAsyncValidateOrFn`\<`TParentData`, `TName`, `TData`\> + +• **TOnDynamic** *extends* `undefined` \| `FieldValidateOrFn`\<`TParentData`, `TName`, `TData`\> + +• **TOnDynamicAsync** *extends* `undefined` \| `FieldAsyncValidateOrFn`\<`TParentData`, `TName`, `TData`\> + +• **TFormOnMount** *extends* `undefined` \| `FormValidateOrFn`\<`TParentData`\> + +• **TFormOnChange** *extends* `undefined` \| `FormValidateOrFn`\<`TParentData`\> + +• **TFormOnChangeAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TParentData`\> + +• **TFormOnBlur** *extends* `undefined` \| `FormValidateOrFn`\<`TParentData`\> + +• **TFormOnBlurAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TParentData`\> + +• **TFormOnSubmit** *extends* `undefined` \| `FormValidateOrFn`\<`TParentData`\> + +• **TFormOnSubmitAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TParentData`\> + +• **TFormOnDynamic** *extends* `undefined` \| `FormValidateOrFn`\<`TParentData`\> + +• **TFormOnDynamicAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TParentData`\> + +• **TFormOnServer** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TParentData`\> + +• **TSubmitMeta** + +## Constructors + +### new TanStackAppField() + +```ts +new TanStackAppField(): TanStackAppField +``` + +Defined in: [app-field.ts:79](https://github.com/TanStack/form/blob/main/packages/angular-form/src/app-field.ts#L79) + +#### Returns + +[`TanStackAppField`](../tanstackappfield.md)\<`TParentData`, `TName`, `TData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnDynamic`, `TFormOnDynamicAsync`, `TFormOnServer`, `TSubmitMeta`\> + +#### Overrides + +[`TanStackField`](../tanstackfield.md).[`constructor`](../TanStackField.md#constructors) + +## Properties + +### \_api + +```ts +_api: Signal>; +``` + +Defined in: [tanstack-field.ts:151](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L151) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`_api`](../TanStackField.md#_api) + +*** + +### asyncAlways + +```ts +asyncAlways: InputSignalWithTransform; +``` + +Defined in: [tanstack-field.ts:76](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L76) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`asyncAlways`](../TanStackField.md#asyncalways) + +*** + +### asyncDebounceMs + +```ts +asyncDebounceMs: InputSignalWithTransform; +``` + +Defined in: [tanstack-field.ts:73](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L73) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`asyncDebounceMs`](../TanStackField.md#asyncdebouncems) + +*** + +### base + +```ts +base: TanStackFieldInjectable; +``` + +Defined in: [app-field.ts:77](https://github.com/TanStack/form/blob/main/packages/angular-form/src/app-field.ts#L77) + +*** + +### cd + +```ts +cd: ChangeDetectorRef; +``` + +Defined in: [tanstack-field.ts:238](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L238) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`cd`](../TanStackField.md#cd) + +*** + +### defaultMeta + +```ts +defaultMeta: InputSignal< + | undefined +| Partial>>; +``` + +Defined in: [tanstack-field.ts:118](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L118) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`defaultMeta`](../TanStackField.md#defaultmeta) + +*** + +### defaultValue + +```ts +defaultValue: InputSignal>; +``` + +Defined in: [tanstack-field.ts:72](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L72) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`defaultValue`](../TanStackField.md#defaultvalue) + +*** + +### disableErrorFlat + +```ts +disableErrorFlat: InputSignal; +``` + +Defined in: [tanstack-field.ts:149](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L149) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`disableErrorFlat`](../TanStackField.md#disableerrorflat) + +*** + +### injector + +```ts +injector: Injector; +``` + +Defined in: [tanstack-field.ts:222](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L222) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`injector`](../TanStackField.md#injector) + +*** + +### listeners + +```ts +listeners: InputSignal< + | undefined +| NoInfer>>; +``` + +Defined in: [tanstack-field.ts:117](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L117) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`listeners`](../TanStackField.md#listeners) + +*** + +### mode + +```ts +mode: InputSignal; +``` + +Defined in: [tanstack-field.ts:147](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L147) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`mode`](../TanStackField.md#mode) + +*** + +### name + +```ts +name: InputSignal; +``` + +Defined in: [tanstack-field.ts:71](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L71) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`name`](../TanStackField.md#name) + +*** + +### options + +```ts +options: Signal>; +``` + +Defined in: [tanstack-field.ts:183](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L183) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`options`](../TanStackField.md#options) + +*** + +### tanstackField + +```ts +tanstackField: InputSignal>; +``` + +Defined in: [tanstack-field.ts:79](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L79) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`tanstackField`](../TanStackField.md#tanstackfield) + +*** + +### validators + +```ts +validators: InputSignal< + | undefined +| NoInfer>>; +``` + +Defined in: [tanstack-field.ts:97](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L97) + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`validators`](../TanStackField.md#validators) + +## Accessors + +### api + +#### Get Signature + +```ts +get api(): FieldApi +``` + +Defined in: [tanstack-field.ts:155](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L155) + +##### Returns + +`FieldApi`\<`TParentData`, `TName`, `TData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnDynamic`, `TFormOnDynamicAsync`, `TFormOnServer`, `TSubmitMeta`\> + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`api`](../TanStackField.md#api) + +## Methods + +### ngOnInit() + +```ts +ngOnInit(): void +``` + +Defined in: [tanstack-field.ts:240](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L240) + +A callback method that is invoked immediately after the +default change detector has checked the directive's +data-bound properties for the first time, +and before any of the view or content children have been checked. +It is invoked only once when the directive is instantiated. + +#### Returns + +`void` + +#### Inherited from + +[`TanStackField`](../tanstackfield.md).[`ngOnInit`](../TanStackField.md#ngoninit) diff --git a/docs/framework/angular/reference/classes/tanstackfield.md b/docs/framework/angular/reference/classes/tanstackfield.md index c15d5b235..46d68e3d5 100644 --- a/docs/framework/angular/reference/classes/tanstackfield.md +++ b/docs/framework/angular/reference/classes/tanstackfield.md @@ -5,9 +5,13 @@ title: TanStackField -# Class: TanStackField\ +# Class: TanStackField\ -Defined in: [tanstack-field.directive.ts:31](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L31) +Defined in: [tanstack-field.ts:37](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L37) + +## Extended by + +- [`TanStackAppField`](../tanstackappfield.md) ## Type Parameters @@ -31,6 +35,10 @@ Defined in: [tanstack-field.directive.ts:31](https://github.com/TanStack/form/bl • **TOnSubmitAsync** *extends* `undefined` \| `FieldAsyncValidateOrFn`\<`TParentData`, `TName`, `TData`\> +• **TOnDynamic** *extends* `undefined` \| `FieldValidateOrFn`\<`TParentData`, `TName`, `TData`\> + +• **TOnDynamicAsync** *extends* `undefined` \| `FieldAsyncValidateOrFn`\<`TParentData`, `TName`, `TData`\> + • **TFormOnMount** *extends* `undefined` \| `FormValidateOrFn`\<`TParentData`\> • **TFormOnChange** *extends* `undefined` \| `FormValidateOrFn`\<`TParentData`\> @@ -45,6 +53,10 @@ Defined in: [tanstack-field.directive.ts:31](https://github.com/TanStack/form/bl • **TFormOnSubmitAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TParentData`\> +• **TFormOnDynamic** *extends* `undefined` \| `FormValidateOrFn`\<`TParentData`\> + +• **TFormOnDynamicAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TParentData`\> + • **TFormOnServer** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TParentData`\> • **TSubmitMeta** @@ -52,249 +64,184 @@ Defined in: [tanstack-field.directive.ts:31](https://github.com/TanStack/form/bl ## Implements - `OnInit` -- `OnChanges` -- `OnDestroy` -- `FieldOptions`\<`TParentData`, `TName`, `TData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`\> ## Constructors ### new TanStackField() ```ts -new TanStackField(): TanStackField +new TanStackField(): TanStackField ``` +Defined in: [tanstack-field.ts:224](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L224) + #### Returns -[`TanStackField`](tanstackfield.md)\<`TParentData`, `TName`, `TData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnServer`, `TSubmitMeta`\> +[`TanStackField`](../tanstackfield.md)\<`TParentData`, `TName`, `TData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnDynamic`, `TFormOnDynamicAsync`, `TFormOnServer`, `TSubmitMeta`\> ## Properties -### api +### \_api ```ts -api: FieldApi; +_api: Signal>; ``` -Defined in: [tanstack-field.directive.ts:129](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L129) +Defined in: [tanstack-field.ts:151](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L151) *** -### asyncAlways? +### asyncAlways ```ts -optional asyncAlways: boolean; +asyncAlways: InputSignalWithTransform; ``` -Defined in: [tanstack-field.directive.ts:78](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L78) - -If `true`, always run async validation, even if there are errors emitted during synchronous validation. - -#### Implementation of - -```ts -FieldOptions.asyncAlways -``` +Defined in: [tanstack-field.ts:76](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L76) *** -### asyncDebounceMs? +### asyncDebounceMs ```ts -optional asyncDebounceMs: number; +asyncDebounceMs: InputSignalWithTransform; ``` -Defined in: [tanstack-field.directive.ts:77](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L77) - -The default time to debounce async validation if there is not a more specific debounce time passed. - -#### Implementation of - -```ts -FieldOptions.asyncDebounceMs -``` +Defined in: [tanstack-field.ts:73](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L73) *** -### defaultMeta? +### cd ```ts -optional defaultMeta: Partial>; +cd: ChangeDetectorRef; ``` -Defined in: [tanstack-field.directive.ts:106](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L106) - -An optional object with default metadata for the field. - -#### Implementation of - -```ts -FieldOptions.defaultMeta -``` +Defined in: [tanstack-field.ts:238](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L238) *** -### defaultValue? +### defaultMeta ```ts -optional defaultValue: NoInfer; +defaultMeta: InputSignal< + | undefined +| Partial>>; ``` -Defined in: [tanstack-field.directive.ts:76](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L76) - -An optional default value for the field. - -#### Implementation of - -```ts -FieldOptions.defaultValue -``` +Defined in: [tanstack-field.ts:118](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L118) *** -### disableErrorFlat? +### defaultValue ```ts -optional disableErrorFlat: boolean; +defaultValue: InputSignal>; ``` -Defined in: [tanstack-field.directive.ts:127](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L127) - -Disable the `flat(1)` operation on `field.errors`. This is useful if you want to keep the error structure as is. Not suggested for most use-cases. - -#### Implementation of - -```ts -FieldOptions.disableErrorFlat -``` +Defined in: [tanstack-field.ts:72](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L72) *** -### listeners? +### disableErrorFlat ```ts -optional listeners: NoInfer>; +disableErrorFlat: InputSignal; ``` -Defined in: [tanstack-field.directive.ts:105](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L105) - -A list of listeners which attach to the corresponding events - -#### Implementation of - -```ts -FieldOptions.listeners -``` +Defined in: [tanstack-field.ts:149](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L149) *** -### name +### injector ```ts -name: TName; +injector: Injector; ``` -Defined in: [tanstack-field.directive.ts:75](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L75) +Defined in: [tanstack-field.ts:222](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L222) -The field name. The type will be `DeepKeys` to ensure your name is a deep key of the parent dataset. +*** -#### Implementation of +### listeners ```ts -FieldOptions.name +listeners: InputSignal< + | undefined +| NoInfer>>; ``` +Defined in: [tanstack-field.ts:117](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L117) + *** -### tanstackField +### mode ```ts -tanstackField: FormApi; +mode: InputSignal; ``` -Defined in: [tanstack-field.directive.ts:79](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L79) +Defined in: [tanstack-field.ts:147](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L147) *** -### unmount()? +### name ```ts -optional unmount: () => void; +name: InputSignal; ``` -Defined in: [tanstack-field.directive.ts:185](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L185) - -#### Returns - -`void` +Defined in: [tanstack-field.ts:71](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L71) *** -### validators? +### options ```ts -optional validators: NoInfer>; +options: Signal>; ``` -Defined in: [tanstack-field.directive.ts:91](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L91) +Defined in: [tanstack-field.ts:183](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L183) -A list of validators to pass to the field +*** -#### Implementation of +### tanstackField ```ts -FieldOptions.validators +tanstackField: InputSignal>; ``` -## Methods +Defined in: [tanstack-field.ts:79](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L79) + +*** -### ngOnChanges() +### validators ```ts -ngOnChanges(): void +validators: InputSignal< + | undefined +| NoInfer>>; ``` -Defined in: [tanstack-field.directive.ts:197](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L197) +Defined in: [tanstack-field.ts:97](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L97) -A callback method that is invoked immediately after the -default change detector has checked data-bound properties -if at least one has changed, and before the view and content -children are checked. +## Accessors -#### Returns - -`void` - -#### Implementation of - -```ts -OnChanges.ngOnChanges -``` - -*** +### api -### ngOnDestroy() +#### Get Signature ```ts -ngOnDestroy(): void +get api(): FieldApi ``` -Defined in: [tanstack-field.directive.ts:193](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L193) +Defined in: [tanstack-field.ts:155](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L155) -A callback method that performs custom clean-up, invoked immediately -before a directive, pipe, or service instance is destroyed. +##### Returns -#### Returns - -`void` - -#### Implementation of +`FieldApi`\<`TParentData`, `TName`, `TData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnDynamic`, `TFormOnDynamicAsync`, `TFormOnServer`, `TSubmitMeta`\> -```ts -OnDestroy.ngOnDestroy -``` - -*** +## Methods ### ngOnInit() @@ -302,7 +249,7 @@ OnDestroy.ngOnDestroy ngOnInit(): void ``` -Defined in: [tanstack-field.directive.ts:187](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.directive.ts#L187) +Defined in: [tanstack-field.ts:240](https://github.com/TanStack/form/blob/main/packages/angular-form/src/tanstack-field.ts#L240) A callback method that is invoked immediately after the default change detector has checked the directive's diff --git a/docs/framework/angular/reference/classes/tanstackfieldinjectable.md b/docs/framework/angular/reference/classes/tanstackfieldinjectable.md new file mode 100644 index 000000000..ad63cfd31 --- /dev/null +++ b/docs/framework/angular/reference/classes/tanstackfieldinjectable.md @@ -0,0 +1,52 @@ +--- +id: TanStackFieldInjectable +title: TanStackFieldInjectable +--- + + + +# Class: TanStackFieldInjectable\ + +Defined in: [injectable.ts:5](https://github.com/TanStack/form/blob/main/packages/angular-form/src/injectable.ts#L5) + +## Type Parameters + +• **T** + +## Constructors + +### new TanStackFieldInjectable() + +```ts +new TanStackFieldInjectable(): TanStackFieldInjectable +``` + +#### Returns + +[`TanStackFieldInjectable`](../tanstackfieldinjectable.md)\<`T`\> + +## Properties + +### \_api + +```ts +_api: WritableSignal>; +``` + +Defined in: [injectable.ts:6](https://github.com/TanStack/form/blob/main/packages/angular-form/src/injectable.ts#L6) + +## Accessors + +### api + +#### Get Signature + +```ts +get api(): FieldApi +``` + +Defined in: [injectable.ts:34](https://github.com/TanStack/form/blob/main/packages/angular-form/src/injectable.ts#L34) + +##### Returns + +`FieldApi`\<`any`, `any`, `T`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `any`\> diff --git a/docs/framework/angular/reference/functions/injectfield.md b/docs/framework/angular/reference/functions/injectfield.md new file mode 100644 index 000000000..2a744439f --- /dev/null +++ b/docs/framework/angular/reference/functions/injectfield.md @@ -0,0 +1,22 @@ +--- +id: injectField +title: injectField +--- + + + +# Function: injectField() + +```ts +function injectField(): TanStackFieldInjectable +``` + +Defined in: [injectable.ts:39](https://github.com/TanStack/form/blob/main/packages/angular-form/src/injectable.ts#L39) + +## Type Parameters + +• **T** + +## Returns + +[`TanStackFieldInjectable`](../../classes/tanstackfieldinjectable.md)\<`T`\> diff --git a/docs/framework/angular/reference/functions/injectform.md b/docs/framework/angular/reference/functions/injectform.md index 08bdeed3c..a65d39b26 100644 --- a/docs/framework/angular/reference/functions/injectform.md +++ b/docs/framework/angular/reference/functions/injectform.md @@ -8,7 +8,7 @@ title: injectForm # Function: injectForm() ```ts -function injectForm(opts?): FormApi +function injectForm(opts?): FormApi ``` Defined in: [inject-form.ts:9](https://github.com/TanStack/form/blob/main/packages/angular-form/src/inject-form.ts#L9) @@ -31,6 +31,10 @@ Defined in: [inject-form.ts:9](https://github.com/TanStack/form/blob/main/packag • **TOnSubmitAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\> +• **TOnDynamic** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\> + +• **TOnDynamicAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\> + • **TOnServer** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\> • **TSubmitMeta** @@ -39,8 +43,8 @@ Defined in: [inject-form.ts:9](https://github.com/TanStack/form/blob/main/packag ### opts? -`FormOptions`\<`TFormData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnServer`, `TSubmitMeta`\> +`FormOptions`\<`TFormData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`, `TOnServer`, `TSubmitMeta`\> ## Returns -`FormApi`\<`TFormData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnServer`, `TSubmitMeta`\> +`FormApi`\<`TFormData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`, `TOnServer`, `TSubmitMeta`\> diff --git a/docs/framework/angular/reference/functions/injectstore.md b/docs/framework/angular/reference/functions/injectstore.md index fad9e10ed..a12d9bfb1 100644 --- a/docs/framework/angular/reference/functions/injectstore.md +++ b/docs/framework/angular/reference/functions/injectstore.md @@ -8,7 +8,7 @@ title: injectStore # Function: injectStore() ```ts -function injectStore(form, selector?): Signal +function injectStore(form, selector?): Signal ``` Defined in: [inject-store.ts:9](https://github.com/TanStack/form/blob/main/packages/angular-form/src/inject-store.ts#L9) @@ -31,17 +31,21 @@ Defined in: [inject-store.ts:9](https://github.com/TanStack/form/blob/main/packa • **TOnSubmitAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\> +• **TOnDynamic** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\> + +• **TOnDynamicAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\> + • **TOnServer** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\> • **TSubmitMeta** -• **TSelected** = `NoInfer`\<`FormState`\<`TFormData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnServer`\>\> +• **TSelected** = `NoInfer`\<`FormState`\<`TFormData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`, `TOnServer`\>\> ## Parameters ### form -`FormApi`\<`TFormData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnServer`, `TSubmitMeta`\> +`FormApi`\<`TFormData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`, `TOnServer`, `TSubmitMeta`\> ### selector? diff --git a/docs/framework/angular/reference/index.md b/docs/framework/angular/reference/index.md index 3110ff697..0b69f24c7 100644 --- a/docs/framework/angular/reference/index.md +++ b/docs/framework/angular/reference/index.md @@ -9,9 +9,12 @@ title: "@tanstack/angular-form" ## Classes -- [TanStackField](classes/tanstackfield.md) +- [TanStackAppField](../classes/tanstackappfield.md) +- [TanStackField](../classes/tanstackfield.md) +- [TanStackFieldInjectable](../classes/tanstackfieldinjectable.md) ## Functions -- [injectForm](functions/injectform.md) -- [injectStore](functions/injectstore.md) +- [injectField](../functions/injectfield.md) +- [injectForm](../functions/injectform.md) +- [injectStore](../functions/injectstore.md) diff --git a/docs/framework/lit/guides/basic-concepts.md b/docs/framework/lit/guides/basic-concepts.md index a67352d3b..efc3a7c60 100644 --- a/docs/framework/lit/guides/basic-concepts.md +++ b/docs/framework/lit/guides/basic-concepts.md @@ -86,17 +86,47 @@ For Example: 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 its `field.state` property. -```tsx +```ts const { value, meta: { errors, isValidating }, } = field.state ``` -There are three field states can be very useful to see how the user interacts with a field. A field is _"touched"_ when the user clicks/tabs into it, _"pristine"_ until the user changes value in it, and _"dirty"_ after the value has been changed. You can check these states via the `isTouched`, `isPristine` and `isDirty` flags, as seen below. +There are four states in the metadata that can be useful to see how the user interacts with a field: -```tsx -const { isTouched, isPristine, isDirty } = field.state.meta +- _"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 ``` ![Field states](https://raw.githubusercontent.com/TanStack/form/main/docs/assets/field-states.png) + +## 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 states extended](https://raw.githubusercontent.com/TanStack/form/main/docs/assets/field-states-extended.png) diff --git a/docs/framework/lit/guides/dynamic-validation.md b/docs/framework/lit/guides/dynamic-validation.md new file mode 100644 index 000000000..c601e4762 --- /dev/null +++ b/docs/framework/lit/guides/dynamic-validation.md @@ -0,0 +1,269 @@ +--- +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. + +```ts +import { LitElement, html } from 'lit' +import { customElement } from 'lit/decorators.js' +import { TanStackFormController, revalidateLogic } from '@tanstack/lit-form' + +@customElement('my-form') +export class MyForm extends LitElement { + #form = new TanStackFormController(this, { + 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 + }, + }, + }) + + render() { + return html`` + } +} +``` + +> By default `onDynamic` is not called, so you need to pass `revalidateLogic()` to the `validationLogic` option of `useForm`. + +## 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: + +```ts +@customElement('my-form') +export class MyForm extends LitElement { + #form = new TanStackFormController(this, { + // ... + 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.api.state.errorMap` object. + +```ts +import { LitElement, html } from 'lit' +import { customElement } from 'lit/decorators.js' +import { TanStackFormController, revalidateLogic } from '@tanstack/lit-form' + +@customElement('my-form') +export class MyForm extends LitElement { + #form = new TanStackFormController(this, { + // ... + validationLogic: revalidateLogic(), + validators: { + onDynamic: ({ value }) => { + if (!value.firstName) { + return { firstName: 'A first name is required' } + } + return undefined + }, + }, + }) + + render() { + return html`

${this.#form.api.state.errorMap.onDynamic?.firstName}

` + } +} +``` + +## Usage with Other Validation Logic + +You can use `onDynamic` validation alongside other validation logic, such as `onChange` or `onBlur`. + +```ts +import { LitElement, html } from 'lit' +import { customElement } from 'lit/decorators.js' +import { TanStackFormController, revalidateLogic } from '@tanstack/lit-form' + +@customElement('my-form') +export class MyForm extends LitElement { + #form = new TanStackFormController(this, { + 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 + }, + }, + }) + + render() { + return html` +
+

${this.#form.api.state.errorMap.onChange?.firstName}

+

${this.#form.api.state.errorMap.onDynamic?.lastName}

+
+ ` + } +} +``` + +### Usage with Fields + +You can also use `onDynamic` validation with fields, just like you would with other validation logic. + +```ts +import { LitElement, html } from 'lit' +import { customElement } from 'lit/decorators.js' +import { TanStackFormController, revalidateLogic } from '@tanstack/lit-form' + +@customElement('my-form') +export class MyForm extends LitElement { + #form = new TanStackFormController(this, { + defaultValues: { + name: '', + age: 0, + }, + validationLogic: revalidateLogic(), + onSubmit({ value }) { + alert(JSON.stringify(value)) + }, + }) + + render() { + return html` +
{ + e.preventDefault() + e.stopPropagation() + this.#form.api.handleSubmit() + }} + > + ${this.#form.field( + { + name: 'age', + validators: { + onDynamic: ({ value }) => + value > 18 ? undefined : 'Age must be greater than 18', + }, + }, + (field) => html` +
+ { + const target = e.target as HTMLInputElement + field.handleChange(target.valueAsNumber) + }} + @blur=${() => field.handleBlur()} + /> +

${field.state.meta.errorMap.onDynamic}

+
+ `, + )} + +
+ ` + } +} +``` + +### 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. + +```ts +import { LitElement } from 'lit' +import { customElement } from 'lit/decorators.js' +import { TanStackFormController, revalidateLogic } from '@tanstack/lit-form' + +@customElement('my-form') +export class MyForm extends LitElement { + #form = new TanStackFormController(this, { + 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. + +```ts +import { LitElement } from 'lit' +import { customElement } from 'lit/decorators.js' +import { TanStackFormController, revalidateLogic } from '@tanstack/lit-form' +import { z } from 'zod' + +const schema = z.object({ + firstName: z.string().min(1, 'A first name is required'), + lastName: z.string().min(1, 'A last name is required'), +}) + +@customElement('my-form') +export class MyForm extends LitElement { + #form = new TanStackFormController(this, { + defaultValues: { + firstName: '', + lastName: '', + }, + validationLogic: revalidateLogic(), + validators: { + onDynamic: schema, + }, + }) +} +``` diff --git a/docs/framework/lit/guides/validation.md b/docs/framework/lit/guides/validation.md new file mode 100644 index 000000000..1dc36aabd --- /dev/null +++ b/docs/framework/lit/guides/validation.md @@ -0,0 +1,631 @@ +--- +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 `field()` method accepts some callbacks as validators 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.state.meta.errors`. + +Here is an example: + +```ts +import { html, nothing } from 'lit' +;`${this.#form.field( + { + name: 'age', + validators: { + onChange: ({ value }) => + value < 13 ? 'You must be 13 to make an account' : undefined, + }, + }, + (field) => { + return html` + + + ${!field.state.meta.isValid + ? html`${field.state.meta.errors.join(', ')}` + : nothing} + ` + }, +)}` +``` + +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: + +```ts +import { html, nothing } from 'lit' +;`${this.#form.field( + { + name: 'age', + validators: { + onBlur: ({ value }) => + value < 13 ? 'You must be 13 to make an account' : undefined, + }, + }, + (field) => { + return html` + + + ${!field.state.meta.isValid + ? html`${field.state.meta.errors.join(', ')}` + : nothing} + ` + }, +)}` +``` + +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: + +```ts +import { html, nothing } from 'lit' +;`${this.#form.field( + { + name: 'age', + validators: { + onChange: ({ value }) => + value < 13 ? 'You must be 13 to make an account' : undefined, + onBlur: ({ value }) => (value < 0 ? 'Invalid value' : undefined), + }, + }, + (field) => { + return html` + + + ${!field.state.meta.isValid + ? html`${field.state.meta.errors.join(', ')}` + : nothing} + ` + }, +)}` +``` + +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: + +```ts +import { html, nothing } from 'lit' +;`${this.#form.field( + { + name: 'age', + validators: { + onChange: ({ value }) => + value < 13 ? 'You must be 13 to make an account' : undefined, + }, + }, + (field) => { + return html` + + ${!field.state.meta.isValid + ? html`${field.state.meta.errors.join(',')}` + : nothing} + ` + }, +)}` +``` + +Or use the `errorMap` property to access the specific error you're looking for: + +```ts +import { html, nothing } from 'lit' +;`${this.#form.field( + { + name: 'age', + validators: { + onChange: ({ value }) => + value < 13 ? 'You must be 13 to make an account' : undefined, + }, + }, + (field) => { + return html` + + ${field.state.meta.errorMap['onChange'] + ? html`${field.state.meta.errorMap['onChange']}` + : nothing} + ` + }, +)}` +``` + +It's worth mentioning that our `errors` array and the `errorMap` matches the types returned by the validators. This means that: + +```ts +import { html, nothing } from 'lit' + +;`${this.#form.field( + { + name: 'age', + validators: { + onChange: ({ value }) => (value < 13 ? { isOldEnough: false } : undefined), + }, + }, + (field) => { + return html` + + + + ${!field.state.meta.errorMap['onChange']?.isOldEnough + ? html`The user is not old enough` + : nothing} + ` + }, +)}` +``` + +## Validation at field level vs at form level + +As shown above, each field 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 `TanStackFormController` constructor. + +Example: + +```ts +import { LitElement, html, nothing } from 'lit' +import { customElement } from 'lit/decorators.js' +import { TanStackFormController } from '@tanstack/lit-form' + +@customElement('my-form') +export class MyForm extends LitElement { + #form = new TanStackFormController(this, { + defaultValues: { + age: 0, + }, + onSubmit: async ({ 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' + } + return undefined + }, + }, + }) + + render() { + return html` +
+ + ${this.#form.api.state.errorMap.onChange + ? html` +
+ There was an error on the form: + ${this.#form.api.state.errorMap.onChange} +
+ ` + : nothing} + +
+ ` + } +} +``` + +### 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. + +```ts +import { LitElement, html, nothing } from 'lit' +import { customElement } from 'lit/decorators.js' +import { TanStackFormController } from '@tanstack/lit-form' + +@customElement('my-form') +export class MyForm extends LitElement { + #form = new TanStackFormController(this, { + defaultValues: { + age: 0, + socials: [], + details: { + email: '', + }, + }, + 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 + }, + }, + }) + + render() { + return html` +
+
+ ${this.#form.field( + { name: 'age' }, + (field) => html` + + + ${!field.state.meta.isValid + ? html`${field.state.meta.errors.join(', ')}` + : nothing} + `, + )} + ${this.#form.api.state.errorMap.onSubmit + ? html` +
+ There was an error on the form: + ${this.#form.api.state.errorMap.onSubmit} +
+ ` + : nothing} + +
+
+ ` + } +} +``` + +> 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: +> +> ```ts +> const form = new TanStackFormController(this, { +> defaultValues: { +> age: 0, +> }, +> validators: { +> onChange: ({ value }) => { +> return { +> fields: { +> age: value.age < 12 ? 'Too young!' : undefined, +> }, +> } +> }, +> }, +> }) +> +> // ... +> +> return html` +> ${this.#form.field( +> { +> name: 'age', +> validators: { +> onChange: ({ value }) => +> value % 2 === 0 ? 'Must be odd!' : undefined, +> }, +> }, +> () => html``, +> )} +> ` +> ``` +> +> Will only show `'Must be odd!` even if the 'Too young!' error is returned by the form-level validation. + +## Asynchronous Functional Validation + +While we suspect most validations will be synchronous, there are many instances where a network call or some other async operation would be useful to validate against. + +To do this, we have dedicated `onChangeAsync`, `onBlurAsync`, and other methods that can be used to validate against: + +```ts +import { html, nothing } from 'lit' +;`${this.#form.field( + { + name: 'age', + validators: { + onChangeAsync: async ({ value }) => { + await new Promise((resolve) => setTimeout(resolve, 1000)) + return value < 13 ? 'You must be 13 to make an account' : undefined + }, + }, + }, + (field) => { + return html` + + + ${!field.state.meta.isValid + ? html`${field.state.meta.errors.join(', ')}` + : nothing} + ` + }, +)}` +``` + +Synchronous and Asynchronous validations can coexist. For example, it is possible to define both `onBlur` and `onBlurAsync` on the same field: + +```ts +import { html, nothing } from 'lit' +;`${this.#form.field( + { + name: 'age', + validators: { + onBlur: ({ value }) => + value < 13 ? 'You must be at least 13' : undefined, + onBlurAsync: async ({ value }) => { + const currentAge = await fetchCurrentAgeOnProfile() + return value < currentAge ? 'You can only increase the age' : undefined + }, + }, + }, + (field) => { + return html` + + + ${!field.state.meta.isValid + ? html`${field.state.meta.errors.join(', ')}` + : nothing} + ` + }, +)}` +``` + +The synchronous validation method (`onBlur`) is run first and the asynchronous method (`onBlurAsync`) is only run if the synchronous one (`onBlur`) succeeds. To change this behaviour, set the `asyncAlways` option to `true`, and the async method will be run regardless of the result of the sync method. + +### Built-in Debouncing + +While async calls are the way to go when validating against the database, running a network request on every keystroke is a good way to DDOS your database. + +Instead, we enable an easy method for debouncing your `async` calls by adding a single property: + +```ts +;`${this.#form.field( + { + name: 'age', + asyncDebounceMs: 500, + validators: { + onChangeAsync: async ({ value }) => { + // ... + }, + }, + }, + (field) => { + return html`` + }, +)}` +``` + +This will debounce every async call with a 500ms delay. You can even override this property on a per-validation property: + +```ts +;`${this.#form.field( + { + name: 'age', + asyncDebounceMs: 500, + validators: { + onChangeAsyncDebounceMs: 1500, + onChangeAsync: async ({ value }) => { + // ... + }, + onBlurAsync: async ({ value }) => { + // ... + }, + }, + }, + (field) => { + return html`` + }, +)}` +``` + +This will run `onChangeAsync` every 1500ms while `onBlurAsync` will run every 500ms. + +## Validation through Schema Libraries + +While functions provide more flexibility and customization over your validation, they can be a bit verbose. To help solve this, there are libraries that provide schema-based validation to make shorthand and type-strict validation substantially easier. You can also define a single schema for your entire form and pass it to the form level, errors will be automatically propagated to the fields. + +### Standard Schema Libraries + +TanStack Form natively supports all libraries following the [Standard Schema specification](https://github.com/standard-schema/standard-schema), most notably: + +- [Zod](https://zod.dev/) +- [Valibot](https://valibot.dev/) +- [ArkType](https://arktype.io/) +- [Effect/Schema](https://effect.website/docs/schema/standard-schema/) + +_Note:_ make sure to use the latest version of the schema libraries as older versions might not support Standard Schema yet. + +> Validation will not provide you with transformed values. See [submission handling](../submission-handling.md) for more information. + +To use schemas from these libraries you can pass them to the `validators` props as you would do with a custom function: + +```ts +import { z } from 'zod' +import { LitElement, html } from 'lit' +import { customElement } from 'lit/decorators.js' +import { TanStackFormController } from '@tanstack/lit-form' + +const userSchema = z.object({ + age: z.number().gte(13, 'You must be 13 to make an account'), +}) + +@customElement('my-form') +export class MyForm extends LitElement { + #form = new TanStackFormController(this, { + defaultValues: { + age: 0, + }, + validators: { + onChange: userSchema, + }, + }) + + render() { + return html` +
+ ${this.#form.field({ name: 'age' }, (field) => { + return html`` + })} +
+ ` + } +} +``` + +Async validations on form and field level are supported as well: + +```ts +import { html } from 'lit' +import { z } from 'zod' + +${this.#form.field( + { + name: 'age', + validators: { + onChange: z.number().gte(13, 'You must be 13 to make an account'), + onChangeAsyncDebounceMs: 500, + onChangeAsync: z.number().refine( + async (value) => { + const currentAge = await fetchCurrentAgeOnProfile() + return value >= currentAge + }, + { + message: 'You can only increase the age', + }, + ), + }, + }, + (field) => { + return html`` + }, +)} +``` + +If you need even more control over your Standard Schema validation, you can combine a Standard Schema with a callback function like so: + +```ts +import { html } from 'lit' +import { z } from 'zod' + +${this.#form.field( + { + name: 'age', + asyncDebounceMs: 500, + validators: { + onChangeAsync: async ({ value, fieldApi }) => { + const errors = fieldApi.parseValueWithSchema( + z.number().gte(13, 'You must be 13 to make an account'), + ) + if (errors) return errors + // continue with your validation + }, + }, + }, + (field) => { + return html`` + }, +)} +``` + +## Preventing invalid forms from being submitted + +The `onChange`, `onBlur` etc... callbacks are also run when the form is submitted and the submission is blocked if the form is invalid. + +The form state object has a `canSubmit` flag that is false when any field is invalid and the form has been touched (`canSubmit` is true until the form has been touched, even if some fields are "technically" invalid based on their `onChange`/`onBlur` props). + +You can access this flag via `this.#form.api.state` and use the value in order to, for example, disable the submit button when the form is invalid (in practice, disabled buttons are not accessible, use `aria-disabled` instead). + +```ts +class MyForm extends LitElement { + #form = new TanStackFormController(this, { + /* ... */ + }) + + render() { + return html` + + + + + ` + } +} +``` diff --git a/docs/framework/lit/reference/classes/tanstackformcontroller.md b/docs/framework/lit/reference/classes/tanstackformcontroller.md index 30f5e3e7b..6bf1822dd 100644 --- a/docs/framework/lit/reference/classes/tanstackformcontroller.md +++ b/docs/framework/lit/reference/classes/tanstackformcontroller.md @@ -5,9 +5,9 @@ title: TanStackFormController -# Class: TanStackFormController\ +# Class: TanStackFormController\ -Defined in: [tanstack-form-controller.ts:190](https://github.com/TanStack/form/blob/main/packages/lit-form/src/tanstack-form-controller.ts#L190) +Defined in: [tanstack-form-controller.ts:222](https://github.com/TanStack/form/blob/main/packages/lit-form/src/tanstack-form-controller.ts#L222) ## Type Parameters @@ -27,6 +27,10 @@ Defined in: [tanstack-form-controller.ts:190](https://github.com/TanStack/form/b • **TFormOnSubmitAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TParentData`\> +• **TFormOnDynamic** *extends* `undefined` \| `FormValidateOrFn`\<`TParentData`\> + +• **TFormOnDynamicAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TParentData`\> + • **TFormOnServer** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TParentData`\> • **TSubmitMeta** @@ -40,10 +44,10 @@ Defined in: [tanstack-form-controller.ts:190](https://github.com/TanStack/form/b ### new TanStackFormController() ```ts -new TanStackFormController(host, config?): TanStackFormController +new TanStackFormController(host, config?): TanStackFormController ``` -Defined in: [tanstack-form-controller.ts:219](https://github.com/TanStack/form/blob/main/packages/lit-form/src/tanstack-form-controller.ts#L219) +Defined in: [tanstack-form-controller.ts:255](https://github.com/TanStack/form/blob/main/packages/lit-form/src/tanstack-form-controller.ts#L255) #### Parameters @@ -53,31 +57,31 @@ Defined in: [tanstack-form-controller.ts:219](https://github.com/TanStack/form/b ##### config? -`FormOptions`\<`TParentData`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnServer`, `TSubmitMeta`\> +`FormOptions`\<`TParentData`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnDynamic`, `TFormOnDynamicAsync`, `TFormOnServer`, `TSubmitMeta`\> #### Returns -[`TanStackFormController`](tanstackformcontroller.md)\<`TParentData`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnServer`, `TSubmitMeta`\> +[`TanStackFormController`](../tanstackformcontroller.md)\<`TParentData`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnDynamic`, `TFormOnDynamicAsync`, `TFormOnServer`, `TSubmitMeta`\> ## Properties ### api ```ts -api: FormApi; +api: FormApi; ``` -Defined in: [tanstack-form-controller.ts:206](https://github.com/TanStack/form/blob/main/packages/lit-form/src/tanstack-form-controller.ts#L206) +Defined in: [tanstack-form-controller.ts:240](https://github.com/TanStack/form/blob/main/packages/lit-form/src/tanstack-form-controller.ts#L240) ## Methods ### field() ```ts -field(fieldConfig, render): object +field(fieldConfig, render): object ``` -Defined in: [tanstack-form-controller.ts:259](https://github.com/TanStack/form/blob/main/packages/lit-form/src/tanstack-form-controller.ts#L259) +Defined in: [tanstack-form-controller.ts:299](https://github.com/TanStack/form/blob/main/packages/lit-form/src/tanstack-form-controller.ts#L299) #### Type Parameters @@ -99,15 +103,19 @@ Defined in: [tanstack-form-controller.ts:259](https://github.com/TanStack/form/b • **TOnSubmitAsync** *extends* `undefined` \| `FieldAsyncValidateOrFn`\<`TParentData`, `TName`, `TData`\> +• **TOnDynamic** *extends* `undefined` \| `FieldValidateOrFn`\<`TParentData`, `TName`, `TData`\> + +• **TOnDynamicAsync** *extends* `undefined` \| `FieldAsyncValidateOrFn`\<`TParentData`, `TName`, `TData`\> + #### Parameters ##### fieldConfig -`FieldOptions`\<`TParentData`, `TName`, `TData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`\> +`FieldOptions`\<`TParentData`, `TName`, `TData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`\> ##### render -`renderCallback`\<`TParentData`, `TName`, `TData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnServer`, `TSubmitMeta`\> +`renderCallback`\<`TParentData`, `TName`, `TData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`, `TFormOnMount`, `TFormOnChange`, `TFormOnChangeAsync`, `TFormOnBlur`, `TFormOnBlurAsync`, `TFormOnSubmit`, `TFormOnSubmitAsync`, `TFormOnDynamic`, `TFormOnDynamicAsync`, `TFormOnServer`, `TSubmitMeta`\> #### Returns @@ -122,19 +130,19 @@ values: object; ###### values.form ```ts -form: FormApi; +form: FormApi; ``` ###### values.options ```ts -options: FieldOptions; +options: FieldOptions; ``` ###### values.render ```ts -render: renderCallback; +render: renderCallback; ``` *** @@ -145,7 +153,7 @@ render: renderCallback ``` -More information can be found at [Listeners](./listeners.md) +More information can be found at [Listeners](../listeners.md) ## Array Fields diff --git a/docs/framework/react/guides/custom-errors.md b/docs/framework/react/guides/custom-errors.md index 5e52d5d80..0ecb12a07 100644 --- a/docs/framework/react/guides/custom-errors.md +++ b/docs/framework/react/guides/custom-errors.md @@ -69,10 +69,8 @@ Useful for representing quantities, thresholds, or magnitudes: Display in UI: ```tsx -{ - /* TypeScript knows the error is a number based on your validator */ -} -;
+// TypeScript knows the error is a number based on your validator +
You need {field.state.meta.errors[0]} more years to be eligible
``` diff --git a/docs/framework/react/guides/devtools.md b/docs/framework/react/guides/devtools.md new file mode 100644 index 000000000..576c5f9a3 --- /dev/null +++ b/docs/framework/react/guides/devtools.md @@ -0,0 +1,52 @@ +--- +id: devtools +title: Devtools +--- + +TanStack Form comes with a ready to go suit of devtools. + +## Setup + +Install the [TanStack Devtools](https://tanstack.com/devtools/latest/docs/quick-start) library and the [TanStack Form plugin](http://npmjs.com/package/@tanstack/react-form-devtools), from the framework adapter that your working in (in this case `@tanstack/react-devtools`, and `@tanstack/react-form-devtools`). + +```bash +npm i @tanstack/react-devtools +npm i @tanstack/react-form-devtools +``` + +Next in the root of your application import the `TanStackDevtools`. + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' + +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + + + , +) +``` + +Import the `FormDevtoolsPlugin` from **TanStack Form** and provide it to the `TanStackDevtools` component. + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' +import { FormDevtoolsPlugin } from '@tanstack/react-form-devtools' + +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + + + , +) +``` + +Finally add any additional configuration you desire to the `TanStackDevtools` component, more information can be found under the [TanStack Devtools Configuration](https://tanstack.com/devtools/) section. + +A complete working example can be found in our [examples section](https://tanstack.com/form/latest/docs/framework/react/examples/devtools). diff --git a/docs/framework/react/guides/dynamic-validation.md b/docs/framework/react/guides/dynamic-validation.md new file mode 100644 index 000000000..b1d764ec2 --- /dev/null +++ b/docs/framework/react/guides/dynamic-validation.md @@ -0,0 +1,224 @@ +--- +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. + +```tsx +import { revalidateLogic, useForm } from '@tanstack/react-form' + +// ... + +const form = useForm({ + 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 `useForm`. + +## 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: + +```tsx +const form = useForm({ + // ... + 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.state.errorMap` object. + +```tsx +function App() { + const form = useForm({ + // ... + validationLogic: revalidateLogic(), + validators: { + onDynamic: ({ value }) => { + if (!value.firstName) { + return { firstName: 'A first name is required' } + } + return undefined + }, + }, + }) + + return

{form.state.errorMap.onDynamic?.firstName}

+} +``` + +## Usage with Other Validation Logic + +You can use `onDynamic` validation alongside other validation logic, such as `onChange` or `onBlur`. + +```tsx +import { revalidateLogic, useForm } from '@tanstack/react-form' + +function App() { + const form = useForm({ + 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 + }, + }, + }) + + return ( +
+

{form.state.errorMap.onChange?.firstName}

+

{form.state.errorMap.onDynamic?.lastName}

+
+ ) +} +``` + +### Usage with Fields + +You can also use `onDynamic` validation with fields, just like you would with other validation logic. + +```tsx +function App() { + const form = useForm({ + defaultValues: { + name: '', + age: 0, + }, + validationLogic: revalidateLogic(), + onSubmit({ value }) { + alert(JSON.stringify(value)) + }, + }) + + return ( +
{ + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} + > + + value > 18 ? undefined : 'Age must be greater than 18', + }} + children={(field) => ( +
+ field.handleChange(e.target.valueAsNumber)} + onBlur={field.handleBlur} + value={field.state.value} + /> +

+ {field.state.meta.errorMap.onDynamic} +

+
+ )} + /> + + + ) +} +``` + +### 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. + +```tsx +const form = useForm({ + 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. + +```tsx +import { z } from 'zod' + +const schema = z.object({ + firstName: z.string().min(1, 'A first name is required'), + lastName: z.string().min(1, 'A last name is required'), +}) + +const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + validationLogic: revalidateLogic(), + validators: { + onDynamic: schema, + }, +}) +``` diff --git a/docs/framework/react/guides/focus-management.md b/docs/framework/react/guides/focus-management.md new file mode 100644 index 000000000..6e1a26196 --- /dev/null +++ b/docs/framework/react/guides/focus-management.md @@ -0,0 +1,151 @@ +--- +id: focus-management +title: Focus Management +--- + +In some instances, you may want to focus the first input with an error. + +[Because TanStack Form intentionally does not have insights into your markup](../../../philosophy.md), we cannot add a built-in focus management feature. + +However, you can easily add this feature into your application without this hypothetical built-in feature. + +## React DOM + +```tsx +import { useForm } from '@tanstack/react-form' +import { z } from 'zod' + +export default function App() { + const form = useForm({ + defaultValues: { age: 0 }, + validators: { + onChange: z.object({ + age: z.number().min(12), + }), + }, + onSubmit() { + alert('Submitted!') + }, + onSubmitInvalid({ formApi }) { + // This can be extracted to a function that takes the form ID and `formAPI` as arguments + const errorMap = formApi.state.errorMap.onChange! + const inputs = Array.from( + // Must match the selector used in your form + document.querySelectorAll('#myform input'), + ) as HTMLInputElement[] + + let firstInput: HTMLInputElement | undefined + for (const input of inputs) { + if (!!errorMap[input.name]) { + firstInput = input + break + } + } + firstInput?.focus() + }, + }) + + return ( + // The `id` here is used to isolate the focus management from the rest of the page +
{ + e.preventDefault() + e.stopPropagation() + void form.handleSubmit() + }} + > + ( + + )} + /> +
+ +
+ + ) +} +``` + +## React Native + +Because React Native doesn't have access to the DOM's `querySelectorAll` API, we need to manually manage the element list +of the inputs. This allows us to focus the first input with an error: + +```tsx +import { useRef } from 'react' +import { Text, View, TextInput, Button, Alert } from 'react-native' +import { useForm } from '@tanstack/react-form' +import { z } from 'zod' + +export default function App() { + // This can be extracted to a hook that returns the `fields` ref, a `focusFirstField` function, and a `addField` function + const fields = useRef([] as Array<{ input: TextInput; name: string }>) + + const form = useForm({ + defaultValues: { age: 0 }, + validators: { + onChange: z.object({ + age: z.number().min(12), + }), + }, + onSubmit() { + Alert.alert('Submitted!') + }, + onSubmitInvalid({ formApi }) { + const errorMap = formApi.state.errorMap.onChange + const inputs = fields.current + + let firstInput + for (const input of inputs) { + if (!input || !input.input) continue + if (!!errorMap[input.name]) { + firstInput = input.input + break + } + } + firstInput?.focus() + }, + }) + + return ( + + ( + + Age + { + // fields.current needs to be manually incremented so that we know what fields are rendered or not and in what order + fields.current[0] = { input, name: field.name } + }} + style={{ + borderWidth: 1, + borderColor: '#999999', + borderRadius: 4, + marginTop: 8, + padding: 8, + }} + onChangeText={(val) => field.handleChange(Number(val))} + value={field.state.value} + /> + + )} + /> + } + {(isSubmitting) => ( + + )} ) } @@ -218,7 +222,7 @@ function App() { > Why a higher-order component instead of a hook? -While hooks are the future of React, higher-order components are still a powerful tool for composition. In particular, the API of `useForm` enables us to have strong type-safety without requiring users to pass generics. +While hooks are the future of React, higher-order components are still a powerful tool for composition. In particular, the API of `withForm` enables us to have strong type-safety without requiring users to pass generics. > Why am I getting ESLint errors about hooks in `render`? @@ -244,6 +248,238 @@ const ChildForm = withForm({ }) ``` +## Reusing groups of fields in multiple forms + +Sometimes, a pair of fields are so closely related that it makes sense to group and reuse them — like the password example listed in the [linked fields guide](../linked-fields.md). Instead of repeating this logic across multiple forms, you can utilize the `withFieldGroup` higher-order component. + +> Unlike `withForm`, validators cannot be specified and could be any value. +> Ensure that your fields can accept unknown error types. + +Rewriting the passwords example using `withFieldGroup` would look like this: + +```tsx +const { useAppForm, withForm, withFieldGroup } = createFormHook({ + fieldComponents: { + TextField, + ErrorInfo, + }, + formComponents: { + SubscribeButton, + }, + fieldContext, + formContext, +}) + +type PasswordFields = { + password: string + confirm_password: string +} + +// These default values are not used at runtime, but the keys are needed for mapping purposes. +// This allows you to spread `formOptions` without needing to redeclare it. +const defaultValues: PasswordFields = { + password: '', + confirm_password: '', +} + +const FieldGroupPasswordFields = withFieldGroup({ + defaultValues, + // You may also restrict the group to only use forms that implement this submit meta. + // If none is provided, any form with the right defaultValues may use it. + // onSubmitMeta: { action: '' } + + // Optional, but adds props to the `render` function in addition to `form` + props: { + // These default values are also for type-checking and are not used at runtime + title: 'Password', + }, + // Internally, you will have access to a `group` instead of a `form` + render: function Render({ group, title }) { + // access reactive values using the group store + const password = useStore(group.store, (state) => state.values.password) + // or the form itself + const isSubmitting = useStore( + group.form.store, + (state) => state.isSubmitting, + ) + + return ( +
+

{title}

+ {/* Groups also have access to Field, Subscribe, Field, AppField and AppForm */} + + {(field) => } + + { + // The form could be any values, so it is typed as 'unknown' + const values: unknown = fieldApi.form.state.values + // use the group methods instead + if (value !== group.getFieldValue('password')) { + return 'Passwords do not match' + } + return undefined + }, + }} + > + {(field) => ( +
+ + +
+ )} +
+
+ ) + }, +}) +``` + +We can now use these grouped fields in any form that implements the default values: + +```tsx +// You are allowed to extend the group fields as long as the +// existing properties remain unchanged +type Account = PasswordFields & { + provider: string + username: string +} + +// You may nest the group fields wherever you want +type FormValues = { + name: string + age: number + account_data: PasswordFields + linked_accounts: Account[] +} + +const defaultValues: FormValues = { + name: '', + age: 0, + account_data: { + password: '', + confirm_password: '', + }, + linked_accounts: [ + { + provider: 'TanStack', + username: '', + password: '', + confirm_password: '', + }, + ], +} + +function App() { + const form = useAppForm({ + defaultValues, + // If the group didn't specify an `onSubmitMeta` property, + // the form may implement any meta it wants. + // Otherwise, the meta must be defined and match. + onSubmitMeta: { action: '' }, + }) + + return ( + + + + {(field) => + field.state.value.map((account, i) => ( + + )) + } + + + ) +} +``` + +### Mapping field group values to a different field + +You may want to keep the password fields on the top level of your form, or rename the properties for clarity. You can map field group values +to their true location by changing the `field` property: + +> [!IMPORTANT] +> Due to TypeScript limitations, field mapping is only allowed for objects. You can use records or arrays at the top level of a field group, but you will not be able to map the fields. + +```tsx +// To have an easier form, you can keep the fields on the top level +type FormValues = { + name: string + age: number + password: string + confirm_password: string +} + +const defaultValues: FormValues = { + name: '', + age: 0, + password: '', + confirm_password: '', +} + +function App() { + const form = useAppForm({ + defaultValues, + }) + + return ( + + + + ) +} +``` + +If you expect your fields to always be at the top level of your form, you can create a quick map +of your field groups using a helper function: + +```tsx +const defaultValues: PasswordFields = { + password: '', + confirm_password: '', +} + +const passwordFields = createFieldMap(defaultValues) +/* This generates the following map: + { + 'password': 'password', + 'confirm_password': 'confirm_password' + } +*/ + +// Usage: + +``` + ## Tree-shaking form and field components While the above examples are great for getting started, they're not ideal for certain use-cases where you might have hundreds of form and field components. @@ -268,7 +504,7 @@ export default function TextField({ label }: { label: string }) { return (