@effect-app/vue-components 4.0.0-beta.235 → 4.0.0-beta.236
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/types/components/OmegaForm/meta/defaults.d.ts +19 -0
- package/dist/types/components/OmegaForm/types.d.ts +6 -0
- package/dist/vue-components15.es.js +68 -16
- package/dist/vue-components33.es.js +1 -1
- package/dist/vue-components68.es.js +85 -67
- package/package.json +3 -3
- package/src/components/OmegaForm/OmegaInternalInput.vue +6 -0
- package/src/components/OmegaForm/meta/defaults.ts +127 -0
- package/src/components/OmegaForm/types.ts +6 -0
- package/src/components/OmegaForm/useOmegaForm.ts +45 -5
|
@@ -1,2 +1,21 @@
|
|
|
1
1
|
import * as S from "effect-app/Schema";
|
|
2
2
|
export declare const defaultsValueFromSchema: (schema: S.Schema<any>, record?: Record<string, any>) => any;
|
|
3
|
+
/**
|
|
4
|
+
* Deep-fills a partial form value with schema defaults for any nullable
|
|
5
|
+
* struct that has **materialised**.
|
|
6
|
+
*
|
|
7
|
+
* A nullable struct (`S.NullOr(S.Struct(...))`) left `null` stays `null`, but
|
|
8
|
+
* once it materialises — e.g. the user filled a single child — its untouched
|
|
9
|
+
* children are filled with their schema default (or `null` when nullable).
|
|
10
|
+
* Without this, strict `S.NullOr(...)` children reject the leftover
|
|
11
|
+
* `undefined` with a spurious "field must not be empty" error.
|
|
12
|
+
*
|
|
13
|
+
* Only children of a struct reached *through a nullable union* are filled;
|
|
14
|
+
* the always-present root struct keeps whatever fields it already has, so the
|
|
15
|
+
* form's own default-value priority is left untouched.
|
|
16
|
+
*
|
|
17
|
+
* Reference-preserving: returns `value` unchanged (same reference) when there
|
|
18
|
+
* is nothing to fill, so callers can detect a no-op with `===` and the result
|
|
19
|
+
* is idempotent (`fill(fill(v)) === fill(v)`).
|
|
20
|
+
*/
|
|
21
|
+
export declare const fillNestedDefaults: (ast: S.AST.AST, value: unknown, fillMissing?: boolean) => unknown;
|
|
@@ -134,6 +134,12 @@ export interface OF<From, To> extends OmegaFormApi<From, To> {
|
|
|
134
134
|
meta: MetaRecord<From>;
|
|
135
135
|
unionMeta: Record<string, MetaRecord<From>>;
|
|
136
136
|
clear: () => void;
|
|
137
|
+
/**
|
|
138
|
+
* Backfills the untouched children of any materialised nullable struct with
|
|
139
|
+
* their schema default (or `null`), so the live `values` matches what
|
|
140
|
+
* validation and decoding see. Called after a field change.
|
|
141
|
+
*/
|
|
142
|
+
normalizeNullableStructs: () => void;
|
|
137
143
|
i18nNamespace?: string;
|
|
138
144
|
ignorePreventCloseEvents?: boolean;
|
|
139
145
|
registerField: (field: ComputedRef<{
|
|
@@ -19,44 +19,96 @@ var a = (e) => {
|
|
|
19
19
|
function s(e) {
|
|
20
20
|
return e && "members" in e && Array.isArray(e.members);
|
|
21
21
|
}
|
|
22
|
-
var c = (
|
|
23
|
-
let
|
|
24
|
-
|
|
22
|
+
var c = (e) => typeof e == "object" && !!e && !Array.isArray(e), l = (e) => n.AST.isNull(e) || n.AST.isUndefined(e), u = (e) => {
|
|
23
|
+
let r = t(e);
|
|
24
|
+
return n.AST.isUnion(r) ? r.types.flatMap(u) : [r];
|
|
25
|
+
}, d = (e) => {
|
|
26
|
+
let r = t(e);
|
|
27
|
+
if (n.AST.isLiteral(r)) return r.literal;
|
|
28
|
+
if (n.AST.isUnion(r) && r.types.length === 1) return d(r.types[0]);
|
|
29
|
+
}, f = (e, t) => {
|
|
30
|
+
if (!(!c(t) || t._tag === void 0)) return e.find((e) => {
|
|
31
|
+
let n = e.propertySignatures.find((e) => e.name.toString() === "_tag");
|
|
32
|
+
return n ? d(n.type) === t._tag : !1;
|
|
33
|
+
});
|
|
34
|
+
}, p = (r, i = {}) => {
|
|
35
|
+
let a = r.ast, c = o(a);
|
|
36
|
+
if (c !== void 0) return c;
|
|
25
37
|
if (e(r.ast) === "null") return null;
|
|
26
38
|
if (e(r.ast) === "undefined") return;
|
|
27
|
-
let
|
|
28
|
-
if (
|
|
39
|
+
let l = n.AST.isObjects(a) ? a : n.AST.isDeclaration(a) ? t(a) : void 0;
|
|
40
|
+
if (l && n.AST.isObjects(l)) {
|
|
29
41
|
let t = {};
|
|
30
|
-
for (let r of
|
|
31
|
-
let a = r.name.toString(), s = r.type,
|
|
32
|
-
if (
|
|
33
|
-
t[a] =
|
|
42
|
+
for (let r of l.propertySignatures) {
|
|
43
|
+
let a = r.name.toString(), s = r.type, c = o(s);
|
|
44
|
+
if (c !== void 0) {
|
|
45
|
+
t[a] = c;
|
|
34
46
|
continue;
|
|
35
47
|
}
|
|
36
|
-
let
|
|
37
|
-
|
|
48
|
+
let l = p(n.make(s), i[a] || {});
|
|
49
|
+
l === void 0 ? e(s) === "undefined" && (t[a] = void 0) : t[a] = l;
|
|
38
50
|
}
|
|
39
51
|
return {
|
|
40
52
|
...t,
|
|
41
53
|
...i
|
|
42
54
|
};
|
|
43
55
|
}
|
|
44
|
-
let
|
|
45
|
-
if (
|
|
56
|
+
let u = n.AST.isUnion(a) ? a.types : s(r) ? r.members.map((e) => e.ast) : void 0;
|
|
57
|
+
if (u) {
|
|
46
58
|
let e = {};
|
|
47
|
-
for (let r of
|
|
59
|
+
for (let r of u) {
|
|
48
60
|
let i = t(r);
|
|
49
61
|
if (n.AST.isObjects(i)) for (let t of i.propertySignatures) {
|
|
50
62
|
let n = t.name.toString(), r = o(t.type), i = e[n] ? o(e[n].ast) : void 0;
|
|
51
63
|
(!e[n] || r !== void 0 && i === void 0) && (e[n] = { ast: t.type });
|
|
52
64
|
}
|
|
53
65
|
}
|
|
54
|
-
return Object.keys(e).length === 0 ? Object.keys(i).length > 0 ? i : void 0 : Object.entries(e).reduce((e, [t, { ast: r }]) => (e[t] =
|
|
66
|
+
return Object.keys(e).length === 0 ? Object.keys(i).length > 0 ? i : void 0 : Object.entries(e).reduce((e, [t, { ast: r }]) => (e[t] = p(n.make(r), i[t] || {}), e), i);
|
|
55
67
|
}
|
|
56
68
|
if (Object.keys(i).length === 0) {
|
|
57
69
|
if (n.AST.isString(a)) return "";
|
|
58
70
|
if (n.AST.isBoolean(a)) return !1;
|
|
59
71
|
}
|
|
72
|
+
}, m = (r, i, a = !1) => {
|
|
73
|
+
let s = t(r);
|
|
74
|
+
switch (s._tag) {
|
|
75
|
+
case "Union": {
|
|
76
|
+
if (i == null) return i;
|
|
77
|
+
let e = u(s), t = e.filter(n.AST.isObjects);
|
|
78
|
+
if (t.length === 0) return i;
|
|
79
|
+
let r = e.some(l), o = f(t, i);
|
|
80
|
+
return o ? m(o, i, a || r) : r && t.length === 1 ? m(t[0], i, !0) : i;
|
|
81
|
+
}
|
|
82
|
+
case "Arrays": {
|
|
83
|
+
if (!Array.isArray(i)) return i;
|
|
84
|
+
let e = s.rest[0];
|
|
85
|
+
if (!e) return i;
|
|
86
|
+
let t = !1, n = i.map((n) => {
|
|
87
|
+
let r = m(e, n, a);
|
|
88
|
+
return r !== n && (t = !0), r;
|
|
89
|
+
});
|
|
90
|
+
return t ? n : i;
|
|
91
|
+
}
|
|
92
|
+
case "Objects": {
|
|
93
|
+
if (!c(i)) return i;
|
|
94
|
+
let t = i, n = !1, r = (e, r) => {
|
|
95
|
+
n ||= (t = { ...i }, !0), t[e] = r;
|
|
96
|
+
};
|
|
97
|
+
for (let t of s.propertySignatures) {
|
|
98
|
+
let n = t.name.toString(), s = i[n];
|
|
99
|
+
if (s !== void 0) {
|
|
100
|
+
let e = m(t.type, s, a);
|
|
101
|
+
e !== s && r(n, e);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!a || t.type.context?.isOptional === !0) continue;
|
|
105
|
+
let c = o(t.type);
|
|
106
|
+
c === void 0 ? e(t.type) === "null" && r(n, null) : r(n, c);
|
|
107
|
+
}
|
|
108
|
+
return t;
|
|
109
|
+
}
|
|
110
|
+
default: return i;
|
|
111
|
+
}
|
|
60
112
|
};
|
|
61
113
|
//#endregion
|
|
62
|
-
export {
|
|
114
|
+
export { p as defaultsValueFromSchema, m as fillNestedDefaults };
|
|
@@ -33,7 +33,7 @@ var b = /* @__PURE__ */ o({
|
|
|
33
33
|
id: D
|
|
34
34
|
})));
|
|
35
35
|
let j = t(() => k.value[b.field.name]?.errors ?? []), M = t(() => j.value.map((e) => e?.message).filter(Boolean)), N = (e) => e == null || e === !1 || e === "" || Number.isNaN(e), P = (e) => {
|
|
36
|
-
N(e) && b.meta?.type !== "boolean" ? b.meta?.nullableOrUndefined ? b.field.handleChange(b.meta.nullableOrUndefined === "undefined" ? void 0 : null) : b.meta?.isOptionalKey ? b.field.form.deleteField(b.field.name) : b.field.handleChange(e) : b.field.handleChange(e);
|
|
36
|
+
N(e) && b.meta?.type !== "boolean" ? b.meta?.nullableOrUndefined ? b.field.handleChange(b.meta.nullableOrUndefined === "undefined" ? void 0 : null) : b.meta?.isOptionalKey ? b.field.form.deleteField(b.field.name) : b.field.handleChange(e) : b.field.handleChange(e), b.field.form.normalizeNullableStructs?.();
|
|
37
37
|
}, F = (e) => new Proxy(e, { get(e, t, n) {
|
|
38
38
|
return t === "handleChange" ? P : Reflect.get(...arguments);
|
|
39
39
|
} }), I = t(() => ({
|
|
@@ -1,82 +1,100 @@
|
|
|
1
1
|
import { useIntl as e } from "./vue-components6.es.js";
|
|
2
|
-
import { generateMetaFromSchema as t } from "./vue-components14.es.js";
|
|
3
|
-
import { defaultsValueFromSchema as
|
|
4
|
-
import { toFormSchema as
|
|
5
|
-
import { usePersistency as
|
|
6
|
-
import { annotateLiteralUnionMessages as
|
|
7
|
-
import { eHoc as
|
|
8
|
-
import { fHoc as
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import { makeSubmitHandlers as
|
|
16
|
-
import { watch as
|
|
17
|
-
import * as
|
|
18
|
-
import { revalidateLogic as
|
|
19
|
-
import * as
|
|
20
|
-
import { makeRunPromise as
|
|
2
|
+
import { generateMetaFromSchema as t, unwrapDeclaration as n } from "./vue-components14.es.js";
|
|
3
|
+
import { defaultsValueFromSchema as r, fillNestedDefaults as i } from "./vue-components15.es.js";
|
|
4
|
+
import { toFormSchema as a } from "./vue-components16.es.js";
|
|
5
|
+
import { usePersistency as o } from "./vue-components18.es.js";
|
|
6
|
+
import { annotateLiteralUnionMessages as s, toLocalizedStandardSchemaV1 as c } from "./vue-components19.es.js";
|
|
7
|
+
import { eHoc as l, makeFieldMap as u } from "./vue-components20.es.js";
|
|
8
|
+
import { fHoc as d } from "./vue-components21.es.js";
|
|
9
|
+
import f from "./vue-components23.es.js";
|
|
10
|
+
import p from "./vue-components25.es.js";
|
|
11
|
+
import m from "./vue-components29.es.js";
|
|
12
|
+
import h from "./vue-components36.es.js";
|
|
13
|
+
import g from "./vue-components40.es.js";
|
|
14
|
+
import _ from "./vue-components44.es.js";
|
|
15
|
+
import { makeSubmitHandlers as v, wrapOnSubmit as y } from "./vue-components67.es.js";
|
|
16
|
+
import { watch as b } from "vue";
|
|
17
|
+
import * as x from "effect-app/Schema";
|
|
18
|
+
import { revalidateLogic as S, useForm as C } from "@tanstack/vue-form";
|
|
19
|
+
import * as w from "effect-app/Context";
|
|
20
|
+
import { makeRunPromise as T } from "@effect-app/vue/runtime";
|
|
21
21
|
//#region src/components/OmegaForm/useOmegaForm.ts
|
|
22
|
-
var
|
|
23
|
-
if (!
|
|
24
|
-
let { trans:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
var E = Symbol("OmegaForm"), D = T(w.empty()), O = (w, T, E) => {
|
|
23
|
+
if (!w) throw Error("Schema is required");
|
|
24
|
+
let { trans: O } = e(), k = a(w), A = s(k, O), j = n(k.ast), M = (e) => i(j, e), N = c(A, O), P = {
|
|
25
|
+
...N,
|
|
26
|
+
"~standard": {
|
|
27
|
+
...N["~standard"],
|
|
28
|
+
validate: (e) => N["~standard"].validate(M(e))
|
|
29
|
+
}
|
|
30
|
+
}, F = x.decodeUnknownEffectConcurrently(k), I = (e) => F(M(e)), { meta: L, unionMeta: R } = t(k), z = { form: void 0 }, B = o({
|
|
31
|
+
meta: L,
|
|
32
|
+
persistency: E?.persistency,
|
|
33
|
+
preventWindowExit: E?.preventWindowExit,
|
|
34
|
+
defaultValuesPriority: E?.defaultValuesPriority,
|
|
35
|
+
tanstackDefaultValues: T?.defaultValues,
|
|
36
|
+
schemaDefaultValues: () => r(w),
|
|
37
|
+
getForm: () => z.form
|
|
38
|
+
}), V = C({
|
|
39
|
+
...T,
|
|
40
|
+
validationLogic: S(),
|
|
35
41
|
validators: {
|
|
36
|
-
onDynamic:
|
|
37
|
-
...
|
|
42
|
+
onDynamic: P,
|
|
43
|
+
...T?.validators
|
|
38
44
|
},
|
|
39
|
-
onSubmit:
|
|
40
|
-
defaultValues:
|
|
45
|
+
onSubmit: y(T?.onSubmit, I, D),
|
|
46
|
+
defaultValues: B.defaultValues.value
|
|
41
47
|
});
|
|
42
|
-
|
|
43
|
-
let
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
z.form = V;
|
|
49
|
+
let H = () => {
|
|
50
|
+
let e = V.state.values, t = i(j, e);
|
|
51
|
+
if (t === e || !t || typeof t != "object") return;
|
|
52
|
+
let n = e, r = t;
|
|
53
|
+
for (let e of Object.keys(r)) if (r[e] !== n[e]) {
|
|
54
|
+
let t = e;
|
|
55
|
+
V.setFieldValue(t, r[e], {
|
|
56
|
+
dontUpdateMeta: !0,
|
|
57
|
+
dontValidate: !0
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}, U = () => {
|
|
61
|
+
Object.keys(L).forEach((e) => {
|
|
62
|
+
V.setFieldValue(e, void 0);
|
|
46
63
|
});
|
|
47
64
|
};
|
|
48
|
-
if (
|
|
49
|
-
let e =
|
|
50
|
-
|
|
51
|
-
i && !e && t > 0 && n.value &&
|
|
65
|
+
if (E?.preventWindowExit === "prevent-and-reset") {
|
|
66
|
+
let e = V.useStore((e) => e.isSubmitting), t = V.useStore((e) => e.submissionAttempts), n = V.useStore((e) => e.canSubmit), r = V.useStore((e) => e.values);
|
|
67
|
+
b([e, t], ([e, t], [i]) => {
|
|
68
|
+
i && !e && t > 0 && n.value && V.reset(r.value);
|
|
52
69
|
});
|
|
53
70
|
}
|
|
54
|
-
let { handleSubmit:
|
|
55
|
-
i18nNamespace:
|
|
56
|
-
ignorePreventCloseEvents:
|
|
57
|
-
meta:
|
|
58
|
-
unionMeta:
|
|
59
|
-
clear:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
let { handleSubmit: W, handleSubmitEffect: G } = v(V), { fieldMap: K, registerField: q } = u(), J = Object.assign(V, {
|
|
72
|
+
i18nNamespace: E?.i18nNamespace,
|
|
73
|
+
ignorePreventCloseEvents: E?.ignorePreventCloseEvents,
|
|
74
|
+
meta: L,
|
|
75
|
+
unionMeta: R,
|
|
76
|
+
clear: U,
|
|
77
|
+
normalizeNullableStructs: H,
|
|
78
|
+
handleSubmit: W,
|
|
79
|
+
handleSubmitEffect: G,
|
|
80
|
+
registerField: q
|
|
81
|
+
}), Y = {
|
|
82
|
+
form: J,
|
|
83
|
+
fieldMap: K
|
|
66
84
|
};
|
|
67
|
-
return Object.assign(
|
|
85
|
+
return Object.assign(J, {
|
|
68
86
|
_paths: void 0,
|
|
69
87
|
_keys: void 0,
|
|
70
|
-
_schema:
|
|
71
|
-
errorContext:
|
|
72
|
-
Form:
|
|
73
|
-
Input:
|
|
74
|
-
TaggedUnion:
|
|
75
|
-
Field:
|
|
76
|
-
Errors:
|
|
77
|
-
Array:
|
|
78
|
-
AutoGen:
|
|
88
|
+
_schema: w,
|
|
89
|
+
errorContext: Y,
|
|
90
|
+
Form: d(J)(_),
|
|
91
|
+
Input: d(J)(E?.input ?? h),
|
|
92
|
+
TaggedUnion: d(J)(g),
|
|
93
|
+
Field: V.Field,
|
|
94
|
+
Errors: l(Y)(m),
|
|
95
|
+
Array: d(J)(f),
|
|
96
|
+
AutoGen: d(J)(p)
|
|
79
97
|
});
|
|
80
98
|
};
|
|
81
99
|
//#endregion
|
|
82
|
-
export {
|
|
100
|
+
export { E as OmegaFormKey, O as useOmegaForm };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-app/vue-components",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.236",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"@mdi/js": "^7.4.47",
|
|
6
6
|
"effect": "^4.0.0-beta.68",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"highlight.js": "^11.11.1",
|
|
58
58
|
"mitt": "^3.0.1",
|
|
59
59
|
"vue3-highlightjs": "^1.0.5",
|
|
60
|
-
"@effect-app/vue": "4.0.0-beta.
|
|
61
|
-
"effect-app": "4.0.0-beta.
|
|
60
|
+
"@effect-app/vue": "4.0.0-beta.236",
|
|
61
|
+
"effect-app": "4.0.0-beta.236"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"check": "vue-tsc",
|
|
@@ -145,6 +145,12 @@ const handleChange: OmegaFieldInternalApi<From, Name>["handleChange"] = (value)
|
|
|
145
145
|
} else {
|
|
146
146
|
props.field.handleChange(value)
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
// A change here may have materialised a nullable struct (this field is one
|
|
150
|
+
// of its children); backfill the untouched siblings so the live `values`
|
|
151
|
+
// matches what validation and decoding see.
|
|
152
|
+
const form = props.field.form as { normalizeNullableStructs?: () => void }
|
|
153
|
+
form.normalizeNullableStructs?.()
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
// Note: Default value normalization (converting empty strings to null/undefined for nullable fields)
|
|
@@ -33,10 +33,44 @@ type SchemaWithMembers = {
|
|
|
33
33
|
members: readonly S.Schema<any>[]
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
type UnknownRecord = Record<string, unknown>
|
|
37
|
+
|
|
36
38
|
function hasMembers(schema: any): schema is SchemaWithMembers {
|
|
37
39
|
return schema && "members" in schema && Array.isArray(schema.members)
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
const isRecord = (value: unknown): value is UnknownRecord =>
|
|
43
|
+
value !== null && typeof value === "object" && !Array.isArray(value)
|
|
44
|
+
|
|
45
|
+
const isNullishAst = (ast: S.AST.AST) => S.AST.isNull(ast) || S.AST.isUndefined(ast)
|
|
46
|
+
|
|
47
|
+
const unionMembers = (ast: S.AST.AST): readonly S.AST.AST[] => {
|
|
48
|
+
const resolved = unwrapDeclaration(ast)
|
|
49
|
+
return S.AST.isUnion(resolved)
|
|
50
|
+
? resolved.types.flatMap(unionMembers)
|
|
51
|
+
: [resolved]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const literalValue = (ast: S.AST.AST): unknown => {
|
|
55
|
+
const resolved = unwrapDeclaration(ast)
|
|
56
|
+
if (S.AST.isLiteral(resolved)) return resolved.literal
|
|
57
|
+
if (S.AST.isUnion(resolved) && resolved.types.length === 1) {
|
|
58
|
+
return literalValue(resolved.types[0])
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const findTaggedObjectMember = (
|
|
63
|
+
members: readonly S.AST.Objects[],
|
|
64
|
+
value: unknown
|
|
65
|
+
): S.AST.Objects | undefined => {
|
|
66
|
+
if (!isRecord(value) || value._tag === undefined) return undefined
|
|
67
|
+
|
|
68
|
+
return members.find((member) => {
|
|
69
|
+
const tagProp = member.propertySignatures.find((prop) => prop.name.toString() === "_tag")
|
|
70
|
+
return tagProp ? literalValue(tagProp.type) === value._tag : false
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
40
74
|
// Internal implementation with WeakSet tracking
|
|
41
75
|
export const defaultsValueFromSchema = (
|
|
42
76
|
schema: S.Schema<any>,
|
|
@@ -132,3 +166,96 @@ export const defaultsValueFromSchema = (
|
|
|
132
166
|
}
|
|
133
167
|
}
|
|
134
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Deep-fills a partial form value with schema defaults for any nullable
|
|
172
|
+
* struct that has **materialised**.
|
|
173
|
+
*
|
|
174
|
+
* A nullable struct (`S.NullOr(S.Struct(...))`) left `null` stays `null`, but
|
|
175
|
+
* once it materialises — e.g. the user filled a single child — its untouched
|
|
176
|
+
* children are filled with their schema default (or `null` when nullable).
|
|
177
|
+
* Without this, strict `S.NullOr(...)` children reject the leftover
|
|
178
|
+
* `undefined` with a spurious "field must not be empty" error.
|
|
179
|
+
*
|
|
180
|
+
* Only children of a struct reached *through a nullable union* are filled;
|
|
181
|
+
* the always-present root struct keeps whatever fields it already has, so the
|
|
182
|
+
* form's own default-value priority is left untouched.
|
|
183
|
+
*
|
|
184
|
+
* Reference-preserving: returns `value` unchanged (same reference) when there
|
|
185
|
+
* is nothing to fill, so callers can detect a no-op with `===` and the result
|
|
186
|
+
* is idempotent (`fill(fill(v)) === fill(v)`).
|
|
187
|
+
*/
|
|
188
|
+
export const fillNestedDefaults = (
|
|
189
|
+
ast: S.AST.AST,
|
|
190
|
+
value: unknown,
|
|
191
|
+
fillMissing = false
|
|
192
|
+
): unknown => {
|
|
193
|
+
const resolved = unwrapDeclaration(ast)
|
|
194
|
+
|
|
195
|
+
switch (resolved._tag) {
|
|
196
|
+
case "Union": {
|
|
197
|
+
if (value === null || value === undefined) return value
|
|
198
|
+
const members = unionMembers(resolved)
|
|
199
|
+
const objectMembers = members.filter(S.AST.isObjects)
|
|
200
|
+
if (objectMembers.length === 0) return value
|
|
201
|
+
|
|
202
|
+
const hasNullishMember = members.some(isNullishAst)
|
|
203
|
+
const taggedMember = findTaggedObjectMember(objectMembers, value)
|
|
204
|
+
if (taggedMember) {
|
|
205
|
+
return fillNestedDefaults(taggedMember, value, fillMissing || hasNullishMember)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (hasNullishMember && objectMembers.length === 1) {
|
|
209
|
+
return fillNestedDefaults(objectMembers[0], value, true)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return value
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
case "Arrays": {
|
|
216
|
+
if (!Array.isArray(value)) return value
|
|
217
|
+
const element = resolved.rest[0]
|
|
218
|
+
if (!element) return value
|
|
219
|
+
let changed = false
|
|
220
|
+
const next = value.map((item) => {
|
|
221
|
+
const filled = fillNestedDefaults(element, item, fillMissing)
|
|
222
|
+
if (filled !== item) changed = true
|
|
223
|
+
return filled
|
|
224
|
+
})
|
|
225
|
+
return changed ? next : value
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case "Objects": {
|
|
229
|
+
if (!isRecord(value)) return value
|
|
230
|
+
let result: UnknownRecord = value
|
|
231
|
+
let changed = false
|
|
232
|
+
const set = (key: string, next: unknown) => {
|
|
233
|
+
if (!changed) {
|
|
234
|
+
result = { ...value }
|
|
235
|
+
changed = true
|
|
236
|
+
}
|
|
237
|
+
result[key] = next
|
|
238
|
+
}
|
|
239
|
+
for (const prop of resolved.propertySignatures) {
|
|
240
|
+
const key = prop.name.toString()
|
|
241
|
+
const current = value[key]
|
|
242
|
+
if (current !== undefined) {
|
|
243
|
+
const filled = fillNestedDefaults(prop.type, current, fillMissing)
|
|
244
|
+
if (filled !== current) set(key, filled)
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
if (!fillMissing || prop.type.context?.isOptional === true) continue
|
|
248
|
+
const propDefault = getDefaultFromAst(prop.type)
|
|
249
|
+
if (propDefault !== undefined) {
|
|
250
|
+
set(key, propDefault)
|
|
251
|
+
} else if (isNullableOrUndefined(prop.type) === "null") {
|
|
252
|
+
set(key, null)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return result
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
default:
|
|
259
|
+
return value
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -295,6 +295,12 @@ export interface OF<From, To> extends OmegaFormApi<From, To> {
|
|
|
295
295
|
meta: MetaRecord<From>
|
|
296
296
|
unionMeta: Record<string, MetaRecord<From>>
|
|
297
297
|
clear: () => void
|
|
298
|
+
/**
|
|
299
|
+
* Backfills the untouched children of any materialised nullable struct with
|
|
300
|
+
* their schema default (or `null`), so the live `values` matches what
|
|
301
|
+
* validation and decoding see. Called after a field change.
|
|
302
|
+
*/
|
|
303
|
+
normalizeNullableStructs: () => void
|
|
298
304
|
i18nNamespace?: string
|
|
299
305
|
ignorePreventCloseEvents?: boolean
|
|
300
306
|
registerField: (
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
3
|
|
|
4
|
-
import { type FormAsyncValidateOrFn, type FormValidateOrFn, revalidateLogic, type StandardSchemaV1, useForm } from "@tanstack/vue-form"
|
|
4
|
+
import { type DeepKeys, type DeepValue, type FormAsyncValidateOrFn, type FormValidateOrFn, revalidateLogic, type StandardSchemaV1, useForm } from "@tanstack/vue-form"
|
|
5
5
|
import * as Context from "effect-app/Context"
|
|
6
6
|
import * as S from "effect-app/Schema"
|
|
7
7
|
import { type InjectionKey, watch } from "vue"
|
|
8
8
|
import { eHoc, makeFieldMap } from "./errors"
|
|
9
9
|
import { fHoc } from "./hocs"
|
|
10
|
-
import { generateMetaFromSchema } from "./meta/createMeta"
|
|
11
|
-
import { defaultsValueFromSchema } from "./meta/defaults"
|
|
10
|
+
import { generateMetaFromSchema, unwrapDeclaration } from "./meta/createMeta"
|
|
11
|
+
import { defaultsValueFromSchema, fillNestedDefaults } from "./meta/defaults"
|
|
12
12
|
import { toFormSchema } from "./meta/redacted"
|
|
13
13
|
import OmegaArray from "./OmegaArray.vue"
|
|
14
14
|
import OmegaAutoGen from "./OmegaAutoGen.vue"
|
|
@@ -51,11 +51,29 @@ export const useOmegaForm = <
|
|
|
51
51
|
// (select) and literal-array (multiple) AST nodes with a localized
|
|
52
52
|
// `message` so the formatter picks them up via `findMessage`.
|
|
53
53
|
const localizedSchema = annotateLiteralUnionMessages(formCompatibleSchema, trans)
|
|
54
|
-
|
|
54
|
+
|
|
55
|
+
// A nullable struct (`S.NullOr(S.Struct(...))`) has no slot in the form
|
|
56
|
+
// value until a child is filled. Once it materialises, its untouched
|
|
57
|
+
// children are still `undefined` — which strict `S.NullOr(...)` children
|
|
58
|
+
// reject. Deep-fill those children with their defaults before both
|
|
59
|
+
// validation and decoding so they validate as `null` (or their default).
|
|
60
|
+
const formAst = unwrapDeclaration(formCompatibleSchema.ast)
|
|
61
|
+
const normalizeFormValue = (value: unknown) => fillNestedDefaults(formAst, value)
|
|
62
|
+
|
|
63
|
+
const baseStandardSchema = toLocalizedStandardSchemaV1(
|
|
55
64
|
localizedSchema as any,
|
|
56
65
|
trans
|
|
57
66
|
)
|
|
58
|
-
const
|
|
67
|
+
const standardSchema: typeof baseStandardSchema = {
|
|
68
|
+
...baseStandardSchema,
|
|
69
|
+
"~standard": {
|
|
70
|
+
...baseStandardSchema["~standard"],
|
|
71
|
+
validate: (value) => baseStandardSchema["~standard"].validate(normalizeFormValue(value))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const baseDecode = S.decodeUnknownEffectConcurrently(formCompatibleSchema)
|
|
76
|
+
const decode = (value: From) => baseDecode(normalizeFormValue(value))
|
|
59
77
|
|
|
60
78
|
const { meta, unionMeta } = generateMetaFromSchema(formCompatibleSchema)
|
|
61
79
|
|
|
@@ -100,6 +118,27 @@ export const useOmegaForm = <
|
|
|
100
118
|
}) satisfies OmegaFormApi<To, From>
|
|
101
119
|
formHolder.form = form
|
|
102
120
|
|
|
121
|
+
// Keep the live form state consistent with the schema: when a nullable
|
|
122
|
+
// struct materialises (a child got a value), backfill its untouched
|
|
123
|
+
// children so `values` reflects what is validated and submitted. Called
|
|
124
|
+
// from the field change handler — the only point a struct can materialise.
|
|
125
|
+
const normalizeNullableStructs = () => {
|
|
126
|
+
const current = form.state.values
|
|
127
|
+
const filled = fillNestedDefaults(formAst, current)
|
|
128
|
+
if (filled === current || !filled || typeof filled !== "object") return
|
|
129
|
+
const currentRecord = current as Partial<Record<Extract<keyof From, string>, unknown>>
|
|
130
|
+
const filledRecord = filled as Partial<Record<Extract<keyof From, string>, unknown>>
|
|
131
|
+
for (const key of Object.keys(filledRecord) as Array<Extract<keyof From, string>>) {
|
|
132
|
+
if (filledRecord[key] !== currentRecord[key]) {
|
|
133
|
+
const field = key as DeepKeys<From>
|
|
134
|
+
form.setFieldValue(field, filledRecord[key] as DeepValue<From, typeof field>, {
|
|
135
|
+
dontUpdateMeta: true,
|
|
136
|
+
dontValidate: true
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
103
142
|
const clear = () => {
|
|
104
143
|
Object.keys(meta).forEach((key: any) => {
|
|
105
144
|
form.setFieldValue(key, undefined as any)
|
|
@@ -134,6 +173,7 @@ export const useOmegaForm = <
|
|
|
134
173
|
meta,
|
|
135
174
|
unionMeta,
|
|
136
175
|
clear,
|
|
176
|
+
normalizeNullableStructs,
|
|
137
177
|
handleSubmit,
|
|
138
178
|
// /** @experimental */
|
|
139
179
|
handleSubmitEffect,
|