diff --git a/docs/config.json b/docs/config.json index c7044e561..6f362eb73 100644 --- a/docs/config.json +++ b/docs/config.json @@ -76,8 +76,7 @@ }, { "label": "Guides", - "children": [ - ], + "children": [], "frameworks": [ { "label": "react", @@ -165,9 +164,25 @@ { "label": "solid", "children": [ + { + "label": "Basic Concepts", + "to": "framework/solid/guides/basic-concepts" + }, + { + "label": "Form Validation", + "to": "framework/solid/guides/validation" + }, + { + "label": "Async Initial Values", + "to": "framework/solid/guides/async-initial-values" + }, { "label": "Arrays", "to": "framework/solid/guides/arrays" + }, + { + "label": "Linked Fields", + "to": "framework/solid/guides/linked-fields" } ] } @@ -176,78 +191,168 @@ { "label": "API Reference", "children": [ - {"label": "JavaScript Reference", "to": "reference/index"}, - {"label": "Classes / FieldApi", "to": "reference/fieldapi"}, - {"label": "Classes / FormApi", "to": "reference/formapi"}, - {"label": "Functions / formOptions", "to": "reference/formoptions"}, - {"label": "Functions / mergeForm", "to": "reference/mergeform"}, - {"label": "Interfaces / FieldApiOptions", "to": "reference/fieldapioptions"}, - {"label": "Interfaces / FieldOptions", "to": "reference/fieldoptions"}, - {"label": "Interfaces / FieldValidators", "to": "reference/fieldvalidators"}, - {"label": "Interfaces / FormOptions", "to": "reference/formoptions"}, - {"label": "Interfaces / FormValidators", "to": "reference/formvalidators"}, - {"label": "Types / DeepKeys", "to": "reference/deepkeys"}, - {"label": "Types / DeepValue", "to": "reference/deepvalue"}, - {"label": "Types / FieldInfo", "to": "reference/fieldinfo"}, - {"label": "Types / FieldMeta", "to": "reference/fieldmeta"}, - {"label": "Types / FieldState", "to": "reference/fieldstate"}, - {"label": "Types / FormState", "to": "reference/formstate"}, - {"label": "Types / Updater", "to": "reference/updater"}, - {"label": "Types / UpdaterFn", "to": "reference/updaterfn"}, - {"label": "Types / ValidationError", "to": "reference/validationerror"}, - {"label": "Types / ValidationMeta", "to": "reference/validationmeta"} + { "label": "JavaScript Reference", "to": "reference/index" }, + { "label": "Classes / FieldApi", "to": "reference/fieldapi" }, + { "label": "Classes / FormApi", "to": "reference/formapi" }, + { "label": "Functions / formOptions", "to": "reference/formoptions" }, + { "label": "Functions / mergeForm", "to": "reference/mergeform" }, + { + "label": "Interfaces / FieldApiOptions", + "to": "reference/fieldapioptions" + }, + { + "label": "Interfaces / FieldOptions", + "to": "reference/fieldoptions" + }, + { + "label": "Interfaces / FieldValidators", + "to": "reference/fieldvalidators" + }, + { "label": "Interfaces / FormOptions", "to": "reference/formoptions" }, + { + "label": "Interfaces / FormValidators", + "to": "reference/formvalidators" + }, + { "label": "Types / DeepKeys", "to": "reference/deepkeys" }, + { "label": "Types / DeepValue", "to": "reference/deepvalue" }, + { "label": "Types / FieldInfo", "to": "reference/fieldinfo" }, + { "label": "Types / FieldMeta", "to": "reference/fieldmeta" }, + { "label": "Types / FieldState", "to": "reference/fieldstate" }, + { "label": "Types / FormState", "to": "reference/formstate" }, + { "label": "Types / Updater", "to": "reference/updater" }, + { "label": "Types / UpdaterFn", "to": "reference/updaterfn" }, + { + "label": "Types / ValidationError", + "to": "reference/validationerror" + }, + { "label": "Types / ValidationMeta", "to": "reference/validationmeta" } ], "frameworks": [ { "label": "react", "children": [ - {"label": "React Reference", "to": "framework/react/reference/index"}, - {"label": "Functions / createServerValidate", "to": "framework/react/reference/createservervalidate"}, - {"label": "Functions / Field", "to": "framework/react/reference/field"}, - {"label": "Functions / useField", "to": "framework/react/reference/usefield"}, - {"label": "Functions / useForm", "to": "framework/react/reference/useform"}, - {"label": "Functions / useTransform", "to": "framework/react/reference/usetransform"}, - {"label": "Types / FieldComponent", "to": "framework/react/reference/fieldcomponent"}, - {"label": "Types / UseField", "to": "framework/react/reference/usefield"}, - {"label": "Variables / initialFormState", "to": "framework/react/reference/initialformstate"} + { + "label": "React Reference", + "to": "framework/react/reference/index" + }, + { + "label": "Functions / createServerValidate", + "to": "framework/react/reference/createservervalidate" + }, + { + "label": "Functions / Field", + "to": "framework/react/reference/field" + }, + { + "label": "Functions / useField", + "to": "framework/react/reference/usefield" + }, + { + "label": "Functions / useForm", + "to": "framework/react/reference/useform" + }, + { + "label": "Functions / useTransform", + "to": "framework/react/reference/usetransform" + }, + { + "label": "Types / FieldComponent", + "to": "framework/react/reference/fieldcomponent" + }, + { + "label": "Types / UseField", + "to": "framework/react/reference/usefield" + }, + { + "label": "Variables / initialFormState", + "to": "framework/react/reference/initialformstate" + } ] }, { "label": "vue", "children": [ - {"label": "Vue Reference", "to": "framework/vue/reference/index"}, - {"label": "Functions / Field", "to": "framework/vue/reference/field"}, - {"label": "Functions / useField", "to": "framework/vue/reference/usefield"}, - {"label": "Functions / useForm", "to": "framework/vue/reference/useform"}, - {"label": "Types / FieldComponent", "to": "framework/vue/reference/fieldcomponent"}, - {"label": "Types / UseField", "to": "framework/vue/reference/usefield"} + { "label": "Vue Reference", "to": "framework/vue/reference/index" }, + { + "label": "Functions / Field", + "to": "framework/vue/reference/field" + }, + { + "label": "Functions / useField", + "to": "framework/vue/reference/usefield" + }, + { + "label": "Functions / useForm", + "to": "framework/vue/reference/useform" + }, + { + "label": "Types / FieldComponent", + "to": "framework/vue/reference/fieldcomponent" + }, + { + "label": "Types / UseField", + "to": "framework/vue/reference/usefield" + } ] }, { "label": "solid", "children": [ - {"label": "Solid Reference", "to": "framework/solid/reference/index"}, - {"label": "Functions / createField", "to": "framework/solid/reference/createfield"}, - {"label": "Functions / createForm", "to": "framework/solid/reference/createform"}, - {"label": "Functions / Field", "to": "framework/solid/reference/field"}, - {"label": "Types / CreateField", "to": "framework/solid/reference/createfield"}, - {"label": "Types / FieldComponent", "to": "framework/solid/reference/fieldcomponent"} + { + "label": "Solid Reference", + "to": "framework/solid/reference/index" + }, + { + "label": "Functions / createField", + "to": "framework/solid/reference/createfield" + }, + { + "label": "Functions / createForm", + "to": "framework/solid/reference/createform" + }, + { + "label": "Functions / Field", + "to": "framework/solid/reference/field" + }, + { + "label": "Types / CreateField", + "to": "framework/solid/reference/createfield" + }, + { + "label": "Types / FieldComponent", + "to": "framework/solid/reference/fieldcomponent" + } ] }, { "label": "lit", "children": [ - {"label": "Lit Reference", "to": "framework/lit/reference/index"}, - {"label": "Classes / TanStackFormController", "to": "framework/lit/reference/tanstackformcontroller"} + { "label": "Lit Reference", "to": "framework/lit/reference/index" }, + { + "label": "Classes / TanStackFormController", + "to": "framework/lit/reference/tanstackformcontroller" + } ] }, { "label": "angular", "children": [ - {"label": "Angular Reference", "to": "framework/angular/reference/index"}, - {"label": "Classes / TanStackField", "to": "framework/angular/reference/tanstackfield"}, - {"label": "Functions / injectForm", "to": "framework/angular/reference/injectform"}, - {"label": "Functions / injectStore", "to": "framework/angular/reference/injectstore"} + { + "label": "Angular Reference", + "to": "framework/angular/reference/index" + }, + { + "label": "Classes / TanStackField", + "to": "framework/angular/reference/tanstackfield" + }, + { + "label": "Functions / injectForm", + "to": "framework/angular/reference/injectform" + }, + { + "label": "Functions / injectStore", + "to": "framework/angular/reference/injectstore" + } ] } ] diff --git a/docs/framework/solid/guides/arrays.md b/docs/framework/solid/guides/arrays.md index 1a3dc218b..8e3cb5292 100644 --- a/docs/framework/solid/guides/arrays.md +++ b/docs/framework/solid/guides/arrays.md @@ -5,7 +5,7 @@ title: Arrays TanStack Form supports arrays as values in a form, including sub-object values inside of an array. -# Basic Usage +## Basic Usage To use an array, you can use `field.state.value` on an array value in conjunction with [`Index` from `solid-js`](https://www.solidjs.com/tutorial/flow_index): diff --git a/docs/framework/solid/guides/async-initial-values.md b/docs/framework/solid/guides/async-initial-values.md new file mode 100644 index 000000000..50604d366 --- /dev/null +++ b/docs/framework/solid/guides/async-initial-values.md @@ -0,0 +1,50 @@ +--- +id: async-initial-values +title: Async Initial Values +--- + +Let's say that you want to fetch some data from an API and use it as the initial value of a form. + +While this problem sounds simple on the surface, there are hidden complexities you might not have thought of thus far. + +For example, you might want to show a loading spinner while the data is being fetched, or you might want to handle errors gracefully. +Likewise, you could also find yourself looking for a way to cache the data so that you don't have to fetch it every time the form is rendered. + +While we could implement many of these features from scratch, it would end up looking a lot like another project we maintain: [TanStack Query](https://tanstack.com/query). + +As such, this guide shows you how you can mix-n-match TanStack Form with TanStack Query to achieve the desired behavior. + +## Basic Usage + +```tsx +import { createForm } from '@tanstack/solid-form'; +import { createQuery } from '@tanstack/solid-query'; + +export default function App() { + const { data, isLoading } = createQuery(() => ({ + queryKey: ['data'], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return { firstName: 'FirstName', lastName: 'LastName' }; + }, + })); + + const form = createForm(() => ({ + defaultValues: { + firstName: data?.firstName ?? '', + lastName: data?.lastName ?? '', + }, + onSubmit: async ({ value }) => { + // Do something with form data + console.log(value); + }, + })); + + if (isLoading) return

Loading..

; + + return null; +} + +``` + +This will show a loading spinner until the data is fetched, and then it will render the form with the fetched data as the initial values. diff --git a/docs/framework/solid/guides/basic-concepts.md b/docs/framework/solid/guides/basic-concepts.md new file mode 100644 index 000000000..eb3cf2a13 --- /dev/null +++ b/docs/framework/solid/guides/basic-concepts.md @@ -0,0 +1,288 @@ +--- +id: basic-concepts +title: Basic Concepts and Terminology +--- + +This page introduces the basic concepts and terminology used in the `@tanstack/solid-form` library. Familiarizing yourself with these concepts will help you better understand and work with the library. + +## Form Options + +You can create options for your form so that it can be shared between multiple forms by using the `formOptions` function. + +Example: + +```tsx +const formOpts = formOptions({ + defaultValues: { + firstName: '', + lastName: '', + hobbies: [], + }, +}) +``` + +## Form Instance + +A Form Instance is an object that represents an individual form and provides methods and properties for working with the form. You create a form instance using the `createForm` hook provided by the form options. The hook accepts an object with an `onSubmit` function, which is called when the form is submitted. + +```tsx +const form = createForm(() => ({ + ...formOpts, + onSubmit: async ({ value }) => { + // Do something with form data + console.log(value); + }, +})) +``` + +You may also create a form instance without using `formOptions` by using the standalone `createForm` API: + +```tsx +const form = createForm(() => ({ + onSubmit: async ({ value }) => { + // Do something with form data + console.log(value); + }, + defaultValues: { + firstName: '', + lastName: '', + hobbies: [], + }, +})) +``` + +## Field + +A Field represents a single form input element, such as a text input or a checkbox. Fields are created using the `form.Field` component provided by the form instance. The component accepts a name prop, which should match a key in the form's default values. It also accepts a children prop, which is a render prop function that takes a field object as its argument. + +Example: + +```tsx + ( + field().handleChange(e.target.value)} + /> + )} +/> +``` + +## Field State + +Each field has its own state, which includes its current value, validation status, error messages, and other metadata. You can access a field's state using the `field().state` property. + +Example: + +```tsx +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. + +```tsx +const { isTouched, isPristine, isDirty } = field().state.meta +``` + +![Field states](https://raw.githubusercontent.com/TanStack/form/main/docs/assets/field-states.png) + +## Field API + +The Field API is an object passed to the render prop function when creating a field. It provides methods for working with the field's state. + +Example: + +```tsx + field().handleChange(e.target.value)} +/> +``` + +## Validation + +`@tanstack/solid-form` provides both synchronous and asynchronous validation out of the box. Validation functions can be passed to the `form.Field` component using the `validators` prop. + +Example: + +```tsx + + !value + ? 'A first name is required' + : value.length < 3 + ? 'First name must be at least 3 characters' + : undefined, + onChangeAsync: async ({ value }) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return value.includes('error') && 'No "error" allowed in first name'; + }, + }} + children={(field) => ( + <> + field().handleChange(e.target.value)} + /> +

{field().state.meta.errors[0]}

+ + )} +/> +``` + +## Validation Adapters + +In addition to hand-rolled validation options, we also provide adapters like `@tanstack/zod-form-adapter`, `@tanstack/yup-form-adapter`, and `@tanstack/valibot-form-adapter` to enable usage with common schema validation tools like [Zod](https://zod.dev/), [Yup](https://github.com/jquense/yup), and [Valibot](https://valibot.dev/). + +Example: + +```tsx +import { zodValidator } from '@tanstack/zod-form-adapter' +import { z } from 'zod' + +// ... + { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return !value.includes('error'); + }, + { + message: 'No "error" allowed in first name', + } + ), + }} + children={(field) => ( + <> + field().handleChange(e.target.value)} + /> +

{field().state.meta.errors[0]}

+ + )} +/> +``` + +## Reactivity + +`@tanstack/solid-form` offers various ways to subscribe to form and field state changes, most notably the `form.useStore` hook and the `form.Subscribe` component. These methods allow you to optimize your form's rendering performance by only updating components when necessary. + +Example: + +```tsx +const firstName = form.useStore((state) => state.values.firstName) +//... + ({ + canSubmit: state.canSubmit, + isSubmitting: state.isSubmitting, + })} + children={(state) => ( + + )} +/> +``` + +## Array Fields + +Array fields allow you to manage a list of values within a form, such as a list of hobbies. You can create an array field using the `form.Field` component with the `mode="array"` prop. + +When working with array fields, you can use the fields `pushValue`, `removeValue`, `swapValues` and `moveValue` methods to add, remove, and swap values in the array. + +Example: + +```tsx + ( +
+ Hobbies +
+ 0} + fallback={'No hobbies found.'} + > + + {(_, i) => ( +
+ ( +
+ + field().handleChange(e.target.value)} + /> + +
+ )} + /> + { + return ( +
+ + field().handleChange(e.target.value)} + /> +
+ ); + }} + /> +
+ )} +
+
+
+ +
+ )} +/> +``` + +These are the basic concepts and terminology used in the `@tanstack/solid-form` library. Understanding these concepts will help you work more effectively with the library and create complex forms with ease. diff --git a/docs/framework/solid/guides/linked-fields.md b/docs/framework/solid/guides/linked-fields.md new file mode 100644 index 000000000..9c9757208 --- /dev/null +++ b/docs/framework/solid/guides/linked-fields.md @@ -0,0 +1,77 @@ +--- +id: linked-fields +title: Link Two Form Fields Together +--- + +You may find yourself needing to link two fields together; when one is validated as another field's value has changed. +One such usage is when you have both a `password` and `confirm_password` field, +where you want to `confirm_password` to error out when `password`'s value does not match; +regardless of which field triggered the value change. + +Imagine the following userflow: + +- User updates confirm password field. +- User updates the non-confirm password field. + +In this example, the form will still have errors present, +as the "confirm password" field validation has not been re-ran to mark as accepted. + +To solve this, we need to make sure that the "confirm password" validation is re-run when the password field is updated. +To do this, you can add a `onChangeListenTo` property to the `confirm_password` field. + +```tsx +export default function App() { + const form = createForm(() => ({ + defaultValues: { + password: '', + confirm_password: '', + }, + // ... + })); + + return ( +
+ + {(field) => ( + + )} + + { + if (value !== fieldApi.form.getFieldValue('password')) { + return 'Passwords do not match'; + } + return undefined; + }, + }} + > + {(field) => ( +
+ + + {(err) =>
{err()}
} +
+
+ )} +
+
+ ); +} +``` + +This similarly works with `onBlurListenTo` property, which will re-run the validation when the field is blurred. diff --git a/docs/framework/solid/guides/validation.md b/docs/framework/solid/guides/validation.md new file mode 100644 index 000000000..354b721bb --- /dev/null +++ b/docs/framework/solid/guides/validation.md @@ -0,0 +1,406 @@ +--- +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 `` component accepts some callbacks as props such as `onChange` or `onBlur`. Those callbacks are passed the current value of the field, as well as the fieldAPI object, so that you can perform the validation. If you find a validation error, simply return the error message as string and it will be available in `field().state.meta.errors`. + +Here is an example: + +```tsx + + value < 13 ? 'You must be 13 to make an account' : undefined, + }} +> + {(field) => ( + <> + + field().handleChange(e.target.valueAsNumber)} + /> + {field().state.meta.errors ? ( + {field().state.meta.errors.join(', ')} + ) : null} + + )} + +``` + +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: + +```tsx + + value < 13 ? 'You must be 13 to make an account' : undefined, + }} +> + {(field) => ( + <> + + field().handleChange(e.target.valueAsNumber)} + /> + {field().state.meta.errors ? ( + {field().state.meta.errors.join(', ')} + ) : null} + + )} + +``` + +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: + +```tsx + + value < 13 ? 'You must be 13 to make an account' : undefined, + onBlur: ({ value }) => (value < 0 ? 'Invalid value' : undefined), + }} +> + {(field) => ( + <> + + field().handleChange(e.target.valueAsNumber)} + /> + {field().state.meta.errors ? ( + {field().state.meta.errors.join(', ')} + ) : null} + + )} + +``` + +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: + +```tsx + + value < 13 ? 'You must be 13 to make an account' : undefined, + }} +> + {(field) => { + return ( + <> + {/* ... */} + {field().state.meta.errors.length ? ( + {field().state.meta.errors.join(',')} + ) : null} + + ); + }} + +``` + +Or use the `errorMap` property to access the specific error you're looking for: + +```tsx + + value < 13 ? 'You must be 13 to make an account' : undefined, + }} +> + {(field) => ( + <> + {/* ... */} + {field().state.meta.errorMap['onChange'] ? ( + {field().state.meta.errorMap['onChange']} + ) : null} + + )} + +``` + +## Validation at field level vs at form level + +As shown above, each `` 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 `createForm()` hook. + +Example: + +```tsx +export default function App() { + const form = createForm(() => ({ + 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; + }, + }, + })); + + // Subscribe to the form's error map so that updates to it will render + // alternately, you can use `form.Subscribe` + const formErrorMap = form.useStore((state) => state.errorMap); + + return ( +
+ {/* ... */} + {formErrorMap().onChange ? ( +
+ There was an error on the form: {formErrorMap().onChange} +
+ ) : null} + {/* ... */} +
+ ); +} +``` + +## 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: + +```tsx + { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return value < 13 ? 'You must be 13 to make an account' : undefined; + }, + }} +> + {(field) => ( + <> + + field().handleChange(e.target.valueAsNumber)} + /> + {field().state.meta.errors ? ( + {field().state.meta.errors.join(', ')} + ) : null} + + )} + +``` + +Synchronous and Asynchronous validations can coexist. For example, it is possible to define both `onBlur` and `onBlurAsync` on the same field: + +```tsx + (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) => ( + <> + + field().handleChange(e.target.valueAsNumber)} + /> + {field().state.meta.errors ? ( + {field().state.meta.errors.join(', ')} + ) : null} + + )} + +``` + +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: + +```tsx + { + // ... + } + }} + children={(field) => { + return ( + <> + {/* ... */} + + ); + }} +/> +``` + +This will debounce every async call with a 500ms delay. You can even override this property on a per-validation property: + +```tsx + { + // ... + }, + onBlurAsync: async ({ value }) => { + // ... + }, + }} + children={(field) => { + return <>{/* ... */} + }} +/> +``` + +> This will run `onChangeAsync` every 1500ms while `onBlurAsync` will run every 500ms. + +## Adapter-Based Validation (Zod, Yup, Valibot) + +While functions provide more flexibility and customization over your validation, they can be a bit verbose. To help solve this, there are libraries like [Valibot](https://valibot.dev/), [Yup](https://github.com/jquense/yup), and [Zod](https://zod.dev/) that provide schema-based validation to make shorthand and type-strict validation substantially easier. + +Luckily, we support all of these libraries through official adapters: + +```bash +$ npm install @tanstack/zod-form-adapter zod +# or +$ npm install @tanstack/yup-form-adapter yup +# or +$ npm install @tanstack/valibot-form-adapter valibot +``` + +Once done, we can add the adapter to the `validator` property on the form or field: + +```tsx +import { zodValidator } from '@tanstack/zod-form-adapter' +import { z } from 'zod' + +// ... + +const form = createForm(() => ({ + // Either add the validator here or on `Field` + validatorAdapter: zodValidator(), + // ... +})); + + { + return <>{/* ... */} + }} +/> +``` + +These adapters also support async operations using the proper property names: + +```tsx + { + const currentAge = await fetchCurrentAgeOnProfile() + return value >= currentAge + }, + { + message: 'You can only increase the age', + }, + ), + }} + children={(field) => { + return <>{/* ... */} + }} +/> +``` + +## 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 subscribe to it via `form.Subscribe` 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). + +```tsx +const form = createForm(() => ({/* ... */})) + +return ( + /* ... */ + + // Dynamic submit button + ({ + canSubmit: state.canSubmit, + isSubmitting: state.isSubmitting, + })} + children={(state) => ( + + )} + /> +) +```