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 +40 -19
- package/dist/index.d.ts +79 -34
- package/dist/index.js +166 -43
- package/package.json +14 -14
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/
|
|
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.
|
|
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
|
|
46
|
-
console.log('Form
|
|
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
|
|
54
|
+
:form
|
|
55
55
|
@submit="form.submit"
|
|
56
|
+
@reset="form.reset()"
|
|
56
57
|
>
|
|
57
|
-
<NotField
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
>
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
>
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
83
|
+
* @default 'eager'
|
|
68
84
|
*/
|
|
69
|
-
validationMode?:
|
|
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. */
|
|
84
|
-
readonly
|
|
85
|
-
|
|
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>>>;
|
|
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;
|
|
127
|
-
|
|
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;
|
|
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;
|
|
152
|
-
|
|
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>;
|
|
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>>>;
|
|
173
|
-
|
|
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. */
|
|
281
|
-
|
|
282
|
-
|
|
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;
|
|
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. */
|
|
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;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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. */
|
|
452
|
-
|
|
453
|
-
|
|
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 */
|
|
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/
|
|
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
|
|
314
|
+
const temporaryKey = itemKeys.value[indexA];
|
|
176
315
|
itemKeys.value[indexA] = itemKeys.value[indexB];
|
|
177
|
-
itemKeys.value[indexB] =
|
|
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
|
|
323
|
+
const temporaryValue = current[indexA];
|
|
180
324
|
current[indexA] = current[indexB];
|
|
181
|
-
current[indexB] =
|
|
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 [
|
|
191
|
-
itemKeys.value.splice(to, 0,
|
|
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 [
|
|
194
|
-
current.splice(to, 0,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
48
|
+
"dot-prop": "^10.2.0",
|
|
49
49
|
"klona": "^2.0.6"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@favorodera/eslint-config": "^0.
|
|
53
|
-
"@types/node": "^
|
|
54
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
55
|
-
"@vitest/ui": "^4.1.
|
|
56
|
-
"@vue/test-utils": "^2.4.
|
|
57
|
-
"happy-dom": "^20.
|
|
58
|
-
"tsdown": "^0.22.
|
|
59
|
-
"type-fest": "^5.
|
|
60
|
-
"vite": "^8.0
|
|
61
|
-
"vitest": "^4.1.
|
|
62
|
-
"vue": "^3.
|
|
63
|
-
"vue-tsc": "^3.3.
|
|
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",
|