@teamnovu/kit-vue-forms 0.1.16 → 0.1.18
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/FormPart.vue.d.ts +1 -1
- package/dist/composables/useFieldRegistry.d.ts +2 -2
- package/dist/composables/useForm.d.ts +25 -4
- package/dist/composables/useSubform.d.ts +2 -1
- package/dist/composables/useSubmitHandler.d.ts +8 -0
- package/dist/index.js +222 -214
- package/dist/types/form.d.ts +1 -1
- package/dist/utils/path.d.ts +1 -1
- package/package.json +2 -2
- package/src/composables/useFieldRegistry.ts +74 -74
- package/src/composables/useForm.ts +12 -28
- package/src/composables/useSubform.ts +112 -69
- package/src/composables/useSubmitHandler.ts +29 -0
- package/src/types/form.ts +1 -1
- package/src/utils/path.ts +41 -42
package/dist/types/form.d.ts
CHANGED
|
@@ -44,5 +44,5 @@ export interface Form<T extends FormDataDefault> {
|
|
|
44
44
|
reset: () => void;
|
|
45
45
|
validateForm: () => Promise<ValidationResult>;
|
|
46
46
|
submitHandler: (onSubmit: (data: T) => Awaitable<void>) => (event: SubmitEvent) => Promise<void>;
|
|
47
|
-
getSubForm: <P extends EntityPaths<T>>(path: P, options?: SubformOptions<PickEntity<T, P>>) => Form<PickEntity<T, P
|
|
47
|
+
getSubForm: <P extends EntityPaths<T>>(path: P, options?: SubformOptions<PickEntity<T, P>>) => Omit<Form<PickEntity<T, P>>, 'submitHandler'>;
|
|
48
48
|
}
|
package/dist/utils/path.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare function existsFieldPath<T, K extends Paths<T>>(field: FormField<
|
|
|
8
8
|
export declare function getNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>): PickProps<T, K>;
|
|
9
9
|
export declare function setNestedValue<T, K extends Paths<T>>(obj: MaybeRef<T>, path: K | SplitPath<K>, value: PickProps<T, K>): void;
|
|
10
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>>;
|
|
11
|
-
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}`;
|
|
12
12
|
export declare function joinPath<Base extends string, Sub extends string>(basePath: Base, subPath: Sub): JoinPath<Base, Sub>;
|
|
13
13
|
export declare function filterErrorsForPath(errors: ErrorBag, path: string): ErrorBag;
|
|
14
14
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamnovu/kit-vue-forms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@vitest/ui": "^2.0.0",
|
|
27
27
|
"happy-dom": "^12.0.0",
|
|
28
28
|
"vitest": "^2.0.0",
|
|
29
|
-
"vue": "^3.5.
|
|
29
|
+
"vue": "^3.5.25"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@vueuse/core": "^13.5.0",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Awaitable } from "@vueuse/core";
|
|
1
2
|
import {
|
|
2
3
|
computed,
|
|
3
4
|
onScopeDispose,
|
|
@@ -6,65 +7,69 @@ import {
|
|
|
6
7
|
toRef,
|
|
7
8
|
triggerRef,
|
|
8
9
|
unref,
|
|
9
|
-
|
|
10
|
+
watch,
|
|
10
11
|
type MaybeRef,
|
|
11
|
-
} from
|
|
12
|
-
import type { FieldsTuple, FormDataDefault, FormField } from
|
|
13
|
-
import type { Paths, PickProps } from
|
|
14
|
-
import { existsPath, getLens, getNestedValue } from
|
|
15
|
-
import { Rc } from
|
|
16
|
-
import { useField, type UseFieldOptions } from
|
|
17
|
-
import type { ValidationState } from
|
|
18
|
-
import type { Awaitable } from '@vueuse/core'
|
|
12
|
+
} from "vue";
|
|
13
|
+
import type { FieldsTuple, FormDataDefault, FormField } from "../types/form";
|
|
14
|
+
import type { Paths, PickProps } from "../types/util";
|
|
15
|
+
import { existsPath, getLens, getNestedValue } from "../utils/path";
|
|
16
|
+
import { Rc } from "../utils/rc";
|
|
17
|
+
import { useField, type UseFieldOptions } from "./useField";
|
|
18
|
+
import type { ValidationState } from "./useValidation";
|
|
19
19
|
|
|
20
20
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
-
type FieldRegistryCache<T> = Map<Paths<T>, FormField<any, string
|
|
21
|
+
type FieldRegistryCache<T> = Map<Paths<T>, FormField<any, string>>;
|
|
22
22
|
|
|
23
23
|
export type ResolvedFormField<T, K extends Paths<T>> = FormField<
|
|
24
24
|
PickProps<T, K>,
|
|
25
25
|
K
|
|
26
|
-
|
|
26
|
+
>;
|
|
27
27
|
|
|
28
28
|
export type DefineFieldOptions<F, K extends string> = Pick<
|
|
29
29
|
UseFieldOptions<F, K>,
|
|
30
|
-
|
|
30
|
+
"path"
|
|
31
31
|
> & {
|
|
32
|
-
onBlur?: () => void
|
|
33
|
-
onFocus?: () => void
|
|
34
|
-
}
|
|
32
|
+
onBlur?: () => void;
|
|
33
|
+
onFocus?: () => void;
|
|
34
|
+
};
|
|
35
35
|
|
|
36
36
|
interface FormState<
|
|
37
37
|
T extends FormDataDefault,
|
|
38
38
|
TIn extends FormDataDefault = T,
|
|
39
39
|
> {
|
|
40
|
-
data: T
|
|
41
|
-
initialData: TIn
|
|
40
|
+
data: T;
|
|
41
|
+
initialData: TIn;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
interface FieldRegistryOptions {
|
|
45
|
-
keepValuesOnUnmount?: MaybeRef<boolean
|
|
46
|
-
onBlur?: (path: string) => Awaitable<void
|
|
47
|
-
onFocus?: (path: string) => Awaitable<void
|
|
45
|
+
keepValuesOnUnmount?: MaybeRef<boolean>;
|
|
46
|
+
onBlur?: (path: string) => Awaitable<void>;
|
|
47
|
+
onFocus?: (path: string) => Awaitable<void>;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const optionDefaults = {
|
|
51
51
|
keepValuesOnUnmount: true,
|
|
52
|
-
}
|
|
52
|
+
};
|
|
53
53
|
|
|
54
54
|
// A computed that always reflects the latest value from the getter
|
|
55
55
|
// This computed forces updates even if the value is the same (to trigger watchers)
|
|
56
|
-
function
|
|
57
|
-
|
|
56
|
+
function initialDataSync<T extends FormDataDefault>(
|
|
57
|
+
formState: FormState<T>,
|
|
58
|
+
path: Paths<T>,
|
|
59
|
+
) {
|
|
60
|
+
const getNewValue = () => getNestedValue(formState.initialData, path);
|
|
61
|
+
const initialValueRef = shallowRef(getNewValue());
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
watch(
|
|
64
|
+
() => formState.initialData,
|
|
60
65
|
() => {
|
|
61
|
-
initialValueRef.value =
|
|
62
|
-
triggerRef(initialValueRef)
|
|
66
|
+
initialValueRef.value = getNewValue();
|
|
67
|
+
triggerRef(initialValueRef);
|
|
63
68
|
},
|
|
64
|
-
{ flush:
|
|
65
|
-
)
|
|
69
|
+
{ flush: "sync" },
|
|
70
|
+
);
|
|
66
71
|
|
|
67
|
-
return initialValueRef
|
|
72
|
+
return initialValueRef;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
export function useFieldRegistry<T extends FormDataDefault>(
|
|
@@ -72,104 +77,99 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
72
77
|
validationState: ValidationState<T>,
|
|
73
78
|
fieldRegistryOptions?: FieldRegistryOptions,
|
|
74
79
|
) {
|
|
75
|
-
const fieldReferenceCounter = new Map<Paths<T>, Rc>()
|
|
76
|
-
const fields = shallowReactive(new Map()) as FieldRegistryCache<T
|
|
80
|
+
const fieldReferenceCounter = new Map<Paths<T>, Rc>();
|
|
81
|
+
const fields = shallowReactive(new Map()) as FieldRegistryCache<T>;
|
|
77
82
|
const registryOptions = {
|
|
78
83
|
...optionDefaults,
|
|
79
84
|
...fieldRegistryOptions,
|
|
80
|
-
}
|
|
85
|
+
};
|
|
81
86
|
|
|
82
87
|
const registerField = <K extends Paths<T>>(
|
|
83
88
|
field: ResolvedFormField<T, K>,
|
|
84
89
|
) => {
|
|
85
|
-
const path = unref(field.path) as Paths<T
|
|
86
|
-
fields.set(path, field)
|
|
87
|
-
}
|
|
90
|
+
const path = unref(field.path) as Paths<T>;
|
|
91
|
+
fields.set(path, field);
|
|
92
|
+
};
|
|
88
93
|
|
|
89
94
|
const deregisterField = (path: Paths<T>) => {
|
|
90
95
|
if (!registryOptions?.keepValuesOnUnmount) {
|
|
91
|
-
fields.get(path)?.reset()
|
|
96
|
+
fields.get(path)?.reset();
|
|
92
97
|
}
|
|
93
|
-
fields.delete(path)
|
|
94
|
-
}
|
|
98
|
+
fields.delete(path);
|
|
99
|
+
};
|
|
95
100
|
|
|
96
101
|
const track = (path: Paths<T>) => {
|
|
97
102
|
if (!fieldReferenceCounter.has(path)) {
|
|
98
|
-
fieldReferenceCounter.set(path, new Rc(() => deregisterField(path)))
|
|
103
|
+
fieldReferenceCounter.set(path, new Rc(() => deregisterField(path)));
|
|
99
104
|
} else {
|
|
100
|
-
fieldReferenceCounter.get(path)?.inc()
|
|
105
|
+
fieldReferenceCounter.get(path)?.inc();
|
|
101
106
|
}
|
|
102
|
-
}
|
|
107
|
+
};
|
|
103
108
|
|
|
104
109
|
const untrack = (path: Paths<T>) => {
|
|
105
110
|
if (fieldReferenceCounter.has(path)) {
|
|
106
|
-
fieldReferenceCounter.get(path)?.dec()
|
|
111
|
+
fieldReferenceCounter.get(path)?.dec();
|
|
107
112
|
}
|
|
108
|
-
}
|
|
113
|
+
};
|
|
109
114
|
|
|
110
115
|
const getField = <K extends Paths<T>>(
|
|
111
116
|
options: DefineFieldOptions<PickProps<T, K>, K>,
|
|
112
117
|
): ResolvedFormField<T, K> => {
|
|
113
|
-
const { path } = options
|
|
118
|
+
const { path } = options;
|
|
114
119
|
|
|
115
120
|
if (!fields.has(path)) {
|
|
116
121
|
const field = useField({
|
|
117
122
|
path,
|
|
118
|
-
value: getLens(toRef(formState,
|
|
119
|
-
initialValue:
|
|
120
|
-
getNestedValue(formState.initialData, path)),
|
|
123
|
+
value: getLens(toRef(formState, "data"), path),
|
|
124
|
+
initialValue: initialDataSync(formState, path),
|
|
121
125
|
existsInForm: computed(() => existsPath(formState.data, unref(path))),
|
|
122
126
|
errors: computed({
|
|
123
127
|
get() {
|
|
124
|
-
return validationState.errors.value.propertyErrors[path] || []
|
|
128
|
+
return validationState.errors.value.propertyErrors[path] || [];
|
|
125
129
|
},
|
|
126
130
|
set(newErrors) {
|
|
127
|
-
validationState.errors.value.propertyErrors[path] = newErrors
|
|
131
|
+
validationState.errors.value.propertyErrors[path] = newErrors;
|
|
128
132
|
},
|
|
129
133
|
}),
|
|
130
134
|
onBlur: async () => {
|
|
131
|
-
await Promise.all(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
],
|
|
136
|
-
)
|
|
135
|
+
await Promise.all([
|
|
136
|
+
registryOptions?.onBlur?.(unref(path)),
|
|
137
|
+
options.onBlur?.(),
|
|
138
|
+
]);
|
|
137
139
|
},
|
|
138
140
|
onFocus: async () => {
|
|
139
|
-
await Promise.all(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
],
|
|
144
|
-
)
|
|
141
|
+
await Promise.all([
|
|
142
|
+
registryOptions?.onFocus?.(unref(path)),
|
|
143
|
+
options.onFocus?.(),
|
|
144
|
+
]);
|
|
145
145
|
},
|
|
146
|
-
})
|
|
146
|
+
});
|
|
147
147
|
|
|
148
|
-
registerField(field)
|
|
148
|
+
registerField(field);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
const field = fields.get(path) as ResolvedFormField<T, K
|
|
151
|
+
const field = fields.get(path) as ResolvedFormField<T, K>;
|
|
152
152
|
|
|
153
|
-
track(path)
|
|
153
|
+
track(path);
|
|
154
154
|
|
|
155
155
|
// Clean up field on unmount
|
|
156
156
|
onScopeDispose(() => {
|
|
157
|
-
untrack(path)
|
|
158
|
-
})
|
|
157
|
+
untrack(path);
|
|
158
|
+
});
|
|
159
159
|
|
|
160
|
-
return field
|
|
161
|
-
}
|
|
160
|
+
return field;
|
|
161
|
+
};
|
|
162
162
|
|
|
163
163
|
const defineField = <K extends Paths<T>>(
|
|
164
164
|
options: DefineFieldOptions<PickProps<T, K>, K>,
|
|
165
165
|
): ResolvedFormField<T, K> => {
|
|
166
|
-
const field = getField(options)
|
|
166
|
+
const field = getField(options);
|
|
167
167
|
|
|
168
168
|
// TODO: If more options are ever needed than only the path we have to update the field
|
|
169
169
|
// here with the new options
|
|
170
170
|
|
|
171
|
-
return field
|
|
172
|
-
}
|
|
171
|
+
return field;
|
|
172
|
+
};
|
|
173
173
|
|
|
174
174
|
return {
|
|
175
175
|
fields: computed(() => [...fields.values()] as FieldsTuple<T>),
|
|
@@ -177,9 +177,9 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
177
177
|
registerField,
|
|
178
178
|
deregisterField,
|
|
179
179
|
defineField,
|
|
180
|
-
}
|
|
180
|
+
};
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
export type FieldRegistry<T extends FormDataDefault> = ReturnType<
|
|
184
184
|
typeof useFieldRegistry<T>
|
|
185
|
-
|
|
185
|
+
>;
|
|
@@ -18,8 +18,7 @@ import { useFieldRegistry } from "./useFieldRegistry";
|
|
|
18
18
|
import { useFormState } from "./useFormState";
|
|
19
19
|
import { createSubformInterface, type SubformOptions } from "./useSubform";
|
|
20
20
|
import { useValidation, type ValidationOptions } from "./useValidation";
|
|
21
|
-
|
|
22
|
-
// TODO @Elias implement validation strategy handling
|
|
21
|
+
import { useSubmitHandler } from "./useSubmitHandler";
|
|
23
22
|
|
|
24
23
|
export interface UseFormOptions<T extends FormDataDefault>
|
|
25
24
|
extends ValidationOptions<T> {
|
|
@@ -29,9 +28,7 @@ export interface UseFormOptions<T extends FormDataDefault>
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
32
|
-
const initialData = computed(() =>
|
|
33
|
-
Object.freeze(cloneRefValue(options.initialData)),
|
|
34
|
-
);
|
|
31
|
+
const initialData = computed(() => cloneRefValue(options.initialData));
|
|
35
32
|
|
|
36
33
|
const data = ref<T>(cloneRefValue(initialData)) as Ref<T>;
|
|
37
34
|
|
|
@@ -53,29 +50,12 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
|
53
50
|
keepValuesOnUnmount: options.keepValuesOnUnmount,
|
|
54
51
|
onBlur: async (path: string) => {
|
|
55
52
|
if (unref(options.validationStrategy) === "onTouch") {
|
|
56
|
-
// TODO: Only validate the specific field that was touched
|
|
57
53
|
validationState.validateField(path);
|
|
58
54
|
}
|
|
59
55
|
},
|
|
60
56
|
});
|
|
61
57
|
const formState = useFormState(fieldRegistry);
|
|
62
58
|
|
|
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
|
-
}
|
|
70
|
-
|
|
71
|
-
if (!validationState.isValid.value) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
await onSubmit(state.data);
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
|
|
79
59
|
const reset = () => {
|
|
80
60
|
data.value = cloneRefValue(initialData);
|
|
81
61
|
validationState.reset();
|
|
@@ -86,25 +66,29 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
|
86
66
|
|
|
87
67
|
function getSubForm<K extends EntityPaths<T>>(
|
|
88
68
|
path: K,
|
|
89
|
-
|
|
90
|
-
): Form<PickEntity<T, K
|
|
91
|
-
return createSubformInterface(formInterface, path, options);
|
|
69
|
+
subformOptions?: SubformOptions<PickEntity<T, K>>,
|
|
70
|
+
): Omit<Form<PickEntity<T, K>>, "submitHandler"> {
|
|
71
|
+
return createSubformInterface(formInterface, path, options, subformOptions);
|
|
92
72
|
}
|
|
93
73
|
|
|
94
|
-
const formInterface: Form<T> = {
|
|
74
|
+
const formInterface: Omit<Form<T>, "submitHandler"> = {
|
|
95
75
|
...fieldRegistry,
|
|
96
76
|
...validationState,
|
|
97
77
|
...formState,
|
|
98
78
|
reset,
|
|
99
79
|
getSubForm,
|
|
100
|
-
submitHandler,
|
|
101
80
|
initialData: toRef(state, "initialData") as Form<T>["initialData"],
|
|
102
81
|
data: toRef(state, "data") as Form<T>["data"],
|
|
103
82
|
};
|
|
104
83
|
|
|
84
|
+
const submitHandler = useSubmitHandler(formInterface, options);
|
|
85
|
+
|
|
105
86
|
if (unref(options.validationStrategy) === "onFormOpen") {
|
|
106
87
|
validationState.validateForm();
|
|
107
88
|
}
|
|
108
89
|
|
|
109
|
-
return
|
|
90
|
+
return {
|
|
91
|
+
...formInterface,
|
|
92
|
+
submitHandler,
|
|
93
|
+
};
|
|
110
94
|
}
|
|
@@ -1,30 +1,47 @@
|
|
|
1
|
-
import { computed, isRef, unref, type Ref } from
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { computed, isRef, unref, type Ref } from "vue";
|
|
2
|
+
import type {
|
|
3
|
+
FieldsTuple,
|
|
4
|
+
Form,
|
|
5
|
+
FormDataDefault,
|
|
6
|
+
FormField,
|
|
7
|
+
} from "../types/form";
|
|
8
|
+
import type { EntityPaths, Paths, PickEntity, PickProps } from "../types/util";
|
|
9
|
+
import type { ValidationResult, Validator } from "../types/validation";
|
|
10
|
+
import {
|
|
11
|
+
filterErrorsForPath,
|
|
12
|
+
getLens,
|
|
13
|
+
getNestedValue,
|
|
14
|
+
joinPath,
|
|
15
|
+
} from "../utils/path";
|
|
16
|
+
import type { DefineFieldOptions } from "./useFieldRegistry";
|
|
17
|
+
import type { UseFormOptions } from "./useForm";
|
|
18
|
+
import { useSubmitHandler } from "./useSubmitHandler";
|
|
19
|
+
import {
|
|
20
|
+
createValidator,
|
|
21
|
+
SuccessValidationResult,
|
|
22
|
+
type ValidatorOptions,
|
|
23
|
+
} from "./useValidation";
|
|
8
24
|
|
|
9
25
|
export interface SubformOptions<_T extends FormDataDefault> {
|
|
10
26
|
// Additional subform-specific options can be added here
|
|
11
27
|
}
|
|
12
28
|
|
|
13
|
-
class NestedValidator<T extends FormDataDefault, P extends Paths<T>>
|
|
29
|
+
class NestedValidator<T extends FormDataDefault, P extends Paths<T>>
|
|
30
|
+
implements Validator<T>
|
|
31
|
+
{
|
|
14
32
|
constructor(
|
|
15
33
|
private path: P,
|
|
16
34
|
private validator: Validator<PickEntity<T, P>> | undefined,
|
|
17
|
-
) {
|
|
18
|
-
}
|
|
35
|
+
) {}
|
|
19
36
|
|
|
20
37
|
async validate(data: T): Promise<ValidationResult> {
|
|
21
|
-
const subFormData = getNestedValue(data, this.path) as PickEntity<T, P
|
|
38
|
+
const subFormData = getNestedValue(data, this.path) as PickEntity<T, P>;
|
|
22
39
|
|
|
23
40
|
if (!this.validator) {
|
|
24
|
-
return SuccessValidationResult
|
|
41
|
+
return SuccessValidationResult;
|
|
25
42
|
}
|
|
26
43
|
|
|
27
|
-
const validationResult = await this.validator.validate(subFormData)
|
|
44
|
+
const validationResult = await this.validator.validate(subFormData);
|
|
28
45
|
|
|
29
46
|
return {
|
|
30
47
|
isValid: validationResult.isValid,
|
|
@@ -32,14 +49,13 @@ class NestedValidator<T extends FormDataDefault, P extends Paths<T>> implements
|
|
|
32
49
|
general: validationResult.errors.general || [],
|
|
33
50
|
propertyErrors: validationResult.errors.propertyErrors
|
|
34
51
|
? Object.fromEntries(
|
|
35
|
-
Object.entries(validationResult.errors.propertyErrors).map(
|
|
36
|
-
joinPath(this.path, key),
|
|
37
|
-
|
|
38
|
-
]),
|
|
52
|
+
Object.entries(validationResult.errors.propertyErrors).map(
|
|
53
|
+
([key, errors]) => [joinPath(this.path, key), errors],
|
|
54
|
+
),
|
|
39
55
|
)
|
|
40
56
|
: {},
|
|
41
57
|
},
|
|
42
|
-
}
|
|
58
|
+
};
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
61
|
|
|
@@ -47,21 +63,22 @@ export function createSubformInterface<
|
|
|
47
63
|
T extends FormDataDefault,
|
|
48
64
|
K extends EntityPaths<T>,
|
|
49
65
|
>(
|
|
50
|
-
mainForm: Form<T>,
|
|
66
|
+
mainForm: Omit<Form<T>, "submitHandler">,
|
|
51
67
|
path: K,
|
|
68
|
+
formOptions?: UseFormOptions<T>,
|
|
52
69
|
_options?: SubformOptions<PickEntity<T, K>>,
|
|
53
70
|
): Form<PickEntity<T, K>> {
|
|
54
|
-
type ST = PickEntity<T, K
|
|
55
|
-
type SP = Paths<ST
|
|
56
|
-
type MP<P extends SP> = `${K}.${P}
|
|
57
|
-
type ScopedMainPaths = Paths<T> & MP<SP
|
|
71
|
+
type ST = PickEntity<T, K>;
|
|
72
|
+
type SP = Paths<ST>;
|
|
73
|
+
type MP<P extends SP> = `${K}.${P}`;
|
|
74
|
+
type ScopedMainPaths = Paths<T> & MP<SP>;
|
|
58
75
|
|
|
59
76
|
// Create reactive data scoped to subform path
|
|
60
|
-
const data = getLens(mainForm.data, path) as Ref<ST
|
|
77
|
+
const data = getLens(mainForm.data, path) as Ref<ST>;
|
|
61
78
|
|
|
62
79
|
const initialData = computed(() => {
|
|
63
|
-
return getNestedValue(mainForm.initialData.value, path) as ST
|
|
64
|
-
})
|
|
80
|
+
return getNestedValue(mainForm.initialData.value, path) as ST;
|
|
81
|
+
});
|
|
65
82
|
|
|
66
83
|
const adaptMainFormField = <S extends SP>(
|
|
67
84
|
field: FormField<PickProps<T, ScopedMainPaths>, ScopedMainPaths>,
|
|
@@ -69,92 +86,111 @@ export function createSubformInterface<
|
|
|
69
86
|
// Where P ist the full path in the main form, we need to adapt it to the subform's path
|
|
70
87
|
return {
|
|
71
88
|
...field,
|
|
72
|
-
path: computed(() => unref(field.path).replace(path +
|
|
89
|
+
path: computed(() => unref(field.path).replace(path + ".", "")),
|
|
73
90
|
setData: (newData: PickProps<ST, S>) => {
|
|
74
|
-
field.setData(newData as PickProps<T, ScopedMainPaths>)
|
|
91
|
+
field.setData(newData as PickProps<T, ScopedMainPaths>);
|
|
75
92
|
},
|
|
76
|
-
} as unknown as FormField<PickProps<ST, S>, S
|
|
77
|
-
}
|
|
93
|
+
} as unknown as FormField<PickProps<ST, S>, S>;
|
|
94
|
+
};
|
|
78
95
|
|
|
79
96
|
const getField = <P extends SP>(fieldPath: P) => {
|
|
80
|
-
const fullPath = joinPath(path, fieldPath)
|
|
81
|
-
const mainFormField = mainForm.getField(fullPath as ScopedMainPaths)
|
|
97
|
+
const fullPath = joinPath(path, fieldPath);
|
|
98
|
+
const mainFormField = mainForm.getField(fullPath as ScopedMainPaths);
|
|
82
99
|
|
|
83
100
|
if (!mainFormField) {
|
|
84
|
-
return {} as FormField<PickProps<ST, P>, P
|
|
101
|
+
return {} as FormField<PickProps<ST, P>, P>;
|
|
85
102
|
}
|
|
86
103
|
|
|
87
|
-
return adaptMainFormField<P>(mainFormField)
|
|
88
|
-
}
|
|
104
|
+
return adaptMainFormField<P>(mainFormField);
|
|
105
|
+
};
|
|
89
106
|
|
|
90
107
|
// Field operations with path transformation
|
|
91
|
-
const defineField = <P extends SP>(
|
|
92
|
-
|
|
108
|
+
const defineField = <P extends SP>(
|
|
109
|
+
fieldOptions: DefineFieldOptions<ST, P>,
|
|
110
|
+
) => {
|
|
111
|
+
const fullPath = joinPath(path, fieldOptions.path);
|
|
93
112
|
|
|
94
113
|
const mainField = mainForm.defineField({
|
|
95
114
|
...fieldOptions,
|
|
96
115
|
path: fullPath as ScopedMainPaths,
|
|
97
|
-
})
|
|
116
|
+
});
|
|
98
117
|
|
|
99
|
-
return adaptMainFormField<P>(mainField)
|
|
100
|
-
}
|
|
118
|
+
return adaptMainFormField<P>(mainField);
|
|
119
|
+
};
|
|
101
120
|
|
|
102
121
|
const fields = computed(<P extends SP>() => {
|
|
103
|
-
return (
|
|
122
|
+
return (
|
|
123
|
+
mainForm.fields.value as FormField<
|
|
124
|
+
PickProps<T, ScopedMainPaths>,
|
|
125
|
+
ScopedMainPaths
|
|
126
|
+
>[]
|
|
127
|
+
)
|
|
104
128
|
.filter((field) => {
|
|
105
|
-
const fieldPath = field.path.value
|
|
106
|
-
return fieldPath.startsWith(path +
|
|
129
|
+
const fieldPath = field.path.value;
|
|
130
|
+
return fieldPath.startsWith(path + ".") || fieldPath === path;
|
|
107
131
|
})
|
|
108
|
-
.map(field => adaptMainFormField(field)) as FieldsTuple<ST, P
|
|
109
|
-
})
|
|
132
|
+
.map((field) => adaptMainFormField(field)) as FieldsTuple<ST, P>;
|
|
133
|
+
});
|
|
110
134
|
|
|
111
135
|
// Helper function to get all fields without type parameter
|
|
112
136
|
const getAllSubformFields = () => {
|
|
113
|
-
return (
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
137
|
+
return (
|
|
138
|
+
mainForm.fields.value as FormField<
|
|
139
|
+
PickProps<T, ScopedMainPaths>,
|
|
140
|
+
ScopedMainPaths
|
|
141
|
+
>[]
|
|
142
|
+
).filter((field) => {
|
|
143
|
+
const fieldPath = field.path.value;
|
|
144
|
+
return fieldPath.startsWith(path + ".") || fieldPath === path;
|
|
145
|
+
});
|
|
146
|
+
};
|
|
119
147
|
|
|
120
148
|
// State computed from main form with path filtering
|
|
121
|
-
const isDirty = computed(() =>
|
|
122
|
-
|
|
149
|
+
const isDirty = computed(() =>
|
|
150
|
+
getAllSubformFields().some((field) => field.dirty.value),
|
|
151
|
+
);
|
|
152
|
+
const isTouched = computed(() =>
|
|
153
|
+
getAllSubformFields().some((field) => field.touched.value),
|
|
154
|
+
);
|
|
123
155
|
|
|
124
156
|
// Validation delegates to main form
|
|
125
|
-
const isValid = computed(() => mainForm.isValid.value)
|
|
126
|
-
const isValidated = computed(() => mainForm.isValidated.value)
|
|
127
|
-
const errors = computed(() =>
|
|
157
|
+
const isValid = computed(() => mainForm.isValid.value);
|
|
158
|
+
const isValidated = computed(() => mainForm.isValidated.value);
|
|
159
|
+
const errors = computed(() =>
|
|
160
|
+
filterErrorsForPath(unref(mainForm.errors), path),
|
|
161
|
+
);
|
|
128
162
|
|
|
129
|
-
const validateForm = () => mainForm.validateForm()
|
|
163
|
+
const validateForm = () => mainForm.validateForm();
|
|
130
164
|
|
|
131
165
|
// Nested subforms
|
|
132
166
|
const getSubForm = <P extends EntityPaths<ST>>(
|
|
133
167
|
subPath: P,
|
|
134
168
|
subOptions?: SubformOptions<PickEntity<ST, P>>,
|
|
135
169
|
) => {
|
|
136
|
-
const fullPath = joinPath(path, subPath) as EntityPaths<T
|
|
170
|
+
const fullPath = joinPath(path, subPath) as EntityPaths<T>;
|
|
137
171
|
return mainForm.getSubForm(
|
|
138
172
|
fullPath,
|
|
139
173
|
subOptions as SubformOptions<PickEntity<T, typeof fullPath>>,
|
|
140
|
-
) as Form<PickEntity<ST, P
|
|
141
|
-
}
|
|
174
|
+
) as Form<PickEntity<ST, P>>;
|
|
175
|
+
};
|
|
142
176
|
|
|
143
177
|
// Reset scoped to this subform
|
|
144
|
-
const reset = () => getAllSubformFields().forEach(field => field.reset())
|
|
178
|
+
const reset = () => getAllSubformFields().forEach((field) => field.reset());
|
|
145
179
|
|
|
146
|
-
const defineValidator = (
|
|
147
|
-
|
|
180
|
+
const defineValidator = (
|
|
181
|
+
options: ValidatorOptions<ST> | Ref<Validator<ST>>,
|
|
182
|
+
) => {
|
|
183
|
+
const subValidator = isRef(options) ? options : createValidator(options);
|
|
148
184
|
const validator = computed(
|
|
149
185
|
() => new NestedValidator<T, K>(path, unref(subValidator)),
|
|
150
|
-
)
|
|
186
|
+
);
|
|
151
187
|
|
|
152
|
-
mainForm.defineValidator(validator)
|
|
188
|
+
mainForm.defineValidator(validator);
|
|
153
189
|
|
|
154
|
-
return subValidator
|
|
155
|
-
}
|
|
190
|
+
return subValidator;
|
|
191
|
+
};
|
|
156
192
|
|
|
157
|
-
|
|
193
|
+
const subFormInterface: Omit<Form<ST>, "submitHandler"> = {
|
|
158
194
|
data: data,
|
|
159
195
|
fields,
|
|
160
196
|
initialData,
|
|
@@ -169,5 +205,12 @@ export function createSubformInterface<
|
|
|
169
205
|
reset,
|
|
170
206
|
validateForm,
|
|
171
207
|
getSubForm,
|
|
172
|
-
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const submitHandler = useSubmitHandler(subFormInterface, formOptions ?? {});
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
...subFormInterface,
|
|
214
|
+
submitHandler,
|
|
215
|
+
};
|
|
173
216
|
}
|