notform 2.1.1 → 2.1.3
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 +22 -7
- package/dist/index.d.ts +268 -255
- package/dist/index.js +578 -523
- package/package.json +27 -25
package/dist/index.js
CHANGED
|
@@ -1,263 +1,13 @@
|
|
|
1
|
-
import { computed, createBlock, createCommentVNode, createElementBlock, createTextVNode, defineComponent,
|
|
2
|
-
import { klona } from "klona/full";
|
|
1
|
+
import { computed, createBlock, createCommentVNode, createElementBlock, createTextVNode, defineComponent, inject, markRaw, mergeProps, nextTick, normalizeProps, onMounted, onUnmounted, openBlock, provide, reactive, ref, renderSlot, resolveDynamicComponent, toDisplayString, toValue, watch, withCtx } from "vue";
|
|
3
2
|
import { dequal } from "dequal";
|
|
4
3
|
import { deepKeys, deleteProperty, getProperty, hasProperty, parsePath, setProperty } from "dot-prop";
|
|
5
|
-
|
|
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
|
-
* Checks if a validation issue path matches a target field path.
|
|
17
|
-
* @param issuePath The path array from the validation issue.
|
|
18
|
-
* @param targetPath The normalized path to compare against.
|
|
19
|
-
* @returns True if the paths are equivalent.
|
|
20
|
-
*/
|
|
21
|
-
function isIssuePathEqual(issuePath, targetPath) {
|
|
22
|
-
if (!issuePath) return false;
|
|
23
|
-
if (issuePath.length !== targetPath.length) return false;
|
|
24
|
-
return issuePath.every((segment, index) => {
|
|
25
|
-
const normalizedSegment = normalizeSegment(segment);
|
|
26
|
-
const targetSegment = targetPath[index];
|
|
27
|
-
if (typeof normalizedSegment === "number" || typeof targetSegment === "number") return Number(normalizedSegment) === Number(targetSegment);
|
|
28
|
-
return normalizedSegment === targetSegment;
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/composables/use-not-form.ts
|
|
33
|
-
function useNotForm(config) {
|
|
34
|
-
/** Runs the schema against the current values and returns the raw result. */
|
|
35
|
-
const runSchema = () => {
|
|
36
|
-
return toValue(config.schema)["~standard"].validate(values);
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Marks all current leaf paths as touched.
|
|
40
|
-
* Called internally on submit so all field errors surface at once.
|
|
41
|
-
*/
|
|
42
|
-
const touchAllFields = () => {
|
|
43
|
-
deepKeys(values).forEach((path) => touchedFields.add(path));
|
|
44
|
-
};
|
|
45
|
-
/**
|
|
46
|
-
* Marks all current leaf paths as dirty.
|
|
47
|
-
* Called internally on submit so required-field errors are not suppressed.
|
|
48
|
-
*/
|
|
49
|
-
const dirtyAllFields = () => {
|
|
50
|
-
deepKeys(values).forEach((path) => dirtyFields.add(path));
|
|
51
|
-
};
|
|
52
|
-
/** Removes a path from the dirty set without exposing the operation publicly. */
|
|
53
|
-
const unDirtyField = (path) => {
|
|
54
|
-
dirtyFields.delete(path);
|
|
55
|
-
};
|
|
56
|
-
/**
|
|
57
|
-
* Mutable so `reset()` can replace the reference when new values are provided.
|
|
58
|
-
* Always deep-cloned to prevent external mutation from affecting the baseline.
|
|
59
|
-
*
|
|
60
|
-
* These are intentionally `let` — `reset()` replaces them, and the instance
|
|
61
|
-
* exposes them via getters so consumers always read the current snapshot.
|
|
62
|
-
* The `readonly` modifier on the type prevents external assignment while still
|
|
63
|
-
* allowing the getter to return the latest value after a reset.
|
|
64
|
-
*/
|
|
65
|
-
let initialValues = klona(config.initialValues ?? {});
|
|
66
|
-
let initialErrors = klona(config.initialErrors ?? []);
|
|
67
|
-
const validateOn = {
|
|
68
|
-
onBlur: config.validateOn?.onBlur ?? true,
|
|
69
|
-
onChange: config.validateOn?.onChange ?? true,
|
|
70
|
-
onInput: config.validateOn?.onInput ?? true,
|
|
71
|
-
onMount: config.validateOn?.onMount ?? false,
|
|
72
|
-
onFocus: config.validateOn?.onFocus ?? false
|
|
73
|
-
};
|
|
74
|
-
const validationMode = {
|
|
75
|
-
eager: config.validationMode?.eager ?? true,
|
|
76
|
-
lazy: config.validationMode?.lazy ?? false
|
|
77
|
-
};
|
|
78
|
-
/**
|
|
79
|
-
* Deeply reactive object — access directly as `form.values.email`.
|
|
80
|
-
* Using `reactive()` instead of `ref()` keeps behaviour consistent
|
|
81
|
-
* across components, composables, and Pinia stores (which auto-unwrap refs).
|
|
82
|
-
*/
|
|
83
|
-
const values = reactive(klona(initialValues));
|
|
84
|
-
/**
|
|
85
|
-
* Reactive array mutated in-place to preserve reactivity.
|
|
86
|
-
* Using `reactive()` instead of `ref()` prevents Pinia from unwrapping
|
|
87
|
-
* the array and losing its reactive proxy.
|
|
88
|
-
*/
|
|
89
|
-
const errors = reactive([...initialErrors]);
|
|
90
|
-
const isSubmitting = ref(false);
|
|
91
|
-
const isValidating = ref(false);
|
|
92
|
-
/**
|
|
93
|
-
* Counter-based validation tracking.
|
|
94
|
-
*
|
|
95
|
-
* A boolean flag flips to `false` as soon as the first concurrent
|
|
96
|
-
* validation resolves, even if others are still running.
|
|
97
|
-
* A counter fixes this: `isValidating` stays `true` until every
|
|
98
|
-
* in-flight call has settled.
|
|
99
|
-
*/
|
|
100
|
-
let validatingCount = 0;
|
|
101
|
-
/** Increments the validation counter and sets `isValidating` to true. */
|
|
102
|
-
const beginValidating = () => {
|
|
103
|
-
validatingCount++;
|
|
104
|
-
isValidating.value = true;
|
|
105
|
-
};
|
|
106
|
-
/** Decrements the validation counter and sets `isValidating` to false if the counter reaches zero. */
|
|
107
|
-
const endValidating = () => {
|
|
108
|
-
validatingCount--;
|
|
109
|
-
if (validatingCount === 0) isValidating.value = false;
|
|
110
|
-
};
|
|
111
|
-
/**
|
|
112
|
-
* Reactive Sets using `reactive()` for the same Pinia-compatibility reason
|
|
113
|
-
* as `errors` above — `ref(new Set())` would be unwrapped to a plain Set.
|
|
114
|
-
*/
|
|
115
|
-
const touchedFields = reactive(/* @__PURE__ */ new Set());
|
|
116
|
-
const dirtyFields = reactive(/* @__PURE__ */ new Set());
|
|
117
|
-
const isValid = computed(() => errors.length === 0);
|
|
118
|
-
const isDirty = computed(() => dirtyFields.size > 0);
|
|
119
|
-
const isTouched = computed(() => touchedFields.size > 0);
|
|
120
|
-
const errorsMap = computed(() => {
|
|
121
|
-
return errors.reduce((errorsByPath, issue) => {
|
|
122
|
-
if (!issue.path) return errorsByPath;
|
|
123
|
-
const path = issue.path.map(normalizeSegment).join(".");
|
|
124
|
-
if (path && !errorsByPath[path]) errorsByPath[path] = issue.message;
|
|
125
|
-
return errorsByPath;
|
|
126
|
-
}, {});
|
|
127
|
-
});
|
|
128
|
-
const setValue = (path, value) => {
|
|
129
|
-
setProperty(values, path, value);
|
|
130
|
-
if (dequal(value, getProperty(initialValues, path))) unDirtyField(path);
|
|
131
|
-
else dirtyField(path);
|
|
132
|
-
};
|
|
133
|
-
const touchField = (path) => {
|
|
134
|
-
touchedFields.add(path);
|
|
135
|
-
};
|
|
136
|
-
const dirtyField = (path) => {
|
|
137
|
-
dirtyFields.add(path);
|
|
138
|
-
};
|
|
139
|
-
const setError = (newIssue) => {
|
|
140
|
-
const newPath = newIssue.path?.map(normalizeSegment).join(".");
|
|
141
|
-
const existingIndex = errors.findIndex((error) => error.path?.map(normalizeSegment).join(".") === newPath);
|
|
142
|
-
if (existingIndex !== -1) errors.splice(existingIndex, 1, newIssue);
|
|
143
|
-
else errors.push(newIssue);
|
|
144
|
-
};
|
|
145
|
-
const setErrors = (newIssues) => {
|
|
146
|
-
errors.splice(0, errors.length, ...newIssues);
|
|
147
|
-
};
|
|
148
|
-
const clearErrors = () => {
|
|
149
|
-
errors.splice(0, errors.length);
|
|
150
|
-
};
|
|
151
|
-
const getFieldErrors = (path) => {
|
|
152
|
-
const pathSegments = parsePath(path);
|
|
153
|
-
return errors.filter((error) => isIssuePathEqual(error.path, pathSegments));
|
|
154
|
-
};
|
|
155
|
-
const validate = async () => {
|
|
156
|
-
beginValidating();
|
|
157
|
-
try {
|
|
158
|
-
const result = await runSchema();
|
|
159
|
-
if (result?.issues) {
|
|
160
|
-
setErrors([...result.issues]);
|
|
161
|
-
return { issues: result.issues };
|
|
162
|
-
}
|
|
163
|
-
clearErrors();
|
|
164
|
-
return { value: result.value };
|
|
165
|
-
} finally {
|
|
166
|
-
endValidating();
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
const validateField = async (path) => {
|
|
170
|
-
beginValidating();
|
|
171
|
-
try {
|
|
172
|
-
const result = await runSchema();
|
|
173
|
-
const pathSegments = parsePath(path);
|
|
174
|
-
const staleIndices = errors.reduce((indices, error, index) => {
|
|
175
|
-
if (isIssuePathEqual(error.path, pathSegments)) indices.push(index);
|
|
176
|
-
return indices;
|
|
177
|
-
}, []);
|
|
178
|
-
for (let index = staleIndices.length - 1; index >= 0; index--) errors.splice(staleIndices[index], 1);
|
|
179
|
-
if (result?.issues) {
|
|
180
|
-
const fieldIssues = result.issues.filter((issue) => isIssuePathEqual(issue.path, pathSegments));
|
|
181
|
-
if (fieldIssues.length > 0) {
|
|
182
|
-
errors.push(...fieldIssues);
|
|
183
|
-
return { issues: fieldIssues };
|
|
184
|
-
}
|
|
185
|
-
return { value: getProperty(values, path) };
|
|
186
|
-
}
|
|
187
|
-
return { value: getProperty(result.value, path) };
|
|
188
|
-
} finally {
|
|
189
|
-
endValidating();
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
const submit = async (event) => {
|
|
193
|
-
isSubmitting.value = true;
|
|
194
|
-
try {
|
|
195
|
-
touchAllFields();
|
|
196
|
-
dirtyAllFields();
|
|
197
|
-
const result = await validate();
|
|
198
|
-
if (result?.issues) {
|
|
199
|
-
event.preventDefault();
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (config.onSubmit) {
|
|
203
|
-
event.preventDefault();
|
|
204
|
-
await config.onSubmit(result.value);
|
|
205
|
-
}
|
|
206
|
-
} catch {
|
|
207
|
-
event.preventDefault();
|
|
208
|
-
} finally {
|
|
209
|
-
isSubmitting.value = false;
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
const reset = (newValues, newErrors) => {
|
|
213
|
-
if (newValues) initialValues = klona(newValues);
|
|
214
|
-
if (newErrors) initialErrors = klona(newErrors);
|
|
215
|
-
const freshValues = klona(initialValues);
|
|
216
|
-
const current = values;
|
|
217
|
-
for (const key of Object.keys(current)) if (!hasProperty(freshValues, key)) deleteProperty(values, key);
|
|
218
|
-
for (const key of Object.keys(freshValues)) setProperty(values, key, getProperty(freshValues, key));
|
|
219
|
-
errors.splice(0, errors.length, ...klona(initialErrors));
|
|
220
|
-
touchedFields.clear();
|
|
221
|
-
dirtyFields.clear();
|
|
222
|
-
};
|
|
223
|
-
return markRaw({
|
|
224
|
-
get initialValues() {
|
|
225
|
-
return initialValues;
|
|
226
|
-
},
|
|
227
|
-
get initialErrors() {
|
|
228
|
-
return initialErrors;
|
|
229
|
-
},
|
|
230
|
-
validateOn,
|
|
231
|
-
validationMode,
|
|
232
|
-
values,
|
|
233
|
-
setValue,
|
|
234
|
-
touchedFields,
|
|
235
|
-
touchField,
|
|
236
|
-
isTouched,
|
|
237
|
-
dirtyFields,
|
|
238
|
-
dirtyField,
|
|
239
|
-
isDirty,
|
|
240
|
-
errors,
|
|
241
|
-
errorsMap,
|
|
242
|
-
setError,
|
|
243
|
-
setErrors,
|
|
244
|
-
clearErrors,
|
|
245
|
-
getFieldErrors,
|
|
246
|
-
isValidating,
|
|
247
|
-
validate,
|
|
248
|
-
validateField,
|
|
249
|
-
isValid,
|
|
250
|
-
isSubmitting,
|
|
251
|
-
submit,
|
|
252
|
-
reset
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
//#endregion
|
|
4
|
+
import { klona } from "klona/full";
|
|
256
5
|
//#region src/utils/instance-utils.ts
|
|
257
6
|
/** The injection key for the NotForm instance. */
|
|
258
7
|
const NOT_FORM_INSTANCE_KEY = Symbol("notform:instance");
|
|
259
8
|
/**
|
|
260
9
|
* Provides the NotForm instance to the component tree.
|
|
10
|
+
* @template TSchema The validation object schema.
|
|
261
11
|
* @param instance The NotForm instance to provide.
|
|
262
12
|
*/
|
|
263
13
|
function provideNotFormInstance(instance) {
|
|
@@ -265,6 +15,7 @@ function provideNotFormInstance(instance) {
|
|
|
265
15
|
}
|
|
266
16
|
/**
|
|
267
17
|
* Retrieves the NotForm instance from the component tree.
|
|
18
|
+
* @template TSchema The form schema.
|
|
268
19
|
* @param explicitInstance Optional explicit instance to use.
|
|
269
20
|
* @returns The NotForm instance.
|
|
270
21
|
*/
|
|
@@ -278,31 +29,18 @@ function useNotFormInstance(explicitInstance) {
|
|
|
278
29
|
return instance;
|
|
279
30
|
}
|
|
280
31
|
//#endregion
|
|
281
|
-
//#region src/components/not-
|
|
282
|
-
var
|
|
283
|
-
__name: "not-
|
|
284
|
-
props: { form: {
|
|
285
|
-
type: null,
|
|
286
|
-
required: true
|
|
287
|
-
} },
|
|
288
|
-
setup(__props) {
|
|
289
|
-
const attributes = useAttrs();
|
|
290
|
-
provideNotFormInstance(__props.form);
|
|
291
|
-
return (_ctx, _cache) => {
|
|
292
|
-
return openBlock(), createElementBlock("form", normalizeProps(guardReactiveProps(unref(attributes))), [renderSlot(_ctx.$slots, "default")], 16);
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
//#endregion
|
|
297
|
-
//#region src/components/not-field.vue
|
|
298
|
-
var not_field_default = /* @__PURE__ */ defineComponent({
|
|
299
|
-
inheritAttrs: false,
|
|
300
|
-
__name: "not-field",
|
|
32
|
+
//#region src/components/not-array-field.vue
|
|
33
|
+
var not_array_field_default = /* @__PURE__ */ defineComponent({
|
|
34
|
+
__name: "not-array-field",
|
|
301
35
|
props: {
|
|
302
36
|
path: {
|
|
303
37
|
type: String,
|
|
304
38
|
required: true
|
|
305
39
|
},
|
|
40
|
+
itemSchema: {
|
|
41
|
+
type: null,
|
|
42
|
+
required: false
|
|
43
|
+
},
|
|
306
44
|
form: {
|
|
307
45
|
type: null,
|
|
308
46
|
required: false
|
|
@@ -310,167 +48,187 @@ var not_field_default = /* @__PURE__ */ defineComponent({
|
|
|
310
48
|
validateOn: {
|
|
311
49
|
type: Object,
|
|
312
50
|
required: false
|
|
313
|
-
},
|
|
314
|
-
debounce: {
|
|
315
|
-
type: Number,
|
|
316
|
-
required: false
|
|
317
51
|
}
|
|
318
52
|
},
|
|
319
53
|
setup(__props) {
|
|
320
54
|
const props = __props;
|
|
321
55
|
const form = useNotFormInstance(props.form);
|
|
56
|
+
const isValidating = ref(false);
|
|
57
|
+
/** Counter to ensure absolute uniqueness for generated keys. */
|
|
58
|
+
let keyCounter = 0;
|
|
59
|
+
/** Stable keys per item that survive reorders, removals, and inserts. */
|
|
60
|
+
const itemKeys = ref((() => {
|
|
61
|
+
const initial = getProperty(form.values, props.path);
|
|
62
|
+
const length = Array.isArray(initial) ? initial.length : 0;
|
|
63
|
+
return Array.from({ length }, () => `${props.path}-${keyCounter++}`);
|
|
64
|
+
})());
|
|
322
65
|
const validateOn = computed(() => ({
|
|
323
|
-
|
|
324
|
-
|
|
66
|
+
onChange: props.validateOn?.onChange ?? form.validateOn.onChange,
|
|
67
|
+
onMount: props.validateOn?.onMount ?? form.validateOn.onMount
|
|
325
68
|
}));
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
debounceTimer = void 0;
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
/**
|
|
341
|
-
* Schedules a validation run, respecting the field's `debounce` prop.
|
|
342
|
-
*
|
|
343
|
-
* - If `debounce` is `0` or omitted, validation runs synchronously.
|
|
344
|
-
* - Otherwise, any pending timer is cancelled and a new one is started.
|
|
345
|
-
* Only the final call within the window actually validates — useful for
|
|
346
|
-
* async checks (availability lookups, server-side rules) where firing on
|
|
347
|
-
* every keystroke would be wasteful.
|
|
348
|
-
*/
|
|
349
|
-
const scheduleValidation = () => {
|
|
350
|
-
if (!props.debounce) {
|
|
351
|
-
validate();
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
clearDebounce();
|
|
355
|
-
debounceTimer = setTimeout(validate, props.debounce);
|
|
356
|
-
};
|
|
357
|
-
const value = computed(() => getProperty(form.values, props.path));
|
|
69
|
+
const array = computed(() => {
|
|
70
|
+
const value = getProperty(form.values, props.path);
|
|
71
|
+
return Array.isArray(value) ? value : [];
|
|
72
|
+
});
|
|
73
|
+
const items = computed(() => {
|
|
74
|
+
return array.value.map((_, index) => ({
|
|
75
|
+
index,
|
|
76
|
+
key: itemKeys.value[index] ?? `${props.path}-fallback-${index}`,
|
|
77
|
+
path: `${props.path}.${index}`
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
358
80
|
const errors = computed(() => form.getFieldErrors(props.path));
|
|
359
81
|
const isValid = computed(() => errors.value.length === 0);
|
|
360
|
-
const isTouched = computed(() =>
|
|
361
|
-
|
|
82
|
+
const isTouched = computed(() => {
|
|
83
|
+
return form.touchedFields.has(props.path) || [...form.touchedFields].some((path) => path.startsWith(`${props.path}.`));
|
|
84
|
+
});
|
|
85
|
+
const isDirty = computed(() => {
|
|
86
|
+
return form.dirtyFields.has(props.path) || [...form.dirtyFields].some((path) => path.startsWith(`${props.path}.`));
|
|
87
|
+
});
|
|
362
88
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
* and compares it against the field's initial value.
|
|
89
|
+
* Generates a unique key string using the field path and counter.
|
|
90
|
+
* @returns A unique key string
|
|
366
91
|
*/
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
92
|
+
function generateKey() {
|
|
93
|
+
return `${props.path}-${keyCounter++}`;
|
|
94
|
+
}
|
|
95
|
+
/** Re-aligns itemKeys with current array length (used mostly after external resets) */
|
|
96
|
+
function syncKeys() {
|
|
97
|
+
const arrayLength = array.value.length;
|
|
98
|
+
if (itemKeys.value.length > arrayLength) itemKeys.value.length = arrayLength;
|
|
99
|
+
else while (itemKeys.value.length < arrayLength) itemKeys.value.push(generateKey());
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Applies an array update, syncs form dirty/touched state, and triggers validation.
|
|
103
|
+
* @param updater Function that modifies the array
|
|
104
|
+
*/
|
|
105
|
+
function mutate(updater) {
|
|
106
|
+
const current = [...array.value];
|
|
107
|
+
updater(current);
|
|
108
|
+
setProperty(form.values, props.path, current);
|
|
109
|
+
form.touchField(props.path);
|
|
110
|
+
if (dequal(current, getProperty(form.initialValues, props.path))) form.dirtyFields.delete(props.path);
|
|
111
|
+
else form.dirtyField(props.path);
|
|
112
|
+
if (validateOn.value.onChange) validate();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Validates the array field
|
|
116
|
+
* @returns Promise that resolves to the validation result
|
|
117
|
+
*/
|
|
118
|
+
async function validate() {
|
|
119
|
+
isValidating.value = true;
|
|
120
|
+
try {
|
|
121
|
+
return await form.validateField(props.path);
|
|
122
|
+
} finally {
|
|
376
123
|
isValidating.value = false;
|
|
377
124
|
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Appends an item to the array
|
|
128
|
+
* @param value The value to append
|
|
129
|
+
*/
|
|
130
|
+
function append(value) {
|
|
131
|
+
itemKeys.value.push(generateKey());
|
|
132
|
+
mutate((current) => current.push(value));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Prepends an item to the array
|
|
136
|
+
* @param value The value to prepend
|
|
137
|
+
*/
|
|
138
|
+
function prepend(value) {
|
|
139
|
+
itemKeys.value.unshift(generateKey());
|
|
140
|
+
mutate((current) => current.unshift(value));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Removes an item from the array
|
|
144
|
+
* @param index The index of the item to remove
|
|
145
|
+
*/
|
|
146
|
+
function remove(index) {
|
|
147
|
+
itemKeys.value.splice(index, 1);
|
|
148
|
+
mutate((current) => current.splice(index, 1));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Inserts an item at a specific index
|
|
152
|
+
* @param index The index to insert at
|
|
153
|
+
* @param value The value to insert
|
|
154
|
+
*/
|
|
155
|
+
function insert(index, value) {
|
|
156
|
+
itemKeys.value.splice(index, 0, generateKey());
|
|
157
|
+
mutate((current) => current.splice(index, 0, value));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Updates an item at a specific index
|
|
161
|
+
* @param index The index to update
|
|
162
|
+
* @param value The new value
|
|
163
|
+
*/
|
|
164
|
+
function update(index, value) {
|
|
165
|
+
mutate((current) => {
|
|
166
|
+
current[index] = value;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Swaps two items in the array
|
|
171
|
+
* @param indexA The first index
|
|
172
|
+
* @param indexB The second index
|
|
173
|
+
*/
|
|
174
|
+
function swap(indexA, indexB) {
|
|
175
|
+
const tempKey = itemKeys.value[indexA];
|
|
176
|
+
itemKeys.value[indexA] = itemKeys.value[indexB];
|
|
177
|
+
itemKeys.value[indexB] = tempKey;
|
|
178
|
+
mutate((current) => {
|
|
179
|
+
const temp = current[indexA];
|
|
180
|
+
current[indexA] = current[indexB];
|
|
181
|
+
current[indexB] = temp;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Moves an item from one index to another
|
|
186
|
+
* @param from The source index
|
|
187
|
+
* @param to The destination index
|
|
188
|
+
*/
|
|
189
|
+
function move(from, to) {
|
|
190
|
+
const [key] = itemKeys.value.splice(from, 1);
|
|
191
|
+
itemKeys.value.splice(to, 0, key);
|
|
192
|
+
mutate((current) => {
|
|
193
|
+
const [item] = current.splice(from, 1);
|
|
194
|
+
current.splice(to, 0, item);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
397
197
|
onMounted(async () => {
|
|
398
198
|
await nextTick();
|
|
399
199
|
if (validateOn.value.onMount) validate();
|
|
400
200
|
});
|
|
401
|
-
|
|
402
|
-
const slotProps = computed(() => ({
|
|
403
|
-
path: props.path,
|
|
404
|
-
value: value.value,
|
|
405
|
-
errors: errors.value,
|
|
406
|
-
isValid: isValid.value,
|
|
407
|
-
isTouched: isTouched.value,
|
|
408
|
-
isDirty: isDirty.value,
|
|
409
|
-
isValidating: isValidating.value,
|
|
410
|
-
validate,
|
|
411
|
-
events: {
|
|
412
|
-
onBlur,
|
|
413
|
-
onInput,
|
|
414
|
-
onChange,
|
|
415
|
-
onFocus
|
|
416
|
-
}
|
|
417
|
-
}));
|
|
201
|
+
watch(() => array.value.length, syncKeys);
|
|
418
202
|
return (_ctx, _cache) => {
|
|
419
|
-
return renderSlot(_ctx.$slots, "default",
|
|
203
|
+
return renderSlot(_ctx.$slots, "default", {
|
|
204
|
+
append,
|
|
205
|
+
errors: errors.value,
|
|
206
|
+
insert,
|
|
207
|
+
isDirty: isDirty.value,
|
|
208
|
+
isTouched: isTouched.value,
|
|
209
|
+
isValid: isValid.value,
|
|
210
|
+
isValidating: isValidating.value,
|
|
211
|
+
items: items.value,
|
|
212
|
+
move,
|
|
213
|
+
path: __props.path,
|
|
214
|
+
prepend,
|
|
215
|
+
remove,
|
|
216
|
+
swap,
|
|
217
|
+
update,
|
|
218
|
+
validate
|
|
219
|
+
});
|
|
420
220
|
};
|
|
421
221
|
}
|
|
422
222
|
});
|
|
423
223
|
//#endregion
|
|
424
|
-
//#region src/components/not-
|
|
425
|
-
var
|
|
426
|
-
|
|
427
|
-
__name: "not-message",
|
|
224
|
+
//#region src/components/not-field.vue
|
|
225
|
+
var not_field_default = /* @__PURE__ */ defineComponent({
|
|
226
|
+
__name: "not-field",
|
|
428
227
|
props: {
|
|
429
228
|
path: {
|
|
430
229
|
type: String,
|
|
431
230
|
required: true
|
|
432
231
|
},
|
|
433
|
-
as: {
|
|
434
|
-
type: String,
|
|
435
|
-
required: false,
|
|
436
|
-
default: "span"
|
|
437
|
-
},
|
|
438
|
-
form: {
|
|
439
|
-
type: null,
|
|
440
|
-
required: false
|
|
441
|
-
}
|
|
442
|
-
},
|
|
443
|
-
setup(__props) {
|
|
444
|
-
const attributes = useAttrs();
|
|
445
|
-
const props = __props;
|
|
446
|
-
const form = useNotFormInstance(props.form);
|
|
447
|
-
const message = computed(() => form.errorsMap.value[props.path]);
|
|
448
|
-
const slotProps = computed(() => ({
|
|
449
|
-
message: message.value,
|
|
450
|
-
attributes
|
|
451
|
-
}));
|
|
452
|
-
return (_ctx, _cache) => {
|
|
453
|
-
return renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps(slotProps.value)), () => [message.value ? (openBlock(), createBlock(resolveDynamicComponent(props.as), normalizeProps(mergeProps({ key: 0 }, unref(attributes))), {
|
|
454
|
-
default: withCtx(() => [createTextVNode(toDisplayString(message.value), 1)]),
|
|
455
|
-
_: 1
|
|
456
|
-
}, 16)) : createCommentVNode("v-if", true)]);
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
//#endregion
|
|
461
|
-
//#region src/components/not-array-field.vue
|
|
462
|
-
var not_array_field_default = /* @__PURE__ */ defineComponent({
|
|
463
|
-
inheritAttrs: false,
|
|
464
|
-
__name: "not-array-field",
|
|
465
|
-
props: {
|
|
466
|
-
path: {
|
|
467
|
-
type: String,
|
|
468
|
-
required: true
|
|
469
|
-
},
|
|
470
|
-
itemSchema: {
|
|
471
|
-
type: null,
|
|
472
|
-
required: false
|
|
473
|
-
},
|
|
474
232
|
form: {
|
|
475
233
|
type: null,
|
|
476
234
|
required: false
|
|
@@ -478,147 +236,444 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
|
|
|
478
236
|
validateOn: {
|
|
479
237
|
type: Object,
|
|
480
238
|
required: false
|
|
239
|
+
},
|
|
240
|
+
debounce: {
|
|
241
|
+
type: Number,
|
|
242
|
+
required: false
|
|
481
243
|
}
|
|
482
244
|
},
|
|
483
245
|
setup(__props) {
|
|
484
246
|
const props = __props;
|
|
485
247
|
const form = useNotFormInstance(props.form);
|
|
248
|
+
const isValidating = ref(false);
|
|
249
|
+
/** Timer handle for the current pending debounced validation. */
|
|
250
|
+
let debounceTimer;
|
|
251
|
+
/** Merges per-field overrides with the form-wide validation config */
|
|
486
252
|
const validateOn = computed(() => ({
|
|
487
|
-
|
|
488
|
-
|
|
253
|
+
...form.validateOn,
|
|
254
|
+
...props.validateOn
|
|
489
255
|
}));
|
|
490
|
-
const
|
|
491
|
-
/**
|
|
492
|
-
* Stable keys per item — survive reorders, removals, and inserts.
|
|
493
|
-
* Seeded from the length of the initial array so existing items have keys from the start.
|
|
494
|
-
*/
|
|
495
|
-
const itemKeys = ref((() => {
|
|
496
|
-
const initial = getProperty(form.values, props.path);
|
|
497
|
-
const length = Array.isArray(initial) ? initial.length : 0;
|
|
498
|
-
return Array.from({ length }, (_, index) => `${props.path}-${index}-${Date.now()}`);
|
|
499
|
-
})());
|
|
500
|
-
let keyCounter = itemKeys.value.length;
|
|
501
|
-
const generateKey = () => `${props.path}-${keyCounter++}-${Date.now()}`;
|
|
502
|
-
const array = computed(() => {
|
|
503
|
-
const value = getProperty(form.values, props.path);
|
|
504
|
-
return Array.isArray(value) ? value : [];
|
|
505
|
-
});
|
|
506
|
-
const items = computed(() => array.value.map((_, index) => ({
|
|
507
|
-
key: itemKeys.value[index] ?? `${props.path}-fallback-${index}`,
|
|
508
|
-
index,
|
|
509
|
-
path: `${props.path}.${index}`
|
|
510
|
-
})));
|
|
256
|
+
const value = computed(() => getProperty(form.values, props.path));
|
|
511
257
|
const errors = computed(() => form.getFieldErrors(props.path));
|
|
512
258
|
const isValid = computed(() => errors.value.length === 0);
|
|
259
|
+
const isTouched = computed(() => form.touchedFields.has(props.path));
|
|
260
|
+
const isDirty = computed(() => form.dirtyFields.has(props.path));
|
|
261
|
+
/** Cancels pending debounced validation on blur/unmount to prevent rogue execution. */
|
|
262
|
+
function clearDebounce() {
|
|
263
|
+
if (debounceTimer !== void 0) {
|
|
264
|
+
clearTimeout(debounceTimer);
|
|
265
|
+
debounceTimer = void 0;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Compares current value against baseline to sync dirty state tracking. */
|
|
269
|
+
function updateDirty() {
|
|
270
|
+
if (dequal(value.value, getProperty(form.initialValues, props.path))) form.dirtyFields.delete(props.path);
|
|
271
|
+
else form.dirtyField(props.path);
|
|
272
|
+
}
|
|
273
|
+
/** Replaces pending validations with a new timer, or runs synchronously if no debounce. */
|
|
274
|
+
function scheduleValidation() {
|
|
275
|
+
if (!props.debounce) {
|
|
276
|
+
validate();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
clearDebounce();
|
|
280
|
+
debounceTimer = setTimeout(validate, props.debounce);
|
|
281
|
+
}
|
|
513
282
|
/**
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*/
|
|
517
|
-
const isTouched = computed(() => form.touchedFields.has(props.path) || [...form.touchedFields].some((path) => path.startsWith(`${props.path}.`)));
|
|
518
|
-
/**
|
|
519
|
-
* Derived from the form's dirtyFields set.
|
|
520
|
-
* True if the array path itself or any of its item paths are dirty.
|
|
283
|
+
* Validates the field and returns the validation result.
|
|
284
|
+
* @returns A promise that resolves to the validation result.
|
|
521
285
|
*/
|
|
522
|
-
|
|
523
|
-
const validate = async () => {
|
|
286
|
+
async function validate() {
|
|
524
287
|
isValidating.value = true;
|
|
525
288
|
try {
|
|
526
289
|
return await form.validateField(props.path);
|
|
527
290
|
} finally {
|
|
528
291
|
isValidating.value = false;
|
|
529
292
|
}
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (itemKeys.value.length > arrayLength) itemKeys.value.length = arrayLength;
|
|
535
|
-
else while (itemKeys.value.length < arrayLength) itemKeys.value.push(generateKey());
|
|
536
|
-
};
|
|
537
|
-
/**
|
|
538
|
-
* Applies an update to a copy of the current array, writes it back to the form values,
|
|
539
|
-
* and marks the array path as touched. Dirty state is derived from the form's Sets
|
|
540
|
-
* automatically via the computed above so no explicit dirty call is needed here.
|
|
541
|
-
*/
|
|
542
|
-
const mutate = (updater) => {
|
|
543
|
-
const current = [...array.value];
|
|
544
|
-
updater(current);
|
|
545
|
-
setProperty(form.values, props.path, current);
|
|
293
|
+
}
|
|
294
|
+
/** Handles the blur event for the field. */
|
|
295
|
+
function onBlur() {
|
|
296
|
+
clearDebounce();
|
|
546
297
|
form.touchField(props.path);
|
|
547
|
-
if (
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
mutate((current) => current.splice(index, 0, value));
|
|
566
|
-
};
|
|
567
|
-
const update = (index, value) => {
|
|
568
|
-
mutate((current) => {
|
|
569
|
-
current[index] = value;
|
|
570
|
-
});
|
|
571
|
-
};
|
|
572
|
-
const swap = (indexA, indexB) => {
|
|
573
|
-
mutate((current) => {
|
|
574
|
-
[current[indexA], current[indexB]] = [current[indexB], current[indexA]];
|
|
575
|
-
});
|
|
576
|
-
[itemKeys.value[indexA], itemKeys.value[indexB]] = [itemKeys.value[indexB], itemKeys.value[indexA]];
|
|
577
|
-
};
|
|
578
|
-
const move = (from, to) => {
|
|
579
|
-
mutate((current) => {
|
|
580
|
-
const [item] = current.splice(from, 1);
|
|
581
|
-
current.splice(to, 0, item);
|
|
582
|
-
});
|
|
583
|
-
const [key] = itemKeys.value.splice(from, 1);
|
|
584
|
-
itemKeys.value.splice(to, 0, key);
|
|
585
|
-
};
|
|
298
|
+
if (validateOn.value.onBlur) validate();
|
|
299
|
+
}
|
|
300
|
+
/** Handles the input event for the field. */
|
|
301
|
+
function onInput() {
|
|
302
|
+
updateDirty();
|
|
303
|
+
if (!validateOn.value.onInput) return;
|
|
304
|
+
if (form.validationMode.eager && !isValid.value) scheduleValidation();
|
|
305
|
+
}
|
|
306
|
+
/** Handles the change event for the field. */
|
|
307
|
+
function onChange() {
|
|
308
|
+
updateDirty();
|
|
309
|
+
if (!validateOn.value.onChange) return;
|
|
310
|
+
if (form.validationMode.eager && !isValid.value) scheduleValidation();
|
|
311
|
+
}
|
|
312
|
+
/** Handles the focus event for the field. */
|
|
313
|
+
function onFocus() {
|
|
314
|
+
if (validateOn.value.onFocus) scheduleValidation();
|
|
315
|
+
}
|
|
586
316
|
onMounted(async () => {
|
|
587
317
|
await nextTick();
|
|
588
318
|
if (validateOn.value.onMount) validate();
|
|
589
319
|
});
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
320
|
+
onUnmounted(() => {
|
|
321
|
+
clearDebounce();
|
|
322
|
+
});
|
|
323
|
+
return (_ctx, _cache) => {
|
|
324
|
+
return renderSlot(_ctx.$slots, "default", {
|
|
325
|
+
errors: errors.value,
|
|
326
|
+
isValid: isValid.value,
|
|
327
|
+
isTouched: isTouched.value,
|
|
328
|
+
isDirty: isDirty.value,
|
|
329
|
+
isValidating: isValidating.value,
|
|
330
|
+
validate,
|
|
331
|
+
events: {
|
|
332
|
+
onBlur,
|
|
333
|
+
onChange,
|
|
334
|
+
onFocus,
|
|
335
|
+
onInput
|
|
336
|
+
},
|
|
337
|
+
path: __props.path,
|
|
338
|
+
value: value.value
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/components/not-form.vue
|
|
345
|
+
var not_form_default = /* @__PURE__ */ defineComponent({
|
|
346
|
+
__name: "not-form",
|
|
347
|
+
props: { form: {
|
|
348
|
+
type: null,
|
|
349
|
+
required: true
|
|
350
|
+
} },
|
|
351
|
+
setup(__props) {
|
|
352
|
+
provideNotFormInstance(__props.form);
|
|
618
353
|
return (_ctx, _cache) => {
|
|
619
|
-
return renderSlot(_ctx.$slots, "default"
|
|
354
|
+
return openBlock(), createElementBlock("form", null, [renderSlot(_ctx.$slots, "default")]);
|
|
620
355
|
};
|
|
621
356
|
}
|
|
622
357
|
});
|
|
623
358
|
//#endregion
|
|
359
|
+
//#region src/components/not-message.vue
|
|
360
|
+
var not_message_default = /* @__PURE__ */ defineComponent({
|
|
361
|
+
inheritAttrs: false,
|
|
362
|
+
__name: "not-message",
|
|
363
|
+
props: {
|
|
364
|
+
as: {
|
|
365
|
+
type: null,
|
|
366
|
+
required: false,
|
|
367
|
+
default: "span"
|
|
368
|
+
},
|
|
369
|
+
path: {
|
|
370
|
+
type: String,
|
|
371
|
+
required: true
|
|
372
|
+
},
|
|
373
|
+
form: {
|
|
374
|
+
type: null,
|
|
375
|
+
required: false
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
setup(__props) {
|
|
379
|
+
const props = __props;
|
|
380
|
+
const form = useNotFormInstance(props.form);
|
|
381
|
+
const message = computed(() => form.errorsMap.value[props.path]);
|
|
382
|
+
return (_ctx, _cache) => {
|
|
383
|
+
return message.value ? (openBlock(), createBlock(resolveDynamicComponent(__props.as), normalizeProps(mergeProps({ key: 0 }, _ctx.$attrs)), {
|
|
384
|
+
default: withCtx(() => [renderSlot(_ctx.$slots, "default", { message: message.value }, () => [createTextVNode(toDisplayString(message.value), 1)])]),
|
|
385
|
+
_: 3
|
|
386
|
+
}, 16)) : createCommentVNode("v-if", true);
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
//#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
|
+
//#region src/composables/use-not-form.ts
|
|
419
|
+
/**
|
|
420
|
+
* Creates a reactive NotFormInstance for managing form state and validation.
|
|
421
|
+
* @template TSchema The standard schema type.
|
|
422
|
+
* @param config Configuration object.
|
|
423
|
+
* @returns A reactive NotFormInstance.
|
|
424
|
+
*/
|
|
425
|
+
function useNotForm(config) {
|
|
426
|
+
let initialValues = klona(config.initialValues ?? {});
|
|
427
|
+
let initialErrors = klona(config.initialErrors ?? []);
|
|
428
|
+
const validateOn = {
|
|
429
|
+
onBlur: config.validateOn?.onBlur ?? true,
|
|
430
|
+
onChange: config.validateOn?.onChange ?? true,
|
|
431
|
+
onFocus: config.validateOn?.onFocus ?? false,
|
|
432
|
+
onInput: config.validateOn?.onInput ?? true,
|
|
433
|
+
onMount: config.validateOn?.onMount ?? false
|
|
434
|
+
};
|
|
435
|
+
const validationMode = {
|
|
436
|
+
eager: config.validationMode?.eager ?? true,
|
|
437
|
+
lazy: config.validationMode?.lazy ?? false
|
|
438
|
+
};
|
|
439
|
+
const values = reactive(klona(initialValues));
|
|
440
|
+
const errors = reactive([...initialErrors]);
|
|
441
|
+
const touchedFields = reactive(/* @__PURE__ */ new Set());
|
|
442
|
+
const dirtyFields = reactive(/* @__PURE__ */ new Set());
|
|
443
|
+
const isSubmitting = ref(false);
|
|
444
|
+
const isValidating = ref(false);
|
|
445
|
+
/**
|
|
446
|
+
* Tracks concurrent validations to keep `isValidating` true until all finish.
|
|
447
|
+
* @internal
|
|
448
|
+
*/
|
|
449
|
+
let validatingCount = 0;
|
|
450
|
+
const isValid = computed(() => errors.length === 0);
|
|
451
|
+
const isDirty = computed(() => dirtyFields.size > 0);
|
|
452
|
+
const isTouched = computed(() => touchedFields.size > 0);
|
|
453
|
+
const errorsMap = computed(() => {
|
|
454
|
+
const result = {};
|
|
455
|
+
for (const issue of errors) if (issue.path) {
|
|
456
|
+
const path = issue.path.map((element) => normalizeSegment(element)).join(".");
|
|
457
|
+
if (path && !result[path]) result[path] = issue.message;
|
|
458
|
+
}
|
|
459
|
+
return result;
|
|
460
|
+
});
|
|
461
|
+
/**
|
|
462
|
+
* Increments validation counter and activates `isValidating`.
|
|
463
|
+
* @internal
|
|
464
|
+
*/
|
|
465
|
+
function beginValidating() {
|
|
466
|
+
validatingCount++;
|
|
467
|
+
isValidating.value = true;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Decrements validation counter and deactivates `isValidating` when zero.
|
|
471
|
+
* @internal
|
|
472
|
+
*/
|
|
473
|
+
function endValidating() {
|
|
474
|
+
validatingCount--;
|
|
475
|
+
if (validatingCount === 0) isValidating.value = false;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Resolves and executes the standard schema validation.
|
|
479
|
+
* @internal
|
|
480
|
+
* @returns The validation result containing either `value` or `issues`.
|
|
481
|
+
*/
|
|
482
|
+
function runSchema() {
|
|
483
|
+
return toValue(config.schema)["~standard"].validate(values);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Iterates through deeply nested keys to mark all as touched.
|
|
487
|
+
* @internal
|
|
488
|
+
*/
|
|
489
|
+
function touchAllFields() {
|
|
490
|
+
for (const path of deepKeys(values)) touchedFields.add(path);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Removes a specific field path from the dirty tracking set.
|
|
494
|
+
* @internal
|
|
495
|
+
* @param path The field path to mark as clean.
|
|
496
|
+
*/
|
|
497
|
+
function unDirtyField(path) {
|
|
498
|
+
dirtyFields.delete(path);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Iterates through deeply nested keys to mark all as dirty.
|
|
502
|
+
* @internal
|
|
503
|
+
*/
|
|
504
|
+
function dirtyAllFields() {
|
|
505
|
+
for (const path of deepKeys(values)) dirtyFields.add(path);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Marks a specific field path as touched.
|
|
509
|
+
* @param path The field path to mark as touched.
|
|
510
|
+
*/
|
|
511
|
+
function touchField(path) {
|
|
512
|
+
touchedFields.add(path);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Marks a specific field path as dirty.
|
|
516
|
+
* @param path The field path to mark as dirty.
|
|
517
|
+
*/
|
|
518
|
+
function dirtyField(path) {
|
|
519
|
+
dirtyFields.add(path);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Sets a value at the specified path and updates dirty tracking based on comparison with initial values.
|
|
523
|
+
* @param path The field path to set the value at.
|
|
524
|
+
* @param value The value to set.
|
|
525
|
+
*/
|
|
526
|
+
function setValue(path, value) {
|
|
527
|
+
setProperty(values, path, value);
|
|
528
|
+
if (dequal(value, getProperty(initialValues, path))) unDirtyField(path);
|
|
529
|
+
else dirtyField(path);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Sets or updates an error issue in the errors array, replacing any existing issue for the same path.
|
|
533
|
+
* @param newIssue The issue to set.
|
|
534
|
+
*/
|
|
535
|
+
function setError(newIssue) {
|
|
536
|
+
const newPath = newIssue.path?.map((element) => normalizeSegment(element)).join(".");
|
|
537
|
+
const existingIndex = errors.findIndex((error) => {
|
|
538
|
+
return error.path?.map((element) => normalizeSegment(element)).join(".") === newPath;
|
|
539
|
+
});
|
|
540
|
+
if (existingIndex === -1) errors.push(newIssue);
|
|
541
|
+
else errors[existingIndex] = newIssue;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Sets multiple error issues in the errors array, replacing any existing issues.
|
|
545
|
+
* @param newIssues The array of issues to set.
|
|
546
|
+
*/
|
|
547
|
+
function setErrors(newIssues) {
|
|
548
|
+
errors.splice(0, errors.length, ...newIssues);
|
|
549
|
+
}
|
|
550
|
+
/** Clears all error issues from the errors array. */
|
|
551
|
+
function clearErrors() {
|
|
552
|
+
errors.splice(0);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Retrieves all error issues associated with a specific field path.
|
|
556
|
+
* @param path The field path to retrieve errors for.
|
|
557
|
+
* @returns An array of error issues associated with the field path.
|
|
558
|
+
*/
|
|
559
|
+
function getFieldErrors(path) {
|
|
560
|
+
const pathSegments = parsePath(path);
|
|
561
|
+
return errors.filter((error) => isIssuePathEqual(error.path, pathSegments));
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Validates the entire form against the schema, updating errors and returning the result.
|
|
565
|
+
* @returns An object containing either `value` or `issues` based on validation outcome.
|
|
566
|
+
*/
|
|
567
|
+
async function validate() {
|
|
568
|
+
beginValidating();
|
|
569
|
+
try {
|
|
570
|
+
const result = await runSchema();
|
|
571
|
+
if (result?.issues) {
|
|
572
|
+
setErrors([...result.issues]);
|
|
573
|
+
return { issues: result.issues };
|
|
574
|
+
}
|
|
575
|
+
clearErrors();
|
|
576
|
+
return { value: result.value };
|
|
577
|
+
} finally {
|
|
578
|
+
endValidating();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Validates a specific field against the schema, updating errors and returning the result.
|
|
583
|
+
* @param path The field path to validate.
|
|
584
|
+
* @returns An object containing either `value` or `issues` based on validation outcome.
|
|
585
|
+
*/
|
|
586
|
+
async function validateField(path) {
|
|
587
|
+
beginValidating();
|
|
588
|
+
try {
|
|
589
|
+
const result = await runSchema();
|
|
590
|
+
const pathSegments = parsePath(path);
|
|
591
|
+
const staleIndices = [];
|
|
592
|
+
for (const [index, error] of errors.entries()) if (isIssuePathEqual(error.path, pathSegments)) staleIndices.push(index);
|
|
593
|
+
for (let i = staleIndices.length - 1; i >= 0; i--) errors.splice(staleIndices[i], 1);
|
|
594
|
+
if (result?.issues) {
|
|
595
|
+
const fieldIssues = result.issues.filter((issue) => isIssuePathEqual(issue.path, pathSegments));
|
|
596
|
+
if (fieldIssues.length > 0) {
|
|
597
|
+
errors.push(...fieldIssues);
|
|
598
|
+
return { issues: fieldIssues };
|
|
599
|
+
}
|
|
600
|
+
return { value: getProperty(values, path) };
|
|
601
|
+
}
|
|
602
|
+
return { value: getProperty(result.value, path) };
|
|
603
|
+
} finally {
|
|
604
|
+
endValidating();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Submits the form, validating it and executing the submit handler if provided.
|
|
609
|
+
* @param event The submit event.
|
|
610
|
+
*/
|
|
611
|
+
async function submit(event) {
|
|
612
|
+
isSubmitting.value = true;
|
|
613
|
+
try {
|
|
614
|
+
touchAllFields();
|
|
615
|
+
dirtyAllFields();
|
|
616
|
+
const result = await validate();
|
|
617
|
+
if (result?.issues) {
|
|
618
|
+
event.preventDefault();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (config.onSubmit) {
|
|
622
|
+
event.preventDefault();
|
|
623
|
+
await config.onSubmit(result.value);
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
event.preventDefault();
|
|
627
|
+
} finally {
|
|
628
|
+
isSubmitting.value = false;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Resets the form to its initial state.
|
|
633
|
+
* @param newValues The new values to reset the form with.
|
|
634
|
+
* @param newErrors The new errors to reset the form with.
|
|
635
|
+
*/
|
|
636
|
+
function reset(newValues, newErrors) {
|
|
637
|
+
if (newValues) initialValues = klona(newValues);
|
|
638
|
+
if (newErrors) initialErrors = klona(newErrors);
|
|
639
|
+
const freshValues = klona(initialValues);
|
|
640
|
+
for (const key of Object.keys(values)) if (!hasProperty(freshValues, key)) deleteProperty(values, key);
|
|
641
|
+
for (const key of Object.keys(freshValues)) setProperty(values, key, getProperty(freshValues, key));
|
|
642
|
+
errors.splice(0, errors.length, ...klona(initialErrors));
|
|
643
|
+
touchedFields.clear();
|
|
644
|
+
dirtyFields.clear();
|
|
645
|
+
}
|
|
646
|
+
return markRaw({
|
|
647
|
+
get initialErrors() {
|
|
648
|
+
return initialErrors;
|
|
649
|
+
},
|
|
650
|
+
get initialValues() {
|
|
651
|
+
return initialValues;
|
|
652
|
+
},
|
|
653
|
+
validateOn,
|
|
654
|
+
validationMode,
|
|
655
|
+
setValue,
|
|
656
|
+
values,
|
|
657
|
+
isTouched,
|
|
658
|
+
touchedFields,
|
|
659
|
+
touchField,
|
|
660
|
+
dirtyField,
|
|
661
|
+
dirtyFields,
|
|
662
|
+
isDirty,
|
|
663
|
+
clearErrors,
|
|
664
|
+
errors,
|
|
665
|
+
errorsMap,
|
|
666
|
+
getFieldErrors,
|
|
667
|
+
setError,
|
|
668
|
+
setErrors,
|
|
669
|
+
isValid,
|
|
670
|
+
isValidating,
|
|
671
|
+
validate,
|
|
672
|
+
validateField,
|
|
673
|
+
isSubmitting,
|
|
674
|
+
submit,
|
|
675
|
+
reset
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
//#endregion
|
|
624
679
|
export { not_array_field_default as NotArrayField, not_field_default as NotField, not_form_default as NotForm, not_message_default as NotMessage, useNotForm };
|