@tanstack/form-core 0.41.3 → 0.42.0

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.
@@ -0,0 +1,8 @@
1
+ import { FormApi } from './FormApi.js';
2
+ import { Validator } from './types.js';
3
+ import { DeepKeys } from './util-types.js';
4
+ type ArrayFieldMode = 'insert' | 'remove' | 'swap' | 'move';
5
+ export declare function metaHelper<TFormData, TFormValidator extends Validator<TFormData, unknown> | undefined = undefined>(formApi: FormApi<TFormData, TFormValidator>): {
6
+ handleArrayFieldMetaShift: (field: DeepKeys<TFormData>, index: number, mode: ArrayFieldMode, secondIndex?: number) => void;
7
+ };
8
+ export {};
@@ -0,0 +1,116 @@
1
+ function metaHelper(formApi) {
2
+ function handleArrayFieldMetaShift(field, index, mode, secondIndex) {
3
+ const affectedFields = getAffectedFields(field, index, mode, secondIndex);
4
+ const handlers = {
5
+ insert: () => handleInsertMode(affectedFields, field, index),
6
+ remove: () => handleRemoveMode(affectedFields),
7
+ swap: () => secondIndex !== void 0 && handleSwapMode(affectedFields, field, index, secondIndex),
8
+ move: () => secondIndex !== void 0 && handleMoveMode(affectedFields, field, index, secondIndex)
9
+ };
10
+ handlers[mode]();
11
+ }
12
+ function getFieldPath(field, index) {
13
+ return `${field}[${index}]`;
14
+ }
15
+ function getAffectedFields(field, index, mode, secondIndex) {
16
+ const affectedFieldKeys = [getFieldPath(field, index)];
17
+ if (mode === "swap") {
18
+ affectedFieldKeys.push(getFieldPath(field, secondIndex));
19
+ } else if (mode === "move") {
20
+ const [startIndex, endIndex] = [
21
+ Math.min(index, secondIndex),
22
+ Math.max(index, secondIndex)
23
+ ];
24
+ for (let i = startIndex; i <= endIndex; i++) {
25
+ affectedFieldKeys.push(getFieldPath(field, i));
26
+ }
27
+ } else {
28
+ const currentValue = formApi.getFieldValue(field);
29
+ const fieldItems = Array.isArray(currentValue) ? currentValue.length : 0;
30
+ for (let i = index + 1; i < fieldItems; i++) {
31
+ affectedFieldKeys.push(getFieldPath(field, i));
32
+ }
33
+ }
34
+ return Object.keys(formApi.fieldInfo).filter(
35
+ (fieldKey) => affectedFieldKeys.some((key) => fieldKey.startsWith(key))
36
+ );
37
+ }
38
+ function updateIndex(fieldKey, direction) {
39
+ return fieldKey.replace(/\[(\d+)\]/, (_, num) => {
40
+ const currIndex = parseInt(num, 10);
41
+ const newIndex = direction === "up" ? currIndex + 1 : Math.max(0, currIndex - 1);
42
+ return `[${newIndex}]`;
43
+ });
44
+ }
45
+ function shiftMeta(fields, direction) {
46
+ const sortedFields = direction === "up" ? fields : [...fields].reverse();
47
+ sortedFields.forEach((fieldKey) => {
48
+ const nextFieldKey = updateIndex(fieldKey.toString(), direction);
49
+ const nextFieldMeta = formApi.getFieldMeta(nextFieldKey);
50
+ if (nextFieldMeta) {
51
+ formApi.setFieldMeta(fieldKey, nextFieldMeta);
52
+ }
53
+ });
54
+ }
55
+ const getEmptyFieldMeta = () => ({
56
+ isValidating: false,
57
+ isTouched: false,
58
+ isBlurred: false,
59
+ isDirty: false,
60
+ isPristine: true,
61
+ errors: [],
62
+ errorMap: {}
63
+ });
64
+ const handleInsertMode = (fields, field, insertIndex) => {
65
+ shiftMeta(fields, "down");
66
+ fields.forEach((fieldKey) => {
67
+ if (fieldKey.toString().startsWith(getFieldPath(field, insertIndex))) {
68
+ formApi.setFieldMeta(fieldKey, getEmptyFieldMeta());
69
+ }
70
+ });
71
+ };
72
+ const handleRemoveMode = (fields) => {
73
+ shiftMeta(fields, "up");
74
+ };
75
+ const handleMoveMode = (fields, field, fromIndex, toIndex) => {
76
+ const fromFields = new Map(
77
+ Object.keys(formApi.fieldInfo).filter(
78
+ (fieldKey) => fieldKey.startsWith(getFieldPath(field, fromIndex))
79
+ ).map((fieldKey) => [
80
+ fieldKey,
81
+ formApi.getFieldMeta(fieldKey)
82
+ ])
83
+ );
84
+ shiftMeta(fields, fromIndex < toIndex ? "up" : "down");
85
+ Object.keys(formApi.fieldInfo).filter((fieldKey) => fieldKey.startsWith(getFieldPath(field, toIndex))).forEach((fieldKey) => {
86
+ const fromKey = fieldKey.replace(
87
+ getFieldPath(field, toIndex),
88
+ getFieldPath(field, fromIndex)
89
+ );
90
+ const fromMeta = fromFields.get(fromKey);
91
+ if (fromMeta) {
92
+ formApi.setFieldMeta(fieldKey, fromMeta);
93
+ }
94
+ });
95
+ };
96
+ const handleSwapMode = (fields, field, index, secondIndex) => {
97
+ fields.forEach((fieldKey) => {
98
+ if (!fieldKey.toString().startsWith(getFieldPath(field, index))) return;
99
+ const swappedKey = fieldKey.toString().replace(
100
+ getFieldPath(field, index),
101
+ getFieldPath(field, secondIndex)
102
+ );
103
+ const [meta1, meta2] = [
104
+ formApi.getFieldMeta(fieldKey),
105
+ formApi.getFieldMeta(swappedKey)
106
+ ];
107
+ if (meta1) formApi.setFieldMeta(swappedKey, meta1);
108
+ if (meta2) formApi.setFieldMeta(fieldKey, meta2);
109
+ });
110
+ };
111
+ return { handleArrayFieldMetaShift };
112
+ }
113
+ export {
114
+ metaHelper
115
+ };
116
+ //# sourceMappingURL=metaHelper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metaHelper.js","sources":["../../src/metaHelper.ts"],"sourcesContent":["import type { FieldMeta } from './FieldApi'\nimport type { FormApi } from './FormApi'\nimport type { Validator } from './types'\nimport type { DeepKeys } from './util-types'\n\ntype ArrayFieldMode = 'insert' | 'remove' | 'swap' | 'move'\n\nexport function metaHelper<\n TFormData,\n TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,\n>(formApi: FormApi<TFormData, TFormValidator>) {\n function handleArrayFieldMetaShift(\n field: DeepKeys<TFormData>,\n index: number,\n mode: ArrayFieldMode,\n secondIndex?: number,\n ) {\n const affectedFields = getAffectedFields(field, index, mode, secondIndex)\n\n const handlers = {\n insert: () => handleInsertMode(affectedFields, field, index),\n remove: () => handleRemoveMode(affectedFields),\n swap: () =>\n secondIndex !== undefined &&\n handleSwapMode(affectedFields, field, index, secondIndex),\n move: () =>\n secondIndex !== undefined &&\n handleMoveMode(affectedFields, field, index, secondIndex),\n }\n\n handlers[mode]()\n }\n\n function getFieldPath(field: DeepKeys<TFormData>, index: number): string {\n return `${field}[${index}]`\n }\n\n function getAffectedFields(\n field: DeepKeys<TFormData>,\n index: number,\n mode: ArrayFieldMode,\n secondIndex?: number,\n ): DeepKeys<TFormData>[] {\n const affectedFieldKeys = [getFieldPath(field, index)]\n\n if (mode === 'swap') {\n affectedFieldKeys.push(getFieldPath(field, secondIndex!))\n } else if (mode === 'move') {\n const [startIndex, endIndex] = [\n Math.min(index, secondIndex!),\n Math.max(index, secondIndex!),\n ]\n for (let i = startIndex; i <= endIndex; i++) {\n affectedFieldKeys.push(getFieldPath(field, i))\n }\n } else {\n const currentValue = formApi.getFieldValue(field)\n const fieldItems = Array.isArray(currentValue) ? currentValue.length : 0\n for (let i = index + 1; i < fieldItems; i++) {\n affectedFieldKeys.push(getFieldPath(field, i))\n }\n }\n\n return Object.keys(formApi.fieldInfo).filter((fieldKey) =>\n affectedFieldKeys.some((key) => fieldKey.startsWith(key)),\n ) as DeepKeys<TFormData>[]\n }\n\n function updateIndex(\n fieldKey: string,\n direction: 'up' | 'down',\n ): DeepKeys<TFormData> {\n return fieldKey.replace(/\\[(\\d+)\\]/, (_, num) => {\n const currIndex = parseInt(num, 10)\n const newIndex =\n direction === 'up' ? currIndex + 1 : Math.max(0, currIndex - 1)\n return `[${newIndex}]`\n }) as DeepKeys<TFormData>\n }\n\n function shiftMeta(fields: DeepKeys<TFormData>[], direction: 'up' | 'down') {\n const sortedFields = direction === 'up' ? fields : [...fields].reverse()\n\n sortedFields.forEach((fieldKey) => {\n const nextFieldKey = updateIndex(fieldKey.toString(), direction)\n const nextFieldMeta = formApi.getFieldMeta(nextFieldKey)\n if (nextFieldMeta) {\n formApi.setFieldMeta(fieldKey, nextFieldMeta)\n }\n })\n }\n\n const getEmptyFieldMeta = (): FieldMeta => ({\n isValidating: false,\n isTouched: false,\n isBlurred: false,\n isDirty: false,\n isPristine: true,\n errors: [],\n errorMap: {},\n })\n\n const handleInsertMode = (\n fields: DeepKeys<TFormData>[],\n field: DeepKeys<TFormData>,\n insertIndex: number,\n ) => {\n shiftMeta(fields, 'down')\n\n fields.forEach((fieldKey) => {\n if (fieldKey.toString().startsWith(getFieldPath(field, insertIndex))) {\n formApi.setFieldMeta(fieldKey, getEmptyFieldMeta())\n }\n })\n }\n\n const handleRemoveMode = (fields: DeepKeys<TFormData>[]) => {\n shiftMeta(fields, 'up')\n }\n\n const handleMoveMode = (\n fields: DeepKeys<TFormData>[],\n field: DeepKeys<TFormData>,\n fromIndex: number,\n toIndex: number,\n ) => {\n // Store the original field meta that will be reapplied at the destination index\n const fromFields = new Map(\n Object.keys(formApi.fieldInfo)\n .filter((fieldKey) =>\n fieldKey.startsWith(getFieldPath(field, fromIndex)),\n )\n .map((fieldKey) => [\n fieldKey as DeepKeys<TFormData>,\n formApi.getFieldMeta(fieldKey as DeepKeys<TFormData>),\n ]),\n )\n\n shiftMeta(fields, fromIndex < toIndex ? 'up' : 'down')\n\n // Reapply the stored field meta at the destination index\n Object.keys(formApi.fieldInfo)\n .filter((fieldKey) => fieldKey.startsWith(getFieldPath(field, toIndex)))\n .forEach((fieldKey) => {\n const fromKey = fieldKey.replace(\n getFieldPath(field, toIndex),\n getFieldPath(field, fromIndex),\n ) as DeepKeys<TFormData>\n\n const fromMeta = fromFields.get(fromKey)\n if (fromMeta) {\n formApi.setFieldMeta(fieldKey as DeepKeys<TFormData>, fromMeta)\n }\n })\n }\n\n const handleSwapMode = (\n fields: DeepKeys<TFormData>[],\n field: DeepKeys<TFormData>,\n index: number,\n secondIndex: number,\n ) => {\n fields.forEach((fieldKey) => {\n if (!fieldKey.toString().startsWith(getFieldPath(field, index))) return\n\n const swappedKey = fieldKey\n .toString()\n .replace(\n getFieldPath(field, index),\n getFieldPath(field, secondIndex),\n ) as DeepKeys<TFormData>\n\n const [meta1, meta2] = [\n formApi.getFieldMeta(fieldKey),\n formApi.getFieldMeta(swappedKey),\n ]\n\n if (meta1) formApi.setFieldMeta(swappedKey, meta1)\n if (meta2) formApi.setFieldMeta(fieldKey, meta2)\n })\n }\n\n return { handleArrayFieldMetaShift }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/form-core",
3
- "version": "0.41.3",
3
+ "version": "0.42.0",
4
4
  "description": "Powerful, type-safe, framework agnostic forms.",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
