fix: refactor to flat generics with consistent names & patterns

This commit is contained in:
Tanner Linsley
2023-09-13 14:46:16 -06:00
parent 46b49ca669
commit 1fb28c53eb
15 changed files with 252 additions and 225 deletions

View File

@@ -3,18 +3,23 @@ id: field
title: Field title: Field
--- ---
### `FieldComponent<TFormData>` ### `FieldComponent<TParentData>`
A type alias representing a field component for a specific form data type. A type alias representing a field component for a specific form data type.
```tsx ```tsx
export type FieldComponent = <TField extends DeepKeys<TFormData>>({ export type FieldComponent = <TField extends DeepKeys<TParentData>>({
children, children,
...fieldOptions ...fieldOptions
}: { }: {
children: (fieldApi: FieldApi<DeepValue<TFormData, TField>, TFormData>) => any children: (
fieldApi: FieldApi<DeepValue<TParentData, TField>, TParentData>,
) => any
name: TField name: TField
} & Omit<FieldOptions<DeepValue<TFormData, TField>, TFormData>, 'name'>) => any } & Omit<
FieldOptions<DeepValue<TParentData, TField>, TParentData>,
'name'
>) => any
``` ```
A function component that takes field options and a render function as children and returns a React component. A function component that takes field options and a render function as children and returns a React component.
@@ -22,23 +27,23 @@ A function component that takes field options and a render function as children
### `Field` ### `Field`
```tsx ```tsx
export function Field<TData, TFormData>({ export function Field<TData, TParentData>({
children, children,
...fieldOptions ...fieldOptions
}: { children: (fieldApi: FieldApi<TData, TFormData>) => any } & FieldOptions< }: { children: (fieldApi: FieldApi<TData, TParentData>) => any } & FieldOptions<
TData, TData,
TFormData TParentData
>): any >): any
``` ```
A functional React component that renders a form field. A functional React component that renders a form field.
- ```tsx - ```tsx
children: (fieldApi: FieldApi<TData, TFormData>) => any children: (fieldApi: FieldApi<TData, TParentData>) => any
``` ```
- A render function that takes a field API instance and returns a React element. - A render function that takes a field API instance and returns a React element.
- ```tsx - ```tsx
fieldOptions: FieldOptions<TData, TFormData> fieldOptions: FieldOptions<TData, TParentData>
``` ```
- The field options. - The field options.
@@ -47,14 +52,14 @@ The `Field` component uses the `useField` hook internally to manage the field in
### `createFieldComponent` ### `createFieldComponent`
```tsx ```tsx
export function createFieldComponent<TFormData>( export function createFieldComponent<TParentData>(
formApi: FormApi<TFormData>, formApi: FormApi<TParentData>,
): FieldComponent<TFormData> ): FieldComponent<TParentData>
``` ```
A factory function that creates a connected field component for a specific form API instance. A factory function that creates a connected field component for a specific form API instance.
- ```tsx - ```tsx
formApi: FormApi<TFormData> formApi: FormApi<TParentData>
``` ```
- The form API instance to connect the field component to. - The form API instance to connect the field component to.

View File

@@ -3,11 +3,11 @@ id: fieldApi
title: Field API title: Field API
--- ---
### `FieldApi<TData, TFormData>` ### `FieldApi<TData, TParentData>`
When using `@tanstack/react-form`, the [core field API](../../reference/fieldApi) is extended with additional methods for React-specific functionality: When using `@tanstack/react-form`, the [core field API](../../reference/fieldApi) is extended with additional methods for React-specific functionality:
- ```tsx - ```tsx
Field: FieldComponent<TData, TFormData> Field: FieldComponent<TData, TParentData>
``` ```
- A pre-bound and type-safe sub-field component using this field as a root. - A pre-bound and type-safe sub-field component using this field as a root.

View File

