@teamnovu/kit-vue-forms 0.1.14 → 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/components/FormFieldWrapper.vue.d.ts +1 -1
- package/dist/composables/useField.d.ts +7 -3
- package/dist/composables/useFieldRegistry.d.ts +13 -4
- package/dist/composables/useValidation.d.ts +1 -0
- package/dist/index.js +353 -278
- package/dist/types/form.d.ts +3 -1
- package/dist/utils/path.d.ts +4 -1
- package/docs/reference.md +0 -2
- package/package.json +1 -1
- package/src/components/FormFieldWrapper.vue +8 -0
- package/src/composables/useField.ts +23 -5
- package/src/composables/useFieldRegistry.ts +82 -15
- package/src/composables/useForm.ts +79 -32
- package/src/composables/useValidation.ts +17 -0
- package/src/types/form.ts +23 -8
- package/src/utils/path.ts +81 -46
- package/tests/subform.test.ts +17 -0
- package/tests/useField.test.ts +17 -0
- package/tests/useForm.test.ts +352 -200
- package/tests/useValidation.test.ts +97 -1
package/dist/types/form.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { Awaitable } from '@vueuse/core';
|
|
1
2
|
import { Ref } from 'vue';
|
|
2
3
|
import { DefineFieldOptions } from '../composables/useFieldRegistry';
|
|
3
4
|
import { SubformOptions } from '../composables/useSubform';
|
|
5
|
+
import { ValidatorOptions } from '../composables/useValidation';
|
|
4
6
|
import { EntityPaths, Paths, PickEntity, PickProps } from './util';
|
|
5
7
|
import { ErrorBag, ValidationErrorMessage, ValidationErrors, ValidationResult, Validator } from './validation';
|
|
6
|
-
import { ValidatorOptions } from '../composables/useValidation';
|
|
7
8
|
export type FormDataDefault = object;
|
|
8
9
|
export interface FormField<T, P extends string> {
|
|
9
10
|
data: Ref<T>;
|
|
@@ -42,5 +43,6 @@ export interface Form<T extends FormDataDefault> {
|
|
|
42
43
|
defineValidator: <TData extends T>(options: ValidatorOptions<TData> | Ref<Validator<TData>>) => Ref<Validator<TData> | undefined>;
|
|
43
44
|
reset: () => void;
|
|
44
45
|
validateForm: () => Promise<ValidationResult>;
|
|
46
|
+
submitHandler: (onSubmit: (data: T) => Awaitable<void>) => (event: SubmitEvent) => Promise<void>;
|
|
45
47
|
getSubForm: <P extends EntityPaths<T>>(path: P, options?: SubformOptions<PickEntity<T, P>>) => Form<PickEntity<T, P>>;
|
|
46
48
|
}
|
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/docs/reference.md
CHANGED
|
@@ -17,10 +17,8 @@ function useForm<T extends object>(options: {
|
|
|
17
17
|
// this is additional to the schema for custom validations
|
|
18
18
|
validateFn?: MaybeRef<ValidationFunction<T>>
|
|
19
19
|
// if the form data of a property should be reset or kept if all fields corresponding to this property are unmounted
|
|
20
|
-
// !! currently not implemented !!
|
|
21
20
|
keepValuesOnUnmount?: MaybeRef<boolean>
|
|
22
21
|
// when validation should be done (on touch, pre submit, etc.)
|
|
23
|
-
// !! currently not implemented !!
|
|
24
22
|
validationStrategy?: MaybeRef<ValidationStrategy>
|
|
25
23
|
}): Form<T>
|
|
26
24
|
```
|
package/package.json
CHANGED
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
@update:model-value="setData"
|
|
14
14
|
>
|
|
15
15
|
<slot />
|
|
16
|
+
|
|
17
|
+
<!-- https://vue-land.github.io/faq/forwarding-slots#passing-all-slots -->
|
|
18
|
+
<template
|
|
19
|
+
v-for="(_, slotName) in $slots"
|
|
20
|
+
#[slotName]="slotProps"
|
|
21
|
+
>
|
|
22
|
+
<slot :name="slotName" v-bind="slotProps ?? {}" />
|
|
23
|
+
</template>
|
|
16
24
|
</component>
|
|
17
25
|
</Field>
|
|
18
26
|
</template>
|
|
@@ -1,4 +1,5 @@
|
|
|
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'
|
|
@@ -7,10 +8,21 @@ export interface UseFieldOptions<T, K extends string> {
|
|
|
7
8
|
value?: MaybeRef<T>
|
|
8
9
|
initialValue?: MaybeRefOrGetter<Readonly<T>>
|
|
9
10
|
path: K
|
|
10
|
-
errors?:
|
|
11
|
+
errors?: Ref<ValidationErrors>
|
|
12
|
+
existsInForm?: MaybeRef<boolean>
|
|
13
|
+
onBlur?: () => Awaitable<void>
|
|
14
|
+
onFocus?: () => Awaitable<void>
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
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
|
+
}
|
|
14
26
|
const initialValue = shallowRef(Object.freeze(cloneRefValue(options.initialValue))) as Ref<Readonly<T | undefined>>
|
|
15
27
|
|
|
16
28
|
const state = reactive({
|
|
@@ -40,14 +52,20 @@ export function useField<T, K extends string>(options: UseFieldOptions<T, K>): F
|
|
|
40
52
|
|
|
41
53
|
const onBlur = (): void => {
|
|
42
54
|
state.touched = true
|
|
55
|
+
state.errors = []
|
|
56
|
+
|
|
57
|
+
options.onBlur?.()
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
const onFocus = (): void => {
|
|
46
|
-
|
|
61
|
+
options.onFocus?.()
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
const reset = (): void => {
|
|
50
|
-
|
|
65
|
+
const lastPathPart = state.path.split('.').at(-1) || ''
|
|
66
|
+
if (unref(options.existsInForm) && !/^\d+$/.test(lastPathPart)) {
|
|
67
|
+
state.value = cloneRefValue(state.initialValue)
|
|
68
|
+
}
|
|
51
69
|
state.touched = false
|
|
52
70
|
state.errors = []
|
|
53
71
|
}
|
|
@@ -1,23 +1,56 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
onScopeDispose,
|
|
4
|
+
shallowReactive,
|
|
5
|
+
shallowRef,
|
|
6
|
+
toRef,
|
|
7
|
+
triggerRef,
|
|
8
|
+
unref,
|
|
9
|
+
watchEffect,
|
|
10
|
+
type MaybeRef,
|
|
11
|
+
} from 'vue'
|
|
2
12
|
import type { FieldsTuple, FormDataDefault, FormField } from '../types/form'
|
|
3
13
|
import type { Paths, PickProps } from '../types/util'
|
|
4
|
-
import { getLens, getNestedValue } from '../utils/path'
|
|
14
|
+
import { existsPath, getLens, getNestedValue } from '../utils/path'
|
|
5
15
|
import { Rc } from '../utils/rc'
|
|
6
16
|
import { useField, type UseFieldOptions } from './useField'
|
|
7
17
|
import type { ValidationState } from './useValidation'
|
|
18
|
+
import type { Awaitable } from '@vueuse/core'
|
|
8
19
|
|
|
9
20
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
21
|
type FieldRegistryCache<T> = Map<Paths<T>, FormField<any, string>>
|
|
11
22
|
|
|
12
|
-
export type ResolvedFormField<T, K extends Paths<T>> = FormField<
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
export type ResolvedFormField<T, K extends Paths<T>> = FormField<
|
|
24
|
+
PickProps<T, K>,
|
|
25
|
+
K
|
|
26
|
+
>
|
|
27
|
+
|
|
28
|
+
export type DefineFieldOptions<F, K extends string> = Pick<
|
|
29
|
+
UseFieldOptions<F, K>,
|
|
30
|
+
'path'
|
|
31
|
+
> & {
|
|
32
|
+
onBlur?: () => void
|
|
33
|
+
onFocus?: () => void
|
|
34
|
+
}
|
|
15
35
|
|
|
16
|
-
interface FormState<
|
|
36
|
+
interface FormState<
|
|
37
|
+
T extends FormDataDefault,
|
|
38
|
+
TIn extends FormDataDefault = T,
|
|
39
|
+
> {
|
|
17
40
|
data: T
|
|
18
41
|
initialData: TIn
|
|
19
42
|
}
|
|
20
43
|
|
|
44
|
+
interface FieldRegistryOptions {
|
|
45
|
+
keepValuesOnUnmount?: MaybeRef<boolean>
|
|
46
|
+
onBlur?: (path: string) => Awaitable<void>
|
|
47
|
+
onFocus?: (path: string) => Awaitable<void>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const optionDefaults = {
|
|
51
|
+
keepValuesOnUnmount: true,
|
|
52
|
+
}
|
|
53
|
+
|
|
21
54
|
// A computed that always reflects the latest value from the getter
|
|
22
55
|
// This computed forces updates even if the value is the same (to trigger watchers)
|
|
23
56
|
function alwaysComputed<T>(getter: () => T) {
|
|
@@ -37,16 +70,26 @@ function alwaysComputed<T>(getter: () => T) {
|
|
|
37
70
|
export function useFieldRegistry<T extends FormDataDefault>(
|
|
38
71
|
formState: FormState<T>,
|
|
39
72
|
validationState: ValidationState<T>,
|
|
73
|
+
fieldRegistryOptions?: FieldRegistryOptions,
|
|
40
74
|
) {
|
|
41
75
|
const fieldReferenceCounter = new Map<Paths<T>, Rc>()
|
|
42
76
|
const fields = shallowReactive(new Map()) as FieldRegistryCache<T>
|
|
77
|
+
const registryOptions = {
|
|
78
|
+
...optionDefaults,
|
|
79
|
+
...fieldRegistryOptions,
|
|
80
|
+
}
|
|
43
81
|
|
|
44
|
-
const registerField = <K extends Paths<T>>(
|
|
82
|
+
const registerField = <K extends Paths<T>>(
|
|
83
|
+
field: ResolvedFormField<T, K>,
|
|
84
|
+
) => {
|
|
45
85
|
const path = unref(field.path) as Paths<T>
|
|
46
86
|
fields.set(path, field)
|
|
47
87
|
}
|
|
48
88
|
|
|
49
89
|
const deregisterField = (path: Paths<T>) => {
|
|
90
|
+
if (!registryOptions?.keepValuesOnUnmount) {
|
|
91
|
+
fields.get(path)?.reset()
|
|
92
|
+
}
|
|
50
93
|
fields.delete(path)
|
|
51
94
|
}
|
|
52
95
|
|
|
@@ -64,14 +107,18 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
64
107
|
}
|
|
65
108
|
}
|
|
66
109
|
|
|
67
|
-
const getField = <K extends Paths<T>>(
|
|
110
|
+
const getField = <K extends Paths<T>>(
|
|
111
|
+
options: DefineFieldOptions<PickProps<T, K>, K>,
|
|
112
|
+
): ResolvedFormField<T, K> => {
|
|
113
|
+
const { path } = options
|
|
114
|
+
|
|
68
115
|
if (!fields.has(path)) {
|
|
69
116
|
const field = useField({
|
|
70
117
|
path,
|
|
71
118
|
value: getLens(toRef(formState, 'data'), path),
|
|
72
|
-
initialValue: alwaysComputed(
|
|
73
|
-
|
|
74
|
-
),
|
|
119
|
+
initialValue: alwaysComputed(() =>
|
|
120
|
+
getNestedValue(formState.initialData, path)),
|
|
121
|
+
existsInForm: computed(() => existsPath(formState.data, unref(path))),
|
|
75
122
|
errors: computed({
|
|
76
123
|
get() {
|
|
77
124
|
return validationState.errors.value.propertyErrors[path] || []
|
|
@@ -80,6 +127,22 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
80
127
|
validationState.errors.value.propertyErrors[path] = newErrors
|
|
81
128
|
},
|
|
82
129
|
}),
|
|
130
|
+
onBlur: async () => {
|
|
131
|
+
await Promise.all(
|
|
132
|
+
[
|
|
133
|
+
registryOptions?.onBlur?.(unref(path)),
|
|
134
|
+
options.onBlur?.(),
|
|
135
|
+
],
|
|
136
|
+
)
|
|
137
|
+
},
|
|
138
|
+
onFocus: async () => {
|
|
139
|
+
await Promise.all(
|
|
140
|
+
[
|
|
141
|
+
registryOptions?.onFocus?.(unref(path)),
|
|
142
|
+
options.onFocus?.(),
|
|
143
|
+
],
|
|
144
|
+
)
|
|
145
|
+
},
|
|
83
146
|
})
|
|
84
147
|
|
|
85
148
|
registerField(field)
|
|
@@ -97,8 +160,10 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
97
160
|
return field
|
|
98
161
|
}
|
|
99
162
|
|
|
100
|
-
const defineField = <K extends Paths<T>>(
|
|
101
|
-
|
|
163
|
+
const defineField = <K extends Paths<T>>(
|
|
164
|
+
options: DefineFieldOptions<PickProps<T, K>, K>,
|
|
165
|
+
): ResolvedFormField<T, K> => {
|
|
166
|
+
const field = getField(options)
|
|
102
167
|
|
|
103
168
|
// TODO: If more options are ever needed than only the path we have to update the field
|
|
104
169
|
// here with the new options
|
|
@@ -108,11 +173,13 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
108
173
|
|
|
109
174
|
return {
|
|
110
175
|
fields: computed(() => [...fields.values()] as FieldsTuple<T>),
|
|
111
|
-
getField,
|
|
176
|
+
getField: <P extends Paths<T>>(path: P) => getField({ path }),
|
|
112
177
|
registerField,
|
|
113
178
|
deregisterField,
|
|
114
179
|
defineField,
|
|
115
180
|
}
|
|
116
181
|
}
|
|
117
182
|
|
|
118
|
-
export type FieldRegistry<T extends FormDataDefault> = ReturnType<
|
|
183
|
+
export type FieldRegistry<T extends FormDataDefault> = ReturnType<
|
|
184
|
+
typeof useFieldRegistry<T>
|
|
185
|
+
>
|
|
@@ -1,52 +1,94 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import type { Awaitable } from "@vueuse/core";
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
reactive,
|
|
5
|
+
ref,
|
|
6
|
+
toRef,
|
|
7
|
+
unref,
|
|
8
|
+
watch,
|
|
9
|
+
type MaybeRef,
|
|
10
|
+
type MaybeRefOrGetter,
|
|
11
|
+
type Ref,
|
|
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";
|
|
10
21
|
|
|
11
22
|
// TODO @Elias implement validation strategy handling
|
|
12
23
|
|
|
13
|
-
export interface UseFormOptions<T extends FormDataDefault>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
export interface UseFormOptions<T extends FormDataDefault>
|
|
25
|
+
extends ValidationOptions<T> {
|
|
26
|
+
initialData: MaybeRefOrGetter<T>;
|
|
27
|
+
validationStrategy?: MaybeRef<ValidationStrategy>;
|
|
28
|
+
keepValuesOnUnmount?: MaybeRef<boolean>;
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
20
|
-
const initialData = computed(() =>
|
|
32
|
+
const initialData = computed(() =>
|
|
33
|
+
Object.freeze(cloneRefValue(options.initialData)),
|
|
34
|
+
);
|
|
21
35
|
|
|
22
|
-
const data = ref<T>(cloneRefValue(initialData)) as Ref<T
|
|
36
|
+
const data = ref<T>(cloneRefValue(initialData)) as Ref<T>;
|
|
23
37
|
|
|
24
38
|
const state = reactive({
|
|
25
39
|
initialData,
|
|
26
40
|
data,
|
|
27
|
-
})
|
|
41
|
+
});
|
|
28
42
|
|
|
29
|
-
watch(
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
watch(
|
|
44
|
+
initialData,
|
|
45
|
+
(newValue) => {
|
|
46
|
+
state.data = cloneRefValue(newValue);
|
|
47
|
+
},
|
|
48
|
+
{ flush: "sync" },
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const validationState = useValidation(state, options);
|
|
52
|
+
const fieldRegistry = useFieldRegistry(state, validationState, {
|
|
53
|
+
keepValuesOnUnmount: options.keepValuesOnUnmount,
|
|
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);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const formState = useFormState(fieldRegistry);
|
|
62
|
+
|
|
63
|
+
const submitHandler = (onSubmit: (data: T) => Awaitable<void>) => {
|
|
64
|
+
return async (event: SubmitEvent) => {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
|
|
67
|
+
if (unref(options.validationStrategy) !== "none") {
|
|
68
|
+
await validationState.validateForm();
|
|
69
|
+
}
|
|
32
70
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
71
|
+
if (!validationState.isValid.value) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await onSubmit(state.data);
|
|
76
|
+
};
|
|
77
|
+
};
|
|
36
78
|
|
|
37
79
|
const reset = () => {
|
|
38
|
-
data.value = cloneRefValue(initialData)
|
|
39
|
-
validationState.reset()
|
|
40
|
-
fieldRegistry.fields.value
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
80
|
+
data.value = cloneRefValue(initialData);
|
|
81
|
+
validationState.reset();
|
|
82
|
+
for (const field of fieldRegistry.fields.value) {
|
|
83
|
+
field.reset();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
44
86
|
|
|
45
87
|
function getSubForm<K extends EntityPaths<T>>(
|
|
46
88
|
path: K,
|
|
47
89
|
options?: SubformOptions<PickEntity<T, K>>,
|
|
48
90
|
): Form<PickEntity<T, K>> {
|
|
49
|
-
return createSubformInterface(formInterface, path, options)
|
|
91
|
+
return createSubformInterface(formInterface, path, options);
|
|
50
92
|
}
|
|
51
93
|
|
|
52
94
|
const formInterface: Form<T> = {
|
|
@@ -55,9 +97,14 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
|
55
97
|
...formState,
|
|
56
98
|
reset,
|
|
57
99
|
getSubForm,
|
|
58
|
-
|
|
59
|
-
|
|
100
|
+
submitHandler,
|
|
101
|
+
initialData: toRef(state, "initialData") as Form<T>["initialData"],
|
|
102
|
+
data: toRef(state, "data") as Form<T>["data"],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (unref(options.validationStrategy) === "onFormOpen") {
|
|
106
|
+
validationState.validateForm();
|
|
60
107
|
}
|
|
61
108
|
|
|
62
|
-
return formInterface
|
|
109
|
+
return formInterface;
|
|
63
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/types/form.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import type { Awaitable } from '@vueuse/core'
|
|
1
2
|
import type { Ref } from 'vue'
|
|
2
3
|
import type { DefineFieldOptions } from '../composables/useFieldRegistry'
|
|
3
4
|
import type { SubformOptions } from '../composables/useSubform'
|
|
4
|
-
import type { EntityPaths, Paths, PickEntity, PickProps } from './util'
|
|
5
|
-
import type { ErrorBag, ValidationErrorMessage, ValidationErrors, ValidationResult, Validator } from './validation'
|
|
6
5
|
import type { ValidatorOptions } from '../composables/useValidation'
|
|
6
|
+
import type { EntityPaths, Paths, PickEntity, PickProps } from './util'
|
|
7
|
+
import type {
|
|
8
|
+
ErrorBag,
|
|
9
|
+
ValidationErrorMessage,
|
|
10
|
+
ValidationErrors,
|
|
11
|
+
ValidationResult,
|
|
12
|
+
Validator,
|
|
13
|
+
} from './validation'
|
|
7
14
|
|
|
8
15
|
export type FormDataDefault = object
|
|
9
16
|
|
|
@@ -27,13 +34,13 @@ export interface FormField<T, P extends string> {
|
|
|
27
34
|
clearErrors: () => void
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
export type FieldsTuple<T, TPaths = Paths<T>> = [
|
|
31
|
-
TPaths extends infer P
|
|
37
|
+
export type FieldsTuple<T, TPaths = Paths<T>> = [
|
|
38
|
+
...(TPaths extends infer P
|
|
32
39
|
? P extends string
|
|
33
40
|
? FormField<PickProps<T, P>, P>
|
|
34
41
|
: never
|
|
35
|
-
: never
|
|
36
|
-
|
|
42
|
+
: never)[],
|
|
43
|
+
]
|
|
37
44
|
|
|
38
45
|
export type AnyField<T> = FormField<PickProps<T, Paths<T>>, Paths<T>>
|
|
39
46
|
|
|
@@ -45,7 +52,9 @@ export interface Form<T extends FormDataDefault> {
|
|
|
45
52
|
fields: Ref<FieldsTuple<T>>
|
|
46
53
|
|
|
47
54
|
// Field operations
|
|
48
|
-
defineField: <P extends Paths<T>>(
|
|
55
|
+
defineField: <P extends Paths<T>>(
|
|
56
|
+
options: DefineFieldOptions<PickProps<T, P>, P>,
|
|
57
|
+
) => FormField<PickProps<T, P>, P>
|
|
49
58
|
getField: <P extends Paths<T>>(path: P) => FormField<PickProps<T, P>, P>
|
|
50
59
|
|
|
51
60
|
// State properties
|
|
@@ -55,12 +64,18 @@ export interface Form<T extends FormDataDefault> {
|
|
|
55
64
|
isValidated: Ref<boolean>
|
|
56
65
|
errors: Ref<ErrorBag>
|
|
57
66
|
|
|
58
|
-
defineValidator: <TData extends T>(
|
|
67
|
+
defineValidator: <TData extends T>(
|
|
68
|
+
options: ValidatorOptions<TData> | Ref<Validator<TData>>,
|
|
69
|
+
) => Ref<Validator<TData> | undefined>
|
|
59
70
|
|
|
60
71
|
// Operations
|
|
61
72
|
reset: () => void
|
|
62
73
|
validateForm: () => Promise<ValidationResult>
|
|
63
74
|
|
|
75
|
+
submitHandler: (
|
|
76
|
+
onSubmit: (data: T) => Awaitable<void>,
|
|
77
|
+
) => (event: SubmitEvent) => Promise<void>
|
|
78
|
+
|
|
64
79
|
// Nested subforms
|
|
65
80
|
getSubForm: <P extends EntityPaths<T>>(
|
|
66
81
|
path: P,
|