Skip to content

Commit 62a82fe

Browse files
committed
fix: array-field mode and utilities
1 parent ab651b4 commit 62a82fe

File tree

12 files changed

+265
-124
lines changed

12 files changed

+265
-124
lines changed

docs/reference/formApi.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ A class representing the Form API. It handles the logic and interactions with th
138138
```
139139
- Inserts a value into an array field at the specified index.
140140
- ```tsx
141-
spliceFieldValue<TField extends DeepKeys<TFormData>>(field: TField, index: number, opts?: { touch?: boolean })
141+
removeFieldValue<TField extends DeepKeys<TFormData>>(field: TField, index: number, opts?: { touch?: boolean })
142142
```
143143
- Removes a value from an array field at the specified index.
144144
- ```tsx

examples/react/simple/src/index.tsx

+81-10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ type Person = {
1111
type Hobby = {
1212
name: string;
1313
description: string;
14+
yearsOfExperience: number;
1415
};
1516

1617
const formFactory = createFormFactory<Person>({
@@ -61,8 +62,7 @@ export default function App() {
6162
children={(field) => (
6263
// Avoid hasty abstractions. Render props are great!
6364
<>
64-
<label htmlFor={field.name}>First Name:</label>
65-
<input name={field.name} {...field.getInputProps()} />
65+
<input {...field.getInputProps()} />
6666
<FieldInfo field={field} />
6767
</>
6868
)}
@@ -73,8 +73,7 @@ export default function App() {
7373
name="lastName"
7474
children={(field) => (
7575
<>
76-
<label htmlFor={field.name}>Last Name:</label>
77-
<input name={field.name} {...field.getInputProps()} />
76+
<input {...field.getInputProps()} />
7877
<FieldInfo field={field} />
7978
</>
8079
)}
@@ -83,12 +82,84 @@ export default function App() {
8382
<div>
8483
<form.Field
8584
name="hobbies"
86-
children={(field) => (
87-
<>
88-
<label htmlFor={field.name}>Last Name:</label>
89-
<input name={field.name} {...field.getInputProps()} />
90-
<FieldInfo field={field} />
91-
</>
85+
mode="array"
86+
children={(hobbiesField) => (
87+
<div>
88+
Hobbies
89+
<div
90+
style={{
91+
paddingLeft: "1rem",
92+
display: "flex",
93+
flexDirection: "column",
94+
gap: "1rem",
95+
}}
96+
>
97+
{!hobbiesField.state.value.length
98+
? "No hobbies found."
99+
: hobbiesField.state.value.map((value, i) => (
100+
<div
101+
key={i}
102+
style={{
103+
borderLeft: "2px solid gray",
104+
paddingLeft: ".5rem",
105+
}}
106+
>
107+
<hobbiesField.Field
108+
index={i}
109+
name="name"
110+
children={(field) => {
111+
return (
112+
<div>
113+
<label htmlFor={field.name}>Name:</label>
114+
<input
115+
name={field.name}
116+
{...field.getInputProps()}
117+
/>
118+
<button
119+
type="button"
120+
onClick={() => hobbiesField.removeValue(i)}
121+
>
122+
X
123+
</button>
124+
<FieldInfo field={field} />
125+
</div>
126+
);
127+
}}
128+
/>
129+
<hobbiesField.Field
130+
index={i}
131+
name="description"
132+
children={(field) => {
133+
return (
134+
<div>
135+
<label htmlFor={field.name}>
136+
Description:
137+
</label>
138+
<input
139+
name={field.name}
140+
{...field.getInputProps()}
141+
/>
142+
<FieldInfo field={field} />
143+
</div>
144+
);
145+
}}
146+
/>
147+
</div>
148+
))}
149+
</div>
150+
<button
151+
type="button"
152+
onClick={() =>
153+
hobbiesField.pushValue({
154+
name: "",
155+
description: "",
156+
yearsOfExperience: 0,
157+
})
158+
}
159+
>
160+
Add hobby
161+
</button>
162+
</div>
92163
)}
93164
/>
94165
</div>

packages/form-core/src/FieldApi.ts

+12-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type ValidationCause = 'change' | 'blur' | 'submit'
77

88
export interface FieldOptions<TData, TFormData> {
99
name: unknown extends TFormData ? string : DeepKeys<TFormData>
10+
index?: TData extends any[] ? number : never
1011
defaultValue?: TData
1112
validate?: (
1213
value: TData,
@@ -84,12 +85,12 @@ export class FieldApi<TData, TFormData> {
8485
this.form = opts.form
8586
this.uid = uid++
8687
// Support field prefixing from FieldScope
87-
let fieldPrefix = ''
88-
if (this.form.fieldName) {
89-
fieldPrefix = `${this.form.fieldName}.`
90-
}
88+
// let fieldPrefix = ''
89+
// if (this.form.fieldName) {
90+
// fieldPrefix = `${this.form.fieldName}.`
91+
// }
9192

92-
this.name = (fieldPrefix + opts.name) as any
93+
this.name = opts.name as any
9394

9495
this.store = new Store<FieldState<TData>>(
9596
{
@@ -113,6 +114,7 @@ export class FieldApi<TData, TFormData> {
113114
if (next.value !== prevState.value) {
114115
this.validate('change', next.value)
115116
}
117+
console.log(this)
116118
},
117119
},
118120
)
@@ -178,7 +180,9 @@ export class FieldApi<TData, TFormData> {
178180
}
179181
}
180182

181-
getValue = (): TData => this.form.getFieldValue(this.name)
183+
getValue = (): TData => {
184+
return this.form.getFieldValue(this.name)
185+
}
182186
setValue = (
183187
updater: Updater<TData>,
184188
options?: { touch?: boolean; notify?: boolean },
@@ -190,11 +194,11 @@ export class FieldApi<TData, TFormData> {
190194

191195
getInfo = () => this.form.getFieldInfo(this.name)
192196

193-
pushValue = (value: TData) =>
197+
pushValue = (value: TData extends any[] ? TData[number] : never) =>
194198
this.form.pushFieldValue(this.name, value as any)
195199
insertValue = (index: number, value: TData) =>
196200
this.form.insertFieldValue(this.name, index, value as any)
197-
removeValue = (index: number) => this.form.spliceFieldValue(this.name, index)
201+
removeValue = (index: number) => this.form.removeFieldValue(this.name, index)
198202
swapValues = (aIndex: number, bIndex: number) =>
199203
this.form.swapFieldValues(this.name, aIndex, bIndex)
200204

packages/form-core/src/FormApi.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export class FormApi<TFormData> {
138138
// Write it back to the store
139139
this.store.state = next
140140
this.state = next
141+
console.log(this.state)
141142
},
142143
},
143144
)
@@ -402,7 +403,7 @@ export class FormApi<TFormData> {
402403
)
403404
}
404405

405-
spliceFieldValue = <TField extends DeepKeys<TFormData>>(
406+
removeFieldValue = <TField extends DeepKeys<TFormData>>(
406407
field: TField,
407408
index: number,
408409
opts?: { touch?: boolean },

packages/form-core/src/utils.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export type DeepKeys<T> = unknown extends T
130130
: T extends readonly any[] & IsTuple<T>
131131
? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>>
132132
: T extends any[]
133-
? never & 'Dynamic length array indexing is not supported'
133+
? DeepKeys<T[number]>
134134
: T extends Date
135135
? never
136136
: T extends object
@@ -146,3 +146,18 @@ export type DeepValue<T, TProp> = T extends Record<string | number, any>
146146
? DeepValue<T[TBranch], TDeepProp>
147147
: T[TProp & string]
148148
: never
149+
150+
type Narrowable = string | number | bigint | boolean
151+
152+
type NarrowRaw<A> =
153+
| (A extends [] ? [] : never)
154+
| (A extends Narrowable ? A : never)
155+
| {
156+
[K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw<A[K]>
157+
}
158+
159+
export type Narrow<A extends any> = Try<A, [], NarrowRaw<A>>
160+
161+
type Try<A1 extends any, A2 extends any, Catch = never> = A1 extends A2
162+
? A1
163+
: Catch

packages/react-form/src/Field.tsx

-36
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import type { FormApi, FormOptions } from '@tanstack/form-core'
2-
import { createUseField, type UseField } from './useField'
2+
import { type UseField, type FieldComponent, Field, useField } from './useField'
33
import { useForm } from './useForm'
4-
import { createFieldComponent, type FieldComponent } from './Field'
54

65
export type FormFactory<TFormData> = {
76
useForm: (opts?: FormOptions<TFormData>) => FormApi<TFormData>
87
useField: UseField<TFormData>
9-
Field: FieldComponent<TFormData>
8+
Field: FieldComponent<TFormData, TFormData>
109
}
1110

1211
export function createFormFactory<TFormData>(
@@ -16,7 +15,7 @@ export function createFormFactory<TFormData>(
1615
useForm: (opts) => {
1716
return useForm<TFormData>({ ...defaultOpts, ...opts } as any) as any
1817
},
19-
useField: createUseField<TFormData>(),
20-
Field: createFieldComponent<TFormData>(),
18+
useField: useField as any,
19+
Field: Field as any,
2120
}
2221
}

packages/react-form/src/formContext.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { FormApi } from '@tanstack/form-core'
22
import * as React from 'react'
33

4-
export const formContext = React.createContext<FormApi<any> | null>(null)
4+
export const formContext = React.createContext<{
5+
formApi: FormApi<any>
6+
parentFieldName?: string
7+
} | null>(null!)
58

69
export function useFormContext() {
710
const formApi = React.useContext(formContext)

packages/react-form/src/index.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,8 @@ export { FormApi, FieldApi, functionalUpdate } from '@tanstack/form-core'
2525
export type { FormComponent, FormProps } from './useForm'
2626
export { useForm } from './useForm'
2727

28-
export type { FieldComponent } from './Field'
29-
export { Field } from './Field'
30-
31-
export type { UseField } from './useField'
32-
export { useField } from './useField'
28+
export type { UseField, FieldComponent } from './useField'
29+
export { useField, Field } from './useField'
3330

3431
export type { FormFactory } from './createFormFactory'
3532
export { createFormFactory } from './createFormFactory'

packages/react-form/src/useField.ts

-50
This file was deleted.

0 commit comments

Comments
 (0)