mirror of
https://github.com/LukeHagar/form.git
synced 2025-12-09 20:37:47 +00:00
fix: array-field mode and utilities
This commit is contained in:
@@ -138,7 +138,7 @@ A class representing the Form API. It handles the logic and interactions with th
|
|||||||
```
|
```
|
||||||
- Inserts a value into an array field at the specified index.
|
- Inserts a value into an array field at the specified index.
|
||||||
- ```tsx
|
- ```tsx
|
||||||
spliceFieldValue<TField extends DeepKeys<TFormData>>(field: TField, index: number, opts?: { touch?: boolean })
|
removeFieldValue<TField extends DeepKeys<TFormData>>(field: TField, index: number, opts?: { touch?: boolean })
|
||||||
```
|
```
|
||||||
- Removes a value from an array field at the specified index.
|
- Removes a value from an array field at the specified index.
|
||||||
- ```tsx
|
- ```tsx
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type Person = {
|
|||||||
type Hobby = {
|
type Hobby = {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
yearsOfExperience: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formFactory = createFormFactory<Person>({
|
const formFactory = createFormFactory<Person>({
|
||||||
@@ -61,8 +62,7 @@ export default function App() {
|
|||||||
children={(field) => (
|
children={(field) => (
|
||||||
// Avoid hasty abstractions. Render props are great!
|
// Avoid hasty abstractions. Render props are great!
|
||||||
<>
|
<>
|
||||||
<label htmlFor={field.name}>First Name:</label>
|
<input {...field.getInputProps()} />
|
||||||
<input name={field.name} {...field.getInputProps()} />
|
|
||||||
<FieldInfo field={field} />
|
<FieldInfo field={field} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -73,8 +73,7 @@ export default function App() {
|
|||||||
name="lastName"
|
name="lastName"
|
||||||
children={(field) => (
|
children={(field) => (
|
||||||
<>
|
<>
|
||||||
<label htmlFor={field.name}>Last Name:</label>
|
<input {...field.getInputProps()} />
|
||||||
<input name={field.name} {...field.getInputProps()} />
|
|
||||||
<FieldInfo field={field} />
|
<FieldInfo field={field} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -83,12 +82,84 @@ export default function App() {
|
|||||||
<div>
|
<div>
|
||||||
<form.Field
|
<form.Field
|
||||||
name="hobbies"
|
name="hobbies"
|
||||||
children={(field) => (
|
mode="array"
|
||||||
<>
|
children={(hobbiesField) => (
|
||||||
<label htmlFor={field.name}>Last Name:</label>
|
<div>
|
||||||
<input name={field.name} {...field.getInputProps()} />
|
Hobbies
|
||||||
<FieldInfo field={field} />
|
<div
|
||||||
</>
|
style={{
|
||||||
|
paddingLeft: "1rem",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!hobbiesField.state.value.length
|
||||||
|
? "No hobbies found."
|
||||||
|
: hobbiesField.state.value.map((value, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
borderLeft: "2px solid gray",
|
||||||
|
paddingLeft: ".5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<hobbiesField.Field
|
||||||
|
index={i}
|
||||||
|
name="name"
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={field.name}>Name:</label>
|
||||||
|
<input
|
||||||
|
name={field.name}
|
||||||
|
{...field.getInputProps()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => hobbiesField.removeValue(i)}
|
||||||
|
>
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
<FieldInfo field={field} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<hobbiesField.Field
|
||||||
|
index={i}
|
||||||
|
name="description"
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={field.name}>
|
||||||
|
Description:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name={field.name}
|
||||||
|
{...field.getInputProps()}
|
||||||
|
/>
|
||||||
|
<FieldInfo field={field} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
hobbiesField.pushValue({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
yearsOfExperience: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add hobby
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type ValidationCause = 'change' | 'blur' | 'submit'
|
|||||||
|
|
||||||
export interface FieldOptions<TData, TFormData> {
|
export interface FieldOptions<TData, TFormData> {
|
||||||
name: unknown extends TFormData ? string : DeepKeys<TFormData>
|
name: unknown extends TFormData ? string : DeepKeys<TFormData>
|
||||||
|
index?: TData extends any[] ? number : never
|
||||||
defaultValue?: TData
|
defaultValue?: TData
|
||||||
validate?: (
|
validate?: (
|
||||||
value: TData,
|
value: TData,
|
||||||
@@ -84,12 +85,12 @@ export class FieldApi<TData, TFormData> {
|
|||||||
this.form = opts.form
|
this.form = opts.form
|
||||||
this.uid = uid++
|
this.uid = uid++
|
||||||
// Support field prefixing from FieldScope
|
// Support field prefixing from FieldScope
|
||||||
let fieldPrefix = ''
|
// let fieldPrefix = ''
|
||||||
if (this.form.fieldName) {
|
// if (this.form.fieldName) {
|
||||||
fieldPrefix = `${this.form.fieldName}.`
|
// fieldPrefix = `${this.form.fieldName}.`
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.name = (fieldPrefix + opts.name) as any
|
this.name = opts.name as any
|
||||||
|
|
||||||
this.store = new Store<FieldState<TData>>(
|
this.store = new Store<FieldState<TData>>(
|
||||||
{
|
{
|
||||||
@@ -113,6 +114,7 @@ export class FieldApi<TData, TFormData> {
|
|||||||
if (next.value !== prevState.value) {
|
if (next.value !== prevState.value) {
|
||||||
this.validate('change', next.value)
|
this.validate('change', next.value)
|
||||||
}
|
}
|
||||||
|
console.log(this)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -178,7 +180,9 @@ export class FieldApi<TData, TFormData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getValue = (): TData => this.form.getFieldValue(this.name)
|
getValue = (): TData => {
|
||||||
|
return this.form.getFieldValue(this.name)
|
||||||
|
}
|
||||||
setValue = (
|
setValue = (
|
||||||
updater: Updater<TData>,
|
updater: Updater<TData>,
|
||||||
options?: { touch?: boolean; notify?: boolean },
|
options?: { touch?: boolean; notify?: boolean },
|
||||||
@@ -190,11 +194,11 @@ export class FieldApi<TData, TFormData> {
|
|||||||
|
|
||||||
getInfo = () => this.form.getFieldInfo(this.name)
|
getInfo = () => this.form.getFieldInfo(this.name)
|
||||||
|
|
||||||
pushValue = (value: TData) =>
|
pushValue = (value: TData extends any[] ? TData[number] : never) =>
|
||||||
this.form.pushFieldValue(this.name, value as any)
|
this.form.pushFieldValue(this.name, value as any)
|
||||||
insertValue = (index: number, value: TData) =>
|
insertValue = (index: number, value: TData) =>
|
||||||
this.form.insertFieldValue(this.name, index, value as any)
|
this.form.insertFieldValue(this.name, index, value as any)
|
||||||
removeValue = (index: number) => this.form.spliceFieldValue(this.name, index)
|
removeValue = (index: number) => this.form.removeFieldValue(this.name, index)
|
||||||
swapValues = (aIndex: number, bIndex: number) =>
|
swapValues = (aIndex: number, bIndex: number) =>
|
||||||
this.form.swapFieldValues(this.name, aIndex, bIndex)
|
this.form.swapFieldValues(this.name, aIndex, bIndex)
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export class FormApi<TFormData> {
|
|||||||
// Write it back to the store
|
// Write it back to the store
|
||||||
this.store.state = next
|
this.store.state = next
|
||||||
this.state = next
|
this.state = next
|
||||||
|
console.log(this.state)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -402,7 +403,7 @@ export class FormApi<TFormData> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
spliceFieldValue = <TField extends DeepKeys<TFormData>>(
|
removeFieldValue = <TField extends DeepKeys<TFormData>>(
|
||||||
field: TField,
|
field: TField,
|
||||||
index: number,
|
index: number,
|
||||||
opts?: { touch?: boolean },
|
opts?: { touch?: boolean },
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export type DeepKeys<T> = unknown extends T
|
|||||||
: T extends readonly any[] & IsTuple<T>
|
: T extends readonly any[] & IsTuple<T>
|
||||||
? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>>
|
? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>>
|
||||||
: T extends any[]
|
: T extends any[]
|
||||||
? never & 'Dynamic length array indexing is not supported'
|
? DeepKeys<T[number]>
|
||||||
: T extends Date
|
: T extends Date
|
||||||
? never
|
? never
|
||||||
: T extends object
|
: T extends object
|
||||||
@@ -146,3 +146,18 @@ export type DeepValue<T, TProp> = T extends Record<string | number, any>
|
|||||||
? DeepValue<T[TBranch], TDeepProp>
|
? DeepValue<T[TBranch], TDeepProp>
|
||||||
: T[TProp & string]
|
: T[TProp & string]
|
||||||
: never
|
: never
|
||||||
|
|
||||||
|
type Narrowable = string | number | bigint | boolean
|
||||||
|
|
||||||
|
type NarrowRaw<A> =
|
||||||
|
| (A extends [] ? [] : never)
|
||||||
|
| (A extends Narrowable ? A : never)
|
||||||
|
| {
|
||||||
|
[K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw<A[K]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Narrow<A extends any> = Try<A, [], NarrowRaw<A>>
|
||||||
|
|
||||||
|
type Try<A1 extends any, A2 extends any, Catch = never> = A1 extends A2
|
||||||
|
? A1
|
||||||
|
: Catch
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import * as React from 'react'
|
|
||||||
import {
|
|
||||||
functionalUpdate,
|
|
||||||
type DeepKeys,
|
|
||||||
type DeepValue,
|
|
||||||
type FieldApi,
|
|
||||||
type FieldOptions,
|
|
||||||
} from '@tanstack/form-core'
|
|
||||||
import { useField } from './useField'
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
export type FieldComponent<TFormData> = <TField extends DeepKeys<TFormData>>({
|
|
||||||
children,
|
|
||||||
...fieldOptions
|
|
||||||
}: {
|
|
||||||
children: (fieldApi: FieldApi<DeepValue<TFormData, TField>, TFormData>) => any
|
|
||||||
name: TField
|
|
||||||
} & Omit<FieldOptions<DeepValue<TFormData, TField>, TFormData>, 'name'>) => any
|
|
||||||
|
|
||||||
export function createFieldComponent<TFormData>() {
|
|
||||||
const ConnectedField: FieldComponent<TFormData> = (props) => (
|
|
||||||
<Field {...(props as any)} />
|
|
||||||
)
|
|
||||||
return ConnectedField
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Field<TData, TFormData>({
|
|
||||||
children,
|
|
||||||
...fieldOptions
|
|
||||||
}: {
|
|
||||||
children: (fieldApi: FieldApi<TData, TFormData>) => any
|
|
||||||
} & FieldOptions<TData, TFormData>) {
|
|
||||||
const fieldApi = useField(fieldOptions as any)
|
|
||||||
return functionalUpdate(children, fieldApi as any)
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import type { FormApi, FormOptions } from '@tanstack/form-core'
|
import type { FormApi, FormOptions } from '@tanstack/form-core'
|
||||||
import { createUseField, type UseField } from './useField'
|
import { type UseField, type FieldComponent, Field, useField } from './useField'
|
||||||
import { useForm } from './useForm'
|
import { useForm } from './useForm'
|
||||||
import { createFieldComponent, type FieldComponent } from './Field'
|
|
||||||
|
|
||||||
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>
|
Field: FieldComponent<TFormData, TFormData>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFormFactory<TFormData>(
|
export function createFormFactory<TFormData>(
|
||||||
@@ -16,7 +15,7 @@ export function createFormFactory<TFormData>(
|
|||||||
useForm: (opts) => {
|
useForm: (opts) => {
|
||||||
return useForm<TFormData>({ ...defaultOpts, ...opts } as any) as any
|
return useForm<TFormData>({ ...defaultOpts, ...opts } as any) as any
|
||||||
},
|
},
|
||||||
useField: createUseField<TFormData>(),
|
useField: useField as any,
|
||||||
Field: createFieldComponent<TFormData>(),
|
Field: Field as any,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import type { FormApi } from '@tanstack/form-core'
|
import type { FormApi } from '@tanstack/form-core'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
export const formContext = React.createContext<FormApi<any> | null>(null)
|
export const formContext = React.createContext<{
|
||||||
|
formApi: FormApi<any>
|
||||||
|
parentFieldName?: string
|
||||||
|
} | null>(null!)
|
||||||
|
|
||||||
export function useFormContext() {
|
export function useFormContext() {
|
||||||
const formApi = React.useContext(formContext)
|
const formApi = React.useContext(formContext)
|
||||||
|
|||||||
@@ -25,11 +25,8 @@ export { FormApi, FieldApi, functionalUpdate } from '@tanstack/form-core'
|
|||||||
export type { FormComponent, FormProps } from './useForm'
|
export type { FormComponent, FormProps } from './useForm'
|
||||||
export { useForm } from './useForm'
|
export { useForm } from './useForm'
|
||||||
|
|
||||||
export type { FieldComponent } from './Field'
|
export type { UseField, FieldComponent } from './useField'
|
||||||
export { Field } from './Field'
|
export { useField, Field } from './useField'
|
||||||
|
|
||||||
export type { UseField } from './useField'
|
|
||||||
export { useField } from './useField'
|
|
||||||
|
|
||||||
export type { FormFactory } from './createFormFactory'
|
export type { FormFactory } from './createFormFactory'
|
||||||
export { createFormFactory } from './createFormFactory'
|
export { createFormFactory } from './createFormFactory'
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import * as React from 'react'
|
|
||||||
//
|
|
||||||
import { useStore } from '@tanstack/react-store'
|
|
||||||
import type { DeepKeys, DeepValue, FieldOptions } from '@tanstack/form-core'
|
|
||||||
import { FieldApi } from '@tanstack/form-core'
|
|
||||||
import { useFormContext } from './formContext'
|
|
||||||
import type { FormFactory } from './createFormFactory'
|
|
||||||
|
|
||||||
declare module '@tanstack/form-core' {
|
|
||||||
// eslint-disable-next-line no-shadow
|
|
||||||
interface FieldOptions<TData, TFormData> {
|
|
||||||
formFactory?: FormFactory<TFormData>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UseField<TFormData> = <TField extends DeepKeys<TFormData>>(
|
|
||||||
opts?: { name: TField } & FieldOptions<
|
|
||||||
DeepValue<TFormData, TField>,
|
|
||||||
TFormData
|
|
||||||
>,
|
|
||||||
) => FieldApi<DeepValue<TFormData, TField>, TFormData>
|
|
||||||
|
|
||||||
export function createUseField<TFormData>(): UseField<TFormData> {
|
|
||||||
return (opts) => {
|
|
||||||
return useField(opts as any)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useField<TData, TFormData>(
|
|
||||||
opts: FieldOptions<TData, TFormData> & {
|
|
||||||
// selector: (state: FieldApi<TData, TFormData>) => TSelected
|
|
||||||
},
|
|
||||||
): FieldApi<TData, TFormData> {
|
|
||||||
// Get the form API either manually or from context
|
|
||||||
const formApi = useFormContext()
|
|
||||||
|
|
||||||
const [fieldApi] = React.useState<FieldApi<TData, TFormData>>(
|
|
||||||
() => new FieldApi({ ...opts, form: formApi }),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Keep options up to date as they are rendered
|
|
||||||
fieldApi.update({ ...opts, form: formApi })
|
|
||||||
|
|
||||||
useStore(fieldApi.store)
|
|
||||||
|
|
||||||
// Instantiates field meta and removes it when unrendered
|
|
||||||
React.useEffect(() => fieldApi.mount(), [fieldApi])
|
|
||||||
|
|
||||||
return fieldApi
|
|
||||||
}
|
|
||||||
138
packages/react-form/src/useField.tsx
Normal file
138
packages/react-form/src/useField.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
//
|
||||||
|
import { useStore } from '@tanstack/react-store'
|
||||||
|
import type {
|
||||||
|
DeepKeys,
|
||||||
|
DeepValue,
|
||||||
|
FieldOptions,
|
||||||
|
Narrow,
|
||||||
|
} from '@tanstack/form-core'
|
||||||
|
import { FieldApi, functionalUpdate } from '@tanstack/form-core'
|
||||||
|
import { useFormContext, formContext } from './formContext'
|
||||||
|
|
||||||
|
declare module '@tanstack/form-core' {
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
interface FieldApi<TData, TFormData> {
|
||||||
|
Field: FieldComponent<TData, TFormData>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseFieldOptions<TData, TFormData> = FieldOptions<
|
||||||
|
TData,
|
||||||
|
TFormData
|
||||||
|
> & {
|
||||||
|
mode?: 'value' | 'array'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseField<TFormData> = <TField extends DeepKeys<TFormData>>(
|
||||||
|
opts?: { name: Narrow<TField> } & UseFieldOptions<
|
||||||
|
DeepValue<TFormData, TField>,
|
||||||
|
TFormData
|
||||||
|
>,
|
||||||
|
) => FieldApi<DeepValue<TFormData, TField>, TFormData>
|
||||||
|
|
||||||
|
export function useField<TData, TFormData>(
|
||||||
|
opts: UseFieldOptions<TData, TFormData>,
|
||||||
|
): FieldApi<TData, TFormData> {
|
||||||
|
// Get the form API either manually or from context
|
||||||
|
const { formApi, parentFieldName } = useFormContext()
|
||||||
|
|
||||||
|
const [fieldApi] = React.useState<FieldApi<TData, TFormData>>(() => {
|
||||||
|
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, name: name as any })
|
||||||
|
|
||||||
|
api.Field = Field as any
|
||||||
|
|
||||||
|
return api
|
||||||
|
})
|
||||||
|
|
||||||
|
// Keep options up to date as they are rendered
|
||||||
|
fieldApi.update({ ...opts, form: formApi })
|
||||||
|
|
||||||
|
useStore(
|
||||||
|
fieldApi.store,
|
||||||
|
opts.mode === 'array'
|
||||||
|
? (state: any) => {
|
||||||
|
return [state.meta, Object.keys(state.value || []).length]
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Instantiates field meta and removes it when unrendered
|
||||||
|
React.useEffect(() => fieldApi.mount(), [fieldApi])
|
||||||
|
|
||||||
|
return fieldApi
|
||||||
|
}
|
||||||
|
|
||||||
|
// export type FieldValue<TFormData, TField> = TFormData extends any[]
|
||||||
|
// ? TField extends `[${infer TIndex extends number | 'i'}].${infer TRest}`
|
||||||
|
// ? DeepValue<TFormData[TIndex extends 'i' ? number : TIndex], TRest>
|
||||||
|
// : TField extends `[${infer TIndex extends number | 'i'}]`
|
||||||
|
// ? TFormData[TIndex extends 'i' ? number : TIndex]
|
||||||
|
// : never
|
||||||
|
// : TField extends `${infer TPrefix}[${infer TIndex extends
|
||||||
|
// | number
|
||||||
|
// | 'i'}].${infer TRest}`
|
||||||
|
// ? DeepValue<
|
||||||
|
// DeepValue<TFormData, TPrefix>[TIndex extends 'i' ? number : TIndex],
|
||||||
|
// TRest
|
||||||
|
// >
|
||||||
|
// : TField extends `${infer TPrefix}[${infer TIndex extends number | 'i'}]`
|
||||||
|
// ? DeepValue<TFormData, TPrefix>[TIndex extends 'i' ? number : TIndex]
|
||||||
|
// : DeepValue<TFormData, TField>
|
||||||
|
|
||||||
|
export type FieldValue<TFormData, TField> = TFormData extends any[]
|
||||||
|
? unknown extends TField
|
||||||
|
? TFormData[number]
|
||||||
|
: DeepValue<TFormData[number], TField>
|
||||||
|
: DeepValue<TFormData, TField>
|
||||||
|
|
||||||
|
// type Test1 = FieldValue<{ foo: { bar: string }[] }, 'foo'>
|
||||||
|
// // ^?
|
||||||
|
// type Test2 = FieldValue<{ foo: { bar: string }[] }, 'foo[i]'>
|
||||||
|
// // ^?
|
||||||
|
// type Test3 = FieldValue<{ foo: { bar: string }[] }, 'foo[2].bar'>
|
||||||
|
// // ^?
|
||||||
|
|
||||||
|
export type FieldComponent<TParentData, TFormData> = <TField>({
|
||||||
|
children,
|
||||||
|
...fieldOptions
|
||||||
|
}: {
|
||||||
|
children: (
|
||||||
|
fieldApi: FieldApi<FieldValue<TParentData, TField>, TFormData>,
|
||||||
|
) => any
|
||||||
|
} & Omit<
|
||||||
|
UseFieldOptions<FieldValue<TParentData, TField>, TFormData>,
|
||||||
|
'name' | 'index'
|
||||||
|
> &
|
||||||
|
(TParentData extends any[]
|
||||||
|
? {
|
||||||
|
name?: TField extends undefined ? TField : DeepKeys<TParentData>
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: TField extends undefined ? TField : DeepKeys<TParentData>
|
||||||
|
index?: never
|
||||||
|
})) => any
|
||||||
|
|
||||||
|
export function Field<TData, TFormData>({
|
||||||
|
children,
|
||||||
|
...fieldOptions
|
||||||
|
}: {
|
||||||
|
children: (fieldApi: FieldApi<TData, TFormData>) => any
|
||||||
|
} & UseFieldOptions<TData, TFormData>) {
|
||||||
|
const fieldApi = useField(fieldOptions as any)
|
||||||
|
return (
|
||||||
|
<formContext.Provider
|
||||||
|
value={{ formApi: fieldApi.form, parentFieldName: fieldApi.name }}
|
||||||
|
children={functionalUpdate(children, fieldApi as any)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,8 +3,7 @@ import { FormApi, functionalUpdate } from '@tanstack/form-core'
|
|||||||
import type { NoInfer } from '@tanstack/react-store'
|
import type { NoInfer } from '@tanstack/react-store'
|
||||||
import { useStore } from '@tanstack/react-store'
|
import { useStore } from '@tanstack/react-store'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createFieldComponent, type FieldComponent } from './Field'
|
import { type UseField, type FieldComponent, Field, useField } from './useField'
|
||||||
import { createUseField, type UseField } from './useField'
|
|
||||||
import { formContext } from './formContext'
|
import { formContext } from './formContext'
|
||||||
|
|
||||||
declare module '@tanstack/form-core' {
|
declare module '@tanstack/form-core' {
|
||||||
@@ -15,7 +14,7 @@ declare module '@tanstack/form-core' {
|
|||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
interface FormApi<TFormData> {
|
interface FormApi<TFormData> {
|
||||||
Form: FormComponent
|
Form: FormComponent
|
||||||
Field: FieldComponent<TFormData>
|
Field: FieldComponent<TFormData, 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,
|
||||||
@@ -35,8 +34,8 @@ export function useForm<TData>(opts?: FormOptions<TData>): FormApi<TData> {
|
|||||||
const api = new FormApi<TData>(opts)
|
const api = new FormApi<TData>(opts)
|
||||||
|
|
||||||
api.Form = createFormComponent(api)
|
api.Form = createFormComponent(api)
|
||||||
api.Field = createFieldComponent<TData>()
|
api.Field = Field as any
|
||||||
api.useField = createUseField<TData>()
|
api.useField = useField as any
|
||||||
api.useStore = (
|
api.useStore = (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
selector,
|
selector,
|
||||||
@@ -73,7 +72,7 @@ function createFormComponent(formApi: FormApi<any>) {
|
|||||||
const isSubmitting = formApi.useStore((state) => state.isSubmitting)
|
const isSubmitting = formApi.useStore((state) => state.isSubmitting)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<formContext.Provider value={formApi}>
|
<formContext.Provider value={{ formApi }}>
|
||||||
{noFormElement ? (
|
{noFormElement ? (
|
||||||
children
|
children
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user