Skip to content

Commit 93e3caa

Browse files
committed
adjust field names on array changes
1 parent ee4b55c commit 93e3caa

File tree

3 files changed

+216
-75
lines changed

3 files changed

+216
-75
lines changed

packages/form-core/src/FieldApi.ts

Lines changed: 137 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Derived, batch } from '@tanstack/store'
1+
import { Derived, Store, batch } from '@tanstack/store'
22
import {
33
isStandardSchemaValidator,
44
standardSchemaValidators,
55
} from './standardSchemaValidator'
66
import { defaultFieldMeta } from './metaHelper'
77
import {
88
determineFieldLevelErrorSourceAndValue,
9+
evaluate,
910
getAsyncValidatorArray,
1011
getBy,
1112
getSyncValidatorArray,
@@ -864,6 +865,58 @@ export type AnyFieldMeta = FieldMeta<
864865
any
865866
>
866867

868+
export type FieldBaseState<
869+
TParentData,
870+
TName extends DeepKeys<TParentData>,
871+
TData extends DeepValue<TParentData, TName>,
872+
TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
873+
TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
874+
TOnChangeAsync extends
875+
| undefined
876+
| FieldAsyncValidateOrFn<TParentData, TName, TData>,
877+
TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
878+
TOnBlurAsync extends
879+
| undefined
880+
| FieldAsyncValidateOrFn<TParentData, TName, TData>,
881+
TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
882+
TOnSubmitAsync extends
883+
| undefined
884+
| FieldAsyncValidateOrFn<TParentData, TName, TData>,
885+
TOnDynamic extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
886+
TOnDynamicAsync extends
887+
| undefined
888+
| FieldAsyncValidateOrFn<TParentData, TName, TData>,
889+
> = {
890+
name: TName
891+
defaultMeta:
892+
| Partial<
893+
FieldMeta<
894+
TParentData,
895+
TName,
896+
TData,
897+
TOnMount,
898+
TOnChange,
899+
TOnChangeAsync,
900+
TOnBlur,
901+
TOnBlurAsync,
902+
TOnSubmit,
903+
TOnSubmitAsync,
904+
TOnDynamic,
905+
TOnDynamicAsync,
906+
any,
907+
any,
908+
any,
909+
any,
910+
any,
911+
any,
912+
any,
913+
any,
914+
any
915+
>
916+
>
917+
| undefined
918+
}
919+
867920
/**
868921
* An object type representing the state of a field.
869922
*/
@@ -1049,10 +1102,31 @@ export class FieldApi<
10491102
TFormOnServer,
10501103
TParentSubmitMeta
10511104
>['form']
1105+
1106+
baseStore: Store<
1107+
FieldBaseState<
1108+
TParentData,
1109+
TName,
1110+
TData,
1111+
TOnMount,
1112+
TOnChange,
1113+
TOnChangeAsync,
1114+
TOnBlur,
1115+
TOnBlurAsync,
1116+
TOnSubmit,
1117+
TOnSubmitAsync,
1118+
TOnDynamic,
1119+
TOnDynamicAsync
1120+
>
1121+
>
1122+
10521123
/**
10531124
* The field name.
10541125
*/
1055-
name!: DeepKeys<TParentData>
1126+
get name(): DeepKeys<TParentData> {
1127+
return this.baseStore.state.name
1128+
}
1129+
10561130
/**
10571131
* The field options.
10581132
*/
@@ -1152,20 +1226,42 @@ export class FieldApi<
11521226
>,
11531227
) {
11541228
this.form = opts.form as never
1155-
this.name = opts.name as never
11561229
this.timeoutIds = {
11571230
validations: {} as Record<ValidationCause, never>,
11581231
listeners: {} as Record<ListenerCause, never>,
11591232
formListeners: {} as Record<ListenerCause, never>,
11601233
}
11611234

1235+
this.baseStore = new Store<
1236+
FieldBaseState<
1237+
TParentData,
1238+
TName,
1239+
TData,
1240+
TOnMount,
1241+
TOnChange,
1242+
TOnChangeAsync,
1243+
TOnBlur,
1244+
TOnBlurAsync,
1245+
TOnSubmit,
1246+
TOnSubmitAsync,
1247+
TOnDynamic,
1248+
TOnDynamicAsync
1249+
>
1250+
>({
1251+
name: opts.name,
1252+
defaultMeta: opts.defaultMeta,
1253+
})
1254+
11621255
this.store = new Derived({
1163-
deps: [this.form.store],
1164-
fn: () => {
1165-
const value = this.form.getFieldValue(this.name)
1166-
const meta = this.form.getFieldMeta(this.name) ?? {
1256+
deps: [this.form.store, this.baseStore],
1257+
fn: ({ currDepVals }) => {
1258+
const fieldName = currDepVals[1].name
1259+
const defaultMeta = currDepVals[1].defaultMeta
1260+
1261+
const value = this.form.getFieldValue(fieldName)
1262+
const meta = this.form.getFieldMeta(fieldName) ?? {
11671263
...defaultFieldMeta,
1168-
...opts.defaultMeta,
1264+
...(defaultMeta ?? {}),
11691265
}
11701266

11711267
return {
@@ -1232,11 +1328,14 @@ export class FieldApi<
12321328
mount = () => {
12331329
const cleanup = this.store.mount()
12341330

1235-
if ((this.options.defaultValue as unknown) !== undefined) {
1236-
this.form.setFieldValue(this.name, this.options.defaultValue as never, {
1331+
if (this.options.defaultValue !== undefined) {
1332+
this.form.setFieldValue(this.name, this.options.defaultValue, {
12371333
dontUpdateMeta: true,
12381334
})
12391335
}
1336+
if (this.options.name !== this.name) {
1337+
this.baseStore.setState((prev) => ({ ...prev, name: this.options.name }))
1338+
}
12401339

12411340
const info = this.getInfo()
12421341
info.instance = this as never
@@ -1309,39 +1408,41 @@ export class FieldApi<
13091408
TParentSubmitMeta
13101409
>,
13111410
) => {
1312-
this.options = opts as never
1411+
const shouldUpdateValue =
1412+
opts.defaultValue !== undefined &&
1413+
!this.state.meta.isTouched &&
1414+
!evaluate(opts.defaultValue, this.options.defaultValue)
1415+
1416+
const shouldUpdateName = !evaluate(this.name, opts.name)
1417+
1418+
const shouldUpdateMeta = !evaluate(
1419+
this.baseStore.state.defaultMeta,
1420+
opts.defaultMeta,
1421+
)
13131422

1314-
const nameHasChanged = this.name !== opts.name
1315-
this.name = opts.name
1316-
1317-
// Default Value
1318-
if ((this.state.value as unknown) === undefined) {
1319-
const formDefault = getBy(
1320-
opts.form.options.defaultValues,
1321-
opts.name,
1322-
).value
1323-
1324-
const defaultValue = (opts.defaultValue as unknown) ?? formDefault
1325-
1326-
// The name is dynamic in array fields. It changes when the user performs operations like removing or reordering.
1327-
// In this case, we don't want to force a default value if the store managed to find an existing value.
1328-
1329-
// TODO test what is actually needed here
1330-
// if (nameHasChanged) {
1331-
// this.setValue((val) => (val as unknown) || defaultValue, {
1332-
// dontUpdateMeta: true,
1333-
// })
1334-
// } else if (defaultValue !== undefined) {
1335-
// this.setValue(defaultValue as never, {
1336-
// dontUpdateMeta: true,
1337-
// })
1338-
// }
1423+
if (shouldUpdateValue) {
1424+
this.form.setFieldValue(this.name, this.options.defaultValue!, {
1425+
dontUpdateMeta: true,
1426+
})
1427+
}
1428+
1429+
if (shouldUpdateName) {
1430+
this.baseStore.setState((prev) => ({ ...prev, name: opts.name }))
1431+
}
1432+
1433+
if (shouldUpdateMeta) {
1434+
this.baseStore.setState((prev) => ({
1435+
...prev,
1436+
defaultMeta: opts.defaultMeta,
1437+
}))
13391438
}
13401439

13411440
// Default Meta
13421441
if (this.form.getFieldMeta(this.name) === undefined) {
13431442
this.setMeta(this.state.meta)
13441443
}
1444+
1445+
this.options = opts as never
13451446
}
13461447

13471448
/**

packages/form-core/src/metaHelper.ts

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type {
33
FormAsyncValidateOrFn,
44
FormValidateOrFn,
55
} from './FormApi'
6-
import type { AnyFieldMeta } from './FieldApi'
6+
import type { AnyFieldApi, AnyFieldMeta } from './FieldApi'
77
import type { DeepKeys } from './util-types'
88

99
type ValueFieldMode = 'insert' | 'remove' | 'swap' | 'move'
@@ -68,16 +68,19 @@ export function metaHelper<
6868

6969
// Store the original field meta that will be reapplied at the destination index
7070
const fromFields = Object.keys(formApi.fieldInfo).reduce(
71-
(fieldMap, fieldKey) => {
71+
(fieldMap, fieldKey: DeepKeys<TFormData>) => {
7272
if (fieldKey.startsWith(getFieldPath(field, fromIndex))) {
73-
fieldMap.set(
74-
fieldKey as DeepKeys<TFormData>,
75-
formApi.getFieldMeta(fieldKey as DeepKeys<TFormData>),
76-
)
73+
fieldMap.set(fieldKey, {
74+
meta: formApi.getFieldMeta(fieldKey),
75+
instance: formApi.getFieldInfo(fieldKey).instance,
76+
})
7777
}
7878
return fieldMap
7979
},
80-
new Map<DeepKeys<TFormData>, AnyFieldMeta | undefined>(),
80+
new Map<
81+
DeepKeys<TFormData>,
82+
{ meta: AnyFieldMeta | undefined; instance: AnyFieldApi | null }
83+
>(),
8184
)
8285

8386
shiftMeta(affectedFields, fromIndex < toIndex ? 'up' : 'down')
@@ -92,9 +95,10 @@ export function metaHelper<
9295
) as DeepKeys<TFormData>
9396

9497
const fromMeta = fromFields.get(fromKey)
95-
if (fromMeta) {
96-
formApi.setFieldMeta(fieldKey as DeepKeys<TFormData>, fromMeta)
98+
if (fromMeta?.meta) {
99+
formApi.setFieldMeta(fieldKey, fromMeta.meta)
97100
}
101+
setName(fromMeta?.instance, fieldKey)
98102
})
99103
}
100104

@@ -136,6 +140,9 @@ export function metaHelper<
136140

137141
if (meta1) formApi.setFieldMeta(swappedKey, meta1)
138142
if (meta2) formApi.setFieldMeta(fieldKey, meta2)
143+
144+
changeInstanceName(fieldKey, swappedKey)
145+
changeInstanceName(swappedKey, fieldKey)
139146
})
140147
}
141148

@@ -154,34 +161,6 @@ export function metaHelper<
154161
})
155162
}
156163

157-
/**
158-
* Handle the meta shift from filtering out indeces.
159-
* @param remainingIndices An array of indeces that were NOT filtered out of the original array.
160-
*/
161-
function handleArrayFilter(
162-
field: DeepKeys<TFormData>,
163-
remainingIndices: number[],
164-
) {
165-
if (remainingIndices.length === 0) return
166-
167-
// create a map between the index and its new location
168-
remainingIndices.forEach((fromIndex, toIndex) => {
169-
if (fromIndex === toIndex) return
170-
// assign it the original meta
171-
const fieldKey = getFieldPath(field, toIndex)
172-
const originalFieldKey = getFieldPath(field, fromIndex)
173-
const originalFieldMeta = formApi.getFieldMeta(originalFieldKey)
174-
if (originalFieldMeta) {
175-
formApi.setFieldMeta(fieldKey, originalFieldMeta)
176-
} else {
177-
formApi.setFieldMeta(fieldKey, {
178-
...getEmptyFieldMeta(),
179-
isTouched: originalFieldKey as unknown as boolean,
180-
})
181-
}
182-
})
183-
}
184-
185164
function getFieldPath(
186165
field: DeepKeys<TFormData>,
187166
index: number,
@@ -244,8 +223,11 @@ export function metaHelper<
244223
const sortedFields = direction === 'up' ? fields : [...fields].reverse()
245224

246225
sortedFields.forEach((fieldKey) => {
247-
const nextFieldKey = updateIndex(fieldKey.toString(), direction)
226+
const nextFieldKey = updateIndex(fieldKey, direction)
248227
const nextFieldMeta = formApi.getFieldMeta(nextFieldKey)
228+
229+
changeInstanceName(nextFieldKey, fieldKey)
230+
249231
if (nextFieldMeta) {
250232
formApi.setFieldMeta(fieldKey, nextFieldMeta)
251233
} else {
@@ -254,13 +236,27 @@ export function metaHelper<
254236
})
255237
}
256238

239+
function changeInstanceName(
240+
fromName: DeepKeys<TFormData>,
241+
toName: DeepKeys<TFormData>,
242+
) {
243+
setName(formApi.getFieldInfo(fromName).instance, toName)
244+
}
245+
246+
function setName(
247+
instance: AnyFieldApi | null | undefined,
248+
name: DeepKeys<TFormData>,
249+
) {
250+
if (!instance) return
251+
instance.baseStore.setState((prev) => ({ ...prev, name }))
252+
}
253+
257254
const getEmptyFieldMeta = (): AnyFieldMeta => defaultFieldMeta
258255

259256
return {
260257
handleArrayMove,
261258
handleArrayRemove,
262259
handleArraySwap,
263260
handleArrayInsert,
264-
handleArrayFilter,
265261
}
266262
}

0 commit comments

Comments
 (0)