@teamnovu/kit-vue-forms 0.1.5 → 0.1.7
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/useValidation.d.ts +1 -0
- package/dist/index.js +228 -228
- package/docs/FormInput-example.md +59 -0
- package/docs/example.md +212 -0
- package/docs/index.md +26 -0
- package/docs/info.json +3 -0
- package/docs/reference.md +136 -0
- package/package.json +1 -1
- package/src/composables/useForm.ts +1 -0
- package/src/composables/useValidation.ts +6 -0
- package/src/utils/path.ts +1 -4
- package/tests/useValidation.test.ts +41 -0
|
@@ -18,6 +18,7 @@ export declare function useValidation<T extends FormDataDefault>(formState: {
|
|
|
18
18
|
validateForm: () => Promise<ValidationResult>;
|
|
19
19
|
defineValidator: <TData extends T>(options: ValidatorOptions<TData> | Ref<Validator<TData>>) => Ref<Validator<TData> | undefined, Validator<TData> | undefined>;
|
|
20
20
|
isValid: ComputedRef<boolean>;
|
|
21
|
+
reset: () => void;
|
|
21
22
|
validators: Ref<Ref<Validator<T> | undefined, Validator<T> | undefined>[], Ref<Validator<T> | undefined, Validator<T> | undefined>[]>;
|
|
22
23
|
isValidated: Ref<boolean, boolean>;
|
|
23
24
|
errors: Ref<{
|
package/dist/index.js
CHANGED
|
@@ -1,54 +1,51 @@
|
|
|
1
1
|
var T = Object.defineProperty;
|
|
2
|
-
var G = (
|
|
3
|
-
var E = (
|
|
4
|
-
import { toValue as L, toRaw as Z, computed as
|
|
2
|
+
var G = (t, e, r) => e in t ? T(t, e, { enumerable: !0, configurable: !0, writable: !0, value: r }) : t[e] = r;
|
|
3
|
+
var E = (t, e, r) => G(t, typeof e != "symbol" ? e + "" : e, r);
|
|
4
|
+
import { toValue as L, toRaw as Z, computed as c, unref as d, reactive as D, toRefs as N, shallowReactive as q, toRef as _, onScopeDispose as H, ref as W, watch as w, isRef as k, getCurrentScope as Q, onBeforeUnmount as X, defineComponent as j, renderSlot as O, normalizeProps as z, guardReactiveProps as B, resolveComponent as Y, createBlock as $, openBlock as A, withCtx as C, resolveDynamicComponent as x, mergeProps as ee } from "vue";
|
|
5
5
|
import { cloneDeep as re } from "lodash-es";
|
|
6
6
|
import "zod";
|
|
7
|
-
function
|
|
8
|
-
const e = L(
|
|
9
|
-
return re(
|
|
7
|
+
function g(t) {
|
|
8
|
+
const e = L(t), r = Z(e);
|
|
9
|
+
return re(r);
|
|
10
10
|
}
|
|
11
|
-
function K(
|
|
12
|
-
return
|
|
11
|
+
function K(t) {
|
|
12
|
+
return t === "" ? [] : t.split(/\s*\.\s*/).filter(Boolean);
|
|
13
13
|
}
|
|
14
|
-
function
|
|
14
|
+
function P(t, e) {
|
|
15
15
|
return (Array.isArray(e) ? e : K(e)).reduce(
|
|
16
16
|
(s, o) => s == null ? void 0 : s[o],
|
|
17
|
-
|
|
17
|
+
t
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
|
-
function te(
|
|
21
|
-
const s = Array.isArray(e) ? e : K(e)
|
|
22
|
-
|
|
23
|
-
throw new Error("Path cannot be empty");
|
|
24
|
-
const o = s.at(-1), n = s.slice(0, -1).reduce(
|
|
25
|
-
(h, v) => h[v],
|
|
20
|
+
function te(t, e, r) {
|
|
21
|
+
const s = Array.isArray(e) ? e : K(e), o = s.at(-1), n = s.slice(0, -1).reduce(
|
|
22
|
+
(p, h) => p[h],
|
|
26
23
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
-
|
|
24
|
+
t
|
|
28
25
|
);
|
|
29
|
-
n[o] =
|
|
26
|
+
n[o] = o ? r : n;
|
|
30
27
|
}
|
|
31
|
-
const U = (
|
|
28
|
+
const U = (t, e) => c({
|
|
32
29
|
get() {
|
|
33
|
-
return
|
|
30
|
+
return P(d(t), d(e));
|
|
34
31
|
},
|
|
35
|
-
set(
|
|
36
|
-
te(
|
|
32
|
+
set(r) {
|
|
33
|
+
te(d(t), d(e), r);
|
|
37
34
|
}
|
|
38
35
|
});
|
|
39
|
-
function
|
|
40
|
-
return !
|
|
36
|
+
function R(t, e) {
|
|
37
|
+
return !t && !e ? "" : !t && e ? e : !e && t ? t : `${t}.${e}`;
|
|
41
38
|
}
|
|
42
|
-
function se(
|
|
39
|
+
function se(t, e) {
|
|
43
40
|
if (!e)
|
|
44
|
-
return
|
|
45
|
-
const
|
|
46
|
-
Object.entries(
|
|
47
|
-
([o, n]) => [o.slice(
|
|
41
|
+
return t;
|
|
42
|
+
const r = `${e}.`, s = Object.fromEntries(
|
|
43
|
+
Object.entries(t.propertyErrors).filter(([o]) => o.startsWith(r)).map(
|
|
44
|
+
([o, n]) => [o.slice(r.length), n]
|
|
48
45
|
)
|
|
49
46
|
);
|
|
50
47
|
return {
|
|
51
|
-
general:
|
|
48
|
+
general: t.general,
|
|
52
49
|
// Keep general errors
|
|
53
50
|
propertyErrors: s
|
|
54
51
|
};
|
|
@@ -65,138 +62,138 @@ class ae {
|
|
|
65
62
|
this.rc > 0 && (this.rc -= 1, this.rc === 0 && this.drop && this.drop());
|
|
66
63
|
}
|
|
67
64
|
}
|
|
68
|
-
function oe(
|
|
69
|
-
const e =
|
|
70
|
-
value:
|
|
71
|
-
path:
|
|
72
|
-
initialValue:
|
|
73
|
-
errors:
|
|
65
|
+
function oe(t) {
|
|
66
|
+
const e = D({
|
|
67
|
+
value: t.value,
|
|
68
|
+
path: t.path,
|
|
69
|
+
initialValue: c(() => Object.freeze(g(t.initialValue))),
|
|
70
|
+
errors: t.errors,
|
|
74
71
|
touched: !1
|
|
75
|
-
}),
|
|
72
|
+
}), r = c(() => JSON.stringify(e.value) !== JSON.stringify(e.initialValue)), s = (a) => {
|
|
76
73
|
e.value = a;
|
|
77
74
|
}, o = () => {
|
|
78
75
|
e.touched = !0;
|
|
79
76
|
}, n = () => {
|
|
80
|
-
},
|
|
81
|
-
e.value =
|
|
82
|
-
},
|
|
77
|
+
}, p = () => {
|
|
78
|
+
e.value = g(e.initialValue), e.touched = !1, e.errors = [];
|
|
79
|
+
}, h = (a) => {
|
|
83
80
|
e.errors = a;
|
|
84
|
-
},
|
|
81
|
+
}, V = () => {
|
|
85
82
|
e.errors = [];
|
|
86
|
-
},
|
|
83
|
+
}, i = N(e);
|
|
87
84
|
return {
|
|
88
|
-
data:
|
|
89
|
-
path:
|
|
90
|
-
initialValue:
|
|
91
|
-
errors:
|
|
92
|
-
touched:
|
|
93
|
-
dirty:
|
|
85
|
+
data: i.value,
|
|
86
|
+
path: i.path,
|
|
87
|
+
initialValue: i.initialValue,
|
|
88
|
+
errors: i.errors,
|
|
89
|
+
touched: i.touched,
|
|
90
|
+
dirty: r,
|
|
94
91
|
setData: s,
|
|
95
92
|
onBlur: o,
|
|
96
93
|
onFocus: n,
|
|
97
|
-
reset:
|
|
98
|
-
setErrors:
|
|
99
|
-
clearErrors:
|
|
94
|
+
reset: p,
|
|
95
|
+
setErrors: h,
|
|
96
|
+
clearErrors: V
|
|
100
97
|
};
|
|
101
98
|
}
|
|
102
|
-
function ne(
|
|
103
|
-
const
|
|
104
|
-
const u =
|
|
99
|
+
function ne(t, e) {
|
|
100
|
+
const r = /* @__PURE__ */ new Map(), s = q(/* @__PURE__ */ new Map()), o = (a) => {
|
|
101
|
+
const u = d(a.path);
|
|
105
102
|
s.set(u, a);
|
|
106
103
|
}, n = (a) => {
|
|
107
104
|
s.delete(a);
|
|
108
|
-
},
|
|
105
|
+
}, p = (a) => {
|
|
109
106
|
var u;
|
|
110
|
-
|
|
111
|
-
},
|
|
107
|
+
r.has(a) ? (u = r.get(a)) == null || u.inc() : r.set(a, new ae(() => n(a)));
|
|
108
|
+
}, h = (a) => {
|
|
112
109
|
var u;
|
|
113
|
-
|
|
114
|
-
},
|
|
110
|
+
r.has(a) && ((u = r.get(a)) == null || u.dec());
|
|
111
|
+
}, V = (a) => {
|
|
115
112
|
if (!s.has(a)) {
|
|
116
|
-
const
|
|
113
|
+
const v = oe({
|
|
117
114
|
path: a,
|
|
118
|
-
value: U(_(
|
|
119
|
-
initialValue:
|
|
120
|
-
errors:
|
|
115
|
+
value: U(_(t, "data"), a),
|
|
116
|
+
initialValue: c(() => P(t.initialData, a)),
|
|
117
|
+
errors: c({
|
|
121
118
|
get() {
|
|
122
119
|
return e.errors.value.propertyErrors[a] || [];
|
|
123
120
|
},
|
|
124
|
-
set(
|
|
125
|
-
e.errors.value.propertyErrors[a] =
|
|
121
|
+
set(F) {
|
|
122
|
+
e.errors.value.propertyErrors[a] = F;
|
|
126
123
|
}
|
|
127
124
|
})
|
|
128
125
|
});
|
|
129
|
-
o(
|
|
126
|
+
o(v);
|
|
130
127
|
}
|
|
131
128
|
const u = s.get(a);
|
|
132
|
-
return
|
|
133
|
-
|
|
129
|
+
return p(a), H(() => {
|
|
130
|
+
h(a);
|
|
134
131
|
}), u;
|
|
135
|
-
},
|
|
132
|
+
}, i = (a) => V(a.path);
|
|
136
133
|
return {
|
|
137
|
-
fields:
|
|
138
|
-
getField:
|
|
134
|
+
fields: c(() => [...s.values()]),
|
|
135
|
+
getField: V,
|
|
139
136
|
registerField: o,
|
|
140
137
|
deregisterField: n,
|
|
141
|
-
defineField:
|
|
138
|
+
defineField: i
|
|
142
139
|
};
|
|
143
140
|
}
|
|
144
|
-
function ie(
|
|
145
|
-
const e =
|
|
141
|
+
function ie(t) {
|
|
142
|
+
const e = c(() => t.fields.value.some((s) => d(s.dirty))), r = c(() => t.fields.value.some((s) => d(s.touched)));
|
|
146
143
|
return {
|
|
147
144
|
isDirty: e,
|
|
148
|
-
isTouched:
|
|
145
|
+
isTouched: r
|
|
149
146
|
};
|
|
150
147
|
}
|
|
151
|
-
function le(
|
|
152
|
-
return
|
|
153
|
-
(e,
|
|
148
|
+
function le(t) {
|
|
149
|
+
return t.filter(
|
|
150
|
+
(e, r, s) => s.indexOf(e) === r
|
|
154
151
|
);
|
|
155
152
|
}
|
|
156
|
-
function I(...
|
|
157
|
-
return
|
|
158
|
-
if (!e && !
|
|
153
|
+
function I(...t) {
|
|
154
|
+
return t.slice(1).reduce((e, r) => {
|
|
155
|
+
if (!e && !r)
|
|
159
156
|
return;
|
|
160
|
-
const s = ((
|
|
161
|
-
if (!e && ((
|
|
162
|
-
return
|
|
157
|
+
const s = ((r == null ? void 0 : r.length) ?? 0) > 0;
|
|
158
|
+
if (!e && ((r == null ? void 0 : r.length) ?? 0) > 0)
|
|
159
|
+
return r;
|
|
163
160
|
if (!s)
|
|
164
161
|
return e;
|
|
165
|
-
const o = (e ?? []).concat(
|
|
162
|
+
const o = (e ?? []).concat(r);
|
|
166
163
|
return le(o);
|
|
167
|
-
},
|
|
164
|
+
}, t[0]);
|
|
168
165
|
}
|
|
169
|
-
function ce(...
|
|
170
|
-
return
|
|
171
|
-
const o =
|
|
166
|
+
function ce(...t) {
|
|
167
|
+
return t.map((r) => Object.keys(r)).flat().reduce((r, s) => {
|
|
168
|
+
const o = t.map((n) => n[s]).filter(Boolean);
|
|
172
169
|
return {
|
|
173
|
-
...
|
|
170
|
+
...r,
|
|
174
171
|
[s]: I(...o)
|
|
175
172
|
};
|
|
176
173
|
}, {});
|
|
177
174
|
}
|
|
178
|
-
function S(...
|
|
179
|
-
if (!
|
|
175
|
+
function S(...t) {
|
|
176
|
+
if (!t.length)
|
|
180
177
|
return {
|
|
181
178
|
general: [],
|
|
182
179
|
propertyErrors: {}
|
|
183
180
|
};
|
|
184
|
-
const e =
|
|
185
|
-
return
|
|
186
|
-
(
|
|
187
|
-
general: I(
|
|
188
|
-
propertyErrors: ce(
|
|
181
|
+
const e = t[0];
|
|
182
|
+
return t.length === 1 ? e : t.slice(1).reduce(
|
|
183
|
+
(r, s) => ({
|
|
184
|
+
general: I(r.general, s.general),
|
|
185
|
+
propertyErrors: ce(r.propertyErrors ?? {}, s.propertyErrors ?? {})
|
|
189
186
|
}),
|
|
190
187
|
e
|
|
191
188
|
);
|
|
192
189
|
}
|
|
193
|
-
function M(
|
|
190
|
+
function M(t) {
|
|
194
191
|
var s;
|
|
195
|
-
const e = (((s =
|
|
196
|
-
return e ||
|
|
192
|
+
const e = (((s = t.general) == null ? void 0 : s.length) ?? 0) > 0, r = Object.entries(t.propertyErrors).filter(([, o]) => o == null ? void 0 : o.length).length > 0;
|
|
193
|
+
return e || r;
|
|
197
194
|
}
|
|
198
|
-
function ue(
|
|
199
|
-
const e =
|
|
195
|
+
function ue(t) {
|
|
196
|
+
const e = t.issues.filter((s) => s.path.length === 0).map((s) => s.message), r = t.issues.filter((s) => s.path.length > 0).reduce((s, o) => {
|
|
200
197
|
const n = o.path.join(".");
|
|
201
198
|
return {
|
|
202
199
|
...s,
|
|
@@ -205,7 +202,7 @@ function ue(r) {
|
|
|
205
202
|
}, {});
|
|
206
203
|
return {
|
|
207
204
|
general: e,
|
|
208
|
-
propertyErrors:
|
|
205
|
+
propertyErrors: r
|
|
209
206
|
};
|
|
210
207
|
}
|
|
211
208
|
const m = {
|
|
@@ -222,10 +219,10 @@ class de {
|
|
|
222
219
|
async validate(e) {
|
|
223
220
|
if (!this.schema)
|
|
224
221
|
return m;
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
222
|
+
const r = await this.schema.safeParseAsync(e);
|
|
223
|
+
if (r.success)
|
|
227
224
|
return m;
|
|
228
|
-
const s = ue(
|
|
225
|
+
const s = ue(r.error);
|
|
229
226
|
return {
|
|
230
227
|
isValid: !1,
|
|
231
228
|
errors: {
|
|
@@ -243,13 +240,13 @@ class fe {
|
|
|
243
240
|
if (!this.validateFn)
|
|
244
241
|
return m;
|
|
245
242
|
try {
|
|
246
|
-
const
|
|
247
|
-
return
|
|
248
|
-
} catch (
|
|
243
|
+
const r = await this.validateFn(e);
|
|
244
|
+
return r.isValid ? m : r;
|
|
245
|
+
} catch (r) {
|
|
249
246
|
return {
|
|
250
247
|
isValid: !1,
|
|
251
248
|
errors: {
|
|
252
|
-
general: [
|
|
249
|
+
general: [r.message || "Validation error"],
|
|
253
250
|
propertyErrors: {}
|
|
254
251
|
}
|
|
255
252
|
};
|
|
@@ -257,108 +254,111 @@ class fe {
|
|
|
257
254
|
}
|
|
258
255
|
}
|
|
259
256
|
class pe {
|
|
260
|
-
constructor(e,
|
|
257
|
+
constructor(e, r) {
|
|
261
258
|
E(this, "schemaValidator");
|
|
262
259
|
E(this, "functionValidator");
|
|
263
|
-
this.schema = e, this.validateFn =
|
|
260
|
+
this.schema = e, this.validateFn = r, this.schemaValidator = new de(this.schema), this.functionValidator = new fe(this.validateFn);
|
|
264
261
|
}
|
|
265
262
|
async validate(e) {
|
|
266
|
-
const [
|
|
263
|
+
const [r, s] = await Promise.all([
|
|
267
264
|
this.schemaValidator.validate(e),
|
|
268
265
|
this.functionValidator.validate(e)
|
|
269
266
|
]);
|
|
270
267
|
return {
|
|
271
|
-
isValid:
|
|
272
|
-
errors: S(
|
|
268
|
+
isValid: r.isValid && s.isValid,
|
|
269
|
+
errors: S(r.errors, s.errors)
|
|
273
270
|
};
|
|
274
271
|
}
|
|
275
272
|
}
|
|
276
|
-
function b(
|
|
277
|
-
return
|
|
278
|
-
|
|
279
|
-
|
|
273
|
+
function b(t) {
|
|
274
|
+
return c(() => new pe(
|
|
275
|
+
d(t.schema),
|
|
276
|
+
d(t.validateFn)
|
|
280
277
|
));
|
|
281
278
|
}
|
|
282
|
-
function he(
|
|
283
|
-
const
|
|
279
|
+
function he(t, e) {
|
|
280
|
+
const r = D({
|
|
284
281
|
validators: W([b(e)]),
|
|
285
282
|
isValidated: !1,
|
|
286
|
-
errors:
|
|
287
|
-
}), s = (
|
|
288
|
-
|
|
283
|
+
errors: d(e.errors) ?? m.errors
|
|
284
|
+
}), s = (i = m.errors) => {
|
|
285
|
+
r.errors = S(d(e.errors) ?? m.errors, i);
|
|
289
286
|
};
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
const
|
|
293
|
-
s(
|
|
287
|
+
w(() => d(e.errors), async () => {
|
|
288
|
+
if (r.isValidated) {
|
|
289
|
+
const i = await n();
|
|
290
|
+
s(i.errors);
|
|
294
291
|
} else
|
|
295
292
|
s();
|
|
296
|
-
}, { immediate: !0 }),
|
|
297
|
-
[() =>
|
|
298
|
-
async (
|
|
299
|
-
if (
|
|
300
|
-
if (
|
|
301
|
-
const
|
|
302
|
-
|
|
293
|
+
}, { immediate: !0 }), w(
|
|
294
|
+
[() => r.validators],
|
|
295
|
+
async (i) => {
|
|
296
|
+
if (r.isValidated)
|
|
297
|
+
if (i) {
|
|
298
|
+
const a = await n();
|
|
299
|
+
r.errors = a.errors;
|
|
303
300
|
} else
|
|
304
|
-
|
|
301
|
+
r.errors = m.errors;
|
|
305
302
|
},
|
|
306
303
|
{ immediate: !0 }
|
|
307
|
-
),
|
|
308
|
-
|
|
304
|
+
), w(() => t.data, () => {
|
|
305
|
+
r.isValidated && p();
|
|
309
306
|
});
|
|
310
|
-
const o = (
|
|
311
|
-
const
|
|
312
|
-
return
|
|
313
|
-
|
|
314
|
-
(
|
|
307
|
+
const o = (i) => {
|
|
308
|
+
const a = k(i) ? i : b(i);
|
|
309
|
+
return r.validators.push(a), Q() && X(() => {
|
|
310
|
+
r.validators = r.validators.filter(
|
|
311
|
+
(u) => u !== a
|
|
315
312
|
);
|
|
316
|
-
}),
|
|
313
|
+
}), a;
|
|
317
314
|
};
|
|
318
315
|
async function n() {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
),
|
|
322
|
-
let { errors:
|
|
323
|
-
if (!
|
|
324
|
-
const
|
|
325
|
-
|
|
316
|
+
const i = await Promise.all(
|
|
317
|
+
r.validators.filter((v) => d(v) !== void 0).map((v) => d(v).validate(t.data))
|
|
318
|
+
), a = i.every((v) => v.isValid);
|
|
319
|
+
let { errors: u } = m;
|
|
320
|
+
if (!a) {
|
|
321
|
+
const v = i.map((F) => F.errors);
|
|
322
|
+
u = S(...v);
|
|
326
323
|
}
|
|
327
324
|
return {
|
|
328
|
-
errors:
|
|
329
|
-
isValid:
|
|
325
|
+
errors: u,
|
|
326
|
+
isValid: a
|
|
330
327
|
};
|
|
331
328
|
}
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
return s(
|
|
335
|
-
isValid: !M(
|
|
336
|
-
errors:
|
|
329
|
+
const p = async () => {
|
|
330
|
+
const i = await n();
|
|
331
|
+
return s(i.errors), r.isValidated = !0, {
|
|
332
|
+
isValid: !M(i.errors),
|
|
333
|
+
errors: r.errors
|
|
337
334
|
};
|
|
338
|
-
},
|
|
335
|
+
}, h = c(() => !M(r.errors)), V = () => {
|
|
336
|
+
r.isValidated = !1, r.errors = d(e.errors) ?? m.errors;
|
|
337
|
+
};
|
|
339
338
|
return {
|
|
340
|
-
...N(
|
|
341
|
-
validateForm:
|
|
339
|
+
...N(r),
|
|
340
|
+
validateForm: p,
|
|
342
341
|
defineValidator: o,
|
|
343
|
-
isValid:
|
|
342
|
+
isValid: h,
|
|
343
|
+
reset: V
|
|
344
344
|
};
|
|
345
345
|
}
|
|
346
346
|
class ve {
|
|
347
|
-
constructor(e,
|
|
348
|
-
this.path = e, this.validator =
|
|
347
|
+
constructor(e, r) {
|
|
348
|
+
this.path = e, this.validator = r;
|
|
349
349
|
}
|
|
350
350
|
async validate(e) {
|
|
351
|
-
const
|
|
351
|
+
const r = P(e, this.path);
|
|
352
352
|
if (!this.validator)
|
|
353
353
|
return m;
|
|
354
|
-
const s = await this.validator.validate(
|
|
354
|
+
const s = await this.validator.validate(r);
|
|
355
355
|
return {
|
|
356
356
|
isValid: s.isValid,
|
|
357
357
|
errors: {
|
|
358
358
|
general: s.errors.general || [],
|
|
359
359
|
propertyErrors: s.errors.propertyErrors ? Object.fromEntries(
|
|
360
360
|
Object.entries(s.errors.propertyErrors).map(([o, n]) => [
|
|
361
|
-
|
|
361
|
+
R(this.path, o),
|
|
362
362
|
n
|
|
363
363
|
])
|
|
364
364
|
) : {}
|
|
@@ -366,83 +366,83 @@ class ve {
|
|
|
366
366
|
};
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
|
-
function me(
|
|
370
|
-
const s = U(
|
|
371
|
-
...
|
|
372
|
-
path:
|
|
373
|
-
setData: (
|
|
374
|
-
|
|
369
|
+
function me(t, e, r) {
|
|
370
|
+
const s = U(t.data, e), o = c(() => P(t.initialData.value, e)), n = (l) => ({
|
|
371
|
+
...l,
|
|
372
|
+
path: c(() => d(l.path).replace(e + ".", "")),
|
|
373
|
+
setData: (f) => {
|
|
374
|
+
l.setData(f);
|
|
375
375
|
}
|
|
376
|
-
}),
|
|
377
|
-
const
|
|
378
|
-
return
|
|
379
|
-
},
|
|
380
|
-
const
|
|
381
|
-
...
|
|
382
|
-
path:
|
|
376
|
+
}), p = (l) => {
|
|
377
|
+
const f = R(e, l), y = t.getField(f);
|
|
378
|
+
return y ? n(y) : {};
|
|
379
|
+
}, h = (l) => {
|
|
380
|
+
const f = R(e, l.path), y = t.defineField({
|
|
381
|
+
...l,
|
|
382
|
+
path: f
|
|
383
383
|
});
|
|
384
|
-
return n(
|
|
385
|
-
},
|
|
386
|
-
const
|
|
387
|
-
return
|
|
388
|
-
}).map((
|
|
389
|
-
const
|
|
390
|
-
return
|
|
391
|
-
}), a =
|
|
384
|
+
return n(y);
|
|
385
|
+
}, V = c(() => t.fields.value.filter((l) => {
|
|
386
|
+
const f = l.path.value;
|
|
387
|
+
return f.startsWith(e + ".") || f === e;
|
|
388
|
+
}).map((l) => n(l))), i = () => t.fields.value.filter((l) => {
|
|
389
|
+
const f = l.path.value;
|
|
390
|
+
return f.startsWith(e + ".") || f === e;
|
|
391
|
+
}), a = c(() => i().some((l) => l.dirty.value)), u = c(() => i().some((l) => l.touched.value)), v = c(() => t.isValid.value), F = c(() => t.isValidated.value), J = c(() => se(d(t.errors), e));
|
|
392
392
|
return {
|
|
393
393
|
data: s,
|
|
394
|
-
fields:
|
|
394
|
+
fields: V,
|
|
395
395
|
initialData: o,
|
|
396
|
-
defineField:
|
|
397
|
-
getField:
|
|
396
|
+
defineField: h,
|
|
397
|
+
getField: p,
|
|
398
398
|
isDirty: a,
|
|
399
399
|
isTouched: u,
|
|
400
|
-
isValid:
|
|
401
|
-
isValidated:
|
|
400
|
+
isValid: v,
|
|
401
|
+
isValidated: F,
|
|
402
402
|
errors: J,
|
|
403
|
-
defineValidator: (
|
|
404
|
-
const
|
|
405
|
-
() => new ve(e, f
|
|
403
|
+
defineValidator: (l) => {
|
|
404
|
+
const f = k(l) ? l : b(l), y = c(
|
|
405
|
+
() => new ve(e, d(f))
|
|
406
406
|
);
|
|
407
|
-
return
|
|
407
|
+
return t.defineValidator(y), f;
|
|
408
408
|
},
|
|
409
|
-
reset: () =>
|
|
410
|
-
validateForm: () =>
|
|
411
|
-
getSubForm: (
|
|
412
|
-
const
|
|
413
|
-
return
|
|
414
|
-
|
|
415
|
-
|
|
409
|
+
reset: () => i().forEach((l) => l.reset()),
|
|
410
|
+
validateForm: () => t.validateForm(),
|
|
411
|
+
getSubForm: (l, f) => {
|
|
412
|
+
const y = R(e, l);
|
|
413
|
+
return t.getSubForm(
|
|
414
|
+
y,
|
|
415
|
+
f
|
|
416
416
|
);
|
|
417
417
|
}
|
|
418
418
|
};
|
|
419
419
|
}
|
|
420
|
-
function Pe(
|
|
421
|
-
const e =
|
|
420
|
+
function Pe(t) {
|
|
421
|
+
const e = c(() => Object.freeze(g(t.initialData))), r = W(g(e)), s = D({
|
|
422
422
|
initialData: e,
|
|
423
|
-
data:
|
|
423
|
+
data: r
|
|
424
424
|
});
|
|
425
|
-
|
|
426
|
-
s.data =
|
|
425
|
+
w(e, (a) => {
|
|
426
|
+
s.data = g(a);
|
|
427
427
|
});
|
|
428
|
-
const o = he(s,
|
|
429
|
-
|
|
428
|
+
const o = he(s, t), n = ne(s, o), p = ie(n), h = () => {
|
|
429
|
+
r.value = g(e), o.reset(), n.fields.value.forEach(
|
|
430
430
|
(a) => a.reset()
|
|
431
431
|
);
|
|
432
432
|
};
|
|
433
|
-
function
|
|
434
|
-
return me(
|
|
433
|
+
function V(a, u) {
|
|
434
|
+
return me(i, a);
|
|
435
435
|
}
|
|
436
|
-
const
|
|
436
|
+
const i = {
|
|
437
437
|
...n,
|
|
438
438
|
...o,
|
|
439
|
-
...
|
|
440
|
-
reset:
|
|
441
|
-
getSubForm:
|
|
439
|
+
...p,
|
|
440
|
+
reset: h,
|
|
441
|
+
getSubForm: V,
|
|
442
442
|
initialData: _(s, "initialData"),
|
|
443
443
|
data: _(s, "data")
|
|
444
444
|
};
|
|
445
|
-
return
|
|
445
|
+
return i;
|
|
446
446
|
}
|
|
447
447
|
const _e = /* @__PURE__ */ j({
|
|
448
448
|
__name: "Field",
|
|
@@ -453,10 +453,10 @@ const _e = /* @__PURE__ */ j({
|
|
|
453
453
|
path: {},
|
|
454
454
|
errors: {}
|
|
455
455
|
},
|
|
456
|
-
setup(
|
|
457
|
-
const e =
|
|
456
|
+
setup(t) {
|
|
457
|
+
const e = t, r = e.form.defineField({
|
|
458
458
|
path: e.path
|
|
459
|
-
}), s =
|
|
459
|
+
}), s = D(r);
|
|
460
460
|
return (o, n) => O(o.$slots, "default", z(B(s)));
|
|
461
461
|
}
|
|
462
462
|
}), Se = /* @__PURE__ */ j({
|
|
@@ -468,19 +468,19 @@ const _e = /* @__PURE__ */ j({
|
|
|
468
468
|
form: {},
|
|
469
469
|
path: {}
|
|
470
470
|
},
|
|
471
|
-
setup(
|
|
472
|
-
return (e,
|
|
471
|
+
setup(t) {
|
|
472
|
+
return (e, r) => {
|
|
473
473
|
const s = Y("Field");
|
|
474
474
|
return A(), $(s, {
|
|
475
475
|
form: e.form,
|
|
476
476
|
path: e.path
|
|
477
477
|
}, {
|
|
478
|
-
default: C(({ errors: o, data: n, setData:
|
|
478
|
+
default: C(({ errors: o, data: n, setData: p }) => [
|
|
479
479
|
(A(), $(x(e.component), ee({ ...e.componentProps, ...e.$attrs }, {
|
|
480
480
|
"model-value": n,
|
|
481
481
|
errors: o,
|
|
482
482
|
name: e.path,
|
|
483
|
-
"onUpdate:modelValue":
|
|
483
|
+
"onUpdate:modelValue": p
|
|
484
484
|
}), {
|
|
485
485
|
default: C(() => [
|
|
486
486
|
O(e.$slots, "default")
|
|
@@ -498,9 +498,9 @@ const _e = /* @__PURE__ */ j({
|
|
|
498
498
|
form: {},
|
|
499
499
|
path: {}
|
|
500
500
|
},
|
|
501
|
-
setup(
|
|
502
|
-
const e =
|
|
503
|
-
return (s, o) => O(s.$slots, "default", z(B({ subform:
|
|
501
|
+
setup(t) {
|
|
502
|
+
const e = t, r = c(() => e.form.getSubForm(e.path));
|
|
503
|
+
return (s, o) => O(s.$slots, "default", z(B({ subform: r.value })));
|
|
504
504
|
}
|
|
505
505
|
});
|
|
506
506
|
export {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# FormInput Example
|
|
2
|
+
|
|
3
|
+
To define a form input component you need a plain input component that has at least the following props and emits:
|
|
4
|
+
```typescript
|
|
5
|
+
interface Props {
|
|
6
|
+
modelValue: T
|
|
7
|
+
errors?: string[] | undefined // if the input should display errors
|
|
8
|
+
}
|
|
9
|
+
interface Emits {
|
|
10
|
+
'update:modelValue': [T]
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
Then we can easily use the `FormFieldWrapper` component from this package.
|
|
14
|
+
|
|
15
|
+
In the example below we use a text field `TextField.vue`.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
```vue
|
|
19
|
+
<!-- FormTextInput.vue -->
|
|
20
|
+
<template>
|
|
21
|
+
<FormFieldWrapper
|
|
22
|
+
:component="TextField"
|
|
23
|
+
:component-props="$props"
|
|
24
|
+
:path="path"
|
|
25
|
+
:form="form"
|
|
26
|
+
/>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts" generic="TData extends object, TPath extends Paths<TData>">
|
|
30
|
+
import type { TextFieldProps } from '#components/utils/form/plainInput/TextField.vue';
|
|
31
|
+
import TextField from '#components/utils/form/plainInput/TextField.vue';
|
|
32
|
+
import { type Paths, FormFieldWrapper } from '@teamnovu/kit-vue-forms';
|
|
33
|
+
import type {
|
|
34
|
+
ExcludedFieldProps,
|
|
35
|
+
FormComponentProps,
|
|
36
|
+
} from '#types/form/FormComponentProps';
|
|
37
|
+
|
|
38
|
+
export type Props<TData extends object, TPath extends Paths<TData>> =
|
|
39
|
+
FormComponentProps<TData, TPath, TextFieldProps['modelValue']> & Omit<TextFieldProps, ExcludedFieldProps>;
|
|
40
|
+
|
|
41
|
+
defineProps<Props<TData, TPath>>();
|
|
42
|
+
</script>
|
|
43
|
+
```
|
|
44
|
+
Note the usage of the generic types `TData` and `TPath` to make the component fully type-safe. With this, the `path` prop
|
|
45
|
+
will only allow valid paths of the form data. Moreover, it will throw a type error if the property at `path` of the `form`
|
|
46
|
+
has the wrong type. This is ensured by using the `FormComponentProps` type above.
|
|
47
|
+
|
|
48
|
+
The usage of such a form input component is as follows (assuming the input should handle the "firstName" property of the form data):
|
|
49
|
+
```vue
|
|
50
|
+
<FormTextInput
|
|
51
|
+
:form="form"
|
|
52
|
+
path="firstName"
|
|
53
|
+
/>
|
|
54
|
+
```
|
|
55
|
+
Here, `form` is the Form component that was created with [`useForm`](index.md#useform). All additional props are passed
|
|
56
|
+
to the underlying plain input component, e.g. `label`, `placeholder`, etc.
|
|
57
|
+
|
|
58
|
+
It is recommended to use this pattern of a styled "plain input" that works with v-model and a "form input" to work with form
|
|
59
|
+
and path as props for all your form inputs. Like that, you can easily use the plain component outside of forms as well.
|
package/docs/example.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Example
|
|
2
|
+
|
|
3
|
+
The following example shows how to use this library for a more complicated form with nested objects and arrays.
|
|
4
|
+
The data structure of the form is as follows:
|
|
5
|
+
```typescript
|
|
6
|
+
{
|
|
7
|
+
person: {
|
|
8
|
+
firstName: string,
|
|
9
|
+
lastName: string | undefined,
|
|
10
|
+
address: {
|
|
11
|
+
street: string,
|
|
12
|
+
city: string,
|
|
13
|
+
},
|
|
14
|
+
hobbies: string[]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
We can create a form like this:
|
|
20
|
+
|
|
21
|
+
```vue
|
|
22
|
+
<template>
|
|
23
|
+
<form @submit.prevent="submit">
|
|
24
|
+
<!-- Simple form inputs -->
|
|
25
|
+
<!-- the "label" prop is passed through the FormTextField to the underlying TextField -->
|
|
26
|
+
<FormTextField
|
|
27
|
+
:form="form"
|
|
28
|
+
path="person.firstName"
|
|
29
|
+
label="First Name"
|
|
30
|
+
/>
|
|
31
|
+
<FormTextField
|
|
32
|
+
:form="form"
|
|
33
|
+
path="person.lastName"
|
|
34
|
+
label="Last Name"
|
|
35
|
+
/>
|
|
36
|
+
|
|
37
|
+
<!-- Oh oh, my path was wrong here, the form type does not have a property "person.middleName" -->
|
|
38
|
+
<!-- I get a typescript error that this path is not allowed
|
|
39
|
+
<FormTextField
|
|
40
|
+
:form="form"
|
|
41
|
+
path="person.middleName"
|
|
42
|
+
/>
|
|
43
|
+
-->
|
|
44
|
+
<!-- Oh oh, I used a Text component (which allows "string | number | null | undefined"), but the property at the path is a boolean -->
|
|
45
|
+
<!-- I get a typescript error that this path and form combination is not allowed
|
|
46
|
+
<FormTextField
|
|
47
|
+
:form="form"
|
|
48
|
+
path="person.isMale"
|
|
49
|
+
/>
|
|
50
|
+
-->
|
|
51
|
+
|
|
52
|
+
<!-- Nested forms in subcomponents -->
|
|
53
|
+
<!-- My FormAddressField handles forms of type { street: string, city: string } -->
|
|
54
|
+
<!-- so we can make a subform for the address property of our main form -->
|
|
55
|
+
<FormPart :form="form" path="person.address" #="{ subform }">
|
|
56
|
+
<FormAddressField :form="subform" />
|
|
57
|
+
</FormPart>
|
|
58
|
+
|
|
59
|
+
<div v-for="(_, index) in unref(form.data).person.hobbies" :key="index" class="ml-4">
|
|
60
|
+
<!-- Note the path format of an array item. You just use the index number in the dot-path -->
|
|
61
|
+
<FormTextField
|
|
62
|
+
:form="form"
|
|
63
|
+
:path="`person.hobbies.${index}`"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<button type="button" @click="toggleComment">
|
|
68
|
+
Toggle Comment
|
|
69
|
+
</button>
|
|
70
|
+
|
|
71
|
+
<!-- If you need other data, use the data ref of the form object; don't use getField here -->
|
|
72
|
+
<!-- Alternatively, in this case you could use the variable "isCommentEnabled" that was defined in the script setup -->
|
|
73
|
+
<FormTextField
|
|
74
|
+
v-if="unref(form.data).commentEnabled"
|
|
75
|
+
:form="form"
|
|
76
|
+
path="comment"
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<!-- This is a one-off special thing; I don't really want to make a FormInput out of it -->
|
|
80
|
+
<!-- I can use the Field component, or I could use form.getField in the script setup -->
|
|
81
|
+
<Field v-slot="{ data, setData, errors }" path="person.parent" :form="form">
|
|
82
|
+
<div>
|
|
83
|
+
Father: {{ data.fatherName }}
|
|
84
|
+
</div>
|
|
85
|
+
<div>
|
|
86
|
+
Mother: {{ data.motherName }}
|
|
87
|
+
</div>
|
|
88
|
+
<!-- the "errors" will be an array of strings of the errors corresponding to the property with the given path (here "person.parent") -->
|
|
89
|
+
<InputError :errors="errors" />
|
|
90
|
+
<button type="button" @click="() => setData({ fatherName: 'New Father', motherName: 'New Mother' })">
|
|
91
|
+
Change Parent Names
|
|
92
|
+
</button>
|
|
93
|
+
</Field>
|
|
94
|
+
|
|
95
|
+
<button type="submit">
|
|
96
|
+
Submit Form
|
|
97
|
+
</button>
|
|
98
|
+
|
|
99
|
+
</form>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script setup lang="ts">
|
|
103
|
+
import { Field, useForm, FormPart } from '@teamnovu/kit-vue-forms';
|
|
104
|
+
import { z } from 'zod';
|
|
105
|
+
import { unref } from 'vue';
|
|
106
|
+
import FormTextField from '#components/utils/form/formInput/FormTextField.vue';
|
|
107
|
+
import FormAddressField from '#/FormAddressField.vue';
|
|
108
|
+
import InputError from '#components/utils/form/InputError.vue';
|
|
109
|
+
|
|
110
|
+
const form = useForm<{ // this type might be inferred automatically from the initialData; but here, it would not work, because lastName is not defined in the initialData
|
|
111
|
+
person: {
|
|
112
|
+
firstName: string
|
|
113
|
+
lastName?: string | undefined
|
|
114
|
+
address: {
|
|
115
|
+
street: string
|
|
116
|
+
city: string
|
|
117
|
+
}
|
|
118
|
+
hobbies: string[]
|
|
119
|
+
parent: {
|
|
120
|
+
fatherName: string
|
|
121
|
+
motherName: string
|
|
122
|
+
}
|
|
123
|
+
isMale: boolean
|
|
124
|
+
}
|
|
125
|
+
commentEnabled?: boolean
|
|
126
|
+
comment: string
|
|
127
|
+
}>({
|
|
128
|
+
initialData: {
|
|
129
|
+
person: {
|
|
130
|
+
firstName: '',
|
|
131
|
+
// I don't need to define an initial value for lastName here, because it's optional; the form will still handle it correctly
|
|
132
|
+
address: {
|
|
133
|
+
street: '',
|
|
134
|
+
city: '',
|
|
135
|
+
},
|
|
136
|
+
hobbies: ['cooking', 'coding'],
|
|
137
|
+
parent: {
|
|
138
|
+
fatherName: 'Hans Müller',
|
|
139
|
+
motherName: 'Hannah Schmidt',
|
|
140
|
+
},
|
|
141
|
+
isMale: false
|
|
142
|
+
},
|
|
143
|
+
commentEnabled: false,
|
|
144
|
+
comment: '',
|
|
145
|
+
},
|
|
146
|
+
// optional zod schema for validation; might also just define part of the schema if other properties should be ignored
|
|
147
|
+
schema: z.object({
|
|
148
|
+
person: z.object({
|
|
149
|
+
hobbies: z.array(z.string().min(1, 'Hobby cannot be empty')),
|
|
150
|
+
}),
|
|
151
|
+
}),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const sendToBackend = async (data) => {
|
|
155
|
+
// send data object to backend
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const submit = async () => {
|
|
159
|
+
// validate the form
|
|
160
|
+
// if the zod schema is not satisfied, isValid will be false and the errors will be set on the corresponding fields
|
|
161
|
+
// if your TextInput component handles errors correctly, it should be directly visible
|
|
162
|
+
const { isValid } = await form.validateForm();
|
|
163
|
+
|
|
164
|
+
// only submit if the form is valid
|
|
165
|
+
if (isValid) {
|
|
166
|
+
await sendToBackend(unref(form.data));
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// To programmatically change form values, use form.getField to get a ref and a setter function
|
|
171
|
+
// never modify form.data directly
|
|
172
|
+
const { data: isCommentEnabled, setData: setCommentEnabled } = form.getField('commentEnabled');
|
|
173
|
+
const toggleComment = () => {
|
|
174
|
+
setCommentEnabled(!unref(isCommentEnabled));
|
|
175
|
+
};
|
|
176
|
+
</script>
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
An example of the `FormAddressField` that was used above would be:
|
|
181
|
+
```vue
|
|
182
|
+
<!-- FormAddressField.vue -->
|
|
183
|
+
<template>
|
|
184
|
+
<div>
|
|
185
|
+
<FormTextField
|
|
186
|
+
:form="form"
|
|
187
|
+
path="street"
|
|
188
|
+
label="Street Address"
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
<FormTextField
|
|
192
|
+
:form="form"
|
|
193
|
+
path="city"
|
|
194
|
+
label="City"
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
198
|
+
|
|
199
|
+
<script setup lang="ts">
|
|
200
|
+
import type { Form } from '@teamnovu/kit-vue-forms';
|
|
201
|
+
import FormTextField from '#components/utils/form/formInput/FormTextField.vue';
|
|
202
|
+
|
|
203
|
+
interface Props {
|
|
204
|
+
form: Form<{
|
|
205
|
+
street: string
|
|
206
|
+
city: string
|
|
207
|
+
}>
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
defineProps<Props>();
|
|
211
|
+
</script>
|
|
212
|
+
```
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @teamnovu/kit-vue-forms
|
|
2
|
+
|
|
3
|
+
A library for data and error handling of forms, including validation with zod schemas.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @teamnovu/kit-vue-forms
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
This package provides composables and some data components to simplify the data management of forms and associated errors.
|
|
13
|
+
It was designed as a replacement of Vee-Validate with more flexibility and full type safety. We don't use a provided and injected form object,
|
|
14
|
+
but pass it down as a prop to guarantee type safety.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
1. Wrap your plain input components into `FormFieldWrapper`, see [FormInput Example](./FormInput-example.md).
|
|
19
|
+
2. Inside your parent component where you want to use a form, use the composable `useForm` to create a form object.
|
|
20
|
+
This object contains the form data, errors, and methods to define
|
|
21
|
+
fields, validate the form, reset it, and create subforms for nested objects or arrays, see [here](./reference#composable-useform).
|
|
22
|
+
3. Pass this form object to any form input component together with a path to the property of the form data that this input
|
|
23
|
+
should manage. You can also make subforms, pass the form down to child components, etc. See the examples for inspiration.
|
|
24
|
+
4. See Reference documentation for all types and methods: [here](./reference.md).
|
|
25
|
+
|
|
26
|
+
An example of the most common usages can be found [here](./example.md).
|
package/docs/info.json
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Reference
|
|
2
|
+
## Composable `useForm`
|
|
3
|
+
This is the main composable which creates the form. It has the following signature:
|
|
4
|
+
```typescript
|
|
5
|
+
function useForm<T extends object>(options: {
|
|
6
|
+
// the initial data of the form
|
|
7
|
+
// reactive changes to this object will propagate to the form data
|
|
8
|
+
initialData: MaybeRefOrGetter<T>
|
|
9
|
+
// an ErrorBag object or ref of an ErrorBag object with external errors
|
|
10
|
+
// this is used e.g. for server side validation errors
|
|
11
|
+
// these errors will be merged with the internal errors of the form based on validationFn below and/or the zod schema
|
|
12
|
+
errors?: MaybeRef<ErrorBag | undefined>
|
|
13
|
+
// a zod schema of the form data
|
|
14
|
+
// this is validated based on the validationStrategy or by manually triggering validateForm on the form object
|
|
15
|
+
schema?: MaybeRef<z.ZodType>
|
|
16
|
+
// a custom validation function which is called with the current form data
|
|
17
|
+
// this is additional to the schema for custom validations
|
|
18
|
+
validateFn?: MaybeRef<ValidationFunction<T>>
|
|
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
|
+
keepValuesOnUnmount?: MaybeRef<boolean>
|
|
22
|
+
// when validation should be done (on touch, pre submit, etc.)
|
|
23
|
+
// !! currently not implemented !!
|
|
24
|
+
validationStrategy?: MaybeRef<ValidationStrategy>
|
|
25
|
+
}): Form<T>
|
|
26
|
+
```
|
|
27
|
+
This composable returns a `Form<T>` object.
|
|
28
|
+
|
|
29
|
+
## Type `Form<T>`
|
|
30
|
+
Here, `T` is the type of the form data, which is inferred from the `initialData` property of the options object passed to `useForm`.
|
|
31
|
+
You can also explicitly provide a type argument to `useForm<T>` if needed (useful if the initial data is an empty object `{}`).
|
|
32
|
+
|
|
33
|
+
This object has the following properties and methods:
|
|
34
|
+
```typescript
|
|
35
|
+
interface Form<T extends object> {
|
|
36
|
+
// the current working data of the form
|
|
37
|
+
// this might differ from initialData if the user has changed some values
|
|
38
|
+
data: Ref<T>
|
|
39
|
+
// the initial data of the form as passed to useForm
|
|
40
|
+
initialData: Readonly<Ref<T>>
|
|
41
|
+
|
|
42
|
+
// all fields of the form that are currently managed
|
|
43
|
+
fields: Ref<FieldsTuple<T>>
|
|
44
|
+
|
|
45
|
+
// defines a field such that it will be managed by the form
|
|
46
|
+
// without this, the form will not track changes, touched state or validation for this field
|
|
47
|
+
// use this like a composable
|
|
48
|
+
defineField: <P extends Paths<T>>(options: DefineFieldOptions<PickProps<T, P>, P>) => FormField<PickProps<T, P>, P>
|
|
49
|
+
// gets an already defined field by its path
|
|
50
|
+
// note: if the field was not yet defined, it will be defined automatically
|
|
51
|
+
// the output is a reactive object containing the data, errors and states as well as some methods
|
|
52
|
+
// use this like a composable
|
|
53
|
+
getField: <P extends Paths<T>>(path: P) => FormField<PickProps<T, P>, P>
|
|
54
|
+
|
|
55
|
+
// true if the form has been modified from its initial state
|
|
56
|
+
isDirty: Ref<boolean>
|
|
57
|
+
// true if any field of the form has been touched (i.e. onBlur was called on any field)
|
|
58
|
+
isTouched: Ref<boolean>
|
|
59
|
+
// true if the form data is valid based on the schema and/or the validateFn
|
|
60
|
+
isValid: Ref<boolean>
|
|
61
|
+
// true if the form has been validated at least once
|
|
62
|
+
isValidated: Ref<boolean>
|
|
63
|
+
// the ErrorBag object containing all errors of the form
|
|
64
|
+
// this is a merge of internal errors based on schema/validateFn and external errors passed to useForm
|
|
65
|
+
// the errors are structured based on the paths of the form fields
|
|
66
|
+
errors: Ref<ErrorBag>
|
|
67
|
+
|
|
68
|
+
// defines a custom validator for the form
|
|
69
|
+
// with this, a subcomponent might add a validator function and/or schema to the form
|
|
70
|
+
// without needing access to the initial useForm call
|
|
71
|
+
defineValidator: <TData extends T>(options: ValidatorOptions<TData> | Ref<Validator<TData>>) => Ref<Validator<TData> | undefined>
|
|
72
|
+
|
|
73
|
+
// resets the form data and errors, as well as the dirty, touched etc. state of all fields
|
|
74
|
+
reset: () => void
|
|
75
|
+
// manually triggers validation of the form data based on schema and/or validateFn
|
|
76
|
+
validateForm: () => Promise<ValidationResult>
|
|
77
|
+
|
|
78
|
+
// creates a subform for a nested object or array property of the form data
|
|
79
|
+
// the subform is again a Form<T> object, where T is the type of the nested property
|
|
80
|
+
// it will contain all errors that have paths starting with the path of the subform
|
|
81
|
+
// changes to the subform data will propagate to the main form data and vice versa
|
|
82
|
+
getSubForm: <P extends EntityPaths<T>>(
|
|
83
|
+
path: P,
|
|
84
|
+
options?: SubformOptions<PickEntity<T, P>>,
|
|
85
|
+
) => Form<PickEntity<T, P>>
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Type `ErrorBag`
|
|
90
|
+
The errors in the form are structured in an `ErrorBag` object. Most errors are tied to properties in the form. However,
|
|
91
|
+
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:
|
|
92
|
+
```typescript
|
|
93
|
+
interface ErrorBag {
|
|
94
|
+
// an array of general error messages not tied to a specific property
|
|
95
|
+
general: string[] | undefined
|
|
96
|
+
// a record of property paths to arrays of error messages
|
|
97
|
+
// nested properties are dot-separated and array indices are just numbers, e.g. "person.address.street" or "person.hobbies.0.name"
|
|
98
|
+
// for subforms the errors will be all errors that start with the path of the subform
|
|
99
|
+
propertyErrors: Record<string, ValidationErrorMessage[] | undefined>
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Component `FormPart`
|
|
104
|
+
A component to define a subform part for a nested object or array property of the form data. This corresponds to the
|
|
105
|
+
`getSubForm` method of the `Form<T>` object, but in component form to be easily used in the template. An example usage is as follows:
|
|
106
|
+
```vue
|
|
107
|
+
<template>
|
|
108
|
+
<FormPart :form="form" path="person.address" #="{ subform }">
|
|
109
|
+
<MyAdddressComponent :form="subform" />
|
|
110
|
+
</FormPart>
|
|
111
|
+
</template>
|
|
112
|
+
```
|
|
113
|
+
Here, the `subform` will be a `Form` object with the type of the `address` property of the `person` object of the main form data.
|
|
114
|
+
Note that the errors of the subform will only contain the errors that start with the path of the subform.
|
|
115
|
+
|
|
116
|
+
## Component `Field`
|
|
117
|
+
This is a helper component to access form fields. It corresponds to the `defineField` method of the `Form<T>` object,
|
|
118
|
+
but in component form to be easily used in the template.
|
|
119
|
+
|
|
120
|
+
An example usage is as follows:
|
|
121
|
+
```vue
|
|
122
|
+
<template>
|
|
123
|
+
<Field :form="form" path="firstName" #="{ data, setData, onBlur, errors }">
|
|
124
|
+
<input :value="data" @input="setData" @blur="onBlur" />
|
|
125
|
+
<div v-if="errors.length > 0">
|
|
126
|
+
<span v-for="error in errors" :key="error">{{ error }}</span>
|
|
127
|
+
</div>
|
|
128
|
+
</Field>
|
|
129
|
+
</template>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Note: it is recommended to use the `FormFieldWrapper` component and create a reusable form input component instead.
|
|
133
|
+
|
|
134
|
+
## Component `FormFieldWrapper`
|
|
135
|
+
This component is a helper component to easily create form input components based on plain input components.
|
|
136
|
+
See the [FormInput Example](./FormInput-example.md) for details and an example implementation of a form input component.
|
package/package.json
CHANGED
|
@@ -36,6 +36,7 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
|
36
36
|
|
|
37
37
|
const reset = () => {
|
|
38
38
|
data.value = cloneRefValue(initialData)
|
|
39
|
+
validationState.reset()
|
|
39
40
|
fieldRegistry.fields.value.forEach(
|
|
40
41
|
(field: AnyField<T>) => field.reset(),
|
|
41
42
|
)
|
|
@@ -217,11 +217,17 @@ export function useValidation<T extends FormDataDefault>(
|
|
|
217
217
|
|
|
218
218
|
const isValid = computed(() => !hasErrors(validationState.errors))
|
|
219
219
|
|
|
220
|
+
const reset = () => {
|
|
221
|
+
validationState.isValidated = false
|
|
222
|
+
validationState.errors = unref(options.errors) ?? SuccessValidationResult.errors
|
|
223
|
+
}
|
|
224
|
+
|
|
220
225
|
return {
|
|
221
226
|
...toRefs(validationState),
|
|
222
227
|
validateForm,
|
|
223
228
|
defineValidator,
|
|
224
229
|
isValid,
|
|
230
|
+
reset,
|
|
225
231
|
}
|
|
226
232
|
}
|
|
227
233
|
|
package/src/utils/path.ts
CHANGED
|
@@ -19,9 +19,6 @@ export function getNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPat
|
|
|
19
19
|
|
|
20
20
|
export function setNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>, value: PickProps<T, K>): void {
|
|
21
21
|
const keys = Array.isArray(path) ? path : splitPath(path)
|
|
22
|
-
if (keys.length === 0) {
|
|
23
|
-
throw new Error('Path cannot be empty')
|
|
24
|
-
}
|
|
25
22
|
|
|
26
23
|
const lastKey = keys.at(-1)!
|
|
27
24
|
const target = keys
|
|
@@ -32,7 +29,7 @@ export function setNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPat
|
|
|
32
29
|
obj as Record<string, any>,
|
|
33
30
|
)
|
|
34
31
|
|
|
35
|
-
target[lastKey] = value
|
|
32
|
+
target[lastKey] = lastKey ? value : target
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
export const getLens = <T, K extends Paths<T>>(data: MaybeRef<T>, key: MaybeRef<K | SplitPath<K>>) => {
|
|
@@ -238,4 +238,45 @@ describe('useValidation', () => {
|
|
|
238
238
|
|
|
239
239
|
expect(field.errors.value).toEqual(['Name error'])
|
|
240
240
|
})
|
|
241
|
+
|
|
242
|
+
it('should initialize properly when using a zod schema', async () => {
|
|
243
|
+
const schema = z.object({
|
|
244
|
+
name: z.string().min(2),
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
const formState = { data: { name: 'A' } }
|
|
248
|
+
const validation = useValidation(formState, {
|
|
249
|
+
schema,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
expect(validation.errors.value).toEqual(SuccessValidationResult.errors)
|
|
253
|
+
expect(validation.isValid.value).toEqual(true)
|
|
254
|
+
|
|
255
|
+
const result = await validation.validateForm()
|
|
256
|
+
|
|
257
|
+
expect(result.isValid).toBe(false)
|
|
258
|
+
expect(result.errors.propertyErrors.name).toEqual(['Too small: expected string to have >=2 characters']) // From schema validation
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('should reset the form errors', async () => {
|
|
262
|
+
const schema = z.object({
|
|
263
|
+
name: z.string().min(2),
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const formState = { data: { name: 'A' } }
|
|
267
|
+
const validation = useValidation(formState, {
|
|
268
|
+
schema,
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
await validation.validateForm()
|
|
272
|
+
|
|
273
|
+
expect(validation.isValidated.value).toBe(true)
|
|
274
|
+
expect(validation.isValid.value).toBe(false)
|
|
275
|
+
expect(validation.errors.value.propertyErrors.name).toEqual(['Too small: expected string to have >=2 characters']) // From schema validation
|
|
276
|
+
|
|
277
|
+
validation.reset()
|
|
278
|
+
|
|
279
|
+
expect(validation.isValidated.value).toBe(false)
|
|
280
|
+
expect(validation.errors.value).toEqual(SuccessValidationResult.errors)
|
|
281
|
+
})
|
|
241
282
|
})
|