import { createComponent, createContext, mergeProps, splitProps, useContext, } from 'solid-js' import { createFieldGroup } from './createFieldGroup' import { createForm } from './createForm' import type { AnyFieldApi, AnyFormApi, BaseFormOptions, DeepKeysOfType, FieldApi, FieldsMap, FormAsyncValidateOrFn, FormOptions, FormValidateOrFn, } from '@tanstack/form-core' import type { Accessor, Component, Context, JSXElement, ParentProps, } from 'solid-js' import type { FieldComponent } from './createField' import type { AppFieldExtendedSolidFieldGroupApi } from './createFieldGroup' import type { SolidFormExtendedApi } from './createForm' /** * TypeScript inferencing is weird. * * If you have: * * @example * * interface Args { * arg?: T * } * * function test(arg?: Partial>): T { * return 0 as any; * } * * const a = test({}); * * Then `T` will default to `unknown`. * * However, if we change `test` to be: * * @example * * function test(arg?: Partial>): T; * * Then `T` becomes `undefined`. * * Here, we are checking if the passed type `T` extends `DefaultT` and **only** * `DefaultT`, as if that's the case we assume that inferencing has not occurred. */ type UnwrapOrAny = [unknown] extends [T] ? any : T type UnwrapDefaultOrAny = [DefaultT] extends [T] ? [T] extends [DefaultT] ? any : T : T export function createFormHookContexts() { // We should never hit the `null` case here const fieldContext = createContext>( null as unknown as Accessor, ) function useFieldContext() { const field = useContext(fieldContext) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!field) { throw new Error( '`fieldContext` only works when within a `fieldComponent` passed to `createFormHook`', ) } return field as Accessor< FieldApi< any, string, TData, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any > > } // We should never hit the `null` case here const formContext = createContext(null as unknown as AnyFormApi) 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 SolidFormExtendedApi< // 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 } } interface CreateFormHookProps< TFieldComponents extends Record>, TFormComponents extends Record>, > { fieldComponents: TFieldComponents fieldContext: Context> formComponents: TFormComponents formContext: Context } /** * @private */ export type AppFieldExtendedSolidFormApi< 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, TFieldComponents extends Record>, TFormComponents extends Record>, > = SolidFormExtendedApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > & NoInfer & { AppField: FieldComponent< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, NoInfer > AppForm: Component } export interface WithFormProps< 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, TFieldComponents extends Record>, TFormComponents extends Record>, TRenderProps extends Record = Record, > extends FormOptions< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > { // Optional, but adds props to the `render` function outside of `form` props?: TRenderProps render: ( props: ParentProps< NoInfer & { form: AppFieldExtendedSolidFormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, TFieldComponents, TFormComponents > } >, ) => JSXElement } export interface WithFieldGroupProps< TFieldGroupData, TFieldComponents extends Record>, TFormComponents extends Record>, TSubmitMeta, TRenderProps extends Record = Record, > extends BaseFormOptions { // Optional, but adds props to the `render` function outside of `form` props?: TRenderProps render: ( props: ParentProps< NoInfer & { group: AppFieldExtendedSolidFieldGroupApi< unknown, TFieldGroupData, string | FieldsMap, undefined | FormValidateOrFn, undefined | FormValidateOrFn, undefined | FormAsyncValidateOrFn, undefined | FormValidateOrFn, undefined | FormAsyncValidateOrFn, undefined | FormValidateOrFn, undefined | FormAsyncValidateOrFn, undefined | FormValidateOrFn, undefined | FormAsyncValidateOrFn, undefined | FormAsyncValidateOrFn, // this types it as 'never' in the render prop. It should prevent any // untyped meta passed to the handleSubmit by accident. unknown extends TSubmitMeta ? never : TSubmitMeta, TFieldComponents, TFormComponents > } >, ) => JSXElement } export function createFormHook< const TComponents extends Record>, const TFormComponents extends Record>, >(opts: CreateFormHookProps) { function useAppForm< 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: Accessor< FormOptions< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > >, ): AppFieldExtendedSolidFormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, TComponents, TFormComponents > { const form = createForm(props) const AppForm = ((formProps) => { return ( {formProps.children} ) }) as Component const AppField = ((_props) => { const [childProps, fieldProps] = splitProps(_props, ['children']) return ( {(field) => ( {createComponent( () => childProps.children( Object.assign(field, opts.fieldComponents), ), {}, )} )} ) }) as FieldComponent< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, TComponents > const extendedForm: AppFieldExtendedSolidFormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, TComponents, TFormComponents > = form as never extendedForm.AppField = AppField extendedForm.AppForm = AppForm for (const [key, value] of Object.entries(opts.formComponents)) { // Since it's a generic I need to cast it to an object ;(extendedForm as Record)[key] = value } return extendedForm } function withForm< 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, TRenderProps extends Record = {}, >({ render, props, }: WithFormProps< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, TComponents, TFormComponents, TRenderProps >): WithFormProps< UnwrapOrAny, UnwrapDefaultOrAny, TOnMount>, UnwrapDefaultOrAny, TOnChange>, UnwrapDefaultOrAny, TOnChangeAsync>, UnwrapDefaultOrAny, TOnBlur>, UnwrapDefaultOrAny, TOnBlurAsync>, UnwrapDefaultOrAny, TOnSubmit>, UnwrapDefaultOrAny, TOnSubmitAsync>, UnwrapDefaultOrAny, TOnDynamic>, UnwrapDefaultOrAny< undefined | FormValidateOrFn, TOnDynamicAsync >, UnwrapDefaultOrAny, TOnServer>, UnwrapOrAny, UnwrapOrAny, UnwrapOrAny, UnwrapOrAny >['render'] { return (innerProps) => createComponent( render as Component, mergeProps(props ?? {}, innerProps), ) } function withFieldGroup< TFieldGroupData, TSubmitMeta, TRenderProps extends Record = {}, >({ render, props, defaultValues, }: WithFieldGroupProps< TFieldGroupData, TComponents, TFormComponents, TSubmitMeta, TRenderProps >): < TFormData, TFields extends | DeepKeysOfType | FieldsMap, 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, TFormSubmitMeta, >( params: ParentProps< NoInfer & { form: | AppFieldExtendedSolidFormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, unknown extends TSubmitMeta ? TFormSubmitMeta : TSubmitMeta, TComponents, TFormComponents > | AppFieldExtendedSolidFieldGroupApi< // Since this only occurs if you nest it within other field groups, it can be more // lenient with the types. unknown, TFormData, string | FieldsMap, any, any, any, any, any, any, any, any, any, any, unknown extends TSubmitMeta ? TFormSubmitMeta : TSubmitMeta, TComponents, TFormComponents > fields: TFields } >, ) => JSXElement { return function Render(innerProps) { const fieldGroupProps = { form: innerProps.form, fields: innerProps.fields, defaultValues, formComponents: opts.formComponents, } const fieldGroupApi = createFieldGroup(() => fieldGroupProps) return createComponent( render as Component, mergeProps(props ?? {}, innerProps, { group: fieldGroupApi as any }), ) } } return { useAppForm, withForm, withFieldGroup, } }