@teamnovu/kit-vue-forms 0.2.17 → 0.2.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/CHANGELOG.md +25 -0
- package/dist/components/FormFieldWrapper.vue.d.ts +1 -3
- package/dist/composables/useSubform.d.ts +2 -2
- package/dist/index.js +42 -29
- package/dist/utils/submitHandler.d.ts +1 -1
- package/docs/reference.md +47 -0
- package/package.json +1 -1
- package/src/components/FormFieldWrapper.vue +2 -0
- package/src/composables/useForm.ts +1 -1
- package/src/composables/useSubform.ts +8 -4
- package/src/composables/useValidation.ts +72 -26
- package/src/types/form.ts +5 -5
- package/src/types/util.ts +0 -1
- package/src/utils/submitHandler.ts +1 -1
- package/src/utils/zod.ts +11 -8
- package/tests/path-utils.test.ts +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.18] - 2025-03-03
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Correctly pass `validationState` to subForm creation instead of `formOptions`
|
|
13
|
+
- Fix build error through type on `FormFieldWrapper`
|
|
14
|
+
|
|
15
|
+
## [0.2.17] - 2025-01-22
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `keepValuesOnUnmount` was `false` instead of `true` by default
|
|
20
|
+
|
|
21
|
+
## [0.1.27] - 2025-01-22
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `keepValuesOnUnmount` was `false` instead of `true` by default
|
|
@@ -12,9 +12,7 @@ declare const _default: <TData extends FormDataDefault, TPath extends Paths<TDat
|
|
|
12
12
|
props: __VLS_PrettifyLocal<Pick<Partial<{}> & Omit<{} & VNodeProps & AllowedComponentProps & ComponentCustomProps, never>, never> & FormFieldWrapperProps<TData, TPath, TComponent, TDataOut> & Partial<{}>> & PublicProps;
|
|
13
13
|
expose(exposed: ShallowUnwrapRef<{}>): void;
|
|
14
14
|
attrs: any;
|
|
15
|
-
slots:
|
|
16
|
-
default?(_: {}): any;
|
|
17
|
-
};
|
|
15
|
+
slots: Readonly<Record<string, (props: any) => any>> & Record<string, (props: any) => any>;
|
|
18
16
|
emit: {};
|
|
19
17
|
}>) => VNode & {
|
|
20
18
|
__ctx?: Awaited<typeof __VLS_setup>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Form, FormDataDefault } from '../types/form';
|
|
2
2
|
import { EntityPaths, PickEntity } from '../types/util';
|
|
3
|
-
import {
|
|
3
|
+
import { ValidationState } from './useValidation';
|
|
4
4
|
export interface SubformOptions<_T extends FormDataDefault> {
|
|
5
5
|
}
|
|
6
|
-
export declare function createSubformInterface<T extends FormDataDefault, K extends EntityPaths<T>, TOut = T>(mainForm: Form<T, TOut>, path: K,
|
|
6
|
+
export declare function createSubformInterface<T extends FormDataDefault, K extends EntityPaths<T>, TOut = T>(mainForm: Form<T, TOut>, path: K, validationState: ValidationState<T, TOut>, _options?: SubformOptions<PickEntity<T, K>>): Form<PickEntity<T, K>, PickEntity<TOut, K>>;
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,6 @@ var re = (r, t, e) => t in r ? te(r, t, { enumerable: !0, configurable: !0, writ
|
|
|
3
3
|
var P = (r, t, e) => re(r, typeof t != "symbol" ? t + "" : t, e);
|
|
4
4
|
import { toValue as J, toRaw as ae, shallowRef as O, watch as b, computed as m, unref as h, isRef as x, reactive as A, toRefs as G, shallowReactive as se, toRef as U, onScopeDispose as ne, triggerRef as ie, ref as Z, getCurrentScope as oe, onBeforeUnmount as le, defineComponent as N, renderSlot as C, normalizeProps as T, guardReactiveProps as W, createBlock as K, openBlock as L, withCtx as M, resolveDynamicComponent as ue, mergeProps as ce, createSlots as de, renderList as fe } from "vue";
|
|
5
5
|
import { cloneDeep as he, merge as ve, omit as me } from "lodash-es";
|
|
6
|
-
import "zod";
|
|
7
6
|
function D(r) {
|
|
8
7
|
const t = J(r), e = ae(t);
|
|
9
8
|
return he(e);
|
|
@@ -359,13 +358,16 @@ function $e(r) {
|
|
|
359
358
|
};
|
|
360
359
|
}
|
|
361
360
|
function Ie(r) {
|
|
362
|
-
const t = r.issues.filter((a) => a.path.length === 0).map((a) => a.message), e = r.issues.filter((a) => a.path.length > 0).reduce(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
361
|
+
const t = r.issues.filter((a) => a.path.length === 0).map((a) => a.message), e = r.issues.filter((a) => a.path.length > 0).reduce(
|
|
362
|
+
(a, s) => {
|
|
363
|
+
const n = s.path.join(".");
|
|
364
|
+
return {
|
|
365
|
+
...a,
|
|
366
|
+
[n]: [...a[n] ?? [], s.message]
|
|
367
|
+
};
|
|
368
|
+
},
|
|
369
|
+
{}
|
|
370
|
+
);
|
|
369
371
|
return {
|
|
370
372
|
general: t,
|
|
371
373
|
propertyErrors: e
|
|
@@ -450,10 +452,12 @@ class Be {
|
|
|
450
452
|
}
|
|
451
453
|
}
|
|
452
454
|
function _(r) {
|
|
453
|
-
return m(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
455
|
+
return m(
|
|
456
|
+
() => new Be(
|
|
457
|
+
h(r.schema),
|
|
458
|
+
h(r.validateFn)
|
|
459
|
+
)
|
|
460
|
+
);
|
|
457
461
|
}
|
|
458
462
|
function ke(r, t) {
|
|
459
463
|
const e = A({
|
|
@@ -461,15 +465,22 @@ function ke(r, t) {
|
|
|
461
465
|
isValidated: !1,
|
|
462
466
|
errors: h(t.errors) ?? w.errors
|
|
463
467
|
}), a = (i = w.errors) => {
|
|
464
|
-
e.errors = j(
|
|
468
|
+
e.errors = j(
|
|
469
|
+
h(t.errors) ?? w.errors,
|
|
470
|
+
i
|
|
471
|
+
);
|
|
465
472
|
};
|
|
466
|
-
b(
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
+
b(
|
|
474
|
+
() => h(t.errors),
|
|
475
|
+
async () => {
|
|
476
|
+
if (e.isValidated) {
|
|
477
|
+
const i = await n();
|
|
478
|
+
a(i.errors);
|
|
479
|
+
} else
|
|
480
|
+
a();
|
|
481
|
+
},
|
|
482
|
+
{ immediate: !0 }
|
|
483
|
+
), b(
|
|
473
484
|
[() => e.validators],
|
|
474
485
|
async (i) => {
|
|
475
486
|
if (e.isValidated)
|
|
@@ -485,7 +496,9 @@ function ke(r, t) {
|
|
|
485
496
|
});
|
|
486
497
|
const s = (i) => {
|
|
487
498
|
const o = x(i) ? i : _(i);
|
|
488
|
-
return e.validators.push(
|
|
499
|
+
return e.validators.push(
|
|
500
|
+
o
|
|
501
|
+
), oe() && le(() => {
|
|
489
502
|
e.validators = e.validators.filter(
|
|
490
503
|
(f) => f !== o
|
|
491
504
|
);
|
|
@@ -613,7 +626,7 @@ function Ue(r, t, e, a) {
|
|
|
613
626
|
p
|
|
614
627
|
);
|
|
615
628
|
},
|
|
616
|
-
submitHandler: (u) => X(k, e
|
|
629
|
+
submitHandler: (u) => X(k, e)(u),
|
|
617
630
|
getFieldArray: (u, p) => Y(k, u, p)
|
|
618
631
|
};
|
|
619
632
|
return k;
|
|
@@ -622,7 +635,7 @@ const je = {
|
|
|
622
635
|
keepValuesOnUnmount: !0,
|
|
623
636
|
...Oe
|
|
624
637
|
};
|
|
625
|
-
function
|
|
638
|
+
function We(r) {
|
|
626
639
|
r = ve({}, je, r);
|
|
627
640
|
const t = m(() => D(r.initialData)), e = Z(D(t)), a = A({
|
|
628
641
|
initialData: t,
|
|
@@ -661,7 +674,7 @@ function ze(r) {
|
|
|
661
674
|
data: U(a, "data"),
|
|
662
675
|
validateForm: s.validateForm,
|
|
663
676
|
submitHandler: (l) => X(y, s)(l),
|
|
664
|
-
getSubForm: (l, d) => Ue(y, l,
|
|
677
|
+
getSubForm: (l, d) => Ue(y, l, s),
|
|
665
678
|
getFieldArray: (l, d) => Y(y, l, d)
|
|
666
679
|
};
|
|
667
680
|
return y;
|
|
@@ -685,7 +698,7 @@ const _e = /* @__PURE__ */ N({
|
|
|
685
698
|
}), a = A(e);
|
|
686
699
|
return (s, n) => C(s.$slots, "default", T(W(a)));
|
|
687
700
|
}
|
|
688
|
-
}),
|
|
701
|
+
}), ze = /* @__PURE__ */ N({
|
|
689
702
|
inheritAttrs: !1,
|
|
690
703
|
__name: "FormFieldWrapper",
|
|
691
704
|
props: {
|
|
@@ -724,7 +737,7 @@ const _e = /* @__PURE__ */ N({
|
|
|
724
737
|
_: 3
|
|
725
738
|
}, 8, ["form", "path"]));
|
|
726
739
|
}
|
|
727
|
-
}),
|
|
740
|
+
}), He = /* @__PURE__ */ N({
|
|
728
741
|
__name: "FormPart",
|
|
729
742
|
props: {
|
|
730
743
|
form: {},
|
|
@@ -737,7 +750,7 @@ const _e = /* @__PURE__ */ N({
|
|
|
737
750
|
});
|
|
738
751
|
export {
|
|
739
752
|
_e as Field,
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
753
|
+
ze as FormFieldWrapper,
|
|
754
|
+
He as FormPart,
|
|
755
|
+
We as useForm
|
|
743
756
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Awaitable } from '@vueuse/core';
|
|
2
2
|
import { ValidationState } from '../composables/useValidation';
|
|
3
3
|
import { Form, FormDataDefault } from '../types/form';
|
|
4
|
-
export declare function makeSubmitHandler<T extends FormDataDefault, TOut = T>(form: Form<T, TOut>, validationState: ValidationState<T, TOut>): (onSubmit: (data: TOut) => Awaitable<void>) => (event?: SubmitEvent) => Promise<void>;
|
|
4
|
+
export declare function makeSubmitHandler<T extends FormDataDefault, TOut = T>(form: Form<T, TOut>, validationState: Pick<ValidationState<T, TOut>, 'canValidate'>): (onSubmit: (data: TOut) => Awaitable<void>) => (event?: SubmitEvent) => Promise<void>;
|
package/docs/reference.md
CHANGED
|
@@ -116,6 +116,53 @@ interface Form<T extends object, TOut = T> {
|
|
|
116
116
|
}
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
## Method `setInitialData`
|
|
120
|
+
The `setInitialData` method is available on `FormField` objects (returned by `getField` or `defineField`). It updates what is considered the "initial" value of a field. If the field is not dirty, it also updates the current data.
|
|
121
|
+
|
|
122
|
+
This is useful when loading data asynchronously after the form is initialized:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const { setInitialData } = form.getField('person.firstName')
|
|
126
|
+
|
|
127
|
+
// After fetching data from an API
|
|
128
|
+
const fetchedData = await fetchUserData()
|
|
129
|
+
setInitialData(fetchedData.firstName)
|
|
130
|
+
// If the field wasn't dirty, the current data is also updated
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Method `defineValidator`
|
|
134
|
+
The `defineValidator` method allows subcomponents to add validation rules to a form without needing access to the initial `useForm` call. The validator is automatically removed when the component unmounts.
|
|
135
|
+
|
|
136
|
+
This is particularly useful on subforms, where a reusable component can define its own validation:
|
|
137
|
+
|
|
138
|
+
```vue
|
|
139
|
+
<!-- AddressFields.vue - a reusable address component -->
|
|
140
|
+
<script setup lang="ts">
|
|
141
|
+
import type { Form } from '@teamnovu/kit-vue-forms'
|
|
142
|
+
import { z } from 'zod'
|
|
143
|
+
|
|
144
|
+
const props = defineProps<{
|
|
145
|
+
form: Form<{ street: string; city: string; zip: string }>
|
|
146
|
+
}>()
|
|
147
|
+
|
|
148
|
+
// This validation only applies while AddressFields is mounted
|
|
149
|
+
props.form.defineValidator({
|
|
150
|
+
schema: z.object({
|
|
151
|
+
street: z.string().min(1, 'Street is required'),
|
|
152
|
+
city: z.string().min(1, 'City is required'),
|
|
153
|
+
zip: z.string().regex(/^\d{4}$/, 'Invalid zip code'),
|
|
154
|
+
}),
|
|
155
|
+
})
|
|
156
|
+
</script>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```vue
|
|
160
|
+
<!-- Parent component using FormPart -->
|
|
161
|
+
<FormPart :form="form" path="person.address" #="{ subform }">
|
|
162
|
+
<AddressFields :form="subform" />
|
|
163
|
+
</FormPart>
|
|
164
|
+
```
|
|
165
|
+
|
|
119
166
|
## Type `ErrorBag`
|
|
120
167
|
The errors in the form are structured in an `ErrorBag` object. Most errors are tied to properties in the form. However,
|
|
121
168
|
there might be cases where there are errors, that cannot be tied to one property. To account for that the `ErrorBag` satisfies the following interface:
|
package/package.json
CHANGED
|
@@ -113,7 +113,7 @@ export function useForm<T extends FormDataDefault, TOut = T>(
|
|
|
113
113
|
validateForm: validationState.validateForm as Form<T, TOut>['validateForm'],
|
|
114
114
|
submitHandler: onSubmit => makeSubmitHandler(form, validationState)(onSubmit),
|
|
115
115
|
getSubForm: (path, subformOptions) => {
|
|
116
|
-
return createSubformInterface(form, path,
|
|
116
|
+
return createSubformInterface(form, path, validationState, subformOptions)
|
|
117
117
|
},
|
|
118
118
|
getFieldArray: (path, fieldArrayOptions) => {
|
|
119
119
|
return useFieldArray(form, path, fieldArrayOptions)
|
|
@@ -16,10 +16,10 @@ import {
|
|
|
16
16
|
import { makeSubmitHandler } from '../utils/submitHandler'
|
|
17
17
|
import { useFieldArray } from './useFieldArray'
|
|
18
18
|
import type { DefineFieldOptions } from './useFieldRegistry'
|
|
19
|
-
import type { UseFormOptions } from './useForm'
|
|
20
19
|
import {
|
|
21
20
|
createValidator,
|
|
22
21
|
SuccessValidationResult,
|
|
22
|
+
type ValidationState,
|
|
23
23
|
type ValidatorOptions,
|
|
24
24
|
} from './useValidation'
|
|
25
25
|
|
|
@@ -65,7 +65,7 @@ export function createSubformInterface<
|
|
|
65
65
|
>(
|
|
66
66
|
mainForm: Form<T, TOut>,
|
|
67
67
|
path: K,
|
|
68
|
-
|
|
68
|
+
validationState: ValidationState<T, TOut>,
|
|
69
69
|
_options?: SubformOptions<PickEntity<T, K>>,
|
|
70
70
|
): Form<PickEntity<T, K>, PickEntity<TOut, K>> {
|
|
71
71
|
type ST = PickEntity<T, K>
|
|
@@ -158,7 +158,10 @@ export function createSubformInterface<
|
|
|
158
158
|
const errors = computed(() =>
|
|
159
159
|
filterErrorsForPath(unref(mainForm.errors), path))
|
|
160
160
|
|
|
161
|
-
const validateForm = (() => mainForm.validateForm()) as Form<
|
|
161
|
+
const validateForm = (() => mainForm.validateForm()) as Form<
|
|
162
|
+
ST,
|
|
163
|
+
STOut
|
|
164
|
+
>['validateForm']
|
|
162
165
|
|
|
163
166
|
// Nested subforms
|
|
164
167
|
const getSubForm = <P extends EntityPaths<ST>>(
|
|
@@ -203,7 +206,8 @@ export function createSubformInterface<
|
|
|
203
206
|
reset,
|
|
204
207
|
validateForm,
|
|
205
208
|
getSubForm,
|
|
206
|
-
submitHandler: onSubmit =>
|
|
209
|
+
submitHandler: onSubmit =>
|
|
210
|
+
makeSubmitHandler(subForm, validationState)(onSubmit),
|
|
207
211
|
getFieldArray: (fieldArrayPath, fieldArrayOptions) => {
|
|
208
212
|
return useFieldArray(subForm, fieldArrayPath, fieldArrayOptions)
|
|
209
213
|
},
|
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
getCurrentScope,
|
|
4
|
+
isRef,
|
|
5
|
+
onBeforeUnmount,
|
|
6
|
+
reactive,
|
|
7
|
+
ref,
|
|
8
|
+
toRefs,
|
|
9
|
+
toValue,
|
|
10
|
+
unref,
|
|
11
|
+
watch,
|
|
12
|
+
type MaybeRef,
|
|
13
|
+
type MaybeRefOrGetter,
|
|
14
|
+
type Ref,
|
|
15
|
+
} from 'vue'
|
|
16
|
+
import type z from 'zod'
|
|
3
17
|
import type { FormDataDefault } from '../types/form'
|
|
4
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
ErrorBag,
|
|
20
|
+
ValidationFunction,
|
|
21
|
+
ValidationResult,
|
|
22
|
+
Validator,
|
|
23
|
+
} from '../types/validation'
|
|
5
24
|
import { hasErrors, isValidResult, mergeErrors } from '../utils/validation'
|
|
6
25
|
import { flattenError } from '../utils/zod'
|
|
7
26
|
|
|
@@ -35,7 +54,8 @@ export interface ValidatorOptions<T, TOut = T> {
|
|
|
35
54
|
validateFn?: MaybeRef<ValidationFunction<T, TOut> | undefined>
|
|
36
55
|
}
|
|
37
56
|
|
|
38
|
-
export interface ValidationOptions<T, TOut = T>
|
|
57
|
+
export interface ValidationOptions<T, TOut = T>
|
|
58
|
+
extends ValidatorOptions<T, TOut> {
|
|
39
59
|
errors?: MaybeRef<ErrorBag | undefined>
|
|
40
60
|
validationBeforeSubmit?: ValidationFlags
|
|
41
61
|
validationAfterSubmit?: ValidationFlags
|
|
@@ -48,7 +68,8 @@ export const SuccessValidationResult: ValidationResult<never> = {
|
|
|
48
68
|
},
|
|
49
69
|
}
|
|
50
70
|
|
|
51
|
-
class ZodSchemaValidator<T extends FormDataDefault, TOut = T>
|
|
71
|
+
class ZodSchemaValidator<T extends FormDataDefault, TOut = T>
|
|
72
|
+
implements Validator<T, TOut> {
|
|
52
73
|
constructor(private schema?: z.ZodType<TOut, T>) {}
|
|
53
74
|
|
|
54
75
|
async validate(data: T): Promise<ValidationResult<TOut>> {
|
|
@@ -74,7 +95,8 @@ class ZodSchemaValidator<T extends FormDataDefault, TOut = T> implements Validat
|
|
|
74
95
|
}
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
class FunctionValidator<T extends FormDataDefault, TOut = T>
|
|
98
|
+
class FunctionValidator<T extends FormDataDefault, TOut = T>
|
|
99
|
+
implements Validator<T, TOut> {
|
|
78
100
|
constructor(private validateFn?: ValidationFunction<T, TOut>) {}
|
|
79
101
|
|
|
80
102
|
async validate(data: T): Promise<ValidationResult<TOut>> {
|
|
@@ -101,7 +123,8 @@ class FunctionValidator<T extends FormDataDefault, TOut = T> implements Validato
|
|
|
101
123
|
}
|
|
102
124
|
}
|
|
103
125
|
|
|
104
|
-
class CombinedValidator<T extends FormDataDefault, TOut = T>
|
|
126
|
+
class CombinedValidator<T extends FormDataDefault, TOut = T>
|
|
127
|
+
implements Validator<T, TOut> {
|
|
105
128
|
private schemaValidator: ZodSchemaValidator<T, TOut>
|
|
106
129
|
private functionValidator: FunctionValidator<T, TOut>
|
|
107
130
|
|
|
@@ -129,10 +152,13 @@ class CombinedValidator<T extends FormDataDefault, TOut = T> implements Validato
|
|
|
129
152
|
export function createValidator<T extends FormDataDefault, TOut = T>(
|
|
130
153
|
options: ValidatorOptions<T, TOut>,
|
|
131
154
|
): Ref<Validator<T, TOut> | undefined> {
|
|
132
|
-
return computed(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
155
|
+
return computed(
|
|
156
|
+
() =>
|
|
157
|
+
new CombinedValidator(
|
|
158
|
+
unref(options.schema) as z.ZodType<TOut, T>,
|
|
159
|
+
unref(options.validateFn) as ValidationFunction<T, TOut>,
|
|
160
|
+
),
|
|
161
|
+
)
|
|
136
162
|
}
|
|
137
163
|
|
|
138
164
|
export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
@@ -145,20 +171,29 @@ export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
|
145
171
|
errors: unref(options.errors) ?? SuccessValidationResult.errors,
|
|
146
172
|
})
|
|
147
173
|
|
|
148
|
-
const updateErrors = (
|
|
149
|
-
|
|
174
|
+
const updateErrors = (
|
|
175
|
+
newErrors: ErrorBag = SuccessValidationResult.errors,
|
|
176
|
+
) => {
|
|
177
|
+
validationState.errors = mergeErrors(
|
|
178
|
+
unref(options.errors) ?? SuccessValidationResult.errors,
|
|
179
|
+
newErrors,
|
|
180
|
+
)
|
|
150
181
|
}
|
|
151
182
|
|
|
152
183
|
// Watch for changes in the error bag and update validation state
|
|
153
|
-
watch(
|
|
154
|
-
|
|
155
|
-
|
|
184
|
+
watch(
|
|
185
|
+
() => unref(options.errors),
|
|
186
|
+
async () => {
|
|
187
|
+
if (validationState.isValidated) {
|
|
188
|
+
const validationResults = await getValidationResults()
|
|
156
189
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
190
|
+
updateErrors(validationResults.errors)
|
|
191
|
+
} else {
|
|
192
|
+
updateErrors()
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{ immediate: true },
|
|
196
|
+
)
|
|
162
197
|
|
|
163
198
|
// Watch for changes in validation function or schema
|
|
164
199
|
// to trigger validation. Only run if validation is already validated.
|
|
@@ -187,11 +222,15 @@ export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
|
187
222
|
})
|
|
188
223
|
|
|
189
224
|
const defineValidator = <TData extends T, TDataOut extends TOut>(
|
|
190
|
-
options:
|
|
225
|
+
options:
|
|
226
|
+
| ValidatorOptions<TData, TDataOut>
|
|
227
|
+
| Ref<Validator<TData, TDataOut>>,
|
|
191
228
|
) => {
|
|
192
229
|
const validator = isRef(options) ? options : createValidator(options)
|
|
193
230
|
|
|
194
|
-
validationState.validators.push(
|
|
231
|
+
validationState.validators.push(
|
|
232
|
+
validator as Ref<Validator<T, TOut> | undefined>,
|
|
233
|
+
)
|
|
195
234
|
|
|
196
235
|
if (getCurrentScope()) {
|
|
197
236
|
onBeforeUnmount(() => {
|
|
@@ -240,7 +279,9 @@ export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
|
240
279
|
}
|
|
241
280
|
}
|
|
242
281
|
|
|
243
|
-
const validateField = async (
|
|
282
|
+
const validateField = async (
|
|
283
|
+
path: string,
|
|
284
|
+
): Promise<ValidationResult<TOut>> => {
|
|
244
285
|
const validationResults = await getValidationResults()
|
|
245
286
|
|
|
246
287
|
updateErrors({
|
|
@@ -272,7 +313,10 @@ export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
|
272
313
|
return toValue(flags?.[flag] ?? false)
|
|
273
314
|
}
|
|
274
315
|
|
|
275
|
-
const validateStrategy = <K extends keyof ValidationFlags>(
|
|
316
|
+
const validateStrategy = <K extends keyof ValidationFlags>(
|
|
317
|
+
flag: K,
|
|
318
|
+
path: string,
|
|
319
|
+
) => {
|
|
276
320
|
if (!canValidate(flag)) {
|
|
277
321
|
return
|
|
278
322
|
}
|
|
@@ -292,4 +336,6 @@ export function useValidation<T extends FormDataDefault, TOut = T>(
|
|
|
292
336
|
}
|
|
293
337
|
}
|
|
294
338
|
|
|
295
|
-
export type ValidationState<T extends FormDataDefault, TOut = T> = ReturnType<
|
|
339
|
+
export type ValidationState<T extends FormDataDefault, TOut = T> = ReturnType<
|
|
340
|
+
typeof useValidation<T, TOut>
|
|
341
|
+
>
|
package/src/types/form.ts
CHANGED
|
@@ -5,11 +5,11 @@ import type { SubformOptions } from '../composables/useSubform'
|
|
|
5
5
|
import type { ValidatorOptions } from '../composables/useValidation'
|
|
6
6
|
import type { EntityPaths, Paths, PickEntity, PickProps } from './util'
|
|
7
7
|
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
ErrorBag,
|
|
9
|
+
ValidationErrorMessage,
|
|
10
|
+
ValidationErrors,
|
|
11
|
+
ValidationResult,
|
|
12
|
+
Validator,
|
|
13
13
|
} from './validation'
|
|
14
14
|
|
|
15
15
|
export type FormDataDefault = object
|
package/src/types/util.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { isValidResult } from './validation'
|
|
|
5
5
|
|
|
6
6
|
export function makeSubmitHandler<T extends FormDataDefault, TOut = T>(
|
|
7
7
|
form: Form<T, TOut>,
|
|
8
|
-
validationState: ValidationState<T, TOut>,
|
|
8
|
+
validationState: Pick<ValidationState<T, TOut>, 'canValidate'>,
|
|
9
9
|
) {
|
|
10
10
|
return (onSubmit: (data: TOut) => Awaitable<void>) => {
|
|
11
11
|
return async (event?: SubmitEvent) => {
|
package/src/utils/zod.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import z from 'zod'
|
|
1
|
+
import type z from 'zod'
|
|
2
2
|
import type { ErrorBag } from '../types/validation'
|
|
3
3
|
|
|
4
4
|
export function flattenError(error: z.ZodError): ErrorBag {
|
|
@@ -8,14 +8,17 @@ export function flattenError(error: z.ZodError): ErrorBag {
|
|
|
8
8
|
|
|
9
9
|
const propertyErrors = error.issues
|
|
10
10
|
.filter(issue => issue.path.length > 0)
|
|
11
|
-
.reduce(
|
|
12
|
-
|
|
11
|
+
.reduce(
|
|
12
|
+
(acc, issue) => {
|
|
13
|
+
const path = issue.path.join('.')
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
return {
|
|
16
|
+
...acc,
|
|
17
|
+
[path]: [...(acc[path] ?? []), issue.message],
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{} as ErrorBag['propertyErrors'],
|
|
21
|
+
)
|
|
19
22
|
|
|
20
23
|
return {
|
|
21
24
|
general,
|
package/tests/path-utils.test.ts
CHANGED