@teamnovu/kit-vue-forms 0.0.1
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/PLAN.md +209 -0
- package/dist/composables/useField.d.ts +12 -0
- package/dist/composables/useFieldRegistry.d.ts +15 -0
- package/dist/composables/useForm.d.ts +10 -0
- package/dist/composables/useFormState.d.ts +7 -0
- package/dist/composables/useSubform.d.ts +5 -0
- package/dist/composables/useValidation.d.ts +30 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +414 -0
- package/dist/types/form.d.ts +41 -0
- package/dist/types/util.d.ts +26 -0
- package/dist/types/validation.d.ts +16 -0
- package/dist/utils/general.d.ts +2 -0
- package/dist/utils/path.d.ts +11 -0
- package/dist/utils/type-helpers.d.ts +3 -0
- package/dist/utils/validation.d.ts +3 -0
- package/dist/utils/zod.d.ts +3 -0
- package/package.json +41 -0
- package/src/composables/useField.ts +74 -0
- package/src/composables/useFieldRegistry.ts +53 -0
- package/src/composables/useForm.ts +54 -0
- package/src/composables/useFormData.ts +16 -0
- package/src/composables/useFormState.ts +21 -0
- package/src/composables/useSubform.ts +173 -0
- package/src/composables/useValidation.ts +227 -0
- package/src/index.ts +11 -0
- package/src/types/form.ts +58 -0
- package/src/types/util.ts +73 -0
- package/src/types/validation.ts +22 -0
- package/src/utils/general.ts +7 -0
- package/src/utils/path.ts +87 -0
- package/src/utils/type-helpers.ts +3 -0
- package/src/utils/validation.ts +66 -0
- package/src/utils/zod.ts +24 -0
- package/tests/formState.test.ts +138 -0
- package/tests/integration.test.ts +200 -0
- package/tests/nestedPath.test.ts +651 -0
- package/tests/path-utils.test.ts +159 -0
- package/tests/subform.test.ts +1348 -0
- package/tests/useField.test.ts +147 -0
- package/tests/useForm.test.ts +178 -0
- package/tests/useValidation.test.ts +216 -0
- package/tsconfig.json +18 -0
- package/vite.config.js +39 -0
- package/vitest.config.ts +14 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
var K = Object.defineProperty;
|
|
2
|
+
var W = (t, r, e) => r in t ? K(t, r, { enumerable: !0, configurable: !0, writable: !0, value: e }) : t[r] = e;
|
|
3
|
+
var w = (t, r, e) => W(t, typeof r != "symbol" ? r + "" : r, e);
|
|
4
|
+
import { toValue as $, toRaw as I, computed as d, unref as u, reactive as b, watch as E, toRefs as P, toRef as R, ref as N, isRef as A, getCurrentScope as T, onBeforeUnmount as B } from "vue";
|
|
5
|
+
import "zod";
|
|
6
|
+
function y(t) {
|
|
7
|
+
const r = $(t), e = I(r);
|
|
8
|
+
return structuredClone(e);
|
|
9
|
+
}
|
|
10
|
+
function J(t) {
|
|
11
|
+
return t === "" ? [] : t.split(/\s*\.\s*/).filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
function m(t, r) {
|
|
14
|
+
return (Array.isArray(r) ? r : J(r)).reduce(
|
|
15
|
+
(a, s) => a == null ? void 0 : a[s],
|
|
16
|
+
t
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
function G(t, r, e) {
|
|
20
|
+
const a = Array.isArray(r) ? r : J(r);
|
|
21
|
+
if (a.length === 0)
|
|
22
|
+
throw new Error("Path cannot be empty");
|
|
23
|
+
const s = a.at(-1), n = a.slice(0, -1).reduce(
|
|
24
|
+
(c, v) => c[v],
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
t
|
|
27
|
+
);
|
|
28
|
+
n[s] = e;
|
|
29
|
+
}
|
|
30
|
+
const M = (t, r) => d({
|
|
31
|
+
get() {
|
|
32
|
+
return m(u(t), u(r));
|
|
33
|
+
},
|
|
34
|
+
set(e) {
|
|
35
|
+
G(u(t), u(r), e);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
function F(t, r) {
|
|
39
|
+
return !t && !r ? "" : !t && r ? r : !r && t ? t : `${t}.${r}`;
|
|
40
|
+
}
|
|
41
|
+
function L(t, r) {
|
|
42
|
+
if (!r)
|
|
43
|
+
return t;
|
|
44
|
+
const e = `${r}.`, a = Object.fromEntries(
|
|
45
|
+
Object.entries(t.propertyErrors).filter(([s]) => s.startsWith(e)).map(
|
|
46
|
+
([s, n]) => [s.slice(e.length), n]
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
return {
|
|
50
|
+
general: t.general,
|
|
51
|
+
// Keep general errors
|
|
52
|
+
propertyErrors: a
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function U(t) {
|
|
56
|
+
const r = b({
|
|
57
|
+
value: t.value,
|
|
58
|
+
path: t.path,
|
|
59
|
+
initialValue: d(() => Object.freeze(y(t.initialValue))),
|
|
60
|
+
errors: u(t.errors) || [],
|
|
61
|
+
touched: !1
|
|
62
|
+
});
|
|
63
|
+
E(() => u(t.errors), (f) => {
|
|
64
|
+
r.errors = f || [];
|
|
65
|
+
});
|
|
66
|
+
const e = d(() => JSON.stringify(r.value) !== JSON.stringify(r.initialValue)), a = (f) => {
|
|
67
|
+
r.value = f;
|
|
68
|
+
}, s = () => {
|
|
69
|
+
r.touched = !0;
|
|
70
|
+
}, n = () => {
|
|
71
|
+
}, c = () => {
|
|
72
|
+
r.value = y(r.initialValue), r.touched = !1, r.errors = [];
|
|
73
|
+
}, v = (f) => {
|
|
74
|
+
r.errors = f;
|
|
75
|
+
}, o = () => {
|
|
76
|
+
r.errors = [];
|
|
77
|
+
}, l = P(r);
|
|
78
|
+
return {
|
|
79
|
+
value: l.value,
|
|
80
|
+
path: l.path,
|
|
81
|
+
initialValue: l.initialValue,
|
|
82
|
+
errors: l.errors,
|
|
83
|
+
touched: l.touched,
|
|
84
|
+
dirty: e,
|
|
85
|
+
setValue: a,
|
|
86
|
+
onBlur: s,
|
|
87
|
+
onFocus: n,
|
|
88
|
+
reset: c,
|
|
89
|
+
setErrors: v,
|
|
90
|
+
clearErrors: o
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function Z(t) {
|
|
94
|
+
const r = {}, e = (c) => {
|
|
95
|
+
const v = u(c.path);
|
|
96
|
+
r[v] = c;
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
fields: r,
|
|
100
|
+
getField: (c) => r[c],
|
|
101
|
+
getFields: () => Object.values(r),
|
|
102
|
+
registerField: e,
|
|
103
|
+
defineField: (c) => {
|
|
104
|
+
const v = U({
|
|
105
|
+
...c,
|
|
106
|
+
value: M(R(t, "formData"), c.path),
|
|
107
|
+
initialValue: d(() => m(t.initialData, u(c.path)))
|
|
108
|
+
});
|
|
109
|
+
return e(v), v;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function _(t, r) {
|
|
114
|
+
const e = d(() => JSON.stringify(t.formData) !== JSON.stringify(t.initialData)), a = d(() => r.getFields().some((s) => u(s.touched)));
|
|
115
|
+
return {
|
|
116
|
+
isDirty: e,
|
|
117
|
+
isTouched: a
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function k(t) {
|
|
121
|
+
return t.filter(
|
|
122
|
+
(r, e, a) => a.indexOf(r) === e
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
function x(...t) {
|
|
126
|
+
return t.slice(1).reduce((r, e) => {
|
|
127
|
+
if (!r && !e)
|
|
128
|
+
return;
|
|
129
|
+
const a = ((e == null ? void 0 : e.length) ?? 0) > 0;
|
|
130
|
+
if (!r && ((e == null ? void 0 : e.length) ?? 0) > 0)
|
|
131
|
+
return e;
|
|
132
|
+
if (!a)
|
|
133
|
+
return r;
|
|
134
|
+
const s = (r ?? []).concat(e);
|
|
135
|
+
return k(s);
|
|
136
|
+
}, t[0]);
|
|
137
|
+
}
|
|
138
|
+
function q(...t) {
|
|
139
|
+
return t.map((e) => Object.keys(e)).flat().reduce((e, a) => {
|
|
140
|
+
const s = t.map((n) => n[a]).filter(Boolean);
|
|
141
|
+
return {
|
|
142
|
+
...e,
|
|
143
|
+
[a]: x(...s)
|
|
144
|
+
};
|
|
145
|
+
}, {});
|
|
146
|
+
}
|
|
147
|
+
function S(...t) {
|
|
148
|
+
if (!t.length)
|
|
149
|
+
return {
|
|
150
|
+
general: [],
|
|
151
|
+
propertyErrors: {}
|
|
152
|
+
};
|
|
153
|
+
const r = t[0];
|
|
154
|
+
return t.length === 1 ? r : t.slice(1).reduce(
|
|
155
|
+
(e, a) => ({
|
|
156
|
+
general: x(e.general, a.general),
|
|
157
|
+
propertyErrors: q(e.propertyErrors ?? {}, a.propertyErrors ?? {})
|
|
158
|
+
}),
|
|
159
|
+
r
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
function j(t) {
|
|
163
|
+
var a;
|
|
164
|
+
const r = (((a = t.general) == null ? void 0 : a.length) ?? 0) > 0, e = Object.entries(t.propertyErrors).filter(([, s]) => s == null ? void 0 : s.length).length > 0;
|
|
165
|
+
return r || e;
|
|
166
|
+
}
|
|
167
|
+
function H(t) {
|
|
168
|
+
const r = t.issues.filter((a) => a.path.length === 0).map((a) => a.message), e = t.issues.filter((a) => a.path.length > 0).reduce((a, s) => {
|
|
169
|
+
const n = s.path.join(".");
|
|
170
|
+
return {
|
|
171
|
+
...a,
|
|
172
|
+
[n]: [...a[n] ?? [], s.message]
|
|
173
|
+
};
|
|
174
|
+
}, {});
|
|
175
|
+
return {
|
|
176
|
+
general: r,
|
|
177
|
+
propertyErrors: e
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const p = {
|
|
181
|
+
isValid: !0,
|
|
182
|
+
errors: {
|
|
183
|
+
general: [],
|
|
184
|
+
propertyErrors: {}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
class Q {
|
|
188
|
+
constructor(r) {
|
|
189
|
+
this.schema = r;
|
|
190
|
+
}
|
|
191
|
+
async validate(r) {
|
|
192
|
+
if (!this.schema)
|
|
193
|
+
return p;
|
|
194
|
+
const e = await this.schema.safeParseAsync(r);
|
|
195
|
+
if (e.success)
|
|
196
|
+
return p;
|
|
197
|
+
const a = H(e.error);
|
|
198
|
+
return {
|
|
199
|
+
isValid: !1,
|
|
200
|
+
errors: {
|
|
201
|
+
general: a.general ?? [],
|
|
202
|
+
propertyErrors: a.propertyErrors ?? {}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
class X {
|
|
208
|
+
constructor(r) {
|
|
209
|
+
this.validateFn = r;
|
|
210
|
+
}
|
|
211
|
+
async validate(r) {
|
|
212
|
+
if (!this.validateFn)
|
|
213
|
+
return p;
|
|
214
|
+
try {
|
|
215
|
+
const e = await this.validateFn(r);
|
|
216
|
+
return e.isValid ? p : e;
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return {
|
|
219
|
+
isValid: !1,
|
|
220
|
+
errors: {
|
|
221
|
+
general: [e.message || "Validation error"],
|
|
222
|
+
propertyErrors: {}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
class Y {
|
|
229
|
+
constructor(r, e) {
|
|
230
|
+
w(this, "schemaValidator");
|
|
231
|
+
w(this, "functionValidator");
|
|
232
|
+
this.schema = r, this.validateFn = e, this.schemaValidator = new Q(this.schema), this.functionValidator = new X(this.validateFn);
|
|
233
|
+
}
|
|
234
|
+
async validate(r) {
|
|
235
|
+
const [e, a] = await Promise.all([
|
|
236
|
+
this.schemaValidator.validate(r),
|
|
237
|
+
this.functionValidator.validate(r)
|
|
238
|
+
]);
|
|
239
|
+
return {
|
|
240
|
+
isValid: e.isValid && a.isValid,
|
|
241
|
+
errors: S(e.errors, a.errors)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function O(t) {
|
|
246
|
+
return d(() => new Y(
|
|
247
|
+
u(t.schema),
|
|
248
|
+
u(t.validateFn)
|
|
249
|
+
));
|
|
250
|
+
}
|
|
251
|
+
function rr(t, r) {
|
|
252
|
+
const e = b({
|
|
253
|
+
validators: N([O(r)]),
|
|
254
|
+
isValidated: !1,
|
|
255
|
+
errors: u(r.errors) ?? {
|
|
256
|
+
general: [],
|
|
257
|
+
propertyErrors: {}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
E(() => u(r.errors), async () => {
|
|
261
|
+
const o = await s();
|
|
262
|
+
n(o.errors);
|
|
263
|
+
}, { immediate: !0 }), E(
|
|
264
|
+
[() => e.validators],
|
|
265
|
+
async (o) => {
|
|
266
|
+
if (e.isValidated)
|
|
267
|
+
if (o) {
|
|
268
|
+
const l = await s();
|
|
269
|
+
e.errors = l.errors;
|
|
270
|
+
} else
|
|
271
|
+
e.errors = p.errors;
|
|
272
|
+
},
|
|
273
|
+
{ immediate: !0 }
|
|
274
|
+
), E(() => t.formData, () => {
|
|
275
|
+
e.isValidated && c();
|
|
276
|
+
});
|
|
277
|
+
const a = (o) => {
|
|
278
|
+
const l = A(o) ? o : O(o);
|
|
279
|
+
return e.validators.push(l), T() && B(() => {
|
|
280
|
+
e.validators = e.validators.filter(
|
|
281
|
+
(f) => f !== l
|
|
282
|
+
);
|
|
283
|
+
}), l;
|
|
284
|
+
};
|
|
285
|
+
async function s() {
|
|
286
|
+
const o = await Promise.all(
|
|
287
|
+
e.validators.filter((V) => u(V) !== void 0).map((V) => u(V).validate(t.formData))
|
|
288
|
+
), l = o.every((V) => V.isValid);
|
|
289
|
+
let { errors: f } = p;
|
|
290
|
+
if (!l) {
|
|
291
|
+
const V = o.map((D) => D.errors);
|
|
292
|
+
f = S(...V);
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
errors: f,
|
|
296
|
+
isValid: l
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const n = (o) => {
|
|
300
|
+
e.errors = S(u(r.errors) ?? p.errors, o);
|
|
301
|
+
}, c = async () => {
|
|
302
|
+
const o = await s();
|
|
303
|
+
return n(o.errors), e.isValidated = !0, {
|
|
304
|
+
isValid: !j(o.errors),
|
|
305
|
+
errors: e.errors
|
|
306
|
+
};
|
|
307
|
+
}, v = d(() => !j(e.errors));
|
|
308
|
+
return {
|
|
309
|
+
...P(e),
|
|
310
|
+
validateForm: c,
|
|
311
|
+
defineValidator: a,
|
|
312
|
+
isValid: v
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
class tr {
|
|
316
|
+
constructor(r, e) {
|
|
317
|
+
this.path = r, this.validator = e;
|
|
318
|
+
}
|
|
319
|
+
async validate(r) {
|
|
320
|
+
const e = m(r, this.path);
|
|
321
|
+
if (!this.validator)
|
|
322
|
+
return p;
|
|
323
|
+
const a = await this.validator.validate(e);
|
|
324
|
+
return {
|
|
325
|
+
isValid: a.isValid,
|
|
326
|
+
errors: {
|
|
327
|
+
general: a.errors.general || [],
|
|
328
|
+
propertyErrors: a.errors.propertyErrors ? Object.fromEntries(
|
|
329
|
+
Object.entries(a.errors.propertyErrors).map(([s, n]) => [
|
|
330
|
+
F(this.path, s),
|
|
331
|
+
n
|
|
332
|
+
])
|
|
333
|
+
) : {}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function er(t, r, e) {
|
|
339
|
+
const a = M(t.formData, r), s = d(() => m(t.initialData.value, r)), n = (i) => ({
|
|
340
|
+
...i,
|
|
341
|
+
path: d(() => u(i.path).replace(r + ".", "")),
|
|
342
|
+
setValue: (h) => {
|
|
343
|
+
i.setValue(h);
|
|
344
|
+
}
|
|
345
|
+
}), c = (i) => {
|
|
346
|
+
const h = F(r, i), g = t.getField(h);
|
|
347
|
+
if (g)
|
|
348
|
+
return n(g);
|
|
349
|
+
}, v = (i) => {
|
|
350
|
+
const h = F(r, i.path), g = t.defineField({
|
|
351
|
+
...i,
|
|
352
|
+
path: h
|
|
353
|
+
});
|
|
354
|
+
return n(g);
|
|
355
|
+
}, o = () => t.getFields().filter((i) => {
|
|
356
|
+
const h = i.path.value;
|
|
357
|
+
return h.startsWith(r + ".") || h === r;
|
|
358
|
+
}).map((i) => n(i)), l = () => t.getFields().filter((i) => {
|
|
359
|
+
const h = i.path.value;
|
|
360
|
+
return h.startsWith(r + ".") || h === r;
|
|
361
|
+
}), f = d(() => l().some((i) => i.dirty.value)), V = d(() => l().some((i) => i.touched.value)), D = d(() => t.isValid.value), z = d(() => t.isValidated.value), C = d(() => L(u(t.errors), r));
|
|
362
|
+
return {
|
|
363
|
+
formData: a,
|
|
364
|
+
initialData: s,
|
|
365
|
+
defineField: v,
|
|
366
|
+
getField: c,
|
|
367
|
+
getFields: o,
|
|
368
|
+
isDirty: f,
|
|
369
|
+
isTouched: V,
|
|
370
|
+
isValid: D,
|
|
371
|
+
isValidated: z,
|
|
372
|
+
errors: C,
|
|
373
|
+
defineValidator: (i) => {
|
|
374
|
+
const h = A(i) ? i : O(i), g = d(
|
|
375
|
+
() => new tr(r, u(h))
|
|
376
|
+
);
|
|
377
|
+
return t.defineValidator(g), h;
|
|
378
|
+
},
|
|
379
|
+
reset: () => l().forEach((i) => i.reset()),
|
|
380
|
+
validateForm: () => t.validateForm(),
|
|
381
|
+
getSubForm: (i, h) => {
|
|
382
|
+
const g = F(r, i);
|
|
383
|
+
return t.getSubForm(
|
|
384
|
+
g,
|
|
385
|
+
h
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function ur(t) {
|
|
391
|
+
const r = d(() => Object.freeze(y(t.initialData))), e = N(y(r)), a = b({
|
|
392
|
+
initialData: r,
|
|
393
|
+
formData: e
|
|
394
|
+
}), s = Z(a), n = rr(a, t), c = _(a, s), v = () => {
|
|
395
|
+
e.value = y(r), s.getFields().forEach((f) => f.reset());
|
|
396
|
+
};
|
|
397
|
+
function o(f, V) {
|
|
398
|
+
return er(l, f);
|
|
399
|
+
}
|
|
400
|
+
const l = {
|
|
401
|
+
...s,
|
|
402
|
+
...n,
|
|
403
|
+
...c,
|
|
404
|
+
reset: v,
|
|
405
|
+
getSubForm: o,
|
|
406
|
+
initialData: R(a, "initialData"),
|
|
407
|
+
formData: R(a, "formData")
|
|
408
|
+
};
|
|
409
|
+
return l;
|
|
410
|
+
}
|
|
411
|
+
export {
|
|
412
|
+
U as useField,
|
|
413
|
+
ur as useForm
|
|
414
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Ref } from 'vue';
|
|
2
|
+
import { DefineFieldOptions } from '../composables/useFieldRegistry';
|
|
3
|
+
import { SubformOptions } from '../composables/useSubform';
|
|
4
|
+
import { EntityPaths, Paths, PickEntity, PickProps } from './util';
|
|
5
|
+
import { ErrorBag, ValidationErrorMessage, ValidationErrors, ValidationResult, Validator } from './validation';
|
|
6
|
+
import { ValidatorOptions } from '../composables/useValidation';
|
|
7
|
+
export type FormDataDefault = object;
|
|
8
|
+
export interface FormState<T extends FormDataDefault, TIn extends FormDataDefault = T> {
|
|
9
|
+
formData: T;
|
|
10
|
+
initialData: TIn;
|
|
11
|
+
}
|
|
12
|
+
export interface FormField<T, P extends string> {
|
|
13
|
+
value: Ref<T>;
|
|
14
|
+
path: Ref<P>;
|
|
15
|
+
initialValue: Readonly<Ref<T>>;
|
|
16
|
+
errors: Ref<ValidationErrors>;
|
|
17
|
+
touched: Ref<boolean>;
|
|
18
|
+
dirty: Ref<boolean>;
|
|
19
|
+
setValue: (newValue: T) => void;
|
|
20
|
+
onBlur: () => void;
|
|
21
|
+
onFocus: () => void;
|
|
22
|
+
reset: () => void;
|
|
23
|
+
setErrors: (newErrors: ValidationErrorMessage[]) => void;
|
|
24
|
+
clearErrors: () => void;
|
|
25
|
+
}
|
|
26
|
+
export interface Form<T extends FormDataDefault> {
|
|
27
|
+
formData: Ref<T>;
|
|
28
|
+
initialData: Readonly<Ref<T>>;
|
|
29
|
+
defineField: <P extends Paths<T>>(options: DefineFieldOptions<PickProps<T, P>, P>) => FormField<PickProps<T, P>, P>;
|
|
30
|
+
getField: <P extends Paths<T>>(path: P) => FormField<PickProps<T, P>, P> | undefined;
|
|
31
|
+
getFields: () => FormField<PickProps<T, Paths<T>>, Paths<T>>[];
|
|
32
|
+
isDirty: Ref<boolean>;
|
|
33
|
+
isTouched: Ref<boolean>;
|
|
34
|
+
isValid: Ref<boolean>;
|
|
35
|
+
isValidated: Ref<boolean>;
|
|
36
|
+
errors: Ref<ErrorBag>;
|
|
37
|
+
defineValidator: (options: ValidatorOptions<T> | Ref<Validator<T>>) => Ref<Validator<T> | undefined>;
|
|
38
|
+
reset: () => void;
|
|
39
|
+
validateForm: () => Promise<ValidationResult>;
|
|
40
|
+
getSubForm: <P extends EntityPaths<T>>(path: P, options?: SubformOptions<PickEntity<T, P>>) => Form<PickEntity<T, P>>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FormDataDefault } from './form';
|
|
2
|
+
/**
|
|
3
|
+
* Takes a dot-connected path and returns a tuple of its parts.
|
|
4
|
+
*/
|
|
5
|
+
export type SplitPath<TPath extends string> = TPath extends `${infer T1}.${infer T2}` ? [T1, ...SplitPath<T2>] : [TPath];
|
|
6
|
+
/**
|
|
7
|
+
* Picks the exact type of the Entity at the nested PropertyKeys path.
|
|
8
|
+
*/
|
|
9
|
+
export type PickProps<Entity, PropertyKeys extends string> = PropertyKeys extends `${infer TRoot}.${infer TRest}` ? TRoot extends keyof Entity ? TRest extends string ? Entity[TRoot] extends object ? PickProps<Entity[TRoot], TRest> : never : never : TRoot extends `${number}` ? Entity extends unknown[] ? TRest extends string ? Entity[number] extends object ? PickProps<Entity[number], TRest> : never : never : never : never : PropertyKeys extends keyof Entity ? Entity[PropertyKeys] : PropertyKeys extends `${number}` ? Entity extends unknown[] ? Entity[number] : never : never;
|
|
10
|
+
/**
|
|
11
|
+
* Resolves to a union of dot-connected paths of all nested properties of T.
|
|
12
|
+
*/
|
|
13
|
+
export type Paths<T, Seen = never> = T extends Seen ? never : T extends Array<infer ArrayType> ? `${number}` | `${number}.${Paths<ArrayType, Seen | T>}` : T extends object ? {
|
|
14
|
+
[K in keyof T]-?: `${Exclude<K, symbol>}${'' | `.${Paths<T[K], Seen | T>}`}`;
|
|
15
|
+
}[keyof T] : never;
|
|
16
|
+
/**
|
|
17
|
+
* Removes the last part of a dot-connected path.
|
|
18
|
+
*/
|
|
19
|
+
export type ButLast<T extends string> = T extends `${infer Rest}.${infer Last}` ? ButLast<Last> extends '' ? Rest : `${Rest}.${ButLast<Last>}` : never;
|
|
20
|
+
/**
|
|
21
|
+
* Combines Paths<T> with ButLast<Paths<T>> to include all paths except the last part.
|
|
22
|
+
* The & Paths<T> ensures that there are no entity paths that are not also available in Paths<T>.
|
|
23
|
+
*/
|
|
24
|
+
export type EntityPaths<T> = ButLast<Paths<T>> & Paths<T>;
|
|
25
|
+
export type PickEntity<Entity, PropertyKeys extends string> = PropertyKeys extends unknown ? PickProps<Entity, EntityPaths<Entity> & PropertyKeys> & FormDataDefault : never;
|
|
26
|
+
export type RestPath<T extends string, P extends string> = P extends `${T}.${infer Rest}` ? Rest : never;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FormDataDefault } from './form';
|
|
2
|
+
export type ValidationStrategy = 'onTouch' | 'onFormOpen' | 'none' | 'preSubmit';
|
|
3
|
+
export type ValidationErrorMessage = string;
|
|
4
|
+
export type ValidationErrors = ValidationErrorMessage[] | undefined;
|
|
5
|
+
export interface ErrorBag {
|
|
6
|
+
general: ValidationErrors;
|
|
7
|
+
propertyErrors: Record<string, ValidationErrors>;
|
|
8
|
+
}
|
|
9
|
+
export interface ValidationResult {
|
|
10
|
+
isValid: boolean;
|
|
11
|
+
errors: ErrorBag;
|
|
12
|
+
}
|
|
13
|
+
export interface Validator<T extends FormDataDefault = FormDataDefault> {
|
|
14
|
+
validate: (data: T) => Promise<ValidationResult>;
|
|
15
|
+
}
|
|
16
|
+
export type ValidationFunction<T> = (data: T) => Promise<ValidationResult>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MaybeRef, WritableComputedRef } from 'vue';
|
|
2
|
+
import { Paths, PickProps, SplitPath } from '../types/util';
|
|
3
|
+
import { ErrorBag } from '../types/validation';
|
|
4
|
+
export declare function splitPath(path: string): string[];
|
|
5
|
+
export declare function getNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>): PickProps<T, K>;
|
|
6
|
+
export declare function setNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>, value: PickProps<T, K>): void;
|
|
7
|
+
export declare const getLens: <T, K extends Paths<T>>(formData: MaybeRef<T>, key: MaybeRef<K | SplitPath<K>>) => WritableComputedRef<PickProps<T, K>, PickProps<T, K>>;
|
|
8
|
+
type JoinPath<Base extends string, Sub extends string> = `${Base}${Base extends '' ? '' : Sub extends '' ? '' : '.'}${Sub}`;
|
|
9
|
+
export declare function joinPath<Base extends string, Sub extends string>(basePath: Base, subPath: Sub): JoinPath<Base, Sub>;
|
|
10
|
+
export declare function filterErrorsForPath(errors: ErrorBag, path: string): ErrorBag;
|
|
11
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@teamnovu/kit-vue-forms",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.mjs"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "vite build",
|
|
14
|
+
"watch": "NODE_ENV=development vite build --watch",
|
|
15
|
+
"lint": "eslint --fix --ignore-pattern 'dist/**' .",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"test:ui": "vitest --ui"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"vue",
|
|
22
|
+
"forms",
|
|
23
|
+
"validation",
|
|
24
|
+
"typescript"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"vue": "^3.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"vue": "^3.0.0",
|
|
33
|
+
"vitest": "^2.0.0",
|
|
34
|
+
"@vitest/ui": "^2.0.0",
|
|
35
|
+
"happy-dom": "^12.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@vueuse/core": "^13.5.0",
|
|
39
|
+
"zod": "^4"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { computed, reactive, toRefs, unref, watch, type MaybeRef, type MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import type { FormField } from '../types/form'
|
|
3
|
+
import type { ValidationErrorMessage, ValidationErrors } from '../types/validation'
|
|
4
|
+
import { cloneRefValue } from '../utils/general'
|
|
5
|
+
|
|
6
|
+
export interface UseFieldOptions<T, K extends string> {
|
|
7
|
+
value?: MaybeRef<T>
|
|
8
|
+
initialValue?: MaybeRefOrGetter<Readonly<T>>
|
|
9
|
+
type?: MaybeRef<string>
|
|
10
|
+
required?: MaybeRef<boolean>
|
|
11
|
+
path: K
|
|
12
|
+
errors?: MaybeRef<ValidationErrors>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useField<T, K extends string>(options: UseFieldOptions<T, K>): FormField<T, K> {
|
|
16
|
+
const state = reactive({
|
|
17
|
+
value: options.value,
|
|
18
|
+
path: options.path,
|
|
19
|
+
initialValue: computed(() => Object.freeze(cloneRefValue(options.initialValue))),
|
|
20
|
+
errors: unref(options.errors) || [],
|
|
21
|
+
touched: false,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
watch(() => unref(options.errors), (newValue) => {
|
|
25
|
+
state.errors = newValue || []
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const dirty = computed(() => {
|
|
29
|
+
return JSON.stringify(state.value) !== JSON.stringify(state.initialValue)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const setValue = (newValue: T): void => {
|
|
33
|
+
state.value = newValue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const onBlur = (): void => {
|
|
37
|
+
state.touched = true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const onFocus = (): void => {
|
|
41
|
+
// TODO: Implement focus logic if needed
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const reset = (): void => {
|
|
45
|
+
state.value = cloneRefValue(state.initialValue)
|
|
46
|
+
state.touched = false
|
|
47
|
+
state.errors = []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const setErrors = (newErrors: ValidationErrorMessage[]): void => {
|
|
51
|
+
state.errors = newErrors
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const clearErrors = (): void => {
|
|
55
|
+
state.errors = []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const refs = toRefs(state)
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
value: refs.value as FormField<T, K>['value'],
|
|
62
|
+
path: refs.path as FormField<T, K>['path'],
|
|
63
|
+
initialValue: refs.initialValue as FormField<T, K>['initialValue'],
|
|
64
|
+
errors: refs.errors as FormField<T, K>['errors'],
|
|
65
|
+
touched: refs.touched as FormField<T, K>['touched'],
|
|
66
|
+
dirty,
|
|
67
|
+
setValue,
|
|
68
|
+
onBlur,
|
|
69
|
+
onFocus,
|
|
70
|
+
reset,
|
|
71
|
+
setErrors,
|
|
72
|
+
clearErrors,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { computed, toRef, unref } from 'vue'
|
|
2
|
+
import type { FormDataDefault, FormField, FormState } from '../types/form'
|
|
3
|
+
import type { Paths, PickProps } from '../types/util'
|
|
4
|
+
import { getLens, getNestedValue } from '../utils/path'
|
|
5
|
+
import { useField, type UseFieldOptions } from './useField'
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
type FieldRegistryCache<T> = Record<Paths<T>, FormField<any, string>>
|
|
9
|
+
|
|
10
|
+
export type ResolvedFormField<T, K extends Paths<T>> = FormField<PickProps<T, K>, K>
|
|
11
|
+
|
|
12
|
+
export type DefineFieldOptions<F, K extends string> = Pick<UseFieldOptions<F, K>, 'path' | 'type' | 'required'>
|
|
13
|
+
|
|
14
|
+
export function useFieldRegistry<T extends FormDataDefault>(
|
|
15
|
+
formState: FormState<T>,
|
|
16
|
+
) {
|
|
17
|
+
const fields = {} as FieldRegistryCache<T>
|
|
18
|
+
|
|
19
|
+
const registerField = <K extends Paths<T>>(field: ResolvedFormField<T, K>) => {
|
|
20
|
+
const path = unref(field.path) as Paths<T>
|
|
21
|
+
fields[path] = field
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const getField = <K extends Paths<T>>(path: K) => {
|
|
25
|
+
return fields[path] as ResolvedFormField<T, K> | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const getFields = () => {
|
|
29
|
+
return Object.values(fields) as ResolvedFormField<T, Paths<T>>[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const defineField = <K extends Paths<T>>(options: DefineFieldOptions<PickProps<T, K>, K>) => {
|
|
33
|
+
const field = useField({
|
|
34
|
+
...options,
|
|
35
|
+
value: getLens(toRef(formState, 'formData'), options.path),
|
|
36
|
+
initialValue: computed(() => getNestedValue(formState.initialData, unref(options.path))),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
registerField(field)
|
|
40
|
+
|
|
41
|
+
return field
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
fields,
|
|
46
|
+
getField,
|
|
47
|
+
getFields,
|
|
48
|
+
registerField,
|
|
49
|
+
defineField,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type FieldRegistry<T extends FormDataDefault> = ReturnType<typeof useFieldRegistry<T>>
|