mirror of
https://github.com/LukeHagar/form.git
synced 2025-12-09 20:37:47 +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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user