From ad6b2e1a9f5bdcb77f724a31d870309d9b09789a Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 28 Jan 2024 01:29:16 -0800 Subject: [PATCH 01/23] feat: support array notation in DeepKeys and DeepValue types --- packages/form-core/src/FieldApi.ts | 3 +- packages/form-core/src/FormApi.ts | 3 +- .../form-core/src/tests/util-types.test-d.ts | 111 ++++++++++++++++++ packages/form-core/src/util-types.ts | 101 ++++++++++++++++ packages/form-core/src/utils.ts | 74 ------------ 5 files changed, 216 insertions(+), 76 deletions(-) create mode 100644 packages/form-core/src/tests/util-types.test-d.ts create mode 100644 packages/form-core/src/util-types.ts diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 6ea34631c..d0adcec48 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -6,7 +6,8 @@ import type { ValidationErrorMap, Validator, } from './types' -import type { DeepKeys, DeepValue, Updater } from './utils' +import type { Updater } from './utils' +import type { DeepKeys, DeepValue } from './util-types' import { getAsyncValidatorArray, getSyncValidatorArray } from './utils' export type FieldValidateFn< diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 473027df4..7e84b373a 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1,5 +1,6 @@ import { Store } from '@tanstack/store' -import type { DeepKeys, DeepValue, Updater } from './utils' +import type { Updater } from './utils' +import type { DeepKeys, DeepValue } from './util-types' import { getAsyncValidatorArray, getSyncValidatorArray, diff --git a/packages/form-core/src/tests/util-types.test-d.ts b/packages/form-core/src/tests/util-types.test-d.ts new file mode 100644 index 000000000..652514209 --- /dev/null +++ b/packages/form-core/src/tests/util-types.test-d.ts @@ -0,0 +1,111 @@ +import { DeepKeys, DeepValue } from "../util-types"; + +/** + * Properly recognizes that `0` is not an object and should not have subkeys + */ +type TupleSupport = DeepKeys<{ topUsers: [User, 0, User] }>; +assertType<"topUsers" | "topUsers[0]" | "topUsers[0].name" | "topUsers[0].id" | "topUsers[0].age" | "topUsers[1]" | "topUsers[2]" | "topUsers[2].name" | "topUsers[2].id" | "topUsers[2].age">( + 0 as never as TupleSupport +) + +/** + * Properly recognizes that a normal number index won't cut it and should be `[number]` prefixed instead + */ +type ArraySupport = DeepKeys<{ users: User[] }>; +assertType<"users" | "users[number].name" | "users[number].id" | "users[number].age">( + 0 as never as ArraySupport +) + +/** + * Properly handles deep object nesting like so: + */ +type NestedSupport = DeepKeys<{ meta: { mainUser: User } }>; +assertType<"meta" | "meta.mainUser" | "meta.mainUser.name" | "meta.mainUser.id" | "meta.mainUser.age">( + 0 as never as NestedSupport +) + +/** + * Properly handles `object` edgecase nesting like so: + */ +type ObjectNestedEdgecase = DeepKeys<{ meta: { mainUser: object } }>; +assertType<"meta" | "meta.mainUser" | `meta.mainUser.${string}`>( + 0 as never as ObjectNestedEdgecase +) + +/** + * Properly handles `object` edgecase like so: + */ +type ObjectEdgecase = DeepKeys; +assertType( + 0 as never as ObjectEdgecase +) + +/** + * Properly handles `object` edgecase nesting like so: + */ +type UnknownNestedEdgecase = DeepKeys<{ meta: { mainUser: unknown } }>; +assertType<"meta" | "meta.mainUser" | `meta.mainUser.${string}`>( + 0 as never as UnknownNestedEdgecase +) + +/** + * Properly handles `object` edgecase like so: + */ +type UnknownEdgecase = DeepKeys; +assertType( + 0 as never as UnknownEdgecase +) + +type NestedKeysExample = DeepValue< + { meta: { mainUser: User } }, + "meta.mainUser.age" +>; +assertType( + 0 as never as NestedKeysExample +) + +type NestedArrayExample = DeepValue<{ users: User[] }, "users[0].age">; +assertType( + 0 as never as NestedArrayExample +) + +type NestedLooseArrayExample = DeepValue< + { users: User[] }, + "users[number].age" +>; +assertType( + 0 as never as NestedLooseArrayExample +) + +type NestedTupleExample = DeepValue< + { topUsers: [User, 0, User] }, + "topUsers[0].age" +>; +assertType( + 0 as never as NestedTupleExample +) + +type NestedTupleItemExample = DeepValue< + { topUsers: [User, 0, User] }, + "topUsers[1]" +>; +assertType<0>( + 0 as never as NestedTupleItemExample +) + +type ArrayExample = DeepValue<[1,2,3], "[1]"> +assertType<2>( + 0 as never as ArrayExample +) + +type NonNestedObjExample = DeepValue<{a: 1}, "a"> +assertType<1>( + 0 as never as NonNestedObjExample +) + +interface User { + name: string; + id: string; + age: number; + } + \ No newline at end of file diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts new file mode 100644 index 000000000..9c0b6e45b --- /dev/null +++ b/packages/form-core/src/util-types.ts @@ -0,0 +1,101 @@ +export type RequiredByKey = Omit & + Required> + +type Narrowable = string | number | bigint | boolean + +type NarrowRaw = + | (A extends [] ? [] : never) + | (A extends Narrowable ? A : never) + | { + [K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw + } + +export type Narrow = Try> + +type Try = A1 extends A2 ? A1 : Catch + +// Hack to get TypeScript to show simplified types in error messages +export type Pretty = { [K in keyof T]: T[K] } & {} + +type ComputeRange< + N extends number, + Result extends Array = [], +> = Result["length"] extends N + ? Result + : ComputeRange; +type Index40 = ComputeRange<40>[number]; + +// Is this type a tuple? +type IsTuple = T extends readonly any[] & { length: infer Length } + ? Length extends Index40 + ? T + : never + : never; + +// If this type is a tuple, what indices are allowed? +type AllowedIndexes< + Tuple extends ReadonlyArray, + Keys extends number = never, +> = Tuple extends readonly [] + ? Keys + : Tuple extends readonly [infer _, ...infer Tail] + ? AllowedIndexes + : Keys; + +type PrefixArrayAccessor = { + [K in keyof T]: `[number]${DeepKeys}`; +}[number]; + +type PrefixTupleAccessor< + T extends any[], + TIndex extends number, + TDepth extends any[], +> = { + [K in TIndex]: `[${K}]` | `[${K}]${DeepKeys}`; +}[TIndex]; + +type PrefixObjectAccessor = { + [K in keyof T]: K extends string | number + ? + | PrefixFromDepth + | `${PrefixFromDepth}${DeepKeys}` + : never; +}[keyof T]; + +export type DeepKeys = TDepth["length"] extends 5 + ? never + : unknown extends T + ? PrefixFromDepth + : object extends T + ? PrefixFromDepth + : T extends readonly any[] & IsTuple + ? PrefixTupleAccessor, TDepth> + : T extends any[] + ? PrefixArrayAccessor + : T extends Date + ? never + : T extends object + ? PrefixObjectAccessor + : never; + +type PrefixFromDepth< + T extends string | number, + TDepth extends any[], +> = TDepth["length"] extends 0 ? T : `.${T}`; + +export type DeepValue = TValue extends Record< + string | number, + any +> + ? TAccessor extends `${infer TBefore}[${infer TBrackets}].${infer TAfter}` + ? TBrackets extends "number" + ? DeepValue + : DeepValue + : TAccessor extends `[${infer TBrackets}]` + ? DeepValue + : TAccessor extends `${infer TBefore}[${infer TBrackets}]` + ? DeepValue + : TAccessor extends `${infer TBefore}.${infer TAfter}` + ? DeepValue + : TValue[TAccessor & string] + : never; \ No newline at end of file diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index db2909687..e74d8c054 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -267,77 +267,3 @@ export function getSyncValidatorArray( return [changeValidator, serverValidator] as never } } - -export type RequiredByKey = Omit & - Required> - -type ComputeRange< - N extends number, - Result extends Array = [], -> = Result['length'] extends N - ? Result - : ComputeRange -type Index40 = ComputeRange<40>[number] - -// Is this type a tuple? -type IsTuple = T extends readonly any[] & { length: infer Length } - ? Length extends Index40 - ? T - : never - : never - -// If this type is a tuple, what indices are allowed? -type AllowedIndexes< - Tuple extends ReadonlyArray, - Keys extends number = never, -> = Tuple extends readonly [] - ? Keys - : Tuple extends readonly [infer _, ...infer Tail] - ? AllowedIndexes - : Keys - -export type DeepKeys = TDepth['length'] extends 5 - ? never - : unknown extends T - ? string - : object extends T - ? string - : T extends readonly any[] & IsTuple - ? AllowedIndexes | DeepKeysPrefix, TDepth> - : T extends any[] - ? DeepKeys - : T extends Date - ? never - : T extends object - ? (keyof T & string) | DeepKeysPrefix - : never - -type DeepKeysPrefix< - T, - TPrefix, - TDepth extends any[], -> = TPrefix extends keyof T & (number | string) - ? `${TPrefix}.${DeepKeys & string}` - : never - -export type DeepValue = T extends Record - ? TProp extends `${infer TBranch}.${infer TDeepProp}` - ? DeepValue - : T[TProp & string] - : never - -type Narrowable = string | number | bigint | boolean - -type NarrowRaw = - | (A extends [] ? [] : never) - | (A extends Narrowable ? A : never) - | { - [K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw - } - -export type Narrow = Try> - -type Try = A1 extends A2 ? A1 : Catch - -// Hack to get TypeScript to show simplified types in error messages -export type Pretty = { [K in keyof T]: T[K] } & {} From 41afa97d8d1cad1cad5cf09bf35224000623f7df Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 28 Jan 2024 03:22:56 -0800 Subject: [PATCH 02/23] fix: support numerical values for arrays in DeepValue better --- packages/form-core/src/tests/FormApi.spec.ts | 6 ++--- .../form-core/src/tests/util-types.test-d.ts | 25 +++++++++---------- packages/form-core/src/util-types.ts | 6 ++--- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/form-core/src/tests/FormApi.spec.ts b/packages/form-core/src/tests/FormApi.spec.ts index 207277203..8b9a732a1 100644 --- a/packages/form-core/src/tests/FormApi.spec.ts +++ b/packages/form-core/src/tests/FormApi.spec.ts @@ -272,7 +272,7 @@ describe('form api', () => { const fieldInArray = new FieldApi({ form, - name: `employees.${0}.firstName`, + name: `employees[0].firstName`, defaultValue: 'Darcy', }) fieldInArray.mount() @@ -300,11 +300,11 @@ describe('form api', () => { const fieldInArray = new FieldApi({ form, - name: `employees.${0}.firstName`, + name: `employees[0].firstName`, defaultValue: 'Darcy', }) fieldInArray.mount() - form.deleteField(`employees.${0}.firstName`) + form.deleteField(`employees[0].firstName`) expect(field.state.value.length).toBe(1) expect(Object.keys(field.state.value[0]!).length).toBe(0) }) diff --git a/packages/form-core/src/tests/util-types.test-d.ts b/packages/form-core/src/tests/util-types.test-d.ts index 652514209..214390ed7 100644 --- a/packages/form-core/src/tests/util-types.test-d.ts +++ b/packages/form-core/src/tests/util-types.test-d.ts @@ -12,7 +12,7 @@ assertType<"topUsers" | "topUsers[0]" | "topUsers[0].name" | "topUsers[0].id" | * Properly recognizes that a normal number index won't cut it and should be `[number]` prefixed instead */ type ArraySupport = DeepKeys<{ users: User[] }>; -assertType<"users" | "users[number].name" | "users[number].id" | "users[number].age">( +assertType<"users" | `users[${number}].name` | `users[${number}].id` | `users[${number}].age`>( 0 as never as ArraySupport ) @@ -57,8 +57,8 @@ assertType( ) type NestedKeysExample = DeepValue< - { meta: { mainUser: User } }, - "meta.mainUser.age" + { meta: { mainUser: User } }, + "meta.mainUser.age" >; assertType( 0 as never as NestedKeysExample @@ -70,35 +70,35 @@ assertType( ) type NestedLooseArrayExample = DeepValue< - { users: User[] }, - "users[number].age" + { users: User[] }, + "users[number].age" >; assertType( 0 as never as NestedLooseArrayExample ) type NestedTupleExample = DeepValue< - { topUsers: [User, 0, User] }, - "topUsers[0].age" + { topUsers: [User, 0, User] }, + "topUsers[0].age" >; assertType( 0 as never as NestedTupleExample ) type NestedTupleItemExample = DeepValue< - { topUsers: [User, 0, User] }, - "topUsers[1]" + { topUsers: [User, 0, User] }, + "topUsers[1]" >; assertType<0>( 0 as never as NestedTupleItemExample ) -type ArrayExample = DeepValue<[1,2,3], "[1]"> +type ArrayExample = DeepValue<[1, 2, 3], "[1]"> assertType<2>( 0 as never as ArrayExample ) -type NonNestedObjExample = DeepValue<{a: 1}, "a"> +type NonNestedObjExample = DeepValue<{ a: 1 }, "a"> assertType<1>( 0 as never as NonNestedObjExample ) @@ -107,5 +107,4 @@ interface User { name: string; id: string; age: number; - } - \ No newline at end of file +} diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts index 9c0b6e45b..cda2c55a7 100644 --- a/packages/form-core/src/util-types.ts +++ b/packages/form-core/src/util-types.ts @@ -43,7 +43,7 @@ type AllowedIndexes< : Keys; type PrefixArrayAccessor = { - [K in keyof T]: `[number]${DeepKeys}`; + [K in keyof T]: `[${number}]${DeepKeys}`; }[number]; type PrefixTupleAccessor< @@ -88,9 +88,7 @@ export type DeepValue = TValue extends Record< any > ? TAccessor extends `${infer TBefore}[${infer TBrackets}].${infer TAfter}` - ? TBrackets extends "number" - ? DeepValue - : DeepValue + ? DeepValue : TAccessor extends `[${infer TBrackets}]` ? DeepValue : TAccessor extends `${infer TBefore}[${infer TBrackets}]` From 87c3eac54d6e86804b806297581d453cc914c154 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 28 Jan 2024 03:37:34 -0800 Subject: [PATCH 03/23] feat!: remove form.Provider from React package --- packages/form-core/src/index.ts | 1 + packages/react-form/src/formContext.ts | 17 ----- .../src/tests/createFormFactory.test.tsx | 4 +- .../react-form/src/tests/useField.test-d.tsx | 8 +- .../react-form/src/tests/useField.test.tsx | 40 +++++----- .../react-form/src/tests/useForm.test.tsx | 44 +++++------ packages/react-form/src/types.ts | 10 ++- packages/react-form/src/useField.tsx | 76 ++++++------------- packages/react-form/src/useForm.tsx | 17 ++--- 9 files changed, 85 insertions(+), 132 deletions(-) delete mode 100644 packages/react-form/src/formContext.ts diff --git a/packages/form-core/src/index.ts b/packages/form-core/src/index.ts index 2d2d0acaa..bf037e63c 100644 --- a/packages/form-core/src/index.ts +++ b/packages/form-core/src/index.ts @@ -1,5 +1,6 @@ export * from './FormApi' export * from './FieldApi' export * from './utils' +export * from './util-types' export * from './types' export * from './mergeForm' diff --git a/packages/react-form/src/formContext.ts b/packages/react-form/src/formContext.ts deleted file mode 100644 index 9eb8c0e4e..000000000 --- a/packages/react-form/src/formContext.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { FormApi, Validator } from '@tanstack/form-core' -import { createContext, useContext } from 'rehackt' - -export const formContext = createContext<{ - formApi: FormApi | undefined> - parentFieldName?: string -} | null>(null!) - -export function useFormContext() { - const formApi = useContext(formContext) - - if (!formApi) { - throw new Error(`You are trying to use the form API outside of a form!`) - } - - return formApi -} diff --git a/packages/react-form/src/tests/createFormFactory.test.tsx b/packages/react-form/src/tests/createFormFactory.test.tsx index d8648cd21..62c793f4e 100644 --- a/packages/react-form/src/tests/createFormFactory.test.tsx +++ b/packages/react-form/src/tests/createFormFactory.test.tsx @@ -22,14 +22,14 @@ describe('createFormFactory', () => { const form = formFactory.useForm({}) return ( - + <> { return

{field.state.value}

}} /> -
+ ) } diff --git a/packages/react-form/src/tests/useField.test-d.tsx b/packages/react-form/src/tests/useField.test-d.tsx index c6a45629e..d3e8106dc 100644 --- a/packages/react-form/src/tests/useField.test-d.tsx +++ b/packages/react-form/src/tests/useField.test-d.tsx @@ -12,7 +12,7 @@ it('should type state.value properly', () => { } as const) return ( - + <> { @@ -25,7 +25,7 @@ it('should type state.value properly', () => { assertType<84>(field.state.value) }} /> - + ) } }) @@ -40,7 +40,7 @@ it('should type onChange properly', () => { } as const) return ( - + <> { }} children={() => null} /> - + ) } }) diff --git a/packages/react-form/src/tests/useField.test.tsx b/packages/react-form/src/tests/useField.test.tsx index 5cb306a50..6f2f847d5 100644 --- a/packages/react-form/src/tests/useField.test.tsx +++ b/packages/react-form/src/tests/useField.test.tsx @@ -26,7 +26,7 @@ describe('useField', () => { }) return ( - + <> { @@ -40,7 +40,7 @@ describe('useField', () => { ) }} /> - + ) } @@ -66,7 +66,7 @@ describe('useField', () => { }) return ( - + <> { ) }} /> - + ) } @@ -103,7 +103,7 @@ describe('useField', () => { const form = formFactory.useForm() return ( - + <> { )} /> - + ) } @@ -145,7 +145,7 @@ describe('useField', () => { const form = formFactory.useForm() return ( - + <> { )} /> - + ) } @@ -190,7 +190,7 @@ describe('useField', () => { const form = formFactory.useForm() return ( - + <> { )} /> - + ) } @@ -241,7 +241,7 @@ describe('useField', () => { const form = formFactory.useForm() return ( - + <> { )} /> - + ) } @@ -290,7 +290,7 @@ describe('useField', () => { const form = formFactory.useForm() return ( - + <> { )} /> - + ) } @@ -348,7 +348,7 @@ describe('useField', () => { const form = formFactory.useForm() return ( - + <> { )} /> - + ) } @@ -396,7 +396,7 @@ describe('useField', () => { function Comp() { form = formFactory.useForm() return ( - + <> { ) }} /> - + ) } @@ -434,7 +434,7 @@ describe('useField', () => { function Comp() { form = formFactory.useForm() return ( - + <> { ) }} /> - + ) } diff --git a/packages/react-form/src/tests/useForm.test.tsx b/packages/react-form/src/tests/useForm.test.tsx index 0ddcca409..8a7942d58 100644 --- a/packages/react-form/src/tests/useForm.test.tsx +++ b/packages/react-form/src/tests/useForm.test.tsx @@ -21,7 +21,7 @@ describe('useForm', () => { const form = formFactory.useForm() return ( - + <> { ) }} /> - + ) } @@ -64,14 +64,14 @@ describe('useForm', () => { }) return ( - + <> { return

{field.state.value}

}} /> -
+ ) } @@ -96,7 +96,7 @@ describe('useForm', () => { }) return ( - + <> { @@ -112,7 +112,7 @@ describe('useForm', () => { /> {submittedData &&

Submitted data: {submittedData.firstName}

} -
+ ) } @@ -146,9 +146,9 @@ describe('useForm', () => { return ( <> {mountForm ? ( - + <>

{formMounted ? 'Form mounted' : 'Not mounted'}

-
+ ) : ( )} @@ -180,7 +180,7 @@ describe('useForm', () => { }) const onChangeError = form.useStore((s) => s.errorMap.onChange) return ( - + <> ( @@ -194,7 +194,7 @@ describe('useForm', () => { )} />

{onChangeError}

-
+ ) } @@ -225,7 +225,7 @@ describe('useForm', () => { const errors = form.useStore((s) => s.errors) return ( - + <> ( @@ -241,7 +241,7 @@ describe('useForm', () => { )} /> - + ) } @@ -269,7 +269,7 @@ describe('useForm', () => { }) const errors = form.useStore((s) => s.errorMap) return ( - + <> { )} /> - + ) } @@ -320,7 +320,7 @@ describe('useForm', () => { const errors = form.useStore((s) => s.errorMap) return ( - + <> { )} /> - + ) } const { getByTestId, getByText, queryByText } = render() @@ -371,7 +371,7 @@ describe('useForm', () => { }) const errors = form.useStore((s) => s.errorMap) return ( - + <> { )} /> - + ) } @@ -426,7 +426,7 @@ describe('useForm', () => { const errors = form.useStore((s) => s.errorMap) return ( - + <> { )} /> - + ) } @@ -484,7 +484,7 @@ describe('useForm', () => { const errors = form.useStore((s) => s.errors) return ( - + <> { )} /> - + ) } diff --git a/packages/react-form/src/types.ts b/packages/react-form/src/types.ts index ad037a921..19ff92bdf 100644 --- a/packages/react-form/src/types.ts +++ b/packages/react-form/src/types.ts @@ -1,5 +1,5 @@ import type { - FieldOptions, + FieldApiOptions, DeepKeys, DeepValue, Validator, @@ -15,6 +15,12 @@ export type UseFieldOptions< | Validator | undefined = undefined, TData extends DeepValue = DeepValue, -> = FieldOptions & { +> = FieldApiOptions< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData +> & { mode?: 'value' | 'array' } diff --git a/packages/react-form/src/useField.tsx b/packages/react-form/src/useField.tsx index 2bb39c2ec..a955c7c19 100644 --- a/packages/react-form/src/useField.tsx +++ b/packages/react-form/src/useField.tsx @@ -7,7 +7,6 @@ import type { Validator, } from '@tanstack/form-core' import { FieldApi, functionalUpdate } from '@tanstack/form-core' -import { useFormContext, formContext } from './formContext' import type { UseFieldOptions } from './types' import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' import { useIsomorphicEffectOnce } from './useIsomorphicEffectOnce' @@ -29,20 +28,20 @@ declare module '@tanstack/form-core' { } } -export type UseField = < +export type UseField< + TParentData, + TFormValidator extends + | Validator + | undefined = undefined, +> = < TName extends DeepKeys, TFieldValidator extends | Validator, unknown> | undefined = undefined, - TFormValidator extends - | Validator - | undefined = undefined, >( - opts?: { name: Narrow } & UseFieldOptions< - TParentData, - TName, - TFieldValidator, - TFormValidator + opts: Omit< + UseFieldOptions, + 'form' >, ) => FieldApi< TParentData, @@ -64,23 +63,11 @@ export function useField< >( opts: UseFieldOptions, ): FieldApi { - // Get the form API either manually or from context - const { formApi, parentFieldName } = useFormContext() - const [fieldApi] = useState(() => { - const name = ( - typeof opts.index === 'number' - ? [parentFieldName, opts.index, opts.name] - : [parentFieldName, opts.name] - ) - .filter((d) => d !== undefined) - .join('.') - const api = new FieldApi({ ...opts, - form: formApi as never, - // TODO: Fix typings to include `index` and `parentFieldName`, if present - name: name as typeof opts.name as never, + form: opts.form, + name: opts.name, }) api.Field = Field as never @@ -93,7 +80,7 @@ export function useField< * that we need to keep updated every render with the most up-to-date information. */ useIsomorphicLayoutEffect(() => { - fieldApi.update({ ...opts, form: formApi } as never) + fieldApi.update(opts) }) useStore( @@ -141,19 +128,7 @@ type FieldComponentProps< TData >, ) => any -} & (TParentData extends any[] - ? { - name?: TName - index: number - } - : { - name: TName - index?: never - }) & - Omit< - UseFieldOptions, - 'name' | 'index' - > +} & UseFieldOptions export type FieldComponent< TParentData, @@ -169,12 +144,15 @@ export type FieldComponent< >({ children, ...fieldOptions -}: FieldComponentProps< - TParentData, - TName, - TFieldValidator, - TFormValidator, - TData +}: Omit< + FieldComponentProps< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData + >, + 'form' >) => any export function Field< @@ -196,13 +174,5 @@ export function Field< } & UseFieldOptions) { const fieldApi = useField(fieldOptions as any) - return ( - - ) + return <>{functionalUpdate(children, fieldApi as any)} } diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 5402821b4..42c4aa059 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -8,15 +8,13 @@ import React, { useState, } from 'rehackt' import { type UseField, type FieldComponent, Field, useField } from './useField' -import { formContext } from './formContext' import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' declare module '@tanstack/form-core' { // eslint-disable-next-line no-shadow interface FormApi { - Provider: (props: PropsWithChildren) => JSX.Element Field: FieldComponent - useField: UseField + useField: UseField useStore: >>( selector?: (state: NoInfer>) => TSelected, ) => TSelected @@ -36,15 +34,8 @@ export function useForm< const [formApi] = useState(() => { // @ts-ignore const api = new FormApi(opts) - - api.Provider = function Provider(props) { - useIsomorphicLayoutEffect(api.mount, []) - return ( - - ) - } - api.Field = Field as any - api.useField = useField as any + api.Field = (props) => + api.useField = (props) => useField({ ...props, form: api }) api.useStore = ( // @ts-ignore selector, @@ -66,6 +57,8 @@ export function useForm< return api }) + useIsomorphicLayoutEffect(formApi.mount, []) + formApi.useStore((state) => state.isSubmitting) /** From ee69c9b8824059e681f5151b83357ad625d112b9 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 28 Jan 2024 03:44:00 -0800 Subject: [PATCH 04/23] chore: fix utils tests in regards to arrays --- packages/form-core/src/tests/utils.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/form-core/src/tests/utils.spec.ts b/packages/form-core/src/tests/utils.spec.ts index 1843b6e14..c36aa3aa2 100644 --- a/packages/form-core/src/tests/utils.spec.ts +++ b/packages/form-core/src/tests/utils.spec.ts @@ -19,8 +19,8 @@ describe('getBy', () => { }) it('should get array subfields by path', () => { - expect(getBy(structure, 'kids.0.name')).toBe(structure.kids[0]!.name) - expect(getBy(structure, 'kids.0.age')).toBe(structure.kids[0]!.age) + expect(getBy(structure, 'kids[0].name')).toBe(structure.kids[0]!.name) + expect(getBy(structure, 'kids[0].age')).toBe(structure.kids[0]!.age) }) }) @@ -42,10 +42,10 @@ describe('setBy', () => { }) it('should set array subfields by path', () => { - expect(setBy(structure, 'kids.0.name', 'Taylor').kids[0].name).toBe( + expect(setBy(structure, 'kids[0].name', 'Taylor').kids[0].name).toBe( 'Taylor', ) - expect(setBy(structure, 'kids.0.age', 20).kids[0].age).toBe(20) + expect(setBy(structure, 'kids[0].age', 20).kids[0].age).toBe(20) }) }) @@ -67,14 +67,14 @@ describe('deleteBy', () => { }) it('should delete array subfields by path', () => { - expect(deleteBy(structure, 'kids.0.name').kids[0].name).not.toBeDefined() - expect(deleteBy(structure, 'kids.0.age').kids[0].age).not.toBeDefined() + expect(deleteBy(structure, 'kids[0].name').kids[0].name).not.toBeDefined() + expect(deleteBy(structure, 'kids[0].age').kids[0].age).not.toBeDefined() }) it('should delete non-existent paths like a noop', () => { expect(deleteBy(structure, 'nonexistent')).toEqual(structure) expect(deleteBy(structure, 'nonexistent.nonexistent')).toEqual(structure) - expect(deleteBy(structure, 'kids.3.name')).toEqual(structure) - expect(deleteBy(structure, 'nonexistent.3.nonexistent')).toEqual(structure) + expect(deleteBy(structure, 'kids[3].name')).toEqual(structure) + expect(deleteBy(structure, 'nonexistent[3].nonexistent')).toEqual(structure) }) }) From 5f56e9c896af48117f9b40bd4907f9338650c399 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 28 Jan 2024 04:15:22 -0800 Subject: [PATCH 05/23] feat!: remove form.Provider from Solid package --- packages/react-form/src/createFormFactory.ts | 8 +- packages/solid-form/src/createField.tsx | 81 ++++++------------- packages/solid-form/src/createForm.tsx | 19 ++--- packages/solid-form/src/createFormFactory.ts | 6 +- .../src/tests/createField.test-d.tsx | 8 +- .../solid-form/src/tests/createField.test.tsx | 32 ++++---- .../solid-form/src/tests/createForm.test.tsx | 40 ++++----- .../src/tests/createFormFactory.test.tsx | 4 +- packages/solid-form/src/types.ts | 10 ++- 9 files changed, 90 insertions(+), 118 deletions(-) diff --git a/packages/react-form/src/createFormFactory.ts b/packages/react-form/src/createFormFactory.ts index f5294be4c..963f001ab 100644 --- a/packages/react-form/src/createFormFactory.ts +++ b/packages/react-form/src/createFormFactory.ts @@ -11,8 +11,8 @@ export type FormFactory< useForm: ( opts?: FormOptions, ) => FormApi - useField: UseField - Field: FieldComponent + useField: typeof useField + Field: typeof Field validateFormData: ValidateFormData initialFormState: Partial['state']> } @@ -28,8 +28,8 @@ export function createFormFactory< const formOptions = Object.assign({}, defaultOpts, opts) return useForm(formOptions) }, - useField: useField as any, - Field: Field as any, + useField: useField, + Field: Field, validateFormData: getValidateFormData(defaultOpts) as never, initialFormState: { errorMap: { diff --git a/packages/solid-form/src/createField.tsx b/packages/solid-form/src/createField.tsx index 68f560096..e6fd484a4 100644 --- a/packages/solid-form/src/createField.tsx +++ b/packages/solid-form/src/createField.tsx @@ -7,7 +7,6 @@ import { onCleanup, onMount, } from 'solid-js' -import { formContext, useFormContext } from './formContext' import type { DeepKeys, DeepValue, Narrow } from '@tanstack/form-core' import type { JSXElement } from 'solid-js' @@ -30,20 +29,20 @@ declare module '@tanstack/form-core' { } } -export type CreateField = < +export type CreateField< + TParentData, + TFormValidator extends + | Validator + | undefined = undefined, +> = < TName extends DeepKeys, TFieldValidator extends | Validator, unknown> | undefined = undefined, - TFormValidator extends - | Validator - | undefined = undefined, >( - opts: () => { name: Narrow } & CreateFieldOptions< - TParentData, - TName, - TFieldValidator, - TFormValidator + opts: () => { name: Narrow } & Omit< + CreateFieldOptions, + 'form' >, ) => () => FieldApi< TParentData, @@ -81,23 +80,9 @@ export function createField< TFormValidator >, ): () => FieldApi { - // Get the form API either manually or from context - const { formApi, parentFieldName } = useFormContext() - const options = opts() - const name = ( - typeof options.index === 'number' - ? [parentFieldName, options.index, options.name] - : [parentFieldName, options.name] - ) - .filter((d) => d !== undefined) - .join('.') - const fieldApi = new FieldApi({ - ...options, - form: formApi, - name: name as typeof options.name as never, - }) + const fieldApi = new FieldApi(options) fieldApi.Field = Field as never /** @@ -106,7 +91,7 @@ export function createField< * * createComputed to make sure this effect runs before render effects */ - createComputed(() => fieldApi.update({ ...opts(), form: formApi } as never)) + createComputed(() => fieldApi.update(opts())) // Instantiates field meta and removes it when unrendered onMount(() => onCleanup(fieldApi.mount())) @@ -134,19 +119,10 @@ type FieldComponentProps< TData >, ) => JSXElement -} & (TParentData extends any[] - ? { - name?: TName - index: number - } - : { - name: TName - index?: never - }) & - Omit< - CreateFieldOptions, - 'name' | 'index' - > +} & Omit< + CreateFieldOptions, + 'form' +> export type FieldComponent< TParentData, @@ -162,12 +138,15 @@ export type FieldComponent< >({ children, ...fieldOptions -}: FieldComponentProps< - TParentData, - TName, - TFieldValidator, - TFormValidator, - TData +}: Omit< + FieldComponentProps< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData + >, + 'form' >) => JSXElement export function Field< @@ -201,15 +180,5 @@ export function Field< return fieldOptions }) - return ( - - {/* createComponent to make sure the signals in the children component are not tracked */} - {createComponent(() => props.children(fieldApi), {})} - - ) + return <>{createComponent(() => props.children(fieldApi), {})} } diff --git a/packages/solid-form/src/createForm.tsx b/packages/solid-form/src/createForm.tsx index 2a67fa5c1..feb08af98 100644 --- a/packages/solid-form/src/createForm.tsx +++ b/packages/solid-form/src/createForm.tsx @@ -8,16 +8,14 @@ import { type CreateField, type FieldComponent, } from './createField' -import { formContext } from './formContext' type NoInfer = [T][T extends any ? 0 : never] declare module '@tanstack/form-core' { // eslint-disable-next-line no-shadow interface FormApi { - Provider: (props: { children: any }) => JSXElement Field: FieldComponent - createField: CreateField + createField: CreateField useStore: >>( selector?: (state: NoInfer>) => TSelected, ) => () => TSelected @@ -39,18 +37,17 @@ export function createForm< const options = opts?.() const formApi = new FormApi(options) - formApi.Provider = function Provider(props) { - onMount(formApi.mount) - return ( - - ) - } - formApi.Field = Field as any - formApi.createField = createField as CreateField + formApi.Field = (props) => + formApi.createField = (props) => + createField(() => { + return { ...props(), form: formApi } + }) formApi.useStore = (selector) => useStore(formApi.store, selector) formApi.Subscribe = (props) => functionalUpdate(props.children, useStore(formApi.store, props.selector)) + onMount(formApi.mount) + /** * formApi.update should not have any side effects. Think of it like a `useRef` * that we need to keep updated every render with the most up-to-date information. diff --git a/packages/solid-form/src/createFormFactory.ts b/packages/solid-form/src/createFormFactory.ts index 1a0d3db45..e130f6486 100644 --- a/packages/solid-form/src/createFormFactory.ts +++ b/packages/solid-form/src/createFormFactory.ts @@ -16,8 +16,8 @@ export type FormFactory< createForm: ( opts?: () => FormOptions, ) => FormApi - createField: CreateField - Field: FieldComponent + createField: typeof createField + Field: typeof Field } export function createFormFactory< @@ -32,6 +32,6 @@ export function createFormFactory< mergeProps(defaultOpts?.() ?? {}, opts?.() ?? {}), ), createField, - Field: Field as never, + Field: Field, } } diff --git a/packages/solid-form/src/tests/createField.test-d.tsx b/packages/solid-form/src/tests/createField.test-d.tsx index 17d8afa99..3c1b2c17f 100644 --- a/packages/solid-form/src/tests/createField.test-d.tsx +++ b/packages/solid-form/src/tests/createField.test-d.tsx @@ -15,7 +15,7 @@ it('should type state.value properly', () => { ) return ( - + <> { @@ -30,7 +30,7 @@ it('should type state.value properly', () => { return null }} /> - + ) } }) @@ -48,7 +48,7 @@ it('should type onChange properly', () => { ) return ( - + <> { }} children={() => null} /> - + ) } }) diff --git a/packages/solid-form/src/tests/createField.test.tsx b/packages/solid-form/src/tests/createField.test.tsx index 102cc0bcd..fb2ae88df 100644 --- a/packages/solid-form/src/tests/createField.test.tsx +++ b/packages/solid-form/src/tests/createField.test.tsx @@ -20,7 +20,7 @@ describe('createField', () => { const form = formFactory.createForm() return ( - + <> { ) }} /> - + ) } @@ -61,7 +61,7 @@ describe('createField', () => { })) return ( - + <> { ) }} /> - + ) } @@ -98,7 +98,7 @@ describe('createField', () => { const form = formFactory.createForm() return ( - + <> { )} /> - + ) } @@ -141,7 +141,7 @@ describe('createField', () => { const form = formFactory.createForm() return ( - + <> { ) }} /> - + ) } @@ -189,7 +189,7 @@ describe('createField', () => { const form = formFactory.createForm() return ( - + <> { )} /> - + ) } @@ -240,7 +240,7 @@ describe('createField', () => { const form = formFactory.createForm() return ( - + <> { )} /> - + ) } @@ -289,7 +289,7 @@ describe('createField', () => { const form = formFactory.createForm() return ( - + <> { )} /> - + ) } @@ -347,7 +347,7 @@ describe('createField', () => { const form = formFactory.createForm() return ( - + <> { )} /> - + ) } diff --git a/packages/solid-form/src/tests/createForm.test.tsx b/packages/solid-form/src/tests/createForm.test.tsx index cf84efca5..02a82889d 100644 --- a/packages/solid-form/src/tests/createForm.test.tsx +++ b/packages/solid-form/src/tests/createForm.test.tsx @@ -21,7 +21,7 @@ describe('createForm', () => { function Comp() { const form = formFactory.createForm() return ( - + <> { /> )} /> - + ) } @@ -62,14 +62,14 @@ describe('createForm', () => { })) return ( - + <> { return

{field().state.value}

}} /> -
+ ) } @@ -91,7 +91,7 @@ describe('createForm', () => { })) return ( - + <> { @@ -106,7 +106,7 @@ describe('createForm', () => { }} /> - + ) } @@ -141,9 +141,9 @@ describe('createForm', () => { } > - + <>

Form mounted

-
+ ) } @@ -173,7 +173,7 @@ describe('createForm', () => { const errors = form.useStore((s) => s.errors) return ( - + <> ( @@ -189,7 +189,7 @@ describe('createForm', () => { )} /> - + ) } @@ -220,7 +220,7 @@ describe('createForm', () => { onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( - + <> { ) }} /> - + ) } @@ -274,7 +274,7 @@ describe('createForm', () => { onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( - + <> { )} /> - + ) } @@ -329,7 +329,7 @@ describe('createForm', () => { onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( - + <> { )} /> - + ) } @@ -386,7 +386,7 @@ describe('createForm', () => { onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( - + <> { )} /> - + ) } @@ -450,7 +450,7 @@ describe('createForm', () => { ) return ( - + <> { )} /> - + ) } diff --git a/packages/solid-form/src/tests/createFormFactory.test.tsx b/packages/solid-form/src/tests/createFormFactory.test.tsx index ce3d464d8..ac5e4a293 100644 --- a/packages/solid-form/src/tests/createFormFactory.test.tsx +++ b/packages/solid-form/src/tests/createFormFactory.test.tsx @@ -21,14 +21,14 @@ describe('createFormFactory', () => { const form = formFactory.createForm() return ( - + <> { return

{field().state.value}

}} /> -
+ ) } diff --git a/packages/solid-form/src/types.ts b/packages/solid-form/src/types.ts index d64b25104..dc30abbd0 100644 --- a/packages/solid-form/src/types.ts +++ b/packages/solid-form/src/types.ts @@ -1,5 +1,5 @@ import type { - FieldOptions, + FieldApiOptions, DeepKeys, DeepValue, Validator, @@ -15,6 +15,12 @@ export type CreateFieldOptions< | Validator | undefined = undefined, TData extends DeepValue = DeepValue, -> = FieldOptions & { +> = FieldApiOptions< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData +> & { mode?: 'value' | 'array' } From 20803b884bef2e9b463008a6431bd23de8deaf5d Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 28 Jan 2024 03:58:02 -0800 Subject: [PATCH 06/23] chore!: proof of working arrays in react --- examples/react/simple/src/index.tsx | 120 ++++++++-------------------- 1 file changed, 32 insertions(+), 88 deletions(-) diff --git a/examples/react/simple/src/index.tsx b/examples/react/simple/src/index.tsx index b70d52c16..dd804d1b8 100644 --- a/examples/react/simple/src/index.tsx +++ b/examples/react/simple/src/index.tsx @@ -1,108 +1,52 @@ import * as React from 'react' import { createRoot } from 'react-dom/client' import { useForm } from '@tanstack/react-form' -import type { FieldApi } from '@tanstack/react-form' - -function FieldInfo({ field }: { field: FieldApi }) { - return ( - <> - {field.state.meta.touchedErrors ? ( - {field.state.meta.touchedErrors} - ) : null} - {field.state.meta.isValidating ? 'Validating...' : null} - - ) -} export default function App() { const form = useForm({ defaultValues: { - firstName: '', - lastName: '', + people: [] as Array<{age: number, name: string}> }, onSubmit: async ({ value }) => { // Do something with form data - console.log(value) + alert(JSON.stringify(value, null, 2)) }, }) return (

Simple Form Example

- -
{ - e.preventDefault() - e.stopPropagation() - void form.handleSubmit() + { + e.preventDefault() + e.stopPropagation() + void form.handleSubmit() + }} + > + + {(field) => { + return
+ {field.state.value.map((_, i) => { + return + {subField => { + return ( +
+ )}} +
+ })} + +
}} - > -
- {/* A type-safe field component*/} - - !value - ? 'A first name is required' - : value.length < 3 - ? 'First name must be at least 3 characters' - : undefined, - onChangeAsyncDebounceMs: 500, - onChangeAsync: async ({ value }) => { - await new Promise((resolve) => setTimeout(resolve, 1000)) - return ( - value.includes('error') && - 'No "error" allowed in first name' - ) - }, - }} - children={(field) => { - // Avoid hasty abstractions. Render props are great! - return ( - <> - - field.handleChange(e.target.value)} - /> - - - ) - }} - /> -
-
- ( - <> - - field.handleChange(e.target.value)} - /> - - - )} - /> -
- [state.canSubmit, state.isSubmitting]} - children={([canSubmit, isSubmitting]) => ( - - )} - /> - -
+ + [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> +
) } From ce52a3a0bbcc45d78ca28b13a014bffe8c6b1b0e Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 4 Mar 2024 17:37:16 -0800 Subject: [PATCH 07/23] chore: change Vue behavior and types --- examples/vue/simple/src/App.vue | 2 - examples/vue/valibot/src/App.vue | 2 - examples/vue/yup/src/App.vue | 2 - examples/vue/zod/src/App.vue | 2 - packages/vue-form/src/createFormFactory.ts | 8 +- packages/vue-form/src/formContext.ts | 29 ------- packages/vue-form/src/index.ts | 1 - packages/vue-form/src/tests/useField.test.tsx | 12 +-- packages/vue-form/src/tests/useForm.test.tsx | 27 +----- packages/vue-form/src/types.ts | 10 ++- packages/vue-form/src/useField.tsx | 83 ++++++------------- packages/vue-form/src/useForm.tsx | 27 +++--- 12 files changed, 50 insertions(+), 155 deletions(-) delete mode 100644 packages/vue-form/src/formContext.ts diff --git a/examples/vue/simple/src/App.vue b/examples/vue/simple/src/App.vue index 1aad2cf1f..14d4a836a 100644 --- a/examples/vue/simple/src/App.vue +++ b/examples/vue/simple/src/App.vue @@ -13,8 +13,6 @@ const form = useForm({ }, }) -form.provideFormContext() - async function onChangeFirstName({ value }: { value: string }) { await new Promise((resolve) => setTimeout(resolve, 1000)) return value.includes(`error`) && `No 'error' allowed in first name` diff --git a/examples/vue/valibot/src/App.vue b/examples/vue/valibot/src/App.vue index 5cc367ac2..78e52e7c2 100644 --- a/examples/vue/valibot/src/App.vue +++ b/examples/vue/valibot/src/App.vue @@ -17,8 +17,6 @@ const form = useForm({ validatorAdapter: valibotValidator, }) -form.provideFormContext() - const onChangeFirstName = stringAsync([ customAsync(async (value) => { await new Promise((resolve) => setTimeout(resolve, 1000)) diff --git a/examples/vue/yup/src/App.vue b/examples/vue/yup/src/App.vue index 235997f70..4d8923865 100644 --- a/examples/vue/yup/src/App.vue +++ b/examples/vue/yup/src/App.vue @@ -17,8 +17,6 @@ const form = useForm({ validatorAdapter: yupValidator, }) -form.provideFormContext() - const onChangeFirstName = yup .string() .test('no-error', "No 'error' allowed in first name", async (value) => { diff --git a/examples/vue/zod/src/App.vue b/examples/vue/zod/src/App.vue index 424a805ba..fcde47cd9 100644 --- a/examples/vue/zod/src/App.vue +++ b/examples/vue/zod/src/App.vue @@ -17,8 +17,6 @@ const form = useForm({ validatorAdapter: zodValidator, }) -form.provideFormContext() - const onChangeFirstName = z.string().refine( async (value) => { await new Promise((resolve) => setTimeout(resolve, 1000)) diff --git a/packages/vue-form/src/createFormFactory.ts b/packages/vue-form/src/createFormFactory.ts index 74bd1143c..a0538c679 100644 --- a/packages/vue-form/src/createFormFactory.ts +++ b/packages/vue-form/src/createFormFactory.ts @@ -10,8 +10,8 @@ export type FormFactory< useForm: ( opts?: FormOptions, ) => FormApi - useField: UseField - Field: FieldComponent + useField: typeof useField + Field: typeof Field } export function createFormFactory< @@ -25,7 +25,7 @@ export function createFormFactory< const formOptions = Object.assign({}, defaultOpts, opts) return useForm(formOptions) }, - useField: useField as any, - Field: Field as any, + useField: useField, + Field: Field, } } diff --git a/packages/vue-form/src/formContext.ts b/packages/vue-form/src/formContext.ts deleted file mode 100644 index a5372a67c..000000000 --- a/packages/vue-form/src/formContext.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { inject, provide } from 'vue' -import type { FormApi, Validator } from '@tanstack/form-core' - -export type FormContext< - TFormData = any, - TFormValidator extends Validator | undefined = undefined, -> = { - formApi: FormApi - parentFieldName?: string -} | null - -export const formContext = Symbol('FormContext') - -export function provideFormContext< - TFormData = any, - TFormValidator extends Validator | undefined = undefined, ->(val: FormContext) { - provide(formContext, val) -} - -export function useFormContext() { - const formApi = inject(formContext) as FormContext - - if (!formApi) { - throw new Error(`You are trying to use the form API outside of a form!`) - } - - return formApi -} diff --git a/packages/vue-form/src/index.ts b/packages/vue-form/src/index.ts index 2c7d89e6f..1c57320a1 100644 --- a/packages/vue-form/src/index.ts +++ b/packages/vue-form/src/index.ts @@ -1,5 +1,4 @@ export * from '@tanstack/form-core' export * from './createFormFactory' -export * from './formContext' export * from './useField' export * from './useForm' diff --git a/packages/vue-form/src/tests/useField.test.tsx b/packages/vue-form/src/tests/useField.test.tsx index 50ecb7b12..5ee76798c 100644 --- a/packages/vue-form/src/tests/useField.test.tsx +++ b/packages/vue-form/src/tests/useField.test.tsx @@ -3,7 +3,7 @@ import { defineComponent, h } from 'vue' import { render, waitFor } from '@testing-library/vue' import '@testing-library/jest-dom' import userEvent from '@testing-library/user-event' -import { createFormFactory, provideFormContext } from '../index' +import { createFormFactory } from '../index' import { sleep } from './utils' import type { FieldApi } from '../index' @@ -21,8 +21,6 @@ describe('useField', () => { const Comp = defineComponent(() => { const form = formFactory.useForm() - provideFormContext({ formApi: form }) - return () => ( {({ @@ -60,8 +58,6 @@ describe('useField', () => { const Comp = defineComponent(() => { const form = formFactory.useForm() - provideFormContext({ formApi: form }) - return () => ( { const Comp = defineComponent(() => { const form = formFactory.useForm() - provideFormContext({ formApi: form }) - return () => ( { const Comp = defineComponent(() => { const form = formFactory.useForm() - provideFormContext({ formApi: form }) - return () => ( { const Comp = defineComponent(() => { const form = formFactory.useForm() - provideFormContext({ formApi: form }) - return () => ( { const Comp = defineComponent(() => { const form = formFactory.useForm() - provideFormContext({ formApi: form }) - return () => ( {({ @@ -66,7 +59,6 @@ describe('useForm', () => { lastName: 'LastName', }, }) - form.provideFormContext() return () => ( @@ -96,7 +88,6 @@ describe('useForm', () => { submittedData.value = value }, }) - form.provideFormContext() return () => (
@@ -153,8 +144,6 @@ describe('useForm', () => { }, }) - form.provideFormContext() - return () => mountForm.value ? (
@@ -184,8 +173,6 @@ describe('useForm', () => { }, }) - form.provideFormContext() - return () => (
@@ -234,8 +221,6 @@ describe('useForm', () => { const errors = form.useStore((s) => s.errors) - form.provideFormContext() - return () => (
@@ -283,8 +268,6 @@ describe('useForm', () => { const errors = form.useStore((s) => s.errorMap) - form.provideFormContext() - return () => (
@@ -341,8 +324,6 @@ describe('useForm', () => { const errors = form.useStore((s) => s.errorMap) - form.provideFormContext() - return () => (
@@ -396,8 +377,6 @@ describe('useForm', () => { const errors = form.useStore((s) => s.errorMap) - form.provideFormContext() - return () => (
@@ -453,8 +432,6 @@ describe('useForm', () => { }) const errors = form.useStore((s) => s.errorMap) - form.provideFormContext() - return () => (
@@ -513,8 +490,6 @@ describe('useForm', () => { }) const errors = form.useStore((s) => s.errors) - form.provideFormContext() - return () => (
diff --git a/packages/vue-form/src/types.ts b/packages/vue-form/src/types.ts index 947857fd2..8a19e8af4 100644 --- a/packages/vue-form/src/types.ts +++ b/packages/vue-form/src/types.ts @@ -1,7 +1,7 @@ import type { DeepKeys, DeepValue, - FieldOptions, + FieldApiOptions, Validator, } from '@tanstack/form-core' @@ -15,6 +15,12 @@ export type UseFieldOptions< | Validator | undefined = undefined, TData extends DeepValue = DeepValue, -> = FieldOptions & { +> = FieldApiOptions< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData +> & { mode?: 'value' | 'array' } diff --git a/packages/vue-form/src/useField.tsx b/packages/vue-form/src/useField.tsx index 6daae5471..1b93146a4 100644 --- a/packages/vue-form/src/useField.tsx +++ b/packages/vue-form/src/useField.tsx @@ -1,7 +1,6 @@ import { FieldApi } from '@tanstack/form-core' import { useStore } from '@tanstack/vue-store' import { defineComponent, onMounted, onUnmounted, watch } from 'vue' -import { provideFormContext, useFormContext } from './formContext' import type { DeepKeys, DeepValue, @@ -38,21 +37,26 @@ export type UseField< TFieldValidator extends | Validator, unknown> | undefined = undefined, + TData extends DeepValue = DeepValue, >( - opts?: { name: Narrow } & UseFieldOptions< - TParentData, - TName, - TFieldValidator, - TFormValidator, - DeepValue + opts: Omit< + UseFieldOptions, + 'form' >, -) => FieldApi< - TParentData, - TName, - TFieldValidator, - TFormValidator, - DeepValue -> +) => { + api: FieldApi + state: Readonly< + Ref< + FieldApi< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData + >['state'] + > + > +} export function useField< TParentData, @@ -73,16 +77,7 @@ export function useField< TData >, ): { - api: FieldApi< - TParentData, - TName, - TFieldValidator, - TFormValidator, - TData - // Omit & { - // form: FormApi - // } - > + api: FieldApi state: Readonly< Ref< FieldApi< @@ -95,13 +90,10 @@ export function useField< > > } { - // Get the form API either manually or from context - const { formApi, parentFieldName } = useFormContext() - const fieldApi = (() => { const api = new FieldApi({ ...opts, - form: formApi, + form: opts.form, name: opts.name, } as never) @@ -125,19 +117,13 @@ export function useField< () => opts, () => { // Keep options up to date as they are rendered - fieldApi.update({ ...opts, form: formApi } as never) + fieldApi.update({ ...opts, form: opts.form } as never) }, ) return { api: fieldApi, state: fieldState } as never } -export type FieldValue = TParentData extends any[] - ? unknown extends TName - ? TParentData[number] - : DeepValue - : DeepValue - type FieldComponentProps< TParentData, TName extends DeepKeys, @@ -147,19 +133,7 @@ type FieldComponentProps< TFormValidator extends | Validator | undefined = undefined, -> = (TParentData extends any[] - ? { - name?: TName - index: number - } - : { - name: TName - index?: never - }) & - Omit< - UseFieldOptions, - 'name' | 'index' - > +> = UseFieldOptions export type FieldComponent< TParentData, @@ -173,11 +147,9 @@ export type FieldComponent< | undefined = undefined, TData extends DeepValue = DeepValue, >( - fieldOptions: FieldComponentProps< - TParentData, - TName, - TFieldValidator, - TFormValidator + fieldOptions: Omit< + FieldComponentProps, + 'form' >, context: SetupContext< {}, @@ -223,11 +195,6 @@ export const Field = defineComponent( ) => { const fieldApi = useField({ ...fieldOptions, ...context.attrs } as any) - provideFormContext({ - formApi: fieldApi.api.form, - parentFieldName: fieldApi.api.name, - } as never) - return () => context.slots.default!({ field: fieldApi.api, diff --git a/packages/vue-form/src/useForm.tsx b/packages/vue-form/src/useForm.tsx index ff1586cd2..cc5454e4b 100644 --- a/packages/vue-form/src/useForm.tsx +++ b/packages/vue-form/src/useForm.tsx @@ -1,8 +1,7 @@ import { FormApi } from '@tanstack/form-core' import { useStore } from '@tanstack/vue-store' -import { defineComponent, onMounted } from 'vue' +import { defineComponent, h, onMounted } from 'vue' import { Field, useField } from './useField' -import { provideFormContext } from './formContext' import type { FormOptions, FormState, Validator } from '@tanstack/form-core' import type { NoInfer } from '@tanstack/vue-store' import type { EmitsOptions, Ref, SetupContext, SlotsType } from 'vue' @@ -11,8 +10,6 @@ import type { FieldComponent, UseField } from './useField' declare module '@tanstack/form-core' { // eslint-disable-next-line no-shadow interface FormApi { - Provider: (props: Record & {}) => any - provideFormContext: () => void Field: FieldComponent useField: UseField useStore: >>( @@ -39,20 +36,16 @@ export function useForm< const formApi = (() => { const api = new FormApi(opts) - api.Provider = defineComponent( - (_, context) => { - onMounted(api.mount) - provideFormContext({ formApi: formApi as never }) - return () => context.slots.default!() + api.Field = defineComponent( + (props, context) => { + return () => h(Field as never, { ...props, ...context.attrs, form: api }, context.slots) + }, + { + name: 'APIField', + inheritAttrs: false, }, - { name: 'Provider' }, ) - api.provideFormContext = () => { - onMounted(api.mount) - provideFormContext({ formApi: formApi as never }) - } - api.Field = Field as never - api.useField = useField as never + api.useField = (props) => useField({ ...props, form: api }) api.useStore = (selector) => { return useStore(api.store as never, selector as never) as never } @@ -72,6 +65,8 @@ export function useForm< return api })() + onMounted(formApi.mount) + // formApi.useStore((state) => state.isSubmitting) formApi.update(opts) From ce7c6bdf335c80cbe95c8757e08f77fb273d4eb1 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 4 Mar 2024 18:06:32 -0800 Subject: [PATCH 08/23] chore: fix CI, apply formatter, etc --- .../src/app/client-component.tsx | 81 ++++----- .../react/ui-libraries/src/MainComponent.tsx | 162 +++++++++--------- examples/react/valibot/src/index.tsx | 114 ++++++------ examples/react/yup/src/index.tsx | 122 +++++++------ examples/react/zod/src/index.tsx | 120 +++++++------ examples/solid/simple/src/index.tsx | 125 +++++++------- examples/solid/valibot/src/index.tsx | 122 +++++++------ examples/solid/yup/src/index.tsx | 130 +++++++------- examples/solid/zod/src/index.tsx | 128 +++++++------- .../react-form/src/tests/useField.test.tsx | 123 +++++++------ packages/solid-form/src/formContext.ts | 20 --- packages/vue-form/src/useForm.tsx | 7 +- 12 files changed, 604 insertions(+), 650 deletions(-) delete mode 100644 packages/solid-form/src/formContext.ts diff --git a/examples/react/next-server-actions/src/app/client-component.tsx b/examples/react/next-server-actions/src/app/client-component.tsx index 6628926eb..b83afe0eb 100644 --- a/examples/react/next-server-actions/src/app/client-component.tsx +++ b/examples/react/next-server-actions/src/app/client-component.tsx @@ -20,50 +20,43 @@ export const ClientComp = () => { const formErrors = form.useStore((formState) => formState.errors) return ( - -
form.handleSubmit()}> - {formErrors.map((error) => ( -

{error}

- ))} + form.handleSubmit()}> + {formErrors.map((error) => ( +

{error}

+ ))} - - value < 8 - ? 'Client validation: You must be at least 8' - : undefined, - }} - > - {(field) => { - return ( -
- field.handleChange(e.target.valueAsNumber)} - /> - {field.state.meta.errors.map((error) => ( -

{error}

- ))} -
- ) - }} -
- [ - formState.canSubmit, - formState.isSubmitting, - ]} - > - {([canSubmit, isSubmitting]) => ( - - )} - -
-
+ + value < 8 ? 'Client validation: You must be at least 8' : undefined, + }} + > + {(field) => { + return ( +
+ field.handleChange(e.target.valueAsNumber)} + /> + {field.state.meta.errors.map((error) => ( +

{error}

+ ))} +
+ ) + }} +
+ [formState.canSubmit, formState.isSubmitting]} + > + {([canSubmit, isSubmitting]) => ( + + )} + + ) } diff --git a/examples/react/ui-libraries/src/MainComponent.tsx b/examples/react/ui-libraries/src/MainComponent.tsx index 4b58630ee..b05fd260c 100644 --- a/examples/react/ui-libraries/src/MainComponent.tsx +++ b/examples/react/ui-libraries/src/MainComponent.tsx @@ -6,96 +6,92 @@ import TextField from '@mui/material/TextField' import { Checkbox as MuiCheckbox } from '@mui/material' export default function MainComponent() { - const { Provider, Field, Subscribe, handleSubmit, state, useStore } = useForm( - { - defaultValues: { - firstName: '', - lastName: '', - isChecked: false, - isMuiCheckBox: false, - }, - onSubmit: async ({ value }) => { - // Do something with form data - console.log(value) - }, + const { Field, Subscribe, handleSubmit, state, useStore } = useForm({ + defaultValues: { + firstName: '', + lastName: '', + isChecked: false, + isMuiCheckBox: false, }, - ) + onSubmit: async ({ value }) => { + // Do something with form data + console.log(value) + }, + }) console.log({ values: useStore((state) => state.values), }) return ( - -
{ - e.preventDefault() - e.stopPropagation() - void handleSubmit() + { + e.preventDefault() + e.stopPropagation() + void handleSubmit() + }} + > + { + return ( + handleChange(e.target.value)} + onBlur={handleBlur} + placeholder="Enter your name" + /> + ) + }} + /> + { + return ( + handleChange(e.target.value)} + onBlur={handleBlur} + placeholder="Enter your last name" + /> + ) + }} + /> + { + return ( + handleChange(e.target.checked)} + onBlur={handleBlur} + checked={state.value} + /> + ) + }} + /> + { + return ( + handleChange(e.target.checked)} + onBlur={handleBlur} + checked={state.value} + /> + ) }} - > - { - return ( - handleChange(e.target.value)} - onBlur={handleBlur} - placeholder="Enter your name" - /> - ) - }} - /> - { - return ( - handleChange(e.target.value)} - onBlur={handleBlur} - placeholder="Enter your last name" - /> - ) - }} - /> - { - return ( - handleChange(e.target.checked)} - onBlur={handleBlur} - checked={state.value} - /> - ) - }} - /> - { - return ( - handleChange(e.target.checked)} - onBlur={handleBlur} - checked={state.value} - /> - ) - }} - /> - [state.canSubmit, state.isSubmitting]} - children={([canSubmit, isSubmitting]) => ( - - )} - /> - -
+ /> + [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> +
{JSON.stringify(state.values, null, 2)}
diff --git a/examples/react/valibot/src/index.tsx b/examples/react/valibot/src/index.tsx index 6e5af1fad..db62ff1e2 100644 --- a/examples/react/valibot/src/index.tsx +++ b/examples/react/valibot/src/index.tsx @@ -33,54 +33,34 @@ export default function App() { return (

Valibot Form Example

- -
{ - e.preventDefault() - e.stopPropagation() - void form.handleSubmit() - }} - > -
- {/* A type-safe field component*/} - { - await new Promise((resolve) => setTimeout(resolve, 1000)) - return !value.includes('error') - }, "No 'error' allowed in first name"), - ]), - }} - children={(field) => { - // Avoid hasty abstractions. Render props are great! - return ( - <> - - field.handleChange(e.target.value)} - /> - - - ) - }} - /> -
-
- ( + { + e.preventDefault() + e.stopPropagation() + void form.handleSubmit() + }} + > +
+ {/* A type-safe field component*/} + { + await new Promise((resolve) => setTimeout(resolve, 1000)) + return !value.includes('error') + }, "No 'error' allowed in first name"), + ]), + }} + children={(field) => { + // Avoid hasty abstractions. Render props are great! + return ( <> - + - )} - /> -
- [state.canSubmit, state.isSubmitting]} - children={([canSubmit, isSubmitting]) => ( - + ) + }} + /> +
+
+ ( + <> + + field.handleChange(e.target.value)} + /> + + )} /> - - +
+ [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> +
) } diff --git a/examples/react/yup/src/index.tsx b/examples/react/yup/src/index.tsx index 0a3ed3a51..9057d7670 100644 --- a/examples/react/yup/src/index.tsx +++ b/examples/react/yup/src/index.tsx @@ -33,58 +33,38 @@ export default function App() { return (

Yup Form Example

- -
{ - e.preventDefault() - e.stopPropagation() - void form.handleSubmit() - }} - > -
- {/* A type-safe field component*/} - { - await new Promise((resolve) => setTimeout(resolve, 1000)) - return !value?.includes('error') - }, - ), - }} - children={(field) => { - // Avoid hasty abstractions. Render props are great! - return ( - <> - - field.handleChange(e.target.value)} - /> - - - ) - }} - /> -
-
- ( + { + e.preventDefault() + e.stopPropagation() + void form.handleSubmit() + }} + > +
+ {/* A type-safe field component*/} + { + await new Promise((resolve) => setTimeout(resolve, 1000)) + return !value?.includes('error') + }, + ), + }} + children={(field) => { + // Avoid hasty abstractions. Render props are great! + return ( <> - + - )} - /> -
- [state.canSubmit, state.isSubmitting]} - children={([canSubmit, isSubmitting]) => ( - + ) + }} + /> +
+
+ ( + <> + + field.handleChange(e.target.value)} + /> + + )} /> - - +
+ [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> +
) } diff --git a/examples/react/zod/src/index.tsx b/examples/react/zod/src/index.tsx index e6c030808..daa78e259 100644 --- a/examples/react/zod/src/index.tsx +++ b/examples/react/zod/src/index.tsx @@ -33,57 +33,37 @@ export default function App() { return (

Zod Form Example

- -
{ - e.preventDefault() - e.stopPropagation() - void form.handleSubmit() - }} - > -
- {/* A type-safe field component*/} - { - 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 ( - <> - - field.handleChange(e.target.value)} - /> - - - ) - }} - /> -
-
- ( + { + e.preventDefault() + e.stopPropagation() + void form.handleSubmit() + }} + > +
+ {/* A type-safe field component*/} + { + 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 ( <> - + - )} - /> -
- [state.canSubmit, state.isSubmitting]} - children={([canSubmit, isSubmitting]) => ( - + ) + }} + /> +
+
+ ( + <> + + field.handleChange(e.target.value)} + /> + + )} /> - - +
+ [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> +
) } diff --git a/examples/solid/simple/src/index.tsx b/examples/solid/simple/src/index.tsx index f4e2a757b..05ec35658 100644 --- a/examples/solid/simple/src/index.tsx +++ b/examples/solid/simple/src/index.tsx @@ -34,58 +34,37 @@ function App() { return (

Simple Form Example

- -
{ - e.preventDefault() - e.stopPropagation() - void form.handleSubmit() - }} - > -
- {/* A type-safe field component*/} - - !value - ? 'A first name is required' - : value.length < 3 - ? 'First name must be at least 3 characters' - : undefined, - onChangeAsyncDebounceMs: 500, - onChangeAsync: async ({ value }) => { - await new Promise((resolve) => setTimeout(resolve, 1000)) - return ( - value.includes('error') && - 'No "error" allowed in first name' - ) - }, - }} - children={(field) => { - // Avoid hasty abstractions. Render props are great! + { + e.preventDefault() + e.stopPropagation() + void form.handleSubmit() + }} + > +
+ {/* A type-safe field component*/} + + !value + ? 'A first name is required' + : value.length < 3 + ? 'First name must be at least 3 characters' + : undefined, + onChangeAsyncDebounceMs: 500, + onChangeAsync: async ({ value }) => { + await new Promise((resolve) => setTimeout(resolve, 1000)) return ( - <> - - field().handleChange(e.target.value)} - /> - - + value.includes('error') && 'No "error" allowed in first name' ) - }} - /> -
-
- ( + }, + }} + children={(field) => { + // Avoid hasty abstractions. Render props are great! + return ( <> - + - )} - /> -
- ({ - canSubmit: state.canSubmit, - isSubmitting: state.isSubmitting, - })} - children={(state) => { - return ( - ) }} /> - - +
+
+ ( + <> + + field().handleChange(e.target.value)} + /> + + + )} + /> +
+ ({ + canSubmit: state.canSubmit, + isSubmitting: state.isSubmitting, + })} + children={(state) => { + return ( + + ) + }} + /> +
) } diff --git a/examples/solid/valibot/src/index.tsx b/examples/solid/valibot/src/index.tsx index 1c839fa4e..4224e0c74 100644 --- a/examples/solid/valibot/src/index.tsx +++ b/examples/solid/valibot/src/index.tsx @@ -38,54 +38,34 @@ function App() { return (

Valibot Form Example

- -
{ - e.preventDefault() - e.stopPropagation() - void form.handleSubmit() - }} - > -
- {/* A type-safe field component*/} - { - await new Promise((resolve) => setTimeout(resolve, 1000)) - return !value.includes('error') - }, "No 'error' allowed in first name"), - ]), - }} - children={(field) => { - // Avoid hasty abstractions. Render props are great! - return ( - <> - - field().handleChange(e.target.value)} - /> - - - ) - }} - /> -
-
- ( + { + e.preventDefault() + e.stopPropagation() + void form.handleSubmit() + }} + > +
+ {/* A type-safe field component*/} + { + await new Promise((resolve) => setTimeout(resolve, 1000)) + return !value.includes('error') + }, "No 'error' allowed in first name"), + ]), + }} + children={(field) => { + // Avoid hasty abstractions. Render props are great! + return ( <> - + - )} - /> -
- ({ - canSubmit: state.canSubmit, - isSubmitting: state.isSubmitting, - })} - children={(state) => { - return ( - ) }} /> - - +
+
+ ( + <> + + field().handleChange(e.target.value)} + /> + + + )} + /> +
+ ({ + canSubmit: state.canSubmit, + isSubmitting: state.isSubmitting, + })} + children={(state) => { + return ( + + ) + }} + /> +
) } diff --git a/examples/solid/yup/src/index.tsx b/examples/solid/yup/src/index.tsx index 999f2e849..7bd23e2b9 100644 --- a/examples/solid/yup/src/index.tsx +++ b/examples/solid/yup/src/index.tsx @@ -38,58 +38,38 @@ function App() { return (

Yup Form Example

- -
{ - e.preventDefault() - e.stopPropagation() - void form.handleSubmit() - }} - > -
- {/* A type-safe field component*/} - { - await new Promise((resolve) => setTimeout(resolve, 1000)) - return !value?.includes('error') - }, - ), - }} - children={(field) => { - // Avoid hasty abstractions. Render props are great! - return ( - <> - - field().handleChange(e.target.value)} - /> - - - ) - }} - /> -
-
- ( + { + e.preventDefault() + e.stopPropagation() + void form.handleSubmit() + }} + > +
+ {/* A type-safe field component*/} + { + await new Promise((resolve) => setTimeout(resolve, 1000)) + return !value?.includes('error') + }, + ), + }} + children={(field) => { + // Avoid hasty abstractions. Render props are great! + return ( <> - + - )} - /> -
- ({ - canSubmit: state.canSubmit, - isSubmitting: state.isSubmitting, - })} - children={(state) => { - return ( - ) }} /> - - +
+
+ ( + <> + + field().handleChange(e.target.value)} + /> + + + )} + /> +
+ ({ + canSubmit: state.canSubmit, + isSubmitting: state.isSubmitting, + })} + children={(state) => { + return ( + + ) + }} + /> +
) } diff --git a/examples/solid/zod/src/index.tsx b/examples/solid/zod/src/index.tsx index 4d36d1945..97c9dba78 100644 --- a/examples/solid/zod/src/index.tsx +++ b/examples/solid/zod/src/index.tsx @@ -38,57 +38,37 @@ function App() { return (

Zod Form Example

- -
{ - e.preventDefault() - e.stopPropagation() - void form.handleSubmit() - }} - > -
- {/* A type-safe field component*/} - { - 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 ( - <> - - field().handleChange(e.target.value)} - /> - - - ) - }} - /> -
-
- ( + { + e.preventDefault() + e.stopPropagation() + void form.handleSubmit() + }} + > +
+ {/* A type-safe field component*/} + { + 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 ( <> - + - )} - /> -
- ({ - canSubmit: state.canSubmit, - isSubmitting: state.isSubmitting, - })} - children={(state) => { - return ( - ) }} /> - - +
+
+ ( + <> + + field().handleChange(e.target.value)} + /> + + + )} + /> +
+ ({ + canSubmit: state.canSubmit, + isSubmitting: state.isSubmitting, + })} + children={(state) => { + return ( + + ) + }} + /> +
) } diff --git a/packages/react-form/src/tests/useField.test.tsx b/packages/react-form/src/tests/useField.test.tsx index f0f11277b..693cc1222 100644 --- a/packages/react-form/src/tests/useField.test.tsx +++ b/packages/react-form/src/tests/useField.test.tsx @@ -529,74 +529,69 @@ describe('useField', () => { return (
- -
{ - e.preventDefault() - e.stopPropagation() - void form.handleSubmit() - }} - > -
- {/* A type-safe field component*/} - {showField ? ( - - !value ? 'A first name is required' : undefined, - }} - children={(field) => { - // Avoid hasty abstractions. Render props are great! - return ( - <> - - field.handleChange(e.target.value)} - /> - - - ) - }} - /> - ) : null} -
-
+ { + e.preventDefault() + e.stopPropagation() + void form.handleSubmit() + }} + > +
+ {/* A type-safe field component*/} + {showField ? ( ( - <> - - field.handleChange(e.target.value)} - /> - - - )} + name="firstName" + validators={{ + onChange: ({ value }) => + !value ? 'A first name is required' : undefined, + }} + children={(field) => { + // Avoid hasty abstractions. Render props are great! + return ( + <> + + field.handleChange(e.target.value)} + /> + + + ) + }} /> -
- [state.canSubmit, state.isSubmitting]} - children={([canSubmit, isSubmitting]) => ( - + ) : null} +
+
+ ( + <> + + field.handleChange(e.target.value)} + /> + + )} /> - - - +
+ [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> + +
) } diff --git a/packages/solid-form/src/formContext.ts b/packages/solid-form/src/formContext.ts deleted file mode 100644 index 8408b8086..000000000 --- a/packages/solid-form/src/formContext.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createContext, useContext } from 'solid-js' -import type { FormApi, Validator } from '@tanstack/form-core' - -type FormContextType = - | undefined - | { - formApi: FormApi | undefined> - parentFieldName?: string - } - -export const formContext = createContext(undefined) - -export function useFormContext() { - const formApi: FormContextType = useContext(formContext) - - if (!formApi) - throw new Error(`You are trying to use the form API outside of a form!`) - - return formApi -} diff --git a/packages/vue-form/src/useForm.tsx b/packages/vue-form/src/useForm.tsx index cc5454e4b..e41b9a5a3 100644 --- a/packages/vue-form/src/useForm.tsx +++ b/packages/vue-form/src/useForm.tsx @@ -38,7 +38,12 @@ export function useForm< api.Field = defineComponent( (props, context) => { - return () => h(Field as never, { ...props, ...context.attrs, form: api }, context.slots) + return () => + h( + Field as never, + { ...props, ...context.attrs, form: api }, + context.slots, + ) }, { name: 'APIField', From 2afe39066e6db82f3b896388f392d6b3c962fb98 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 4 Mar 2024 18:10:43 -0800 Subject: [PATCH 09/23] docs: remove mention of provider from docs --- docs/framework/react/guides/ssr.md | 7 ++----- docs/framework/react/guides/ui-libraries.md | 6 ++---- docs/framework/react/quick-start.md | 2 -- docs/framework/react/reference/formApi.md | 4 ---- docs/framework/solid/quick-start.md | 2 -- docs/framework/vue/quick-start.md | 6 ++---- docs/overview.md | 2 -- 7 files changed, 6 insertions(+), 23 deletions(-) diff --git a/docs/framework/react/guides/ssr.md b/docs/framework/react/guides/ssr.md index 5b1006df4..ac1ad25c6 100644 --- a/docs/framework/react/guides/ssr.md +++ b/docs/framework/react/guides/ssr.md @@ -98,7 +98,7 @@ const ClientComp = () => { formFactory.initialFormState ); - const { useStore, Provider, Subscribe, handleSubmit, Field } = + const { useStore, Subscribe, handleSubmit, Field } = formFactory.useForm({ transform: useTransform( (baseForm: FormApi) => mergeForm(baseForm, state), @@ -120,7 +120,6 @@ const ClientComp = () => { - Benefits of `formFactory`: Much like the useForm hook, formFactory streamlines the process of form management. It provides us with necessary functionalities such as: - `useStore`: Observes and reflects the current state of the form on the client side. - - `Provider`: Acts as a context provider for the form, ensuring state and actions are accessible throughout the component. - `Subscribe`: Enables the component to listen to form-specific events, like `canSubmit` and `isSubmitting`. - `handleSubmit`: Orchestrates the submission logic of the form. - `Field`: Manages individual form fields, adopting the `renderProps` pattern for greater flexibility. @@ -136,7 +135,6 @@ const ClientComp = () => { ... return ( -
handleSubmit()}> {formErrors.map((error) => (

{error}

@@ -181,11 +179,10 @@ const ClientComp = () => { )}
-
); }; ``` -- In our UI, implementing the form is straightforward. We encapsulate our form within a `Provider`. A notable aspect here is the integration of our server action within the `form`. For the form's `action`, we utilize the `action` obtained from `useFormState`. This setup triggers the server action upon form submission. If everything processes successfully, the action will complete without issues. Otherwise, we'll encounter an error like "Server validation: You must be at least 12 to sign up." +- In our UI, implementing the form is straightforward. A notable aspect here is the integration of our server action within the `form`. For the form's `action`, we utilize the `action` obtained from `useFormState`. This setup triggers the server action upon form submission. If everything processes successfully, the action will complete without issues. Otherwise, we'll encounter an error like "Server validation: You must be at least 12 to sign up." - You might now be wondering about client-side validation. How do we implement it? 🤔 The answer lies in the `handleSubmit` function. By assigning `handleSubmit` to the form's `onSubmit` event, we can handle client-side validation in the normal client side manner. diff --git a/docs/framework/react/guides/ui-libraries.md b/docs/framework/react/guides/ui-libraries.md index 27a2ee626..a8f55d4b2 100644 --- a/docs/framework/react/guides/ui-libraries.md +++ b/docs/framework/react/guides/ui-libraries.md @@ -27,7 +27,7 @@ import { TextInput, Checkbox } from '@mantine/core' import { useForm } from '@tanstack/react-form' export default function App() { - const { Provider, Field, handleSubmit, state } = useForm({ + const { Field, handleSubmit, state } = useForm({ defaultValues: { firstName: '', lastName: '', @@ -41,7 +41,6 @@ export default function App() { return ( <> -
{ e.preventDefault() @@ -70,7 +69,6 @@ export default function App() { )} />
-
{JSON.stringify(state.values, null, 2)}
@@ -80,7 +78,7 @@ export default function App() { ``` - Initially, we utilize the `useForm` hook from TanStack and destructure the necessary properties. This step is optional; alternatively, you could use `const form = useForm()` if preferred. TypeScript's type inference ensures a smooth experience regardless of the approach. -- Next, we encapsulate our form elements within the `Provider` component, a critical step for enabling form functionalities. The `Field` component, derived from `useForm`, accepts several properties, such as `validators`. For this demonstration, we focus on two primary properties: `name` and `children`. +- The `Field` component, derived from `useForm`, accepts several properties, such as `validators`. For this demonstration, we focus on two primary properties: `name` and `children`. - The `name` property identifies each `Field`, for instance, `firstName` in our example. - The `children` property leverages the concept of render props, allowing us to integrate components without unnecessary abstractions. - TanStack's design relies heavily on render props, providing access to `children` within the `Field` component. This approach is entirely type-safe. When integrating with Mantine components, such as `TextInput`, we selectively destructure properties like `state.value`, `handleChange`, and `handleBlur`. This selective approach is due to the slight differences in types between `TextInput` and the `field` we get in the children. diff --git a/docs/framework/react/quick-start.md b/docs/framework/react/quick-start.md index c1a7d6aa6..3ad03dd28 100644 --- a/docs/framework/react/quick-start.md +++ b/docs/framework/react/quick-start.md @@ -23,7 +23,6 @@ export default function App() { return (
-
{ e.preventDefault(); @@ -46,7 +45,6 @@ export default function App() {
-
) } diff --git a/docs/framework/react/reference/formApi.md b/docs/framework/react/reference/formApi.md index 1a966802c..3b67e9ff3 100644 --- a/docs/framework/react/reference/formApi.md +++ b/docs/framework/react/reference/formApi.md @@ -7,10 +7,6 @@ title: Form API When using `@tanstack/react-form`, the [core form API](../../reference/formApi) is extended at type level with additional methods for React-specific functionality: -- ```tsx - Provider: (props: PropsWithChildren) => JSX.Element - ``` - - React provider use to wrap your components. Reference React's [ContextProvider]("https://react.dev/reference/react/createContext#provider") - ```tsx Field: FieldComponent ``` diff --git a/docs/framework/solid/quick-start.md b/docs/framework/solid/quick-start.md index a00a736ae..913da7377 100644 --- a/docs/framework/solid/quick-start.md +++ b/docs/framework/solid/quick-start.md @@ -22,7 +22,6 @@ function App() { return (

Simple Form Example

-
{ e.preventDefault() @@ -45,7 +44,6 @@ function App() {
-
) } diff --git a/docs/framework/vue/quick-start.md b/docs/framework/vue/quick-start.md index 96740703a..5102daaad 100644 --- a/docs/framework/vue/quick-start.md +++ b/docs/framework/vue/quick-start.md @@ -3,11 +3,11 @@ id: quick-start title: Quick Start --- -> There is a bug in Vue's TypeScript support that's impacting our types that you'll likely run into: +> There is a bug in Vue's TypeScript support that's impacting our types that you'll likely run into: > > https://github.com/vuejs/language-tools/issues/3782 > -> Please give it a thumbs up, but _do not reply with comments such as "+1" or "When will this be fixed?", as such comments are unhelpful and rude_. +> Please give it a thumbs up, but _do not reply with comments such as "+1" or "When will this be fixed?", as such comments are unhelpful and rude_. The bare minimum to get started with TanStack Form is to create a form and add a field. Keep in mind that this example does not include any validation or error handling... yet. @@ -25,8 +25,6 @@ const form = useForm({ console.log(value) }, }) - -form.provideFormContext()