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:
Corbin Crutchley
2023-10-18 02:22:05 -07:00
committed by GitHub
parent b35ecd1107
commit 54652ee674
104 changed files with 4311 additions and 1932 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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'
}

View File

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

View File

@@ -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