package/src/FieldApi.ts CHANGED
@@ -989,12 +989,15 @@ export class FieldApi<
989
989
  */
990
990
  validate = (
991
991
  cause: ValidationCause,
992
+ opts?: { skipFormValidation?: boolean },
992
993
  ): ValidationError[] | Promise<ValidationError[]> => {
993
994
  // If the field is pristine, do not validate
994
995
  if (!this.state.meta.isTouched) return []
995
996
 
996
997
  // Attempt to sync validate first
997
- const { fieldsErrorMap } = this.form.validateSync(cause)
998
+ const { fieldsErrorMap } = opts?.skipFormValidation
999
+ ? { fieldsErrorMap: {} as never }
1000
+ : this.form.validateSync(cause)
998
1001
  const { hasErrored } = this.validateSync(
999
1002
  cause,
1000
1003
  fieldsErrorMap[this.name] ?? {},
@@ -1008,7 +1011,9 @@ export class FieldApi<
1008
1011
  }
1009
1012
 
1010
1013
  // No error? Attempt async validation
1011
- const formValidationResultPromise = this.form.validateAsync(cause)
1014
+ const formValidationResultPromise = opts?.skipFormValidation
1015
+ ? Promise.resolve({})
1016
+ : this.form.validateAsync(cause)
1012
1017
  return this.validateAsync(cause, formValidationResultPromise)
1013
1018
  }
