diff --git a/.changeset/great-ghosts-sin.md b/.changeset/great-ghosts-sin.md new file mode 100644 index 000000000..ad153f41d --- /dev/null +++ b/.changeset/great-ghosts-sin.md @@ -0,0 +1,5 @@ +--- +'@tanstack/react-form': patch +--- + +- add `useTypedAppFormContext` diff --git a/packages/react-form/src/createFormHook.tsx b/packages/react-form/src/createFormHook.tsx index 4413aa999..a654ee8e7 100644 --- a/packages/react-form/src/createFormHook.tsx +++ b/packages/react-form/src/createFormHook.tsx @@ -64,6 +64,33 @@ type UnwrapDefaultOrAny = [DefaultT] extends [T] : T : T +function useFormContext() { + const form = useContext(formContext) + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!form) { + throw new Error( + '`formContext` only works when within a `formComponent` passed to `createFormHook`', + ) + } + + return form as ReactFormExtendedApi< + // If you need access to the form data, you need to use `withForm` instead + Record, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any + > +} + export function createFormHookContexts() { function useFieldContext() { const field = useContext(fieldContext) @@ -102,33 +129,6 @@ export function createFormHookContexts() { > } - function useFormContext() { - const form = useContext(formContext) - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!form) { - throw new Error( - '`formContext` only works when within a `formComponent` passed to `createFormHook`', - ) - } - - return form as ReactFormExtendedApi< - // If you need access to the form data, you need to use `withForm` instead - Record, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any - > - } - return { fieldContext, useFieldContext, useFormContext, formContext } } @@ -536,9 +536,64 @@ export function createFormHook< } } + /** + * ⚠️ **Use withForm whenever possible.** + * + * Gets a typed form from the `` context. + */ + function useTypedAppFormContext< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta, + >( + _props: FormOptions< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + >, + ): AppFieldExtendedReactFormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + TComponents, + TFormComponents + > { + const form = useFormContext() + + return form as never + } + return { useAppForm, withForm, withFieldGroup, + useTypedAppFormContext, } } diff --git a/packages/react-form/tests/createFormHook.test.tsx b/packages/react-form/tests/createFormHook.test.tsx index 7f57c3e2b..3cbc9be64 100644 --- a/packages/react-form/tests/createFormHook.test.tsx +++ b/packages/react-form/tests/createFormHook.test.tsx @@ -31,16 +31,17 @@ function SubscribeButton({ label }: { label: string }) { ) } -const { useAppForm, withForm, withFieldGroup } = createFormHook({ - fieldComponents: { - TextField, - }, - formComponents: { - SubscribeButton, - }, - fieldContext, - formContext, -}) +const { useAppForm, withForm, withFieldGroup, useTypedAppFormContext } = + createFormHook({ + fieldComponents: { + TextField, + }, + formComponents: { + SubscribeButton, + }, + fieldContext, + formContext, + }) describe('createFormHook', () => { it('should allow to set default value', () => { @@ -580,4 +581,47 @@ describe('createFormHook', () => { await user.click(target) expect(result).toHaveTextContent('1') }) + + it('should allow using typed app form', () => { + type Person = { + firstName: string + lastName: string + } + const formOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + function Child() { + const form = useTypedAppFormContext(formOpts) + + return ( + } + /> + ) + } + + function Parent() { + const form = useAppForm({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + return ( + + + + ) + } + + const { getByLabelText } = render() + const input = getByLabelText('Testing') + expect(input).toHaveValue('FirstName') + }) })