notform 2.1.3 → 2.2.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <p><strong>Vue Forms Without the Friction</strong></p>
4
4
  <p>
5
5
  <a href="https://npmx.dev/package/notform"><img src="https://img.shields.io/npm/v/notform.svg?style=plastic&label=NPM%20Version&color=blue" alt="NPM Version"></a>
6
- <a href="https://npmx.dev/package/notform"><img src="https://img.shields.io/npm/dw/notform.svg?style=plastic&label=NPM%20Downloads&color=blue" alt="NPM Downloads"></a>
6
+ <a href="https://npmx.dev/package/notform"><img src="https://img.shields.io/npm/dt/notform.svg?style=plastic&label=NPM%20Downloads&color=blue" alt="NPM Downloads"></a>
7
7
  <a href="https://npmx.dev/package/notform"><img src="https://img.shields.io/npm/unpacked-size/notform?style=plastic&label=NPM%20Unpacked%20Size&color=blue" alt="NPM Unpacked Size"></a>
8
8
  </p>
9
9
  </div>
@@ -37,13 +37,13 @@ import { NotField, NotForm, NotMessage, useNotForm } from 'notform'
37
37
  import { z } from 'zod'
38
38
 
39
39
  const schema = z.object({
40
- email: z.string().email('Invalid email'),
40
+ email: z.email('Invalid email'),
41
41
  name: z.string().min(1, 'Name is required'),
42
42
  })
43
43
 
