notform 2.2.5 → 3.0.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.
- package/README.md +77 -67
- package/dist/index.d.ts +402 -413
- package/dist/index.js +719 -644
- package/package.json +14 -13
package/dist/index.js
CHANGED
|
@@ -1,162 +1,391 @@
|
|
|
1
|
-
import { computed, createBlock, createCommentVNode, createElementBlock, createTextVNode, defineComponent,
|
|
1
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, defineComponent, guardReactiveProps, inject, mergeProps, nextTick, normalizeProps, onMounted, onUnmounted, openBlock, provide, reactive, ref, renderSlot, resolveDynamicComponent, toDisplayString, toValue, unref, watch, withCtx, withModifiers } from "vue";
|
|
2
|
+
import { deepKeys, deleteProperty, getProperty, parsePath, setProperty, stringifyPath } from "dot-prop";
|
|
2
3
|
import { dequal } from "dequal";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
//#region src/utils/form.ts
|
|
4
|
+
import { klona } from "klona";
|
|
5
|
+
//#region src/utils/segments.ts
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @
|
|
9
|
-
* @
|
|
7
|
+
* Unwraps a Standard Schema path segment to a property key.
|
|
8
|
+
* @internal
|
|
9
|
+
* @param segment Plain key or `{ key }`.
|
|
10
|
+
* @returns The underlying property key.
|
|
10
11
|
*/
|
|
11
|
-
function
|
|
12
|
+
function toPropertyKey(segment) {
|
|
12
13
|
if (typeof segment === "object" && segment !== null && "key" in segment) return segment.key;
|
|
13
14
|
return segment;
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
* @
|
|
18
|
-
* @param
|
|
19
|
-
* @
|
|
17
|
+
* Whether two path segments name the same property. `0` and `"0"` are equal.
|
|
18
|
+
* @internal
|
|
19
|
+
* @param segmentA First segment.
|
|
20
|
+
* @param segmentB Second segment.
|
|
21
|
+
* @returns `true` when both refer to the same key.
|
|
20
22
|
*/
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
function areSegmentsEqual(segmentA, segmentB) {
|
|
24
|
+
const keyA = toPropertyKey(segmentA);
|
|
25
|
+
const keyB = toPropertyKey(segmentB);
|
|
26
|
+
if (typeof keyA === "number" && typeof keyB === "string") return Number.isSafeInteger(Number(keyB)) && keyA === Number(keyB);
|
|
27
|
+
if (typeof keyA === "string" && typeof keyB === "number") return Number.isSafeInteger(Number(keyA)) && Number(keyA) === keyB;
|
|
28
|
+
return keyA === keyB;
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
31
|
+
* Whether `pathSegments` names the same field as `scopeSegments`, or a field
|
|
32
|
+
* nested underneath it, at any depth.
|
|
33
|
+
*
|
|
34
|
+
* This is the general **"at-or-under"** test: `isPathWithinScope(path, scope)`
|
|
35
|
+
* is `true` when `path` equals `scope` exactly, or when `path` extends
|
|
36
|
+
* `scope` with one or more extra trailing segments. It underlies both
|
|
37
|
+
* {@linkcode locatePathInArrayField} (which additionally requires at least one
|
|
38
|
+
* extra segment, since it locates a specific item) and the recursive
|
|
39
|
+
* `isValid`/`isTouched`/`isDirty`/`isValidating` aggregates on
|
|
40
|
+
* `<NotArrayField>`, which allow zero extra segments too, since the array's
|
|
41
|
+
* own path should count toward its own aggregate.
|
|
42
|
+
* @template TPathSegment Segment type of the candidate path.
|
|
43
|
+
* @template TScopeSegment Segment type of the scope path.
|
|
44
|
+
* @internal
|
|
45
|
+
* @param pathSegments Candidate path, already split.
|
|
46
|
+
* @param scopeSegments Scope path, already split.
|
|
47
|
+
* @returns `true` when `pathSegments` is `scopeSegments` or a descendant of it.
|
|
30
48
|
*/
|
|
31
|
-
function
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
return issuePath.every((segment, segmentIndex) => {
|
|
35
|
-
const normalizedSegment = normalizeSegment(segment);
|
|
36
|
-
const targetSegment = targetPath[segmentIndex];
|
|
37
|
-
return arePathSegmentsEqual(normalizedSegment, targetSegment);
|
|
38
|
-
});
|
|
49
|
+
function isPathWithinScope(pathSegments, scopeSegments) {
|
|
50
|
+
if (pathSegments.length < scopeSegments.length) return false;
|
|
51
|
+
return scopeSegments.every((scopeSegment, segmentIndex) => areSegmentsEqual(pathSegments[segmentIndex], scopeSegment));
|
|
39
52
|
}
|
|
40
53
|
//#endregion
|
|
41
54
|
//#region src/utils/array-field.ts
|
|
42
55
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @
|
|
56
|
+
* Locates a field path inside an array field, e.g. `users.1.email` under
|
|
57
|
+
* `users`, or `users.1.addresses.0.city` under `users` — `remainingPathSegments`
|
|
58
|
+
* holds whatever comes after the index, however many segments deep that is.
|
|
59
|
+
* @template TPathSegment Segment type of the candidate path.
|
|
60
|
+
* @template TArraySegment Segment type of the array field path.
|
|
61
|
+
* @internal
|
|
62
|
+
* @param pathSegments Candidate path, already split.
|
|
63
|
+
* @param arrayFieldPathSegments Array field path, already split.
|
|
64
|
+
* @returns Item index and leftover segments, or `undefined` when the path is not inside the array.
|
|
47
65
|
*/
|
|
48
|
-
function
|
|
49
|
-
if (pathSegments.length <= arrayFieldPathSegments.length) return;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
function locatePathInArrayField(pathSegments, arrayFieldPathSegments) {
|
|
67
|
+
if (pathSegments.length <= arrayFieldPathSegments.length || !isPathWithinScope(pathSegments, arrayFieldPathSegments)) return;
|
|
68
|
+
const itemIndexSegment = toPropertyKey(pathSegments[arrayFieldPathSegments.length]);
|
|
69
|
+
if (typeof itemIndexSegment !== "number" && typeof itemIndexSegment !== "string") return;
|
|
70
|
+
const itemIndex = Number(itemIndexSegment);
|
|
71
|
+
if (!Number.isSafeInteger(itemIndex) || itemIndex < 0) return;
|
|
53
72
|
return {
|
|
54
|
-
itemIndex,
|
|
55
|
-
|
|
73
|
+
index: itemIndex,
|
|
74
|
+
remainingPathSegments: pathSegments.slice(arrayFieldPathSegments.length + 1)
|
|
56
75
|
};
|
|
57
76
|
}
|
|
58
77
|
/**
|
|
59
|
-
*
|
|
60
|
-
* @
|
|
61
|
-
* @
|
|
62
|
-
* @param
|
|
63
|
-
* @
|
|
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.
|
|
78
|
+
* Rewrites paths in a set after an array mutation. Paths outside the array are left as-is.
|
|
79
|
+
* @template TSchema The form schema.
|
|
80
|
+
* @internal
|
|
81
|
+
* @param paths Touched or dirty path set.
|
|
82
|
+
* @param arraySegments Split array field path.
|
|
83
|
+
* @param remapIndex Previous item index → next index, or `undefined` if removed.
|
|
105
84
|
*/
|
|
106
|
-
function
|
|
85
|
+
function remapPathSet(paths, arraySegments, remapIndex) {
|
|
107
86
|
const pathsToRemove = [];
|
|
108
87
|
const pathsToAdd = [];
|
|
109
|
-
for (const
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
88
|
+
for (const path of paths) {
|
|
89
|
+
const location = locatePathInArrayField(parsePath(path), arraySegments);
|
|
90
|
+
if (!location) continue;
|
|
91
|
+
pathsToRemove.push(path);
|
|
92
|
+
const nextIndex = remapIndex(location.index);
|
|
93
|
+
if (nextIndex === void 0) continue;
|
|
94
|
+
const nextPath = stringifyPath([
|
|
95
|
+
...arraySegments,
|
|
96
|
+
nextIndex,
|
|
97
|
+
...location.remainingPathSegments
|
|
98
|
+
], { preferDotForIndices: true });
|
|
99
|
+
pathsToAdd.push(nextPath);
|
|
116
100
|
}
|
|
117
|
-
for (const
|
|
118
|
-
for (const
|
|
101
|
+
for (const path of pathsToRemove) paths.delete(path);
|
|
102
|
+
for (const path of pathsToAdd) paths.add(path);
|
|
119
103
|
}
|
|
120
104
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
105
|
+
* Moves touched, dirty, and error state with array items after a structural
|
|
106
|
+
* mutation. Because {@linkcode locatePathInArrayField} keeps whatever follows the
|
|
107
|
+
* item's index as `remainingPathSegments`, this already handles state nested
|
|
108
|
+
* arbitrarily deep inside an item — a field on an object item, or an item
|
|
109
|
+
* inside a nested array of its own — not just the item's own top-level path.
|
|
110
|
+
*
|
|
111
|
+
* Validating state is not remapped; generation tracking already drops stale results.
|
|
112
|
+
* @template TSchema The form schema.
|
|
113
|
+
* @internal
|
|
114
|
+
* @param form Full form instance.
|
|
115
|
+
* @param arrayPath Dot path of the array field.
|
|
116
|
+
* @param remapIndex Previous item index → next index, or `undefined` if removed.
|
|
127
117
|
*/
|
|
128
|
-
function remapArrayFieldState(form,
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
118
|
+
function remapArrayFieldState(form, arrayPath, remapIndex) {
|
|
119
|
+
const arraySegments = parsePath(arrayPath);
|
|
120
|
+
remapPathSet(form.touchedFields, arraySegments, remapIndex);
|
|
121
|
+
remapPathSet(form.dirtyFields, arraySegments, remapIndex);
|
|
122
|
+
const remappedErrors = [];
|
|
123
|
+
for (const issue of form.errors) {
|
|
124
|
+
if (!issue.path) {
|
|
125
|
+
remappedErrors.push(issue);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const location = locatePathInArrayField(issue.path, arraySegments);
|
|
129
|
+
if (!location) {
|
|
130
|
+
remappedErrors.push(issue);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const nextIndex = remapIndex(location.index);
|
|
134
|
+
if (nextIndex === void 0) continue;
|
|
135
|
+
remappedErrors.push({
|
|
136
|
+
...issue,
|
|
137
|
+
path: [
|
|
138
|
+
...arraySegments,
|
|
139
|
+
nextIndex,
|
|
140
|
+
...location.remainingPathSegments
|
|
141
|
+
]
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
form.replaceErrors(remappedErrors);
|
|
133
145
|
}
|
|
134
146
|
//#endregion
|
|
135
147
|
//#region src/utils/instance.ts
|
|
136
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* Injection key for the full {@linkcode NotFormInstance}.
|
|
150
|
+
* @internal
|
|
151
|
+
*/
|
|
137
152
|
const NOT_FORM_INSTANCE_KEY = Symbol("notform:instance");
|
|
138
153
|
/**
|
|
139
|
-
* Provides the
|
|
140
|
-
* @template TSchema The
|
|
141
|
-
* @
|
|
154
|
+
* Provides the full form instance to descendants.
|
|
155
|
+
* @template TSchema The form schema.
|
|
156
|
+
* @internal
|
|
157
|
+
* @param instance Public API or full instance.
|
|
142
158
|
*/
|
|
143
159
|
function provideNotFormInstance(instance) {
|
|
144
|
-
provide(NOT_FORM_INSTANCE_KEY, instance);
|
|
160
|
+
provide(NOT_FORM_INSTANCE_KEY, toNotFormInstance(instance));
|
|
161
|
+
}
|
|
162
|
+
function toNotFormInstance(form) {
|
|
163
|
+
return form;
|
|
145
164
|
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/composables/use-not-form-instance.ts
|
|
146
167
|
/**
|
|
147
|
-
*
|
|
168
|
+
* Resolves the full form instance from `explicitInstance` or inject.
|
|
148
169
|
* @template TSchema The form schema.
|
|
149
|
-
* @
|
|
150
|
-
* @
|
|
170
|
+
* @internal
|
|
171
|
+
* @param explicitInstance Optional `form` prop, preferred over inject.
|
|
172
|
+
* @returns The resolved instance.
|
|
173
|
+
* @throws If neither prop nor inject is available.
|
|
151
174
|
*/
|
|
152
175
|
function useNotFormInstance(explicitInstance) {
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
if (!
|
|
156
|
-
[NotForm] No
|
|
157
|
-
Wrap with <NotForm :form="
|
|
158
|
-
|
|
159
|
-
return
|
|
176
|
+
const injectedInstance = inject(NOT_FORM_INSTANCE_KEY, void 0);
|
|
177
|
+
const resolvedInstance = toNotFormInstance(explicitInstance) ?? injectedInstance;
|
|
178
|
+
if (!resolvedInstance) throw new Error(`
|
|
179
|
+
[NotForm] No NotForm instance found.
|
|
180
|
+
Wrap with <NotForm :form="[instance]"> or pass :form="[instance]" directly to the component.
|
|
181
|
+
`);
|
|
182
|
+
return resolvedInstance;
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/composables/use-not-array-field.ts
|
|
186
|
+
/**
|
|
187
|
+
* Array-field state, stable item keys, and mutation helpers for `<NotArrayField>`.
|
|
188
|
+
*
|
|
189
|
+
* `itemKeys` is the only source of `item.key`. Length-only external changes
|
|
190
|
+
* pad or trim keys from the end; use the mutation helpers to keep identity
|
|
191
|
+
* through insert, remove, swap, and move.
|
|
192
|
+
*
|
|
193
|
+
* `isValid`/`isTouched`/`isDirty`/`isValidating` are recursive: they reflect
|
|
194
|
+
* the array field's own path plus every path nested underneath it, at any
|
|
195
|
+
* depth — an item's own value, a field inside an object item, or an item
|
|
196
|
+
* inside a nested array. `errors`, by contrast, is intentionally exact: it
|
|
197
|
+
* only ever reflects issues reported at the array field's own path, the same
|
|
198
|
+
* way `<NotField>`'s `errors` only reflects issues at that one field's path.
|
|
199
|
+
* @template TSchema The form schema.
|
|
200
|
+
* @template TItemSchema Schema used only to type mutation values.
|
|
201
|
+
* @internal
|
|
202
|
+
* @param props Array field path, optional form, and optional item schema.
|
|
203
|
+
* @returns Slot state for `<NotArrayField>`.
|
|
204
|
+
*/
|
|
205
|
+
function useNotArrayField(props) {
|
|
206
|
+
const form = useNotFormInstance(props.form);
|
|
207
|
+
/** Monotonic id used by {@linkcode createItemKey}. */
|
|
208
|
+
let nextItemKeyId = 0;
|
|
209
|
+
/** Stable identity per current array index; mutated in lockstep with values. */
|
|
210
|
+
const itemKeys = ref([]);
|
|
211
|
+
/** Live array at `props.path`, or `[]` when the path is missing or not an array. */
|
|
212
|
+
const arrayValue = computed(() => {
|
|
213
|
+
const value = getProperty(form.values, props.path);
|
|
214
|
+
return Array.isArray(value) ? value : [];
|
|
215
|
+
});
|
|
216
|
+
/** Slot items: current index, stored key, and index-based path. */
|
|
217
|
+
const items = computed(() => arrayValue.value.map((_, index) => ({
|
|
218
|
+
index,
|
|
219
|
+
key: itemKeys.value[index] ?? "",
|
|
220
|
+
path: `${props.path}.${index}`
|
|
221
|
+
})));
|
|
222
|
+
/** `props.path`, pre-split into segments, for the scope checks below. */
|
|
223
|
+
const arrayPathSegments = computed(() => parsePath(props.path));
|
|
224
|
+
/** Issues reported exactly at this array field's own path — never issues from items. */
|
|
225
|
+
const errors = computed(() => form.getFieldErrors(props.path));
|
|
226
|
+
/**
|
|
227
|
+
* Whether the array field's own path, or any path nested under it — an
|
|
228
|
+
* item, a field inside an object item, or an item inside a nested array —
|
|
229
|
+
* has an active issue. Unlike `errors` above, this recurses to any depth.
|
|
230
|
+
* Issues without a `path` are never attributed to a specific field, so
|
|
231
|
+
* they don't affect this — only the top-level `form.isValid` reflects them.
|
|
232
|
+
*/
|
|
233
|
+
const isValid = computed(() => {
|
|
234
|
+
return form.errors.every((issue) => issue.path === void 0 || !isPathWithinScope(issue.path, arrayPathSegments.value));
|
|
235
|
+
});
|
|
236
|
+
/** Whether the array field's own path, or any path nested under it, has been touched. */
|
|
237
|
+
const isTouched = computed(() => {
|
|
238
|
+
return [...form.touchedFields].some((touchedPath) => isPathWithinScope(parsePath(touchedPath), arrayPathSegments.value));
|
|
239
|
+
});
|
|
240
|
+
/** Whether the array field's own path, or any path nested under it, differs from the baseline. */
|
|
241
|
+
const isDirty = computed(() => {
|
|
242
|
+
return [...form.dirtyFields].some((dirtyPath) => isPathWithinScope(parsePath(dirtyPath), arrayPathSegments.value));
|
|
243
|
+
});
|
|
244
|
+
/** Whether the array field's own path, or any path nested under it, is currently validating. */
|
|
245
|
+
const isValidating = computed(() => {
|
|
246
|
+
return [...form.validatingFields].some((validatingPath) => isPathWithinScope(parsePath(validatingPath), arrayPathSegments.value));
|
|
247
|
+
});
|
|
248
|
+
/**
|
|
249
|
+
* Allocates a unique key for one array item.
|
|
250
|
+
* @returns A key that never repeats in this field instance.
|
|
251
|
+
*/
|
|
252
|
+
function createItemKey() {
|
|
253
|
+
return `notform-array-item-${nextItemKeyId++}`;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Pads or trims `itemKeys` from the end so it matches `targetLength`.
|
|
257
|
+
* @param targetLength Desired key count.
|
|
258
|
+
*/
|
|
259
|
+
function syncItemKeysToLength(targetLength) {
|
|
260
|
+
while (itemKeys.value.length < targetLength) itemKeys.value.push(createItemKey());
|
|
261
|
+
if (itemKeys.value.length > targetLength) itemKeys.value.length = targetLength;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Returns the mutable array at `props.path`, creating an empty array if needed.
|
|
265
|
+
* @returns The live array stored on the form.
|
|
266
|
+
*/
|
|
267
|
+
function getOrCreateArrayValue() {
|
|
268
|
+
const existingArray = getProperty(form.values, props.path);
|
|
269
|
+
if (Array.isArray(existingArray)) return existingArray;
|
|
270
|
+
const createdArray = [];
|
|
271
|
+
setProperty(form.values, props.path, createdArray);
|
|
272
|
+
return createdArray;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Moves one element inside `target` from `fromIndex` to `toIndex`.
|
|
276
|
+
* @template TItem Element type.
|
|
277
|
+
* @param target Array to mutate in place.
|
|
278
|
+
* @param fromIndex Index of the element to move.
|
|
279
|
+
* @param toIndex Destination index, clamped to the array bounds.
|
|
280
|
+
*/
|
|
281
|
+
function moveArrayItem(target, fromIndex, toIndex) {
|
|
282
|
+
if (fromIndex === toIndex || fromIndex < 0 || fromIndex >= target.length) return;
|
|
283
|
+
const clampedToIndex = Math.max(0, Math.min(toIndex, target.length - 1));
|
|
284
|
+
const [movedItem] = target.splice(fromIndex, 1);
|
|
285
|
+
target.splice(clampedToIndex, 0, movedItem);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Appends `value` and a new key at the end of the array.
|
|
289
|
+
* @param value Item to append.
|
|
290
|
+
*/
|
|
291
|
+
function append(value) {
|
|
292
|
+
getOrCreateArrayValue().push(value);
|
|
293
|
+
itemKeys.value.push(createItemKey());
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Inserts `value` at the start and remaps later item state — at any depth
|
|
297
|
+
* underneath each item — up by one.
|
|
298
|
+
* @param value Item to prepend.
|
|
299
|
+
*/
|
|
300
|
+
function prepend(value) {
|
|
301
|
+
getOrCreateArrayValue().unshift(value);
|
|
302
|
+
itemKeys.value.unshift(createItemKey());
|
|
303
|
+
remapArrayFieldState(form, props.path, (previousIndex) => previousIndex + 1);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Inserts `value` at `index` and remaps later item state — at any depth
|
|
307
|
+
* underneath each item — up by one.
|
|
308
|
+
* @param index Insertion index.
|
|
309
|
+
* @param value Item to insert.
|
|
310
|
+
*/
|
|
311
|
+
function insert(index, value) {
|
|
312
|
+
getOrCreateArrayValue().splice(index, 0, value);
|
|
313
|
+
itemKeys.value.splice(index, 0, createItemKey());
|
|
314
|
+
remapArrayFieldState(form, props.path, (previousIndex) => previousIndex >= index ? previousIndex + 1 : previousIndex);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Removes the item at `index` and remaps later item state — at any depth
|
|
318
|
+
* underneath each item — down by one. State nested under the removed
|
|
319
|
+
* item's own path, at any depth, is discarded along with it.
|
|
320
|
+
* @param index Index to remove.
|
|
321
|
+
*/
|
|
322
|
+
function remove(index) {
|
|
323
|
+
getOrCreateArrayValue().splice(index, 1);
|
|
324
|
+
itemKeys.value.splice(index, 1);
|
|
325
|
+
remapArrayFieldState(form, props.path, (previousIndex) => {
|
|
326
|
+
if (previousIndex === index) return;
|
|
327
|
+
return previousIndex > index ? previousIndex - 1 : previousIndex;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Replaces the value at `index` without changing its key.
|
|
332
|
+
* @param index Index to update.
|
|
333
|
+
* @param value New item value.
|
|
334
|
+
*/
|
|
335
|
+
function update(index, value) {
|
|
336
|
+
getOrCreateArrayValue()[index] = value;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Swaps two items, moving keys and form state — at any depth underneath
|
|
340
|
+
* each item — with them.
|
|
341
|
+
* @param indexA First index.
|
|
342
|
+
* @param indexB Second index.
|
|
343
|
+
*/
|
|
344
|
+
function swap(indexA, indexB) {
|
|
345
|
+
const array = getOrCreateArrayValue();
|
|
346
|
+
[array[indexA], array[indexB]] = [array[indexB], array[indexA]];
|
|
347
|
+
[itemKeys.value[indexA], itemKeys.value[indexB]] = [itemKeys.value[indexB], itemKeys.value[indexA]];
|
|
348
|
+
remapArrayFieldState(form, props.path, (previousIndex) => {
|
|
349
|
+
if (previousIndex === indexA) return indexB;
|
|
350
|
+
if (previousIndex === indexB) return indexA;
|
|
351
|
+
return previousIndex;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Moves one item to `toIndex`, shifting neighbors and remapping form
|
|
356
|
+
* state — at any depth underneath each item.
|
|
357
|
+
* @param fromIndex Current index.
|
|
358
|
+
* @param toIndex Destination index.
|
|
359
|
+
*/
|
|
360
|
+
function move(fromIndex, toIndex) {
|
|
361
|
+
moveArrayItem(getOrCreateArrayValue(), fromIndex, toIndex);
|
|
362
|
+
moveArrayItem(itemKeys.value, fromIndex, toIndex);
|
|
363
|
+
remapArrayFieldState(form, props.path, (previousIndex) => {
|
|
364
|
+
if (previousIndex === fromIndex) return toIndex;
|
|
365
|
+
if (fromIndex < toIndex) return previousIndex > fromIndex && previousIndex <= toIndex ? previousIndex - 1 : previousIndex;
|
|
366
|
+
return previousIndex >= toIndex && previousIndex < fromIndex ? previousIndex + 1 : previousIndex;
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
watch(() => arrayValue.value.length, (arrayLength) => {
|
|
370
|
+
if (itemKeys.value.length === arrayLength) return;
|
|
371
|
+
syncItemKeysToLength(arrayLength);
|
|
372
|
+
}, { immediate: true });
|
|
373
|
+
return reactive({
|
|
374
|
+
append,
|
|
375
|
+
errors,
|
|
376
|
+
insert,
|
|
377
|
+
isDirty,
|
|
378
|
+
isTouched,
|
|
379
|
+
isValid,
|
|
380
|
+
isValidating,
|
|
381
|
+
items,
|
|
382
|
+
move,
|
|
383
|
+
path: computed(() => props.path),
|
|
384
|
+
prepend,
|
|
385
|
+
remove,
|
|
386
|
+
swap,
|
|
387
|
+
update
|
|
388
|
+
});
|
|
160
389
|
}
|
|
161
390
|
//#endregion
|
|
162
391
|
//#region src/components/not-array-field.vue
|
|
@@ -164,7 +393,7 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
|
|
|
164
393
|
__name: "not-array-field",
|
|
165
394
|
props: {
|
|
166
395
|
path: {
|
|
167
|
-
type: String,
|
|
396
|
+
type: [String, Object],
|
|
168
397
|
required: true
|
|
169
398
|
},
|
|
170
399
|
itemSchema: {
|
|
@@ -172,357 +401,137 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
|
|
|
172
401
|
required: false
|
|
173
402
|
},
|
|
174
403
|
form: {
|
|
175
|
-
type: null,
|
|
176
|
-
required: false
|
|
177
|
-
},
|
|
178
|
-
validateOn: {
|
|
179
404
|
type: Object,
|
|
180
405
|
required: false
|
|
181
406
|
}
|
|
182
407
|
},
|
|
183
408
|
setup(__props) {
|
|
184
|
-
const
|
|
185
|
-
const form = useNotFormInstance(props.form);
|
|
186
|
-
const isValidating = ref(false);
|
|
187
|
-
const validatingCount = ref(0);
|
|
188
|
-
/** Counter to ensure absolute uniqueness for generated keys. */
|
|
189
|
-
const keyCounter = ref(0);
|
|
190
|
-
/** Stable keys per item that survive reorders, removals, and inserts. */
|
|
191
|
-
const itemKeys = ref((() => {
|
|
192
|
-
const initial = getProperty(form.values, props.path);
|
|
193
|
-
const length = Array.isArray(initial) ? initial.length : 0;
|
|
194
|
-
return Array.from({ length }, () => `${props.path}-${keyCounter.value++}`);
|
|
195
|
-
})());
|
|
196
|
-
const validateOn = computed(() => ({
|
|
197
|
-
onChange: props.validateOn?.onChange ?? form.validateOn.onChange,
|
|
198
|
-
onMount: props.validateOn?.onMount ?? form.validateOn.onMount
|
|
199
|
-
}));
|
|
200
|
-
const array = computed(() => {
|
|
201
|
-
const value = getProperty(form.values, props.path);
|
|
202
|
-
return Array.isArray(value) ? value : [];
|
|
203
|
-
});
|
|
204
|
-
const items = computed(() => {
|
|
205
|
-
return array.value.map((_, index) => ({
|
|
206
|
-
index,
|
|
207
|
-
key: itemKeys.value[index] ?? `${props.path}-fallback-${index}`,
|
|
208
|
-
path: `${props.path}.${index}`
|
|
209
|
-
}));
|
|
210
|
-
});
|
|
211
|
-
const errors = computed(() => form.getFieldErrors(props.path));
|
|
212
|
-
const isValid = computed(() => errors.value.length === 0);
|
|
213
|
-
const isTouched = computed(() => {
|
|
214
|
-
return form.touchedFields.has(props.path) || [...form.touchedFields].some((path) => path.startsWith(`${props.path}.`));
|
|
215
|
-
});
|
|
216
|
-
const isDirty = computed(() => {
|
|
217
|
-
return form.dirtyFields.has(props.path) || [...form.dirtyFields].some((path) => path.startsWith(`${props.path}.`));
|
|
218
|
-
});
|
|
219
|
-
/**
|
|
220
|
-
* Starts a local field validation execution.
|
|
221
|
-
*/
|
|
222
|
-
function beginValidation() {
|
|
223
|
-
validatingCount.value++;
|
|
224
|
-
isValidating.value = true;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Ends a local field validation execution.
|
|
228
|
-
*/
|
|
229
|
-
function endValidation() {
|
|
230
|
-
validatingCount.value--;
|
|
231
|
-
if (validatingCount.value === 0) isValidating.value = false;
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Generates a unique key string using the field path and counter.
|
|
235
|
-
* @returns A unique key string.
|
|
236
|
-
*/
|
|
237
|
-
function generateKey() {
|
|
238
|
-
return `${props.path}-${keyCounter.value++}`;
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Re-aligns itemKeys with current array length.
|
|
242
|
-
*
|
|
243
|
-
* Used mostly after external resets.
|
|
244
|
-
*/
|
|
245
|
-
function syncKeys() {
|
|
246
|
-
const arrayLength = array.value.length;
|
|
247
|
-
if (itemKeys.value.length > arrayLength) itemKeys.value.length = arrayLength;
|
|
248
|
-
else while (itemKeys.value.length < arrayLength) itemKeys.value.push(generateKey());
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Applies an array update, syncs form dirty/touched state, and triggers validation.
|
|
252
|
-
* @param updater Function that modifies the array.
|
|
253
|
-
*/
|
|
254
|
-
function mutate(updater) {
|
|
255
|
-
const current = [...array.value];
|
|
256
|
-
updater(current);
|
|
257
|
-
setProperty(form.values, props.path, current);
|
|
258
|
-
form.touchField(props.path);
|
|
259
|
-
if (dequal(current, getProperty(form.initialValues, props.path))) form.dirtyFields.delete(props.path);
|
|
260
|
-
else form.dirtyField(props.path);
|
|
261
|
-
if (validateOn.value.onChange) validate();
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Validates the array field.
|
|
265
|
-
* @returns A promise that resolves to the validation result.
|
|
266
|
-
*/
|
|
267
|
-
async function validate() {
|
|
268
|
-
beginValidation();
|
|
269
|
-
try {
|
|
270
|
-
return await form.validateField(props.path);
|
|
271
|
-
} finally {
|
|
272
|
-
endValidation();
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Appends an item to the array.
|
|
277
|
-
* @param value The value to append.
|
|
278
|
-
*/
|
|
279
|
-
function append(value) {
|
|
280
|
-
itemKeys.value.push(generateKey());
|
|
281
|
-
mutate((current) => {
|
|
282
|
-
current.push(value);
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Prepends an item to the array.
|
|
287
|
-
* @param value The value to prepend.
|
|
288
|
-
*/
|
|
289
|
-
function prepend(value) {
|
|
290
|
-
itemKeys.value.unshift(generateKey());
|
|
291
|
-
remapArrayFieldState(form, props.path, (previousIndex) => previousIndex + 1);
|
|
292
|
-
mutate((current) => {
|
|
293
|
-
current.unshift(value);
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Removes an item from the array.
|
|
298
|
-
* @param index The item index to remove.
|
|
299
|
-
*/
|
|
300
|
-
function remove(index) {
|
|
301
|
-
itemKeys.value.splice(index, 1);
|
|
302
|
-
remapArrayFieldState(form, props.path, (previousIndex) => {
|
|
303
|
-
if (previousIndex === index) return;
|
|
304
|
-
return previousIndex > index ? previousIndex - 1 : previousIndex;
|
|
305
|
-
});
|
|
306
|
-
mutate((current) => current.splice(index, 1));
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Inserts an item at a specific index.
|
|
310
|
-
* @param index The insertion index.
|
|
311
|
-
* @param value The value to insert.
|
|
312
|
-
*/
|
|
313
|
-
function insert(index, value) {
|
|
314
|
-
itemKeys.value.splice(index, 0, generateKey());
|
|
315
|
-
remapArrayFieldState(form, props.path, (previousIndex) => {
|
|
316
|
-
if (previousIndex >= index) return previousIndex + 1;
|
|
317
|
-
return previousIndex;
|
|
318
|
-
});
|
|
319
|
-
mutate((current) => current.splice(index, 0, value));
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Updates an item at a specific index.
|
|
323
|
-
* @param index The item index to update.
|
|
324
|
-
* @param value The new item value.
|
|
325
|
-
*/
|
|
326
|
-
function update(index, value) {
|
|
327
|
-
mutate((current) => {
|
|
328
|
-
current[index] = value;
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Swaps two items in the array.
|
|
333
|
-
* @param indexA The first item index.
|
|
334
|
-
* @param indexB The second item index.
|
|
335
|
-
*/
|
|
336
|
-
function swap(indexA, indexB) {
|
|
337
|
-
const temporaryKey = itemKeys.value[indexA];
|
|
338
|
-
itemKeys.value[indexA] = itemKeys.value[indexB];
|
|
339
|
-
itemKeys.value[indexB] = temporaryKey;
|
|
340
|
-
remapArrayFieldState(form, props.path, (previousIndex) => {
|
|
341
|
-
if (previousIndex === indexA) return indexB;
|
|
342
|
-
if (previousIndex === indexB) return indexA;
|
|
343
|
-
return previousIndex;
|
|
344
|
-
});
|
|
345
|
-
mutate((current) => {
|
|
346
|
-
const temporaryValue = current[indexA];
|
|
347
|
-
current[indexA] = current[indexB];
|
|
348
|
-
current[indexB] = temporaryValue;
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Moves an item from one index to another.
|
|
353
|
-
* @param from The source index.
|
|
354
|
-
* @param to The destination index.
|
|
355
|
-
*/
|
|
356
|
-
function move(from, to) {
|
|
357
|
-
const [movedItemKey] = itemKeys.value.splice(from, 1);
|
|
358
|
-
itemKeys.value.splice(to, 0, movedItemKey);
|
|
359
|
-
remapArrayFieldState(form, props.path, (previousIndex) => {
|
|
360
|
-
if (previousIndex === from) return to;
|
|
361
|
-
if (from < to) {
|
|
362
|
-
if (previousIndex > from && previousIndex <= to) return previousIndex - 1;
|
|
363
|
-
return previousIndex;
|
|
364
|
-
}
|
|
365
|
-
if (previousIndex >= to && previousIndex < from) return previousIndex + 1;
|
|
366
|
-
return previousIndex;
|
|
367
|
-
});
|
|
368
|
-
mutate((current) => {
|
|
369
|
-
const [movedItem] = current.splice(from, 1);
|
|
370
|
-
current.splice(to, 0, movedItem);
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
onMounted(async () => {
|
|
374
|
-
await nextTick();
|
|
375
|
-
if (validateOn.value.onMount) validate();
|
|
376
|
-
});
|
|
377
|
-
watch(() => array.value.length, syncKeys);
|
|
409
|
+
const arrayField = useNotArrayField(__props);
|
|
378
410
|
return (_ctx, _cache) => {
|
|
379
|
-
return renderSlot(_ctx.$slots, "default",
|
|
380
|
-
append,
|
|
381
|
-
errors: errors.value,
|
|
382
|
-
insert,
|
|
383
|
-
isDirty: isDirty.value,
|
|
384
|
-
isTouched: isTouched.value,
|
|
385
|
-
isValid: isValid.value,
|
|
386
|
-
isValidating: isValidating.value,
|
|
387
|
-
items: items.value,
|
|
388
|
-
move,
|
|
389
|
-
path: __props.path,
|
|
390
|
-
prepend,
|
|
391
|
-
remove,
|
|
392
|
-
swap,
|
|
393
|
-
update,
|
|
394
|
-
validate
|
|
395
|
-
});
|
|
411
|
+
return renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps(unref(arrayField))));
|
|
396
412
|
};
|
|
397
413
|
}
|
|
398
414
|
});
|
|
399
415
|
//#endregion
|
|
416
|
+
//#region src/composables/use-not-field.ts
|
|
417
|
+
/**
|
|
418
|
+
* Field state, validation, and mutation helpers for `<NotField>`.
|
|
419
|
+
* @template TSchema The form schema.
|
|
420
|
+
* @internal
|
|
421
|
+
* @param props Path and optional form and validation triggers.
|
|
422
|
+
* @returns Slot state for `<NotField>`.
|
|
423
|
+
*/
|
|
424
|
+
function useNotField(props) {
|
|
425
|
+
const form = useNotFormInstance(props.form);
|
|
426
|
+
const errors = computed(() => form.getFieldErrors(props.path));
|
|
427
|
+
const isValid = computed(() => errors.value.length === 0);
|
|
428
|
+
const isTouched = computed(() => form.touchedFields.has(props.path));
|
|
429
|
+
const isDirty = computed(() => form.dirtyFields.has(props.path));
|
|
430
|
+
const isValidating = computed(() => form.validatingFields.has(props.path));
|
|
431
|
+
const validateOn = computed(() => ({
|
|
432
|
+
onBlur: true,
|
|
433
|
+
onChange: true,
|
|
434
|
+
...props.validateOn
|
|
435
|
+
}));
|
|
436
|
+
let debounceTimer;
|
|
437
|
+
/** Clears a pending debounce timer. */
|
|
438
|
+
function clearDebounce() {
|
|
439
|
+
if (debounceTimer === void 0) return;
|
|
440
|
+
clearTimeout(debounceTimer);
|
|
441
|
+
debounceTimer = void 0;
|
|
442
|
+
}
|
|
443
|
+
/** Runs field validation immediately, or after `debounce` ms. */
|
|
444
|
+
function scheduleValidation() {
|
|
445
|
+
if (!props.debounce) {
|
|
446
|
+
form.validateField(props.path);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
clearDebounce();
|
|
450
|
+
debounceTimer = setTimeout(() => {
|
|
451
|
+
form.validateField(props.path);
|
|
452
|
+
}, props.debounce);
|
|
453
|
+
}
|
|
454
|
+
/** Marks touched and validates on blur when enabled. */
|
|
455
|
+
function onBlur() {
|
|
456
|
+
clearDebounce();
|
|
457
|
+
form.markFieldAsTouched(props.path);
|
|
458
|
+
if (validateOn.value?.onBlur) form.validateField(props.path);
|
|
459
|
+
}
|
|
460
|
+
/** Syncs dirty and, in eager mode, revalidates on input while invalid. */
|
|
461
|
+
function onInput() {
|
|
462
|
+
form.syncDirtyState(props.path);
|
|
463
|
+
if (!validateOn.value?.onInput) return;
|
|
464
|
+
if (props.validationMode === "eager" && !isValid.value) scheduleValidation();
|
|
465
|
+
}
|
|
466
|
+
/** Syncs dirty and, in eager mode, revalidates on change while invalid. */
|
|
467
|
+
function onChange() {
|
|
468
|
+
form.syncDirtyState(props.path);
|
|
469
|
+
if (!validateOn.value?.onChange) return;
|
|
470
|
+
if (props.validationMode === "eager" && !isValid.value) scheduleValidation();
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Validates on mount when `validateOn.onMount` is set. This is the only
|
|
474
|
+
* trigger not exposed through `events` below, since there's no DOM event
|
|
475
|
+
* to bind — it runs on its own via the `onMounted` hook further down.
|
|
476
|
+
*/
|
|
477
|
+
function onMount() {
|
|
478
|
+
if (validateOn.value?.onMount) form.validateField(props.path);
|
|
479
|
+
}
|
|
480
|
+
const events = {
|
|
481
|
+
onBlur,
|
|
482
|
+
onChange,
|
|
483
|
+
onInput
|
|
484
|
+
};
|
|
485
|
+
onMounted(async () => {
|
|
486
|
+
await nextTick();
|
|
487
|
+
onMount();
|
|
488
|
+
});
|
|
489
|
+
onUnmounted(() => {
|
|
490
|
+
clearDebounce();
|
|
491
|
+
});
|
|
492
|
+
return reactive({
|
|
493
|
+
errors,
|
|
494
|
+
events,
|
|
495
|
+
isDirty,
|
|
496
|
+
isTouched,
|
|
497
|
+
isValid,
|
|
498
|
+
isValidating,
|
|
499
|
+
path: computed(() => props.path)
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
//#endregion
|
|
400
503
|
//#region src/components/not-field.vue
|
|
401
504
|
var not_field_default = /* @__PURE__ */ defineComponent({
|
|
402
505
|
__name: "not-field",
|
|
403
506
|
props: {
|
|
404
507
|
path: {
|
|
405
|
-
type: String,
|
|
508
|
+
type: [String, Object],
|
|
406
509
|
required: true
|
|
407
510
|
},
|
|
408
511
|
form: {
|
|
409
|
-
type:
|
|
512
|
+
type: Object,
|
|
410
513
|
required: false
|
|
411
514
|
},
|
|
412
515
|
validateOn: {
|
|
413
516
|
type: Object,
|
|
414
|
-
required: false
|
|
517
|
+
required: false,
|
|
518
|
+
default: void 0
|
|
519
|
+
},
|
|
520
|
+
validationMode: {
|
|
521
|
+
type: String,
|
|
522
|
+
required: false,
|
|
523
|
+
default: "eager"
|
|
415
524
|
},
|
|
416
525
|
debounce: {
|
|
417
526
|
type: Number,
|
|
418
|
-
required: false
|
|
527
|
+
required: false,
|
|
528
|
+
default: void 0
|
|
419
529
|
}
|
|
420
530
|
},
|
|
421
531
|
setup(__props) {
|
|
422
|
-
const
|
|
423
|
-
const form = useNotFormInstance(props.form);
|
|
424
|
-
const isValidating = ref(false);
|
|
425
|
-
const validatingCount = ref(0);
|
|
426
|
-
/** Timer handle for the current pending debounced validation. */
|
|
427
|
-
const debounceTimer = ref();
|
|
428
|
-
/** Merges per-field overrides with the form-wide validation config. */
|
|
429
|
-
const validateOn = computed(() => ({
|
|
430
|
-
...form.validateOn,
|
|
431
|
-
...props.validateOn
|
|
432
|
-
}));
|
|
433
|
-
const value = computed(() => getProperty(form.values, props.path));
|
|
434
|
-
const errors = computed(() => form.getFieldErrors(props.path));
|
|
435
|
-
const isValid = computed(() => errors.value.length === 0);
|
|
436
|
-
const isTouched = computed(() => form.touchedFields.has(props.path));
|
|
437
|
-
const isDirty = computed(() => form.dirtyFields.has(props.path));
|
|
438
|
-
/** Starts a local field validation execution. */
|
|
439
|
-
function beginValidation() {
|
|
440
|
-
validatingCount.value++;
|
|
441
|
-
isValidating.value = true;
|
|
442
|
-
}
|
|
443
|
-
/** Ends a local field validation execution. */
|
|
444
|
-
function endValidation() {
|
|
445
|
-
validatingCount.value--;
|
|
446
|
-
if (validatingCount.value === 0) isValidating.value = false;
|
|
447
|
-
}
|
|
448
|
-
/** Cancels pending debounced validation on blur/unmount to prevent rogue execution. */
|
|
449
|
-
function clearDebounce() {
|
|
450
|
-
if (debounceTimer.value === void 0) return;
|
|
451
|
-
clearTimeout(debounceTimer.value);
|
|
452
|
-
debounceTimer.value = void 0;
|
|
453
|
-
}
|
|
454
|
-
/** Compares current value against baseline to sync dirty state tracking. */
|
|
455
|
-
function updateDirty() {
|
|
456
|
-
if (dequal(value.value, getProperty(form.initialValues, props.path))) form.dirtyFields.delete(props.path);
|
|
457
|
-
else form.dirtyField(props.path);
|
|
458
|
-
}
|
|
459
|
-
/** Replaces pending debounced validation with a new timer, or runs immediately when no debounce is configured. */
|
|
460
|
-
function scheduleValidation() {
|
|
461
|
-
if (!props.debounce) {
|
|
462
|
-
validate();
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
clearDebounce();
|
|
466
|
-
debounceTimer.value = setTimeout(validate, props.debounce);
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Validates the field and returns the validation result.
|
|
470
|
-
* @returns A promise that resolves to the validation result.
|
|
471
|
-
*/
|
|
472
|
-
async function validate() {
|
|
473
|
-
beginValidation();
|
|
474
|
-
try {
|
|
475
|
-
return await form.validateField(props.path);
|
|
476
|
-
} finally {
|
|
477
|
-
endValidation();
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
/** Handles the blur event for the field. */
|
|
481
|
-
function onBlur() {
|
|
482
|
-
clearDebounce();
|
|
483
|
-
form.touchField(props.path);
|
|
484
|
-
if (validateOn.value.onBlur) validate();
|
|
485
|
-
}
|
|
486
|
-
/** Handles the input event for the field. */
|
|
487
|
-
function onInput() {
|
|
488
|
-
updateDirty();
|
|
489
|
-
if (!validateOn.value.onInput) return;
|
|
490
|
-
if (form.validationMode === "eager" && !isValid.value) scheduleValidation();
|
|
491
|
-
}
|
|
492
|
-
/** Handles the change event for the field. */
|
|
493
|
-
function onChange() {
|
|
494
|
-
updateDirty();
|
|
495
|
-
if (!validateOn.value.onChange) return;
|
|
496
|
-
if (form.validationMode === "eager" && !isValid.value) scheduleValidation();
|
|
497
|
-
}
|
|
498
|
-
/** Handles the focus event for the field. */
|
|
499
|
-
function onFocus() {
|
|
500
|
-
if (validateOn.value.onFocus) scheduleValidation();
|
|
501
|
-
}
|
|
502
|
-
onMounted(async () => {
|
|
503
|
-
await nextTick();
|
|
504
|
-
if (validateOn.value.onMount) validate();
|
|
505
|
-
});
|
|
506
|
-
onUnmounted(() => {
|
|
507
|
-
clearDebounce();
|
|
508
|
-
});
|
|
532
|
+
const field = useNotField(__props);
|
|
509
533
|
return (_ctx, _cache) => {
|
|
510
|
-
return renderSlot(_ctx.$slots, "default",
|
|
511
|
-
errors: errors.value,
|
|
512
|
-
isValid: isValid.value,
|
|
513
|
-
isTouched: isTouched.value,
|
|
514
|
-
isDirty: isDirty.value,
|
|
515
|
-
isValidating: isValidating.value,
|
|
516
|
-
validate,
|
|
517
|
-
events: {
|
|
518
|
-
onBlur,
|
|
519
|
-
onChange,
|
|
520
|
-
onFocus,
|
|
521
|
-
onInput
|
|
522
|
-
},
|
|
523
|
-
path: __props.path,
|
|
524
|
-
value: value.value
|
|
525
|
-
});
|
|
534
|
+
return renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps(unref(field))));
|
|
526
535
|
};
|
|
527
536
|
}
|
|
528
537
|
});
|
|
@@ -531,13 +540,13 @@ var not_field_default = /* @__PURE__ */ defineComponent({
|
|
|
531
540
|
var not_form_default = /* @__PURE__ */ defineComponent({
|
|
532
541
|
__name: "not-form",
|
|
533
542
|
props: { form: {
|
|
534
|
-
type:
|
|
543
|
+
type: Object,
|
|
535
544
|
required: true
|
|
536
545
|
} },
|
|
537
546
|
setup(__props) {
|
|
538
547
|
provideNotFormInstance(__props.form);
|
|
539
548
|
return (_ctx, _cache) => {
|
|
540
|
-
return openBlock(), createElementBlock("form", { onReset: _cache[0] || (_cache[0] = withModifiers(() => {}, ["prevent"])) }, [renderSlot(_ctx.$slots, "default")], 32);
|
|
549
|
+
return openBlock(), createElementBlock(Fragment, null, [createCommentVNode("\n Reset is always prevented here, regardless of what (if anything) the\n consumer binds to @reset, since a native reset would otherwise clear\n every input before `values` has a chance to react to it.\n\n Submit is NOT prevented here. `form.submit()` calls `event.preventDefault()`\n itself when it runs, which only happens if the consumer binds @submit to it\n (e.g. `@submit=\"form.submit\"`) — see the NotForm docs for why this matters.\n "), createElementVNode("form", { onReset: _cache[0] || (_cache[0] = withModifiers(() => {}, ["prevent"])) }, [renderSlot(_ctx.$slots, "default")], 32)], 2112);
|
|
541
550
|
};
|
|
542
551
|
}
|
|
543
552
|
});
|
|
@@ -553,18 +562,18 @@ var not_message_default = /* @__PURE__ */ defineComponent({
|
|
|
553
562
|
default: "span"
|
|
554
563
|
},
|
|
555
564
|
path: {
|
|
556
|
-
type: String,
|
|
565
|
+
type: [String, Object],
|
|
557
566
|
required: true
|
|
558
567
|
},
|
|
559
568
|
form: {
|
|
560
|
-
type:
|
|
569
|
+
type: Object,
|
|
561
570
|
required: false
|
|
562
571
|
}
|
|
563
572
|
},
|
|
564
573
|
setup(__props) {
|
|
565
574
|
const props = __props;
|
|
566
575
|
const form = useNotFormInstance(props.form);
|
|
567
|
-
const message = computed(() => form.
|
|
576
|
+
const message = computed(() => form.getFieldErrors(props.path)[0]?.message);
|
|
568
577
|
return (_ctx, _cache) => {
|
|
569
578
|
return message.value ? (openBlock(), createBlock(resolveDynamicComponent(__props.as), normalizeProps(mergeProps({ key: 0 }, _ctx.$attrs)), {
|
|
570
579
|
default: withCtx(() => [renderSlot(_ctx.$slots, "default", { message: message.value }, () => [createTextVNode(toDisplayString(message.value), 1)])]),
|
|
@@ -574,296 +583,362 @@ var not_message_default = /* @__PURE__ */ defineComponent({
|
|
|
574
583
|
}
|
|
575
584
|
});
|
|
576
585
|
//#endregion
|
|
577
|
-
//#region src/
|
|
586
|
+
//#region src/utils/issues.ts
|
|
578
587
|
/**
|
|
579
|
-
*
|
|
580
|
-
* @
|
|
581
|
-
* @param
|
|
582
|
-
* @
|
|
588
|
+
* Whether two issue paths point at the same field after segment normalization.
|
|
589
|
+
* @internal
|
|
590
|
+
* @param issuePathA First path.
|
|
591
|
+
* @param issuePathB Second path.
|
|
592
|
+
* @returns `true` when both paths are defined, same length, and equal segment-wise.
|
|
583
593
|
*/
|
|
584
|
-
function
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
594
|
+
function areIssuePathsEqual(issuePathA, issuePathB) {
|
|
595
|
+
if (!issuePathA || !issuePathB) return false;
|
|
596
|
+
if (issuePathA.length !== issuePathB.length) return false;
|
|
597
|
+
return issuePathA.every((segment, segmentIndex) => areSegmentsEqual(segment, issuePathB[segmentIndex]));
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/factories/create-not-form-instance.ts
|
|
601
|
+
/**
|
|
602
|
+
* Builds the full reactive form instance used by `useNotForm` and field components.
|
|
603
|
+
* @template TSchema The form schema.
|
|
604
|
+
* @internal
|
|
605
|
+
* @param config Schema, initial values/errors, and submit handler.
|
|
606
|
+
* @returns Assembled {@linkcode NotFormInstance}.
|
|
607
|
+
*/
|
|
608
|
+
function createNotFormInstance(config) {
|
|
609
|
+
/** Baseline values for reset. */
|
|
610
|
+
const initialValues = klona(config.initialValues ?? {});
|
|
611
|
+
/** Live field values. */
|
|
595
612
|
const values = reactive(klona(initialValues));
|
|
613
|
+
/** Baseline issues for reset. */
|
|
614
|
+
const initialErrors = klona(config.initialErrors ?? []);
|
|
615
|
+
/** Current validation issues. */
|
|
596
616
|
const errors = reactive([...initialErrors]);
|
|
617
|
+
/** Paths the user has interacted with. */
|
|
597
618
|
const touchedFields = reactive(/* @__PURE__ */ new Set());
|
|
619
|
+
const isTouched = computed(() => touchedFields.size > 0);
|
|
620
|
+
/** Paths whose value differs from the baseline. */
|
|
598
621
|
const dirtyFields = reactive(/* @__PURE__ */ new Set());
|
|
599
|
-
const isSubmitting = ref(false);
|
|
600
|
-
const isValid = computed(() => errors.length === 0);
|
|
601
622
|
const isDirty = computed(() => dirtyFields.size > 0);
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
let validationCycle = 0;
|
|
618
|
-
/** Identifier of the latest validation cycle for each field. */
|
|
619
|
-
const fieldValidationCycles = /* @__PURE__ */ new Map();
|
|
623
|
+
/** Paths with an in-flight validation. */
|
|
624
|
+
const validatingFields = reactive(/* @__PURE__ */ new Set());
|
|
625
|
+
const isValid = computed(() => errors.length === 0);
|
|
626
|
+
const isValidating = computed(() => validatingFields.size > 0);
|
|
627
|
+
const isSubmitting = ref(false);
|
|
628
|
+
/**
|
|
629
|
+
* Bumped by validate, submit, and reset. Older in-flight runs are discarded:
|
|
630
|
+
* each still resolves with its own result, but only the run matching the
|
|
631
|
+
* current generation when it finishes gets to write to `errors`.
|
|
632
|
+
*/
|
|
633
|
+
let generation = 0;
|
|
634
|
+
/** Latest per-field validation cycle id, used to drop stale field results. */
|
|
635
|
+
const fieldValidationCycleMap = /* @__PURE__ */ new Map();
|
|
636
|
+
/** Active validation count per path so overlapping runs do not clear too early. */
|
|
637
|
+
const validatingFieldCounts = /* @__PURE__ */ new Map();
|
|
620
638
|
/**
|
|
621
|
-
*
|
|
622
|
-
*
|
|
639
|
+
* Runs the current schema against `values`. Resolves `config.schema`
|
|
640
|
+
* first via `toValue`, so a ref or a getter function works the same as
|
|
641
|
+
* passing the schema directly.
|
|
642
|
+
* @returns Standard Schema result.
|
|
623
643
|
*/
|
|
624
|
-
function
|
|
625
|
-
|
|
626
|
-
isValidating.value = true;
|
|
627
|
-
return ++validationCycle;
|
|
644
|
+
function executeSchemaValidation() {
|
|
645
|
+
return toValue(config.schema)["~standard"].validate(values);
|
|
628
646
|
}
|
|
629
647
|
/**
|
|
630
|
-
*
|
|
631
|
-
* @
|
|
632
|
-
* @
|
|
648
|
+
* Sets a field value and syncs its dirty flag. Does not validate.
|
|
649
|
+
* @template TPath Field path.
|
|
650
|
+
* @param path Dot path.
|
|
651
|
+
* @param value Value to assign.
|
|
633
652
|
*/
|
|
634
|
-
function
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const key = String(path);
|
|
638
|
-
const cycle = (fieldValidationCycles.get(key) ?? 0) + 1;
|
|
639
|
-
fieldValidationCycles.set(key, cycle);
|
|
640
|
-
return cycle;
|
|
653
|
+
function setValue(path, value) {
|
|
654
|
+
setProperty(values, path, value);
|
|
655
|
+
syncDirtyState(path);
|
|
641
656
|
}
|
|
642
657
|
/**
|
|
643
|
-
*
|
|
644
|
-
* @param
|
|
645
|
-
* @param cycle The validation cycle identifier.
|
|
646
|
-
* @returns Whether the cycle is still current.
|
|
658
|
+
* Upserts an issue at the same path, or appends it.
|
|
659
|
+
* @param error Issue to set.
|
|
647
660
|
*/
|
|
648
|
-
function
|
|
649
|
-
|
|
661
|
+
function setError(error) {
|
|
662
|
+
const existingIndex = errors.findIndex((existing) => areIssuePathsEqual(existing.path, error.path));
|
|
663
|
+
if (existingIndex === -1) errors.push(error);
|
|
664
|
+
else errors[existingIndex] = error;
|
|
650
665
|
}
|
|
651
|
-
/**
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
666
|
+
/**
|
|
667
|
+
* Replaces the entire error list.
|
|
668
|
+
* @internal
|
|
669
|
+
* @param nextErrors Replacement issues.
|
|
670
|
+
*/
|
|
671
|
+
function replaceErrors(nextErrors) {
|
|
672
|
+
errors.splice(0, errors.length, ...nextErrors);
|
|
655
673
|
}
|
|
656
674
|
/**
|
|
657
|
-
*
|
|
658
|
-
* @
|
|
659
|
-
* @returns Whether the cycle is still current.
|
|
675
|
+
* Removes every issue.
|
|
676
|
+
* @internal
|
|
660
677
|
*/
|
|
661
|
-
function
|
|
662
|
-
|
|
678
|
+
function clearErrors() {
|
|
679
|
+
errors.length = 0;
|
|
663
680
|
}
|
|
664
681
|
/**
|
|
665
|
-
*
|
|
666
|
-
* @
|
|
682
|
+
* Issues whose path exactly equals `path` — see the same note on
|
|
683
|
+
* {@linkcode NotFormInstance.getFieldErrors}.
|
|
684
|
+
* @param path Dot path.
|
|
685
|
+
* @returns Matching issues.
|
|
667
686
|
*/
|
|
668
|
-
function
|
|
669
|
-
|
|
687
|
+
function getFieldErrors(path) {
|
|
688
|
+
const targetPath = parsePath(path);
|
|
689
|
+
return errors.filter((error) => areIssuePathsEqual(error.path, targetPath));
|
|
670
690
|
}
|
|
671
|
-
/**
|
|
672
|
-
|
|
673
|
-
|
|
691
|
+
/**
|
|
692
|
+
* Forces a field dirty.
|
|
693
|
+
* @internal
|
|
694
|
+
* @param path Dot path.
|
|
695
|
+
*/
|
|
696
|
+
function markFieldAsDirty(path) {
|
|
697
|
+
dirtyFields.add(path);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Forces every current leaf field dirty. Only paths present in `values`
|
|
701
|
+
* right now are affected — see the note on
|
|
702
|
+
* {@linkcode NotFormInstance.markAllFieldsAsTouched}.
|
|
703
|
+
* @internal
|
|
704
|
+
*/
|
|
705
|
+
function markAllFieldsAsDirty() {
|
|
706
|
+
for (const path of deepKeys(values)) dirtyFields.add(path);
|
|
674
707
|
}
|
|
675
708
|
/**
|
|
676
|
-
*
|
|
677
|
-
* @
|
|
709
|
+
* Clears dirty state for one field.
|
|
710
|
+
* @internal
|
|
711
|
+
* @param path Dot path.
|
|
678
712
|
*/
|
|
679
|
-
function
|
|
713
|
+
function unmarkFieldAsDirty(path) {
|
|
680
714
|
dirtyFields.delete(path);
|
|
681
715
|
}
|
|
682
|
-
/**
|
|
683
|
-
|
|
684
|
-
|
|
716
|
+
/**
|
|
717
|
+
* Clears dirty state for all fields.
|
|
718
|
+
* @internal
|
|
719
|
+
*/
|
|
720
|
+
function unmarkAllFieldsAsDirty() {
|
|
721
|
+
dirtyFields.clear();
|
|
685
722
|
}
|
|
686
723
|
/**
|
|
687
|
-
*
|
|
688
|
-
* @
|
|
724
|
+
* Sets dirty from a deep compare against the baseline.
|
|
725
|
+
* @internal
|
|
726
|
+
* @param path Dot path.
|
|
689
727
|
*/
|
|
690
|
-
function
|
|
691
|
-
|
|
728
|
+
function syncDirtyState(path) {
|
|
729
|
+
const currentValue = getProperty(values, path);
|
|
730
|
+
const baselineValue = getProperty(initialValues, path);
|
|
731
|
+
if (dequal(currentValue, baselineValue)) unmarkFieldAsDirty(path);
|
|
732
|
+
else markFieldAsDirty(path);
|
|
692
733
|
}
|
|
693
734
|
/**
|
|
694
|
-
*
|
|
695
|
-
* @
|
|
735
|
+
* Recalculates dirty state for every current leaf path.
|
|
736
|
+
* @internal
|
|
696
737
|
*/
|
|
697
|
-
function
|
|
698
|
-
|
|
738
|
+
function syncAllDirtyStates() {
|
|
739
|
+
for (const path of deepKeys(values)) syncDirtyState(path);
|
|
699
740
|
}
|
|
700
741
|
/**
|
|
701
|
-
*
|
|
702
|
-
* @
|
|
703
|
-
* @param
|
|
742
|
+
* Marks a field as touched.
|
|
743
|
+
* @internal
|
|
744
|
+
* @param path Dot path.
|
|
704
745
|
*/
|
|
705
|
-
function
|
|
706
|
-
|
|
707
|
-
if (dequal(value, getProperty(initialValues, path))) unDirtyField(path);
|
|
708
|
-
else dirtyField(path);
|
|
746
|
+
function markFieldAsTouched(path) {
|
|
747
|
+
touchedFields.add(path);
|
|
709
748
|
}
|
|
710
749
|
/**
|
|
711
|
-
*
|
|
712
|
-
*
|
|
750
|
+
* Marks every current leaf field as touched. Only paths present in
|
|
751
|
+
* `values` right now are affected — see the note on
|
|
752
|
+
* {@linkcode NotFormInstance.markAllFieldsAsTouched}.
|
|
753
|
+
* @internal
|
|
713
754
|
*/
|
|
714
|
-
function
|
|
715
|
-
const
|
|
716
|
-
const existingIndex = errors.findIndex((error) => {
|
|
717
|
-
return error.path?.map((element) => normalizeSegment(element)).join(".") === newPath;
|
|
718
|
-
});
|
|
719
|
-
if (existingIndex === -1) errors.push(newIssue);
|
|
720
|
-
else errors[existingIndex] = newIssue;
|
|
755
|
+
function markAllFieldsAsTouched() {
|
|
756
|
+
for (const path of deepKeys(values)) touchedFields.add(path);
|
|
721
757
|
}
|
|
722
758
|
/**
|
|
723
|
-
*
|
|
724
|
-
* @
|
|
759
|
+
* Clears touched state for one field.
|
|
760
|
+
* @internal
|
|
761
|
+
* @param path Dot path.
|
|
725
762
|
*/
|
|
726
|
-
function
|
|
727
|
-
|
|
763
|
+
function unmarkFieldAsTouched(path) {
|
|
764
|
+
touchedFields.delete(path);
|
|
728
765
|
}
|
|
729
|
-
/**
|
|
730
|
-
|
|
731
|
-
|
|
766
|
+
/**
|
|
767
|
+
* Clears touched state for all fields.
|
|
768
|
+
* @internal
|
|
769
|
+
*/
|
|
770
|
+
function unmarkAllFieldsAsTouched() {
|
|
771
|
+
touchedFields.clear();
|
|
732
772
|
}
|
|
733
773
|
/**
|
|
734
|
-
*
|
|
735
|
-
* @
|
|
736
|
-
* @
|
|
774
|
+
* Increments the in-flight validation count for each path.
|
|
775
|
+
* @internal
|
|
776
|
+
* @param paths Field paths.
|
|
737
777
|
*/
|
|
738
|
-
function
|
|
739
|
-
const
|
|
740
|
-
|
|
778
|
+
function markFieldsAsValidating(paths) {
|
|
779
|
+
for (const path of paths) {
|
|
780
|
+
validatingFieldCounts.set(path, (validatingFieldCounts.get(path) ?? 0) + 1);
|
|
781
|
+
validatingFields.add(path);
|
|
782
|
+
}
|
|
741
783
|
}
|
|
742
784
|
/**
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
|
|
785
|
+
* Decrements in-flight counts and clears a path when the count hits zero.
|
|
786
|
+
* @internal
|
|
787
|
+
* @param paths Field paths.
|
|
788
|
+
*/
|
|
789
|
+
function unmarkFieldsAsValidating(paths) {
|
|
790
|
+
for (const path of paths) {
|
|
791
|
+
const nextCount = (validatingFieldCounts.get(path) ?? 0) - 1;
|
|
792
|
+
if (nextCount <= 0) {
|
|
793
|
+
validatingFieldCounts.delete(path);
|
|
794
|
+
validatingFields.delete(path);
|
|
795
|
+
} else validatingFieldCounts.set(path, nextCount);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Validates the whole form, replacing all errors. Bumps `generation`; see
|
|
800
|
+
* {@linkcode NotFormInstance.validate} for the concurrency guarantee this gives.
|
|
801
|
+
* @returns Standard Schema result.
|
|
747
802
|
*/
|
|
748
803
|
async function validate() {
|
|
749
|
-
const cycle =
|
|
804
|
+
const cycle = ++generation;
|
|
805
|
+
const paths = [...deepKeys(values)];
|
|
806
|
+
markFieldsAsValidating(paths);
|
|
750
807
|
try {
|
|
751
|
-
const result = await
|
|
752
|
-
if (
|
|
808
|
+
const result = await executeSchemaValidation();
|
|
809
|
+
if (cycle !== generation) return result;
|
|
753
810
|
if (result.issues) {
|
|
754
811
|
const issues = [...result.issues];
|
|
755
|
-
|
|
812
|
+
replaceErrors(issues);
|
|
756
813
|
return { issues };
|
|
757
814
|
}
|
|
758
815
|
clearErrors();
|
|
759
816
|
return { value: result.value };
|
|
760
817
|
} finally {
|
|
761
|
-
|
|
818
|
+
unmarkFieldsAsValidating(paths);
|
|
762
819
|
}
|
|
763
820
|
}
|
|
764
821
|
/**
|
|
765
|
-
* Validates
|
|
822
|
+
* Validates the form and writes issues only for `path`. `path` can be any
|
|
823
|
+
* granularity — see {@linkcode NotFormInstance.validateField}.
|
|
766
824
|
*
|
|
767
|
-
*
|
|
768
|
-
* @param path
|
|
769
|
-
* @returns
|
|
825
|
+
* Dropped if a newer call for the same path or a newer whole-form generation starts.
|
|
826
|
+
* @param path Dot path.
|
|
827
|
+
* @returns Standard Schema result.
|
|
770
828
|
*/
|
|
771
829
|
async function validateField(path) {
|
|
772
|
-
const cycle =
|
|
830
|
+
const cycle = (fieldValidationCycleMap.get(path) ?? 0) + 1;
|
|
831
|
+
fieldValidationCycleMap.set(path, cycle);
|
|
832
|
+
const startGeneration = generation;
|
|
833
|
+
markFieldsAsValidating([path]);
|
|
773
834
|
try {
|
|
774
|
-
const result = await
|
|
775
|
-
if (
|
|
776
|
-
const
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
if (
|
|
781
|
-
|
|
782
|
-
if (fieldIssues.length > 0) {
|
|
783
|
-
errors.push(...fieldIssues);
|
|
784
|
-
return { issues: fieldIssues };
|
|
785
|
-
}
|
|
786
|
-
return { value: getProperty(values, path) };
|
|
787
|
-
}
|
|
788
|
-
return { value: getProperty(result.value, path) };
|
|
835
|
+
const result = await executeSchemaValidation();
|
|
836
|
+
if (generation !== startGeneration || fieldValidationCycleMap.get(path) !== cycle) return result;
|
|
837
|
+
const targetPath = parsePath(path);
|
|
838
|
+
for (let errorIndex = errors.length - 1; errorIndex >= 0; errorIndex--) if (areIssuePathsEqual(errors[errorIndex].path, targetPath)) errors.splice(errorIndex, 1);
|
|
839
|
+
const fieldIssues = (result.issues ?? []).filter((issue) => areIssuePathsEqual(issue.path, targetPath));
|
|
840
|
+
errors.push(...fieldIssues);
|
|
841
|
+
if (fieldIssues.length > 0) return { issues: fieldIssues };
|
|
842
|
+
return { value: getProperty(values, path) };
|
|
789
843
|
} finally {
|
|
790
|
-
|
|
844
|
+
unmarkFieldsAsValidating([path]);
|
|
791
845
|
}
|
|
792
846
|
}
|
|
793
847
|
/**
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
848
|
+
* Touches all fields, validates, then runs `onSubmit` when valid.
|
|
849
|
+
* @param event Optional submit event. `event.preventDefault()` is called
|
|
850
|
+
* immediately if `event` is given, before checking whether a submission
|
|
851
|
+
* is already in flight or whether validation passes.
|
|
798
852
|
*/
|
|
799
853
|
async function submit(event) {
|
|
800
|
-
event
|
|
854
|
+
event?.preventDefault();
|
|
801
855
|
if (isSubmitting.value) return;
|
|
856
|
+
markAllFieldsAsTouched();
|
|
857
|
+
syncAllDirtyStates();
|
|
858
|
+
const cycle = ++generation;
|
|
859
|
+
const paths = [...deepKeys(values)];
|
|
860
|
+
markFieldsAsValidating(paths);
|
|
802
861
|
isSubmitting.value = true;
|
|
803
|
-
const cycle = beginValidation();
|
|
804
862
|
try {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const result = await runSchema();
|
|
808
|
-
if (!isCurrentValidation(cycle)) return;
|
|
863
|
+
const result = await executeSchemaValidation();
|
|
864
|
+
if (cycle !== generation) return;
|
|
809
865
|
if (result.issues) {
|
|
810
|
-
|
|
866
|
+
replaceErrors([...result.issues]);
|
|
811
867
|
return;
|
|
812
868
|
}
|
|
813
869
|
clearErrors();
|
|
814
|
-
|
|
870
|
+
await config.onSubmit?.(result.value);
|
|
815
871
|
} finally {
|
|
816
|
-
|
|
872
|
+
unmarkFieldsAsValidating(paths);
|
|
817
873
|
isSubmitting.value = false;
|
|
818
874
|
}
|
|
819
875
|
}
|
|
820
876
|
/**
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
* @
|
|
877
|
+
* Restores values and errors to the baseline. Optional arguments become
|
|
878
|
+
* the new baseline and fully replace the previous one — see
|
|
879
|
+
* {@linkcode NotFormInstance.reset} for the "replace, not merge" detail.
|
|
880
|
+
* @param nextValues New baseline values.
|
|
881
|
+
* @param nextErrors New baseline issues.
|
|
824
882
|
*/
|
|
825
|
-
function reset(
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
883
|
+
function reset(nextValues, nextErrors) {
|
|
884
|
+
generation++;
|
|
885
|
+
fieldValidationCycleMap.clear();
|
|
886
|
+
validatingFieldCounts.clear();
|
|
887
|
+
validatingFields.clear();
|
|
888
|
+
if (nextValues) {
|
|
889
|
+
const freshValues = klona(nextValues);
|
|
890
|
+
for (const key of Object.keys(initialValues)) deleteProperty(initialValues, key);
|
|
891
|
+
Object.assign(initialValues, freshValues);
|
|
892
|
+
}
|
|
893
|
+
if (nextErrors) initialErrors.splice(0, initialErrors.length, ...klona(nextErrors));
|
|
894
|
+
for (const key of Object.keys(values)) deleteProperty(values, key);
|
|
895
|
+
Object.assign(values, klona(initialValues));
|
|
896
|
+
replaceErrors(klona(initialErrors));
|
|
897
|
+
unmarkAllFieldsAsTouched();
|
|
898
|
+
unmarkAllFieldsAsDirty();
|
|
835
899
|
}
|
|
836
|
-
return
|
|
837
|
-
get initialErrors() {
|
|
838
|
-
return initialErrors;
|
|
839
|
-
},
|
|
840
|
-
get initialValues() {
|
|
841
|
-
return initialValues;
|
|
842
|
-
},
|
|
843
|
-
validateOn,
|
|
844
|
-
validationMode,
|
|
845
|
-
setValue,
|
|
846
|
-
values,
|
|
847
|
-
isTouched,
|
|
848
|
-
touchedFields,
|
|
849
|
-
touchField,
|
|
850
|
-
dirtyField,
|
|
851
|
-
dirtyFields,
|
|
852
|
-
isDirty,
|
|
900
|
+
return reactive({
|
|
853
901
|
clearErrors,
|
|
902
|
+
dirtyFields,
|
|
854
903
|
errors,
|
|
855
|
-
errorsMap,
|
|
856
904
|
getFieldErrors,
|
|
857
|
-
|
|
858
|
-
|
|
905
|
+
isDirty,
|
|
906
|
+
isSubmitting,
|
|
907
|
+
isTouched,
|
|
859
908
|
isValid,
|
|
860
909
|
isValidating,
|
|
910
|
+
markAllFieldsAsDirty,
|
|
911
|
+
markAllFieldsAsTouched,
|
|
912
|
+
markFieldAsDirty,
|
|
913
|
+
markFieldAsTouched,
|
|
914
|
+
replaceErrors,
|
|
915
|
+
reset,
|
|
916
|
+
setError,
|
|
917
|
+
setValue,
|
|
918
|
+
submit,
|
|
919
|
+
syncAllDirtyStates,
|
|
920
|
+
syncDirtyState,
|
|
921
|
+
touchedFields,
|
|
922
|
+
unmarkAllFieldsAsDirty,
|
|
923
|
+
unmarkAllFieldsAsTouched,
|
|
924
|
+
unmarkFieldAsDirty,
|
|
925
|
+
unmarkFieldAsTouched,
|
|
861
926
|
validate,
|
|
862
927
|
validateField,
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
reset
|
|
928
|
+
validatingFields,
|
|
929
|
+
values
|
|
866
930
|
});
|
|
867
931
|
}
|
|
868
932
|
//#endregion
|
|
933
|
+
//#region src/composables/use-not-form.ts
|
|
934
|
+
/**
|
|
935
|
+
* Creates a form instance and returns the public {@linkcode NotFormAPI}.
|
|
936
|
+
* @template TSchema The form schema.
|
|
937
|
+
* @param config Schema, initial values/errors, and submit handler.
|
|
938
|
+
* @returns Public form API. Child components still inject the full instance via `<NotForm>`.
|
|
939
|
+
*/
|
|
940
|
+
function useNotForm(config) {
|
|
941
|
+
return createNotFormInstance(config);
|
|
942
|
+
}
|
|
943
|
+
//#endregion
|
|
869
944
|
export { not_array_field_default as NotArrayField, not_field_default as NotField, not_form_default as NotForm, not_message_default as NotMessage, useNotForm };
|