@teamnovu/kit-vue-forms 0.1.15 → 0.1.16
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/dist/composables/useField.d.ts +3 -2
- package/dist/composables/useValidation.d.ts +1 -0
- package/dist/index.js +344 -315
- package/dist/utils/path.d.ts +4 -1
- package/package.json +1 -1
- package/src/composables/useField.ts +16 -4
- package/src/composables/useFieldRegistry.ts +2 -1
- package/src/composables/useForm.ts +47 -43
- package/src/composables/useValidation.ts +17 -0
- package/src/utils/path.ts +81 -52
- package/tests/useForm.test.ts +40 -0
- package/tests/useValidation.test.ts +26 -2
package/dist/utils/path.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { MaybeRef, WritableComputedRef } from 'vue';
|
|
2
2
|
import { Paths, PickProps, SplitPath } from '../types/util';
|
|
3
3
|
import { ErrorBag } from '../types/validation';
|
|
4
|
+
import { FormField } from '../types/form';
|
|
4
5
|
export declare function splitPath(path: string): string[];
|
|
6
|
+
export declare function existsPath<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>): boolean;
|
|
7
|
+
export declare function existsFieldPath<T, K extends Paths<T>>(field: FormField<T, K>): boolean;
|
|
5
8
|
export declare function getNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>): PickProps<T, K>;
|
|
6
9
|
export declare function setNestedValue<T, K extends Paths<T>>(obj: MaybeRef<T>, path: K | SplitPath<K>, value: PickProps<T, K>): void;
|
|
7
10
|
export declare const getLens: <T, K extends Paths<T>>(data: MaybeRef<T>, key: MaybeRef<K | SplitPath<K>>) => WritableComputedRef<PickProps<T, K>, PickProps<T, K>>;
|
|
8
|
-
type JoinPath<Base extends string, Sub extends string> = `${Base}${Base extends
|
|
11
|
+
type JoinPath<Base extends string, Sub extends string> = `${Base}${Base extends "" ? "" : Sub extends "" ? "" : "."}${Sub}`;
|
|
9
12
|
export declare function joinPath<Base extends string, Sub extends string>(basePath: Base, subPath: Sub): JoinPath<Base, Sub>;
|
|
10
13
|
export declare function filterErrorsForPath(errors: ErrorBag, path: string): ErrorBag;
|
|
11
14
|
export {};
|
package/package.json
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { Awaitable } from '@vueuse/core'
|
|
2
|
+
import { computed, reactive, shallowRef, toRefs, unref, watch, type MaybeRef, type MaybeRefOrGetter, type Ref } from 'vue'
|
|
2
3
|
import type { FormField } from '../types/form'
|
|
3
4
|
import type { ValidationErrorMessage, ValidationErrors } from '../types/validation'
|
|
4
5
|
import { cloneRefValue } from '../utils/general'
|
|
5
|
-
import type { Awaitable } from '@vueuse/core'
|
|
6
6
|
|
|
7
7
|
export interface UseFieldOptions<T, K extends string> {
|
|
8
8
|
value?: MaybeRef<T>
|
|
9
9
|
initialValue?: MaybeRefOrGetter<Readonly<T>>
|
|
10
10
|
path: K
|
|
11
11
|
errors?: Ref<ValidationErrors>
|
|
12
|
+
existsInForm?: MaybeRef<boolean>
|
|
12
13
|
onBlur?: () => Awaitable<void>
|
|
13
14
|
onFocus?: () => Awaitable<void>
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function useField<T, K extends string>(
|
|
17
|
+
export function useField<T, K extends string>(fieldOptions: UseFieldOptions<T, K>): FormField<T, K> {
|
|
18
|
+
const defaultOptions = {
|
|
19
|
+
existsInForm: true,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const options = {
|
|
23
|
+
...defaultOptions,
|
|
24
|
+
...fieldOptions,
|
|
25
|
+
}
|
|
17
26
|
const initialValue = shallowRef(Object.freeze(cloneRefValue(options.initialValue))) as Ref<Readonly<T | undefined>>
|
|
18
27
|
|
|
19
28
|
const state = reactive({
|
|
@@ -53,7 +62,10 @@ export function useField<T, K extends string>(options: UseFieldOptions<T, K>): F
|
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
const reset = (): void => {
|
|
56
|
-
|
|
65
|
+
const lastPathPart = state.path.split('.').at(-1) || ''
|
|
66
|
+
if (unref(options.existsInForm) && !/^\d+$/.test(lastPathPart)) {
|
|
67
|
+
state.value = cloneRefValue(state.initialValue)
|
|
68
|
+
}
|
|
57
69
|
state.touched = false
|
|
58
70
|
state.errors = []
|
|
59
71
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from 'vue'
|
|
12
12
|
import type { FieldsTuple, FormDataDefault, FormField } from '../types/form'
|
|
13
13
|
import type { Paths, PickProps } from '../types/util'
|
|
14
|
-
import { getLens, getNestedValue } from '../utils/path'
|
|
14
|
+
import { existsPath, getLens, getNestedValue } from '../utils/path'
|
|
15
15
|
import { Rc } from '../utils/rc'
|
|
16
16
|
import { useField, type UseFieldOptions } from './useField'
|
|
17
17
|
import type { ValidationState } from './useValidation'
|
|
@@ -118,6 +118,7 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
118
118
|
value: getLens(toRef(formState, 'data'), path),
|
|
119
119
|
initialValue: alwaysComputed(() =>
|
|
120
120
|
getNestedValue(formState.initialData, path)),
|
|
121
|
+
existsInForm: computed(() => existsPath(formState.data, unref(path))),
|
|
121
122
|
errors: computed({
|
|
122
123
|
get() {
|
|
123
124
|
return validationState.errors.value.propertyErrors[path] || []
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Awaitable } from
|
|
1
|
+
import type { Awaitable } from "@vueuse/core";
|
|
2
2
|
import {
|
|
3
3
|
computed,
|
|
4
4
|
reactive,
|
|
@@ -9,82 +9,86 @@ import {
|
|
|
9
9
|
type MaybeRef,
|
|
10
10
|
type MaybeRefOrGetter,
|
|
11
11
|
type Ref,
|
|
12
|
-
} from
|
|
13
|
-
import type {
|
|
14
|
-
import type { EntityPaths, PickEntity } from
|
|
15
|
-
import type { ValidationStrategy } from
|
|
16
|
-
import { cloneRefValue } from
|
|
17
|
-
import { useFieldRegistry } from
|
|
18
|
-
import { useFormState } from
|
|
19
|
-
import { createSubformInterface, type SubformOptions } from
|
|
20
|
-
import { useValidation, type ValidationOptions } from
|
|
12
|
+
} from "vue";
|
|
13
|
+
import type { Form, FormDataDefault } from "../types/form";
|
|
14
|
+
import type { EntityPaths, PickEntity } from "../types/util";
|
|
15
|
+
import type { ValidationStrategy } from "../types/validation";
|
|
16
|
+
import { cloneRefValue } from "../utils/general";
|
|
17
|
+
import { useFieldRegistry } from "./useFieldRegistry";
|
|
18
|
+
import { useFormState } from "./useFormState";
|
|
19
|
+
import { createSubformInterface, type SubformOptions } from "./useSubform";
|
|
20
|
+
import { useValidation, type ValidationOptions } from "./useValidation";
|
|
21
21
|
|
|
22
22
|
// TODO @Elias implement validation strategy handling
|
|
23
23
|
|
|
24
24
|
export interface UseFormOptions<T extends FormDataDefault>
|
|
25
25
|
extends ValidationOptions<T> {
|
|
26
|
-
initialData: MaybeRefOrGetter<T
|
|
27
|
-
validationStrategy?: MaybeRef<ValidationStrategy
|
|
28
|
-
keepValuesOnUnmount?: MaybeRef<boolean
|
|
26
|
+
initialData: MaybeRefOrGetter<T>;
|
|
27
|
+
validationStrategy?: MaybeRef<ValidationStrategy>;
|
|
28
|
+
keepValuesOnUnmount?: MaybeRef<boolean>;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
32
32
|
const initialData = computed(() =>
|
|
33
|
-
Object.freeze(cloneRefValue(options.initialData))
|
|
33
|
+
Object.freeze(cloneRefValue(options.initialData)),
|
|
34
|
+
);
|
|
34
35
|
|
|
35
|
-
const data = ref<T>(cloneRefValue(initialData)) as Ref<T
|
|
36
|
+
const data = ref<T>(cloneRefValue(initialData)) as Ref<T>;
|
|
36
37
|
|
|
37
38
|
const state = reactive({
|
|
38
39
|
initialData,
|
|
39
40
|
data,
|
|
40
|
-
})
|
|
41
|
+
});
|
|
41
42
|
|
|
42
43
|
watch(
|
|
43
44
|
initialData,
|
|
44
45
|
(newValue) => {
|
|
45
|
-
state.data = cloneRefValue(newValue)
|
|
46
|
+
state.data = cloneRefValue(newValue);
|
|
46
47
|
},
|
|
47
|
-
{ flush:
|
|
48
|
-
)
|
|
48
|
+
{ flush: "sync" },
|
|
49
|
+
);
|
|
49
50
|
|
|
50
|
-
const validationState = useValidation(state, options)
|
|
51
|
+
const validationState = useValidation(state, options);
|
|
51
52
|
const fieldRegistry = useFieldRegistry(state, validationState, {
|
|
52
53
|
keepValuesOnUnmount: options.keepValuesOnUnmount,
|
|
53
|
-
onBlur: async () => {
|
|
54
|
-
if (unref(options.validationStrategy) ===
|
|
55
|
-
|
|
54
|
+
onBlur: async (path: string) => {
|
|
55
|
+
if (unref(options.validationStrategy) === "onTouch") {
|
|
56
|
+
// TODO: Only validate the specific field that was touched
|
|
57
|
+
validationState.validateField(path);
|
|
56
58
|
}
|
|
57
59
|
},
|
|
58
|
-
})
|
|
59
|
-
const formState = useFormState(fieldRegistry)
|
|
60
|
+
});
|
|
61
|
+
const formState = useFormState(fieldRegistry);
|
|
60
62
|
|
|
61
63
|
const submitHandler = (onSubmit: (data: T) => Awaitable<void>) => {
|
|
62
64
|
return async (event: SubmitEvent) => {
|
|
63
|
-
event.preventDefault()
|
|
65
|
+
event.preventDefault();
|
|
64
66
|
|
|
65
|
-
if (unref(options.validationStrategy) !==
|
|
66
|
-
await validationState.validateForm()
|
|
67
|
+
if (unref(options.validationStrategy) !== "none") {
|
|
68
|
+
await validationState.validateForm();
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
if (!validationState.isValid.value) {
|
|
70
|
-
return
|
|
72
|
+
return;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
await onSubmit(state.data)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
75
|
+
await onSubmit(state.data);
|
|
76
|
+
};
|
|
77
|
+
};
|
|
76
78
|
|
|
77
79
|
const reset = () => {
|
|
78
|
-
data.value = cloneRefValue(initialData)
|
|
79
|
-
validationState.reset()
|
|
80
|
-
fieldRegistry.fields.value
|
|
81
|
-
|
|
80
|
+
data.value = cloneRefValue(initialData);
|
|
81
|
+
validationState.reset();
|
|
82
|
+
for (const field of fieldRegistry.fields.value) {
|
|
83
|
+
field.reset();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
82
86
|
|
|
83
87
|
function getSubForm<K extends EntityPaths<T>>(
|
|
84
88
|
path: K,
|
|
85
89
|
options?: SubformOptions<PickEntity<T, K>>,
|
|
86
90
|
): Form<PickEntity<T, K>> {
|
|
87
|
-
return createSubformInterface(formInterface, path, options)
|
|
91
|
+
return createSubformInterface(formInterface, path, options);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
const formInterface: Form<T> = {
|
|
@@ -94,13 +98,13 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
|
94
98
|
reset,
|
|
95
99
|
getSubForm,
|
|
96
100
|
submitHandler,
|
|
97
|
-
initialData: toRef(state,
|
|
98
|
-
data: toRef(state,
|
|
99
|
-
}
|
|
101
|
+
initialData: toRef(state, "initialData") as Form<T>["initialData"],
|
|
102
|
+
data: toRef(state, "data") as Form<T>["data"],
|
|
103
|
+
};
|
|
100
104
|
|
|
101
|
-
if (unref(options.validationStrategy) ===
|
|
102
|
-
validationState.validateForm()
|
|
105
|
+
if (unref(options.validationStrategy) === "onFormOpen") {
|
|
106
|
+
validationState.validateForm();
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
return formInterface
|
|
109
|
+
return formInterface;
|
|
106
110
|
}
|
|
@@ -215,6 +215,22 @@ export function useValidation<T extends FormDataDefault>(
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
const validateField = async (path: string): Promise<ValidationResult> => {
|
|
219
|
+
const validationResults = await getValidationResults()
|
|
220
|
+
|
|
221
|
+
updateErrors({
|
|
222
|
+
general: validationResults.errors.general,
|
|
223
|
+
propertyErrors: {
|
|
224
|
+
[path]: validationResults.errors.propertyErrors[path],
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
isValid: !hasErrors(validationResults.errors),
|
|
230
|
+
errors: validationState.errors,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
218
234
|
const isValid = computed(() => !hasErrors(validationState.errors))
|
|
219
235
|
|
|
220
236
|
const reset = () => {
|
|
@@ -225,6 +241,7 @@ export function useValidation<T extends FormDataDefault>(
|
|
|
225
241
|
return {
|
|
226
242
|
...toRefs(validationState),
|
|
227
243
|
validateForm,
|
|
244
|
+
validateField,
|
|
228
245
|
defineValidator,
|
|
229
246
|
isValid,
|
|
230
247
|
reset,
|
package/src/utils/path.ts
CHANGED
|
@@ -1,100 +1,129 @@
|
|
|
1
|
-
import { computed, isRef, unref, type MaybeRef } from
|
|
2
|
-
import type { Paths, PickProps, SplitPath } from
|
|
3
|
-
import type { ErrorBag, ValidationErrors } from
|
|
1
|
+
import { computed, isRef, unref, type MaybeRef } from "vue";
|
|
2
|
+
import type { Paths, PickProps, SplitPath } from "../types/util";
|
|
3
|
+
import type { ErrorBag, ValidationErrors } from "../types/validation";
|
|
4
|
+
import type { FormField } from "../types/form";
|
|
4
5
|
|
|
5
6
|
export function splitPath(path: string): string[] {
|
|
6
|
-
if (path ===
|
|
7
|
-
return []
|
|
7
|
+
if (path === "") {
|
|
8
|
+
return [];
|
|
8
9
|
}
|
|
9
|
-
return path.split(/\s*\.\s*/).filter(Boolean)
|
|
10
|
+
return path.split(/\s*\.\s*/).filter(Boolean);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function
|
|
13
|
-
|
|
13
|
+
export function existsPath<T, K extends Paths<T>>(
|
|
14
|
+
obj: T,
|
|
15
|
+
path: K | SplitPath<K>,
|
|
16
|
+
): boolean {
|
|
17
|
+
const splittedPath = Array.isArray(path) ? path : splitPath(path) as SplitPath<K>;
|
|
18
|
+
return !!getNestedValue(obj, splittedPath.slice(0, -1) as SplitPath<K>);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function existsFieldPath<T, K extends Paths<T>>(field: FormField<T, K>) {
|
|
22
|
+
return existsPath(field.data.value, field.path.value)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getNestedValue<T, K extends Paths<T>>(
|
|
26
|
+
obj: T,
|
|
27
|
+
path: K | SplitPath<K>,
|
|
28
|
+
) {
|
|
29
|
+
const splittedPath = Array.isArray(path) ? path : splitPath(path);
|
|
14
30
|
return splittedPath.reduce(
|
|
15
31
|
(current, key) => current?.[key],
|
|
16
32
|
obj as Record<string, never>,
|
|
17
|
-
) as PickProps<T, K
|
|
33
|
+
) as PickProps<T, K>;
|
|
18
34
|
}
|
|
19
35
|
|
|
20
|
-
export function setNestedValue<T, K extends Paths<T>>(
|
|
21
|
-
|
|
36
|
+
export function setNestedValue<T, K extends Paths<T>>(
|
|
37
|
+
obj: MaybeRef<T>,
|
|
38
|
+
path: K | SplitPath<K>,
|
|
39
|
+
value: PickProps<T, K>,
|
|
40
|
+
): void {
|
|
41
|
+
const keys = Array.isArray(path) ? path : splitPath(path);
|
|
22
42
|
|
|
23
|
-
const lastKey = keys.at(-1)
|
|
43
|
+
const lastKey = keys.at(-1)!;
|
|
24
44
|
|
|
25
45
|
if (!lastKey) {
|
|
26
46
|
if (!isRef(obj)) {
|
|
27
47
|
// We cannot do anything here as we have nothing we can assign to
|
|
28
|
-
return
|
|
48
|
+
return;
|
|
29
49
|
}
|
|
30
50
|
|
|
31
|
-
obj.value = value
|
|
51
|
+
obj.value = value;
|
|
32
52
|
} else {
|
|
33
|
-
const target = keys
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
target[lastKey] = value
|
|
53
|
+
const target = keys.slice(0, -1).reduce(
|
|
54
|
+
(current, key) => {
|
|
55
|
+
if (current?.[key] === undefined) {
|
|
56
|
+
// Create the nested object if it doesn't exist
|
|
57
|
+
current[key] = {};
|
|
58
|
+
}
|
|
59
|
+
return current?.[key];
|
|
60
|
+
},
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
unref(obj) as Record<string, any>,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
target[lastKey] = value;
|
|
48
66
|
}
|
|
49
67
|
}
|
|
50
68
|
|
|
51
|
-
export const getLens = <T, K extends Paths<T>>(
|
|
69
|
+
export const getLens = <T, K extends Paths<T>>(
|
|
70
|
+
data: MaybeRef<T>,
|
|
71
|
+
key: MaybeRef<K | SplitPath<K>>,
|
|
72
|
+
) => {
|
|
52
73
|
return computed({
|
|
53
74
|
get() {
|
|
54
|
-
return getNestedValue(unref(data), unref(key))
|
|
75
|
+
return getNestedValue(unref(data), unref(key));
|
|
55
76
|
},
|
|
56
77
|
set(value: PickProps<T, K>) {
|
|
57
|
-
setNestedValue(data, unref(key), value)
|
|
78
|
+
setNestedValue(data, unref(key), value);
|
|
58
79
|
},
|
|
59
|
-
})
|
|
60
|
-
}
|
|
80
|
+
});
|
|
81
|
+
};
|
|
61
82
|
|
|
62
|
-
type JoinPath<
|
|
63
|
-
|
|
83
|
+
type JoinPath<
|
|
84
|
+
Base extends string,
|
|
85
|
+
Sub extends string,
|
|
86
|
+
> = `${Base}${Base extends "" ? "" : Sub extends "" ? "" : "."}${Sub}`;
|
|
87
|
+
export function joinPath<Base extends string, Sub extends string>(
|
|
88
|
+
basePath: Base,
|
|
89
|
+
subPath: Sub,
|
|
90
|
+
): JoinPath<Base, Sub> {
|
|
64
91
|
if (!basePath && !subPath) {
|
|
65
|
-
return
|
|
92
|
+
return "" as JoinPath<Base, Sub>;
|
|
66
93
|
}
|
|
67
94
|
|
|
68
95
|
if (!basePath && subPath) {
|
|
69
|
-
return subPath as JoinPath<Base, Sub
|
|
96
|
+
return subPath as JoinPath<Base, Sub>;
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
if (!subPath && basePath) {
|
|
73
|
-
return basePath as JoinPath<Base, Sub
|
|
100
|
+
return basePath as JoinPath<Base, Sub>;
|
|
74
101
|
}
|
|
75
102
|
|
|
76
|
-
return `${basePath}.${subPath}` as JoinPath<Base, Sub
|
|
103
|
+
return `${basePath}.${subPath}` as JoinPath<Base, Sub>;
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
export function filterErrorsForPath(errors: ErrorBag, path: string): ErrorBag {
|
|
80
107
|
// Handle empty path - return all errors
|
|
81
108
|
if (!path) {
|
|
82
|
-
return errors
|
|
109
|
+
return errors;
|
|
83
110
|
}
|
|
84
111
|
|
|
85
|
-
const pathPrefix = `${path}
|
|
86
|
-
const filteredPropertyErrors: Record<string, ValidationErrors> =
|
|
87
|
-
Object.
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
([errorPath, errorMessages]) => [
|
|
93
|
-
|
|
94
|
-
|
|
112
|
+
const pathPrefix = `${path}.`;
|
|
113
|
+
const filteredPropertyErrors: Record<string, ValidationErrors> =
|
|
114
|
+
Object.fromEntries(
|
|
115
|
+
Object.entries(errors.propertyErrors)
|
|
116
|
+
.filter(([errorPath]) => {
|
|
117
|
+
return errorPath.startsWith(pathPrefix);
|
|
118
|
+
})
|
|
119
|
+
.map(([errorPath, errorMessages]) => [
|
|
120
|
+
errorPath.slice(pathPrefix.length),
|
|
121
|
+
errorMessages,
|
|
122
|
+
]),
|
|
123
|
+
);
|
|
95
124
|
|
|
96
125
|
return {
|
|
97
126
|
general: errors.general, // Keep general errors
|
|
98
127
|
propertyErrors: filteredPropertyErrors,
|
|
99
|
-
}
|
|
128
|
+
};
|
|
100
129
|
}
|
package/tests/useForm.test.ts
CHANGED
|
@@ -405,6 +405,46 @@ describe("useForm", () => {
|
|
|
405
405
|
expect(form.data.value.name).toBe("A");
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
+
it("it not create empty objects if the field is going to be destroyed", async () => {
|
|
409
|
+
const form = useForm({
|
|
410
|
+
initialData: {
|
|
411
|
+
data: { names: [] as string[] },
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const scope = effectScope();
|
|
416
|
+
|
|
417
|
+
scope.run(() => {
|
|
418
|
+
const nameField = form.defineField({ path: "data.names.0" });
|
|
419
|
+
nameField.setData("Modified");
|
|
420
|
+
form.reset();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
scope.stop();
|
|
424
|
+
|
|
425
|
+
expect(form.data.value.data.names).toHaveLength(0);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("it not create empty objects if the nested array field is going to be destroyed", async () => {
|
|
429
|
+
const form = useForm({
|
|
430
|
+
initialData: {
|
|
431
|
+
data: [] as Array<{ name: string }>,
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const scope = effectScope();
|
|
436
|
+
|
|
437
|
+
scope.run(() => {
|
|
438
|
+
const nameField = form.defineField({ path: "data.0.name" });
|
|
439
|
+
nameField.setData("Modified");
|
|
440
|
+
form.reset();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
scope.stop();
|
|
444
|
+
|
|
445
|
+
expect(form.data.value.data).toHaveLength(0);
|
|
446
|
+
});
|
|
447
|
+
|
|
408
448
|
describe("useForm - submit handler", () => {
|
|
409
449
|
it(
|
|
410
450
|
"it should not call the handler when validation errors exist",
|
|
@@ -294,7 +294,31 @@ describe('useValidation', () => {
|
|
|
294
294
|
|
|
295
295
|
const nameField = form.getField('name')
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
// Simulate blur event
|
|
298
|
+
nameField.onBlur()
|
|
299
|
+
|
|
300
|
+
// onBlur is not async but the validation runs async
|
|
301
|
+
await delay()
|
|
302
|
+
|
|
303
|
+
expect(form.isValid.value).toBe(false)
|
|
304
|
+
expect(form.errors.value.propertyErrors.name).toHaveLength(1)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('should not validate other fields than the blurred one', async () => {
|
|
308
|
+
const schema = z.object({
|
|
309
|
+
name: z.string().min(2),
|
|
310
|
+
email: z.string().email(),
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
const initialData = { name: 'A', email: 'invalid-email' }
|
|
314
|
+
const form = useForm({
|
|
315
|
+
initialData,
|
|
316
|
+
schema,
|
|
317
|
+
validationStrategy: 'onTouch',
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const nameField = form.getField('name')
|
|
321
|
+
form.getField('email')
|
|
298
322
|
|
|
299
323
|
// Simulate blur event
|
|
300
324
|
nameField.onBlur()
|
|
@@ -302,9 +326,9 @@ describe('useValidation', () => {
|
|
|
302
326
|
// onBlur is not async but the validation runs async
|
|
303
327
|
await delay()
|
|
304
328
|
|
|
305
|
-
expect(form.isValidated.value).toBe(true)
|
|
306
329
|
expect(form.isValid.value).toBe(false)
|
|
307
330
|
expect(form.errors.value.propertyErrors.name).toHaveLength(1)
|
|
331
|
+
expect(form.errors.value.propertyErrors.email ?? []).toHaveLength(0)
|
|
308
332
|
})
|
|
309
333
|
|
|
310
334
|
it('should validate the form on form open if configured', async () => {
|