@tanstack/form-core 0.41.4 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/FormApi.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  isStandardSchemaValidator,
14
14
  standardSchemaValidator,
15
15
  } from './standardSchemaValidator'
16
+ import { metaHelper } from './metaHelper'
16
17
  import type { StandardSchemaV1 } from './standardSchemaValidator'
17
18
  import type { FieldApi, FieldMeta, FieldMetaBase } from './FieldApi'
18
19
  import type {
@@ -1265,9 +1266,6 @@ export class FormApi<
1265
1266
  this.validateField(field, 'change')
1266
1267
  }
1267
1268
 
1268
- /**
1269
- * Inserts a value into an array field at the specified index, shifting the subsequent values to the right.
1270
- */
1271
1269
  insertFieldValue = async <TField extends DeepKeys<TFormData>>(
1272
1270
  field: TField,
1273
1271
  index: number,
@@ -1290,6 +1288,11 @@ export class FormApi<
1290
1288
 
1291
1289
  // Validate the whole array + all fields that have shifted
1292
1290
  await this.validateField(field, 'change')
1291
+
1292
+ // Shift down all meta after validating to make sure the new field has been mounted
1293
+ metaHelper(this).handleArrayFieldMetaShift(field, index, 'insert')
1294
+
1295
+ await this.validateArrayFieldsStartingFrom(field, index, 'change')
1293
1296
  }
1294
1297
 
1295
1298
  /**
@@ -1342,6 +1345,9 @@ export class FormApi<
1342
1345
  opts,
1343
1346
  )
1344
1347
 
1348
+ // Shift up all meta
1349
+ metaHelper(this).handleArrayFieldMetaShift(field, index, 'remove')
1350
+
1345
1351
  if (lastIndex !== null) {
1346
1352
  const start = `${field}[${lastIndex}]`
1347
1353
  const fieldsToDelete = Object.keys(this.fieldInfo).filter((f) =>
@@ -1376,6 +1382,9 @@ export class FormApi<
1376
1382
  opts,
1377
1383
  )
1378
1384
 
1385
+ // Swap meta
1386
+ metaHelper(this).handleArrayFieldMetaShift(field, index1, 'swap', index2)
1387
+
1379
1388
  // Validate the whole array
1380
1389
  this.validateField(field, 'change')
1381
1390
  // Validate the swapped fields
@@ -1401,6 +1410,9 @@ export class FormApi<
1401
1410
  opts,
1402
1411
  )
1403
1412
 
1413
+ // Move meta between index1 and index2
1414
+ metaHelper(this).handleArrayFieldMetaShift(field, index1, 'move', index2)
1415
+
1404
1416
  // Validate the whole array
1405
1417
  this.validateField(field, 'change')
1406
1418
  // Validate the moved fields
package/src/mergeForm.ts CHANGED
@@ -2,34 +2,73 @@ import type { FormApi } from './FormApi'
2
2
  import type { Validator } from './types'
3
3
  import type { NoInfer } from './util-types'
4
4
 
5
+ function isValidKey(key: string | number | symbol): boolean {
6
+ const dangerousProps = ['__proto__', 'constructor', 'prototype']
7
+ return !dangerousProps.includes(String(key))
8
+ }
9
+
5
10
  /**
6
11
  * @private
7
12
  */