1014
1019
 
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 {
@@ -722,7 +723,7 @@ export class FormApi<
722
723
  }
723
724
 
724
725
  /**
725
- * Validates form and all fields in using the correct handlers for a given validation cause.
726
+ * Validates all fields using the correct handlers for a given validation cause.
726
727
  */
727
728
  validateAllFields = async (cause: ValidationCause) => {
728
729
  const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
@@ -735,7 +736,9 @@ export class FormApi<
735
736
  // Validate the field
736
737
  fieldValidationPromises.push(
737
738
  // Remember, `validate` is either a sync operation or a promise
738
- Promise.resolve().then(() => fieldInstance.validate(cause)),
739
+ Promise.resolve().then(() =>
740
+ fieldInstance.validate(cause, { skipFormValidation: true }),
741
+ ),
739
742
  )
740
743
  // If any fields are not touched
741
744
  if (!field.instance.state.meta.isTouched) {
@@ -1076,9 +1079,19 @@ export class FormApi<
1076
1079
  this.baseStore.setState((prev) => ({ ...prev, isSubmitting: false }))
1077
1080
  }
1078
1081
 
1079
- // Validate form and all fields
1080
1082
  await this.validateAllFields('submit')
1081
1083
 
1084
+ if (!this.state.isFieldsValid) {
1085
+ done()
1086
+ this.options.onSubmitInvalid?.({
1087
+ value: this.state.values,
1088
+ formApi: this,
1089
+ })
1090
+ return
1091
+ }
1092
+
1093
+ await this.validate('submit')
1094
+
1082
1095
  // Fields are invalid, do not submit
1083
1096
  if (!this.state.isValid) {
1084
1097
  done()
@@ -1253,9 +1266,6 @@ export class FormApi<
1253
1266
  this.validateField(field, 'change')
1254
1267
  }
1255
1268
 
1256
- /**
1257
- * Inserts a value into an array field at the specified index, shifting the subsequent values to the right.
1258
- */
1259
1269
  insertFieldValue = async <TField extends DeepKeys<TFormData>>(
1260
1270
  field: TField,
1261
1271
  index: number,
@@ -1278,6 +1288,11 @@ export class FormApi<
1278
1288
 
1279
1289
  // Validate the whole array + all fields that have shifted
1280
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')
1281
1296
  }
1282
1297
 
1283
1298
  /**
@@ -1330,6 +1345,9 @@ export class FormApi<
1330
1345
  opts,
1331
1346
  )
1332
1347
 
1348
+ // Shift up all meta
1349
+ metaHelper(this).handleArrayFieldMetaShift(field, index, 'remove')
1350
+
1333
1351
  if (lastIndex !== null) {
1334
1352
  const start = `${field}[${lastIndex}]`
1335
1353
  const fieldsToDelete = Object.keys(this.fieldInfo).filter((f) =>
@@ -1364,6 +1382,9 @@ export class FormApi<
1364
1382
  opts,
1365
1383
  )
1366
1384
 
1385
+ // Swap meta
1386
+ metaHelper(this).handleArrayFieldMetaShift(field, index1, 'swap', index2)
1387
+
1367
1388
  // Validate the whole array
1368
1389
  this.validateField(field, 'change')
1369
1390
  // Validate the swapped fields
@@ -1389,6 +1410,9 @@ export class FormApi<
1389
1410
  opts,
1390
1411
  )
1391
1412
 
1413
+ // Move meta between index1 and index2
1414
+ metaHelper(this).handleArrayFieldMetaShift(field, index1, 'move', index2)
1415
+
1392
1416
  // Validate the whole array
1393
1417
  this.validateField(field, 'change')
1394
1418
  // Validate the moved fields
@@ -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
+ }