mirror of
https://github.com/LukeHagar/form.git
synced 2025-12-06 04:19:43 +00:00
feat: Add Yup and Zod validator support (#462)
* chore: first pass * chore: onto something I think * chore: closer but no cigar * chore: infer validator * chore: infer zod * feat: add validation transformer logic * chore: fix typings for react adapter * chore: fix issue with `this` not being defined properly * chore: mostly update FieldInfo types from Vue * chore: work on fixing type inferencing * fix: make ValidatorType optional * chore: make TName restriction easier to grok * chore: fix React types * chore: fix Vue types * chore: fix typing issues * chore: fix various linting items * chore: fix ESlint and validation logic * chore: fix inferencing from formdata * chore: fix form inferencing * chore: fix React TS types to match form validator logic * chore: fix Vue types * chore: migrate zod validation to dedicated package * chore: add first integration test for zod adapter * chore: enable non-validator types to be passed to validator * feat: add yup 1.x adapter * chore: add functionality and tests for form-wide validators * chore: fix typings of async validation types * fix: async validation should now run as-expected more often * chore: add async tests for Yup validator * chore: rename packages to match naming schema better * chore: add Zod examples for React and Vue * chore: add React and Vue Yup support * chore: fix formatting * chore: fix CI types * chore: initial work to drastically improve docs * docs: improve docs for validation * docs: add adapter validation docs
This commit is contained in:
@@ -30,8 +30,12 @@
|
||||
"label": "Guides",
|
||||
"children": [
|
||||
{
|
||||
"label": "Important Defaults",
|
||||
"to": "guides/important-defaults"
|
||||
"label": "Basic Concepts",
|
||||
"to": "guides/basic-concepts"
|
||||
},
|
||||
{
|
||||
"label": "Form Validation",
|
||||
"to": "guides/validation"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -89,6 +93,14 @@
|
||||
{
|
||||
"label": "Simple",
|
||||
"to": "framework/react/examples/simple"
|
||||
},
|
||||
{
|
||||
"label": "Yup",
|
||||
"to": "framework/react/examples/yup"
|
||||
},
|
||||
{
|
||||
"label": "Zod",
|
||||
"to": "framework/react/examples/zod"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -133,6 +145,14 @@
|
||||
{
|
||||
"label": "Simple",
|
||||
"to": "framework/vue/examples/simple"
|
||||
},
|
||||
{
|
||||
"label": "Yup",
|
||||
"to": "framework/vue/examples/yup"
|
||||
},
|
||||
{
|
||||
"label": "Zod",
|
||||
"to": "framework/vue/examples/zod"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ title: Basic Concepts and Terminology
|
||||
|
||||
# Basic Concepts and Terminology
|
||||
|
||||
> Some of these docs may be inaccurate due to an API shift in `0.11.0`. If you're interested in helping us fix these issues, please [join our Discord](https://tlinz.com/discord) and reach out in the `#form` channel.
|
||||
|
||||
This page introduces the basic concepts and terminology used in the @tanstack/react-form library. Familiarizing yourself with these concepts will help you better understand and work with the library.
|
||||
This page introduces the basic concepts and terminology used in the `@tanstack/react-form` library. Familiarizing yourself with these concepts will help you better understand and work with the library.
|
||||
|
||||
## Form Factory
|
||||
|
||||
@@ -86,7 +84,7 @@ Example:
|
||||
|
||||
## Validation
|
||||
|
||||
@tanstack/react-form provides both synchronous and asynchronous validation out of the box. Validation functions can be passed to the form.Field component using the validate and validateAsync props.
|
||||
`@tanstack/react-form` provides both synchronous and asynchronous validation out of the box. Validation functions can be passed to the form.Field component using the validate and validateAsync props.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -111,9 +109,34 @@ Example:
|
||||
/>
|
||||
```
|
||||
|
||||
## Validation Adapters
|
||||
|
||||
In addition to hand-rolled validation options, we also provide adapters like `@tanstack/zod-form-adapter` and `@tanstack/yup-form-adapter` to enable usage with common schema validation tools like [Yup](https://github.com/jquense/yup) and [Zod](https://zod.dev/).
|
||||
|
||||
Example:
|
||||
|
||||
```tsx
|
||||
<form.Field
|
||||
name="firstName"
|
||||
onChange={z
|
||||
.string()
|
||||
.min(3, "First name must be at least 3 characters")}
|
||||
onChangeAsyncDebounceMs={500}
|
||||
onChangeAsync={z.string().refine(
|
||||
async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return !value.includes("error");
|
||||
},
|
||||
{
|
||||
message: "No 'error' allowed in first name",
|
||||
},
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
## Reactivity
|
||||
|
||||
@tanstack/react-form offers various ways to subscribe to form and field state changes, such as the form.useStore hook, the form.Subscribe component, and the form.useField hook. These methods allow you to optimize your form's rendering performance by only updating components when necessary.
|
||||
`@tanstack/react-form` offers various ways to subscribe to form and field state changes, such as the `form.useStore` hook, the `form.Subscribe` component, and the `form.useField` hook. These methods allow you to optimize your form's rendering performance by only updating components when necessary.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -240,8 +263,5 @@ Example:
|
||||
/>
|
||||
```
|
||||
|
||||
These are the basic concepts and terminology used in the @tanstack/react-form library. Understanding these concepts will help you work more effectively with the library and create complex forms with ease.
|
||||
These are the basic concepts and terminology used in the `@tanstack/react-form` library. Understanding these concepts will help you work more effectively with the library and create complex forms with ease.
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
id: important-defaults
|
||||
title: Important Defaults
|
||||
---
|
||||
|
||||
> Some of these docs may be inaccurate due to an API shift in `0.11.0`. If you're interested in helping us fix these issues, please [join our Discord](https://tlinz.com/discord) and reach out in the `#form` channel.
|
||||
|
||||
Out of the box, TanStack Form is configured with **aggressive but sane** defaults. **Sometimes these defaults can catch new users off guard or make learning/debugging difficult if they are unknown by the user.** Keep them in mind as you continue to learn and use TanStack Form:
|
||||
|
||||
- Core
|
||||
- Validation
|
||||
- By default, only touched, dirty fields are validated on blur and submit. This means that if you have a field that is required, it will not show an error until the user has blurred the field (focused and unfocused) or submitted the form. You can change this to include field `change` events or only the `submit` event with a form default or on a per field basis with the `defaultValidateOn` and `validateOn` options. `validatePristine` can also be used to validate pristine fields.
|
||||
- By default, TanStack async validation will only run if synchronous validation succeeds. This is to prevent unnecessary async validation which usually are powered by network requests.
|
||||
- By default, TanStack Form will not validate fields that are not registered. This is to prevent unnecessary validation of fields that are not in the DOM. This can be changed with the `validateUnregistered` option.
|
||||
- React
|
||||
- Reactivity
|
||||
- The `useForm` hook and `form.Form` component are not reactive, which means that as _any_ form state changes, they will not rerender. If they did, you would likely run into performance problems very quickly. Instead, use:
|
||||
- `form.useStore` to subscribe and rerender when form state changes. Selectors are supported.
|
||||
- `form.Subscribe` to render a specific sub-tree of UI that is subscribed to the form state. Selectors are supported.
|
||||
- `form.useField` to subscribe to a specific field's state changes.
|
||||
- `form.Field` **is reactive** to all state changes that happen within a field. This means that as a field's value, error, touched, etc. changes, the component you use it in will rerender. This is a good thing because it means you don't have to worry about manually subscribing to a field's state changes.
|
||||
280
docs/guides/validation.md
Normal file
280
docs/guides/validation.md
Normal file
@@ -0,0 +1,280 @@
|
||||
---
|
||||
id: form-validation
|
||||
title: Form and Field Validation
|
||||
---
|
||||
|
||||
# Form and Field Validation
|
||||
|
||||
At the core of TanStack Form's functionalities is the concept of validation. We currently support three mechanisms of validation:
|
||||
|
||||
- Synchronous functional validation
|
||||
- Asynchronous functional validation
|
||||
- Adapter-based validation
|
||||
|
||||
Let's take a look at each and see how they're built.
|
||||
|
||||
## Synchronous Functional Validation
|
||||
|
||||
With Form, you can pass a function to a field and, if it returns a string, said string will be used as the error:
|
||||
|
||||
```tsx
|
||||
<form.Field
|
||||
name="age"
|
||||
onChange={val => val < 13 ? "You must be 13 to make an account" : undefined}
|
||||
children={(field) => {
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={field.name}>First Name:</label>
|
||||
<input
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
type="number"
|
||||
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
|
||||
/>
|
||||
{field.state.meta.touchedErrors ? (
|
||||
<em>{field.state.meta.touchedErrors}</em>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Displaying Errors
|
||||
|
||||
Once you have your validation in place, you can map the errors from an array to be displayed in your UI:
|
||||
|
||||
```tsx
|
||||
<form.Field
|
||||
name="age"
|
||||
onChange={val => val < 13 ? "You must be 13 to make an account" : undefined}
|
||||
children={(field) => {
|
||||
return (
|
||||
<>
|
||||
{/* ... */}
|
||||
{field.state.meta.errors ? (
|
||||
<em>{field.state.meta.errors}</em>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
Or use the `errorMap` property to access the specific error you're looking for:
|
||||
|
||||
```tsx
|
||||
<form.Field
|
||||
name="age"
|
||||
onChange={val => val < 13 ? "You must be 13 to make an account" : undefined}
|
||||
children={(field) => {
|
||||
return (
|
||||
<>
|
||||
{/* ... */}
|
||||
{field.state.meta.errorMap['onChange'] ? (
|
||||
<em>{field.state.meta.errorMap['onChange']}</em>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Using Alternative Validation Steps
|
||||
|
||||
One of the great benefits of using TanStack Form is that you're not locked into a specific method of validation. For example, if you want to validate a specific field on blur rather than on text change, you can change `onChange` to `onBlur`:
|
||||
|
||||
```tsx
|
||||
<form.Field
|
||||
name="age"
|
||||
onBlur={val => val < 13 ? "You must be 13 to make an account" : undefined}
|
||||
children={(field) => {
|
||||
return (
|
||||
<>
|
||||
{/* ... */}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Asynchronous Functional Validation
|
||||
|
||||
While we suspect most validations will be synchronous, there's 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
|
||||
<form.Field
|
||||
name="firstName"
|
||||
onChangeAsync={async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return (
|
||||
value.includes("error") && 'No "error" allowed in first name'
|
||||
);
|
||||
}}
|
||||
children={(field) => {
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={field.name}>First Name:</label>
|
||||
<input
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
<FieldInfo field={field} />
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
This can be combined with the respective synchronous properties as well:
|
||||
|
||||
``` tsx
|
||||
<form.Field
|
||||
name="firstName"
|
||||
onChange={(value) =>
|
||||
!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) => {
|
||||
return (
|
||||
<>
|
||||
{/* ... */}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### 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
|
||||
<form.Field
|
||||
name="firstName"
|
||||
asyncDebounceMs={500}
|
||||
onChangeAsync={async (value) => {
|
||||
// ...
|
||||
}}
|
||||
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
|
||||
<form.Field
|
||||
name="firstName"
|
||||
asyncDebounceMs={500}
|
||||
onChangeAsyncDebounceMs={1500}
|
||||
onChangeAsync={async (value) => {
|
||||
// ...
|
||||
}}
|
||||
onBlurAsync={async (value) => {
|
||||
// ...
|
||||
}}
|
||||
children={(field) => {
|
||||
return (
|
||||
<>
|
||||
{/* ... */}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
> This will run `onChangeAsync` every 1500ms while `onBlurAsync` will run every 500ms.
|
||||
|
||||
|
||||
## Adapter-Based Validation
|
||||
|
||||
While functions provide more flexibility and customization over your validation, they can be a bit verbose. To help solve this, there are libraries like [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 both of these libraries through official adapters:
|
||||
|
||||
```bash
|
||||
$ npm install @tanstack/zod-form-adapter zod
|
||||
# or
|
||||
$ npm install @tanstack/yup-form-adapter yup
|
||||
```
|
||||
|
||||
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 = useForm({
|
||||
// Either add the validator here or on `Field`
|
||||
validator: zodValidator,
|
||||
// ...
|
||||
});
|
||||
|
||||
<form.Field
|
||||
name="firstName"
|
||||
validator={zodValidator}
|
||||
onChange={z
|
||||
.string()
|
||||
.min(3, "First name must be at least 3 characters")}
|
||||
children={(field) => {
|
||||
return (
|
||||
<>
|
||||
{/* ... */}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
These adapters also support async operations using the proper property names:
|
||||
|
||||
```tsx
|
||||
<form.Field
|
||||
name="firstName"
|
||||
onChange={z
|
||||
.string()
|
||||
.min(3, "First name must be at least 3 characters")}
|
||||
onChangeAsyncDebounceMs={500}
|
||||
onChangeAsync={z.string().refine(
|
||||
async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return !value.includes("error");
|
||||
},
|
||||
{
|
||||
message: "No 'error' allowed in first name",
|
||||
},
|
||||
)}
|
||||
children={(field) => {
|
||||
return (
|
||||
<>
|
||||
{/* ... */}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
@@ -3,11 +3,20 @@ id: installation
|
||||
title: Installation
|
||||
---
|
||||
|
||||
While the core of TanStack Form is framework-agnostic and will be compatible with various front-end frameworks in the future, we only support React today.
|
||||
To use TanStack Form with React, install the adapter via NPM
|
||||
TanStack Form is compatible with various front-end frameworks, including React and Vue. To use TanStack Form with your desired framework, install the corresponding adapter via NPM:
|
||||
|
||||
```bash
|
||||
$ npm i @tanstack/react-form
|
||||
# or
|
||||
$ pnpm add @tanstack/vue-form
|
||||
```
|
||||
|
||||
> Depending on your environment, you might need to add polyfills. If you want to support older browsers, you need to transpile the library from `node_modules` yourselves.
|
||||
|
||||
In addition, we support both Zod and Yup as validators through official validator packages:
|
||||
|
||||
```bash
|
||||
$ yarn add @tanstack/zod-form-adapter zod
|
||||
# or
|
||||
$ npm i @tanstack/yup-form-adapter yup
|
||||
```
|
||||
|
||||
@@ -11,24 +11,44 @@ Normally, you will not need to create a new `FieldApi` instance directly. Instea
|
||||
const fieldApi: FieldApi<TData> = new FieldApi(formOptions: Field Options<TData>)
|
||||
```
|
||||
|
||||
### `FieldOptions<TData, TParentData>`
|
||||
### `FieldOptions<TParentData, TName, ValidatorType, FormValidator, TData>`
|
||||
|
||||
An object type representing the options for a field in a form.
|
||||
|
||||
- ```tsx
|
||||
name
|
||||
name: TName
|
||||
```
|
||||
- The field name. If `TParentData` is `unknown`, the type will be `string`. Otherwise, it will be `DeepKeys<TParentData>`.
|
||||
- The field name. The type will be `DeepKeys<TParentData>` to ensure your name is a deep key of the parent dataset.
|
||||
|
||||
- ```tsx
|
||||
defaultValue?: TData
|
||||
```
|
||||
- An optional default value for the field.
|
||||
|
||||
- ```tsx
|
||||
defaultMeta?: Partial<FieldMeta>
|
||||
```
|
||||
|
||||
- An optional object with default metadata for the field.
|
||||
|
||||
- ```tsx
|
||||
asyncDebounceMs?: number
|
||||
```
|
||||
|
||||
- The default time to debounce async validation if there is not a more specific debounce time passed.
|
||||
|
||||
- ```tsx
|
||||
asyncAlways?: boolean
|
||||
```
|
||||
|
||||
- If `true`, always run async validation, even if there are errors emitted during synchronous validation.
|
||||
|
||||
- ```typescript
|
||||
validator?: ValidatorType
|
||||
```
|
||||
|
||||
- A validator provided by an extension, like `yupValidator` from `@tanstack/yup-form-adapter`
|
||||
|
||||
- ```tsx
|
||||
onMount?: (formApi: FieldApi<TData, TParentData>) => void
|
||||
```
|
||||
@@ -36,35 +56,35 @@ An object type representing the options for a field in a form.
|
||||
- An optional function that takes a param of `formApi` which is a generic type of `TData` and `TParentData`
|
||||
|
||||
- ```tsx
|
||||
onChange?: ValidateFn<TData, TParentData>
|
||||
onChange?: ValidateFn<TData, TParentData>
|
||||
```
|
||||
|
||||
- An optional property that takes a `ValidateFn` which is a generic of `TData` and `TParentData`
|
||||
- An optional property that takes a `ValidateFn` which is a generic of `TData` and `TParentData`. If `validator` is passed, this may also accept a property from the respective validator (IE: `z.string().min(1)` if `zodAdapter` is passed)
|
||||
|
||||
- ```tsx
|
||||
onChangeAsync?: ValidateAsyncFn<TData, TParentData>
|
||||
onChangeAsync?: ValidateAsyncFn<TData, TParentData>
|
||||
```
|
||||
|
||||
- An optional property similar to `onChange` but async validation
|
||||
- An optional property similar to `onChange` but async validation. If `validator` is passed, this may also accept a property from the respective validator (IE: `z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' })` if `zodAdapter` is passed)
|
||||
|
||||
- ```tsx
|
||||
onChangeAsyncDebounceMs?: number
|
||||
onChangeAsyncDebounceMs?: number
|
||||
```
|
||||
|
||||
- An optional number to represent how long the `onChangeAsync` should wait before running
|
||||
- If set to a number larger than 0, will debounce the async validation event by this length of time in milliseconds
|
||||
|
||||
- ```tsx
|
||||
onBlur?: ValidateFn<TData, TParentData>
|
||||
onBlur?: ValidateFn<TData, TParentData>
|
||||
```
|
||||
|
||||
- An optional function, when that run when subscribing to blur event of input
|
||||
- An optional function, when that run when subscribing to blur event of input. If `validator` is passed, this may also accept a property from the respective validator (IE: `z.string().min(1)` if `zodAdapter` is passed)
|
||||
|
||||
- ```tsx
|
||||
onBlurAsync?: ValidateAsyncFn<TData, TParentData>
|
||||
onBlurAsync?: ValidateAsyncFn<TData, TParentData>
|
||||
```
|
||||
|
||||
- An optional function that takes a `ValidateFn` which is a generic of `TData` and `TParentData` happens async
|
||||
- An optional function that takes a `ValidateFn` which is a generic of `TData` and `TParentData` happens async. If `validator` is passed, this may also accept a property from the respective validator (IE: `z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' })` if `zodAdapter` is passed)
|
||||
|
||||
```tsx
|
||||
onBlurAsyncDebounceMs?: number
|
||||
@@ -77,13 +97,15 @@ An object type representing the options for a field in a form.
|
||||
onSubmitAsync?: number
|
||||
```
|
||||
|
||||
- If set to a number larger than 0, will debounce the async validation event by this length of time in milliseconds
|
||||
- If set to a number larger than 0, will debounce the async validation event by this length of time in milliseconds. If `validator` is passed, this may also accept a property from the respective validator (IE: `z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' })` if `zodAdapter` is passed)
|
||||
|
||||
### `ValidationCause`
|
||||
|
||||
A type representing the cause of a validation event.
|
||||
|
||||
- 'change' | 'blur' | 'submit' | 'mount'
|
||||
- ```tsx
|
||||
'change' | 'blur' | 'submit' | 'mount'
|
||||
```
|
||||
|
||||
### `FieldMeta`
|
||||
|
||||
@@ -127,7 +149,7 @@ A class representing the API for managing a form field.
|
||||
```
|
||||
- A unique identifier for the field instance.
|
||||
- ```tsx
|
||||
form: FormApi<TParentData>
|
||||
form: FormApi<TParentData, TData>
|
||||
```
|
||||
- A reference to the form API instance.
|
||||
- ```tsx
|
||||
@@ -153,66 +175,77 @@ A class representing the API for managing a form field.
|
||||
constructor(opts: FieldApiOptions<TData, TParentData>)
|
||||
```
|
||||
- Initializes a new `FieldApi` instance.
|
||||
|
||||
- ```tsx
|
||||
mount(): () => void
|
||||
```
|
||||
- Mounts the field instance to the form.
|
||||
- ```tsx
|
||||
updateStore(): void
|
||||
```
|
||||
- Updates the field store with the latest form state.
|
||||
|
||||
- ```tsx
|
||||
update(opts: FieldApiOptions<TData, TParentData>): void
|
||||
```
|
||||
- Updates the field instance with new options.
|
||||
|
||||
- ```tsx
|
||||
getValue(): TData
|
||||
```
|
||||
- Gets the current field value.
|
||||
|
||||
- ```tsx
|
||||
setValue(updater: Updater<TData>, options?: { touch?: boolean; notify?: boolean }): void
|
||||
```
|
||||
- Sets the field value.
|
||||
- Sets the field value and run the `change` validator.
|
||||
|
||||
- ```tsx
|
||||
getMeta(): FieldMeta
|
||||
```
|
||||
- Gets the current field metadata.
|
||||
|
||||
- ```tsx
|
||||
setMeta(updater: Updater<FieldMeta>): void
|
||||
```
|
||||
- Sets the field metadata.
|
||||
|
||||
- ```tsx
|
||||
getInfo(): any
|
||||
```
|
||||
- Gets the field information object.
|
||||
|
||||
- ```tsx
|
||||
pushValue(value: TData): void
|
||||
```
|
||||
- Pushes a new value to the field.
|
||||
|
||||
- ```tsx
|
||||
insertValue(index: number, value: TData): void
|
||||
```
|
||||
- Inserts a value at the specified index.
|
||||
|
||||
- ```tsx
|
||||
removeValue(index: number): void
|
||||
```
|
||||
- Removes a value at the specified index.
|
||||
|
||||
- ```tsx
|
||||
swapValues(aIndex: number, bIndex: number): void
|
||||
```
|
||||
- Swaps the values at the specified indices.
|
||||
|
||||
- ```tsx
|
||||
getSubField<TName extends DeepKeys<TData>>(name: TName): FieldApi<DeepValue<TData, TName>, TParentData>
|
||||
getSubField<TSubName, TSubData>(name: TName): FieldApi<TData, TSubName, ValidatorType, TSubData>
|
||||
```
|
||||
- Gets a subfield instance.
|
||||
|
||||
- ```tsx
|
||||
validate(): Promise<any>
|
||||
```
|
||||
- Validates the field value.
|
||||
|
||||
- ```tsx
|
||||
handleBlur(): void;
|
||||
```
|
||||
- Handles the blur event.
|
||||
|
||||
- ```tsx
|
||||
handleChange(value: TData): void
|
||||
```
|
||||
@@ -231,62 +264,3 @@ An object type representing the state of a field.
|
||||
```
|
||||
- The current metadata of the field.
|
||||
|
||||
### `UserChangeProps<TData>`
|
||||
|
||||
An object type representing the change and blur event handlers for a field.
|
||||
|
||||
- ```tsx
|
||||
onChange?: (value: TData) => void
|
||||
```
|
||||
- An optional function to further handle the change event.
|
||||
- ```tsx
|
||||
onBlur?: (event: any) => void
|
||||
```
|
||||
- An optional function to further handle the blur event.
|
||||
|
||||
### `UserInputProps`
|
||||
|
||||
An object type representing the input event handlers for a field.
|
||||
|
||||
- ```tsx
|
||||
onChange?: (event: any) => void
|
||||
```
|
||||
- An optional function to further handle the change event.
|
||||
- ```tsx
|
||||
onBlur?: (event: any) => void
|
||||
```
|
||||
- An optional function to further handle the blur event.
|
||||
|
||||
### `ChangeProps<TData>`
|
||||
|
||||
An object type representing the change and blur event handlers for a field.
|
||||
|
||||
- ```tsx
|
||||
value: TData
|
||||
```
|
||||
- The current value of the field.
|
||||
- ```tsx
|
||||
onChange: (value: TData) => void
|
||||
```
|
||||
- A function to handle the change event.
|
||||
- ```tsx
|
||||
onBlur: (event: any) => void
|
||||
```
|
||||
- A function to handle the blur event.
|
||||
|
||||
### `InputProps`
|
||||
|
||||
An object type representing the input event handlers for a field.
|
||||
|
||||
- ```tsx
|
||||
value: string
|
||||
```
|
||||
- The current value of the field, coerced to a string.
|
||||
- ```tsx
|
||||
onChange: (event: any) => void
|
||||
```
|
||||
- A function to handle the change event.
|
||||
- ```tsx
|
||||
onBlur: (event: any) => void
|
||||
```
|
||||
- A function to handle the blur event.
|
||||
|
||||
@@ -13,14 +13,14 @@ Normally, you will not need to create a new `FormApi` instance directly. Instead
|
||||
const formApi: FormApi<TData> = new FormApi(formOptions: FormOptions<TData>)
|
||||
```
|
||||
|
||||
### `FormOptions<TData>`
|
||||
### `FormOptions<TData, ValidatorType>`
|
||||
|
||||
An object representing the options for a form.
|
||||
|
||||
- ```tsx
|
||||
defaultValues?: TData
|
||||
```
|
||||
- Set initial values for you form.
|
||||
- Set initial values for yonu form.
|
||||
- ```tsx
|
||||
defaultState?: Partial<FormState<TData>>
|
||||
```
|
||||
@@ -32,7 +32,7 @@ An object representing the options for a form.
|
||||
- Optional time in milliseconds if you want to introduce a delay before firing off an async action.
|
||||
|
||||
- ```tsx
|
||||
onMount?: (values: TData, formApi: FormApi<TData>) => ValidationError
|
||||
onMount?: (values: TData, formApi: FormApi<TData>) => ValidationError
|
||||
```
|
||||
- Optional function that fires as soon as the component mounts.
|
||||
- ```tsx
|
||||
@@ -75,7 +75,7 @@ An object representing the options for a form.
|
||||
- The default time in milliseconds that if set to a number larger than 0, will debounce the async validation event by this length of time in milliseconds.
|
||||
|
||||
- ```tsx
|
||||
onSubmit?: (values: TData, formApi: FormApi<TData>) => any | Promise<any>
|
||||
onSubmit?: (values: TData, formApi: FormApi<TData>) => any | Promise<any>
|
||||
```
|
||||
- A function to be called when the form is submitted, what should happen once the user submits a valid form returns `any` or a promise `Promise<any>`
|
||||
|
||||
@@ -85,7 +85,7 @@ An object representing the options for a form.
|
||||
- Specify an action for scenarios where the user tries to submit an invalid form.
|
||||
|
||||
|
||||
### `FormApi<TFormData>`
|
||||
### `FormApi<TFormData, ValidatorType>`
|
||||
|
||||
A class representing the Form API. It handles the logic and interactions with the form state.
|
||||
|
||||
@@ -136,9 +136,9 @@ A class representing the Form API. It handles the logic and interactions with th
|
||||
- Validates all fields in the form.
|
||||
|
||||
- ```tsx
|
||||
handleSubmit(e: FormSubmitEvent)
|
||||
handleSubmit()
|
||||
```
|
||||
- Handles the form submission event, performs validation, and calls the appropriate onSubmit or onInvalidSubmit callbacks.
|
||||
- Handles the form submission, performs validation, and calls the appropriate onSubmit or onInvalidSubmit callbacks.
|
||||
- ```tsx
|
||||
getFieldValue<TField extends DeepKeys<TFormData>>(field: TField)
|
||||
```
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
"axios": "^0.26.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"zod": "^3.21.4",
|
||||
"@tanstack/form-core": "0.3.7",
|
||||
"@tanstack/vue-form": "0.3.7"
|
||||
"@tanstack/form-core": "0.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import type { FieldApi } from "@tanstack/react-form";
|
||||
|
||||
function FieldInfo({ field }: { field: FieldApi<any, any> }) {
|
||||
function FieldInfo({ field }: { field: FieldApi<any, any, unknown, unknown> }) {
|
||||
return (
|
||||
<>
|
||||
{field.state.meta.touchedErrors ? (
|
||||
|
||||
15
examples/react/yup/.eslintrc.cjs
Normal file
15
examples/react/yup/.eslintrc.cjs
Normal file
@@ -0,0 +1,15 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {
|
||||
extends: ["plugin:react/recommended", "plugin:react-hooks/recommended"],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
rules: {
|
||||
"react/no-children-prop": "off",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
27
examples/react/yup/.gitignore
vendored
Normal file
27
examples/react/yup/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
1
examples/react/yup/.prettierrc
Normal file
1
examples/react/yup/.prettierrc
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
6
examples/react/yup/README.md
Normal file
6
examples/react/yup/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Example
|
||||
|
||||
To run this example:
|
||||
|
||||
- `npm install`
|
||||
- `npm run dev`
|
||||
16
examples/react/yup/index.html
Normal file
16
examples/react/yup/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<title>TanStack Form React Yup Example App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
49
examples/react/yup/package.json
Normal file
49
examples/react/yup/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@tanstack/form-example-react-yup",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index.jsx",
|
||||
"scripts": {
|
||||
"dev": "vite --port=3001",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-form": "0.3.7",
|
||||
"axios": "^0.26.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"yup": "^1.3.2",
|
||||
"@tanstack/form-core": "0.3.7",
|
||||
"@tanstack/yup-form-adapter": "0.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
"vite": "^4.4.9"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"nx": {
|
||||
"implicitDependencies": [
|
||||
"@tanstack/form-core",
|
||||
"@tanstack/react-form",
|
||||
"@tanstack/yup-form-adapter"
|
||||
],
|
||||
"targets": {
|
||||
"test:types": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
examples/react/yup/public/emblem-light.svg
Normal file
13
examples/react/yup/public/emblem-light.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="190px" height="190px" viewBox="0 0 190 190" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
||||
<title>emblem-light</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<path d="M39.7239712,61.3436237 C36.631224,46.362877 35.9675112,34.8727722 37.9666331,26.5293551 C39.1555965,21.5671678 41.3293088,17.5190846 44.6346064,14.5984631 C48.1241394,11.5150478 52.5360327,10.0020122 57.493257,10.0020122 C65.6712013,10.0020122 74.2682602,13.7273214 83.4557246,20.8044264 C87.2031203,23.6910458 91.0924366,27.170411 95.1316515,31.2444746 C95.4531404,30.8310265 95.8165416,30.4410453 96.2214301,30.0806152 C107.64098,19.9149716 117.255245,13.5989272 125.478408,11.1636507 C130.367899,9.715636 134.958526,9.57768202 139.138936,10.983031 C143.551631,12.4664684 147.06766,15.5329489 149.548314,19.8281091 C153.642288,26.9166735 154.721918,36.2310983 153.195595,47.7320243 C152.573451,52.4199112 151.50985,57.5263831 150.007094,63.0593153 C150.574045,63.1277086 151.142416,63.2532808 151.705041,63.4395297 C166.193932,68.2358678 176.453582,73.3937462 182.665021,79.2882839 C186.360669,82.7953831 188.773972,86.6998434 189.646365,91.0218204 C190.567176,95.5836746 189.669313,100.159332 187.191548,104.451297 C183.105211,111.529614 175.591643,117.11221 164.887587,121.534031 C160.589552,123.309539 155.726579,124.917559 150.293259,126.363748 C150.541176,126.92292 150.733521,127.516759 150.862138,128.139758 C153.954886,143.120505 154.618598,154.61061 152.619477,162.954027 C151.430513,167.916214 149.256801,171.964297 145.951503,174.884919 C142.46197,177.968334 138.050077,179.48137 133.092853,179.48137 C124.914908,179.48137 116.31785,175.756061 107.130385,168.678956 C103.343104,165.761613 99.4108655,162.238839 95.3254337,158.108619 C94.9050753,158.765474 94.3889681,159.376011 93.7785699,159.919385 C82.3590198,170.085028 72.744755,176.401073 64.5215915,178.836349 C59.6321009,180.284364 55.0414736,180.422318 50.8610636,179.016969 C46.4483686,177.533532 42.9323404,174.467051 40.4516862,170.171891 C36.3577116,163.083327 35.2780823,153.768902 36.8044053,142.267976 C37.449038,137.410634 38.56762,132.103898 40.1575891,126.339009 C39.5361041,126.276104 38.9120754,126.144816 38.2949591,125.940529 C23.8060684,121.144191 13.5464184,115.986312 7.33497892,110.091775 C3.63933121,106.584675 1.22602752,102.680215 0.353635235,98.3582381 C-0.567176333,93.7963839 0.330686581,89.2207269 2.80845236,84.9287618 C6.89478863,77.8504443 14.4083565,72.2678481 25.1124133,67.8460273 C29.5385143,66.0176154 34.5637208,64.366822 40.1939394,62.8874674 C39.9933393,62.3969171 39.8349374,61.8811235 39.7239712,61.3436237 Z" fill="#002C4B" fill-rule="nonzero" transform="translate(95.000000, 95.000000) scale(-1, 1) translate(-95.000000, -95.000000) "></path>
|
||||
<path d="M80.3968824,64 L109.608177,64 C111.399254,64 113.053521,64.958025 113.944933,66.5115174 L128.577138,92.0115174 C129.461464,93.5526583 129.461464,95.4473417 128.577138,96.9884826 L113.944933,122.488483 C113.053521,124.041975 111.399254,125 109.608177,125 L80.3968824,125 C78.6058059,125 76.9515387,124.041975 76.0601262,122.488483 L61.4279211,96.9884826 C60.543596,95.4473417 60.543596,93.5526583 61.4279211,92.0115174 L76.0601262,66.5115174 C76.9515387,64.958025 78.6058059,64 80.3968824,64 Z M105.987827,70.2765273 C107.779849,70.2765273 109.434839,71.2355558 110.325899,72.7903404 L121.343038,92.0138131 C122.225607,93.5537825 122.225607,95.4462175 121.343038,96.9861869 L110.325899,116.20966 C109.434839,117.764444 107.779849,118.723473 105.987827,118.723473 L84.0172329,118.723473 C82.2252106,118.723473 80.5702207,117.764444 79.6791602,116.20966 L68.6620219,96.9861869 C67.7794521,95.4462175 67.7794521,93.5537825 68.6620219,92.0138131 L79.6791602,72.7903404 C80.5702207,71.2355558 82.2252106,70.2765273 84.0172329,70.2765273 L105.987827,70.2765273 Z M102.080648,77.1414791 L87.9244113,77.1414791 C86.1342282,77.1414791 84.4806439,78.0985567 83.5888998,79.6508285 L83.5888998,79.6508285 L76.4892166,92.0093494 C75.6032319,93.5515958 75.6032319,95.4484042 76.4892166,96.9906506 L76.4892166,96.9906506 L83.5888998,109.349172 C84.4806439,110.901443 86.1342282,111.858521 87.9244113,111.858521 L87.9244113,111.858521 L102.080648,111.858521 C103.870831,111.858521 105.524416,110.901443 106.41616,109.349172 L106.41616,109.349172 L113.515843,96.9906506 C114.401828,95.4484042 114.401828,93.5515958 113.515843,92.0093494 L113.515843,92.0093494 L106.41616,79.6508285 C105.524416,78.0985567 103.870831,77.1414791 102.080648,77.1414791 L102.080648,77.1414791 Z M98.3191856,83.7122186 C100.108028,83.7122186 101.760587,84.6678753 102.652827,86.2183156 L105.983552,92.0060969 C106.87203,93.5500005 106.87203,95.4499995 105.983552,96.9939031 L102.652827,102.781684 C101.760587,104.332125 100.108028,105.287781 98.3191856,105.287781 L91.685874,105.287781 C89.8970316,105.287781 88.2444725,104.332125 87.3522326,102.781684 L84.021508,96.9939031 C83.1330298,95.4499995 83.1330298,93.5500005 84.021508,92.0060969 L87.3522326,86.2183156 C88.2444725,84.6678753 89.8970316,83.7122186 91.685874,83.7122186 L98.3191856,83.7122186 Z M95.0037937,90.1848875 C93.459294,90.1848875 92.0343817,91.0072828 91.2630046,92.3424437 C90.4917325,93.6774232 90.4917325,95.3225768 91.2630046,96.6575563 C92.0343817,97.9927172 93.459294,98.8151125 95.0012659,98.8151125 L95.0012659,98.8151125 C96.5457656,98.8151125 97.9706779,97.9927172 98.7420549,96.6575563 C99.5133271,95.3225768 99.5133271,93.6774232 98.7420549,92.3424437 C97.9706779,91.0072828 96.5457656,90.1848875 95.0037937,90.1848875 L95.0037937,90.1848875 Z M60,94.5009646 L67.7677636,94.5009646" fill="#FFD94C"></path>
|
||||
<path d="M54.8601729,108.357758 C56.1715224,107.608286 57.8360246,108.074601 58.5779424,109.399303 L58.5779424,109.399303 L59.0525843,110.24352 C62.8563392,116.982993 66.8190116,123.380176 70.9406016,129.435068 C75.8078808,136.585427 81.28184,143.82411 87.3624792,151.151115 C88.3168778,152.30114 88.1849437,154.011176 87.065686,154.997937 L87.065686,154.997937 L86.4542085,155.534625 C66.3465389,173.103314 53.2778188,176.612552 47.2480482,166.062341 C41.3500652,155.742717 43.4844915,136.982888 53.6513274,109.782853 C53.876818,109.179582 54.3045861,108.675291 54.8601729,108.357758 Z M140.534177,129.041504 C141.986131,128.785177 143.375496,129.742138 143.65963,131.194242 L143.65963,131.194242 L143.812815,131.986376 C148.782365,157.995459 145.283348,171 133.315764,171 C121.609745,171 106.708724,159.909007 88.6127018,137.727022 C88.2113495,137.235047 87.9945723,136.617371 88,135.981509 C88.013158,134.480686 89.2357854,133.274651 90.730918,133.287756 L90.730918,133.287756 L91.6846544,133.294531 C99.3056979,133.335994 106.714387,133.071591 113.910723,132.501323 C122.409039,131.82788 131.283523,130.674607 140.534177,129.041504 Z M147.408726,73.8119663 C147.932139,72.4026903 149.508386,71.6634537 150.954581,72.149012 L150.954581,72.149012 L151.742552,72.4154854 C177.583763,81.217922 187.402356,90.8916805 181.198332,101.436761 C175.129904,111.751366 157.484347,119.260339 128.26166,123.963678 C127.613529,124.067994 126.948643,123.945969 126.382735,123.618843 C125.047025,122.846729 124.602046,121.158214 125.388848,119.847438 L125.388848,119.847438 L125.889328,119.0105 C129.877183,112.31633 133.481358,105.654262 136.701854,99.0242957 C140.50501,91.1948179 144.073967,82.7907081 147.408726,73.8119663 Z M61.7383398,66.0363218 C62.3864708,65.9320063 63.0513565,66.0540315 63.6172646,66.3811573 C64.9529754,67.153271 65.3979538,68.8417862 64.6111517,70.1525615 L64.6111517,70.1525615 L64.1106718,70.9895001 C60.1228168,77.6836699 56.5186416,84.3457379 53.2981462,90.9757043 C49.49499,98.8051821 45.9260328,107.209292 42.5912744,116.188034 C42.0678608,117.59731 40.4916142,118.336546 39.045419,117.850988 L39.045419,117.850988 L38.2574475,117.584515 C12.4162372,108.782078 2.59764398,99.1083195 8.80166786,88.5632391 C14.8700957,78.2486335 32.515653,70.7396611 61.7383398,66.0363218 Z M103.545792,34.4653746 C123.653461,16.8966864 136.722181,13.3874478 142.751952,23.9376587 C148.649935,34.2572826 146.515508,53.0171122 136.348673,80.2171474 C136.123182,80.8204179 135.695414,81.324709 135.139827,81.6422422 C133.828478,82.3917144 132.163975,81.9253986 131.422058,80.6006966 L131.422058,80.6006966 L130.947416,79.7564798 C127.143661,73.0170065 123.180988,66.6198239 119.059398,60.564932 C114.192119,53.4145727 108.71816,46.1758903 102.637521,38.8488847 C101.683122,37.6988602 101.815056,35.9888243 102.934314,35.0020629 L102.934314,35.0020629 Z M57.6842361,18 C69.3902551,18 84.2912758,29.0909926 102.387298,51.2729777 C102.788651,51.7649527 103.005428,52.3826288 103,53.0184911 C102.986842,54.5193144 101.764215,55.7253489 100.269082,55.7122445 L100.269082,55.7122445 L99.3153456,55.7054689 C91.6943021,55.6640063 84.2856126,55.9284091 77.0892772,56.4986773 C68.5909612,57.17212 59.7164767,58.325393 50.4658235,59.9584962 C49.0138691,60.2148231 47.6245044,59.2578618 47.3403697,57.805758 L47.3403697,57.805758 L47.1871846,57.0136235 C42.2176347,31.0045412 45.7166519,18 57.6842361,18 Z" fill="#FF4154"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
114
examples/react/yup/src/index.tsx
Normal file
114
examples/react/yup/src/index.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import * as React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import { yupValidator } from "@tanstack/yup-form-adapter";
|
||||
import * as yup from "yup";
|
||||
import type { FieldApi } from "@tanstack/react-form";
|
||||
|
||||
function FieldInfo({ field }: { field: FieldApi<any, any, unknown, unknown> }) {
|
||||
return (
|
||||
<>
|
||||
{field.state.meta.touchedErrors ? (
|
||||
<em>{field.state.meta.touchedErrors}</em>
|
||||
) : null}
|
||||
{field.state.meta.isValidating ? "Validating..." : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const form = useForm({
|
||||
// Memoize your default values to prevent re-renders
|
||||
defaultValues: {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
// Do something with form data
|
||||
console.log(values);
|
||||
},
|
||||
// Add a validator to support Yup usage in Form and Field
|
||||
validator: yupValidator,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Simple Form Example</h1>
|
||||
{/* A pre-bound form component */}
|
||||
<form.Provider>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{/* A type-safe and pre-bound field component*/}
|
||||
<form.Field
|
||||
name="firstName"
|
||||
onChange={yup
|
||||
.string()
|
||||
.min(3, "First name must be at least 3 characters")}
|
||||
onChangeAsyncDebounceMs={500}
|
||||
onChangeAsync={yup
|
||||
.string()
|
||||
.test(
|
||||
"no error",
|
||||
"No 'error' allowed in first name",
|
||||
async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return !value.includes("error");
|
||||
},
|
||||
)}
|
||||
children={(field) => {
|
||||
// Avoid hasty abstractions. Render props are great!
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={field.name}>First Name:</label>
|
||||
<input
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
<FieldInfo field={field} />
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<form.Field
|
||||
name="lastName"
|
||||
children={(field) => (
|
||||
<>
|
||||
<label htmlFor={field.name}>Last Name:</label>
|
||||
<input
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
<FieldInfo field={field} />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<form.Subscribe
|
||||
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
||||
children={([canSubmit, isSubmitting]) => (
|
||||
<button type="submit" disabled={!canSubmit}>
|
||||
{isSubmitting ? "..." : "Submit"}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</form.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById("root")!;
|
||||
|
||||
createRoot(rootElement).render(<App />);
|
||||
7
examples/react/yup/tsconfig.json
Normal file
7
examples/react/yup/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"noEmit": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2020"]
|
||||
}
|
||||
}
|
||||
15
examples/react/zod/.eslintrc.cjs
Normal file
15
examples/react/zod/.eslintrc.cjs
Normal file
@@ -0,0 +1,15 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {
|
||||
extends: ["plugin:react/recommended", "plugin:react-hooks/recommended"],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
rules: {
|
||||
"react/no-children-prop": "off",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
27
examples/react/zod/.gitignore
vendored
Normal file
27
examples/react/zod/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
1
examples/react/zod/.prettierrc
Normal file
1
examples/react/zod/.prettierrc
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
6
examples/react/zod/README.md
Normal file
6
examples/react/zod/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Example
|
||||
|
||||
To run this example:
|
||||
|
||||
- `npm install`
|
||||
- `npm run dev`
|
||||
16
examples/react/zod/index.html
Normal file
16
examples/react/zod/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<title>TanStack Form React Zod Example App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
49
examples/react/zod/package.json
Normal file
49
examples/react/zod/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@tanstack/form-example-react-zod",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index.jsx",
|
||||
"scripts": {
|
||||
"dev": "vite --port=3001",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-form": "0.3.7",
|
||||
"axios": "^0.26.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"zod": "^3.21.4",
|
||||
"@tanstack/form-core": "0.3.7",
|
||||
"@tanstack/zod-form-adapter": "0.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
"vite": "^4.4.9"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"nx": {
|
||||
"implicitDependencies": [
|
||||
"@tanstack/form-core",
|
||||
"@tanstack/react-form",
|
||||
"@tanstack/zod-form-adapter"
|
||||
],
|
||||
"targets": {
|
||||
"test:types": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
examples/react/zod/public/emblem-light.svg
Normal file
13
examples/react/zod/public/emblem-light.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="190px" height="190px" viewBox="0 0 190 190" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
||||
<title>emblem-light</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<path d="M39.7239712,61.3436237 C36.631224,46.362877 35.9675112,34.8727722 37.9666331,26.5293551 C39.1555965,21.5671678 41.3293088,17.5190846 44.6346064,14.5984631 C48.1241394,11.5150478 52.5360327,10.0020122 57.493257,10.0020122 C65.6712013,10.0020122 74.2682602,13.7273214 83.4557246,20.8044264 C87.2031203,23.6910458 91.0924366,27.170411 95.1316515,31.2444746 C95.4531404,30.8310265 95.8165416,30.4410453 96.2214301,30.0806152 C107.64098,19.9149716 117.255245,13.5989272 125.478408,11.1636507 C130.367899,9.715636 134.958526,9.57768202 139.138936,10.983031 C143.551631,12.4664684 147.06766,15.5329489 149.548314,19.8281091 C153.642288,26.9166735 154.721918,36.2310983 153.195595,47.7320243 C152.573451,52.4199112 151.50985,57.5263831 150.007094,63.0593153 C150.574045,63.1277086 151.142416,63.2532808 151.705041,63.4395297 C166.193932,68.2358678 176.453582,73.3937462 182.665021,79.2882839 C186.360669,82.7953831 188.773972,86.6998434 189.646365,91.0218204 C190.567176,95.5836746 189.669313,100.159332 187.191548,104.451297 C183.105211,111.529614 175.591643,117.11221 164.887587,121.534031 C160.589552,123.309539 155.726579,124.917559 150.293259,126.363748 C150.541176,126.92292 150.733521,127.516759 150.862138,128.139758 C153.954886,143.120505 154.618598,154.61061 152.619477,162.954027 C151.430513,167.916214 149.256801,171.964297 145.951503,174.884919 C142.46197,177.968334 138.050077,179.48137 133.092853,179.48137 C124.914908,179.48137 116.31785,175.756061 107.130385,168.678956 C103.343104,165.761613 99.4108655,162.238839 95.3254337,158.108619 C94.9050753,158.765474 94.3889681,159.376011 93.7785699,159.919385 C82.3590198,170.085028 72.744755,176.401073 64.5215915,178.836349 C59.6321009,180.284364 55.0414736,180.422318 50.8610636,179.016969 C46.4483686,177.533532 42.9323404,174.467051 40.4516862,170.171891 C36.3577116,163.083327 35.2780823,153.768902 36.8044053,142.267976 C37.449038,137.410634 38.56762,132.103898 40.1575891,126.339009 C39.5361041,126.276104 38.9120754,126.144816 38.2949591,125.940529 C23.8060684,121.144191 13.5464184,115.986312 7.33497892,110.091775 C3.63933121,106.584675 1.22602752,102.680215 0.353635235,98.3582381 C-0.567176333,93.7963839 0.330686581,89.2207269 2.80845236,84.9287618 C6.89478863,77.8504443 14.4083565,72.2678481 25.1124133,67.8460273 C29.5385143,66.0176154 34.5637208,64.366822 40.1939394,62.8874674 C39.9933393,62.3969171 39.8349374,61.8811235 39.7239712,61.3436237 Z" fill="#002C4B" fill-rule="nonzero" transform="translate(95.000000, 95.000000) scale(-1, 1) translate(-95.000000, -95.000000) "></path>
|
||||
<path d="M80.3968824,64 L109.608177,64 C111.399254,64 113.053521,64.958025 113.944933,66.5115174 L128.577138,92.0115174 C129.461464,93.5526583 129.461464,95.4473417 128.577138,96.9884826 L113.944933,122.488483 C113.053521,124.041975 111.399254,125 109.608177,125 L80.3968824,125 C78.6058059,125 76.9515387,124.041975 76.0601262,122.488483 L61.4279211,96.9884826 C60.543596,95.4473417 60.543596,93.5526583 61.4279211,92.0115174 L76.0601262,66.5115174 C76.9515387,64.958025 78.6058059,64 80.3968824,64 Z M105.987827,70.2765273 C107.779849,70.2765273 109.434839,71.2355558 110.325899,72.7903404 L121.343038,92.0138131 C122.225607,93.5537825 122.225607,95.4462175 121.343038,96.9861869 L110.325899,116.20966 C109.434839,117.764444 107.779849,118.723473 105.987827,118.723473 L84.0172329,118.723473 C82.2252106,118.723473 80.5702207,117.764444 79.6791602,116.20966 L68.6620219,96.9861869 C67.7794521,95.4462175 67.7794521,93.5537825 68.6620219,92.0138131 L79.6791602,72.7903404 C80.5702207,71.2355558 82.2252106,70.2765273 84.0172329,70.2765273 L105.987827,70.2765273 Z M102.080648,77.1414791 L87.9244113,77.1414791 C86.1342282,77.1414791 84.4806439,78.0985567 83.5888998,79.6508285 L83.5888998,79.6508285 L76.4892166,92.0093494 C75.6032319,93.5515958 75.6032319,95.4484042 76.4892166,96.9906506 L76.4892166,96.9906506 L83.5888998,109.349172 C84.4806439,110.901443 86.1342282,111.858521 87.9244113,111.858521 L87.9244113,111.858521 L102.080648,111.858521 C103.870831,111.858521 105.524416,110.901443 106.41616,109.349172 L106.41616,109.349172 L113.515843,96.9906506 C114.401828,95.4484042 114.401828,93.5515958 113.515843,92.0093494 L113.515843,92.0093494 L106.41616,79.6508285 C105.524416,78.0985567 103.870831,77.1414791 102.080648,77.1414791 L102.080648,77.1414791 Z M98.3191856,83.7122186 C100.108028,83.7122186 101.760587,84.6678753 102.652827,86.2183156 L105.983552,92.0060969 C106.87203,93.5500005 106.87203,95.4499995 105.983552,96.9939031 L102.652827,102.781684 C101.760587,104.332125 100.108028,105.287781 98.3191856,105.287781 L91.685874,105.287781 C89.8970316,105.287781 88.2444725,104.332125 87.3522326,102.781684 L84.021508,96.9939031 C83.1330298,95.4499995 83.1330298,93.5500005 84.021508,92.0060969 L87.3522326,86.2183156 C88.2444725,84.6678753 89.8970316,83.7122186 91.685874,83.7122186 L98.3191856,83.7122186 Z M95.0037937,90.1848875 C93.459294,90.1848875 92.0343817,91.0072828 91.2630046,92.3424437 C90.4917325,93.6774232 90.4917325,95.3225768 91.2630046,96.6575563 C92.0343817,97.9927172 93.459294,98.8151125 95.0012659,98.8151125 L95.0012659,98.8151125 C96.5457656,98.8151125 97.9706779,97.9927172 98.7420549,96.6575563 C99.5133271,95.3225768 99.5133271,93.6774232 98.7420549,92.3424437 C97.9706779,91.0072828 96.5457656,90.1848875 95.0037937,90.1848875 L95.0037937,90.1848875 Z M60,94.5009646 L67.7677636,94.5009646" fill="#FFD94C"></path>
|
||||
<path d="M54.8601729,108.357758 C56.1715224,107.608286 57.8360246,108.074601 58.5779424,109.399303 L58.5779424,109.399303 L59.0525843,110.24352 C62.8563392,116.982993 66.8190116,123.380176 70.9406016,129.435068 C75.8078808,136.585427 81.28184,143.82411 87.3624792,151.151115 C88.3168778,152.30114 88.1849437,154.011176 87.065686,154.997937 L87.065686,154.997937 L86.4542085,155.534625 C66.3465389,173.103314 53.2778188,176.612552 47.2480482,166.062341 C41.3500652,155.742717 43.4844915,136.982888 53.6513274,109.782853 C53.876818,109.179582 54.3045861,108.675291 54.8601729,108.357758 Z M140.534177,129.041504 C141.986131,128.785177 143.375496,129.742138 143.65963,131.194242 L143.65963,131.194242 L143.812815,131.986376 C148.782365,157.995459 145.283348,171 133.315764,171 C121.609745,171 106.708724,159.909007 88.6127018,137.727022 C88.2113495,137.235047 87.9945723,136.617371 88,135.981509 C88.013158,134.480686 89.2357854,133.274651 90.730918,133.287756 L90.730918,133.287756 L91.6846544,133.294531 C99.3056979,133.335994 106.714387,133.071591 113.910723,132.501323 C122.409039,131.82788 131.283523,130.674607 140.534177,129.041504 Z M147.408726,73.8119663 C147.932139,72.4026903 149.508386,71.6634537 150.954581,72.149012 L150.954581,72.149012 L151.742552,72.4154854 C177.583763,81.217922 187.402356,90.8916805 181.198332,101.436761 C175.129904,111.751366 157.484347,119.260339 128.26166,123.963678 C127.613529,124.067994 126.948643,123.945969 126.382735,123.618843 C125.047025,122.846729 124.602046,121.158214 125.388848,119.847438 L125.388848,119.847438 L125.889328,119.0105 C129.877183,112.31633 133.481358,105.654262 136.701854,99.0242957 C140.50501,91.1948179 144.073967,82.7907081 147.408726,73.8119663 Z M61.7383398,66.0363218 C62.3864708,65.9320063 63.0513565,66.0540315 63.6172646,66.3811573 C64.9529754,67.153271 65.3979538,68.8417862 64.6111517,70.1525615 L64.6111517,70.1525615 L64.1106718,70.9895001 C60.1228168,77.6836699 56.5186416,84.3457379 53.2981462,90.9757043 C49.49499,98.8051821 45.9260328,107.209292 42.5912744,116.188034 C42.0678608,117.59731 40.4916142,118.336546 39.045419,117.850988 L39.045419,117.850988 L38.2574475,117.584515 C12.4162372,108.782078 2.59764398,99.1083195 8.80166786,88.5632391 C14.8700957,78.2486335 32.515653,70.7396611 61.7383398,66.0363218 Z M103.545792,34.4653746 C123.653461,16.8966864 136.722181,13.3874478 142.751952,23.9376587 C148.649935,34.2572826 146.515508,53.0171122 136.348673,80.2171474 C136.123182,80.8204179 135.695414,81.324709 135.139827,81.6422422 C133.828478,82.3917144 132.163975,81.9253986 131.422058,80.6006966 L131.422058,80.6006966 L130.947416,79.7564798 C127.143661,73.0170065 123.180988,66.6198239 119.059398,60.564932 C114.192119,53.4145727 108.71816,46.1758903 102.637521,38.8488847 C101.683122,37.6988602 101.815056,35.9888243 102.934314,35.0020629 L102.934314,35.0020629 Z M57.6842361,18 C69.3902551,18 84.2912758,29.0909926 102.387298,51.2729777 C102.788651,51.7649527 103.005428,52.3826288 103,53.0184911 C102.986842,54.5193144 101.764215,55.7253489 100.269082,55.7122445 L100.269082,55.7122445 L99.3153456,55.7054689 C91.6943021,55.6640063 84.2856126,55.9284091 77.0892772,56.4986773 C68.5909612,57.17212 59.7164767,58.325393 50.4658235,59.9584962 C49.0138691,60.2148231 47.6245044,59.2578618 47.3403697,57.805758 L47.3403697,57.805758 L47.1871846,57.0136235 C42.2176347,31.0045412 45.7166519,18 57.6842361,18 Z" fill="#FF4154"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
113
examples/react/zod/src/index.tsx
Normal file
113
examples/react/zod/src/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import * as React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import { zodValidator } from "@tanstack/zod-form-adapter";
|
||||
import { z } from "zod";
|
||||
import type { FieldApi } from "@tanstack/react-form";
|
||||
|
||||
function FieldInfo({ field }: { field: FieldApi<any, any, unknown, unknown> }) {
|
||||
return (
|
||||
<>
|
||||
{field.state.meta.touchedErrors ? (
|
||||
<em>{field.state.meta.touchedErrors}</em>
|
||||
) : null}
|
||||
{field.state.meta.isValidating ? "Validating..." : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const form = useForm({
|
||||
// Memoize your default values to prevent re-renders
|
||||
defaultValues: {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
// Do something with form data
|
||||
console.log(values);
|
||||
},
|
||||
// Add a validator to support Zod usage in Form and Field
|
||||
validator: zodValidator,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Simple Form Example</h1>
|
||||
{/* A pre-bound form component */}
|
||||
<form.Provider>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{/* A type-safe and pre-bound field component*/}
|
||||
<form.Field
|
||||
name="firstName"
|
||||
onChange={z
|
||||
.string()
|
||||
.min(3, "First name must be at least 3 characters")}
|
||||
onChangeAsyncDebounceMs={500}
|
||||
onChangeAsync={z.string().refine(
|
||||
async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return !value.includes("error");
|
||||
},
|
||||
{
|
||||
message: "No 'error' allowed in first name",
|
||||
},
|
||||
)}
|
||||
children={(field) => {
|
||||
// Avoid hasty abstractions. Render props are great!
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={field.name}>First Name:</label>
|
||||
<input
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
<FieldInfo field={field} />
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<form.Field
|
||||
name="lastName"
|
||||
children={(field) => (
|
||||
<>
|
||||
<label htmlFor={field.name}>Last Name:</label>
|
||||
<input
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
<FieldInfo field={field} />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<form.Subscribe
|
||||
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
||||
children={([canSubmit, isSubmitting]) => (
|
||||
<button type="submit" disabled={!canSubmit}>
|
||||
{isSubmitting ? "..." : "Submit"}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</form.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById("root")!;
|
||||
|
||||
createRoot(rootElement).render(<App />);
|
||||
7
examples/react/zod/tsconfig.json
Normal file
7
examples/react/zod/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"noEmit": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2020"]
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vue Form Example</title>
|
||||
<title>TanStack Form Vue Simple Example App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
"dependencies": {
|
||||
"@tanstack/form-core": "0.3.7",
|
||||
"@tanstack/vue-form": "0.3.7",
|
||||
"vue": "^3.3.4",
|
||||
"@tanstack/react-form": "0.3.7"
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
|
||||
@@ -34,7 +34,7 @@ async function onChangeFirstName(value: string) {
|
||||
<div>
|
||||
<form.Field
|
||||
name="firstName"
|
||||
:onChange="
|
||||
@change="
|
||||
(value) =>
|
||||
!value
|
||||
? `A first name is required`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { FieldApi } from '@tanstack/vue-form'
|
||||
|
||||
const props = defineProps<{
|
||||
state: FieldApi<any, any>['state']
|
||||
state: FieldApi<any, any, unknown, unknown>['state']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
||||
9
examples/vue/yup/.gitignore
vendored
Normal file
9
examples/vue/yup/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
6
examples/vue/yup/README.md
Normal file
6
examples/vue/yup/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Basic example
|
||||
|
||||
To run this example:
|
||||
|
||||
- `npm install` or `yarn` or `pnpm i`
|
||||
- `npm run dev` or `yarn dev` or `pnpm dev`
|
||||
12
examples/vue/yup/index.html
Normal file
12
examples/vue/yup/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TanStack Form Vue Yup Example App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
38
examples/vue/yup/package.json
Normal file
38
examples/vue/yup/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@tanstack/form-example-vue-yup",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build -m development",
|
||||
"test:types": "vue-tsc --noEmit",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/form-core": "0.3.7",
|
||||
"@tanstack/vue-form": "0.3.7",
|
||||
"@tanstack/yup-form-adapter": "0.3.7",
|
||||
"vue": "^3.3.4",
|
||||
"yup": "^1.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.4.9",
|
||||
"vue-tsc": "^1.8.10"
|
||||
},
|
||||
"nx": {
|
||||
"implicitDependencies": [
|
||||
"@tanstack/form-core",
|
||||
"@tanstack/vue-form",
|
||||
"@tanstack/yup-form-adapter"
|
||||
],
|
||||
"targets": {
|
||||
"test:types": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
examples/vue/yup/src/App.vue
Normal file
87
examples/vue/yup/src/App.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@tanstack/vue-form'
|
||||
import FieldInfo from './FieldInfo.vue'
|
||||
import { yupValidator } from '@tanstack/yup-form-adapter'
|
||||
import * as yup from 'yup'
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
// Do something with form data
|
||||
alert(JSON.stringify(values))
|
||||
},
|
||||
// Add a validator to support Yup usage in Form and Field
|
||||
validator: yupValidator,
|
||||
})
|
||||
|
||||
form.provideFormContext()
|
||||
|
||||
const onChangeFirstName = yup
|
||||
.string()
|
||||
.test('no-error', "No 'error' allowed in first name", async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
return !value?.includes('error')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
@submit="
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
void form.handleSubmit()
|
||||
}
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<form.Field
|
||||
name="firstName"
|
||||
:onChange="
|
||||
yup.string().min(3, 'First name must be at least 3 characters')
|
||||
"
|
||||
:onChangeAsyncDebounceMs="500"
|
||||
:onChangeAsync="onChangeFirstName"
|
||||
>
|
||||
<template v-slot="{ field, state }">
|
||||
<label :htmlFor="field.name">First Name:</label>
|
||||
<input
|
||||
:name="field.name"
|
||||
:value="field.state.value"
|
||||
@input="
|
||||
(e) => field.handleChange((e.target as HTMLInputElement).value)
|
||||
"
|
||||
@blur="field.handleBlur"
|
||||
/>
|
||||
<FieldInfo :state="state" />
|
||||
</template>
|
||||
</form.Field>
|
||||
</div>
|
||||
<div>
|
||||
<form.Field name="lastName">
|
||||
<template v-slot="{ field, state }">
|
||||
<label :htmlFor="field.name">Last Name:</label>
|
||||
<input
|
||||
:name="field.name"
|
||||
:value="field.state.value"
|
||||
@input="
|
||||
(e) => field.handleChange((e.target as HTMLInputElement).value)
|
||||
"
|
||||
@blur="field.handleBlur"
|
||||
/>
|
||||
<FieldInfo :state="state" />
|
||||
</template>
|
||||
</form.Field>
|
||||
</div>
|
||||
<form.Subscribe>
|
||||
<template v-slot="{ canSubmit, isSubmitting }">
|
||||
<button type="submit" :disabled="!canSubmit">
|
||||
{{ isSubmitting ? '...' : 'Submit' }}
|
||||
</button>
|
||||
</template>
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
</template>
|
||||
12
examples/vue/yup/src/FieldInfo.vue
Normal file
12
examples/vue/yup/src/FieldInfo.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { FieldApi } from '@tanstack/vue-form'
|
||||
|
||||
const props = defineProps<{
|
||||
state: FieldApi<any, any, unknown, unknown>['state']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<em v-for="error of props.state.meta.touchedErrors">{{ error }}</em>
|
||||
{{ props.state.meta.isValidating ? 'Validating...' : null }}
|
||||
</template>
|
||||
5
examples/vue/yup/src/main.ts
Normal file
5
examples/vue/yup/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
5
examples/vue/yup/src/shims-vue.d.ts
vendored
Normal file
5
examples/vue/yup/src/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
6
examples/vue/yup/src/types.d.ts
vendored
Normal file
6
examples/vue/yup/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Post {
|
||||
userId: number
|
||||
id: number
|
||||
title: string
|
||||
body: string
|
||||
}
|
||||
15
examples/vue/yup/tsconfig.json
Normal file
15
examples/vue/yup/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
|
||||
}
|
||||
10
examples/vue/yup/vite.config.ts
Normal file
10
examples/vue/yup/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import createVuePlugin from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [createVuePlugin()],
|
||||
optimizeDeps: {
|
||||
exclude: ['@tanstack/vue-query', 'vue-demi'],
|
||||
},
|
||||
})
|
||||
9
examples/vue/zod/.gitignore
vendored
Normal file
9
examples/vue/zod/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
6
examples/vue/zod/README.md
Normal file
6
examples/vue/zod/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Basic example
|
||||
|
||||
To run this example:
|
||||
|
||||
- `npm install` or `yarn` or `pnpm i`
|
||||
- `npm run dev` or `yarn dev` or `pnpm dev`
|
||||
12
examples/vue/zod/index.html
Normal file
12
examples/vue/zod/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TanStack Form Vue Zod Example App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
38
examples/vue/zod/package.json
Normal file
38
examples/vue/zod/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@tanstack/form-example-vue-zod",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build -m development",
|
||||
"test:types": "vue-tsc --noEmit",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/form-core": "0.3.7",
|
||||
"@tanstack/vue-form": "0.3.7",
|
||||
"@tanstack/zod-form-adapter": "0.3.7",
|
||||
"vue": "^3.3.4",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.4.9",
|
||||
"vue-tsc": "^1.8.10"
|
||||
},
|
||||
"nx": {
|
||||
"implicitDependencies": [
|
||||
"@tanstack/form-core",
|
||||
"@tanstack/vue-form",
|
||||
"@tanstack/zod-form-adapter"
|
||||
],
|
||||
"targets": {
|
||||
"test:types": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
examples/vue/zod/src/App.vue
Normal file
90
examples/vue/zod/src/App.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@tanstack/vue-form'
|
||||
import FieldInfo from './FieldInfo.vue'
|
||||
import { zodValidator } from '@tanstack/zod-form-adapter'
|
||||
import { z } from 'zod'
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
// Do something with form data
|
||||
alert(JSON.stringify(values))
|
||||
},
|
||||
// Add a validator to support Zod usage in Form and Field
|
||||
validator: zodValidator,
|
||||
})
|
||||
|
||||
form.provideFormContext()
|
||||
|
||||
const onChangeFirstName = z.string().refine(
|
||||
async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
return !value.includes('error')
|
||||
},
|
||||
{
|
||||
message: "No 'error' allowed in first name",
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
@submit="
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
void form.handleSubmit()
|
||||
}
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<form.Field
|
||||
name="firstName"
|
||||
:onChange="
|
||||
z.string().min(3, 'First name must be at least 3 characters')
|
||||
"
|
||||
:onChangeAsyncDebounceMs="500"
|
||||
:onChangeAsync="onChangeFirstName"
|
||||
>
|
||||
<template v-slot="{ field, state }">
|
||||
<label :htmlFor="field.name">First Name:</label>
|
||||
<input
|
||||
:name="field.name"
|
||||
:value="field.state.value"
|
||||
@input="
|
||||
(e) => field.handleChange((e.target as HTMLInputElement).value)
|
||||
"
|
||||
@blur="field.handleBlur"
|
||||
/>
|
||||
<FieldInfo :state="state" />
|
||||
</template>
|
||||
</form.Field>
|
||||
</div>
|
||||
<div>
|
||||
<form.Field name="lastName">
|
||||
<template v-slot="{ field, state }">
|
||||
<label :htmlFor="field.name">Last Name:</label>
|
||||
<input
|
||||
:name="field.name"
|
||||
:value="field.state.value"
|
||||
@input="
|
||||
(e) => field.handleChange((e.target as HTMLInputElement).value)
|
||||
"
|
||||
@blur="field.handleBlur"
|
||||
/>
|
||||
<FieldInfo :state="state" />
|
||||
</template>
|
||||
</form.Field>
|
||||
</div>
|
||||
<form.Subscribe>
|
||||
<template v-slot="{ canSubmit, isSubmitting }">
|
||||
<button type="submit" :disabled="!canSubmit">
|
||||
{{ isSubmitting ? '...' : 'Submit' }}
|
||||
</button>
|
||||
</template>
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
</template>
|
||||
12
examples/vue/zod/src/FieldInfo.vue
Normal file
12
examples/vue/zod/src/FieldInfo.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { FieldApi } from '@tanstack/vue-form'
|
||||
|
||||
const props = defineProps<{
|
||||
state: FieldApi<any, any, unknown, unknown>['state']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<em v-for="error of props.state.meta.touchedErrors">{{ error }}</em>
|
||||
{{ props.state.meta.isValidating ? 'Validating...' : null }}
|
||||
</template>
|
||||
5
examples/vue/zod/src/main.ts
Normal file
5
examples/vue/zod/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
5
examples/vue/zod/src/shims-vue.d.ts
vendored
Normal file
5
examples/vue/zod/src/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
6
examples/vue/zod/src/types.d.ts
vendored
Normal file
6
examples/vue/zod/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Post {
|
||||
userId: number
|
||||
id: number
|
||||
title: string
|
||||
body: string
|
||||
}
|
||||
15
examples/vue/zod/tsconfig.json
Normal file
15
examples/vue/zod/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
|
||||
}
|
||||
10
examples/vue/zod/vite.config.ts
Normal file
10
examples/vue/zod/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import createVuePlugin from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [createVuePlugin()],
|
||||
optimizeDeps: {
|
||||
exclude: ['@tanstack/vue-query', 'vue-demi'],
|
||||
},
|
||||
})
|
||||
@@ -93,7 +93,7 @@
|
||||
"solid-js": "^1.6.13",
|
||||
"stream-to-array": "^2.3.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsup": "^7.2.0",
|
||||
"tsup": "7.2.0",
|
||||
"type-fest": "^3.11.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vitest": "^0.34.3",
|
||||
@@ -108,12 +108,15 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@types/testing-library__jest-dom@5.14.5": "patches/@types__testing-library__jest-dom@5.14.5.patch"
|
||||
"@types/testing-library__jest-dom@5.14.5": "patches/@types__testing-library__jest-dom@5.14.5.patch",
|
||||
"tsup@7.2.0": "patches/tsup@7.2.0.patch"
|
||||
},
|
||||
"overrides": {
|
||||
"@tanstack/form-core": "workspace:*",
|
||||
"@tanstack/react-form": "workspace:*",
|
||||
"@tanstack/vue-form": "workspace:*"
|
||||
"@tanstack/vue-form": "workspace:*",
|
||||
"@tanstack/yup-form-adapter": "workspace:*",
|
||||
"@tanstack/zod-form-adapter": "workspace:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,126 @@
|
||||
import { type DeepKeys, type DeepValue, type Updater } from './utils'
|
||||
import type { FormApi, ValidationError, ValidationErrorMap } from './FormApi'
|
||||
import type { FormApi, ValidationErrorMap } from './FormApi'
|
||||
import { Store } from '@tanstack/store'
|
||||
import type { Validator, ValidationError } from './types'
|
||||
|
||||
export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'
|
||||
|
||||
type ValidateFn<TParentData, TName extends DeepKeys<TParentData>, TData> = (
|
||||
type ValidateFn<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
ValidatorType,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
> = (
|
||||
value: TData,
|
||||
fieldApi: FieldApi<TParentData, TName>,
|
||||
fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>,
|
||||
) => ValidationError
|
||||
|
||||
type ValidateOrFn<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
> = ValidatorType extends Validator<TData>
|
||||
?
|
||||
| Parameters<ReturnType<ValidatorType>['validate']>[1]
|
||||
| ValidateFn<TParentData, TName, ValidatorType, TData>
|
||||
: FormValidator extends Validator<TData>
|
||||
?
|
||||
| Parameters<ReturnType<FormValidator>['validate']>[1]
|
||||
| ValidateFn<TParentData, TName, ValidatorType, TData>
|
||||
: ValidateFn<TParentData, TName, ValidatorType, TData>
|
||||
|
||||
type ValidateAsyncFn<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
TData,
|
||||
ValidatorType,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
> = (
|
||||
value: TData,
|
||||
fieldApi: FieldApi<TParentData, TName>,
|
||||
fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>,
|
||||
) => ValidationError | Promise<ValidationError>
|
||||
|
||||
type AsyncValidateOrFn<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
> = ValidatorType extends Validator<TData>
|
||||
?
|
||||
| Parameters<ReturnType<ValidatorType>['validate']>[1]
|
||||
| ValidateAsyncFn<TParentData, TName, ValidatorType, TData>
|
||||
: FormValidator extends Validator<TData>
|
||||
?
|
||||
| Parameters<ReturnType<FormValidator>['validate']>[1]
|
||||
| ValidateAsyncFn<TParentData, TName, ValidatorType, TData>
|
||||
: ValidateAsyncFn<TParentData, TName, ValidatorType, TData>
|
||||
|
||||
export interface FieldOptions<
|
||||
TParentData,
|
||||
/**
|
||||
* This allows us to restrict the name to only be a valid field name while
|
||||
* also assigning it to a generic
|
||||
*/
|
||||
TName extends DeepKeys<TParentData>,
|
||||
/**
|
||||
* If TData is unknown, we can use the TName generic to determine the type
|
||||
*/
|
||||
TData = DeepValue<TParentData, TName>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
> {
|
||||
name: DeepKeys<TParentData>
|
||||
name: TName
|
||||
index?: TData extends any[] ? number : never
|
||||
defaultValue?: TData
|
||||
asyncDebounceMs?: number
|
||||
asyncAlways?: boolean
|
||||
onMount?: (formApi: FieldApi<TParentData, TName>) => void
|
||||
onChange?: ValidateFn<TParentData, TName, TData>
|
||||
onChangeAsync?: ValidateAsyncFn<TParentData, TName, TData>
|
||||
validator?: ValidatorType
|
||||
onMount?: (
|
||||
formApi: FieldApi<TParentData, TName, ValidatorType, TData>,
|
||||
) => void
|
||||
onChange?: ValidateOrFn<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData
|
||||
>
|
||||
onChangeAsync?: AsyncValidateOrFn<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData
|
||||
>
|
||||
onChangeAsyncDebounceMs?: number
|
||||
onBlur?: ValidateFn<TParentData, TName, TData>
|
||||
onBlurAsync?: ValidateAsyncFn<TParentData, TName, TData>
|
||||
onBlur?: ValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>
|
||||
onBlurAsync?: AsyncValidateOrFn<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData
|
||||
>
|
||||
onBlurAsyncDebounceMs?: number
|
||||
onSubmitAsync?: ValidateAsyncFn<TParentData, TName, TData>
|
||||
onSubmitAsync?: AsyncValidateOrFn<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData
|
||||
>
|
||||
defaultMeta?: Partial<FieldMeta>
|
||||
}
|
||||
|
||||
export interface FieldApiOptions<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
TData = DeepValue<TParentData, TName>,
|
||||
> extends FieldOptions<TParentData, TName, TData> {
|
||||
form: FormApi<TParentData>
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
> extends FieldOptions<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData
|
||||
> {
|
||||
form: FormApi<TParentData, FormValidator>
|
||||
}
|
||||
|
||||
export type FieldMeta = {
|
||||
@@ -76,22 +145,28 @@ export type ResolveName<TParentData> = unknown extends TParentData
|
||||
export class FieldApi<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
TData = DeepValue<TParentData, TName>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
> {
|
||||
uid: number
|
||||
form: FieldApiOptions<TParentData, TName, TData>['form']
|
||||
form: FieldApiOptions<TParentData, TName, ValidatorType, TData>['form']
|
||||
name!: DeepKeys<TParentData>
|
||||
options: FieldApiOptions<TParentData, TName> = {} as any
|
||||
options: FieldApiOptions<TParentData, TName, ValidatorType, TData> = {} as any
|
||||
store!: Store<FieldState<TData>>
|
||||
state!: FieldState<TData>
|
||||
prevState!: FieldState<TData>
|
||||
|
||||
constructor(
|
||||
opts: FieldApiOptions<TParentData, TName, TData> & {
|
||||
form: FormApi<TParentData>
|
||||
},
|
||||
opts: FieldApiOptions<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData
|
||||
>,
|
||||
) {
|
||||
this.form = opts.form
|
||||
this.form = opts.form as never
|
||||
this.uid = uid++
|
||||
// Support field prefixing from FieldScope
|
||||
// let fieldPrefix = ''
|
||||
@@ -99,7 +174,7 @@ export class FieldApi<
|
||||
// fieldPrefix = `${this.form.fieldName}.`
|
||||
// }
|
||||
|
||||
this.name = opts.name as any
|
||||
this.name = opts.name as never
|
||||
|
||||
if (opts.defaultValue !== undefined) {
|
||||
this.form.setFieldValue(this.name, opts.defaultValue as never)
|
||||
@@ -167,12 +242,14 @@ export class FieldApi<
|
||||
unsubscribe()
|
||||
delete info.instances[this.uid]
|
||||
if (!Object.keys(info.instances).length) {
|
||||
delete this.form.fieldInfo[this.name]
|
||||
delete this.form.fieldInfo[this.name as never]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update = (opts: FieldApiOptions<TParentData, TName, TData>) => {
|
||||
update = (
|
||||
opts: FieldApiOptions<TParentData, TName, ValidatorType, TData>,
|
||||
) => {
|
||||
// Default Value
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (this.state.value === undefined) {
|
||||
@@ -238,10 +315,10 @@ export class FieldApi<
|
||||
|
||||
getSubField = <
|
||||
TSubName extends DeepKeys<TData>,
|
||||
TSubData = DeepValue<TData, TSubName>,
|
||||
TSubData extends DeepValue<TData, TSubName> = DeepValue<TData, TSubName>,
|
||||
>(
|
||||
name: TSubName,
|
||||
): FieldApi<TData, TSubName, TSubData> =>
|
||||
): FieldApi<TData, TSubName, ValidatorType, TSubData> =>
|
||||
new FieldApi({
|
||||
name: `${this.name}.${name}` as never,
|
||||
form: this.form,
|
||||
@@ -251,13 +328,36 @@ export class FieldApi<
|
||||
const { onChange, onBlur } = this.options
|
||||
const validate =
|
||||
cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur
|
||||
|
||||
if (!validate) return
|
||||
|
||||
// Use the validationCount for all field instances to
|
||||
// track freshness of the validation
|
||||
const validationCount = (this.getInfo().validationCount || 0) + 1
|
||||
this.getInfo().validationCount = validationCount
|
||||
const error = normalizeError(validate(value as never, this as never))
|
||||
|
||||
const doValidate = () => {
|
||||
if (this.options.validator && typeof validate !== 'function') {
|
||||
return (this.options.validator as Validator<TData>)().validate(
|
||||
value,
|
||||
validate,
|
||||
)
|
||||
}
|
||||
|
||||
if (this.form.options.validator && typeof validate !== 'function') {
|
||||
return (this.form.options.validator as Validator<TData>)().validate(
|
||||
value,
|
||||
validate,
|
||||
)
|
||||
}
|
||||
|
||||
return (validate as ValidateFn<TParentData, TName, ValidatorType, TData>)(
|
||||
value,
|
||||
this as never,
|
||||
)
|
||||
}
|
||||
|
||||
const error = normalizeError(doValidate())
|
||||
const errorMapKey = getErrorMapKey(cause)
|
||||
if (this.state.meta.errorMap[errorMapKey] !== error) {
|
||||
this.setMeta((prev) => ({
|
||||
@@ -275,7 +375,7 @@ export class FieldApi<
|
||||
}
|
||||
}
|
||||
|
||||
#leaseValidateAsync = () => {
|
||||
__leaseValidateAsync = () => {
|
||||
const count = (this.getInfo().validationAsyncCount || 0) + 1
|
||||
this.getInfo().validationAsyncCount = count
|
||||
return count
|
||||
@@ -283,7 +383,7 @@ export class FieldApi<
|
||||
|
||||
cancelValidateAsync = () => {
|
||||
// Lease a new validation count to ignore any pending validations
|
||||
this.#leaseValidateAsync()
|
||||
this.__leaseValidateAsync()
|
||||
// Cancel any pending validation state
|
||||
this.setMeta((prev) => ({
|
||||
...prev,
|
||||
@@ -317,12 +417,13 @@ export class FieldApi<
|
||||
asyncDebounceMs ??
|
||||
0
|
||||
|
||||
if (this.state.meta.isValidating !== true)
|
||||
if (this.state.meta.isValidating !== true) {
|
||||
this.setMeta((prev) => ({ ...prev, isValidating: true }))
|
||||
}
|
||||
|
||||
// Use the validationCount for all field instances to
|
||||
// track freshness of the validation
|
||||
const validationAsyncCount = this.#leaseValidateAsync()
|
||||
const validationAsyncCount = this.__leaseValidateAsync()
|
||||
|
||||
const checkLatest = () =>
|
||||
validationAsyncCount === this.getInfo().validationAsyncCount
|
||||
@@ -338,11 +439,31 @@ export class FieldApi<
|
||||
await new Promise((r) => setTimeout(r, debounceMs))
|
||||
}
|
||||
|
||||
const doValidate = () => {
|
||||
if (this.options.validator && typeof validate !== 'function') {
|
||||
return (this.options.validator as Validator<TData>)().validateAsync(
|
||||
value,
|
||||
validate,
|
||||
)
|
||||
}
|
||||
|
||||
if (this.form.options.validator && typeof validate !== 'function') {
|
||||
return (
|
||||
this.form.options.validator as Validator<TData>
|
||||
)().validateAsync(value, validate)
|
||||
}
|
||||
|
||||
return (validate as ValidateFn<TParentData, TName, ValidatorType, TData>)(
|
||||
value,
|
||||
this as never,
|
||||
)
|
||||
}
|
||||
|
||||
// Only kick off validation if this validation is the latest attempt
|
||||
if (checkLatest()) {
|
||||
const prevErrors = this.getMeta().errors
|
||||
try {
|
||||
const rawError = await validate(value as never, this as never)
|
||||
const rawError = await doValidate()
|
||||
if (checkLatest()) {
|
||||
const error = normalizeError(rawError)
|
||||
this.setMeta((prev) => ({
|
||||
@@ -378,12 +499,18 @@ export class FieldApi<
|
||||
): ValidationError[] | Promise<ValidationError[]> => {
|
||||
// If the field is pristine and validatePristine is false, do not validate
|
||||
if (!this.state.meta.isTouched) return []
|
||||
|
||||
// Store the previous error for the errorMapKey (eg. onChange, onBlur, onSubmit)
|
||||
const errorMapKey = getErrorMapKey(cause)
|
||||
const prevError = this.getMeta().errorMap[errorMapKey]
|
||||
|
||||
// Attempt to sync validate first
|
||||
this.validateSync(value, cause)
|
||||
|
||||
const errorMapKey = getErrorMapKey(cause)
|
||||
// If there is an error mapped to the errorMapKey (eg. onChange, onBlur, onSubmit), return the errors array, do not attempt async validation
|
||||
if (this.getMeta().errorMap[errorMapKey]) {
|
||||
// If there is a new error mapped to the errorMapKey (eg. onChange, onBlur, onSubmit), return the errors array, do not attempt async validation
|
||||
const newError = this.getMeta().errorMap[errorMapKey]
|
||||
|
||||
if (prevError !== newError) {
|
||||
if (!this.options.asyncAlways) {
|
||||
return this.state.meta.errors
|
||||
}
|
||||
|
||||
@@ -1,37 +1,49 @@
|
||||
import { Store } from '@tanstack/store'
|
||||
//
|
||||
import type { DeepKeys, DeepValue, Updater } from './utils'
|
||||
import { functionalUpdate, getBy, isNonEmptyArray, setBy } from './utils'
|
||||
import type { FieldApi, FieldMeta, ValidationCause } from './FieldApi'
|
||||
import type { ValidationError, Validator } from './types'
|
||||
|
||||
export type FormOptions<TData> = {
|
||||
type ValidateFn<TData, ValidatorType> = (
|
||||
values: TData,
|
||||
formApi: FormApi<TData, ValidatorType>,
|
||||
) => ValidationError
|
||||
|
||||
type ValidateOrFn<TData, ValidatorType> = ValidatorType extends Validator<TData>
|
||||
? Parameters<ReturnType<ValidatorType>['validate']>[1]
|
||||
: ValidateFn<TData, ValidatorType>
|
||||
|
||||
type ValidateAsyncFn<TData, ValidatorType> = (
|
||||
value: TData,
|
||||
fieldApi: FormApi<TData, ValidatorType>,
|
||||
) => ValidationError | Promise<ValidationError>
|
||||
|
||||
export type FormOptions<TData, ValidatorType> = {
|
||||
defaultValues?: TData
|
||||
defaultState?: Partial<FormState<TData>>
|
||||
asyncDebounceMs?: number
|
||||
onMount?: (values: TData, formApi: FormApi<TData>) => ValidationError
|
||||
onMountAsync?: (
|
||||
values: TData,
|
||||
formApi: FormApi<TData>,
|
||||
) => ValidationError | Promise<ValidationError>
|
||||
validator?: ValidatorType
|
||||
onMount?: ValidateOrFn<TData, ValidatorType>
|
||||
onMountAsync?: ValidateAsyncFn<TData, ValidatorType>
|
||||
onMountAsyncDebounceMs?: number
|
||||
onChange?: (values: TData, formApi: FormApi<TData>) => ValidationError
|
||||
onChangeAsync?: (
|
||||
values: TData,
|
||||
formApi: FormApi<TData>,
|
||||
) => ValidationError | Promise<ValidationError>
|
||||
onChange?: ValidateOrFn<TData, ValidatorType>
|
||||
onChangeAsync?: ValidateAsyncFn<TData, ValidatorType>
|
||||
onChangeAsyncDebounceMs?: number
|
||||
onBlur?: (values: TData, formApi: FormApi<TData>) => ValidationError
|
||||
onBlurAsync?: (
|
||||
values: TData,
|
||||
formApi: FormApi<TData>,
|
||||
) => ValidationError | Promise<ValidationError>
|
||||
onBlur?: ValidateOrFn<TData, ValidatorType>
|
||||
onBlurAsync?: ValidateAsyncFn<TData, ValidatorType>
|
||||
onBlurAsyncDebounceMs?: number
|
||||
onSubmit?: (values: TData, formApi: FormApi<TData>) => any | Promise<any>
|
||||
onSubmitInvalid?: (values: TData, formApi: FormApi<TData>) => void
|
||||
onSubmit?: (
|
||||
values: TData,
|
||||
formApi: FormApi<TData, ValidatorType>,
|
||||
) => any | Promise<any>
|
||||
onSubmitInvalid?: (
|
||||
values: TData,
|
||||
formApi: FormApi<TData, ValidatorType>,
|
||||
) => void
|
||||
}
|
||||
|
||||
export type FieldInfo<TFormData> = {
|
||||
instances: Record<string, FieldApi<TFormData, any, any>>
|
||||
export type FieldInfo<TFormData, ValidatorType> = {
|
||||
instances: Record<string, FieldApi<TFormData, any, unknown, ValidatorType>>
|
||||
} & ValidationMeta
|
||||
|
||||
export type ValidationMeta = {
|
||||
@@ -42,8 +54,6 @@ export type ValidationMeta = {
|
||||
validationReject?: (errors: unknown) => void
|
||||
}
|
||||
|
||||
export type ValidationError = undefined | false | null | string
|
||||
|
||||
export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`
|
||||
|
||||
export type ValidationErrorMap = {
|
||||
@@ -92,18 +102,19 @@ function getDefaultFormState<TData>(
|
||||
}
|
||||
}
|
||||
|
||||
export class FormApi<TFormData> {
|
||||
export class FormApi<TFormData, ValidatorType> {
|
||||
// // This carries the context for nested fields
|
||||
options: FormOptions<TFormData> = {}
|
||||
options: FormOptions<TFormData, ValidatorType> = {}
|
||||
store!: Store<FormState<TFormData>>
|
||||
// Do not use __state directly, as it is not reactive.
|
||||
// Please use form.useStore() utility to subscribe to state
|
||||
state!: FormState<TFormData>
|
||||
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData>> = {} as any
|
||||
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData, ValidatorType>> =
|
||||
{} as any
|
||||
fieldName?: string
|
||||
validationMeta: ValidationMeta = {}
|
||||
|
||||
constructor(opts?: FormOptions<TFormData>) {
|
||||
constructor(opts?: FormOptions<TFormData, ValidatorType>) {
|
||||
this.store = new Store<FormState<TFormData>>(
|
||||
getDefaultFormState({
|
||||
...(opts?.defaultState as any),
|
||||
@@ -157,7 +168,7 @@ export class FormApi<TFormData> {
|
||||
this.update(opts || {})
|
||||
}
|
||||
|
||||
update = (options?: FormOptions<TFormData>) => {
|
||||
update = (options?: FormOptions<TFormData, ValidatorType>) => {
|
||||
if (!options) return
|
||||
|
||||
this.store.batch(() => {
|
||||
@@ -202,21 +213,21 @@ export class FormApi<TFormData> {
|
||||
validateAllFields = async (cause: ValidationCause) => {
|
||||
const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
|
||||
this.store.batch(() => {
|
||||
void (Object.values(this.fieldInfo) as FieldInfo<any>[]).forEach(
|
||||
(field) => {
|
||||
Object.values(field.instances).forEach((instance) => {
|
||||
// If any fields are not touched
|
||||
if (!instance.state.meta.isTouched) {
|
||||
// Mark them as touched
|
||||
instance.setMeta((prev) => ({ ...prev, isTouched: true }))
|
||||
// Validate the field
|
||||
fieldValidationPromises.push(
|
||||
Promise.resolve().then(() => instance.validate(cause)),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
void (
|
||||
Object.values(this.fieldInfo) as FieldInfo<any, ValidatorType>[]
|
||||
).forEach((field) => {
|
||||
Object.values(field.instances).forEach((instance) => {
|
||||
// If any fields are not touched
|
||||
if (!instance.state.meta.isTouched) {
|
||||
// Mark them as touched
|
||||
instance.setMeta((prev) => ({ ...prev, isTouched: true }))
|
||||
// Validate the field
|
||||
fieldValidationPromises.push(
|
||||
Promise.resolve().then(() => instance.validate(cause)),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return Promise.all(fieldValidationPromises)
|
||||
@@ -290,7 +301,7 @@ export class FormApi<TFormData> {
|
||||
|
||||
getFieldInfo = <TField extends DeepKeys<TFormData>>(
|
||||
field: TField,
|
||||
): FieldInfo<TFormData> => {
|
||||
): FieldInfo<TFormData, ValidatorType> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
return (this.fieldInfo[field] ||= {
|
||||
instances: {},
|
||||
@@ -338,7 +349,9 @@ export class FormApi<TFormData> {
|
||||
|
||||
pushFieldValue = <TField extends DeepKeys<TFormData>>(
|
||||
field: TField,
|
||||
value: DeepValue<TFormData, TField>[number],
|
||||
value: DeepValue<TFormData, TField> extends any[]
|
||||
? DeepValue<TFormData, TField>[number]
|
||||
: never,
|
||||
opts?: { touch?: boolean },
|
||||
) => {
|
||||
return this.setFieldValue(
|
||||
@@ -351,7 +364,9 @@ export class FormApi<TFormData> {
|
||||
insertFieldValue = <TField extends DeepKeys<TFormData>>(
|
||||
field: TField,
|
||||
index: number,
|
||||
value: DeepValue<TFormData, TField>[number],
|
||||
value: DeepValue<TFormData, TField> extends any[]
|
||||
? DeepValue<TFormData, TField>[number]
|
||||
: never,
|
||||
opts?: { touch?: boolean },
|
||||
) => {
|
||||
this.setFieldValue(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './FormApi'
|
||||
export * from './FieldApi'
|
||||
export * from './utils'
|
||||
export * from './types'
|
||||
|
||||
@@ -551,7 +551,7 @@ describe('field api', () => {
|
||||
interface Form {
|
||||
name: string
|
||||
}
|
||||
const form = new FormApi<Form>()
|
||||
const form = new FormApi<Form, unknown>()
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
|
||||
@@ -2,24 +2,21 @@ import { assertType } from 'vitest'
|
||||
import { FormApi } from '../FormApi'
|
||||
import { FieldApi } from '../FieldApi'
|
||||
|
||||
it('should type a subfield properly', () => {
|
||||
it('should type value properly', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
names: {
|
||||
first: 'one',
|
||||
second: 'two',
|
||||
},
|
||||
} as const,
|
||||
})
|
||||
name: 'test',
|
||||
},
|
||||
} as const)
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'names',
|
||||
name: 'name',
|
||||
})
|
||||
|
||||
const subfield = field.getSubField('first')
|
||||
|
||||
assertType<'one'>(subfield.getValue())
|
||||
assertType<'test'>(field.state.value)
|
||||
assertType<'name'>(field.options.name)
|
||||
assertType<'test'>(field.getValue())
|
||||
})
|
||||
|
||||
it('should type onChange properly', () => {
|
||||
@@ -39,3 +36,21 @@ it('should type onChange properly', () => {
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should type onChangeAsync properly', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
} as const)
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
onChangeAsync: async (value) => {
|
||||
assertType<'test'>(value)
|
||||
|
||||
return undefined
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -119,7 +119,7 @@ describe('form api', () => {
|
||||
},
|
||||
})
|
||||
|
||||
form.pushFieldValue('name', 'other')
|
||||
form.setFieldValue('name', 'other')
|
||||
form.state.submissionAttempts = 300
|
||||
|
||||
form.reset()
|
||||
|
||||
7
packages/form-core/src/types.ts
Normal file
7
packages/form-core/src/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type ValidationError = undefined | false | null | string
|
||||
|
||||
// If/when TypeScript supports higher-kinded types, this should not be `unknown` anymore
|
||||
export type Validator<Type, Fn = unknown> = () => {
|
||||
validate(value: Type, fn: Fn): ValidationError
|
||||
validateAsync(value: Type, fn: Fn): Promise<ValidationError>
|
||||
}
|
||||
@@ -3,19 +3,21 @@ import type { FormApi, FormOptions } from '@tanstack/form-core'
|
||||
import { type UseField, type FieldComponent, Field, useField } from './useField'
|
||||
import { useForm } from './useForm'
|
||||
|
||||
export type FormFactory<TFormData> = {
|
||||
useForm: (opts?: FormOptions<TFormData>) => FormApi<TFormData>
|
||||
export type FormFactory<TFormData, FormValidator> = {
|
||||
useForm: (
|
||||
opts?: FormOptions<TFormData, FormValidator>,
|
||||
) => FormApi<TFormData, FormValidator>
|
||||
useField: UseField<TFormData>
|
||||
Field: FieldComponent<TFormData>
|
||||
Field: FieldComponent<TFormData, FormValidator>
|
||||
}
|
||||
|
||||
export function createFormFactory<TFormData>(
|
||||
defaultOpts?: FormOptions<TFormData>,
|
||||
): FormFactory<TFormData> {
|
||||
export function createFormFactory<TFormData, FormValidator>(
|
||||
defaultOpts?: FormOptions<TFormData, FormValidator>,
|
||||
): FormFactory<TFormData, FormValidator> {
|
||||
return {
|
||||
useForm: (opts) => {
|
||||
const formOptions = Object.assign({}, defaultOpts, opts)
|
||||
return useForm<TFormData>(formOptions)
|
||||
return useForm<TFormData, FormValidator>(formOptions)
|
||||
},
|
||||
useField: useField as any,
|
||||
Field: Field as any,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { FormApi } from '@tanstack/form-core'
|
||||
import * as React from 'react'
|
||||
|
||||
export const formContext = React.createContext<{
|
||||
formApi: FormApi<any>
|
||||
formApi: FormApi<any, unknown>
|
||||
parentFieldName?: string
|
||||
} | null>(null!)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('createFormFactory', () => {
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const formFactory = createFormFactory<Person>({
|
||||
const formFactory = createFormFactory<Person, unknown>({
|
||||
defaultValues: {
|
||||
firstName: 'FirstName',
|
||||
lastName: 'LastName',
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('useField', () => {
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
function Comp() {
|
||||
const form = formFactory.useForm({
|
||||
@@ -55,7 +55,7 @@ describe('useField', () => {
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
function Comp() {
|
||||
const form = formFactory.useForm({
|
||||
@@ -97,7 +97,7 @@ describe('useField', () => {
|
||||
}
|
||||
const error = 'Please enter a different value'
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
function Comp() {
|
||||
const form = formFactory.useForm()
|
||||
@@ -137,7 +137,7 @@ describe('useField', () => {
|
||||
}
|
||||
const error = 'Please enter a different value'
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
function Comp() {
|
||||
const form = formFactory.useForm()
|
||||
@@ -180,7 +180,7 @@ describe('useField', () => {
|
||||
const onChangeError = 'Please enter a different value (onChangeError)'
|
||||
const onBlurError = 'Please enter a different value (onBlurError)'
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
function Comp() {
|
||||
const form = formFactory.useForm()
|
||||
@@ -229,7 +229,7 @@ describe('useField', () => {
|
||||
}
|
||||
const error = 'Please enter a different value'
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
function Comp() {
|
||||
const form = formFactory.useForm()
|
||||
@@ -276,7 +276,7 @@ describe('useField', () => {
|
||||
const onChangeError = 'Please enter a different value (onChangeError)'
|
||||
const onBlurError = 'Please enter a different value (onBlurError)'
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
function Comp() {
|
||||
const form = formFactory.useForm()
|
||||
@@ -332,7 +332,7 @@ describe('useField', () => {
|
||||
}
|
||||
const mockFn = vi.fn()
|
||||
const error = 'Please enter a different value'
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
function Comp() {
|
||||
const form = formFactory.useForm()
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('useForm', () => {
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
function Comp() {
|
||||
const form = formFactory.useForm()
|
||||
@@ -52,7 +52,7 @@ describe('useForm', () => {
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
function Comp() {
|
||||
const form = formFactory.useForm({
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { FieldOptions, DeepKeys, DeepValue } from '@tanstack/form-core'
|
||||
export type UseFieldOptions<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
TData = DeepValue<TParentData, TName>,
|
||||
> = FieldOptions<TParentData, TName, TData> & {
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
> = FieldOptions<TParentData, TName, ValidatorType, FormValidator, TData> & {
|
||||
mode?: 'value' | 'array'
|
||||
}
|
||||
|
||||
@@ -11,21 +11,45 @@ declare module '@tanstack/form-core' {
|
||||
interface FieldApi<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
TData = DeepValue<TParentData, TName>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
> {
|
||||
Field: FieldComponent<TData>
|
||||
Field: FieldComponent<TData, FormValidator>
|
||||
}
|
||||
}
|
||||
|
||||
export type UseField<TParentData> = <TName extends DeepKeys<TParentData>>(
|
||||
opts?: { name: Narrow<TName> } & UseFieldOptions<TParentData, TName>,
|
||||
) => FieldApi<TParentData, TName, DeepValue<TParentData, TName>>
|
||||
export type UseField<TParentData> = <
|
||||
TName extends DeepKeys<TParentData>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
>(
|
||||
opts?: { name: Narrow<TName> } & UseFieldOptions<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator
|
||||
>,
|
||||
) => FieldApi<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
DeepValue<TParentData, TName>
|
||||
>
|
||||
|
||||
export function useField<TParentData, TName extends DeepKeys<TParentData>>(
|
||||
opts: UseFieldOptions<TParentData, TName>,
|
||||
export function useField<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
>(
|
||||
opts: UseFieldOptions<TParentData, TName, ValidatorType, FormValidator>,
|
||||
): FieldApi<
|
||||
TParentData,
|
||||
TName
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator
|
||||
// Omit<typeof opts, 'onMount'> & {
|
||||
// form: FormApi<TParentData>
|
||||
// }
|
||||
@@ -45,8 +69,9 @@ export function useField<TParentData, TName extends DeepKeys<TParentData>>(
|
||||
const api = new FieldApi({
|
||||
...opts,
|
||||
form: formApi,
|
||||
name: name,
|
||||
} as never)
|
||||
// TODO: Fix typings to include `index` and `parentFieldName`, if present
|
||||
name: name as typeof opts.name,
|
||||
})
|
||||
|
||||
api.Field = Field as never
|
||||
|
||||
@@ -64,23 +89,27 @@ export function useField<TParentData, TName extends DeepKeys<TParentData>>(
|
||||
useStore(
|
||||
fieldApi.store,
|
||||
opts.mode === 'array'
|
||||
? (state: any) => {
|
||||
return [state.meta, Object.keys(state.value || []).length]
|
||||
? (state) => {
|
||||
return [state.meta, Object.keys(state.value).length]
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
// Instantiates field meta and removes it when unrendered
|
||||
useIsomorphicLayoutEffect(() => fieldApi.mount(), [fieldApi])
|
||||
|
||||
return fieldApi as never
|
||||
return fieldApi
|
||||
}
|
||||
|
||||
type FieldComponentProps<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
TData = DeepValue<TParentData, TName>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
> = {
|
||||
children: (fieldApi: FieldApi<TParentData, TName, TData>) => any
|
||||
children: (
|
||||
fieldApi: FieldApi<TParentData, TName, ValidatorType, FormValidator, TData>,
|
||||
) => any
|
||||
} & (TParentData extends any[]
|
||||
? {
|
||||
name?: TName
|
||||
@@ -90,27 +119,47 @@ type FieldComponentProps<
|
||||
name: TName
|
||||
index?: never
|
||||
}) &
|
||||
Omit<UseFieldOptions<TParentData, TName>, 'name' | 'index'>
|
||||
Omit<
|
||||
UseFieldOptions<TParentData, TName, ValidatorType, FormValidator>,
|
||||
'name' | 'index'
|
||||
>
|
||||
|
||||
export type FieldComponent<TParentData> = <
|
||||
export type FieldComponent<TParentData, FormValidator> = <
|
||||
TName extends DeepKeys<TParentData>,
|
||||
TData = DeepValue<TParentData, TName>,
|
||||
ValidatorType,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
>({
|
||||
children,
|
||||
...fieldOptions
|
||||
}: FieldComponentProps<TParentData, TName, TData>) => any
|
||||
}: FieldComponentProps<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData
|
||||
>) => any
|
||||
|
||||
export function Field<TParentData, TName extends DeepKeys<TParentData>>({
|
||||
export function Field<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
>({
|
||||
children,
|
||||
...fieldOptions
|
||||
}: {
|
||||
children: (fieldApi: FieldApi<TParentData, TName>) => any
|
||||
} & UseFieldOptions<TParentData, TName>) {
|
||||
children: (
|
||||
fieldApi: FieldApi<TParentData, TName, ValidatorType, FormValidator>,
|
||||
) => any
|
||||
} & UseFieldOptions<TParentData, TName, ValidatorType, FormValidator>) {
|
||||
const fieldApi = useField(fieldOptions as any)
|
||||
|
||||
return (
|
||||
<formContext.Provider
|
||||
value={{ formApi: fieldApi.form, parentFieldName: fieldApi.name }}
|
||||
value={{
|
||||
formApi: fieldApi.form as never,
|
||||
parentFieldName: fieldApi.name,
|
||||
}}
|
||||
children={functionalUpdate(children, fieldApi as any)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -9,9 +9,9 @@ import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect'
|
||||
|
||||
declare module '@tanstack/form-core' {
|
||||
// eslint-disable-next-line no-shadow
|
||||
interface FormApi<TFormData> {
|
||||
interface FormApi<TFormData, ValidatorType> {
|
||||
Provider: (props: { children: any }) => any
|
||||
Field: FieldComponent<TFormData>
|
||||
Field: FieldComponent<TFormData, ValidatorType>
|
||||
useField: UseField<TFormData>
|
||||
useStore: <TSelected = NoInfer<FormState<TFormData>>>(
|
||||
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,
|
||||
@@ -23,7 +23,9 @@ declare module '@tanstack/form-core' {
|
||||
}
|
||||
}
|
||||
|
||||
export function useForm<TData>(opts?: FormOptions<TData>): FormApi<TData> {
|
||||
export function useForm<TData, FormValidator>(
|
||||
opts?: FormOptions<TData, FormValidator>,
|
||||
): FormApi<TData, FormValidator> {
|
||||
const [formApi] = useState(() => {
|
||||
// @ts-ignore
|
||||
const api = new FormApi<TData>(opts)
|
||||
|
||||
@@ -3,19 +3,21 @@ import type { FormApi, FormOptions } from '@tanstack/form-core'
|
||||
import { type UseField, type FieldComponent, Field, useField } from './useField'
|
||||
import { useForm } from './useForm'
|
||||
|
||||
export type FormFactory<TFormData> = {
|
||||
useForm: (opts?: FormOptions<TFormData>) => FormApi<TFormData>
|
||||
useField: UseField<TFormData>
|
||||
Field: FieldComponent<TFormData>
|
||||
export type FormFactory<TFormData, FormValidator> = {
|
||||
useForm: (
|
||||
opts?: FormOptions<TFormData, FormValidator>,
|
||||
) => FormApi<TFormData, FormValidator>
|
||||
useField: UseField<TFormData, FormValidator>
|
||||
Field: FieldComponent<TFormData, FormValidator>
|
||||
}
|
||||
|
||||
export function createFormFactory<TFormData>(
|
||||
defaultOpts?: FormOptions<TFormData>,
|
||||
): FormFactory<TFormData> {
|
||||
export function createFormFactory<TFormData, FormValidator>(
|
||||
defaultOpts?: FormOptions<TFormData, FormValidator>,
|
||||
): FormFactory<TFormData, FormValidator> {
|
||||
return {
|
||||
useForm: (opts) => {
|
||||
const formOptions = Object.assign({}, defaultOpts, opts)
|
||||
return useForm<TFormData>(formOptions)
|
||||
return useForm<TFormData, FormValidator>(formOptions)
|
||||
},
|
||||
useField: useField as any,
|
||||
Field: Field as any,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { FormApi } from '@tanstack/form-core'
|
||||
import { inject, provide } from 'vue-demi'
|
||||
|
||||
export type FormContext = {
|
||||
formApi: FormApi<any>
|
||||
formApi: FormApi<any, unknown>
|
||||
parentFieldName?: string
|
||||
} | null
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('useField', () => {
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
const Comp = defineComponent(() => {
|
||||
const form = formFactory.useForm()
|
||||
@@ -30,7 +30,11 @@ describe('useField', () => {
|
||||
|
||||
return () => (
|
||||
<form.Field name="firstName" defaultValue="FirstName">
|
||||
{({ field }: { field: FieldApi<Person, 'firstName'> }) => (
|
||||
{({
|
||||
field,
|
||||
}: {
|
||||
field: FieldApi<Person, 'firstName', never, never>
|
||||
}) => (
|
||||
<input
|
||||
data-testid={'fieldinput'}
|
||||
value={field.state.value}
|
||||
@@ -56,7 +60,7 @@ describe('useField', () => {
|
||||
}
|
||||
const error = 'Please enter a different value'
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
const Comp = defineComponent(() => {
|
||||
const form = formFactory.useForm()
|
||||
@@ -68,7 +72,11 @@ describe('useField', () => {
|
||||
name="firstName"
|
||||
onChange={(value) => (value === 'other' ? error : undefined)}
|
||||
>
|
||||
{({ field }: { field: FieldApi<Person, 'firstName'> }) => (
|
||||
{({
|
||||
field,
|
||||
}: {
|
||||
field: FieldApi<Person, 'firstName', never, never>
|
||||
}) => (
|
||||
<div>
|
||||
<input
|
||||
data-testid="fieldinput"
|
||||
@@ -99,7 +107,7 @@ describe('useField', () => {
|
||||
}
|
||||
const error = 'Please enter a different value'
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
const Comp = defineComponent(() => {
|
||||
const form = formFactory.useForm()
|
||||
@@ -111,7 +119,11 @@ describe('useField', () => {
|
||||
name="firstName"
|
||||
onChange={(value) => (value === 'other' ? error : undefined)}
|
||||
>
|
||||
{({ field }: { field: FieldApi<Person, 'firstName'> }) => (
|
||||
{({
|
||||
field,
|
||||
}: {
|
||||
field: FieldApi<Person, 'firstName', never, never>
|
||||
}) => (
|
||||
<div>
|
||||
<input
|
||||
data-testid="fieldinput"
|
||||
@@ -143,7 +155,7 @@ describe('useField', () => {
|
||||
}
|
||||
const error = 'Please enter a different value'
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
const Comp = defineComponent(() => {
|
||||
const form = formFactory.useForm()
|
||||
@@ -159,7 +171,11 @@ describe('useField', () => {
|
||||
return error
|
||||
}}
|
||||
>
|
||||
{({ field }: { field: FieldApi<Person, 'firstName'> }) => (
|
||||
{({
|
||||
field,
|
||||
}: {
|
||||
field: FieldApi<Person, 'firstName', never, never>
|
||||
}) => (
|
||||
<div>
|
||||
<input
|
||||
data-testid="fieldinput"
|
||||
@@ -193,7 +209,7 @@ describe('useField', () => {
|
||||
|
||||
const mockFn = vi.fn()
|
||||
const error = 'Please enter a different value'
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
const Comp = defineComponent(() => {
|
||||
const form = formFactory.useForm()
|
||||
@@ -211,7 +227,11 @@ describe('useField', () => {
|
||||
return error
|
||||
}}
|
||||
>
|
||||
{({ field }: { field: FieldApi<Person, 'firstName'> }) => (
|
||||
{({
|
||||
field,
|
||||
}: {
|
||||
field: FieldApi<Person, 'firstName', never, never>
|
||||
}) => (
|
||||
<div>
|
||||
<input
|
||||
data-testid="fieldinput"
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('useForm', () => {
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
const Comp = defineComponent(() => {
|
||||
const form = formFactory.useForm()
|
||||
@@ -29,7 +29,11 @@ describe('useForm', () => {
|
||||
|
||||
return () => (
|
||||
<form.Field name="firstName" defaultValue="">
|
||||
{({ field }: { field: FieldApi<Person, 'firstName'> }) => (
|
||||
{({
|
||||
field,
|
||||
}: {
|
||||
field: FieldApi<Person, 'firstName', never, never>
|
||||
}) => (
|
||||
<input
|
||||
data-testid={'fieldinput'}
|
||||
value={field.state.value}
|
||||
@@ -56,7 +60,7 @@ describe('useForm', () => {
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
const formFactory = createFormFactory<Person, unknown>()
|
||||
|
||||
const Comp = defineComponent(() => {
|
||||
const form = formFactory.useForm({
|
||||
@@ -69,9 +73,11 @@ describe('useForm', () => {
|
||||
|
||||
return () => (
|
||||
<form.Field name="firstName">
|
||||
{({ field }: { field: FieldApi<Person, 'firstName'> }) => (
|
||||
<p>{field.state.value}</p>
|
||||
)}
|
||||
{({
|
||||
field,
|
||||
}: {
|
||||
field: FieldApi<Person, 'firstName', never, never>
|
||||
}) => <p>{field.state.value}</p>}
|
||||
</form.Field>
|
||||
)
|
||||
})
|
||||
@@ -81,37 +87,6 @@ describe('useForm', () => {
|
||||
expect(queryByText('LastName')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use field default value first', async () => {
|
||||
type Person = {
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const formFactory = createFormFactory<Person>()
|
||||
|
||||
const Comp = defineComponent(() => {
|
||||
const form = formFactory.useForm({
|
||||
defaultValues: {
|
||||
firstName: 'FirstName',
|
||||
lastName: 'LastName',
|
||||
},
|
||||
})
|
||||
form.provideFormContext()
|
||||
|
||||
return () => (
|
||||
<form.Field name="firstName" defaultValue="otherName">
|
||||
{({ field }: { field: FieldApi<Person, 'firstName'> }) => (
|
||||
<p>{field.state.value}</p>
|
||||
)}
|
||||
</form.Field>
|
||||
)
|
||||
})
|
||||
|
||||
const { findByText, queryByText } = render(Comp)
|
||||
expect(await findByText('otherName')).toBeInTheDocument()
|
||||
expect(queryByText('LastName')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle submitting properly', async () => {
|
||||
const Comp = defineComponent(() => {
|
||||
const submittedData = ref<{ firstName: string }>()
|
||||
@@ -132,7 +107,7 @@ describe('useForm', () => {
|
||||
{({
|
||||
field,
|
||||
}: {
|
||||
field: FieldApi<{ firstName: string }, 'firstName'>
|
||||
field: FieldApi<{ firstName: string }, 'firstName', never, never>
|
||||
}) => {
|
||||
return (
|
||||
<input
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { FieldOptions, DeepKeys, DeepValue } from '@tanstack/form-core'
|
||||
export type UseFieldOptions<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
TData = DeepValue<TParentData, TName>,
|
||||
> = FieldOptions<TParentData, TName, TData> & {
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
> = FieldOptions<TParentData, TName, ValidatorType, FormValidator, TData> & {
|
||||
mode?: 'value' | 'array'
|
||||
}
|
||||
|
||||
@@ -11,30 +11,54 @@ declare module '@tanstack/form-core' {
|
||||
interface FieldApi<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData = DeepValue<TParentData, TName>,
|
||||
> {
|
||||
Field: FieldComponent<TData>
|
||||
Field: FieldComponent<TData, FormValidator>
|
||||
}
|
||||
}
|
||||
|
||||
export type UseField<TParentData> = <TName extends DeepKeys<TParentData>>(
|
||||
export type UseField<TParentData, FormValidator> = <
|
||||
TName extends DeepKeys<TParentData>,
|
||||
ValidatorType,
|
||||
>(
|
||||
opts?: { name: Narrow<TName> } & UseFieldOptions<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
DeepValue<TParentData, TName>
|
||||
>,
|
||||
) => FieldApi<TParentData, TName, DeepValue<TParentData, TName>>
|
||||
) => FieldApi<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
DeepValue<TParentData, TName>
|
||||
>
|
||||
|
||||
export function useField<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
TData = DeepValue<TParentData, TName>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
>(
|
||||
opts: UseFieldOptions<TParentData, TName>,
|
||||
opts: UseFieldOptions<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData
|
||||
>,
|
||||
): {
|
||||
api: FieldApi<
|
||||
TParentData,
|
||||
TName
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData
|
||||
// Omit<typeof opts, 'onMount'> & {
|
||||
// form: FormApi<TParentData>
|
||||
// }
|
||||
@@ -44,6 +68,8 @@ export function useField<
|
||||
FieldApi<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData
|
||||
// Omit<typeof opts, 'onMount'> & {
|
||||
// form: FormApi<TParentData>
|
||||
@@ -98,6 +124,8 @@ export type FieldValue<TParentData, TName> = TParentData extends any[]
|
||||
type FieldComponentProps<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
> = (TParentData extends any[]
|
||||
? {
|
||||
name?: TName
|
||||
@@ -107,27 +135,52 @@ type FieldComponentProps<
|
||||
name: TName
|
||||
index?: never
|
||||
}) &
|
||||
Omit<UseFieldOptions<TParentData, TName>, 'name' | 'index'>
|
||||
Omit<
|
||||
UseFieldOptions<TParentData, TName, ValidatorType, FormValidator>,
|
||||
'name' | 'index'
|
||||
>
|
||||
|
||||
export type FieldComponent<TParentData> = <
|
||||
export type FieldComponent<TParentData, FormValidator> = <
|
||||
TName extends DeepKeys<TParentData>,
|
||||
TData = DeepValue<TParentData, TName>,
|
||||
ValidatorType,
|
||||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||
>(
|
||||
fieldOptions: FieldComponentProps<TParentData, TName>,
|
||||
fieldOptions: FieldComponentProps<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator
|
||||
>,
|
||||
context: SetupContext<
|
||||
{},
|
||||
SlotsType<{
|
||||
default: {
|
||||
field: FieldApi<TParentData, TName, TData>
|
||||
state: FieldApi<TParentData, TName, TData>['state']
|
||||
field: FieldApi<TParentData, TName, ValidatorType, FormValidator, TData>
|
||||
state: FieldApi<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
TData
|
||||
>['state']
|
||||
}
|
||||
}>
|
||||
>,
|
||||
) => any
|
||||
|
||||
export const Field = defineComponent(
|
||||
<TParentData, TName extends DeepKeys<TParentData>>(
|
||||
fieldOptions: UseFieldOptions<TParentData, TName>,
|
||||
<
|
||||
TParentData,
|
||||
TName extends DeepKeys<TParentData>,
|
||||
ValidatorType,
|
||||
FormValidator,
|
||||
>(
|
||||
fieldOptions: UseFieldOptions<
|
||||
TParentData,
|
||||
TName,
|
||||
ValidatorType,
|
||||
FormValidator
|
||||
>,
|
||||
context: SetupContext,
|
||||
) => {
|
||||
const fieldApi = useField({ ...fieldOptions, ...context.attrs } as any)
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
|
||||
declare module '@tanstack/form-core' {
|
||||
// eslint-disable-next-line no-shadow
|
||||
interface FormApi<TFormData> {
|
||||
interface FormApi<TFormData, ValidatorType> {
|
||||
Provider: (props: Record<string, any> & {}) => any
|
||||
provideFormContext: () => void
|
||||
Field: FieldComponent<TFormData>
|
||||
useField: UseField<TFormData>
|
||||
Field: FieldComponent<TFormData, ValidatorType>
|
||||
useField: UseField<TFormData, ValidatorType>
|
||||
useStore: <TSelected = NoInfer<FormState<TFormData>>>(
|
||||
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,
|
||||
) => TSelected
|
||||
@@ -31,19 +31,21 @@ declare module '@tanstack/form-core' {
|
||||
}
|
||||
}
|
||||
|
||||
export function useForm<TData>(opts?: FormOptions<TData>): FormApi<TData> {
|
||||
export function useForm<TData, FormValidator>(
|
||||
opts?: FormOptions<TData, FormValidator>,
|
||||
): FormApi<TData, FormValidator> {
|
||||
const formApi = (() => {
|
||||
const api = new FormApi<TData>(opts)
|
||||
const api = new FormApi<TData, FormValidator>(opts)
|
||||
|
||||
api.Provider = defineComponent(
|
||||
(_, context) => {
|
||||
provideFormContext({ formApi })
|
||||
provideFormContext({ formApi: formApi as never })
|
||||
return () => context.slots.default!()
|
||||
},
|
||||
{ name: 'Provider' },
|
||||
)
|
||||
api.provideFormContext = () => {
|
||||
provideFormContext({ formApi })
|
||||
provideFormContext({ formApi: formApi as never })
|
||||
}
|
||||
api.Field = Field as never
|
||||
api.useField = useField as never
|
||||
|
||||
11
packages/yup-form-adapter/.eslintrc.cjs
Normal file
11
packages/yup-form-adapter/.eslintrc.cjs
Normal file
@@ -0,0 +1,11 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.eslint.json',
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
62
packages/yup-form-adapter/package.json
Normal file
62
packages/yup-form-adapter/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@tanstack/yup-form-adapter",
|
||||
"version": "0.3.6",
|
||||
"description": "The Yup adapter for TanStack Form.",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": "tanstack/form",
|
||||
"homepage": "https://tanstack.com/form",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "build/legacy/index.d.ts",
|
||||
"main": "build/legacy/index.cjs",
|
||||
"module": "build/legacy/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./build/modern/index.d.ts",
|
||||
"default": "./build/modern/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./build/modern/index.d.cts",
|
||||
"default": "./build/modern/index.cjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"build",
|
||||
"src"
|
||||
],
|
||||
"nx": {
|
||||
"targets": {
|
||||
"test:build": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build && rimraf ./coverage",
|
||||
"test:eslint": "eslint --ext .ts,.tsx ./src",
|
||||
"test:types": "tsc --noEmit && vitest typecheck",
|
||||
"test:lib": "vitest run --coverage",
|
||||
"test:lib:dev": "pnpm run test:lib --watch",
|
||||
"test:build": "publint --strict",
|
||||
"build": "tsup"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/form-core": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"yup": "^1.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"yup": "^1.3.2"
|
||||
}
|
||||
}
|
||||
1
packages/yup-form-adapter/src/index.ts
Normal file
1
packages/yup-form-adapter/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './validator'
|
||||
112
packages/yup-form-adapter/src/tests/FieldApi.spec.ts
Normal file
112
packages/yup-form-adapter/src/tests/FieldApi.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import { FormApi, FieldApi } from '@tanstack/form-core'
|
||||
import { yupValidator } from '../validator'
|
||||
import yup from 'yup'
|
||||
import { sleep } from './utils'
|
||||
|
||||
describe('yup field api', () => {
|
||||
it('should run an onChange with yup.string validation', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
validator: yupValidator,
|
||||
name: 'name',
|
||||
onChange: yup.string().min(3, 'You must have a length of at least 3'),
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([
|
||||
'You must have a length of at least 3',
|
||||
])
|
||||
field.setValue('asdf', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
|
||||
it('should run an onChange fn with yup validation option enabled', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
validator: yupValidator,
|
||||
name: 'name',
|
||||
onChange: (val) => (val === 'a' ? 'Test' : undefined),
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual(['Test'])
|
||||
field.setValue('asdf', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
|
||||
it('should run an onChangeAsync with z.string validation', async () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
validator: yupValidator,
|
||||
name: 'name',
|
||||
onChangeAsync: yup
|
||||
.string()
|
||||
.test('Testing 123', 'Testing 123', async (val) =>
|
||||
typeof val === 'string' ? val.length > 3 : false,
|
||||
),
|
||||
onChangeAsyncDebounceMs: 0,
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
await sleep(10)
|
||||
expect(field.getMeta().errors).toEqual(['Testing 123'])
|
||||
field.setValue('asdf', { touch: true })
|
||||
await sleep(10)
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
|
||||
it('should run an onChangeAsyc fn with zod validation option enabled', async () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
validator: yupValidator,
|
||||
name: 'name',
|
||||
onChangeAsync: async (val) => (val === 'a' ? 'Test' : undefined),
|
||||
onChangeAsyncDebounceMs: 0,
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
await sleep(10)
|
||||
expect(field.getMeta().errors).toEqual(['Test'])
|
||||
field.setValue('asdf', { touch: true })
|
||||
await sleep(10)
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
})
|
||||
82
packages/yup-form-adapter/src/tests/FieldApi.test-d.ts
Normal file
82
packages/yup-form-adapter/src/tests/FieldApi.test-d.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { yupValidator } from '../validator'
|
||||
import yup from 'yup'
|
||||
import { FieldApi, FormApi } from '@tanstack/form-core'
|
||||
import { assertType } from 'vitest'
|
||||
|
||||
it('should allow a Zod validator to be passed in', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
} as const)
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
validator: yupValidator,
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should allow a Zod validator to handle the correct Zod type', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
validator: yupValidator,
|
||||
onChange: yup.string(),
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should allow a functional onChange to be passed when using a validator', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
validator: yupValidator,
|
||||
onChange: (val) => {
|
||||
assertType<'test'>(val)
|
||||
return undefined
|
||||
},
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should not allow a validator onChange to be passed when not using a validator', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
// @ts-expect-error Requires a validator
|
||||
onChange: z.string(),
|
||||
} as const)
|
||||
})
|
||||
|
||||
// This is not possible without higher-kinded types AFAIK
|
||||
it.skip('should allow not a Zod validator with the wrong Zod type', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
validator: yupValidator,
|
||||
onChange: yup.object({}),
|
||||
} as const)
|
||||
})
|
||||
55
packages/yup-form-adapter/src/tests/FormApi.spec.ts
Normal file
55
packages/yup-form-adapter/src/tests/FormApi.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import { FormApi, FieldApi } from '@tanstack/form-core'
|
||||
import { yupValidator } from '../validator'
|
||||
import yup from 'yup'
|
||||
|
||||
describe('yup form api', () => {
|
||||
it('should run an onChange with z.string validation', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
validator: yupValidator,
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
onChange: yup.string().min(3, 'You must have a length of at least 3'),
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([
|
||||
'You must have a length of at least 3',
|
||||
])
|
||||
field.setValue('asdf', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
|
||||
it('should run an onChange fn with zod validation option enabled', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
validator: yupValidator,
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
onChange: (val) => (val === 'a' ? 'Test' : undefined),
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual(['Test'])
|
||||
field.setValue('asdf', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
})
|
||||
77
packages/yup-form-adapter/src/tests/FormApi.test-d.ts
Normal file
77
packages/yup-form-adapter/src/tests/FormApi.test-d.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import yup from 'yup'
|
||||
import { yupValidator } from '../validator'
|
||||
import { FieldApi, FormApi } from '@tanstack/form-core'
|
||||
import { assertType } from 'vitest'
|
||||
|
||||
it('should allow a Zod validator to be passed in', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
validator: yupValidator,
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should allow a Zod validator to handle the correct Zod type', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
validator: yupValidator,
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
onChange: yup.string(),
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should allow a functional onChange to be passed when using a validator', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
validator: yupValidator,
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
onChange: (val) => {
|
||||
assertType<'test'>(val)
|
||||
return undefined
|
||||
},
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should not allow a validator onChange to be passed when not using a validator', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
// @ts-expect-error Requires a validator
|
||||
onChange: yup.string(),
|
||||
} as const)
|
||||
})
|
||||
|
||||
// This is not possible without higher-kinded types AFAIK
|
||||
it.skip('should allow not a Zod validator with the wrong Zod type', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
validator: yupValidator,
|
||||
onChange: yup.object({}),
|
||||
} as const)
|
||||
})
|
||||
5
packages/yup-form-adapter/src/tests/utils.ts
Normal file
5
packages/yup-form-adapter/src/tests/utils.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function sleep(timeout: number): Promise<void> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
setTimeout(resolve, timeout)
|
||||
})
|
||||
}
|
||||
25
packages/yup-form-adapter/src/validator.ts
Normal file
25
packages/yup-form-adapter/src/validator.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ValidationError as YupError, AnySchema } from 'yup'
|
||||
import type { ValidationError, Validator } from '@tanstack/form-core'
|
||||
|
||||
export const yupValidator = (<Fn extends AnySchema = AnySchema>() => {
|
||||
return {
|
||||
validate(value: unknown, fn: Fn): ValidationError {
|
||||
try {
|
||||
fn.validateSync(value)
|
||||
return
|
||||
} catch (_e) {
|
||||
const e = _e as YupError
|
||||
return e.errors.join(', ')
|
||||
}
|
||||
},
|
||||
async validateAsync(value: unknown, fn: Fn): Promise<ValidationError> {
|
||||
try {
|
||||
await fn.validate(value)
|
||||
return
|
||||
} catch (_e) {
|
||||
const e = _e as YupError
|
||||
return e.errors.join(', ')
|
||||
}
|
||||
},
|
||||
}
|
||||
}) satisfies Validator<unknown>
|
||||
7
packages/yup-form-adapter/tsconfig.eslint.json
Normal file
7
packages/yup-form-adapter/tsconfig.eslint.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", ".eslintrc.cjs", "tsup.config.js"]
|
||||
}
|
||||
9
packages/yup-form-adapter/tsconfig.json
Normal file
9
packages/yup-form-adapter/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"composite": true,
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./build/lib",
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
9
packages/yup-form-adapter/tsup.config.js
Normal file
9
packages/yup-form-adapter/tsup.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// @ts-check
|
||||
|
||||
import { defineConfig } from 'tsup'
|
||||
import { legacyConfig, modernConfig } from '../../getTsupConfig.js'
|
||||
|
||||
export default defineConfig([
|
||||
modernConfig({ entry: ['src/*.ts'] }),
|
||||
legacyConfig({ entry: ['src/*.ts'] }),
|
||||
])
|
||||
12
packages/yup-form-adapter/vitest.config.ts
Normal file
12
packages/yup-form-adapter/vitest.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
name: 'form-core',
|
||||
dir: './src',
|
||||
watch: false,
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
coverage: { provider: 'istanbul' },
|
||||
},
|
||||
})
|
||||
11
packages/zod-form-adapter/.eslintrc.cjs
Normal file
11
packages/zod-form-adapter/.eslintrc.cjs
Normal file
@@ -0,0 +1,11 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.eslint.json',
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
62
packages/zod-form-adapter/package.json
Normal file
62
packages/zod-form-adapter/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@tanstack/zod-form-adapter",
|
||||
"version": "0.3.6",
|
||||
"description": "The Zod adapter for TanStack Form.",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": "tanstack/form",
|
||||
"homepage": "https://tanstack.com/form",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "build/legacy/index.d.ts",
|
||||
"main": "build/legacy/index.cjs",
|
||||
"module": "build/legacy/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./build/modern/index.d.ts",
|
||||
"default": "./build/modern/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./build/modern/index.d.cts",
|
||||
"default": "./build/modern/index.cjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"build",
|
||||
"src"
|
||||
],
|
||||
"nx": {
|
||||
"targets": {
|
||||
"test:build": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build && rimraf ./coverage",
|
||||
"test:eslint": "eslint --ext .ts,.tsx ./src",
|
||||
"test:types": "tsc --noEmit && vitest typecheck",
|
||||
"test:lib": "vitest run --coverage",
|
||||
"test:lib:dev": "pnpm run test:lib --watch",
|
||||
"test:build": "publint --strict",
|
||||
"build": "tsup"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/form-core": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
1
packages/zod-form-adapter/src/index.ts
Normal file
1
packages/zod-form-adapter/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './validator'
|
||||
110
packages/zod-form-adapter/src/tests/FieldApi.spec.ts
Normal file
110
packages/zod-form-adapter/src/tests/FieldApi.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import { FormApi, FieldApi } from '@tanstack/form-core'
|
||||
import { zodValidator } from '../validator'
|
||||
import { z } from 'zod'
|
||||
import { sleep } from './utils'
|
||||
|
||||
describe('zod field api', () => {
|
||||
it('should run an onChange with z.string validation', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
validator: zodValidator,
|
||||
name: 'name',
|
||||
onChange: z.string().min(3, 'You must have a length of at least 3'),
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([
|
||||
'You must have a length of at least 3',
|
||||
])
|
||||
field.setValue('asdf', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
|
||||
it('should run an onChange fn with zod validation option enabled', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
validator: zodValidator,
|
||||
name: 'name',
|
||||
onChange: (val) => (val === 'a' ? 'Test' : undefined),
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual(['Test'])
|
||||
field.setValue('asdf', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
|
||||
it('should run an onChangeAsync with z.string validation', async () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
validator: zodValidator,
|
||||
name: 'name',
|
||||
onChangeAsync: z.string().refine(async (val) => val.length > 3, {
|
||||
message: 'Testing 123',
|
||||
}),
|
||||
onChangeAsyncDebounceMs: 0,
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
await sleep(10)
|
||||
expect(field.getMeta().errors).toEqual(['Testing 123'])
|
||||
field.setValue('asdf', { touch: true })
|
||||
await sleep(10)
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
|
||||
it('should run an onChangeAsyc fn with zod validation option enabled', async () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
validator: zodValidator,
|
||||
name: 'name',
|
||||
onChangeAsync: async (val) => (val === 'a' ? 'Test' : undefined),
|
||||
onChangeAsyncDebounceMs: 0,
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
await sleep(10)
|
||||
expect(field.getMeta().errors).toEqual(['Test'])
|
||||
field.setValue('asdf', { touch: true })
|
||||
await sleep(10)
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
})
|
||||
97
packages/zod-form-adapter/src/tests/FieldApi.test-d.ts
Normal file
97
packages/zod-form-adapter/src/tests/FieldApi.test-d.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { zodValidator } from '../validator'
|
||||
import { z } from 'zod'
|
||||
import { FieldApi, FormApi } from '@tanstack/form-core'
|
||||
import { assertType } from 'vitest'
|
||||
|
||||
it('should allow a Zod validator to be passed in', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
} as const)
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
validator: zodValidator,
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should allow a Zod validator to handle the correct Zod type', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
validator: zodValidator,
|
||||
onChange: z.string(),
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should allow a Zod validator to handle the correct Zod type for an async method', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
validator: zodValidator,
|
||||
onChangeAsync: z.string(),
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should allow a functional onChange to be passed when using a validator', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
validator: zodValidator,
|
||||
onChange: (val) => {
|
||||
assertType<'test'>(val)
|
||||
return undefined
|
||||
},
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should not allow a validator onChange to be passed when not using a validator', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
// @ts-expect-error Requires a validator
|
||||
onChange: z.string(),
|
||||
} as const)
|
||||
})
|
||||
|
||||
// This is not possible without higher-kinded types AFAIK
|
||||
it.skip('should allow not a Zod validator with the wrong Zod type', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
validator: zodValidator,
|
||||
onChange: z.object({}),
|
||||
} as const)
|
||||
})
|
||||
55
packages/zod-form-adapter/src/tests/FormApi.spec.ts
Normal file
55
packages/zod-form-adapter/src/tests/FormApi.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import { FormApi, FieldApi } from '@tanstack/form-core'
|
||||
import { zodValidator } from '../validator'
|
||||
import { z } from 'zod'
|
||||
|
||||
describe('zod form api', () => {
|
||||
it('should run an onChange with z.string validation', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
validator: zodValidator,
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
onChange: z.string().min(3, 'You must have a length of at least 3'),
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([
|
||||
'You must have a length of at least 3',
|
||||
])
|
||||
field.setValue('asdf', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
|
||||
it('should run an onChange fn with zod validation option enabled', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
validator: zodValidator,
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
onChange: (val) => (val === 'a' ? 'Test' : undefined),
|
||||
})
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
field.setValue('a', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual(['Test'])
|
||||
field.setValue('asdf', { touch: true })
|
||||
expect(field.getMeta().errors).toEqual([])
|
||||
})
|
||||
})
|
||||
92
packages/zod-form-adapter/src/tests/FormApi.test-d.ts
Normal file
92
packages/zod-form-adapter/src/tests/FormApi.test-d.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { z } from 'zod'
|
||||
import { zodValidator } from '../validator'
|
||||
import { FieldApi, FormApi } from '@tanstack/form-core'
|
||||
import { assertType } from 'vitest'
|
||||
|
||||
it('should allow a Zod validator to be passed in', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
validator: zodValidator,
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should allow a Zod validator to handle the correct Zod type', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
validator: zodValidator,
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
onChange: z.string(),
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should allow a Zod validator to handle the correct Zod type on async methods', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
validator: zodValidator,
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
onChangeAsync: z.string(),
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should allow a functional onChange to be passed when using a validator', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
validator: zodValidator,
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
onChange: (val) => {
|
||||
assertType<'test'>(val)
|
||||
return undefined
|
||||
},
|
||||
} as const)
|
||||
})
|
||||
|
||||
it('should not allow a validator onChange to be passed when not using a validator', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
// @ts-expect-error Requires a validator
|
||||
onChange: z.string(),
|
||||
} as const)
|
||||
})
|
||||
|
||||
// This is not possible without higher-kinded types AFAIK
|
||||
it.skip('should allow not a Zod validator with the wrong Zod type', () => {
|
||||
const form = new FormApi({
|
||||
defaultValues: {
|
||||
name: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const field = new FieldApi({
|
||||
form,
|
||||
name: 'name',
|
||||
validator: zodValidator,
|
||||
onChange: z.object({}),
|
||||
} as const)
|
||||
})
|
||||
5
packages/zod-form-adapter/src/tests/utils.ts
Normal file
5
packages/zod-form-adapter/src/tests/utils.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function sleep(timeout: number): Promise<void> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
setTimeout(resolve, timeout)
|
||||
})
|
||||
}
|
||||
23
packages/zod-form-adapter/src/validator.ts
Normal file
23
packages/zod-form-adapter/src/validator.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { ZodType, ZodTypeAny } from 'zod'
|
||||
import type { ValidationError, Validator } from '@tanstack/form-core'
|
||||
|
||||
export const zodValidator = (<Fn extends ZodType = ZodType>() => {
|
||||
return {
|
||||
validate(value: unknown, fn: Fn): ValidationError {
|
||||
// Call Zod on the value here and return the error message
|
||||
const result = (fn as ZodTypeAny).safeParse(value)
|
||||
if (!result.success) {
|
||||
return result.error.issues.map((issue) => issue.message).join(', ')
|
||||
}
|
||||
return
|
||||
},
|
||||
async validateAsync(value: unknown, fn: Fn): Promise<ValidationError> {
|
||||
// Call Zod on the value here and return the error message
|
||||
const result = await (fn as ZodTypeAny).safeParseAsync(value)
|
||||
if (!result.success) {
|
||||
return result.error.issues.map((issue) => issue.message).join(', ')
|
||||
}
|
||||
return
|
||||
},
|
||||
}
|
||||
}) satisfies Validator<unknown>
|
||||
7
packages/zod-form-adapter/tsconfig.eslint.json
Normal file
7
packages/zod-form-adapter/tsconfig.eslint.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", ".eslintrc.cjs", "tsup.config.js"]
|
||||
}
|
||||
9
packages/zod-form-adapter/tsconfig.json
Normal file
9
packages/zod-form-adapter/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"composite": true,
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./build/lib",
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user