44
44
  const form = useNotForm({
45
- onSubmit: (values) => {
46
- console.log('Form submitted:', values)
45
+ onSubmit(values) {
46
+ console.log('Form:', values)
47
47
  },
48
48
  schema,
49
49
  })
@@ -51,30 +51,51 @@ const form = useNotForm({
51
51
 
52
52
  <template>
53
53
  <NotForm
54
- :form="form"
54
+ :form
55
55
  @submit="form.submit"
56
+ @reset="form.reset()"
56
57
  >
57
- <NotField path="name">
58
- <input
59
- v-model="form.values.name"
60
- type="text"
61
- >
62
-
63
- <NotMessage path="name" />
58
+ <NotField
59
+ v-slot="{events,path}"
60
+ path="name"
61
+ >
62
+ <label :for="path">
63
+ Name
64
+ <input
65
+ v-bind="events"
66
+ :id="path"
67
+ v-model="form.values.name"
68
+ type="text"
69
+ >
70
+ </label>
71
+
72
+ <NotMessage :path="path" />
64
73
  </NotField>
65
74
 
66
- <NotField path="email">
67
- <input
68
- v-model="form.values.email"
69
- type="email"
70
- >
71
-
72
- <NotMessage path="email" />
75
+ <NotField
76
+ v-slot="{events,path}"
77
+ path="email"
78
+ >
79
+ <label :for="path">
80
+ Email
81
+ <input
82
+ v-bind="events"
83
+ :id="path"
84
+ v-model="form.values.email"
85
+ type="email"
86
+ >
87
+ </label>
88
+
89
+ <NotMessage :path="path" />
73
90
  </NotField>
74
91
 
75
92
  <button type="submit">
76
93
  Submit
77
94
  </button>
95
+
96
+ <button type="reset">
97
+ Reset Form
98
+ </button>
78
99
  </NotForm>
79
100
  </template>
80
101
  ```
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Component, ComputedRef, MaybeRefOrGetter, Raw, Ref } from "vue";
2
2
  import { StandardSchemaV1 } from "@standard-schema/spec";
3
3
  import { Get, PartialDeep, Paths as Paths$1 } from "type-fest";
4
-
5
4
  //#region src/types/shared.d.ts
6
5
  /**
7
6
  * The validation mode determines when validation occurs.
@@ -44,6 +43,23 @@ type ObjectSchema = StandardSchemaV1 & {
44
43
  };
45
44
  };
46
45
  };
46
+ /**
47
+ * Represents a segment of a validation path.
48
+ * It can be either a string (for object properties) or a number (for array indices).
49
+ */
50
+ type Segment = PropertyKey | StandardSchemaV1.PathSegment;
51
+ /**
52
+ * Maps the index an array item occupied before a mutation to the index it
53
+ * occupies after that mutation, or to `undefined` when the mutation removed the
54
+ * item entirely.
55
+ *
56
+ * Each array mutation in `NotArrayField` (`prepend`, `remove`, `insert`,
57
+ * `swap`, `move`) builds one of these to describe exactly how it reshuffles
58
+ * indices, then passes it to `remapArrayFieldState`.
59
+ * @param previousIndex The index the item occupied before the mutation.
60
+ * @returns The index the item occupies after the mutation, or `undefined` if the item was removed.
61
+ */
62
+ type ArrayItemIndexMap = (previousIndex: number) => number | undefined;
47
63
  //#endregion
48
64
  //#region src/types/use-not-form.d.ts
49
65
  /**
@@ -64,9 +80,9 @@ interface UseNotFormConfig<TSchema extends ObjectSchema> {
64
80
  validateOn?: Partial<Record<ValidationTrigger, boolean>>;
65
81
  /**
66
82
  * The validation mode of the form.
67
- * @default { eager: true }
83
+ * @default 'eager'
68
84
  */
69
- validationMode?: Partial<Record<ValidationMode, boolean>>;
85
+ validationMode?: ValidationMode;
70
86
  /**
71
87
  * Callback triggered when form validation passes and the form is submitted.
72
88
  * @param values The validated output data from the schema.
@@ -80,9 +96,13 @@ interface UseNotFormConfig<TSchema extends ObjectSchema> {
80
96
  * @template TSchema The form schema.
81
97
  */
82
98
  type NotFormInstance<TSchema extends ObjectSchema> = Raw<{
83
- /** The values the form was initialised or last reset with. */readonly initialValues: UseNotFormConfig<TSchema>['initialValues']; /** The errors the form was initialised or last reset with. */
84
- readonly initialErrors: UseNotFormConfig<TSchema>['initialErrors']; /** The resolved validation triggers for this form. */
85
- readonly validateOn: Required<NonNullable<UseNotFormConfig<TSchema>['validateOn']>>; /** The resolved validation mode for this form. */
99
+ /** The values the form was initialised or last reset with. */
100
+ readonly initialValues: UseNotFormConfig<TSchema>['initialValues'];
101
+ /** The errors the form was initialised or last reset with. */
102
+ readonly initialErrors: UseNotFormConfig<TSchema>['initialErrors'];
103
+ /** The resolved validation triggers for this form. */
104
+ readonly validateOn: Required<NonNullable<UseNotFormConfig<TSchema>['validateOn']>>;
105
+ /** The resolved validation mode for this form. */
86
106
  readonly validationMode: Required<NonNullable<UseNotFormConfig<TSchema>['validationMode']>>;
87
107
  /**
88
108
  * Deeply reactive object of field values.
@@ -117,20 +137,24 @@ type NotFormInstance<TSchema extends ObjectSchema> = Raw<{
117
137
  * The set of field paths the user has interacted with.
118
138
  * Populated for all paths when the form is submitted.
119
139
  */
120
- touchedFields: Set<Paths<StandardSchemaV1.InferInput<TSchema>>>; /** Whether any field has been touched. */
140
+ touchedFields: Set<Paths<StandardSchemaV1.InferInput<TSchema>>>;
141
+ /** Whether any field has been touched. */
121
142
  isTouched: ComputedRef<boolean>;
122
143
  /**
123
144
  * Marks a field as touched.
124
145
  * Called automatically by the field's `onBlur` handler.
125
146
  */
126
- touchField: (path: Paths<StandardSchemaV1.InferInput<TSchema>>) => void; /** The set of field paths whose current value differs from the initial value. */
127
- dirtyFields: Set<Paths<StandardSchemaV1.InferInput<TSchema>>>; /** Whether any field value differs from its initial value. */
147
+ touchField: (path: Paths<StandardSchemaV1.InferInput<TSchema>>) => void;
148
+ /** The set of field paths whose current value differs from the initial value. */
149
+ dirtyFields: Set<Paths<StandardSchemaV1.InferInput<TSchema>>>;
150
+ /** Whether any field value differs from its initial value. */
128
151
  isDirty: ComputedRef<boolean>;
129
152
  /**
130
153
  * Marks a field as dirty.
131
154
  * Called automatically when a field value changes.
132
155
  */
133
- dirtyField: (path: Paths<StandardSchemaV1.InferInput<TSchema>>) => void; /** The raw validation issues produced by the last validation run. */
156
+ dirtyField: (path: Paths<StandardSchemaV1.InferInput<TSchema>>) => void;
157
+ /** The raw validation issues produced by the last validation run. */
134
158
  errors: Array<StandardSchemaV1.Issue>;
135
159
  /**
136
160
  * A flat map of field path to its first error message.
@@ -148,8 +172,10 @@ type NotFormInstance<TSchema extends ObjectSchema> = Raw<{
148
172
  * Replaces an existing error for the same path, or appends it if none exists.
149
173
  * Useful for setting server-side errors after submission.
150
174
  */
151
- setError: (error: StandardSchemaV1.Issue) => void; /** Replaces all current errors with the provided issues. */
152
- setErrors: (errors: Array<StandardSchemaV1.Issue>) => void; /** Removes all active validation errors. */
175
+ setError: (error: StandardSchemaV1.Issue) => void;
176
+ /** Replaces all current errors with the provided issues. */
177
+ setErrors: (errors: Array<StandardSchemaV1.Issue>) => void;
178
+ /** Removes all active validation errors. */
153
179
  clearErrors: () => void;
154
180
  /**
155
181
  * Returns all validation issues for a specific field path.
@@ -158,7 +184,8 @@ type NotFormInstance<TSchema extends ObjectSchema> = Raw<{
158
184
  * form.getFieldErrors('address.city')
159
185
  * ```
160
186
  */
161
- getFieldErrors: (path: Paths<StandardSchemaV1.InferInput<TSchema>>) => Array<StandardSchemaV1.Issue>; /** Whether a validation run is currently in progress. */
187
+ getFieldErrors: (path: Paths<StandardSchemaV1.InferInput<TSchema>>) => Array<StandardSchemaV1.Issue>;
188
+ /** Whether a validation run is currently in progress. */
162
189
  isValidating: Ref<boolean>;
163
190
  /**
164
191
  * Validates the entire form against the schema.
@@ -169,8 +196,10 @@ type NotFormInstance<TSchema extends ObjectSchema> = Raw<{
169
196
  * Validates a single field against the schema.
170
197
  * Only replaces errors for that field — all other fields are left untouched.
171
198
  */
172
- validateField: (path: Paths<StandardSchemaV1.InferInput<TSchema>>) => Promise<StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>>; /** Whether the form currently has no validation errors. */
173
- isValid: ComputedRef<boolean>; /** Whether the form is currently running its submit handler. */
199
+ validateField: (path: Paths<StandardSchemaV1.InferInput<TSchema>>) => Promise<StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>>;
200
+ /** Whether the form currently has no validation errors. */
201
+ isValid: ComputedRef<boolean>;
202
+ /** Whether the form is currently running its submit handler. */
174
203
  isSubmitting: Ref<boolean>;
175
204
  /**
176
205
  * Validates the form and runs `onSubmit` if it passes.
@@ -277,9 +306,13 @@ interface NotArrayFieldItem {
277
306
  interface NotArrayFieldSlots<TSchema extends ObjectSchema, TItem = any> {
278
307
  /** The default slot receives the full array state and manipulation methods. */
279
308
  default: (props: {
280
- /** The dot-separated path to this array field. */path: string; /** The array items with stable keys and paths for use with `v-for`. */
281
- items: Array<NotArrayFieldItem>; /** All validation issues for this array field from the last validation run. */
282
- errors: Array<StandardSchemaV1.Issue>; /** Whether this array field currently has no validation errors. */
309
+ /** The dot-separated path to this array field. */
310
+ path: string;
311
+ /** The array items with stable keys and paths for use with `v-for`. */
312
+ items: Array<NotArrayFieldItem>;
313
+ /** All validation issues for this array field from the last validation run. */
314
+ errors: Array<StandardSchemaV1.Issue>;
315
+ /** Whether this array field currently has no validation errors. */
283
316
  isValid: boolean;
284
317
  /**
285
318
  * Whether any item in this array has been touched.
@@ -290,7 +323,8 @@ interface NotArrayFieldSlots<TSchema extends ObjectSchema, TItem = any> {
290
323
  * Whether any item in this array differs from its initial value.
291
324
  * Derived from the form's `dirtyFields` set.
292
325
  */
293
- isDirty: boolean; /** Whether validation is currently running for this array field. */
326
+ isDirty: boolean;
327
+ /** Whether validation is currently running for this array field. */
294
328
  isValidating: boolean;
295
329
  /**
296
330
  * Manually triggers validation for this array field.
@@ -349,10 +383,10 @@ declare const __VLS_export$3: <TSchema extends ObjectSchema, TItemSchema extends
349
383
  slots: NotArrayFieldSlots<TSchema, TItem>;
350
384
  emit: {};
351
385
  }>) => import("vue").VNode & {
352
- __ctx?: Awaited<typeof __VLS_setup>;
386
+ __ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
353
387
  };
354
388
  declare const _default: typeof __VLS_export$3;
355
- type __VLS_PrettifyLocal$1<T> = (T extends any ? { [K in keyof T]: T[K] } : { [K in keyof T as K]: T[K] }) & {};
389
+ type __VLS_PrettifyLocal$1<T> = (T extends any ? { [K in keyof T]: T[K]; } : { [K in keyof T as K]: T[K]; }) & {};
356
390
  //#endregion
357
391
  //#region src/types/not-field.d.ts
358
392
  /** Props for the `NotField` component. */
@@ -416,17 +450,23 @@ interface NotFieldProps {
416
450
  interface NotFieldSlots<TSchema extends ObjectSchema> {
417
451
  /** The default slot receives the full field state and event handlers. */
418
452
  default?: (props: {
419
- /** The dot-separated path to this field. */path: string;
453
+ /** The dot-separated path to this field. */
454
+ path: string;
420
455
  /**
421
456
  * The current value of this field — read-only snapshot for display purposes.
422
457
  * Do not mutate directly or use with `v-model`.
423
458
  * For two-way binding use `v-model="form.values.fieldName"` instead.
424
459
  */
425
- value: any; /** All validation issues for this field from the last validation run. */
426
- errors: Array<StandardSchemaV1.Issue>; /** Whether this field currently has no validation errors. */
427
- isValid: boolean; /** Whether the user has interacted with this field, or the form has been submitted. */
428
- isTouched: boolean; /** Whether this field's current value differs from its initial value. */
429
- isDirty: boolean; /** Whether an async validator is currently running for this field. */
460
+ value: any;
461
+ /** All validation issues for this field from the last validation run. */
462
+ errors: Array<StandardSchemaV1.Issue>;
463
+ /** Whether this field currently has no validation errors. */
464
+ isValid: boolean;
465
+ /** Whether the user has interacted with this field, or the form has been submitted. */
466
+ isTouched: boolean;
467
+ /** Whether this field's current value differs from its initial value. */
468
+ isDirty: boolean;
469
+ /** Whether an async validator is currently running for this field. */
430
470
  isValidating: boolean;
431
471
  /**
432
472
  * Manually triggers validation for this field.
@@ -448,9 +488,13 @@ interface NotFieldSlots<TSchema extends ObjectSchema> {
448
488
  * ```
449
489
  */
450
490
  events: {
451
- /** Triggered when the field loses focus. */onBlur: () => void; /** Triggered on every keystroke or value change. */
452
- onInput: () => void; /** Triggered when the field value is committed. */
453
- onChange: () => void; /** Triggered when the field gains focus. */
491
+ /** Triggered when the field loses focus. */
492
+ onBlur: () => void;
493
+ /** Triggered on every keystroke or value change. */
494
+ onInput: () => void;
495
+ /** Triggered when the field value is committed. */
496
+ onChange: () => void;
497
+ /** Triggered when the field gains focus. */
454
498
  onFocus: () => void;
455
499
  };
456
500
  }) => any;
@@ -466,10 +510,10 @@ declare const __VLS_export$2: <TSchema extends ObjectSchema>(__VLS_props: NonNul
466
510
  slots: NotFieldSlots<TSchema>;
467
511
  emit: {};
468
512
  }>) => import("vue").VNode & {
469
- __ctx?: Awaited<typeof __VLS_setup>;
513
+ __ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
470
514
  };
471
515
  declare const _default$1: typeof __VLS_export$2;
472
- type __VLS_PrettifyLocal<T> = (T extends any ? { [K in keyof T]: T[K] } : { [K in keyof T as K]: T[K] }) & {};
516
+ type __VLS_PrettifyLocal<T> = (T extends any ? { [K in keyof T]: T[K]; } : { [K in keyof T as K]: T[K]; }) & {};
473
517
  //#endregion
474
518
  //#region src/components/not-form.vue.d.ts
475
519
  type __VLS_Slots$1 = NotFormSlots;
@@ -509,7 +553,8 @@ interface NotMessageProps {
509
553
  interface NotMessageSlots {
510
554
  /** The default slot receives the error message context for custom rendering */
511
555
  default?: (props: {
512
- /** The first active validation error message for the specified field */message?: string;
556
+ /** The first active validation error message for the specified field */
557
+ message?: string;
513
558
  }) => any;
514
559
  }
515
560
  //#endregion
@@ -535,4 +580,4 @@ type __VLS_WithSlots<T, S> = T & {
535
580
  */
536
581
  declare function useNotForm<TSchema extends ObjectSchema>(config: UseNotFormConfig<TSchema>): NotFormInstance<TSchema>;
537
582
  //#endregion
538
- export { type DeepPartial, _default as NotArrayField, type NotArrayFieldItem, type NotArrayFieldProps, type NotArrayFieldSlots, _default$1 as NotField, type NotFieldProps, type NotFieldSlots, _default$2 as NotForm, type NotFormInstance, type NotFormProps, type NotFormSlots, _default$3 as NotMessage, type NotMessageProps, type NotMessageSlots, type ObjectSchema, type Paths, type UseNotFormConfig, type ValidationMode, type ValidationTrigger, useNotForm };
583
+ export { type ArrayItemIndexMap, type DeepPartial, _default as NotArrayField, type NotArrayFieldItem, type NotArrayFieldProps, type NotArrayFieldSlots, _default$1 as NotField, type NotFieldProps, type NotFieldSlots, _default$2 as NotForm, type NotFormInstance, type NotFormProps, type NotFormSlots, _default$3 as NotMessage, type NotMessageProps, type NotMessageSlots, type ObjectSchema, type Paths, type Segment, type UseNotFormConfig, type ValidationMode, type ValidationTrigger, useNotForm };
package/dist/index.js CHANGED
@@ -2,7 +2,137 @@ import { computed, createBlock, createCommentVNode, createElementBlock, createTe
2
2
  import { dequal } from "dequal";
3
3
  import { deepKeys, deleteProperty, getProperty, hasProperty, parsePath, setProperty } from "dot-prop";
4
4
  import { klona } from "klona/full";
5
- //#region src/utils/instance-utils.ts
5
+ //#region src/utils/form.ts
6
+ /**
7
+ * Normalizes a validation path segment into a standard property key.
8
+ * @param segment The path segment to normalize.
9
+ * @returns The normalized key.
10
+ */
11
+ function normalizeSegment(segment) {
12
+ if (typeof segment === "object" && segment !== null && "key" in segment) return segment.key;
13
+ return segment;
14
+ }
15
+ /**
16
+ * Compares two individual path segments for equality
17
+ * @param firstSegment The first segment to compare.
18
+ * @param secondSegment The second segment to compare.
19
+ * @returns True if the two segments refer to the same object key or array index.
20
+ */
21
+ function arePathSegmentsEqual(firstSegment, secondSegment) {
22
+ if (typeof firstSegment === "number" || typeof secondSegment === "number") return Number(firstSegment) === Number(secondSegment);
23
+ return firstSegment === secondSegment;
24
+ }
25
+ /**
26
+ * Checks if a validation issue path matches a target field path.
27
+ * @param issuePath The path array from the validation issue.
28
+ * @param targetPath The normalized path to compare against.
29
+ * @returns True if the paths are equivalent.
30
+ */
31
+ function isIssuePathEqual(issuePath, targetPath) {
32
+ if (!issuePath) return false;
33
+ if (issuePath.length !== targetPath.length) return false;
34
+ return issuePath.every((segment, segmentIndex) => {
35
+ const normalizedSegment = normalizeSegment(segment);
36
+ const targetSegment = targetPath[segmentIndex];
37
+ return arePathSegmentsEqual(normalizedSegment, targetSegment);
38
+ });
39
+ }
40
+ //#endregion
41
+ //#region src/utils/array-field.ts
42
+ /**
43
+ * Finds where a path sits inside an array field, if at all.
44
+ * @param pathSegments The full path segments to test, e.g. the parsed segments of `"tags.1.label"`.
45
+ * @param arrayFieldPathSegments The parsed path segments of the array field itself, e.g. the segments of `"tags"`.
46
+ * @returns The item's index and any trailing segments, or undefined if the path isn't inside this array.
47
+ */
48
+ function getArrayItemLocation(pathSegments, arrayFieldPathSegments) {
49
+ if (pathSegments.length <= arrayFieldPathSegments.length) return;
50
+ for (const [segmentIndex, arrayFieldPathSegment] of arrayFieldPathSegments.entries()) if (!arePathSegmentsEqual(pathSegments[segmentIndex], arrayFieldPathSegment)) return;
51
+ const itemIndex = Number(pathSegments[arrayFieldPathSegments.length]);
52
+ if (Number.isNaN(itemIndex)) return;
53
+ return {
54
+ itemIndex,
55
+ remainingSegments: pathSegments.slice(arrayFieldPathSegments.length + 1)
56
+ };
57
+ }
58
+ /**
59
+ * Rebuilds a dot-separated path from an array field's path, an item index, and any trailing segments.
60
+ * @param arrayFieldPath The array field's dot-separated path, e.g. `"tags"`.
61
+ * @param itemIndex The item's index within the array.
62
+ * @param remainingSegments Segments after the index, e.g. `['label']` for a nested field.
63
+ * @returns The reconstructed path, e.g. `"tags.0.label"`.
64
+ */
65
+ function buildArrayItemPath(arrayFieldPath, itemIndex, remainingSegments) {
66
+ return [
67
+ arrayFieldPath,
68
+ itemIndex,
69
+ ...remainingSegments
70
+ ].join(".");
71
+ }
72
+ /**
73
+ * Relocates or drops `form.errors` entries that belong to an item that moved or was removed.
74
+ * Errors on the array field itself (no index segment) are left untouched.
75
+ * @param form The form instance whose errors should be remapped.
76
+ * @param arrayFieldPathSegments The parsed path segments of the array field.
77
+ * @param arrayItemIndexMap Maps each previous item index to its new index, or undefined if removed.
78
+ */
79
+ function remapArrayFieldErrors(form, arrayFieldPathSegments, arrayItemIndexMap) {
80
+ for (let errorIndex = form.errors.length - 1; errorIndex >= 0; errorIndex--) {
81
+ const issue = form.errors[errorIndex];
82
+ if (issue.path) {
83
+ const itemLocation = getArrayItemLocation(issue.path.map((segment) => normalizeSegment(segment)), arrayFieldPathSegments);
84
+ if (itemLocation) {
85
+ const newItemIndex = arrayItemIndexMap(itemLocation.itemIndex);
86
+ if (typeof newItemIndex === "number") form.errors[errorIndex] = {
87
+ ...issue,
88
+ path: [
89
+ ...arrayFieldPathSegments,
90
+ newItemIndex,
91
+ ...itemLocation.remainingSegments
92
+ ]
93
+ };
94
+ else form.errors.splice(errorIndex, 1);
95
+ }
96
+ }
97
+ }
98
+ }
99
+ /**
100
+ * Relocates or drops entries in a `touchedFields`/`dirtyFields` set that belong to an item that moved or was removed.
101
+ * @param fieldPathSet The reactive set of field paths to remap.
102
+ * @param arrayFieldPath The dot-separated path of the array field.
103
+ * @param arrayFieldPathSegments The parsed path segments of the array field.
104
+ * @param arrayItemIndexMap Maps each previous item index to its new index, or undefined if removed.
105
+ */
106
+ function remapArrayFieldPathSet(fieldPathSet, arrayFieldPath, arrayFieldPathSegments, arrayItemIndexMap) {
107
+ const pathsToRemove = [];
108
+ const pathsToAdd = [];
109
+ for (const fieldPath of fieldPathSet) {
110
+ const itemLocation = getArrayItemLocation(parsePath(fieldPath), arrayFieldPathSegments);
111
+ if (itemLocation) {
112
+ pathsToRemove.push(fieldPath);
113
+ const newItemIndex = arrayItemIndexMap(itemLocation.itemIndex);
114
+ if (typeof newItemIndex === "number") pathsToAdd.push(buildArrayItemPath(arrayFieldPath, newItemIndex, itemLocation.remainingSegments));
115
+ }
116
+ }
117
+ for (const fieldPath of pathsToRemove) fieldPathSet.delete(fieldPath);
118
+ for (const fieldPath of pathsToAdd) fieldPathSet.add(fieldPath);
119
+ }
120
+ /**
121
+ * Keeps an array field's errors, touched paths, and dirty paths attached to the item they
122
+ * describe, rather than the array slot that item happened to occupy. Call before applying
123
+ * the mutation to `form.values`.
124
+ * @param form The form instance whose error, touched, and dirty state should be remapped.
125
+ * @param arrayFieldPath The dot-separated path of the array field being mutated, e.g. `"tags"`.
126
+ * @param arrayItemIndexMap Maps each previous item index to its new index, or undefined if removed.
127
+ */
128
+ function remapArrayFieldState(form, arrayFieldPath, arrayItemIndexMap) {
129
+ const arrayFieldPathSegments = parsePath(arrayFieldPath);
130
+ remapArrayFieldErrors(form, arrayFieldPathSegments, arrayItemIndexMap);
131
+ remapArrayFieldPathSet(form.touchedFields, arrayFieldPath, arrayFieldPathSegments, arrayItemIndexMap);
132
+ remapArrayFieldPathSet(form.dirtyFields, arrayFieldPath, arrayFieldPathSegments, arrayItemIndexMap);
133
+ }
134
+ //#endregion
135
+ //#region src/utils/instance.ts
6
136
  /** The injection key for the NotForm instance. */
7
137
  const NOT_FORM_INSTANCE_KEY = Symbol("notform:instance");
8
138
  /**
@@ -137,6 +267,7 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
137
267
  */
138
268
  function prepend(value) {
139
269
  itemKeys.value.unshift(generateKey());
270
+ remapArrayFieldState(form, props.path, (previousIndex) => previousIndex + 1);
140
271
  mutate((current) => current.unshift(value));
141
272
  }
142
273
  /**
@@ -145,6 +276,10 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
145
276
  */
146
277
  function remove(index) {
147
278
  itemKeys.value.splice(index, 1);
279
+ remapArrayFieldState(form, props.path, (previousIndex) => {
280
+ if (previousIndex === index) return;
281
+ return previousIndex > index ? previousIndex - 1 : previousIndex;
282
+ });
148
283
  mutate((current) => current.splice(index, 1));
149
284
  }
150
285
  /**
@@ -154,6 +289,10 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
154
289
  */
155
290
  function insert(index, value) {
156
291
  itemKeys.value.splice(index, 0, generateKey());
292
+ remapArrayFieldState(form, props.path, (previousIndex) => {
293
+ if (previousIndex >= index) return previousIndex + 1;
294
+ return previousIndex;
295
+ });
157
296
  mutate((current) => current.splice(index, 0, value));
158
297
  }
159
298
  /**
@@ -172,13 +311,18 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
172
311
  * @param indexB The second index
173
312
  */
174
313
  function swap(indexA, indexB) {
175
- const tempKey = itemKeys.value[indexA];
314
+ const temporaryKey = itemKeys.value[indexA];
176
315
  itemKeys.value[indexA] = itemKeys.value[indexB];
177
- itemKeys.value[indexB] = tempKey;
316
+ itemKeys.value[indexB] = temporaryKey;
317
+ remapArrayFieldState(form, props.path, (previousIndex) => {
318
+ if (previousIndex === indexA) return indexB;
319
+ if (previousIndex === indexB) return indexA;
320
+ return previousIndex;
321
+ });
178
322
  mutate((current) => {
179
- const temp = current[indexA];
323
+ const temporaryValue = current[indexA];
180
324
  current[indexA] = current[indexB];
181
- current[indexB] = temp;
325
+ current[indexB] = temporaryValue;
182
326
  });
183
327
  }
184
328
  /**
@@ -187,11 +331,20 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
187
331
  * @param to The destination index
188
332
  */
189
333
  function move(from, to) {
190
- const [key] = itemKeys.value.splice(from, 1);
191
- itemKeys.value.splice(to, 0, key);
334
+ const [movedItemKey] = itemKeys.value.splice(from, 1);
335
+ itemKeys.value.splice(to, 0, movedItemKey);
336
+ remapArrayFieldState(form, props.path, (previousIndex) => {
337
+ if (previousIndex === from) return to;
338
+ if (from < to) {
339
+ if (previousIndex > from && previousIndex <= to) return previousIndex - 1;
340
+ return previousIndex;
341
+ }
342
+ if (previousIndex >= to && previousIndex < from) return previousIndex + 1;
343
+ return previousIndex;
344
+ });
192
345
  mutate((current) => {
193
- const [item] = current.splice(from, 1);
194
- current.splice(to, 0, item);
346
+ const [movedItem] = current.splice(from, 1);
347
+ current.splice(to, 0, movedItem);
195
348
  });
196
349
  }
197
350
  onMounted(async () => {
@@ -301,13 +454,13 @@ var not_field_default = /* @__PURE__ */ defineComponent({
301
454
  function onInput() {
302
455
  updateDirty();
303
456
  if (!validateOn.value.onInput) return;
304
- if (form.validationMode.eager && !isValid.value) scheduleValidation();
457
+ if (form.validationMode === "eager" && !isValid.value) scheduleValidation();
305
458
  }
306
459
  /** Handles the change event for the field. */
307
460
  function onChange() {
308
461
  updateDirty();
309
462
  if (!validateOn.value.onChange) return;
310
- if (form.validationMode.eager && !isValid.value) scheduleValidation();
463
+ if (form.validationMode === "eager" && !isValid.value) scheduleValidation();
311
464
  }
312
465
  /** Handles the focus event for the field. */
313
466
  function onFocus() {
@@ -388,33 +541,6 @@ var not_message_default = /* @__PURE__ */ defineComponent({
388
541
  }
389
542
  });
390
543
  //#endregion
391
- //#region src/utils/form-utils.ts
392
- /**
393
- * Normalizes a validation path segment into a standard property key.
394
- * @param segment The path segment to normalize.
395
- * @returns The normalized key.
396
- */
397
- function normalizeSegment(segment) {
398
- if (typeof segment === "object" && segment !== null && "key" in segment) return segment.key;
399
- return segment;
400
- }
401
- /**
402
- * Checks if a validation issue path matches a target field path.
403
- * @param issuePath The path array from the validation issue.
404
- * @param targetPath The normalized path to compare against.
405
- * @returns True if the paths are equivalent.
406
- */
407
- function isIssuePathEqual(issuePath, targetPath) {
408
- if (!issuePath) return false;
409
- if (issuePath.length !== targetPath.length) return false;
410
- return issuePath.every((segment, index) => {
411
- const normalizedSegment = normalizeSegment(segment);
412
- const targetSegment = targetPath[index];
413
- if (typeof normalizedSegment === "number" || typeof targetSegment === "number") return Number(normalizedSegment) === Number(targetSegment);
414
- return normalizedSegment === targetSegment;
415
- });
416
- }
417
- //#endregion
418
544
  //#region src/composables/use-not-form.ts
419
545
  /**
420
546
  * Creates a reactive NotFormInstance for managing form state and validation.
@@ -432,10 +558,7 @@ function useNotForm(config) {
432
558
  onInput: config.validateOn?.onInput ?? true,
433
559
  onMount: config.validateOn?.onMount ?? false
434
560
  };
435
- const validationMode = {
436
- eager: config.validationMode?.eager ?? true,
437
- lazy: config.validationMode?.lazy ?? false
438
- };
561
+ const validationMode = config.validationMode || "eager";
439
562
  const values = reactive(klona(initialValues));
440
563
  const errors = reactive([...initialErrors]);
441
564
  const touchedFields = reactive(/* @__PURE__ */ new Set());
@@ -590,7 +713,7 @@ function useNotForm(config) {
590
713
  const pathSegments = parsePath(path);
591
714
  const staleIndices = [];
592
715
  for (const [index, error] of errors.entries()) if (isIssuePathEqual(error.path, pathSegments)) staleIndices.push(index);
593
- for (let i = staleIndices.length - 1; i >= 0; i--) errors.splice(staleIndices[i], 1);
716
+ for (let index = staleIndices.length - 1; index >= 0; index--) errors.splice(staleIndices[index], 1);
594
717
  if (result?.issues) {
595
718
  const fieldIssues = result.issues.filter((issue) => isIssuePathEqual(issue.path, pathSegments));
596
719
  if (fieldIssues.length > 0) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "notform",
3
3
  "type": "module",
4
- "version": "2.1.3",
4
+ "version": "2.2.1",
5
5
  "private": false,
6
6
  "description": "Vue Forms Without the Friction",
7
7
  "author": "Favour Emeka <favorodera@gmail.com>",
@@ -45,22 +45,22 @@
45
45
  "dependencies": {
46
46
  "@standard-schema/spec": "^1.1.0",
47
47
  "dequal": "^2.0.3",
48
- "dot-prop": "^10.1.0",
48
+ "dot-prop": "^10.2.0",
49
49
  "klona": "^2.0.6"
50
50
  },
51
51
  "devDependencies": {
52
- "@favorodera/eslint-config": "^0.1.4",
53
- "@types/node": "^25.6.0",
54
- "@vitejs/plugin-vue": "^6.0.6",
55
- "@vitest/ui": "^4.1.4",
56
- "@vue/test-utils": "^2.4.6",
57
- "happy-dom": "^20.10.6",
58
- "tsdown": "^0.22.2",
59
- "type-fest": "^5.5.0",
60
- "vite": "^8.0.16",
61
- "vitest": "^4.1.8",
62
- "vue": "^3.5.34",
63
- "vue-tsc": "^3.3.5"
52
+ "@favorodera/eslint-config": "^1.0.4",
53
+ "@types/node": "^26.1.2",
54
+ "@vitejs/plugin-vue": "^6.0.8",
55
+ "@vitest/ui": "^4.1.10",
56
+ "@vue/test-utils": "^2.4.11",
57
+ "happy-dom": "^20.11.1",
58
+ "tsdown": "^0.22.14",
59
+ "type-fest": "^5.7.0",
60
+ "vite": "^8.2.0",
61
+ "vitest": "^4.1.10",
62
+ "vue": "^3.0.0",
63
+ "vue-tsc": "^3.3.9"
64
64
  },
65
65
  "scripts": {
66
66
  "build": "tsdown",