@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/dist/cjs/FormApi.cjs +6 -0
- package/dist/cjs/FormApi.cjs.map +1 -1
- package/dist/cjs/FormApi.d.cts +0 -3
- package/dist/cjs/mergeForm.cjs +33 -9
- package/dist/cjs/mergeForm.cjs.map +1 -1
- package/dist/cjs/mergeForm.d.cts +1 -1
- package/dist/cjs/metaHelper.cjs +116 -0
- package/dist/cjs/metaHelper.cjs.map +1 -0
- package/dist/cjs/metaHelper.d.cts +8 -0
- package/dist/esm/FormApi.d.ts +0 -3
- package/dist/esm/FormApi.js +6 -0
- package/dist/esm/FormApi.js.map +1 -1
- package/dist/esm/mergeForm.d.ts +1 -1
- package/dist/esm/mergeForm.js +33 -9
- package/dist/esm/mergeForm.js.map +1 -1
- package/dist/esm/metaHelper.d.ts +8 -0
- package/dist/esm/metaHelper.js +116 -0
- package/dist/esm/metaHelper.js.map +1 -0
- package/package.json +1 -1
- package/src/FormApi.ts +15 -3
- package/src/mergeForm.ts +58 -19
- package/src/metaHelper.ts +184 -0
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(
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
}
|