8
- export function mutateMergeDeep(target: object, source: object): object {
13
+ export function mutateMergeDeep(
14
+ target: object | null | undefined,
15
+ source: object | null | undefined,
16
+ ): object {
17
+ // Early return if either is not an object
18
+ if (target === null || target === undefined || typeof target !== 'object')
19
+ return {} as object
20
+ if (source === null || source === undefined || typeof source !== 'object')
21
+ return target
22
+
9
23
  const targetKeys = Object.keys(target)
10
24
  const sourceKeys = Object.keys(source)
11
25
  const keySet = new Set([...targetKeys, ...sourceKeys])
26
+
12
27
  for (const key of keySet) {
13
- const targetKey = key as never as keyof typeof target
14
- const sourceKey = key as never as keyof typeof source
15
-
16
- if (Array.isArray(target[targetKey]) && Array.isArray(source[sourceKey])) {
17
- // always use the source array to prevent array fields from multiplying
18
- target[targetKey] = source[sourceKey] as [] as never
19
- } else if (
20
- typeof target[targetKey] === 'object' &&
21
- typeof source[sourceKey] === 'object'
22
- ) {
23
- mutateMergeDeep(target[targetKey] as {}, source[sourceKey] as {})
24
- } else {
25
- // Prevent assigning undefined to target, only if undefined is not explicitly set on source
26
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
27
- if (!(sourceKey in source) && source[sourceKey] === undefined) {
28
- continue
29
- }
30
- target[targetKey] = source[sourceKey] as never
28
+ if (!isValidKey(key)) continue
29
+
30
+ const targetKey = key as keyof typeof target
31
+ const sourceKey = key as keyof typeof source
32
+
33
+ if (!Object.hasOwn(source, sourceKey)) continue
34
+
35
+ const sourceValue = source[sourceKey] as unknown
36
+ const targetValue = target[targetKey] as unknown
37
+
38
+ // Handle arrays
39
+ if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
40
+ Object.defineProperty(target, key, {
41
+ value: [...sourceValue],
42
+ enumerable: true,
43
+ writable: true,
44
+ configurable: true,
45
+ })
46
+ continue
47
+ }
48
+
49
+ // Handle nested objects (type assertion to satisfy ESLint)
50
+ const isTargetObj = typeof targetValue === 'object' && targetValue !== null
51
+ const isSourceObj = typeof sourceValue === 'object' && sourceValue !== null
52
+ const areObjects =
53
+ isTargetObj &&
54
+ isSourceObj &&
55
+ !Array.isArray(targetValue) &&
56
+ !Array.isArray(sourceValue)
57
+
58
+ if (areObjects) {
59
+ mutateMergeDeep(targetValue as object, sourceValue as object)
60
+ continue
31
61
  }
62
+
63
+ // Handle all other cases
64
+ Object.defineProperty(target, key, {
65
+ value: sourceValue,
66
+ enumerable: true,
67
+ writable: true,
68
+ configurable: true,
69
+ })
32
70
  }
71
+
33
72
  return target
34
73
  }
35
74
 
@@ -0,0 +1,184 @@
1
+ import type { FieldMeta } from './FieldApi'
2
+ import type { FormApi } from './FormApi'
3
+ import type { Validator } from './types'
4
+ import type { DeepKeys } from './util-types'
5
+
6
+ type ArrayFieldMode = 'insert' | 'remove' | 'swap' | 'move'
7
+
8
+ export function metaHelper<
9
+ TFormData,
10
+ TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
11
+ >(formApi: FormApi<TFormData, TFormValidator>) {
12
+ function handleArrayFieldMetaShift(
13
+ field: DeepKeys<TFormData>,
14
+ index: number,
15
+ mode: ArrayFieldMode,
16
+ secondIndex?: number,
17
+ ) {
18
+ const affectedFields = getAffectedFields(field, index, mode, secondIndex)
19
+
20
+ const handlers = {
21
+ insert: () => handleInsertMode(affectedFields, field, index),
22
+ remove: () => handleRemoveMode(affectedFields),
23
+ swap: () =>
24
+ secondIndex !== undefined &&
25
+ handleSwapMode(affectedFields, field, index, secondIndex),
26
+ move: () =>
27
+ secondIndex !== undefined &&
28
+ handleMoveMode(affectedFields, field, index, secondIndex),
29
+ }
30
+
31
+ handlers[mode]()
32
+ }
33
+
34
+ function getFieldPath(field: DeepKeys<TFormData>, index: number): string {
35
+ return `${field}[${index}]`
36
+ }
37
+
38
+ function getAffectedFields(
39
+ field: DeepKeys<TFormData>,
40
+ index: number,
41
+ mode: ArrayFieldMode,
42
+ secondIndex?: number,
43
+ ): DeepKeys<TFormData>[] {
44
+ const affectedFieldKeys = [getFieldPath(field, index)]
45
+
46
+ if (mode === 'swap') {
47
+ affectedFieldKeys.push(getFieldPath(field, secondIndex!))
48
+ } else if (mode === 'move') {
49
+ const [startIndex, endIndex] = [
50
+ Math.min(index, secondIndex!),
51
+ Math.max(index, secondIndex!),
52
+ ]
53
+ for (let i = startIndex; i <= endIndex; i++) {
54
+ affectedFieldKeys.push(getFieldPath(field, i))
55
+ }
56
+ } else {
57
+ const currentValue = formApi.getFieldValue(field)
58
+ const fieldItems = Array.isArray(currentValue) ? currentValue.length : 0
59
+ for (let i = index + 1; i < fieldItems; i++) {
60
+ affectedFieldKeys.push(getFieldPath(field, i))
61
+ }
62
+ }
63
+
64
+ return Object.keys(formApi.fieldInfo).filter((fieldKey) =>
65
+ affectedFieldKeys.some((key) => fieldKey.startsWith(key)),
66
+ ) as DeepKeys<TFormData>[]
67
+ }
68
+
69
+ function updateIndex(
70
+ fieldKey: string,
71
+ direction: 'up' | 'down',
72
+ ): DeepKeys<TFormData> {
73
+ return fieldKey.replace(/\[(\d+)\]/, (_, num) => {
74
+ const currIndex = parseInt(num, 10)
75
+ const newIndex =
76
+ direction === 'up' ? currIndex + 1 : Math.max(0, currIndex - 1)
77
+ return `[${newIndex}]`
78
+ }) as DeepKeys<TFormData>
79
+ }
80
+
81
+ function shiftMeta(fields: DeepKeys<TFormData>[], direction: 'up' | 'down') {
82
+ const sortedFields = direction === 'up' ? fields : [...fields].reverse()
83
+
84
+ sortedFields.forEach((fieldKey) => {
85
+ const nextFieldKey = updateIndex(fieldKey.toString(), direction)
86
+ const nextFieldMeta = formApi.getFieldMeta(nextFieldKey)
87
+ if (nextFieldMeta) {
88
+ formApi.setFieldMeta(fieldKey, nextFieldMeta)
89
+ }
90
+ })
91
+ }
92
+
93
+ const getEmptyFieldMeta = (): FieldMeta => ({
94
+ isValidating: false,
95
+ isTouched: false,
96
+ isBlurred: false,
97
+ isDirty: false,
98
+ isPristine: true,
99
+ errors: [],
100
+ errorMap: {},
101
+ })
102
+
103
+ const handleInsertMode = (
104
+ fields: DeepKeys<TFormData>[],
105
+ field: DeepKeys<TFormData>,
106
+ insertIndex: number,
107
+ ) => {
108
+ shiftMeta(fields, 'down')
109
+
110
+ fields.forEach((fieldKey) => {
111
+ if (fieldKey.toString().startsWith(getFieldPath(field, insertIndex))) {
112
+ formApi.setFieldMeta(fieldKey, getEmptyFieldMeta())
113
+ }
114
+ })
115
+ }
116
+
117
+ const handleRemoveMode = (fields: DeepKeys<TFormData>[]) => {
118
+ shiftMeta(fields, 'up')
119
+ }
120
+
121
+ const handleMoveMode = (
122
+ fields: DeepKeys<TFormData>[],
123
+ field: DeepKeys<TFormData>,
124
+ fromIndex: number,
125
+ toIndex: number,
126
+ ) => {
127
+ // Store the original field meta that will be reapplied at the destination index
128
+ const fromFields = new Map(
129
+ Object.keys(formApi.fieldInfo)
130
+ .filter((fieldKey) =>
131
+ fieldKey.startsWith(getFieldPath(field, fromIndex)),
132
+ )
133
+ .map((fieldKey) => [
134
+ fieldKey as DeepKeys<TFormData>,
135
+ formApi.getFieldMeta(fieldKey as DeepKeys<TFormData>),
136
+ ]),
137
+ )
138
+
139
+ shiftMeta(fields, fromIndex < toIndex ? 'up' : 'down')
140
+
141
+ // Reapply the stored field meta at the destination index
142
+ Object.keys(formApi.fieldInfo)
143
+ .filter((fieldKey) => fieldKey.startsWith(getFieldPath(field, toIndex)))
144
+ .forEach((fieldKey) => {
145
+ const fromKey = fieldKey.replace(
146
+ getFieldPath(field, toIndex),
147
+ getFieldPath(field, fromIndex),
148
+ ) as DeepKeys<TFormData>
149
+
150
+ const fromMeta = fromFields.get(fromKey)
151
+ if (fromMeta) {
152
+ formApi.setFieldMeta(fieldKey as DeepKeys<TFormData>, fromMeta)
153
+ }
154
+ })
155
+ }
156
+
157
+ const handleSwapMode = (
158
+ fields: DeepKeys<TFormData>[],
159
+ field: DeepKeys<TFormData>,
160
+ index: number,
161
+ secondIndex: number,
162
+ ) => {
163
+ fields.forEach((fieldKey) => {
164
+ if (!fieldKey.toString().startsWith(getFieldPath(field, index))) return
165
+
166
+ const swappedKey = fieldKey
167
+ .toString()
168
+ .replace(
169
+ getFieldPath(field, index),
170
+ getFieldPath(field, secondIndex),
171
+ ) as DeepKeys<TFormData>
172
+
173
+ const [meta1, meta2] = [
174
+ formApi.getFieldMeta(fieldKey),
175
+ formApi.getFieldMeta(swappedKey),
176
+ ]
177
+
178
+ if (meta1) formApi.setFieldMeta(swappedKey, meta1)
179
+ if (meta2) formApi.setFieldMeta(fieldKey, meta2)
180
+ })
181
+ }
182
+
183
+ return { handleArrayFieldMetaShift }
184
+ }