@@ -3,17 +3,17 @@ id: useField
title: useField title: useField
--- ---
### `UseField<TFormData>` ### `UseField<TParentData>`
A type representing a hook for using a field in a form with the given form data type. A type representing a hook for using a field in a form with the given form data type.
```tsx ```tsx
export type UseField = <TField extends DeepKeys<TFormData>>( export type UseField = <TField extends DeepKeys<TParentData>>(
opts?: { name: TField } & FieldOptions< opts?: { name: TField } & FieldOptions<
DeepValue<TFormData, TField>, DeepValue<TParentData, TField>,
TFormData TParentData
>, >,
) => FieldApi<DeepValue<TFormData, TField>, TFormData> ) => FieldApi<DeepValue<TParentData, TField>, TParentData>
``` ```
- A function that takes an optional object with a `name` property and field options, and returns a `FieldApi` instance for the specified field. - A function that takes an optional object with a `name` property and field options, and returns a `FieldApi` instance for the specified field.
@@ -21,31 +21,31 @@ export type UseField = <TField extends DeepKeys<TFormData>>(
### `useField` ### `useField`
```tsx ```tsx
export function useField<TData, TFormData>( export function useField<TData, TParentData>(
opts: FieldOptions<TData, TFormData>, opts: FieldOptions<TData, TParentData>,
): FieldApi<TData, TFormData> ): FieldApi<TData, TParentData>
``` ```
A hook for managing a field in a form. A hook for managing a field in a form.
- ```tsx - ```tsx
opts: FieldOptions<TData, TFormData> opts: FieldOptions<TData, TParentData>
``` ```
- An object with field options. - An object with field options.
#### Returns #### Returns
- ```tsx - ```tsx
FieldApi<TData, TFormData> FieldApi<TData, TParentData>
``` ```
- The `FieldApi` instance for the specified field. - The `FieldApi` instance for the specified field.
### `createUseField` ### `createUseField`
```tsx ```tsx
export function createUseField<TFormData>( export function createUseField<TParentData>(
formApi: FormApi<TFormData>, formApi: FormApi<TParentData>,
): UseField<TFormData> ): UseField<TParentData>
``` ```
A function that creates a `UseField` hook bound to the given `formApi`. A function that creates a `UseField` hook bound to the given `formApi`.

View File

@@ -11,14 +11,14 @@ Normally, you will not need to create a new `FieldApi` instance directly. Instea
const fieldApi: FieldApi<TData> = new FieldApi(formOptions: Field Options<TData>) const fieldApi: FieldApi<TData> = new FieldApi(formOptions: Field Options<TData>)
``` ```
### `FieldOptions<TData, TFormData>` ### `FieldOptions<TData, TParentData>`
An object type representing the options for a field in a form. An object type representing the options for a field in a form.
- ```tsx - ```tsx
name name
``` ```
- The field name. If `TFormData` is `unknown`, the type will be `string`. Otherwise, it will be `DeepKeys<TFormData>`. - The field name. If `TParentData` is `unknown`, the type will be `string`. Otherwise, it will be `DeepKeys<TParentData>`.
- ```tsx - ```tsx
defaultValue?: TData defaultValue?: TData
``` ```
@@ -30,19 +30,19 @@ An object type representing the options for a field in a form.
- An optional object with default metadata for the field. - An optional object with default metadata for the field.
- ```tsx - ```tsx
onMount?: (formApi: FieldApi<TData, TFormData>) => void onMount?: (formApi: FieldApi<TData, TParentData>) => void
``` ```
- An optional function that takes a param of `formApi` which is a generic type of `TData` and `TFormData` - An optional function that takes a param of `formApi` which is a generic type of `TData` and `TParentData`
- ```tsx - ```tsx
onChange?: ValidateFn<TData, TFormData> onChange?: ValidateFn<TData, TParentData>
``` ```
- An optional property that takes a `ValidateFn` which is a generic of `TData` and `TFormData` - An optional property that takes a `ValidateFn` which is a generic of `TData` and `TParentData`
- ```tsx - ```tsx
onChangeAsync?: ValidateAsyncFn<TData, TFormData> onChangeAsync?: ValidateAsyncFn<TData, TParentData>
``` ```
- An optional property similar to `onChange` but async validation - An optional property similar to `onChange` but async validation
@@ -55,16 +55,16 @@ An object type representing the options for a field in a form.
- If set to a number larger than 0, will debounce the async validation event by this length of time in milliseconds - If set to a number larger than 0, will debounce the async validation event by this length of time in milliseconds
- ```tsx - ```tsx
onBlur?: ValidateFn<TData, TFormData> onBlur?: ValidateFn<TData, TParentData>
``` ```
- An optional function, when that run when subscribing to blur event of input - An optional function, when that run when subscribing to blur event of input
- ```tsx - ```tsx
onBlurAsync?: ValidateAsyncFn<TData, TFormData> onBlurAsync?: ValidateAsyncFn<TData, TParentData>
``` ```
- An optional function that takes a `ValidateFn` which is a generic of `TData` and `TFormData` happens async - An optional function that takes a `ValidateFn` which is a generic of `TData` and `TParentData` happens async
```tsx ```tsx
onBlurAsyncDebounceMs?: number onBlurAsyncDebounceMs?: number
@@ -110,13 +110,13 @@ An object type representing the metadata of a field in a form.
``` ```
- A flag indicating whether the field is currently being validated. - A flag indicating whether the field is currently being validated.
### `FieldApiOptions<TData, TFormData>` ### `FieldApiOptions<TData, TParentData>`
An object type representing the required options for the `FieldApi` class. An object type representing the required options for the `FieldApi` class.
- Inherits from `FieldOptions<TData, TFormData>` with the `form` property set as required. - Inherits from `FieldOptions<TData, TParentData>` with the `form` property set as required.
### `FieldApi<TData, TFormData>` ### `FieldApi<TData, TParentData>`
A class representing the API for managing a form field. A class representing the API for managing a form field.
@@ -127,11 +127,11 @@ A class representing the API for managing a form field.
``` ```
- A unique identifier for the field instance. - A unique identifier for the field instance.
- ```tsx - ```tsx
form: FormApi<TFormData> form: FormApi<TParentData>
``` ```
- A reference to the form API instance. - A reference to the form API instance.
- ```tsx - ```tsx
name: DeepKeys<TFormData> name: DeepKeys<TParentData>
``` ```
- The field name. - The field name.
- ```tsx - ```tsx
@@ -143,14 +143,14 @@ A class representing the API for managing a form field.
``` ```
- The current field state. - The current field state.
- ```tsx - ```tsx
options: RequiredByKey<FieldOptions<TData, TFormData>, 'validateOn'> options: RequiredByKey<FieldOptions<TData, TParentData>, 'validateOn'>
``` ```
- The field options with the `validateOn` property set as required. - The field options with the `validateOn` property set as required.
#### Methods #### Methods
- ```tsx - ```tsx
constructor(opts: FieldApiOptions<TData, TFormData>) constructor(opts: FieldApiOptions<TData, TParentData>)
``` ```
- Initializes a new `FieldApi` instance. - Initializes a new `FieldApi` instance.
- ```tsx - ```tsx
@@ -162,7 +162,7 @@ A class representing the API for managing a form field.
``` ```
- Updates the field store with the latest form state. - Updates the field store with the latest form state.
- ```tsx - ```tsx
update(opts: FieldApiOptions<TData, TFormData>): void update(opts: FieldApiOptions<TData, TParentData>): void
``` ```
- Updates the field instance with new options. - Updates the field instance with new options.
- ```tsx - ```tsx
@@ -202,7 +202,7 @@ A class representing the API for managing a form field.
``` ```
- Swaps the values at the specified indices. - Swaps the values at the specified indices.
- ```tsx - ```tsx
getSubField<TName extends DeepKeys<TData>>(name: TName): FieldApi<DeepValue<TData, TName>, TFormData> getSubField<TName extends DeepKeys<TData>>(name: TName): FieldApi<DeepValue<TData, TName>, TParentData>
``` ```
- Gets a subfield instance. - Gets a subfield instance.
- ```tsx - ```tsx

View File

@@ -4,59 +4,67 @@ import { Store } from '@tanstack/store'
export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount' export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'
type ValidateFn<TData, TFormData> = ( type ValidateFn<TData, TParentData, TName extends DeepKeys<TParentData>> = (
value: TData, value: TData,
fieldApi: FieldApi<TData, TFormData>, fieldApi: FieldApi<TData, TParentData, TName>,
) => ValidationError ) => ValidationError
type ValidateAsyncFn<TData, TFormData> = ( type ValidateAsyncFn<
TData,
TParentData,
TName extends DeepKeys<TParentData>,
> = (
value: TData, value: TData,
fieldApi: FieldApi<TData, TFormData>, fieldApi: FieldApi<TData, TParentData, TName>,
) => ValidationError | Promise<ValidationError> ) => ValidationError | Promise<ValidationError>
export interface FieldOptions< export interface FieldOptions<
_TData, TData,
TFormData, TParentData,
/** /**
* This allows us to restrict the name to only be a valid field name while * This allows us to restrict the name to only be a valid field name while
* also assigning it to a generic * also assigning it to a generic
*/ */
TName = unknown extends TFormData ? string : DeepKeys<TFormData>, TName extends DeepKeys<TParentData>,
/** /**
* If TData is unknown, we can use the TName generic to determine the type * If TData is unknown, we can use the TName generic to determine the type
*/ */
TData = unknown extends _TData ? DeepValue<TFormData, TName> : _TData, TResolvedData = unknown extends TData ? DeepValue<TParentData, TName> : TData,
> { > {
name: TName name: DeepKeys<TParentData>
index?: TData extends any[] ? number : never index?: TResolvedData extends any[] ? number : never
defaultValue?: TData defaultValue?: TResolvedData
asyncDebounceMs?: number asyncDebounceMs?: number
asyncAlways?: boolean asyncAlways?: boolean
onMount?: (formApi: FieldApi<TData, TFormData>) => void onMount?: (formApi: FieldApi<TResolvedData, TParentData, TName>) => void
onChange?: ValidateFn<TData, TFormData> onChange?: ValidateFn<TResolvedData, TParentData, TName>
onChangeAsync?: ValidateAsyncFn<TData, TFormData> onChangeAsync?: ValidateAsyncFn<TResolvedData, TParentData, TName>
onChangeAsyncDebounceMs?: number onChangeAsyncDebounceMs?: number
onBlur?: ValidateFn<TData, TFormData> onBlur?: ValidateFn<TResolvedData, TParentData, TName>
onBlurAsync?: ValidateAsyncFn<TData, TFormData> onBlurAsync?: ValidateAsyncFn<TResolvedData, TParentData, TName>
onBlurAsyncDebounceMs?: number onBlurAsyncDebounceMs?: number
onSubmitAsync?: ValidateAsyncFn<TData, TFormData> onSubmitAsync?: ValidateAsyncFn<TResolvedData, TParentData, TName>
defaultMeta?: Partial<FieldMeta> defaultMeta?: Partial<FieldMeta>
} }
export interface FieldApiOptions< export interface FieldApiOptions<
_TData, TData,
TFormData, TParentData,
/** /**
* This allows us to restrict the name to only be a valid field name while * This allows us to restrict the name to only be a valid field name while
* also assigning it to a generic * also assigning it to a generic
*/ */
TName = unknown extends TFormData ? string : DeepKeys<TFormData>, TName extends DeepKeys<TParentData>,
/** /**
* If TData is unknown, we can use the TName generic to determine the type * If TData is unknown, we can use the TName generic to determine the type
*/ */
TData = unknown extends _TData ? DeepValue<TFormData, TName> : _TData, TResolvedData extends ResolveData<TData, TParentData, TName> = ResolveData<
> extends FieldOptions<_TData, TFormData, TName, TData> { TData,
form: FormApi<TFormData> TParentData,
TName
>,
> extends FieldOptions<TData, TParentData, TName, TResolvedData> {
form: FormApi<TParentData>
} }
export type FieldMeta = { export type FieldMeta = {
@@ -74,43 +82,35 @@ export type FieldState<TData> = {
meta: FieldMeta meta: FieldMeta
} }
type GetTData< export type ResolveData<TData, TParentData, TName> = unknown extends TData
TData, ? DeepValue<TParentData, TName>
TFormData, : TData
Opts extends FieldApiOptions<TData, TFormData>,
> = Opts extends FieldApiOptions< export type ResolveName<TParentData> = unknown extends TParentData
infer _TData, ? string
infer _TFormData, : DeepKeys<TParentData>
infer _TName,
infer RealTData
>
? RealTData
: never
export class FieldApi< export class FieldApi<
_TData, TData,
TFormData, TParentData,
Opts extends FieldApiOptions<_TData, TFormData> = FieldApiOptions< TName extends DeepKeys<TParentData>,
_TData, TResolvedData extends ResolveData<TData, TParentData, TName> = ResolveData<
TFormData TData,
>, TParentData,
TData extends GetTData<_TData, TFormData, Opts> = GetTData< TName
_TData,
TFormData,
Opts
>, >,
> { > {
uid: number uid: number
form: Opts['form'] form: FieldApiOptions<TData, TParentData, TName, TResolvedData>['form']
name!: DeepKeys<TFormData> name!: DeepKeys<TParentData>
options: Opts = {} as any options: FieldApiOptions<TData, TParentData, TName> = {} as any
store!: Store<FieldState<TData>> store!: Store<FieldState<TResolvedData>>
state!: FieldState<TData> state!: FieldState<TResolvedData>
prevState!: FieldState<TData> prevState!: FieldState<TResolvedData>
constructor( constructor(
opts: Opts & { opts: FieldApiOptions<TData, TParentData, TName, TResolvedData> & {
form: FormApi<TFormData> form: FormApi<TParentData>
}, },
) { ) {
this.form = opts.form this.form = opts.form
@@ -123,7 +123,7 @@ export class FieldApi<
this.name = opts.name as any this.name = opts.name as any
this.store = new Store<FieldState<TData>>( this.store = new Store<FieldState<TResolvedData>>(
{ {
value: this.getValue(), value: this.getValue(),
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -190,12 +190,12 @@ export class FieldApi<
} }
} }
update = (opts: FieldApiOptions<TData, TFormData>) => { update = (opts: FieldApiOptions<TResolvedData, TParentData, TName>) => {
// Default Value // Default Value
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (this.state.value === undefined) { if (this.state.value === undefined) {
const formDefault = const formDefault =
opts.form.options.defaultValues?.[opts.name as keyof TFormData] opts.form.options.defaultValues?.[opts.name as keyof TParentData]
if (opts.defaultValue !== undefined) { if (opts.defaultValue !== undefined) {
this.setValue(opts.defaultValue as never) this.setValue(opts.defaultValue as never)
@@ -212,12 +212,12 @@ export class FieldApi<
this.options = opts as never this.options = opts as never
} }
getValue = (): TData => { getValue = (): TResolvedData => {
return this.form.getFieldValue(this.name) return this.form.getFieldValue(this.name) as any
} }
setValue = ( setValue = (
updater: Updater<TData>, updater: Updater<TResolvedData>,
options?: { touch?: boolean; notify?: boolean }, options?: { touch?: boolean; notify?: boolean },
) => { ) => {
this.form.setFieldValue(this.name, updater as never, options) this.form.setFieldValue(this.name, updater as never, options)
@@ -241,12 +241,13 @@ export class FieldApi<
getInfo = () => this.form.getFieldInfo(this.name) getInfo = () => this.form.getFieldInfo(this.name)
pushValue = (value: TData extends any[] ? TData[number] : never) => pushValue = (
this.form.pushFieldValue(this.name, value as any) value: TResolvedData extends any[] ? TResolvedData[number] : never,
) => this.form.pushFieldValue(this.name, value as any)
insertValue = ( insertValue = (
index: number, index: number,
value: TData extends any[] ? TData[number] : never, value: TResolvedData extends any[] ? TResolvedData[number] : never,
) => this.form.insertFieldValue(this.name, index, value as any) ) => this.form.insertFieldValue(this.name, index, value as any)
removeValue = (index: number) => this.form.removeFieldValue(this.name, index) removeValue = (index: number) => this.form.removeFieldValue(this.name, index)
@@ -254,11 +255,21 @@ export class FieldApi<
swapValues = (aIndex: number, bIndex: number) => swapValues = (aIndex: number, bIndex: number) =>
this.form.swapFieldValues(this.name, aIndex, bIndex) this.form.swapFieldValues(this.name, aIndex, bIndex)
getSubField = <TName extends DeepKeys<TData>>(name: TName) => getSubField = <
new FieldApi<DeepValue<TData, TName>, TFormData>({ TSubData,
TSubName extends DeepKeys<TResolvedData>,
TSubResolvedData extends ResolveData<
DeepValue<TResolvedData, TSubName>,
TResolvedData,
TSubName
>,
>(
name: TSubName,
): FieldApi<TSubData, TResolvedData, TSubName, TSubResolvedData> =>
new FieldApi({
name: `${this.name}.${name}` as never, name: `${this.name}.${name}` as never,
form: this.form, form: this.form,
}) }) as any
validateSync = (value = this.state.value, cause: ValidationCause) => { validateSync = (value = this.state.value, cause: ValidationCause) => {
const { onChange, onBlur } = this.options const { onChange, onBlur } = this.options
@@ -387,7 +398,7 @@ export class FieldApi<
validate = ( validate = (
cause: ValidationCause, cause: ValidationCause,
value?: TData, value?: TResolvedData,
): ValidationError[] | Promise<ValidationError[]> => { ): ValidationError[] | Promise<ValidationError[]> => {
// If the field is pristine and validatePristine is false, do not validate // If the field is pristine and validatePristine is false, do not validate
if (!this.state.meta.isTouched) return [] if (!this.state.meta.isTouched) return []
@@ -405,7 +416,7 @@ export class FieldApi<
return this.validateAsync(value, cause) return this.validateAsync(value, cause)
} }
handleChange = (updater: Updater<TData>) => { handleChange = (updater: Updater<TResolvedData>) => {
this.setValue(updater, { touch: true }) this.setValue(updater, { touch: true })
} }

View File

@@ -31,7 +31,7 @@ export type FormOptions<TData> = {
} }
export type FieldInfo<TFormData> = { export type FieldInfo<TFormData> = {
instances: Record<string, FieldApi<any, TFormData>> instances: Record<string, FieldApi<any, TFormData, any>>
} & ValidationMeta } & ValidationMeta
export type ValidationMeta = { export type ValidationMeta = {
@@ -106,7 +106,7 @@ export class FormApi<TFormData> {
constructor(opts?: FormOptions<TFormData>) { constructor(opts?: FormOptions<TFormData>) {
this.store = new Store<FormState<TFormData>>( this.store = new Store<FormState<TFormData>>(
getDefaultFormState({ getDefaultFormState({
...opts?.defaultState, ...(opts?.defaultState as any),
values: opts?.defaultValues ?? opts?.defaultState?.values, values: opts?.defaultValues ?? opts?.defaultState?.values,
isFormValid: true, isFormValid: true,
}), }),
@@ -174,7 +174,7 @@ export class FormApi<TFormData> {
getDefaultFormState( getDefaultFormState(
Object.assign( Object.assign(
{}, {},
this.state, this.state as any,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
shouldUpdateState ? options.defaultState : {}, shouldUpdateState ? options.defaultState : {},
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -194,7 +194,7 @@ export class FormApi<TFormData> {
reset = () => reset = () =>
this.store.setState(() => this.store.setState(() =>
getDefaultFormState({ getDefaultFormState({
...this.options.defaultState, ...(this.options.defaultState as any),
values: this.options.defaultValues ?? this.options.defaultState?.values, values: this.options.defaultValues ?? this.options.defaultState?.values,
}), }),
) )
@@ -288,7 +288,9 @@ export class FormApi<TFormData> {
return this.state.fieldMeta[field] return this.state.fieldMeta[field]
} }
getFieldInfo = <TField extends DeepKeys<TFormData>>(field: TField) => { getFieldInfo = <TField extends DeepKeys<TFormData>>(
field: TField,
): FieldInfo<TFormData> => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return (this.fieldInfo[field] ||= { return (this.fieldInfo[field] ||= {
instances: {}, instances: {},

View File

@@ -133,16 +133,16 @@ type AllowedIndexes<
? AllowedIndexes<Tail, Keys | Tail['length']> ? AllowedIndexes<Tail, Keys | Tail['length']>
: Keys : Keys
export type DeepKeys<T, TDepth extends any[] = []> = TDepth['length'] extends 10 export type DeepKeys<T, TDepth extends any[] = []> = TDepth['length'] extends 5
? never ? never
: unknown extends T : unknown extends T
? keyof T ? string
: object extends T : object extends T
? string ? string
: T extends readonly any[] & IsTuple<T> : T extends readonly any[] & IsTuple<T>
? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>, TDepth> ? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>, TDepth>
: T extends any[] : T extends any[]
? DeepKeys<T[number]> ? DeepKeys<T[number], [...TDepth, any]>
: T extends Date : T extends Date
? never ? never
: T extends object : T extends object

View File

@@ -6,7 +6,7 @@ import { useForm } from './useForm'
export type FormFactory<TFormData> = { export type FormFactory<TFormData> = {
useForm: (opts?: FormOptions<TFormData>) => FormApi<TFormData> useForm: (opts?: FormOptions<TFormData>) => FormApi<TFormData>
useField: UseField<TFormData> useField: UseField<TFormData>
Field: FieldComponent<TFormData, TFormData> Field: FieldComponent<TFormData>
} }
export function createFormFactory<TFormData>( export function createFormFactory<TFormData>(

View File

@@ -2,8 +2,8 @@ import type { FieldOptions, DeepKeys } from '@tanstack/form-core'
export type UseFieldOptions< export type UseFieldOptions<
TData, TData,
TFormData, TParentData,
TName = unknown extends TFormData ? string : DeepKeys<TFormData>, TName extends DeepKeys<TParentData>,
> = FieldOptions<TData, TFormData, TName> & { > = FieldOptions<TData, TParentData, TName> & {
mode?: 'value' | 'array' mode?: 'value' | 'array'
} }

View File

@@ -3,44 +3,51 @@ import { useStore } from '@tanstack/react-store'
import type { import type {
DeepKeys, DeepKeys,
DeepValue, DeepValue,
FieldApiOptions,
Narrow, Narrow,
ResolveData,
} from '@tanstack/form-core' } from '@tanstack/form-core'
import { FieldApi, type FormApi, functionalUpdate } from '@tanstack/form-core' import { FieldApi, functionalUpdate } from '@tanstack/form-core'
import { useFormContext, formContext } from './formContext' import { useFormContext, formContext } from './formContext'
import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect' import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect'
import type { UseFieldOptions } from './types' import type { UseFieldOptions } from './types'
declare module '@tanstack/form-core' { declare module '@tanstack/form-core' {
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
interface FieldApi<_TData, TFormData, Opts, TData> { interface FieldApi<
Field: FieldComponent<TData, TFormData> TData,
TParentData,
TName extends DeepKeys<TParentData>,
TResolvedData extends ResolveData<TData, TParentData, TName> = ResolveData<
TData,
TParentData,
TName
>,
> {
Field: FieldComponent<TData>
} }
} }
export type UseField<TFormData> = <TField extends DeepKeys<TFormData>>( export type UseField<TParentData> = <TName extends DeepKeys<TParentData>>(
opts?: { name: Narrow<TField> } & UseFieldOptions< opts?: { name: Narrow<TName> } & UseFieldOptions<
DeepValue<TFormData, TField>, DeepValue<TParentData, TName>,
TFormData TParentData,
TName
>, >,
) => FieldApi<DeepValue<TFormData, TField>, TFormData> ) => FieldApi<DeepValue<TParentData, TName>, TParentData, TName>
export function useField< export function useField<
TData, TData,
TFormData, TParentData,
TName extends unknown extends TFormData TName extends DeepKeys<TParentData>,
? string
: DeepKeys<TFormData> = unknown extends TFormData
? string
: DeepKeys<TFormData>,
>( >(
opts: UseFieldOptions<TData, TFormData, TName>, opts: UseFieldOptions<TData, TParentData, TName>,
): FieldApi< ): FieldApi<
TData, TData,
TFormData, TParentData,
Omit<typeof opts, 'onMount'> & { TName
form: FormApi<TFormData> // Omit<typeof opts, 'onMount'> & {
} // form: FormApi<TParentData>
// }
> { > {
// Get the form API either manually or from context // Get the form API either manually or from context
const { formApi, parentFieldName } = useFormContext() const { formApi, parentFieldName } = useFormContext()
@@ -88,17 +95,13 @@ export function useField<
} }
type FieldComponentProps< type FieldComponentProps<
TData,
TParentData, TParentData,
TFormData, TName extends DeepKeys<TParentData>,
TField, TResolvedData extends ResolveData<TData, TParentData, TName>,
TName extends unknown extends TFormData ? string : DeepKeys<TFormData>,
> = { > = {
children: ( children: (
fieldApi: FieldApi< fieldApi: FieldApi<TData, TParentData, TName, TResolvedData>,
TField,
TFormData,
FieldApiOptions<TField, TFormData, TName>
>,
) => any ) => any
} & (TParentData extends any[] } & (TParentData extends any[]
? { ? {
@@ -109,24 +112,27 @@ type FieldComponentProps<
name: TName name: TName
index?: never index?: never
}) & }) &
Omit<UseFieldOptions<TField, TFormData, TName>, 'name' | 'index'> Omit<UseFieldOptions<TData, TParentData, TName>, 'name' | 'index'>
export type FieldComponent<TParentData, TFormData> = < export type FieldComponent<TParentData> = <
// Type of the field TData,
TField, TName extends DeepKeys<TParentData>,
// Name of the field TResolvedData extends ResolveData<TData, TParentData, TName> = ResolveData<
TName extends unknown extends TFormData ? string : DeepKeys<TFormData>, TData,
TParentData,
TName
>,
>({ >({
children, children,
...fieldOptions ...fieldOptions
}: FieldComponentProps<TParentData, TFormData, TField, TName>) => any }: FieldComponentProps<TData, TParentData, TName, TResolvedData>) => any
export function Field<TData, TFormData>({ export function Field<TData, TParentData, TName extends DeepKeys<TParentData>>({
children, children,
...fieldOptions ...fieldOptions
}: { }: {
children: (fieldApi: FieldApi<TData, TFormData>) => any children: (fieldApi: FieldApi<TData, TParentData, TName>) => any
} & UseFieldOptions<TData, TFormData>) { } & UseFieldOptions<TData, TParentData, TName>) {
const fieldApi = useField(fieldOptions as any) const fieldApi = useField(fieldOptions as any)
return ( return (

View File

@@ -11,7 +11,7 @@ declare module '@tanstack/form-core' {
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
interface FormApi<TFormData> { interface FormApi<TFormData> {
Provider: (props: { children: any }) => any Provider: (props: { children: any }) => any
Field: FieldComponent<TFormData, TFormData> Field: FieldComponent<TFormData>
useField: UseField<TFormData> useField: UseField<TFormData>
useStore: <TSelected = NoInfer<FormState<TFormData>>>( useStore: <TSelected = NoInfer<FormState<TFormData>>>(
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected, selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,

View File

@@ -6,7 +6,7 @@ import { useForm } from './useForm'
export type FormFactory<TFormData> = { export type FormFactory<TFormData> = {
useForm: (opts?: FormOptions<TFormData>) => FormApi<TFormData> useForm: (opts?: FormOptions<TFormData>) => FormApi<TFormData>
useField: UseField<TFormData> useField: UseField<TFormData>
Field: FieldComponent<TFormData, TFormData> Field: FieldComponent<TFormData>
} }
export function createFormFactory<TFormData>( export function createFormFactory<TFormData>(

View File

@@ -2,8 +2,8 @@ import type { FieldOptions, DeepKeys } from '@tanstack/form-core'
export type UseFieldOptions< export type UseFieldOptions<
TData, TData,
TFormData, TParentData,
TName = unknown extends TFormData ? string : DeepKeys<TFormData>, TName extends DeepKeys<TParentData>,
> = FieldOptions<TData, TFormData, TName> & { > = FieldOptions<TData, TParentData, TName> & {
mode?: 'value' | 'array' mode?: 'value' | 'array'
} }

View File

@@ -1,9 +1,10 @@
import { import { FieldApi } from '@tanstack/form-core'
FieldApi, import type {
type FieldApiOptions, DeepKeys,
type FormApi, DeepValue,
Narrow,
ResolveData,
} from '@tanstack/form-core' } from '@tanstack/form-core'
import type { DeepKeys, DeepValue, Narrow } from '@tanstack/form-core'
import { useStore } from '@tanstack/vue-store' import { useStore } from '@tanstack/vue-store'
import { defineComponent, onMounted, onUnmounted, watch } from 'vue-demi' import { defineComponent, onMounted, onUnmounted, watch } from 'vue-demi'
import type { SlotsType, SetupContext, Ref } from 'vue-demi' import type { SlotsType, SetupContext, Ref } from 'vue-demi'
@@ -12,44 +13,52 @@ import type { UseFieldOptions } from './types'
declare module '@tanstack/form-core' { declare module '@tanstack/form-core' {
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
interface FieldApi<_TData, TFormData, Opts, TData> { interface FieldApi<
Field: FieldComponent<TFormData, TData> TData,
TParentData,
TName extends DeepKeys<TParentData>,
TResolvedData extends ResolveData<TData, TParentData, TName> = ResolveData<
TData,
TParentData,
TName
>,
> {
Field: FieldComponent<TResolvedData>
} }
} }
export type UseField<TFormData> = <TField extends DeepKeys<TFormData>>( export type UseField<TParentData> = <TName extends DeepKeys<TParentData>>(
opts?: { name: Narrow<TField> } & UseFieldOptions< opts?: { name: Narrow<TName> } & UseFieldOptions<
DeepValue<TFormData, TField>, DeepValue<TParentData, TName>,
TFormData TParentData,
TName
>, >,
) => FieldApi<DeepValue<TFormData, TField>, TFormData> ) => FieldApi<DeepValue<TParentData, TName>, TParentData, TName>
export function useField< export function useField<
TData, TData,
TFormData, TParentData,
TName extends unknown extends TFormData TName extends DeepKeys<TParentData>,
? string
: DeepKeys<TFormData> = unknown extends TFormData
? string
: DeepKeys<TFormData>,
>( >(
opts: UseFieldOptions<TData, TFormData, TName>, opts: UseFieldOptions<TData, TParentData, TName>,
): { ): {
api: FieldApi< api: FieldApi<
TData, TData,
TFormData, TParentData,
Omit<typeof opts, 'onMount'> & { TName
form: FormApi<TFormData> // Omit<typeof opts, 'onMount'> & {
} // form: FormApi<TParentData>
// }
> >
state: Readonly< state: Readonly<
Ref< Ref<
FieldApi< FieldApi<
TData, TData,
TFormData, TParentData,
Omit<typeof opts, 'onMount'> & { TName
form: FormApi<TFormData> // Omit<typeof opts, 'onMount'> & {
} // form: FormApi<TParentData>
// }
>['state'] >['state']
> >
> >
@@ -91,17 +100,16 @@ export function useField<
return { api: fieldApi, state: fieldState } as never return { api: fieldApi, state: fieldState } as never
} }
export type FieldValue<TFormData, TField> = TFormData extends any[] export type FieldValue<TParentData, TName> = TParentData extends any[]
? unknown extends TField ? unknown extends TName
? TFormData[number] ? TParentData[number]
: DeepValue<TFormData[number], TField> : DeepValue<TParentData[number], TName>
: DeepValue<TFormData, TField> : DeepValue<TParentData, TName>
type FieldComponentProps< type FieldComponentProps<
TData,
TParentData, TParentData,
TFormData, TName extends DeepKeys<TParentData>,
TField,
TName extends unknown extends TFormData ? string : DeepKeys<TFormData>,
> = (TParentData extends any[] > = (TParentData extends any[]
? { ? {
name?: TName name?: TName
@@ -111,40 +119,35 @@ type FieldComponentProps<
name: TName name: TName
index?: never index?: never
}) & }) &
Omit<UseFieldOptions<TField, TFormData, TName>, 'name' | 'index'> Omit<UseFieldOptions<TData, TParentData, TName>, 'name' | 'index'>
export type FieldComponent<TParentData, TFormData> = < export type FieldComponent<TParentData> = <
// Type of the field TData,
TField, TName extends DeepKeys<TParentData>,
// Name of the field TResolvedData extends ResolveData<TData, TParentData, TName> = ResolveData<
TName extends unknown extends TFormData ? string : DeepKeys<TFormData>, TData,
TParentData,
TName
>,
>( >(
fieldOptions: FieldComponentProps<TParentData, TFormData, TField, TName>, fieldOptions: FieldComponentProps<TData, TParentData, TName>,
context: SetupContext< context: SetupContext<
{}, {},
SlotsType<{ SlotsType<{
default: { default: {
field: FieldApi< field: FieldApi<TData, TParentData, TName, TResolvedData>
TField, state: FieldApi<TData, TParentData, TName, TResolvedData>['state']
TFormData,
FieldApiOptions<TField, TFormData, TName>
>
state: FieldApi<
TField,
TFormData,
FieldApiOptions<TField, TFormData, TName>
>['state']
} }
}> }>
>, >,
) => any ) => any
export const Field = defineComponent( export const Field = defineComponent(
<TData, TFormData>( <TData, TParentData, TName extends DeepKeys<TParentData>>(
fieldOptions: UseFieldOptions<TData, TFormData>, fieldOptions: UseFieldOptions<TData, TParentData, TName>,
context: SetupContext, context: SetupContext,
) => { ) => {
const fieldApi = useField({ ...fieldOptions, ...context.attrs }) const fieldApi = useField({ ...fieldOptions, ...context.attrs } as any)
provideFormContext({ provideFormContext({
formApi: fieldApi.api.form, formApi: fieldApi.api.form,

View File

@@ -14,7 +14,7 @@ declare module '@tanstack/form-core' {
interface FormApi<TFormData> { interface FormApi<TFormData> {
Provider: (props: Record<string, any> & {}) => any Provider: (props: Record<string, any> & {}) => any
provideFormContext: () => void provideFormContext: () => void
Field: FieldComponent<TFormData, TFormData> Field: FieldComponent<TFormData>
useField: UseField<TFormData> useField: UseField<TFormData>
useStore: <TSelected = NoInfer<FormState<TFormData>>>( useStore: <TSelected = NoInfer<FormState<TFormData>>>(
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected, selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,