@teamnovu/kit-vue-forms 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composables/useField.d.ts +2 -3
- package/dist/composables/useFieldRegistry.d.ts +2 -1
- package/dist/{index.mjs → index.js} +151 -142
- package/package.json +6 -4
- package/src/components/Field.vue +3 -1
- package/src/composables/useField.ts +3 -24
- package/src/composables/useFieldRegistry.ts +11 -1
- package/src/composables/useForm.ts +8 -3
- package/tests/integration.test.ts +1 -1
- package/tests/nestedPath.test.ts +3 -5
- package/tests/useForm.test.ts +25 -7
- package/tests/useValidation.test.ts +25 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { MaybeRef, MaybeRefOrGetter } from 'vue';
|
|
1
|
+
import { MaybeRef, MaybeRefOrGetter, WritableComputedRef } from 'vue';
|
|
2
2
|
import { FormField } from '../types/form';
|
|
3
3
|
import { ValidationErrors } from '../types/validation';
|
|
4
4
|
export interface UseFieldOptions<T, K extends string> {
|
|
5
5
|
value?: MaybeRef<T>;
|
|
6
6
|
initialValue?: MaybeRefOrGetter<Readonly<T>>;
|
|
7
7
|
path: K;
|
|
8
|
-
errors?:
|
|
8
|
+
errors?: WritableComputedRef<ValidationErrors>;
|
|
9
9
|
}
|
|
10
|
-
export declare function getEmptyField(): FormField<unknown, string>;
|
|
11
10
|
export declare function useField<T, K extends string>(options: UseFieldOptions<T, K>): FormField<T, K>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FieldsTuple, FormDataDefault, FormField } from '../types/form';
|
|
2
2
|
import { Paths, PickProps } from '../types/util';
|
|
3
3
|
import { UseFieldOptions } from './useField';
|
|
4
|
+
import { ValidationState } from './useValidation';
|
|
4
5
|
type FieldRegistryCache<T> = Record<Paths<T>, FormField<any, string>>;
|
|
5
6
|
export type ResolvedFormField<T, K extends Paths<T>> = FormField<PickProps<T, K>, K>;
|
|
6
7
|
export type DefineFieldOptions<F, K extends string> = Pick<UseFieldOptions<F, K>, 'path'>;
|
|
@@ -8,7 +9,7 @@ interface FormState<T extends FormDataDefault, TIn extends FormDataDefault = T>
|
|
|
8
9
|
data: T;
|
|
9
10
|
initialData: TIn;
|
|
10
11
|
}
|
|
11
|
-
export declare function useFieldRegistry<T extends FormDataDefault>(formState: FormState<T>): {
|
|
12
|
+
export declare function useFieldRegistry<T extends FormDataDefault>(formState: FormState<T>, validationState: ValidationState<T>): {
|
|
12
13
|
fields: FieldRegistryCache<T>;
|
|
13
14
|
getField: <K extends Paths<T>>(path: K) => ResolvedFormField<T, K>;
|
|
14
15
|
getFields: <TData extends T>() => FieldsTuple<TData>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
var x = Object.defineProperty;
|
|
2
2
|
var K = (e, r, t) => r in e ? x(e, r, { enumerable: !0, configurable: !0, writable: !0, value: t }) : e[r] = t;
|
|
3
|
-
var
|
|
4
|
-
import { toValue as W, toRaw as I, computed as
|
|
3
|
+
var D = (e, r, t) => K(e, typeof r != "symbol" ? r + "" : r, t);
|
|
4
|
+
import { toValue as W, toRaw as I, computed as u, unref as d, reactive as m, toRefs as O, toRef as S, ref as A, watch as E, isRef as _, getCurrentScope as J, onBeforeUnmount as T, defineComponent as B, renderSlot as G, normalizeProps as L, guardReactiveProps as U } from "vue";
|
|
5
5
|
import "zod";
|
|
6
|
+
import "@vueuse/core";
|
|
6
7
|
function y(e) {
|
|
7
8
|
const r = W(e), t = I(r);
|
|
8
9
|
return structuredClone(t);
|
|
@@ -20,22 +21,22 @@ function Z(e, r, t) {
|
|
|
20
21
|
const s = Array.isArray(r) ? r : N(r);
|
|
21
22
|
if (s.length === 0)
|
|
22
23
|
throw new Error("Path cannot be empty");
|
|
23
|
-
const a = s.at(-1),
|
|
24
|
-
(
|
|
24
|
+
const a = s.at(-1), o = s.slice(0, -1).reduce(
|
|
25
|
+
(v, c) => v[c],
|
|
25
26
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
27
|
e
|
|
27
28
|
);
|
|
28
|
-
|
|
29
|
+
o[a] = t;
|
|
29
30
|
}
|
|
30
|
-
const z = (e, r) =>
|
|
31
|
+
const z = (e, r) => u({
|
|
31
32
|
get() {
|
|
32
|
-
return w(
|
|
33
|
+
return w(d(e), d(r));
|
|
33
34
|
},
|
|
34
35
|
set(t) {
|
|
35
|
-
Z(
|
|
36
|
+
Z(d(e), d(r), t);
|
|
36
37
|
}
|
|
37
38
|
});
|
|
38
|
-
function
|
|
39
|
+
function F(e, r) {
|
|
39
40
|
return !e && !r ? "" : !e && r ? r : !r && e ? e : `${e}.${r}`;
|
|
40
41
|
}
|
|
41
42
|
function k(e, r) {
|
|
@@ -43,7 +44,7 @@ function k(e, r) {
|
|
|
43
44
|
return e;
|
|
44
45
|
const t = `${r}.`, s = Object.fromEntries(
|
|
45
46
|
Object.entries(e.propertyErrors).filter(([a]) => a.startsWith(t)).map(
|
|
46
|
-
([a,
|
|
47
|
+
([a, o]) => [a.slice(t.length), o]
|
|
47
48
|
)
|
|
48
49
|
);
|
|
49
50
|
return {
|
|
@@ -56,65 +57,69 @@ function q(e) {
|
|
|
56
57
|
const r = m({
|
|
57
58
|
value: e.value,
|
|
58
59
|
path: e.path,
|
|
59
|
-
initialValue:
|
|
60
|
-
errors:
|
|
60
|
+
initialValue: u(() => Object.freeze(y(e.initialValue))),
|
|
61
|
+
errors: e.errors,
|
|
61
62
|
touched: !1
|
|
62
|
-
})
|
|
63
|
-
F(() => u(e.errors), (f) => {
|
|
64
|
-
r.errors = f || [];
|
|
65
|
-
});
|
|
66
|
-
const t = d(() => JSON.stringify(r.value) !== JSON.stringify(r.initialValue)), s = (f) => {
|
|
63
|
+
}), t = u(() => JSON.stringify(r.value) !== JSON.stringify(r.initialValue)), s = (f) => {
|
|
67
64
|
r.value = f;
|
|
68
65
|
}, a = () => {
|
|
69
66
|
r.touched = !0;
|
|
70
|
-
}, n = () => {
|
|
71
67
|
}, o = () => {
|
|
68
|
+
}, v = () => {
|
|
72
69
|
r.value = y(r.initialValue), r.touched = !1, r.errors = [];
|
|
73
|
-
},
|
|
70
|
+
}, c = (f) => {
|
|
74
71
|
r.errors = f;
|
|
75
|
-
},
|
|
72
|
+
}, i = () => {
|
|
76
73
|
r.errors = [];
|
|
77
|
-
},
|
|
74
|
+
}, l = O(r);
|
|
78
75
|
return {
|
|
79
|
-
data:
|
|
80
|
-
path:
|
|
81
|
-
initialValue:
|
|
82
|
-
errors:
|
|
83
|
-
touched:
|
|
76
|
+
data: l.value,
|
|
77
|
+
path: l.path,
|
|
78
|
+
initialValue: l.initialValue,
|
|
79
|
+
errors: l.errors,
|
|
80
|
+
touched: l.touched,
|
|
84
81
|
dirty: t,
|
|
85
82
|
setData: s,
|
|
86
83
|
onBlur: a,
|
|
87
|
-
onFocus:
|
|
88
|
-
reset:
|
|
89
|
-
setErrors:
|
|
90
|
-
clearErrors:
|
|
84
|
+
onFocus: o,
|
|
85
|
+
reset: v,
|
|
86
|
+
setErrors: c,
|
|
87
|
+
clearErrors: i
|
|
91
88
|
};
|
|
92
89
|
}
|
|
93
|
-
function H(e) {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
},
|
|
98
|
-
if (!
|
|
99
|
-
const
|
|
100
|
-
path:
|
|
101
|
-
value: z(
|
|
102
|
-
initialValue:
|
|
90
|
+
function H(e, r) {
|
|
91
|
+
const t = {}, s = (c) => {
|
|
92
|
+
const i = d(c.path);
|
|
93
|
+
t[i] = c;
|
|
94
|
+
}, a = (c) => {
|
|
95
|
+
if (!t[c]) {
|
|
96
|
+
const i = q({
|
|
97
|
+
path: c,
|
|
98
|
+
value: z(S(e, "data"), c),
|
|
99
|
+
initialValue: u(() => w(e.initialData, c)),
|
|
100
|
+
errors: u({
|
|
101
|
+
get() {
|
|
102
|
+
return r.errors.value.propertyErrors[c] || [];
|
|
103
|
+
},
|
|
104
|
+
set(l) {
|
|
105
|
+
r.errors.value.propertyErrors[c] = l;
|
|
106
|
+
}
|
|
107
|
+
})
|
|
103
108
|
});
|
|
104
|
-
return
|
|
109
|
+
return s(i), i;
|
|
105
110
|
}
|
|
106
|
-
return
|
|
111
|
+
return t[c];
|
|
107
112
|
};
|
|
108
113
|
return {
|
|
109
|
-
fields:
|
|
110
|
-
getField:
|
|
111
|
-
getFields: () => Object.values(
|
|
112
|
-
registerField:
|
|
113
|
-
defineField: (
|
|
114
|
+
fields: t,
|
|
115
|
+
getField: a,
|
|
116
|
+
getFields: () => Object.values(t),
|
|
117
|
+
registerField: s,
|
|
118
|
+
defineField: (c) => a(c.path)
|
|
114
119
|
};
|
|
115
120
|
}
|
|
116
121
|
function Q(e) {
|
|
117
|
-
const r =
|
|
122
|
+
const r = u(() => e.getFields().some((s) => d(s.dirty))), t = u(() => e.getFields().some((s) => d(s.touched)));
|
|
118
123
|
return {
|
|
119
124
|
isDirty: r,
|
|
120
125
|
isTouched: t
|
|
@@ -140,7 +145,7 @@ function C(...e) {
|
|
|
140
145
|
}
|
|
141
146
|
function Y(...e) {
|
|
142
147
|
return e.map((t) => Object.keys(t)).flat().reduce((t, s) => {
|
|
143
|
-
const a = e.map((
|
|
148
|
+
const a = e.map((o) => o[s]).filter(Boolean);
|
|
144
149
|
return {
|
|
145
150
|
...t,
|
|
146
151
|
[s]: C(...a)
|
|
@@ -169,10 +174,10 @@ function j(e) {
|
|
|
169
174
|
}
|
|
170
175
|
function rr(e) {
|
|
171
176
|
const r = e.issues.filter((s) => s.path.length === 0).map((s) => s.message), t = e.issues.filter((s) => s.path.length > 0).reduce((s, a) => {
|
|
172
|
-
const
|
|
177
|
+
const o = a.path.join(".");
|
|
173
178
|
return {
|
|
174
179
|
...s,
|
|
175
|
-
[
|
|
180
|
+
[o]: [...s[o] ?? [], a.message]
|
|
176
181
|
};
|
|
177
182
|
}, {});
|
|
178
183
|
return {
|
|
@@ -180,7 +185,7 @@ function rr(e) {
|
|
|
180
185
|
propertyErrors: t
|
|
181
186
|
};
|
|
182
187
|
}
|
|
183
|
-
const
|
|
188
|
+
const V = {
|
|
184
189
|
isValid: !0,
|
|
185
190
|
errors: {
|
|
186
191
|
general: [],
|
|
@@ -193,10 +198,10 @@ class er {
|
|
|
193
198
|
}
|
|
194
199
|
async validate(r) {
|
|
195
200
|
if (!this.schema)
|
|
196
|
-
return
|
|
201
|
+
return V;
|
|
197
202
|
const t = await this.schema.safeParseAsync(r);
|
|
198
203
|
if (t.success)
|
|
199
|
-
return
|
|
204
|
+
return V;
|
|
200
205
|
const s = rr(t.error);
|
|
201
206
|
return {
|
|
202
207
|
isValid: !1,
|
|
@@ -213,10 +218,10 @@ class tr {
|
|
|
213
218
|
}
|
|
214
219
|
async validate(r) {
|
|
215
220
|
if (!this.validateFn)
|
|
216
|
-
return
|
|
221
|
+
return V;
|
|
217
222
|
try {
|
|
218
223
|
const t = await this.validateFn(r);
|
|
219
|
-
return t.isValid ?
|
|
224
|
+
return t.isValid ? V : t;
|
|
220
225
|
} catch (t) {
|
|
221
226
|
return {
|
|
222
227
|
isValid: !1,
|
|
@@ -230,8 +235,8 @@ class tr {
|
|
|
230
235
|
}
|
|
231
236
|
class sr {
|
|
232
237
|
constructor(r, t) {
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
D(this, "schemaValidator");
|
|
239
|
+
D(this, "functionValidator");
|
|
235
240
|
this.schema = r, this.validateFn = t, this.schemaValidator = new er(this.schema), this.functionValidator = new tr(this.validateFn);
|
|
236
241
|
}
|
|
237
242
|
async validate(r) {
|
|
@@ -246,70 +251,70 @@ class sr {
|
|
|
246
251
|
}
|
|
247
252
|
}
|
|
248
253
|
function b(e) {
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
|
|
254
|
+
return u(() => new sr(
|
|
255
|
+
d(e.schema),
|
|
256
|
+
d(e.validateFn)
|
|
252
257
|
));
|
|
253
258
|
}
|
|
254
259
|
function ar(e, r) {
|
|
255
260
|
const t = m({
|
|
256
261
|
validators: A([b(r)]),
|
|
257
262
|
isValidated: !1,
|
|
258
|
-
errors:
|
|
263
|
+
errors: d(r.errors) ?? V.errors
|
|
259
264
|
});
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
}, { immediate: !0 }),
|
|
265
|
+
E(() => d(r.errors), async () => {
|
|
266
|
+
const i = await a();
|
|
267
|
+
o(i.errors);
|
|
268
|
+
}, { immediate: !0 }), E(
|
|
264
269
|
[() => t.validators],
|
|
265
|
-
async (
|
|
270
|
+
async (i) => {
|
|
266
271
|
if (t.isValidated)
|
|
267
|
-
if (
|
|
268
|
-
const
|
|
269
|
-
t.errors =
|
|
272
|
+
if (i) {
|
|
273
|
+
const l = await a();
|
|
274
|
+
t.errors = l.errors;
|
|
270
275
|
} else
|
|
271
|
-
t.errors =
|
|
276
|
+
t.errors = V.errors;
|
|
272
277
|
},
|
|
273
278
|
{ immediate: !0 }
|
|
274
|
-
),
|
|
275
|
-
t.isValidated &&
|
|
279
|
+
), E(() => e.data, () => {
|
|
280
|
+
t.isValidated && v();
|
|
276
281
|
});
|
|
277
|
-
const s = (
|
|
278
|
-
const
|
|
279
|
-
return t.validators.push(
|
|
282
|
+
const s = (i) => {
|
|
283
|
+
const l = _(i) ? i : b(i);
|
|
284
|
+
return t.validators.push(l), J() && T(() => {
|
|
280
285
|
t.validators = t.validators.filter(
|
|
281
|
-
(f) => f !==
|
|
286
|
+
(f) => f !== l
|
|
282
287
|
);
|
|
283
|
-
}),
|
|
288
|
+
}), l;
|
|
284
289
|
};
|
|
285
290
|
async function a() {
|
|
286
|
-
const
|
|
287
|
-
t.validators.filter((
|
|
288
|
-
),
|
|
289
|
-
let { errors: f } =
|
|
290
|
-
if (!
|
|
291
|
-
const
|
|
292
|
-
f = P(...
|
|
291
|
+
const i = await Promise.all(
|
|
292
|
+
t.validators.filter((p) => d(p) !== void 0).map((p) => d(p).validate(e.data))
|
|
293
|
+
), l = i.every((p) => p.isValid);
|
|
294
|
+
let { errors: f } = V;
|
|
295
|
+
if (!l) {
|
|
296
|
+
const p = i.map((R) => R.errors);
|
|
297
|
+
f = P(...p);
|
|
293
298
|
}
|
|
294
299
|
return {
|
|
295
300
|
errors: f,
|
|
296
|
-
isValid:
|
|
301
|
+
isValid: l
|
|
297
302
|
};
|
|
298
303
|
}
|
|
299
|
-
const
|
|
300
|
-
t.errors = P(
|
|
301
|
-
},
|
|
302
|
-
const
|
|
303
|
-
return
|
|
304
|
-
isValid: !j(
|
|
304
|
+
const o = (i) => {
|
|
305
|
+
t.errors = P(d(r.errors) ?? V.errors, i);
|
|
306
|
+
}, v = async () => {
|
|
307
|
+
const i = await a();
|
|
308
|
+
return o(i.errors), t.isValidated = !0, {
|
|
309
|
+
isValid: !j(i.errors),
|
|
305
310
|
errors: t.errors
|
|
306
311
|
};
|
|
307
|
-
},
|
|
312
|
+
}, c = u(() => !j(t.errors));
|
|
308
313
|
return {
|
|
309
314
|
...O(t),
|
|
310
|
-
validateForm:
|
|
315
|
+
validateForm: v,
|
|
311
316
|
defineValidator: s,
|
|
312
|
-
isValid:
|
|
317
|
+
isValid: c
|
|
313
318
|
};
|
|
314
319
|
}
|
|
315
320
|
class ir {
|
|
@@ -319,16 +324,16 @@ class ir {
|
|
|
319
324
|
async validate(r) {
|
|
320
325
|
const t = w(r, this.path);
|
|
321
326
|
if (!this.validator)
|
|
322
|
-
return
|
|
327
|
+
return V;
|
|
323
328
|
const s = await this.validator.validate(t);
|
|
324
329
|
return {
|
|
325
330
|
isValid: s.isValid,
|
|
326
331
|
errors: {
|
|
327
332
|
general: s.errors.general || [],
|
|
328
333
|
propertyErrors: s.errors.propertyErrors ? Object.fromEntries(
|
|
329
|
-
Object.entries(s.errors.propertyErrors).map(([a,
|
|
330
|
-
|
|
331
|
-
|
|
334
|
+
Object.entries(s.errors.propertyErrors).map(([a, o]) => [
|
|
335
|
+
F(this.path, a),
|
|
336
|
+
o
|
|
332
337
|
])
|
|
333
338
|
) : {}
|
|
334
339
|
}
|
|
@@ -336,49 +341,49 @@ class ir {
|
|
|
336
341
|
}
|
|
337
342
|
}
|
|
338
343
|
function nr(e, r, t) {
|
|
339
|
-
const s = z(e.data, r), a =
|
|
340
|
-
...
|
|
341
|
-
path:
|
|
344
|
+
const s = z(e.data, r), a = u(() => w(e.initialData.value, r)), o = (n) => ({
|
|
345
|
+
...n,
|
|
346
|
+
path: u(() => d(n.path).replace(r + ".", "")),
|
|
342
347
|
setData: (h) => {
|
|
343
|
-
|
|
348
|
+
n.setData(h);
|
|
344
349
|
}
|
|
345
|
-
}),
|
|
346
|
-
const h =
|
|
347
|
-
return g ?
|
|
348
|
-
},
|
|
349
|
-
const h =
|
|
350
|
-
...
|
|
350
|
+
}), v = (n) => {
|
|
351
|
+
const h = F(r, n), g = e.getField(h);
|
|
352
|
+
return g ? o(g) : {};
|
|
353
|
+
}, c = (n) => {
|
|
354
|
+
const h = F(r, n.path), g = e.defineField({
|
|
355
|
+
...n,
|
|
351
356
|
path: h
|
|
352
357
|
});
|
|
353
|
-
return
|
|
354
|
-
},
|
|
355
|
-
const h =
|
|
358
|
+
return o(g);
|
|
359
|
+
}, i = () => e.getFields().filter((n) => {
|
|
360
|
+
const h = n.path.value;
|
|
356
361
|
return h.startsWith(r + ".") || h === r;
|
|
357
|
-
}).map((
|
|
358
|
-
const h =
|
|
362
|
+
}).map((n) => o(n)), l = () => e.getFields().filter((n) => {
|
|
363
|
+
const h = n.path.value;
|
|
359
364
|
return h.startsWith(r + ".") || h === r;
|
|
360
|
-
}), f =
|
|
365
|
+
}), f = u(() => l().some((n) => n.dirty.value)), p = u(() => l().some((n) => n.touched.value)), R = u(() => e.isValid.value), M = u(() => e.isValidated.value), $ = u(() => k(d(e.errors), r));
|
|
361
366
|
return {
|
|
362
367
|
data: s,
|
|
363
368
|
initialData: a,
|
|
364
|
-
defineField:
|
|
365
|
-
getField:
|
|
366
|
-
getFields:
|
|
369
|
+
defineField: c,
|
|
370
|
+
getField: v,
|
|
371
|
+
getFields: i,
|
|
367
372
|
isDirty: f,
|
|
368
|
-
isTouched:
|
|
373
|
+
isTouched: p,
|
|
369
374
|
isValid: R,
|
|
370
375
|
isValidated: M,
|
|
371
376
|
errors: $,
|
|
372
|
-
defineValidator: (
|
|
373
|
-
const h = _(
|
|
374
|
-
() => new ir(r,
|
|
377
|
+
defineValidator: (n) => {
|
|
378
|
+
const h = _(n) ? n : b(n), g = u(
|
|
379
|
+
() => new ir(r, d(h))
|
|
375
380
|
);
|
|
376
381
|
return e.defineValidator(g), h;
|
|
377
382
|
},
|
|
378
|
-
reset: () =>
|
|
383
|
+
reset: () => l().forEach((n) => n.reset()),
|
|
379
384
|
validateForm: () => e.validateForm(),
|
|
380
|
-
getSubForm: (
|
|
381
|
-
const g =
|
|
385
|
+
getSubForm: (n, h) => {
|
|
386
|
+
const g = F(r, n);
|
|
382
387
|
return e.getSubForm(
|
|
383
388
|
g,
|
|
384
389
|
h
|
|
@@ -386,28 +391,32 @@ function nr(e, r, t) {
|
|
|
386
391
|
}
|
|
387
392
|
};
|
|
388
393
|
}
|
|
389
|
-
function
|
|
390
|
-
const r =
|
|
394
|
+
function pr(e) {
|
|
395
|
+
const r = u(() => Object.freeze(y(e.initialData))), t = A(y(r)), s = m({
|
|
391
396
|
initialData: r,
|
|
392
397
|
data: t
|
|
393
|
-
})
|
|
394
|
-
|
|
398
|
+
});
|
|
399
|
+
E(r, (f) => {
|
|
400
|
+
s.data = y(f);
|
|
401
|
+
});
|
|
402
|
+
const a = ar(s, e), o = H(s, a), v = Q(o), c = () => {
|
|
403
|
+
t.value = y(r), o.getFields().forEach(
|
|
395
404
|
(f) => f.reset()
|
|
396
405
|
);
|
|
397
406
|
};
|
|
398
|
-
function
|
|
399
|
-
return nr(
|
|
407
|
+
function i(f, p) {
|
|
408
|
+
return nr(l, f);
|
|
400
409
|
}
|
|
401
|
-
const
|
|
402
|
-
...a,
|
|
403
|
-
...n,
|
|
410
|
+
const l = {
|
|
404
411
|
...o,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
412
|
+
...a,
|
|
413
|
+
...v,
|
|
414
|
+
reset: c,
|
|
415
|
+
getSubForm: i,
|
|
416
|
+
initialData: S(s, "initialData"),
|
|
417
|
+
data: S(s, "data")
|
|
409
418
|
};
|
|
410
|
-
return
|
|
419
|
+
return l;
|
|
411
420
|
}
|
|
412
421
|
const Vr = /* @__PURE__ */ B({
|
|
413
422
|
__name: "Field",
|
|
@@ -421,11 +430,11 @@ const Vr = /* @__PURE__ */ B({
|
|
|
421
430
|
setup(e) {
|
|
422
431
|
const r = e, t = r.form.defineField({
|
|
423
432
|
path: r.path
|
|
424
|
-
});
|
|
425
|
-
return (
|
|
433
|
+
}), s = m(t);
|
|
434
|
+
return (a, o) => G(a.$slots, "default", L(U(s)));
|
|
426
435
|
}
|
|
427
436
|
});
|
|
428
437
|
export {
|
|
429
438
|
Vr as Field,
|
|
430
|
-
|
|
439
|
+
pr as useForm
|
|
431
440
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamnovu/kit-vue-forms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
6
7
|
"exports": {
|
|
7
8
|
".": {
|
|
8
9
|
"types": "./dist/index.d.ts",
|
|
@@ -21,10 +22,11 @@
|
|
|
21
22
|
"vue": "^3.0.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"vue": "^
|
|
25
|
-
"vitest": "^2.0.0",
|
|
25
|
+
"@vitejs/plugin-vue": "^6.0.1",
|
|
26
26
|
"@vitest/ui": "^2.0.0",
|
|
27
|
-
"happy-dom": "^12.0.0"
|
|
27
|
+
"happy-dom": "^12.0.0",
|
|
28
|
+
"vitest": "^2.0.0",
|
|
29
|
+
"vue": "^3.5.13"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
30
32
|
"@vueuse/core": "^13.5.0",
|
package/src/components/Field.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<slot v-bind="
|
|
2
|
+
<slot v-bind="slotData" />
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script
|
|
@@ -20,4 +20,6 @@ const props = defineProps<FieldProps<TData, TPath>>()
|
|
|
20
20
|
const field = props.form.defineField({
|
|
21
21
|
path: props.path,
|
|
22
22
|
})
|
|
23
|
+
|
|
24
|
+
const slotData = reactive(field)
|
|
23
25
|
</script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, reactive,
|
|
1
|
+
import { computed, reactive, toRefs, watch, type MaybeRef, type MaybeRefOrGetter, type WritableComputedRef } from 'vue'
|
|
2
2
|
import type { FormField } from '../types/form'
|
|
3
3
|
import type { ValidationErrorMessage, ValidationErrors } from '../types/validation'
|
|
4
4
|
import { cloneRefValue } from '../utils/general'
|
|
@@ -7,24 +7,7 @@ export interface UseFieldOptions<T, K extends string> {
|
|
|
7
7
|
value?: MaybeRef<T>
|
|
8
8
|
initialValue?: MaybeRefOrGetter<Readonly<T>>
|
|
9
9
|
path: K
|
|
10
|
-
errors?:
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function getEmptyField(): FormField<unknown, string> {
|
|
14
|
-
return {
|
|
15
|
-
setData: () => {},
|
|
16
|
-
onBlur: () => {},
|
|
17
|
-
onFocus: () => {},
|
|
18
|
-
reset: () => {},
|
|
19
|
-
setErrors: () => {},
|
|
20
|
-
clearErrors: () => {},
|
|
21
|
-
data: ref(undefined),
|
|
22
|
-
initialValue: ref(undefined),
|
|
23
|
-
path: ref(''),
|
|
24
|
-
errors: ref([]),
|
|
25
|
-
touched: ref(false),
|
|
26
|
-
dirty: computed(() => false),
|
|
27
|
-
}
|
|
10
|
+
errors?: WritableComputedRef<ValidationErrors>
|
|
28
11
|
}
|
|
29
12
|
|
|
30
13
|
export function useField<T, K extends string>(options: UseFieldOptions<T, K>): FormField<T, K> {
|
|
@@ -32,14 +15,10 @@ export function useField<T, K extends string>(options: UseFieldOptions<T, K>): F
|
|
|
32
15
|
value: options.value,
|
|
33
16
|
path: options.path,
|
|
34
17
|
initialValue: computed(() => Object.freeze(cloneRefValue(options.initialValue))),
|
|
35
|
-
errors:
|
|
18
|
+
errors: options.errors,
|
|
36
19
|
touched: false,
|
|
37
20
|
})
|
|
38
21
|
|
|
39
|
-
watch(() => unref(options.errors), (newValue) => {
|
|
40
|
-
state.errors = newValue || []
|
|
41
|
-
})
|
|
42
|
-
|
|
43
22
|
const dirty = computed(() => {
|
|
44
23
|
return JSON.stringify(state.value) !== JSON.stringify(state.initialValue)
|
|
45
24
|
})
|
|
@@ -2,7 +2,8 @@ import { computed, toRef, unref } from 'vue'
|
|
|
2
2
|
import type { FieldsTuple, FormDataDefault, FormField } from '../types/form'
|
|
3
3
|
import type { Paths, PickProps } from '../types/util'
|
|
4
4
|
import { getLens, getNestedValue } from '../utils/path'
|
|
5
|
-
import {
|
|
5
|
+
import { useField, type UseFieldOptions } from './useField'
|
|
6
|
+
import type { ValidationState } from './useValidation'
|
|
6
7
|
|
|
7
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
9
|
type FieldRegistryCache<T> = Record<Paths<T>, FormField<any, string>>
|
|
@@ -18,6 +19,7 @@ interface FormState<T extends FormDataDefault, TIn extends FormDataDefault = T>
|
|
|
18
19
|
|
|
19
20
|
export function useFieldRegistry<T extends FormDataDefault>(
|
|
20
21
|
formState: FormState<T>,
|
|
22
|
+
validationState: ValidationState<T>,
|
|
21
23
|
) {
|
|
22
24
|
const fields = {} as FieldRegistryCache<T>
|
|
23
25
|
|
|
@@ -32,6 +34,14 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
32
34
|
path,
|
|
33
35
|
value: getLens(toRef(formState, 'data'), path),
|
|
34
36
|
initialValue: computed(() => getNestedValue(formState.initialData, path)),
|
|
37
|
+
errors: computed({
|
|
38
|
+
get() {
|
|
39
|
+
return validationState.errors.value.propertyErrors[path] || []
|
|
40
|
+
},
|
|
41
|
+
set(newErrors) {
|
|
42
|
+
validationState.errors.value.propertyErrors[path] = newErrors
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
35
45
|
})
|
|
36
46
|
|
|
37
47
|
registerField(field)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, reactive, ref, toRef, type MaybeRef, type MaybeRefOrGetter, type Ref } from 'vue'
|
|
1
|
+
import { computed, reactive, ref, toRef, watch, type MaybeRef, type MaybeRefOrGetter, type Ref } from 'vue'
|
|
2
2
|
import type { AnyField, Form, FormDataDefault } from '../types/form'
|
|
3
3
|
import type { EntityPaths, PickEntity } from '../types/util'
|
|
4
4
|
import type { ValidationStrategy } from '../types/validation'
|
|
@@ -7,6 +7,7 @@ import { useFieldRegistry } from './useFieldRegistry'
|
|
|
7
7
|
import { useFormState } from './useFormState'
|
|
8
8
|
import { createSubformInterface, type SubformOptions } from './useSubform'
|
|
9
9
|
import { useValidation, type ValidationOptions } from './useValidation'
|
|
10
|
+
import { syncRef } from '@vueuse/core'
|
|
10
11
|
|
|
11
12
|
// TODO @Elias implement validation strategy handling
|
|
12
13
|
|
|
@@ -22,12 +23,16 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
|
22
23
|
const data = ref<T>(cloneRefValue(initialData)) as Ref<T>
|
|
23
24
|
|
|
24
25
|
const state = reactive({
|
|
25
|
-
initialData
|
|
26
|
+
initialData,
|
|
26
27
|
data,
|
|
27
28
|
})
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
watch(initialData, (newValue) => {
|
|
31
|
+
state.data = cloneRefValue(newValue)
|
|
32
|
+
})
|
|
33
|
+
|
|
30
34
|
const validationState = useValidation(state, options)
|
|
35
|
+
const fields = useFieldRegistry(state, validationState)
|
|
31
36
|
const formState = useFormState(fields)
|
|
32
37
|
|
|
33
38
|
const reset = () => {
|
package/tests/nestedPath.test.ts
CHANGED
|
@@ -540,16 +540,14 @@ describe('Nested Path Handling', () => {
|
|
|
540
540
|
})
|
|
541
541
|
|
|
542
542
|
describe('Performance and Memory', () => {
|
|
543
|
-
it('should
|
|
543
|
+
it('should return the same field when defining same path twice', () => {
|
|
544
544
|
const form = useForm({ initialData })
|
|
545
545
|
|
|
546
546
|
const field1 = form.defineField({ path: 'user.name' })
|
|
547
547
|
const field2 = form.defineField({ path: 'user.name' })
|
|
548
548
|
|
|
549
|
-
// Fields are
|
|
550
|
-
expect(field1).
|
|
551
|
-
expect(form.getField('user.name')).toBe(field2)
|
|
552
|
-
expect(form.getFields().length).toBe(1)
|
|
549
|
+
// Fields are the same object
|
|
550
|
+
expect(field1).toBe(field2)
|
|
553
551
|
})
|
|
554
552
|
|
|
555
553
|
it('should handle large numbers of nested fields', () => {
|
package/tests/useForm.test.ts
CHANGED
|
@@ -40,6 +40,31 @@ describe('useForm', () => {
|
|
|
40
40
|
})
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
it('should reinitialize form with new initial data', async () => {
|
|
44
|
+
const initialData = ref({
|
|
45
|
+
name: 'John',
|
|
46
|
+
age: 30,
|
|
47
|
+
})
|
|
48
|
+
const form = useForm({ initialData })
|
|
49
|
+
|
|
50
|
+
expect(form.data.value).toEqual({
|
|
51
|
+
name: 'John',
|
|
52
|
+
age: 30,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Update reactive initial data
|
|
56
|
+
initialData.value = {
|
|
57
|
+
name: 'Jane',
|
|
58
|
+
age: 25,
|
|
59
|
+
}
|
|
60
|
+
await nextTick()
|
|
61
|
+
|
|
62
|
+
expect(form.data.value).toEqual({
|
|
63
|
+
name: 'Jane',
|
|
64
|
+
age: 25,
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
43
68
|
it('should have initial state values', () => {
|
|
44
69
|
const form = useForm({ initialData: { name: 'John' } })
|
|
45
70
|
|
|
@@ -81,13 +106,6 @@ describe('useForm', () => {
|
|
|
81
106
|
expect(retrievedField).toBe(nameField)
|
|
82
107
|
})
|
|
83
108
|
|
|
84
|
-
it('should return undefined for non-existent fields', () => {
|
|
85
|
-
const form = useForm({ initialData: { name: 'John' } })
|
|
86
|
-
|
|
87
|
-
const field = form.getField('nonexistent')
|
|
88
|
-
expect(field).toBeUndefined()
|
|
89
|
-
})
|
|
90
|
-
|
|
91
109
|
it('should handle nested object initial data', () => {
|
|
92
110
|
const initialData = {
|
|
93
111
|
user: {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { nextTick, ref } from 'vue'
|
|
3
3
|
import { z } from 'zod'
|
|
4
|
+
import { useForm } from '../src/composables/useForm'
|
|
4
5
|
import { SuccessValidationResult, useValidation } from '../src/composables/useValidation'
|
|
5
6
|
import { ErrorBag } from '../src/types/validation'
|
|
6
7
|
import { hasErrors } from '../src/utils/validation'
|
|
7
8
|
|
|
9
|
+
const delay = (ms: number = 0) => new Promise(resolve => setTimeout(resolve, ms))
|
|
10
|
+
|
|
8
11
|
describe('useValidation', () => {
|
|
9
12
|
it('should initialize with no errors', () => {
|
|
10
13
|
const formState = { data: { name: 'John' } }
|
|
@@ -213,4 +216,26 @@ describe('useValidation', () => {
|
|
|
213
216
|
expect(result.errors.propertyErrors.name).toEqual(['Too small: expected string to have >=2 characters']) // From schema validation
|
|
214
217
|
expect(result.errors.propertyErrors.email).toEqual(['External email error'])
|
|
215
218
|
})
|
|
219
|
+
|
|
220
|
+
it('should pass errors to the field', async () => {
|
|
221
|
+
const errors = ref<ErrorBag>(SuccessValidationResult.errors)
|
|
222
|
+
|
|
223
|
+
const form = useForm({
|
|
224
|
+
initialData: { name: 'A' },
|
|
225
|
+
errors,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const field = form.getField('name')
|
|
229
|
+
|
|
230
|
+
expect(field.errors.value).toEqual([]) // Initially no errors
|
|
231
|
+
|
|
232
|
+
errors.value = {
|
|
233
|
+
general: [],
|
|
234
|
+
propertyErrors: { name: ['Name error'] },
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await delay()
|
|
238
|
+
|
|
239
|
+
expect(field.errors.value).toEqual(['Name error'])
|
|
240
|
+
})
|
|
216
241
|
})
|