diff --git a/packages/react-form/src/createFormHook.tsx b/packages/react-form/src/createFormHook.tsx index 548fc2322..5b64b9d11 100644 --- a/packages/react-form/src/createFormHook.tsx +++ b/packages/react-form/src/createFormHook.tsx @@ -16,6 +16,7 @@ import type { import type { ComponentType, Context, JSX, PropsWithChildren } from 'react' import type { FieldComponent } from './useField' import type { ReactFormExtendedApi } from './useForm' +import type { AppFieldComponents, AppFormComponents, DataTag } from './types' import type { AppFieldExtendedReactFieldGroupApi } from './useFieldGroup' // We should never hit the `null` case here @@ -59,6 +60,39 @@ type UnwrapDefaultOrAny = [DefaultT] extends [T] : T : T +/** + * Create a field component based on a provided React component. + * If `TFieldValue` is provided, it will restrict its use to AppFields + * that extend that value. + * + * @example + * ```tsx + * interface TextFieldProps { + * label: string; + * } + * function TextField(props: TextFieldProps) { + * const field = useFieldContext(); + * // ... + * return <> + * } + * // create a TextField component that may only be used in string AppFields + * const TextFieldComponent = createFieldComponent(TextField); + * + * // in your form hook + * createFormHook({ + * // ... + * fieldComponents: { + * TextField: TextFieldComponent + * } + * }) + * ``` + */ +function createFieldComponent( + component: ComponentType, +): DataTag, TFieldValue> { + return component as never +} + export function createFormHookContexts() { function useFieldContext() { const field = useContext(fieldContext) @@ -124,12 +158,18 @@ export function createFormHookContexts() { > } - return { fieldContext, useFieldContext, useFormContext, formContext } + return { + fieldContext, + useFieldContext, + useFormContext, + formContext, + createFieldComponent, + } } interface CreateFormHookProps< - TFieldComponents extends Record>, - TFormComponents extends Record>, + TFieldComponents extends AppFieldComponents, + TFormComponents extends AppFormComponents, > { fieldComponents: TFieldComponents fieldContext: Context @@ -153,8 +193,8 @@ export type AppFieldExtendedReactFormApi< TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TOnServer extends undefined | FormAsyncValidateOrFn, TSubmitMeta, - TFieldComponents extends Record>, - TFormComponents extends Record>, + TFieldComponents extends AppFieldComponents, + TFormComponents extends AppFormComponents, > = ReactFormExtendedApi< TFormData, TOnMount, @@ -201,8 +241,8 @@ export interface WithFormProps< TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TOnServer extends undefined | FormAsyncValidateOrFn, TSubmitMeta, - TFieldComponents extends Record>, - TFormComponents extends Record>, + TFieldComponents extends AppFieldComponents, + TFormComponents extends AppFormComponents, TRenderProps extends object = Record, > extends FormOptions< TFormData, @@ -282,8 +322,8 @@ export interface WithFieldGroupProps< } export function createFormHook< - const TComponents extends Record>, - const TFormComponents extends Record>, + const TComponents extends AppFieldComponents, + const TFormComponents extends AppFormComponents, >({ fieldComponents, fieldContext, diff --git a/packages/react-form/src/types.ts b/packages/react-form/src/types.ts index c2614238e..9054c97ef 100644 --- a/packages/react-form/src/types.ts +++ b/packages/react-form/src/types.ts @@ -8,6 +8,51 @@ import type { FormAsyncValidateOrFn, FormValidateOrFn, } from '@tanstack/form-core' +import type { ComponentType } from 'react' + +declare const dataTagFieldValueSymbol: unique symbol + +type AnyDataTag = { + [dataTagFieldValueSymbol]: any +} + +/** + * @private + */ +export type DataTag = TType extends AnyDataTag + ? TType + : TType & { + [dataTagFieldValueSymbol]: TFieldValue + } +/** + * @private + */ +export type AppFieldComponents = Record< + string, + ComponentType | DataTag, any> +> + +/** + * @private + */ +export type AppFieldComponentsOfType< + TFieldValue, + TRecord extends AppFieldComponents, +> = { + [K in keyof TRecord as TRecord[K] extends DataTag< + unknown, + infer TaggedFieldValue + > + ? TaggedFieldValue extends TFieldValue // does the brand match? + ? K + : never // brand doesn't match + : K]: TRecord[K] +} + +/** + * @private + */ +export type AppFormComponents = Record> interface FieldOptionsMode { mode?: 'value' | 'array' diff --git a/packages/react-form/src/useField.tsx b/packages/react-form/src/useField.tsx index 61d12c730..9d727c894 100644 --- a/packages/react-form/src/useField.tsx +++ b/packages/react-form/src/useField.tsx @@ -12,7 +12,12 @@ import type { FormValidateOrFn, } from '@tanstack/form-core' import type { FunctionComponent, ReactNode } from 'react' -import type { UseFieldOptions, UseFieldOptionsBound } from './types' +import type { + AppFieldComponents, + AppFieldComponentsOfType, + UseFieldOptions, + UseFieldOptionsBound, +} from './types' interface ReactFieldApi< TParentData, @@ -26,7 +31,7 @@ interface ReactFieldApi< TFormOnDynamic extends undefined | FormValidateOrFn, TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TFormOnServer extends undefined | FormAsyncValidateOrFn, - TPatentSubmitMeta, + TParentSubmitMeta, > { /** * A pre-bound and type-safe sub-field component using this field as a root. @@ -43,7 +48,7 @@ interface ReactFieldApi< TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, - TPatentSubmitMeta + TParentSubmitMeta > } @@ -64,7 +69,7 @@ export type UseField< TFormOnDynamic extends undefined | FormValidateOrFn, TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TFormOnServer extends undefined | FormAsyncValidateOrFn, - TPatentSubmitMeta, + TParentSubmitMeta, > = < TName extends DeepKeys, TData extends DeepValue, @@ -123,7 +128,7 @@ export type UseField< TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, - TPatentSubmitMeta + TParentSubmitMeta > /** @@ -163,7 +168,7 @@ export function useField< TFormOnDynamic extends undefined | FormValidateOrFn, TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TFormOnServer extends undefined | FormAsyncValidateOrFn, - TPatentSubmitMeta, + TParentSubmitMeta, >( opts: UseFieldOptions< TParentData, @@ -188,7 +193,7 @@ export function useField< TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, - TPatentSubmitMeta + TParentSubmitMeta >, ) { const [fieldApi] = useState(() => { @@ -211,7 +216,7 @@ export function useField< TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, - TPatentSubmitMeta + TParentSubmitMeta > = api as never extendedApi.Field = Field as never @@ -278,8 +283,8 @@ interface FieldComponentProps< TFormOnDynamic extends undefined | FormValidateOrFn, TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TFormOnServer extends undefined | FormAsyncValidateOrFn, - TPatentSubmitMeta, - ExtendedApi = {}, + TParentSubmitMeta, + ExtendedApi extends AppFieldComponents = {}, > extends UseFieldOptions< TParentData, TName, @@ -303,7 +308,7 @@ interface FieldComponentProps< TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, - TPatentSubmitMeta + TParentSubmitMeta > { children: ( fieldApi: FieldApi< @@ -329,9 +334,9 @@ interface FieldComponentProps< TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, - TPatentSubmitMeta + TParentSubmitMeta > & - ExtendedApi, + AppFieldComponentsOfType, ) => ReactNode } @@ -366,8 +371,8 @@ interface FieldComponentBoundProps< TFormOnDynamic extends undefined | FormValidateOrFn, TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TFormOnServer extends undefined | FormAsyncValidateOrFn, - TPatentSubmitMeta, - ExtendedApi = {}, + TParentSubmitMeta, + ExtendedApi extends AppFieldComponents = {}, > extends UseFieldOptionsBound< TParentData, TName, @@ -406,9 +411,9 @@ interface FieldComponentBoundProps< TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, - TPatentSubmitMeta + TParentSubmitMeta > & - ExtendedApi, + AppFieldComponentsOfType, ) => ReactNode } @@ -435,8 +440,8 @@ export type FieldComponent< | undefined | FormAsyncValidateOrFn, in out TFormOnServer extends undefined | FormAsyncValidateOrFn, - in out TPatentSubmitMeta, - in out ExtendedApi = {}, + in out TParentSubmitMeta, + in out ExtendedApi extends AppFieldComponents = {}, > = < const TName extends DeepKeys, TData extends DeepValue, @@ -483,7 +488,7 @@ export type FieldComponent< TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, - TPatentSubmitMeta, + TParentSubmitMeta, ExtendedApi >) => ReactNode @@ -611,7 +616,7 @@ export const Field = (< TFormOnDynamic extends undefined | FormValidateOrFn, TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TFormOnServer extends undefined | FormAsyncValidateOrFn, - TPatentSubmitMeta, + TParentSubmitMeta, >({ children, ...fieldOptions @@ -638,7 +643,7 @@ export const Field = (< TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, - TPatentSubmitMeta + TParentSubmitMeta >): ReactNode => { const fieldApi = useField(fieldOptions as any)