@teamnovu/kit-vue-forms 0.0.20 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Field.vue.d.ts +7 -7
- package/dist/composables/useFieldRegistry.d.ts +2 -3
- package/dist/index.js +197 -198
- package/dist/types/form.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Field.vue +1 -4
- package/src/composables/useField.ts +1 -1
- package/src/composables/useFieldRegistry.ts +4 -9
- package/src/composables/useForm.ts +4 -4
- package/src/composables/useFormState.ts +2 -2
- package/src/composables/useSubform.ts +5 -5
- package/src/types/form.ts +2 -1
- package/src/utils/path.ts +1 -1
- package/tests/integration.test.ts +1 -1
- package/tests/nestedPath.test.ts +1 -1
- package/tests/useField.test.ts +23 -1
- package/tests/useForm.test.ts +2 -2
- package/PLAN.md +0 -211
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Form } from '../types/form.ts';
|
|
2
2
|
import { Paths, PickProps } from '../types/util.ts';
|
|
3
3
|
import { UseFieldOptions } from '../composables/useField.ts';
|
|
4
|
-
import { VNodeProps, AllowedComponentProps, ComponentCustomProps, PublicProps, ShallowUnwrapRef, VNode } from 'vue';
|
|
4
|
+
import { VNodeProps, AllowedComponentProps, ComponentCustomProps, PublicProps, ShallowUnwrapRef, Ref, VNode } from 'vue';
|
|
5
5
|
import { ValidationErrors, ErrorMessage } from '../index.js';
|
|
6
6
|
export interface FieldProps<TData extends object, TPath extends string> extends UseFieldOptions<PickProps<TData, TPath>, TPath> {
|
|
7
7
|
form: Form<TData>;
|
|
@@ -12,12 +12,12 @@ declare const _default: <TData extends object, TPath extends Paths<TData>>(__VLS
|
|
|
12
12
|
attrs: any;
|
|
13
13
|
slots: {
|
|
14
14
|
default?(_: {
|
|
15
|
-
data: PickProps<TData, TPath
|
|
16
|
-
path: TPath
|
|
17
|
-
initialValue: PickProps<TData, TPath
|
|
18
|
-
errors: ValidationErrors
|
|
19
|
-
touched: boolean
|
|
20
|
-
dirty: boolean
|
|
15
|
+
data: Ref<PickProps<TData, TPath>, PickProps<TData, TPath>>;
|
|
16
|
+
path: Ref<TPath, TPath>;
|
|
17
|
+
initialValue: Readonly< Ref<PickProps<TData, TPath>, PickProps<TData, TPath>>>;
|
|
18
|
+
errors: Ref<ValidationErrors>;
|
|
19
|
+
touched: Ref<boolean>;
|
|
20
|
+
dirty: Ref<boolean>;
|
|
21
21
|
setData: (newData: PickProps<TData, TPath>) => void;
|
|
22
22
|
onBlur: () => void;
|
|
23
23
|
onFocus: () => void;
|
|
@@ -2,7 +2,7 @@ import { FieldsTuple, FormDataDefault, FormField } from '../types/form';
|
|
|
2
2
|
import { Paths, PickProps } from '../types/util';
|
|
3
3
|
import { UseFieldOptions } from './useField';
|
|
4
4
|
import { ValidationState } from './useValidation';
|
|
5
|
-
|
|
5
|
+
import { ComputedRef } from 'vue';
|
|
6
6
|
export type ResolvedFormField<T, K extends Paths<T>> = FormField<PickProps<T, K>, K>;
|
|
7
7
|
export type DefineFieldOptions<F, K extends string> = Pick<UseFieldOptions<F, K>, 'path'>;
|
|
8
8
|
interface FormState<T extends FormDataDefault, TIn extends FormDataDefault = T> {
|
|
@@ -10,9 +10,8 @@ interface FormState<T extends FormDataDefault, TIn extends FormDataDefault = T>
|
|
|
10
10
|
initialData: TIn;
|
|
11
11
|
}
|
|
12
12
|
export declare function useFieldRegistry<T extends FormDataDefault>(formState: FormState<T>, validationState: ValidationState<T>): {
|
|
13
|
-
fields:
|
|
13
|
+
fields: ComputedRef<FieldsTuple<T>>;
|
|
14
14
|
getField: <K extends Paths<T>>(path: K) => ResolvedFormField<T, K>;
|
|
15
|
-
getFields: <TData extends T>() => FieldsTuple<TData>;
|
|
16
15
|
registerField: <K extends Paths<T>>(field: ResolvedFormField<T, K>) => void;
|
|
17
16
|
defineField: <K extends Paths<T>>(options: DefineFieldOptions<PickProps<T, K>, K>) => ResolvedFormField<T, K>;
|
|
18
17
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,158 +1,157 @@
|
|
|
1
1
|
var T = Object.defineProperty;
|
|
2
2
|
var G = (e, r, t) => r in e ? T(e, r, { enumerable: !0, configurable: !0, writable: !0, value: t }) : e[r] = t;
|
|
3
|
-
var
|
|
4
|
-
import { toValue as L, toRaw as Z, computed as
|
|
5
|
-
import { cloneDeep as
|
|
3
|
+
var R = (e, r, t) => G(e, typeof r != "symbol" ? r + "" : r, t);
|
|
4
|
+
import { toValue as L, toRaw as Z, computed as c, unref as d, reactive as F, toRefs as N, markRaw as q, toRef as _, ref as W, watch as g, isRef as z, getCurrentScope as H, onBeforeUnmount as Q, defineComponent as S, renderSlot as j, normalizeProps as M, guardReactiveProps as k, resolveComponent as X, createBlock as O, openBlock as $, withCtx as A, resolveDynamicComponent as Y, mergeProps as x } from "vue";
|
|
5
|
+
import { cloneDeep as rr } from "lodash-es";
|
|
6
6
|
import "zod";
|
|
7
|
-
function
|
|
7
|
+
function y(e) {
|
|
8
8
|
const r = L(e), t = Z(r);
|
|
9
|
-
return
|
|
9
|
+
return rr(t);
|
|
10
10
|
}
|
|
11
|
-
function
|
|
11
|
+
function B(e) {
|
|
12
12
|
return e === "" ? [] : e.split(/\s*\.\s*/).filter(Boolean);
|
|
13
13
|
}
|
|
14
14
|
function w(e, r) {
|
|
15
|
-
return (Array.isArray(r) ? r :
|
|
16
|
-
(
|
|
15
|
+
return (Array.isArray(r) ? r : B(r)).reduce(
|
|
16
|
+
(a, s) => a == null ? void 0 : a[s],
|
|
17
17
|
e
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
|
-
function
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
20
|
+
function er(e, r, t) {
|
|
21
|
+
const a = Array.isArray(r) ? r : B(r);
|
|
22
|
+
if (a.length === 0)
|
|
23
23
|
throw new Error("Path cannot be empty");
|
|
24
|
-
const
|
|
25
|
-
(
|
|
24
|
+
const s = a.at(-1), o = a.slice(0, -1).reduce(
|
|
25
|
+
(i, h) => i[h],
|
|
26
26
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
27
|
e
|
|
28
28
|
);
|
|
29
|
-
o[
|
|
29
|
+
o[s] = t;
|
|
30
30
|
}
|
|
31
|
-
const
|
|
31
|
+
const K = (e, r) => c({
|
|
32
32
|
get() {
|
|
33
33
|
return w(d(e), d(r));
|
|
34
34
|
},
|
|
35
35
|
set(t) {
|
|
36
|
-
|
|
36
|
+
er(d(e), d(r), t);
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
|
-
function
|
|
39
|
+
function E(e, r) {
|
|
40
40
|
return !e && !r ? "" : !e && r ? r : !r && e ? e : `${e}.${r}`;
|
|
41
41
|
}
|
|
42
|
-
function
|
|
42
|
+
function tr(e, r) {
|
|
43
43
|
if (!r)
|
|
44
44
|
return e;
|
|
45
|
-
const t = `${r}.`,
|
|
46
|
-
Object.entries(e.propertyErrors).filter(([
|
|
47
|
-
([
|
|
45
|
+
const t = `${r}.`, a = Object.fromEntries(
|
|
46
|
+
Object.entries(e.propertyErrors).filter(([s]) => s.startsWith(t)).map(
|
|
47
|
+
([s, o]) => [s.slice(t.length), o]
|
|
48
48
|
)
|
|
49
49
|
);
|
|
50
50
|
return {
|
|
51
51
|
general: e.general,
|
|
52
52
|
// Keep general errors
|
|
53
|
-
propertyErrors:
|
|
53
|
+
propertyErrors: a
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
-
function
|
|
57
|
-
const r =
|
|
56
|
+
function ar(e) {
|
|
57
|
+
const r = F({
|
|
58
58
|
value: e.value,
|
|
59
59
|
path: e.path,
|
|
60
|
-
initialValue:
|
|
60
|
+
initialValue: c(() => Object.freeze(y(e.initialValue))),
|
|
61
61
|
errors: e.errors,
|
|
62
62
|
touched: !1
|
|
63
|
-
}), t =
|
|
63
|
+
}), t = c(() => JSON.stringify(r.value) !== JSON.stringify(r.initialValue)), a = (f) => {
|
|
64
64
|
r.value = f;
|
|
65
|
-
},
|
|
65
|
+
}, s = () => {
|
|
66
66
|
r.touched = !0;
|
|
67
67
|
}, o = () => {
|
|
68
|
-
},
|
|
69
|
-
r.value =
|
|
70
|
-
},
|
|
68
|
+
}, i = () => {
|
|
69
|
+
r.value = y(r.initialValue), r.touched = !1, r.errors = [];
|
|
70
|
+
}, h = (f) => {
|
|
71
71
|
r.errors = f;
|
|
72
|
-
},
|
|
72
|
+
}, l = () => {
|
|
73
73
|
r.errors = [];
|
|
74
|
-
},
|
|
74
|
+
}, u = N(r);
|
|
75
75
|
return {
|
|
76
|
-
data:
|
|
77
|
-
path:
|
|
78
|
-
initialValue:
|
|
79
|
-
errors:
|
|
80
|
-
touched:
|
|
76
|
+
data: u.value,
|
|
77
|
+
path: u.path,
|
|
78
|
+
initialValue: u.initialValue,
|
|
79
|
+
errors: u.errors,
|
|
80
|
+
touched: u.touched,
|
|
81
81
|
dirty: t,
|
|
82
|
-
setData:
|
|
83
|
-
onBlur:
|
|
82
|
+
setData: a,
|
|
83
|
+
onBlur: s,
|
|
84
84
|
onFocus: o,
|
|
85
|
-
reset:
|
|
86
|
-
setErrors:
|
|
87
|
-
clearErrors:
|
|
85
|
+
reset: i,
|
|
86
|
+
setErrors: h,
|
|
87
|
+
clearErrors: l
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
function sr(e, r) {
|
|
91
|
-
const t = {},
|
|
92
|
-
const
|
|
93
|
-
t[
|
|
94
|
-
},
|
|
95
|
-
if (!t[
|
|
96
|
-
const
|
|
97
|
-
path:
|
|
98
|
-
value:
|
|
99
|
-
initialValue:
|
|
100
|
-
errors:
|
|
91
|
+
const t = F({}), a = (i) => {
|
|
92
|
+
const h = d(i.path);
|
|
93
|
+
t[h] = q(i);
|
|
94
|
+
}, s = (i) => {
|
|
95
|
+
if (!t[i]) {
|
|
96
|
+
const h = ar({
|
|
97
|
+
path: i,
|
|
98
|
+
value: K(_(e, "data"), i),
|
|
99
|
+
initialValue: c(() => w(e.initialData, i)),
|
|
100
|
+
errors: c({
|
|
101
101
|
get() {
|
|
102
|
-
return r.errors.value.propertyErrors[
|
|
102
|
+
return r.errors.value.propertyErrors[i] || [];
|
|
103
103
|
},
|
|
104
104
|
set(l) {
|
|
105
|
-
r.errors.value.propertyErrors[
|
|
105
|
+
r.errors.value.propertyErrors[i] = l;
|
|
106
106
|
}
|
|
107
107
|
})
|
|
108
108
|
});
|
|
109
|
-
return
|
|
109
|
+
return a(h), h;
|
|
110
110
|
}
|
|
111
|
-
return t[
|
|
112
|
-
};
|
|
111
|
+
return t[i];
|
|
112
|
+
}, o = (i) => s(i.path);
|
|
113
113
|
return {
|
|
114
|
-
fields: t,
|
|
115
|
-
getField:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
defineField: (c) => a(c.path)
|
|
114
|
+
fields: c(() => Object.values(t)),
|
|
115
|
+
getField: s,
|
|
116
|
+
registerField: a,
|
|
117
|
+
defineField: o
|
|
119
118
|
};
|
|
120
119
|
}
|
|
121
|
-
function
|
|
122
|
-
const r =
|
|
120
|
+
function or(e) {
|
|
121
|
+
const r = c(() => e.fields.value.some((a) => d(a.dirty))), t = c(() => e.fields.value.some((a) => d(a.touched)));
|
|
123
122
|
return {
|
|
124
123
|
isDirty: r,
|
|
125
124
|
isTouched: t
|
|
126
125
|
};
|
|
127
126
|
}
|
|
128
|
-
function
|
|
127
|
+
function nr(e) {
|
|
129
128
|
return e.filter(
|
|
130
|
-
(r, t,
|
|
129
|
+
(r, t, a) => a.indexOf(r) === t
|
|
131
130
|
);
|
|
132
131
|
}
|
|
133
|
-
function
|
|
132
|
+
function U(...e) {
|
|
134
133
|
return e.slice(1).reduce((r, t) => {
|
|
135
134
|
if (!r && !t)
|
|
136
135
|
return;
|
|
137
|
-
const
|
|
136
|
+
const a = ((t == null ? void 0 : t.length) ?? 0) > 0;
|
|
138
137
|
if (!r && ((t == null ? void 0 : t.length) ?? 0) > 0)
|
|
139
138
|
return t;
|
|
140
|
-
if (!
|
|
139
|
+
if (!a)
|
|
141
140
|
return r;
|
|
142
|
-
const
|
|
143
|
-
return
|
|
141
|
+
const s = (r ?? []).concat(t);
|
|
142
|
+
return nr(s);
|
|
144
143
|
}, e[0]);
|
|
145
144
|
}
|
|
146
|
-
function
|
|
147
|
-
return e.map((t) => Object.keys(t)).flat().reduce((t,
|
|
148
|
-
const
|
|
145
|
+
function ir(...e) {
|
|
146
|
+
return e.map((t) => Object.keys(t)).flat().reduce((t, a) => {
|
|
147
|
+
const s = e.map((o) => o[a]).filter(Boolean);
|
|
149
148
|
return {
|
|
150
149
|
...t,
|
|
151
|
-
[
|
|
150
|
+
[a]: U(...s)
|
|
152
151
|
};
|
|
153
152
|
}, {});
|
|
154
153
|
}
|
|
155
|
-
function
|
|
154
|
+
function D(...e) {
|
|
156
155
|
if (!e.length)
|
|
157
156
|
return {
|
|
158
157
|
general: [],
|
|
@@ -160,24 +159,24 @@ function R(...e) {
|
|
|
160
159
|
};
|
|
161
160
|
const r = e[0];
|
|
162
161
|
return e.length === 1 ? r : e.slice(1).reduce(
|
|
163
|
-
(t,
|
|
164
|
-
general:
|
|
165
|
-
propertyErrors:
|
|
162
|
+
(t, a) => ({
|
|
163
|
+
general: U(t.general, a.general),
|
|
164
|
+
propertyErrors: ir(t.propertyErrors ?? {}, a.propertyErrors ?? {})
|
|
166
165
|
}),
|
|
167
166
|
r
|
|
168
167
|
);
|
|
169
168
|
}
|
|
170
169
|
function C(e) {
|
|
171
|
-
var
|
|
172
|
-
const r = (((
|
|
170
|
+
var a;
|
|
171
|
+
const r = (((a = e.general) == null ? void 0 : a.length) ?? 0) > 0, t = Object.entries(e.propertyErrors).filter(([, s]) => s == null ? void 0 : s.length).length > 0;
|
|
173
172
|
return r || t;
|
|
174
173
|
}
|
|
175
|
-
function
|
|
176
|
-
const r = e.issues.filter((
|
|
177
|
-
const o =
|
|
174
|
+
function lr(e) {
|
|
175
|
+
const r = e.issues.filter((a) => a.path.length === 0).map((a) => a.message), t = e.issues.filter((a) => a.path.length > 0).reduce((a, s) => {
|
|
176
|
+
const o = s.path.join(".");
|
|
178
177
|
return {
|
|
179
|
-
...
|
|
180
|
-
[o]: [...
|
|
178
|
+
...a,
|
|
179
|
+
[o]: [...a[o] ?? [], s.message]
|
|
181
180
|
};
|
|
182
181
|
}, {});
|
|
183
182
|
return {
|
|
@@ -192,7 +191,7 @@ const m = {
|
|
|
192
191
|
propertyErrors: {}
|
|
193
192
|
}
|
|
194
193
|
};
|
|
195
|
-
class
|
|
194
|
+
class ur {
|
|
196
195
|
constructor(r) {
|
|
197
196
|
this.schema = r;
|
|
198
197
|
}
|
|
@@ -202,12 +201,12 @@ class lr {
|
|
|
202
201
|
const t = await this.schema.safeParseAsync(r);
|
|
203
202
|
if (t.success)
|
|
204
203
|
return m;
|
|
205
|
-
const
|
|
204
|
+
const a = lr(t.error);
|
|
206
205
|
return {
|
|
207
206
|
isValid: !1,
|
|
208
207
|
errors: {
|
|
209
|
-
general:
|
|
210
|
-
propertyErrors:
|
|
208
|
+
general: a.general ?? [],
|
|
209
|
+
propertyErrors: a.propertyErrors ?? {}
|
|
211
210
|
}
|
|
212
211
|
};
|
|
213
212
|
}
|
|
@@ -233,91 +232,91 @@ class cr {
|
|
|
233
232
|
}
|
|
234
233
|
}
|
|
235
234
|
}
|
|
236
|
-
class
|
|
235
|
+
class dr {
|
|
237
236
|
constructor(r, t) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
this.schema = r, this.validateFn = t, this.schemaValidator = new
|
|
237
|
+
R(this, "schemaValidator");
|
|
238
|
+
R(this, "functionValidator");
|
|
239
|
+
this.schema = r, this.validateFn = t, this.schemaValidator = new ur(this.schema), this.functionValidator = new cr(this.validateFn);
|
|
241
240
|
}
|
|
242
241
|
async validate(r) {
|
|
243
|
-
const [t,
|
|
242
|
+
const [t, a] = await Promise.all([
|
|
244
243
|
this.schemaValidator.validate(r),
|
|
245
244
|
this.functionValidator.validate(r)
|
|
246
245
|
]);
|
|
247
246
|
return {
|
|
248
|
-
isValid: t.isValid &&
|
|
249
|
-
errors:
|
|
247
|
+
isValid: t.isValid && a.isValid,
|
|
248
|
+
errors: D(t.errors, a.errors)
|
|
250
249
|
};
|
|
251
250
|
}
|
|
252
251
|
}
|
|
253
252
|
function b(e) {
|
|
254
|
-
return
|
|
253
|
+
return c(() => new dr(
|
|
255
254
|
d(e.schema),
|
|
256
255
|
d(e.validateFn)
|
|
257
256
|
));
|
|
258
257
|
}
|
|
259
|
-
function
|
|
260
|
-
const t =
|
|
258
|
+
function fr(e, r) {
|
|
259
|
+
const t = F({
|
|
261
260
|
validators: W([b(r)]),
|
|
262
261
|
isValidated: !1,
|
|
263
262
|
errors: d(r.errors) ?? m.errors
|
|
264
263
|
});
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
o(
|
|
268
|
-
}, { immediate: !0 }),
|
|
264
|
+
g(() => d(r.errors), async () => {
|
|
265
|
+
const l = await s();
|
|
266
|
+
o(l.errors);
|
|
267
|
+
}, { immediate: !0 }), g(
|
|
269
268
|
[() => t.validators],
|
|
270
|
-
async (
|
|
269
|
+
async (l) => {
|
|
271
270
|
if (t.isValidated)
|
|
272
|
-
if (
|
|
273
|
-
const
|
|
274
|
-
t.errors =
|
|
271
|
+
if (l) {
|
|
272
|
+
const u = await s();
|
|
273
|
+
t.errors = u.errors;
|
|
275
274
|
} else
|
|
276
275
|
t.errors = m.errors;
|
|
277
276
|
},
|
|
278
277
|
{ immediate: !0 }
|
|
279
|
-
),
|
|
280
|
-
t.isValidated &&
|
|
278
|
+
), g(() => e.data, () => {
|
|
279
|
+
t.isValidated && i();
|
|
281
280
|
});
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
return t.validators.push(
|
|
281
|
+
const a = (l) => {
|
|
282
|
+
const u = z(l) ? l : b(l);
|
|
283
|
+
return t.validators.push(u), H() && Q(() => {
|
|
285
284
|
t.validators = t.validators.filter(
|
|
286
|
-
(f) => f !==
|
|
285
|
+
(f) => f !== u
|
|
287
286
|
);
|
|
288
|
-
}),
|
|
287
|
+
}), u;
|
|
289
288
|
};
|
|
290
|
-
async function
|
|
291
|
-
const
|
|
289
|
+
async function s() {
|
|
290
|
+
const l = await Promise.all(
|
|
292
291
|
t.validators.filter((v) => d(v) !== void 0).map((v) => d(v).validate(e.data))
|
|
293
|
-
),
|
|
292
|
+
), u = l.every((v) => v.isValid);
|
|
294
293
|
let { errors: f } = m;
|
|
295
|
-
if (!
|
|
296
|
-
const v =
|
|
297
|
-
f =
|
|
294
|
+
if (!u) {
|
|
295
|
+
const v = l.map((P) => P.errors);
|
|
296
|
+
f = D(...v);
|
|
298
297
|
}
|
|
299
298
|
return {
|
|
300
299
|
errors: f,
|
|
301
|
-
isValid:
|
|
300
|
+
isValid: u
|
|
302
301
|
};
|
|
303
302
|
}
|
|
304
|
-
const o = (
|
|
305
|
-
t.errors =
|
|
306
|
-
},
|
|
307
|
-
const
|
|
308
|
-
return o(
|
|
309
|
-
isValid: !C(
|
|
303
|
+
const o = (l) => {
|
|
304
|
+
t.errors = D(d(r.errors) ?? m.errors, l);
|
|
305
|
+
}, i = async () => {
|
|
306
|
+
const l = await s();
|
|
307
|
+
return o(l.errors), t.isValidated = !0, {
|
|
308
|
+
isValid: !C(l.errors),
|
|
310
309
|
errors: t.errors
|
|
311
310
|
};
|
|
312
|
-
},
|
|
311
|
+
}, h = c(() => !C(t.errors));
|
|
313
312
|
return {
|
|
314
313
|
...N(t),
|
|
315
|
-
validateForm:
|
|
316
|
-
defineValidator:
|
|
317
|
-
isValid:
|
|
314
|
+
validateForm: i,
|
|
315
|
+
defineValidator: a,
|
|
316
|
+
isValid: h
|
|
318
317
|
};
|
|
319
318
|
}
|
|
320
|
-
class
|
|
319
|
+
class pr {
|
|
321
320
|
constructor(r, t) {
|
|
322
321
|
this.path = r, this.validator = t;
|
|
323
322
|
}
|
|
@@ -325,14 +324,14 @@ class fr {
|
|
|
325
324
|
const t = w(r, this.path);
|
|
326
325
|
if (!this.validator)
|
|
327
326
|
return m;
|
|
328
|
-
const
|
|
327
|
+
const a = await this.validator.validate(t);
|
|
329
328
|
return {
|
|
330
|
-
isValid:
|
|
329
|
+
isValid: a.isValid,
|
|
331
330
|
errors: {
|
|
332
|
-
general:
|
|
333
|
-
propertyErrors:
|
|
334
|
-
Object.entries(
|
|
335
|
-
|
|
331
|
+
general: a.errors.general || [],
|
|
332
|
+
propertyErrors: a.errors.propertyErrors ? Object.fromEntries(
|
|
333
|
+
Object.entries(a.errors.propertyErrors).map(([s, o]) => [
|
|
334
|
+
E(this.path, s),
|
|
336
335
|
o
|
|
337
336
|
])
|
|
338
337
|
) : {}
|
|
@@ -340,50 +339,50 @@ class fr {
|
|
|
340
339
|
};
|
|
341
340
|
}
|
|
342
341
|
}
|
|
343
|
-
function
|
|
344
|
-
const
|
|
345
|
-
...
|
|
346
|
-
path:
|
|
342
|
+
function hr(e, r, t) {
|
|
343
|
+
const a = K(e.data, r), s = c(() => w(e.initialData.value, r)), o = (n) => ({
|
|
344
|
+
...n,
|
|
345
|
+
path: c(() => d(n.path).replace(r + ".", "")),
|
|
347
346
|
setData: (p) => {
|
|
348
|
-
|
|
347
|
+
n.setData(p);
|
|
349
348
|
}
|
|
350
|
-
}),
|
|
351
|
-
const p =
|
|
349
|
+
}), i = (n) => {
|
|
350
|
+
const p = E(r, n), V = e.getField(p);
|
|
352
351
|
return V ? o(V) : {};
|
|
353
|
-
},
|
|
354
|
-
const p =
|
|
355
|
-
...
|
|
352
|
+
}, h = (n) => {
|
|
353
|
+
const p = E(r, n.path), V = e.defineField({
|
|
354
|
+
...n,
|
|
356
355
|
path: p
|
|
357
356
|
});
|
|
358
357
|
return o(V);
|
|
359
|
-
},
|
|
360
|
-
const p =
|
|
358
|
+
}, l = c(() => e.fields.value.filter((n) => {
|
|
359
|
+
const p = n.path.value;
|
|
361
360
|
return p.startsWith(r + ".") || p === r;
|
|
362
|
-
}).map((
|
|
363
|
-
const p =
|
|
361
|
+
}).map((n) => o(n))), u = () => e.fields.value.filter((n) => {
|
|
362
|
+
const p = n.path.value;
|
|
364
363
|
return p.startsWith(r + ".") || p === r;
|
|
365
|
-
}), f =
|
|
364
|
+
}), f = c(() => u().some((n) => n.dirty.value)), v = c(() => u().some((n) => n.touched.value)), P = c(() => e.isValid.value), I = c(() => e.isValidated.value), J = c(() => tr(d(e.errors), r));
|
|
366
365
|
return {
|
|
367
|
-
data:
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
366
|
+
data: a,
|
|
367
|
+
fields: l,
|
|
368
|
+
initialData: s,
|
|
369
|
+
defineField: h,
|
|
370
|
+
getField: i,
|
|
372
371
|
isDirty: f,
|
|
373
372
|
isTouched: v,
|
|
374
373
|
isValid: P,
|
|
375
374
|
isValidated: I,
|
|
376
375
|
errors: J,
|
|
377
|
-
defineValidator: (
|
|
378
|
-
const p = z(
|
|
379
|
-
() => new
|
|
376
|
+
defineValidator: (n) => {
|
|
377
|
+
const p = z(n) ? n : b(n), V = c(
|
|
378
|
+
() => new pr(r, d(p))
|
|
380
379
|
);
|
|
381
380
|
return e.defineValidator(V), p;
|
|
382
381
|
},
|
|
383
|
-
reset: () =>
|
|
382
|
+
reset: () => u().forEach((n) => n.reset()),
|
|
384
383
|
validateForm: () => e.validateForm(),
|
|
385
|
-
getSubForm: (
|
|
386
|
-
const V =
|
|
384
|
+
getSubForm: (n, p) => {
|
|
385
|
+
const V = E(r, n);
|
|
387
386
|
return e.getSubForm(
|
|
388
387
|
V,
|
|
389
388
|
p
|
|
@@ -391,34 +390,34 @@ function pr(e, r, t) {
|
|
|
391
390
|
}
|
|
392
391
|
};
|
|
393
392
|
}
|
|
394
|
-
function
|
|
395
|
-
const r =
|
|
393
|
+
function Pr(e) {
|
|
394
|
+
const r = c(() => Object.freeze(y(e.initialData))), t = W(y(r)), a = F({
|
|
396
395
|
initialData: r,
|
|
397
396
|
data: t
|
|
398
397
|
});
|
|
399
|
-
|
|
400
|
-
|
|
398
|
+
g(r, (f) => {
|
|
399
|
+
a.data = y(f);
|
|
401
400
|
});
|
|
402
|
-
const
|
|
403
|
-
t.value =
|
|
401
|
+
const s = fr(a, e), o = sr(a, s), i = or(o), h = () => {
|
|
402
|
+
t.value = y(r), o.fields.value.forEach(
|
|
404
403
|
(f) => f.reset()
|
|
405
404
|
);
|
|
406
405
|
};
|
|
407
|
-
function
|
|
408
|
-
return
|
|
406
|
+
function l(f, v) {
|
|
407
|
+
return hr(u, f);
|
|
409
408
|
}
|
|
410
|
-
const
|
|
409
|
+
const u = {
|
|
411
410
|
...o,
|
|
412
|
-
...
|
|
413
|
-
...
|
|
414
|
-
reset:
|
|
415
|
-
getSubForm:
|
|
416
|
-
initialData: _(
|
|
417
|
-
data: _(
|
|
411
|
+
...s,
|
|
412
|
+
...i,
|
|
413
|
+
reset: h,
|
|
414
|
+
getSubForm: l,
|
|
415
|
+
initialData: _(a, "initialData"),
|
|
416
|
+
data: _(a, "data")
|
|
418
417
|
};
|
|
419
|
-
return
|
|
418
|
+
return u;
|
|
420
419
|
}
|
|
421
|
-
const
|
|
420
|
+
const Rr = /* @__PURE__ */ S({
|
|
422
421
|
__name: "Field",
|
|
423
422
|
props: {
|
|
424
423
|
form: {},
|
|
@@ -430,10 +429,10 @@ const Pr = /* @__PURE__ */ S({
|
|
|
430
429
|
setup(e) {
|
|
431
430
|
const r = e, t = r.form.defineField({
|
|
432
431
|
path: r.path
|
|
433
|
-
})
|
|
434
|
-
return (a,
|
|
432
|
+
});
|
|
433
|
+
return (a, s) => j(a.$slots, "default", M(k(d(t))));
|
|
435
434
|
}
|
|
436
|
-
}),
|
|
435
|
+
}), _r = /* @__PURE__ */ S({
|
|
437
436
|
inheritAttrs: !1,
|
|
438
437
|
__name: "FormFieldWrapper",
|
|
439
438
|
props: {
|
|
@@ -444,17 +443,17 @@ const Pr = /* @__PURE__ */ S({
|
|
|
444
443
|
},
|
|
445
444
|
setup(e) {
|
|
446
445
|
return (r, t) => {
|
|
447
|
-
const
|
|
448
|
-
return $(), O(
|
|
446
|
+
const a = X("Field");
|
|
447
|
+
return $(), O(a, {
|
|
449
448
|
form: r.form,
|
|
450
449
|
path: r.path
|
|
451
450
|
}, {
|
|
452
|
-
default: A(({ errors:
|
|
453
|
-
($(), O(
|
|
451
|
+
default: A(({ errors: s, data: o, setData: i }) => [
|
|
452
|
+
($(), O(Y(r.component), x({ ...r.componentProps, ...r.$attrs }, {
|
|
454
453
|
"model-value": o,
|
|
455
|
-
errors:
|
|
454
|
+
errors: s,
|
|
456
455
|
name: r.path,
|
|
457
|
-
"onUpdate:modelValue":
|
|
456
|
+
"onUpdate:modelValue": i
|
|
458
457
|
}), {
|
|
459
458
|
default: A(() => [
|
|
460
459
|
j(r.$slots, "default")
|
|
@@ -466,20 +465,20 @@ const Pr = /* @__PURE__ */ S({
|
|
|
466
465
|
}, 8, ["form", "path"]);
|
|
467
466
|
};
|
|
468
467
|
}
|
|
469
|
-
}),
|
|
468
|
+
}), Dr = /* @__PURE__ */ S({
|
|
470
469
|
__name: "FormPart",
|
|
471
470
|
props: {
|
|
472
471
|
form: {},
|
|
473
472
|
path: {}
|
|
474
473
|
},
|
|
475
474
|
setup(e) {
|
|
476
|
-
const r = e, t =
|
|
477
|
-
return (
|
|
475
|
+
const r = e, t = c(() => r.form.getSubForm(r.path));
|
|
476
|
+
return (a, s) => j(a.$slots, "default", M(k({ subform: t.value })));
|
|
478
477
|
}
|
|
479
478
|
});
|
|
480
479
|
export {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
480
|
+
Rr as Field,
|
|
481
|
+
_r as FormFieldWrapper,
|
|
482
|
+
Dr as FormPart,
|
|
483
|
+
Pr as useForm
|
|
485
484
|
};
|
package/dist/types/form.d.ts
CHANGED
|
@@ -26,9 +26,9 @@ export type AnyField<T> = FormField<PickProps<T, Paths<T>>, Paths<T>>;
|
|
|
26
26
|
export interface Form<T extends FormDataDefault> {
|
|
27
27
|
data: Ref<T>;
|
|
28
28
|
initialData: Readonly<Ref<T>>;
|
|
29
|
+
fields: Ref<FieldsTuple<T>>;
|
|
29
30
|
defineField: <P extends Paths<T>>(options: DefineFieldOptions<PickProps<T, P>, P>) => FormField<PickProps<T, P>, P>;
|
|
30
31
|
getField: <P extends Paths<T>>(path: P) => FormField<PickProps<T, P>, P>;
|
|
31
|
-
getFields: <TData extends T>() => FieldsTuple<TData>;
|
|
32
32
|
isDirty: Ref<boolean>;
|
|
33
33
|
isTouched: Ref<boolean>;
|
|
34
34
|
isValid: Ref<boolean>;
|
package/package.json
CHANGED
package/src/components/Field.vue
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<slot v-bind="
|
|
2
|
+
<slot v-bind="field" />
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script
|
|
6
6
|
setup lang="ts"
|
|
7
7
|
generic="TData extends object, TPath extends Paths<TData>"
|
|
8
8
|
>
|
|
9
|
-
import { reactive } from 'vue'
|
|
10
9
|
import type { Form } from '../types/form.ts'
|
|
11
10
|
import type { Paths, PickProps } from '../types/util.ts'
|
|
12
11
|
import type { UseFieldOptions } from '../composables/useField.ts'
|
|
@@ -20,6 +19,4 @@ const props = defineProps<FieldProps<TData, TPath>>()
|
|
|
20
19
|
const field = props.form.defineField({
|
|
21
20
|
path: props.path,
|
|
22
21
|
})
|
|
23
|
-
|
|
24
|
-
const slotData = reactive(field)
|
|
25
22
|
</script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, reactive, toRefs,
|
|
1
|
+
import { computed, reactive, toRefs, 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'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, toRef, unref } from 'vue'
|
|
1
|
+
import { computed, markRaw, reactive, toRef, unref, watch } 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'
|
|
@@ -21,11 +21,11 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
21
21
|
formState: FormState<T>,
|
|
22
22
|
validationState: ValidationState<T>,
|
|
23
23
|
) {
|
|
24
|
-
const fields = {} as FieldRegistryCache<T>
|
|
24
|
+
const fields = reactive({}) as FieldRegistryCache<T>
|
|
25
25
|
|
|
26
26
|
const registerField = <K extends Paths<T>>(field: ResolvedFormField<T, K>) => {
|
|
27
27
|
const path = unref(field.path) as Paths<T>
|
|
28
|
-
fields[path] = field
|
|
28
|
+
fields[path] = markRaw(field)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const getField = <K extends Paths<T>>(path: K): ResolvedFormField<T, K> => {
|
|
@@ -52,10 +52,6 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
52
52
|
return fields[path] as ResolvedFormField<T, K>
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const getFields = <TData extends T>() => {
|
|
56
|
-
return Object.values(fields) as FieldsTuple<TData>
|
|
57
|
-
}
|
|
58
|
-
|
|
59
55
|
const defineField = <K extends Paths<T>>(options: DefineFieldOptions<PickProps<T, K>, K>): ResolvedFormField<T, K> => {
|
|
60
56
|
const field = getField(options.path)
|
|
61
57
|
|
|
@@ -66,9 +62,8 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
66
62
|
}
|
|
67
63
|
|
|
68
64
|
return {
|
|
69
|
-
fields,
|
|
65
|
+
fields: computed(() => Object.values(fields) as FieldsTuple<T>),
|
|
70
66
|
getField,
|
|
71
|
-
getFields,
|
|
72
67
|
registerField,
|
|
73
68
|
defineField,
|
|
74
69
|
}
|
|
@@ -31,12 +31,12 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
const validationState = useValidation(state, options)
|
|
34
|
-
const
|
|
35
|
-
const formState = useFormState(
|
|
34
|
+
const fieldRegistry = useFieldRegistry(state, validationState)
|
|
35
|
+
const formState = useFormState(fieldRegistry)
|
|
36
36
|
|
|
37
37
|
const reset = () => {
|
|
38
38
|
data.value = cloneRefValue(initialData)
|
|
39
|
-
fields.
|
|
39
|
+
fieldRegistry.fields.value.forEach(
|
|
40
40
|
(field: AnyField<T>) => field.reset(),
|
|
41
41
|
)
|
|
42
42
|
}
|
|
@@ -49,7 +49,7 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const formInterface: Form<T> = {
|
|
52
|
-
...
|
|
52
|
+
...fieldRegistry,
|
|
53
53
|
...validationState,
|
|
54
54
|
...formState,
|
|
55
55
|
reset,
|
|
@@ -6,11 +6,11 @@ export function useFormState<T extends FormDataDefault>(
|
|
|
6
6
|
formFieldRegistry: FieldRegistry<T>,
|
|
7
7
|
) {
|
|
8
8
|
const isDirty = computed(() => {
|
|
9
|
-
return formFieldRegistry.
|
|
9
|
+
return formFieldRegistry.fields.value.some((field: AnyField<T>) => unref(field.dirty))
|
|
10
10
|
})
|
|
11
11
|
|
|
12
12
|
const isTouched = computed(() => {
|
|
13
|
-
return formFieldRegistry.
|
|
13
|
+
return formFieldRegistry.fields.value.some((field: AnyField<T>) => unref(field.touched))
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
return {
|
|
@@ -99,18 +99,18 @@ export function createSubformInterface<
|
|
|
99
99
|
return adaptMainFormField<P>(mainField)
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
const
|
|
103
|
-
return (mainForm.
|
|
102
|
+
const fields = computed(<P extends SP>() => {
|
|
103
|
+
return (mainForm.fields.value as FormField<PickProps<T, ScopedMainPaths>, ScopedMainPaths>[])
|
|
104
104
|
.filter((field) => {
|
|
105
105
|
const fieldPath = field.path.value
|
|
106
106
|
return fieldPath.startsWith(path + '.') || fieldPath === path
|
|
107
107
|
})
|
|
108
108
|
.map(field => adaptMainFormField(field)) as FieldsTuple<ST, P>
|
|
109
|
-
}
|
|
109
|
+
})
|
|
110
110
|
|
|
111
111
|
// Helper function to get all fields without type parameter
|
|
112
112
|
const getAllSubformFields = () => {
|
|
113
|
-
return (mainForm.
|
|
113
|
+
return (mainForm.fields.value as FormField<PickProps<T, ScopedMainPaths>, ScopedMainPaths>[])
|
|
114
114
|
.filter((field) => {
|
|
115
115
|
const fieldPath = field.path.value
|
|
116
116
|
return fieldPath.startsWith(path + '.') || fieldPath === path
|
|
@@ -156,10 +156,10 @@ export function createSubformInterface<
|
|
|
156
156
|
|
|
157
157
|
return {
|
|
158
158
|
data: data,
|
|
159
|
+
fields,
|
|
159
160
|
initialData,
|
|
160
161
|
defineField,
|
|
161
162
|
getField,
|
|
162
|
-
getFields,
|
|
163
163
|
isDirty,
|
|
164
164
|
isTouched,
|
|
165
165
|
isValid,
|
package/src/types/form.ts
CHANGED
|
@@ -37,10 +37,11 @@ export interface Form<T extends FormDataDefault> {
|
|
|
37
37
|
data: Ref<T>
|
|
38
38
|
initialData: Readonly<Ref<T>>
|
|
39
39
|
|
|
40
|
+
fields: Ref<FieldsTuple<T>>
|
|
41
|
+
|
|
40
42
|
// Field operations
|
|
41
43
|
defineField: <P extends Paths<T>>(options: DefineFieldOptions<PickProps<T, P>, P>) => FormField<PickProps<T, P>, P>
|
|
42
44
|
getField: <P extends Paths<T>>(path: P) => FormField<PickProps<T, P>, P>
|
|
43
|
-
getFields: <TData extends T>() => FieldsTuple<TData>
|
|
44
45
|
|
|
45
46
|
// State properties
|
|
46
47
|
isDirty: Ref<boolean>
|
package/src/utils/path.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, unref, type MaybeRef } from 'vue'
|
|
1
|
+
import { computed, isRef, reactive, shallowRef, triggerRef, unref, watch, type MaybeRef } from 'vue'
|
|
2
2
|
import type { Paths, PickProps, SplitPath } from '../types/util'
|
|
3
3
|
import type { ErrorBag, ValidationErrors } from '../types/validation'
|
|
4
4
|
|
|
@@ -187,7 +187,7 @@ describe('Integration Tests', () => {
|
|
|
187
187
|
form.defineField({ path: 'email' })
|
|
188
188
|
|
|
189
189
|
// Check registry
|
|
190
|
-
expect(form.
|
|
190
|
+
expect(form.fields.value.length).toBe(2)
|
|
191
191
|
|
|
192
192
|
// Get specific field
|
|
193
193
|
const retrievedNameField = form.getField('name')
|
package/tests/nestedPath.test.ts
CHANGED
package/tests/useField.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { ref, nextTick } from 'vue'
|
|
2
|
+
import { watch, ref, nextTick, effectScope } from 'vue'
|
|
3
3
|
import { useField } from '../src/composables/useField'
|
|
4
|
+
import { cloneDeep } from 'lodash-es'
|
|
4
5
|
|
|
5
6
|
describe('useField', () => {
|
|
6
7
|
it('should initialize field with path', () => {
|
|
@@ -144,4 +145,25 @@ describe('useField', () => {
|
|
|
144
145
|
field.setData(['a', 'b', 'c', 'd'])
|
|
145
146
|
expect(field.dirty.value).toBe(true)
|
|
146
147
|
})
|
|
148
|
+
|
|
149
|
+
it('should trigger reactivity', { timeout: 500 }, () => new Promise((resolve) => {
|
|
150
|
+
effectScope().run(() => {
|
|
151
|
+
const initialValue = ref(['a', 'b', 'c'])
|
|
152
|
+
const field = useField({
|
|
153
|
+
path: 'array',
|
|
154
|
+
value: initialValue,
|
|
155
|
+
initialValue: cloneDeep(initialValue.value),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
expect(field.data.value).toEqual(initialValue.value)
|
|
159
|
+
expect(field.dirty.value).toBe(false)
|
|
160
|
+
|
|
161
|
+
watch(initialValue, () => {
|
|
162
|
+
resolve(true)
|
|
163
|
+
}, { once: true, deep: true })
|
|
164
|
+
|
|
165
|
+
field.setData(['a', 'b', 'c', 'd'])
|
|
166
|
+
expect(field.dirty.value).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
}))
|
|
147
169
|
})
|
package/tests/useForm.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { ref, nextTick } from 'vue'
|
|
2
|
+
import { ref, nextTick, effectScope, watch, unref, reactive, toRef } from 'vue'
|
|
3
3
|
import { useForm } from '../src/composables/useForm'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
|
|
@@ -87,7 +87,7 @@ describe('useForm', () => {
|
|
|
87
87
|
|
|
88
88
|
expect(nameField.path.value).toBe('name')
|
|
89
89
|
expect(emailField.path.value).toBe('email')
|
|
90
|
-
expect(form.
|
|
90
|
+
expect(form.fields.value.length).toBe(2)
|
|
91
91
|
})
|
|
92
92
|
|
|
93
93
|
it('should get registered fields', () => {
|
package/PLAN.md
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
# DEPRECATED
|
|
2
|
-
|
|
3
|
-
# Vue Forms Library - MVP Plan
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
A type-safe Vue 3 form library with immutable state management, validation, and subform support. The library uses a direct form instance pattern (no provide/inject) to maintain full type safety throughout the component tree.
|
|
8
|
-
|
|
9
|
-
## Core Architecture
|
|
10
|
-
|
|
11
|
-
### Direct Form Instance Pattern
|
|
12
|
-
- Forms are created with `createForm<T>()` and passed directly to components
|
|
13
|
-
- Full TypeScript type safety maintained throughout the component tree
|
|
14
|
-
- No context loss from provide/inject patterns
|
|
15
|
-
|
|
16
|
-
### Subform Extraction Pattern
|
|
17
|
-
```typescript
|
|
18
|
-
// Reusable components work with subforms
|
|
19
|
-
const addressSubform = form.getSubform('address')
|
|
20
|
-
// addressSubform has type FormInstance<Address>
|
|
21
|
-
|
|
22
|
-
<AddressForm :form="addressSubform" />
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
This allows components to be reusable across different parent forms while maintaining type safety.
|
|
26
|
-
|
|
27
|
-
## API Design
|
|
28
|
-
|
|
29
|
-
### Form Creation
|
|
30
|
-
```typescript
|
|
31
|
-
const form = createForm<UserData>({
|
|
32
|
-
initialData: { name: '', email: '', address: { street: '', city: '' } },
|
|
33
|
-
validationSchema: userSchema,
|
|
34
|
-
strategy: 'onTouch'
|
|
35
|
-
})
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### Form Instance Interface
|
|
39
|
-
```typescript
|
|
40
|
-
interface FormInstance<T> {
|
|
41
|
-
// Data access
|
|
42
|
-
getValue<K extends keyof T>(path: K): T[K]
|
|
43
|
-
setValue<K extends keyof T>(path: K, value: T[K]): void
|
|
44
|
-
|
|
45
|
-
// Field management
|
|
46
|
-
getField<K extends keyof T>(path: K): FieldInstance<T[K]>
|
|
47
|
-
|
|
48
|
-
// Subform extraction
|
|
49
|
-
getSubform<K extends keyof T>(path: K): FormInstance<T[K]>
|
|
50
|
-
|
|
51
|
-
// State queries
|
|
52
|
-
isDirty(): boolean
|
|
53
|
-
isTouched(): boolean
|
|
54
|
-
isValid(): boolean
|
|
55
|
-
|
|
56
|
-
// Operations
|
|
57
|
-
validate(): Promise<ValidationResult>
|
|
58
|
-
reset(): void
|
|
59
|
-
submit(): Promise<void>
|
|
60
|
-
|
|
61
|
-
// Error access
|
|
62
|
-
getErrors(path?: keyof T): ErrorMessage[]
|
|
63
|
-
hasErrors(path?: keyof T): boolean
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Field Instance Interface
|
|
68
|
-
```typescript
|
|
69
|
-
interface FieldInstance<T> {
|
|
70
|
-
// Reactive value with getter/setter
|
|
71
|
-
value: Ref<T>
|
|
72
|
-
|
|
73
|
-
// State
|
|
74
|
-
touched: Ref<boolean>
|
|
75
|
-
dirty: Ref<boolean>
|
|
76
|
-
errors: Ref<ErrorMessage[]>
|
|
77
|
-
|
|
78
|
-
// Handlers
|
|
79
|
-
onBlur(): void
|
|
80
|
-
onFocus(): void
|
|
81
|
-
|
|
82
|
-
// Field-specific validation
|
|
83
|
-
validate(): Promise<ValidationResult>
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Directory Structure
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
src/
|
|
91
|
-
├── types/
|
|
92
|
-
│ ├── form.ts # Form instance types
|
|
93
|
-
│ ├── field.ts # Field instance types
|
|
94
|
-
│ ├── validation.ts # Validation types
|
|
95
|
-
│ └── index.ts
|
|
96
|
-
├── core/
|
|
97
|
-
│ ├── form-instance.ts # Form instance implementation
|
|
98
|
-
│ ├── field-instance.ts # Field instance implementation
|
|
99
|
-
│ ├── subform-instance.ts # Subform implementation
|
|
100
|
-
│ └── validation-engine.ts # Validation logic
|
|
101
|
-
├── factories/
|
|
102
|
-
│ └── create-form.ts # Main form factory
|
|
103
|
-
├── utils/
|
|
104
|
-
│ ├── immutable.ts # Immutable helpers
|
|
105
|
-
│ ├── path.ts # Object path utilities
|
|
106
|
-
│ └── type-helpers.ts # TypeScript utility types
|
|
107
|
-
└── index.ts # Main exports
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Core Features
|
|
111
|
-
|
|
112
|
-
### 1. Immutable State Management
|
|
113
|
-
- Store initial data as immutable
|
|
114
|
-
- Working copy is immutable from outside (readonly)
|
|
115
|
-
- Computed getter/setter on working copy
|
|
116
|
-
- Full clone on creation
|
|
117
|
-
|
|
118
|
-
### 2. Form State Tracking
|
|
119
|
-
- **Touched**: Field has been interacted with at least once
|
|
120
|
-
- **Dirty**: Computed property checking if value ≠ initial state
|
|
121
|
-
- State propagation from subforms to parent forms
|
|
122
|
-
|
|
123
|
-
### 3. Validation System
|
|
124
|
-
- **Strategies**: `onTouch`, `onFormOpen`, `none`, `preSubmit`
|
|
125
|
-
- **Zod Integration**: Primary validation with Zod schemas
|
|
126
|
-
- **Backend Validation**: API errors merged with Zod errors
|
|
127
|
-
- **Error Bag Structure**:
|
|
128
|
-
```typescript
|
|
129
|
-
{
|
|
130
|
-
general: ErrorMessage[],
|
|
131
|
-
propertyErrors: Record<string, ErrorMessage[]>
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### 4. Subform Support
|
|
136
|
-
- Extract subforms with `getSubform(path)`
|
|
137
|
-
- Type-safe subform instances
|
|
138
|
-
- Automatic value propagation from subforms to parent
|
|
139
|
-
- Reusable components across different parent forms
|
|
140
|
-
|
|
141
|
-
### 5. Array Functionality (Future)
|
|
142
|
-
- `pushValue()`, `removeValue()`, `getKeys()`
|
|
143
|
-
- Similar to vee-validate's field array API
|
|
144
|
-
|
|
145
|
-
## MVP Implementation Order
|
|
146
|
-
|
|
147
|
-
### Phase 1: Core Infrastructure
|
|
148
|
-
1. **Project Setup**
|
|
149
|
-
- Package.json with proper exports
|
|
150
|
-
- TypeScript configuration
|
|
151
|
-
- Vite build setup
|
|
152
|
-
- Basic documentation structure
|
|
153
|
-
|
|
154
|
-
2. **Type System**
|
|
155
|
-
- Core interfaces and types
|
|
156
|
-
- Validation types
|
|
157
|
-
- Error handling types
|
|
158
|
-
|
|
159
|
-
3. **Basic Form Instance**
|
|
160
|
-
- Form creation factory
|
|
161
|
-
- Basic field management
|
|
162
|
-
- Immutable state handling
|
|
163
|
-
|
|
164
|
-
### Phase 2: Essential Features
|
|
165
|
-
1. **Field Management**
|
|
166
|
-
- Field instance implementation
|
|
167
|
-
- Touch and dirty state tracking
|
|
168
|
-
- Basic event handlers
|
|
169
|
-
|
|
170
|
-
2. **Validation Engine**
|
|
171
|
-
- Zod integration
|
|
172
|
-
- Error collection and formatting
|
|
173
|
-
- Basic validation strategies
|
|
174
|
-
|
|
175
|
-
3. **Subform System**
|
|
176
|
-
- Subform extraction
|
|
177
|
-
- Type-safe subform instances
|
|
178
|
-
- Value propagation
|
|
179
|
-
|
|
180
|
-
### Phase 3: Polish & Testing
|
|
181
|
-
1. **Edge Cases**
|
|
182
|
-
- Error handling
|
|
183
|
-
- Reset functionality
|
|
184
|
-
- Validation edge cases
|
|
185
|
-
|
|
186
|
-
2. **Documentation**
|
|
187
|
-
- API documentation
|
|
188
|
-
- Usage examples
|
|
189
|
-
- Migration guides
|
|
190
|
-
|
|
191
|
-
3. **Testing**
|
|
192
|
-
- Unit tests
|
|
193
|
-
- Integration tests
|
|
194
|
-
- Type safety tests
|
|
195
|
-
|
|
196
|
-
## Key Benefits
|
|
197
|
-
|
|
198
|
-
- **Type Safety**: Full TypeScript support with no type loss
|
|
199
|
-
- **Immutable State**: Predictable state management
|
|
200
|
-
- **Reusable Components**: Subforms enable component reuse
|
|
201
|
-
- **Validation Flexibility**: Multiple validation strategies
|
|
202
|
-
- **Performance**: Reactive updates only where needed
|
|
203
|
-
- **Developer Experience**: Excellent IDE support and debugging
|
|
204
|
-
|
|
205
|
-
## Notes
|
|
206
|
-
|
|
207
|
-
- No provide/inject pattern to avoid type information loss
|
|
208
|
-
- Direct form instance passing maintains type safety
|
|
209
|
-
- Subform extraction enables component reusability
|
|
210
|
-
- Validation strategies can be overridden per field
|
|
211
|
-
- Backend validation seamlessly integrates with Zod
|