@vielzeug/forge 2.3.1 → 3.0.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/README.md +17 -16
- package/dist/_field.cjs +1 -1
- package/dist/_field.cjs.map +1 -1
- package/dist/_field.d.ts +13 -10
- package/dist/_field.d.ts.map +1 -1
- package/dist/_field.js +33 -33
- package/dist/_field.js.map +1 -1
- package/dist/_notify.cjs +1 -1
- package/dist/_notify.cjs.map +1 -1
- package/dist/_notify.d.ts +6 -5
- package/dist/_notify.d.ts.map +1 -1
- package/dist/_notify.js +22 -8
- package/dist/_notify.js.map +1 -1
- package/dist/core/path.cjs +1 -1
- package/dist/core/path.cjs.map +1 -1
- package/dist/core/path.d.ts +8 -11
- package/dist/core/path.d.ts.map +1 -1
- package/dist/core/path.js +122 -110
- package/dist/core/path.js.map +1 -1
- package/dist/dom.cjs +1 -1
- package/dist/dom.cjs.map +1 -1
- package/dist/dom.d.ts +5 -6
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +7 -7
- package/dist/dom.js.map +1 -1
- package/dist/errors.cjs +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +6 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -2
- package/dist/errors.js.map +1 -1
- package/dist/forge.cjs +1 -1
- package/dist/forge.cjs.map +1 -1
- package/dist/forge.iife.js +1 -1
- package/dist/forge.iife.js.map +1 -1
- package/dist/forge.js +1 -1
- package/dist/forge.js.map +1 -1
- package/dist/form-data.cjs +1 -1
- package/dist/form-data.cjs.map +1 -1
- package/dist/form-data.d.ts +2 -2
- package/dist/form-data.d.ts.map +1 -1
- package/dist/form-data.js +23 -16
- package/dist/form-data.js.map +1 -1
- package/dist/form.cjs +1 -1
- package/dist/form.cjs.map +1 -1
- package/dist/form.d.ts +1 -1
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +122 -86
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/persist.cjs +2 -0
- package/dist/persist.cjs.map +1 -0
- package/dist/persist.d.ts +18 -0
- package/dist/persist.d.ts.map +1 -0
- package/dist/persist.js +19 -0
- package/dist/persist.js.map +1 -0
- package/dist/schema.cjs +2 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.ts +22 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +26 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +24 -20
- package/dist/types.d.ts.map +1 -1
- package/package.json +25 -33
- package/dist/spell.cjs +0 -2
- package/dist/spell.cjs.map +0 -1
- package/dist/spell.d.ts +0 -5
- package/dist/spell.d.ts.map +0 -1
- package/dist/spell.js +0 -48
- package/dist/spell.js.map +0 -1
- package/dist/vault.cjs +0 -2
- package/dist/vault.cjs.map +0 -1
- package/dist/vault.d.ts +0 -10
- package/dist/vault.d.ts.map +0 -1
- package/dist/vault.js +0 -14
- package/dist/vault.js.map +0 -1
package/dist/form.js
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import { ForgeConfigError as e, ForgeDisposedError as t, ForgeSubmitError as n, ForgeValidationError as r } from "./errors.js";
|
|
2
|
-
import { assertSafeKey as i, immutable as a, isRecord as o,
|
|
2
|
+
import { assertSafeKey as i, immutable as a, isRecord as o, readAtPath as s, resetAtPath as c, touchAll as l, writeAtPath as u, writeMeta as d } from "./core/path.js";
|
|
3
3
|
import { createField as f } from "./_field.js";
|
|
4
4
|
import { createNotifier as p } from "./_notify.js";
|
|
5
|
+
import { abortable as m } from "@vielzeug/arsenal";
|
|
5
6
|
//#region src/form.ts
|
|
6
|
-
function
|
|
7
|
+
function h(t) {
|
|
8
|
+
if (t !== void 0 && t.length !== 0) return Object.freeze(t.map((t) => {
|
|
9
|
+
if (!t || typeof t.message != "string" || !Array.isArray(t.path)) throw new e("Validators must return issues with a string message and array path.");
|
|
10
|
+
let n = t.path.map((t) => {
|
|
11
|
+
if (typeof t == "number") {
|
|
12
|
+
if (!Number.isSafeInteger(t) || t < 0) throw new e(`Invalid issue path index ${t}.`);
|
|
13
|
+
return t;
|
|
14
|
+
}
|
|
15
|
+
if (typeof t == "string") return i(t), t;
|
|
16
|
+
throw new e("Issue paths may contain only string keys and non-negative integer indexes.");
|
|
17
|
+
});
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
message: t.message,
|
|
20
|
+
path: Object.freeze(n)
|
|
21
|
+
});
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
function g(e) {
|
|
7
25
|
return Object.freeze({
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
26
|
+
formError: e.issues?.find((e) => e.path.length === 0)?.message,
|
|
27
|
+
hasErrors: (e.issues?.length ?? 0) > 0,
|
|
28
|
+
issues: e.issues,
|
|
11
29
|
submitCount: e.submitCount,
|
|
12
30
|
submitting: e.isSubmitting,
|
|
13
31
|
touched: Object.keys(e.touched).length > 0,
|
|
@@ -15,34 +33,41 @@ function m(e) {
|
|
|
15
33
|
validity: e.validity
|
|
16
34
|
});
|
|
17
35
|
}
|
|
18
|
-
function
|
|
19
|
-
let
|
|
20
|
-
baseline:
|
|
21
|
-
errors: void 0,
|
|
22
|
-
formError: void 0,
|
|
36
|
+
function _(_) {
|
|
37
|
+
let v = a(_.initialValues), y = p(_.onSubscriberError), b = new AbortController(), x = !1, S = !1, C = [], w, T = 0, E, D = {
|
|
38
|
+
baseline: v,
|
|
23
39
|
isSubmitting: !1,
|
|
40
|
+
issues: void 0,
|
|
24
41
|
isValidating: !1,
|
|
25
42
|
submitCount: 0,
|
|
26
43
|
touched: {},
|
|
27
44
|
validity: "unknown",
|
|
28
|
-
value:
|
|
29
|
-
};
|
|
30
|
-
function
|
|
31
|
-
if (
|
|
45
|
+
value: v
|
|
46
|
+
}, O = g(D);
|
|
47
|
+
function k(e) {
|
|
48
|
+
if (x) throw new t(e);
|
|
32
49
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
50
|
+
function A(e) {
|
|
51
|
+
if (C.push(e), !S) {
|
|
52
|
+
S = !0;
|
|
53
|
+
try {
|
|
54
|
+
for (; C.length > 0;) D = Object.freeze(C.shift()(D)), O = g(D), y.notify(D);
|
|
55
|
+
} finally {
|
|
56
|
+
S = !1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
35
59
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
60
|
+
function j() {
|
|
61
|
+
w?.abort();
|
|
38
62
|
}
|
|
39
|
-
let
|
|
40
|
-
abortValidation:
|
|
41
|
-
addListener:
|
|
42
|
-
ensureActive:
|
|
43
|
-
|
|
63
|
+
let M = {
|
|
64
|
+
abortValidation: j,
|
|
65
|
+
addListener: y.add,
|
|
66
|
+
ensureActive: k,
|
|
67
|
+
invokeListener: y.call,
|
|
68
|
+
readState: () => D,
|
|
44
69
|
resetValue(e) {
|
|
45
|
-
|
|
70
|
+
A((t) => (T++, E = void 0, {
|
|
46
71
|
...t,
|
|
47
72
|
touched: d(t.touched, e, !1),
|
|
48
73
|
validity: "unknown",
|
|
@@ -50,138 +75,149 @@ function h(h) {
|
|
|
50
75
|
}));
|
|
51
76
|
},
|
|
52
77
|
setTouched(e, t) {
|
|
53
|
-
|
|
78
|
+
A((n) => ({
|
|
54
79
|
...n,
|
|
55
80
|
touched: d(n.touched, e, t)
|
|
56
81
|
}));
|
|
57
82
|
},
|
|
58
83
|
setValue(e, t) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
A((n) => {
|
|
85
|
+
let r = s(n.value, e), i = typeof t == "function" ? t(r) : t;
|
|
86
|
+
return T++, E = void 0, {
|
|
87
|
+
...n,
|
|
88
|
+
validity: "unknown",
|
|
89
|
+
value: u(n.value, e, i)
|
|
90
|
+
};
|
|
91
|
+
});
|
|
64
92
|
}
|
|
65
93
|
};
|
|
66
|
-
async function
|
|
67
|
-
|
|
94
|
+
async function N(e) {
|
|
95
|
+
k("validate"), j();
|
|
68
96
|
let t = new AbortController(), n = e ? AbortSignal.any([t.signal, e]) : t.signal;
|
|
69
|
-
|
|
97
|
+
if (n.aborted) return Object.freeze({ status: "aborted" });
|
|
98
|
+
let i = D.value, a = T;
|
|
99
|
+
w = t, E = void 0, A((e) => ({
|
|
70
100
|
...e,
|
|
71
101
|
isValidating: !0
|
|
72
102
|
}));
|
|
73
103
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
if (n.aborted || T !== a) return Object.freeze({ status: "aborted" });
|
|
105
|
+
let e = _.validate ? await m(Promise.resolve(_.validate(i, n)), n) : void 0;
|
|
106
|
+
if (n.aborted || w !== t || T !== a) return Object.freeze({ status: "aborted" });
|
|
107
|
+
let r = h(e), o = r === void 0 ? "valid" : "invalid";
|
|
108
|
+
return w = void 0, A((e) => ({
|
|
78
109
|
...e,
|
|
79
|
-
|
|
80
|
-
|
|
110
|
+
issues: r,
|
|
111
|
+
isValidating: !1,
|
|
81
112
|
validity: o
|
|
82
|
-
})),
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
})), n.aborted || T !== a || w !== void 0 ? Object.freeze({ status: "aborted" }) : (E = o === "valid" ? {
|
|
114
|
+
revision: T,
|
|
115
|
+
value: i
|
|
116
|
+
} : void 0, Object.freeze(o === "valid" ? { status: "valid" } : {
|
|
117
|
+
issues: r,
|
|
85
118
|
status: "invalid"
|
|
86
|
-
});
|
|
119
|
+
}));
|
|
87
120
|
} catch (e) {
|
|
88
|
-
if (n.aborted ||
|
|
121
|
+
if (n.aborted || w !== t) return Object.freeze({ status: "aborted" });
|
|
89
122
|
throw new r("Form validation failed.", { cause: e });
|
|
90
123
|
} finally {
|
|
91
|
-
|
|
124
|
+
w === t && (w = void 0, x || A((e) => ({
|
|
92
125
|
...e,
|
|
93
126
|
isValidating: !1
|
|
94
127
|
})));
|
|
95
128
|
}
|
|
96
129
|
}
|
|
97
|
-
let
|
|
130
|
+
let P = {
|
|
98
131
|
get disposalSignal() {
|
|
99
|
-
return
|
|
132
|
+
return b.signal;
|
|
100
133
|
},
|
|
101
134
|
dispose() {
|
|
102
|
-
|
|
135
|
+
x || (x = !0, C.length = 0, E = void 0, b.abort(), j(), y.clear());
|
|
103
136
|
},
|
|
104
137
|
get disposed() {
|
|
105
|
-
return
|
|
138
|
+
return x;
|
|
106
139
|
},
|
|
107
140
|
field(t) {
|
|
108
|
-
if (i(t), !o(
|
|
109
|
-
return f([t],
|
|
141
|
+
if (i(t), !o(D.value)) throw new e("Form value must be an object.");
|
|
142
|
+
return f([t], M);
|
|
110
143
|
},
|
|
111
144
|
reset(e) {
|
|
112
|
-
|
|
113
|
-
let t = e === void 0 ?
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
145
|
+
k("reset"), j();
|
|
146
|
+
let t = e === void 0 ? void 0 : a(e);
|
|
147
|
+
A((e) => {
|
|
148
|
+
let n = t ?? e.baseline;
|
|
149
|
+
return T++, E = void 0, {
|
|
150
|
+
...e,
|
|
151
|
+
baseline: n,
|
|
152
|
+
issues: void 0,
|
|
153
|
+
touched: {},
|
|
154
|
+
validity: "unknown",
|
|
155
|
+
value: n
|
|
156
|
+
};
|
|
157
|
+
});
|
|
123
158
|
},
|
|
124
159
|
set(e) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
})
|
|
160
|
+
k("set"), j(), A((t) => {
|
|
161
|
+
let n = typeof e == "function" ? e(t.value) : e;
|
|
162
|
+
return T++, E = void 0, {
|
|
163
|
+
...t,
|
|
164
|
+
issues: void 0,
|
|
165
|
+
validity: "unknown",
|
|
166
|
+
value: a(n)
|
|
167
|
+
};
|
|
168
|
+
});
|
|
134
169
|
},
|
|
135
170
|
get state() {
|
|
136
|
-
return
|
|
171
|
+
return O;
|
|
137
172
|
},
|
|
138
173
|
async submit(e, t) {
|
|
139
|
-
if (
|
|
140
|
-
let r = t ? AbortSignal.any([
|
|
141
|
-
|
|
174
|
+
if (k("submit"), D.isSubmitting) throw new n("submit() called while a submission is already in progress");
|
|
175
|
+
let r = t ? AbortSignal.any([b.signal, t]) : b.signal;
|
|
176
|
+
A((e) => ({
|
|
142
177
|
...e,
|
|
143
178
|
isSubmitting: !0,
|
|
144
179
|
submitCount: e.submitCount + 1,
|
|
145
180
|
touched: l(e.value)
|
|
146
181
|
}));
|
|
147
182
|
try {
|
|
148
|
-
let n = await
|
|
183
|
+
let n = await N(t);
|
|
149
184
|
if (n.status === "aborted") return Object.freeze({ status: "aborted" });
|
|
150
185
|
if (n.status === "invalid") return Object.freeze({
|
|
151
|
-
|
|
152
|
-
formError: n.formError,
|
|
186
|
+
issues: n.issues,
|
|
153
187
|
status: "invalid"
|
|
154
188
|
});
|
|
189
|
+
let i = E;
|
|
190
|
+
if (!i || i.revision !== T || r.aborted) return Object.freeze({ status: "aborted" });
|
|
155
191
|
try {
|
|
156
192
|
return Object.freeze({
|
|
157
193
|
status: "ok",
|
|
158
|
-
value: await e(
|
|
194
|
+
value: await e(i.value, r)
|
|
159
195
|
});
|
|
160
196
|
} catch (e) {
|
|
161
197
|
if (r.aborted) return Object.freeze({ status: "aborted" });
|
|
162
198
|
throw e;
|
|
163
199
|
}
|
|
164
200
|
} finally {
|
|
165
|
-
|
|
201
|
+
x || A((e) => ({
|
|
166
202
|
...e,
|
|
167
203
|
isSubmitting: !1
|
|
168
204
|
}));
|
|
169
205
|
}
|
|
170
206
|
},
|
|
171
207
|
subscribe(e, t = {}) {
|
|
172
|
-
return
|
|
208
|
+
return k("subscribe"), t.immediate && y.call(() => e(O)), y.add(() => e(O));
|
|
173
209
|
},
|
|
174
210
|
[Symbol.dispose]() {
|
|
175
|
-
|
|
211
|
+
P.dispose();
|
|
176
212
|
},
|
|
177
|
-
validate:
|
|
213
|
+
validate: N,
|
|
178
214
|
get value() {
|
|
179
|
-
return
|
|
215
|
+
return D.value;
|
|
180
216
|
}
|
|
181
217
|
};
|
|
182
|
-
return
|
|
218
|
+
return P;
|
|
183
219
|
}
|
|
184
220
|
//#endregion
|
|
185
|
-
export {
|
|
221
|
+
export { _ as createForm };
|
|
186
222
|
|
|
187
223
|
//# sourceMappingURL=form.js.map
|
package/dist/form.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form.js","names":[],"sources":["../src/form.ts"],"sourcesContent":["import { createField, type FieldAccess } from './_field';\nimport { createNotifier } from './_notify';\nimport {\n assertSafeKey,\n immutable,\n isRecord,\n type MetaRoot,\n normalizeErrors,\n resetAtPath,\n touchAll,\n writeAtPath,\n writeMeta,\n} from './core/path';\nimport { ForgeConfigError, ForgeDisposedError, ForgeSubmitError, ForgeValidationError } from './errors';\nimport type {\n Form,\n FormErrors,\n FormOptions,\n FormState,\n MaybePromise,\n ReadonlyDeep,\n SubmitResult,\n Unsubscribe,\n ValidationErrors,\n ValidationResult,\n} from './types';\n\ntype Validity = 'invalid' | 'unknown' | 'valid';\n\ntype InternalState<TValues extends Record<string, unknown>> = Readonly<{\n baseline: TValues;\n errors: FormErrors<TValues> | undefined;\n formError: string | undefined;\n isSubmitting: boolean;\n isValidating: boolean;\n submitCount: number;\n touched: MetaRoot;\n validity: Validity;\n value: TValues;\n}>;\n\nfunction makeFormState<TValues extends Record<string, unknown>>(current: InternalState<TValues>): FormState<TValues> {\n return Object.freeze({\n errors: current.errors,\n formError: current.formError,\n hasErrors: current.formError !== undefined || current.errors !== undefined,\n submitCount: current.submitCount,\n submitting: current.isSubmitting,\n touched: Object.keys(current.touched).length > 0,\n validating: current.isValidating,\n validity: current.validity,\n });\n}\n\n/** One immutable value tree and one explicit full-form validator keep form behavior locally understandable. */\nexport function createForm<TValues extends Record<string, unknown>>(options: FormOptions<TValues>): Form<TValues> {\n const initial = immutable(options.initialValues);\n const notifier = createNotifier(options.onSubscriberError);\n const disposalController = new AbortController();\n let disposed = false;\n let validationController: AbortController | undefined;\n let current: InternalState<TValues> = {\n baseline: initial,\n errors: undefined,\n formError: undefined,\n isSubmitting: false,\n isValidating: false,\n submitCount: 0,\n touched: {},\n validity: 'unknown',\n value: initial,\n };\n\n function ensureActive(operation: string): void {\n if (disposed) throw new ForgeDisposedError(operation);\n }\n\n function write(update: (current: InternalState<TValues>) => InternalState<TValues>): void {\n current = Object.freeze(update(current));\n notifier.notify();\n }\n\n function abortValidation(): void {\n validationController?.abort();\n }\n\n const access: FieldAccess = {\n abortValidation,\n addListener: notifier.add,\n ensureActive,\n readState: () => current,\n resetValue(path) {\n write((c) => ({\n ...c,\n touched: writeMeta(c.touched, path, false),\n validity: 'unknown',\n value: resetAtPath(c.value, c.baseline, path),\n }));\n },\n setTouched(path, touched) {\n write((c) => ({ ...c, touched: writeMeta(c.touched, path, touched) }));\n },\n setValue(path, next) {\n write((c) => ({\n ...c,\n validity: 'unknown',\n value: writeAtPath(c.value, path, next),\n }));\n },\n };\n\n async function validate(externalSignal?: AbortSignal): Promise<ValidationResult<TValues>> {\n ensureActive('validate');\n abortValidation();\n\n const controller = new AbortController();\n const signal = externalSignal ? AbortSignal.any([controller.signal, externalSignal]) : controller.signal;\n\n validationController = controller;\n write((c) => ({ ...c, isValidating: true }));\n\n try {\n const result: ValidationErrors<TValues> | undefined = options.validate\n ? await options.validate(current.value as ReadonlyDeep<TValues>, signal)\n : undefined;\n\n if (signal.aborted || validationController !== controller) return Object.freeze({ status: 'aborted' });\n\n const errors = result?.fields\n ? (normalizeErrors(immutable(result.fields)) as FormErrors<TValues> | undefined)\n : undefined;\n const formError = result?.formError;\n const validity: Validity = errors === undefined && formError === undefined ? 'valid' : 'invalid';\n\n write((c) => ({ ...c, errors, formError, validity }));\n\n return validity === 'valid'\n ? Object.freeze({ status: 'valid' })\n : Object.freeze({ errors, formError, status: 'invalid' });\n } catch (error) {\n if (signal.aborted || validationController !== controller) return Object.freeze({ status: 'aborted' });\n\n throw new ForgeValidationError('Form validation failed.', { cause: error });\n } finally {\n if (validationController === controller) {\n validationController = undefined;\n\n if (!disposed) write((c) => ({ ...c, isValidating: false }));\n }\n }\n }\n\n const form: Form<TValues> = {\n get disposalSignal() {\n return disposalController.signal;\n },\n dispose() {\n if (disposed) return;\n\n disposed = true;\n disposalController.abort();\n abortValidation();\n notifier.clear();\n },\n get disposed() {\n return disposed;\n },\n field(key) {\n assertSafeKey(key);\n if (!isRecord(current.value)) throw new ForgeConfigError('Form value must be an object.');\n\n return createField<TValues[typeof key]>([key], access);\n },\n reset(next) {\n ensureActive('reset');\n abortValidation();\n\n const baseline = next === undefined ? current.baseline : immutable(next);\n\n write((c) => ({\n ...c,\n baseline,\n errors: undefined,\n formError: undefined,\n touched: {},\n validity: 'unknown',\n value: baseline,\n }));\n },\n set(next) {\n ensureActive('set');\n abortValidation();\n\n const value =\n typeof next === 'function' ? (next as (current: ReadonlyDeep<TValues>) => TValues)(form.value) : next;\n\n write((c) => ({ ...c, errors: undefined, formError: undefined, validity: 'unknown', value: immutable(value) }));\n },\n get state() {\n return makeFormState(current);\n },\n async submit<TResult = void>(\n handler: (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<TResult>,\n externalSignal?: AbortSignal,\n ): Promise<SubmitResult<TResult, TValues>> {\n ensureActive('submit');\n\n if (current.isSubmitting) {\n throw new ForgeSubmitError('submit() called while a submission is already in progress');\n }\n\n const signal = externalSignal\n ? AbortSignal.any([disposalController.signal, externalSignal])\n : disposalController.signal;\n\n write((c) => ({\n ...c,\n isSubmitting: true,\n submitCount: c.submitCount + 1,\n touched: touchAll(c.value),\n }));\n\n try {\n const result = await validate(externalSignal);\n\n if (result.status === 'aborted') return Object.freeze({ status: 'aborted' });\n\n if (result.status === 'invalid') {\n return Object.freeze({ errors: result.errors, formError: result.formError, status: 'invalid' });\n }\n\n try {\n return Object.freeze({ status: 'ok', value: await handler(form.value, signal) });\n } catch (error) {\n if (signal.aborted) return Object.freeze({ status: 'aborted' });\n\n throw error;\n }\n } finally {\n if (!disposed) write((c) => ({ ...c, isSubmitting: false }));\n }\n },\n subscribe(listener, subscribeOptions = {}): Unsubscribe {\n ensureActive('subscribe');\n\n if (subscribeOptions.immediate) listener(makeFormState(current));\n\n return notifier.add(() => listener(makeFormState(current)));\n },\n [Symbol.dispose]() {\n form.dispose();\n },\n validate,\n get value() {\n return current.value as ReadonlyDeep<TValues>;\n },\n };\n\n return form;\n}\n"],"mappings":";;;;;AAyCA,SAAS,EAAuD,GAAqD;CACnH,OAAO,OAAO,OAAO;EACnB,QAAQ,EAAQ;EAChB,WAAW,EAAQ;EACnB,WAAW,EAAQ,cAAc,KAAA,KAAa,EAAQ,WAAW,KAAA;EACjE,aAAa,EAAQ;EACrB,YAAY,EAAQ;EACpB,SAAS,OAAO,KAAK,EAAQ,OAAO,CAAC,CAAC,SAAS;EAC/C,YAAY,EAAQ;EACpB,UAAU,EAAQ;CACpB,CAAC;AACH;AAGA,SAAgB,EAAoD,GAA8C;CAChH,IAAM,IAAU,EAAU,EAAQ,aAAa,GACzC,IAAW,EAAe,EAAQ,iBAAiB,GACnD,IAAqB,IAAI,gBAAgB,GAC3C,IAAW,IACX,GACA,IAAkC;EACpC,UAAU;EACV,QAAQ,KAAA;EACR,WAAW,KAAA;EACX,cAAc;EACd,cAAc;EACd,aAAa;EACb,SAAS,CAAC;EACV,UAAU;EACV,OAAO;CACT;CAEA,SAAS,EAAa,GAAyB;EAC7C,IAAI,GAAU,MAAM,IAAI,EAAmB,CAAS;CACtD;CAEA,SAAS,EAAM,GAA2E;EAExF,AADA,IAAU,OAAO,OAAO,EAAO,CAAO,CAAC,GACvC,EAAS,OAAO;CAClB;CAEA,SAAS,IAAwB;EAC/B,GAAsB,MAAM;CAC9B;CAEA,IAAM,IAAsB;EAC1B;EACA,aAAa,EAAS;EACtB;EACA,iBAAiB;EACjB,WAAW,GAAM;GACf,GAAO,OAAO;IACZ,GAAG;IACH,SAAS,EAAU,EAAE,SAAS,GAAM,EAAK;IACzC,UAAU;IACV,OAAO,EAAY,EAAE,OAAO,EAAE,UAAU,CAAI;GAC9C,EAAE;EACJ;EACA,WAAW,GAAM,GAAS;GACxB,GAAO,OAAO;IAAE,GAAG;IAAG,SAAS,EAAU,EAAE,SAAS,GAAM,CAAO;GAAE,EAAE;EACvE;EACA,SAAS,GAAM,GAAM;GACnB,GAAO,OAAO;IACZ,GAAG;IACH,UAAU;IACV,OAAO,EAAY,EAAE,OAAO,GAAM,CAAI;GACxC,EAAE;EACJ;CACF;CAEA,eAAe,EAAS,GAAkE;EAExF,AADA,EAAa,UAAU,GACvB,EAAgB;EAEhB,IAAM,IAAa,IAAI,gBAAgB,GACjC,IAAS,IAAiB,YAAY,IAAI,CAAC,EAAW,QAAQ,CAAc,CAAC,IAAI,EAAW;EAGlG,AADA,IAAuB,GACvB,GAAO,OAAO;GAAE,GAAG;GAAG,cAAc;EAAK,EAAE;EAE3C,IAAI;GACF,IAAM,IAAgD,EAAQ,WAC1D,MAAM,EAAQ,SAAS,EAAQ,OAAgC,CAAM,IACrE,KAAA;GAEJ,IAAI,EAAO,WAAW,MAAyB,GAAY,OAAO,OAAO,OAAO,EAAE,QAAQ,UAAU,CAAC;GAErG,IAAM,IAAS,GAAQ,SAClB,EAAgB,EAAU,EAAO,MAAM,CAAC,IACzC,KAAA,GACE,IAAY,GAAQ,WACpB,IAAqB,MAAW,KAAA,KAAa,MAAc,KAAA,IAAY,UAAU;GAIvF,OAFA,GAAO,OAAO;IAAE,GAAG;IAAG;IAAQ;IAAW;GAAS,EAAE,GAGhD,OAAO,OADJ,MAAa,UACF,EAAE,QAAQ,QAAQ,IAClB;IAAE;IAAQ;IAAW,QAAQ;GAAU,CAAC;EAC5D,SAAS,GAAO;GACd,IAAI,EAAO,WAAW,MAAyB,GAAY,OAAO,OAAO,OAAO,EAAE,QAAQ,UAAU,CAAC;GAErG,MAAM,IAAI,EAAqB,2BAA2B,EAAE,OAAO,EAAM,CAAC;EAC5E,UAAU;GACR,AAAI,MAAyB,MAC3B,IAAuB,KAAA,GAElB,KAAU,GAAO,OAAO;IAAE,GAAG;IAAG,cAAc;GAAM,EAAE;EAE/D;CACF;CAEA,IAAM,IAAsB;EAC1B,IAAI,iBAAiB;GACnB,OAAO,EAAmB;EAC5B;EACA,UAAU;GACJ,MAEJ,IAAW,IACX,EAAmB,MAAM,GACzB,EAAgB,GAChB,EAAS,MAAM;EACjB;EACA,IAAI,WAAW;GACb,OAAO;EACT;EACA,MAAM,GAAK;GAET,IADA,EAAc,CAAG,GACb,CAAC,EAAS,EAAQ,KAAK,GAAG,MAAM,IAAI,EAAiB,+BAA+B;GAExF,OAAO,EAAiC,CAAC,CAAG,GAAG,CAAM;EACvD;EACA,MAAM,GAAM;GAEV,AADA,EAAa,OAAO,GACpB,EAAgB;GAEhB,IAAM,IAAW,MAAS,KAAA,IAAY,EAAQ,WAAW,EAAU,CAAI;GAEvE,GAAO,OAAO;IACZ,GAAG;IACH;IACA,QAAQ,KAAA;IACR,WAAW,KAAA;IACX,SAAS,CAAC;IACV,UAAU;IACV,OAAO;GACT,EAAE;EACJ;EACA,IAAI,GAAM;GAER,AADA,EAAa,KAAK,GAClB,EAAgB;GAEhB,IAAM,IACJ,OAAO,KAAS,aAAc,EAAqD,EAAK,KAAK,IAAI;GAEnG,GAAO,OAAO;IAAE,GAAG;IAAG,QAAQ,KAAA;IAAW,WAAW,KAAA;IAAW,UAAU;IAAW,OAAO,EAAU,CAAK;GAAE,EAAE;EAChH;EACA,IAAI,QAAQ;GACV,OAAO,EAAc,CAAO;EAC9B;EACA,MAAM,OACJ,GACA,GACyC;GAGzC,IAFA,EAAa,QAAQ,GAEjB,EAAQ,cACV,MAAM,IAAI,EAAiB,2DAA2D;GAGxF,IAAM,IAAS,IACX,YAAY,IAAI,CAAC,EAAmB,QAAQ,CAAc,CAAC,IAC3D,EAAmB;GAEvB,GAAO,OAAO;IACZ,GAAG;IACH,cAAc;IACd,aAAa,EAAE,cAAc;IAC7B,SAAS,EAAS,EAAE,KAAK;GAC3B,EAAE;GAEF,IAAI;IACF,IAAM,IAAS,MAAM,EAAS,CAAc;IAE5C,IAAI,EAAO,WAAW,WAAW,OAAO,OAAO,OAAO,EAAE,QAAQ,UAAU,CAAC;IAE3E,IAAI,EAAO,WAAW,WACpB,OAAO,OAAO,OAAO;KAAE,QAAQ,EAAO;KAAQ,WAAW,EAAO;KAAW,QAAQ;IAAU,CAAC;IAGhG,IAAI;KACF,OAAO,OAAO,OAAO;MAAE,QAAQ;MAAM,OAAO,MAAM,EAAQ,EAAK,OAAO,CAAM;KAAE,CAAC;IACjF,SAAS,GAAO;KACd,IAAI,EAAO,SAAS,OAAO,OAAO,OAAO,EAAE,QAAQ,UAAU,CAAC;KAE9D,MAAM;IACR;GACF,UAAU;IACR,AAAK,KAAU,GAAO,OAAO;KAAE,GAAG;KAAG,cAAc;IAAM,EAAE;GAC7D;EACF;EACA,UAAU,GAAU,IAAmB,CAAC,GAAgB;GAKtD,OAJA,EAAa,WAAW,GAEpB,EAAiB,aAAW,EAAS,EAAc,CAAO,CAAC,GAExD,EAAS,UAAU,EAAS,EAAc,CAAO,CAAC,CAAC;EAC5D;EACA,CAAC,OAAO,WAAW;GACjB,EAAK,QAAQ;EACf;EACA;EACA,IAAI,QAAQ;GACV,OAAO,EAAQ;EACjB;CACF;CAEA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"form.js","names":[],"sources":["../src/form.ts"],"sourcesContent":["import { abortable } from '@vielzeug/arsenal';\n\nimport { createField, type FieldAccess } from './_field.js';\nimport { createNotifier } from './_notify.js';\nimport {\n assertSafeKey,\n immutable,\n isRecord,\n type MetaRoot,\n readAtPath,\n resetAtPath,\n touchAll,\n writeAtPath,\n writeMeta,\n} from './core/path.js';\nimport { ForgeConfigError, ForgeDisposedError, ForgeSubmitError, ForgeValidationError } from './errors.js';\nimport type {\n Form,\n FormOptions,\n FormState,\n FormValidator,\n MaybePromise,\n SubmitResult,\n Unsubscribe,\n ValidationIssue,\n ValidationResult,\n} from './types.js';\n\ntype Validity = 'invalid' | 'unknown' | 'valid';\n\ntype InternalState<TValues extends Record<string, unknown>> = Readonly<{\n baseline: TValues;\n issues: readonly ValidationIssue[] | undefined;\n isSubmitting: boolean;\n isValidating: boolean;\n submitCount: number;\n touched: MetaRoot;\n validity: Validity;\n value: TValues;\n}>;\n\nfunction normalizeIssues(result: readonly ValidationIssue[] | undefined): readonly ValidationIssue[] | undefined {\n if (result === undefined || result.length === 0) return undefined;\n\n return Object.freeze(\n result.map((issue) => {\n if (!issue || typeof issue.message !== 'string' || !Array.isArray(issue.path)) {\n throw new ForgeConfigError('Validators must return issues with a string message and array path.');\n }\n\n const path = issue.path.map((part) => {\n if (typeof part === 'number') {\n if (!Number.isSafeInteger(part) || part < 0) throw new ForgeConfigError(`Invalid issue path index ${part}.`);\n return part;\n }\n if (typeof part === 'string') {\n assertSafeKey(part);\n return part;\n }\n throw new ForgeConfigError('Issue paths may contain only string keys and non-negative integer indexes.');\n });\n\n return Object.freeze({ message: issue.message, path: Object.freeze(path) });\n }),\n );\n}\n\nfunction makeFormState<TValues extends Record<string, unknown>>(current: InternalState<TValues>): FormState {\n return Object.freeze({\n formError: current.issues?.find((issue) => issue.path.length === 0)?.message,\n hasErrors: (current.issues?.length ?? 0) > 0,\n issues: current.issues,\n submitCount: current.submitCount,\n submitting: current.isSubmitting,\n touched: Object.keys(current.touched).length > 0,\n validating: current.isValidating,\n validity: current.validity,\n });\n}\n\n/** One immutable value tree and one explicit full-form validator keep form behavior locally understandable. */\nexport function createForm<TValues extends Record<string, unknown>>(options: FormOptions<TValues>): Form<TValues> {\n const initial = immutable(options.initialValues);\n const notifier = createNotifier<InternalState<TValues>>(options.onSubscriberError);\n const disposalController = new AbortController();\n let disposed = false;\n let dispatching = false;\n const pendingWrites: Array<(current: InternalState<TValues>) => InternalState<TValues>> = [];\n let validationController: AbortController | undefined;\n let revision = 0;\n let validatedSnapshot: { revision: number; value: TValues } | undefined;\n let current: InternalState<TValues> = {\n baseline: initial,\n isSubmitting: false,\n issues: undefined,\n isValidating: false,\n submitCount: 0,\n touched: {},\n validity: 'unknown',\n value: initial,\n };\n let publicState = makeFormState(current);\n\n function ensureActive(operation: string): void {\n if (disposed) throw new ForgeDisposedError(operation);\n }\n\n function write(update: (current: InternalState<TValues>) => InternalState<TValues>): void {\n pendingWrites.push(update);\n if (dispatching) return;\n\n dispatching = true;\n try {\n while (pendingWrites.length > 0) {\n current = Object.freeze(pendingWrites.shift()!(current));\n publicState = makeFormState(current);\n notifier.notify(current);\n }\n } finally {\n dispatching = false;\n }\n }\n\n function abortValidation(): void {\n validationController?.abort();\n }\n\n const access: FieldAccess = {\n abortValidation,\n addListener: notifier.add,\n ensureActive,\n invokeListener: notifier.call,\n readState: () => current,\n resetValue(path) {\n write((c) => {\n revision++;\n validatedSnapshot = undefined;\n return {\n ...c,\n touched: writeMeta(c.touched, path, false),\n validity: 'unknown',\n value: resetAtPath(c.value, c.baseline, path),\n };\n });\n },\n setTouched(path, touched) {\n write((c) => ({ ...c, touched: writeMeta(c.touched, path, touched) }));\n },\n setValue(path, next) {\n write((c) => {\n const previous = readAtPath(c.value, path);\n const value = typeof next === 'function' ? next(previous) : next;\n revision++;\n validatedSnapshot = undefined;\n return { ...c, validity: 'unknown', value: writeAtPath(c.value, path, value) };\n });\n },\n };\n\n async function validate(externalSignal?: AbortSignal): Promise<ValidationResult> {\n ensureActive('validate');\n abortValidation();\n\n const controller = new AbortController();\n const signal = externalSignal ? AbortSignal.any([controller.signal, externalSignal]) : controller.signal;\n if (signal.aborted) return Object.freeze({ status: 'aborted' });\n\n const snapshot = current.value;\n const snapshotRevision = revision;\n validationController = controller;\n validatedSnapshot = undefined;\n write((c) => ({ ...c, isValidating: true }));\n\n try {\n if (signal.aborted || revision !== snapshotRevision) return Object.freeze({ status: 'aborted' });\n const result: readonly ValidationIssue[] | undefined = options.validate\n ? await abortable(\n Promise.resolve(options.validate(snapshot as Parameters<FormValidator<TValues>>[0], signal)),\n signal,\n )\n : undefined;\n\n if (signal.aborted || validationController !== controller || revision !== snapshotRevision) {\n return Object.freeze({ status: 'aborted' });\n }\n\n const issues = normalizeIssues(result);\n const validity: Validity = issues === undefined ? 'valid' : 'invalid';\n validationController = undefined;\n write((c) => ({ ...c, issues, isValidating: false, validity }));\n\n if (signal.aborted || revision !== snapshotRevision || validationController !== undefined) {\n return Object.freeze({ status: 'aborted' });\n }\n\n validatedSnapshot = validity === 'valid' ? { revision, value: snapshot } : undefined;\n return validity === 'valid'\n ? Object.freeze({ status: 'valid' })\n : Object.freeze({ issues: issues as readonly ValidationIssue[], status: 'invalid' });\n } catch (error) {\n if (signal.aborted || validationController !== controller) return Object.freeze({ status: 'aborted' });\n throw new ForgeValidationError('Form validation failed.', { cause: error });\n } finally {\n if (validationController === controller) {\n validationController = undefined;\n if (!disposed) write((c) => ({ ...c, isValidating: false }));\n }\n }\n }\n\n const form: Form<TValues> = {\n get disposalSignal() {\n return disposalController.signal;\n },\n dispose() {\n if (disposed) return;\n\n disposed = true;\n pendingWrites.length = 0;\n validatedSnapshot = undefined;\n disposalController.abort();\n abortValidation();\n notifier.clear();\n },\n get disposed() {\n return disposed;\n },\n field(key) {\n assertSafeKey(key);\n if (!isRecord(current.value)) throw new ForgeConfigError('Form value must be an object.');\n\n return createField<TValues[typeof key]>([key], access);\n },\n reset(next) {\n ensureActive('reset');\n abortValidation();\n\n const replacement = next === undefined ? undefined : immutable(next);\n write((c) => {\n const baseline = replacement ?? c.baseline;\n revision++;\n validatedSnapshot = undefined;\n return { ...c, baseline, issues: undefined, touched: {}, validity: 'unknown', value: baseline };\n });\n },\n set(next) {\n ensureActive('set');\n abortValidation();\n\n write((c) => {\n const value =\n typeof next === 'function'\n ? (next as (previous: Form<TValues>['value']) => TValues)(c.value as Form<TValues>['value'])\n : next;\n revision++;\n validatedSnapshot = undefined;\n return { ...c, issues: undefined, validity: 'unknown', value: immutable(value) };\n });\n },\n get state() {\n return publicState;\n },\n async submit<TResult = void>(\n handler: (values: Form<TValues>['value'], signal: AbortSignal) => MaybePromise<TResult>,\n externalSignal?: AbortSignal,\n ): Promise<SubmitResult<TResult>> {\n ensureActive('submit');\n\n if (current.isSubmitting) {\n throw new ForgeSubmitError('submit() called while a submission is already in progress');\n }\n\n const signal = externalSignal\n ? AbortSignal.any([disposalController.signal, externalSignal])\n : disposalController.signal;\n\n write((c) => ({\n ...c,\n isSubmitting: true,\n submitCount: c.submitCount + 1,\n touched: touchAll(c.value),\n }));\n\n try {\n const result = await validate(externalSignal);\n\n if (result.status === 'aborted') return Object.freeze({ status: 'aborted' });\n\n if (result.status === 'invalid') {\n return Object.freeze({ issues: result.issues, status: 'invalid' });\n }\n\n const validated = validatedSnapshot;\n if (!validated || validated.revision !== revision || signal.aborted) {\n return Object.freeze({ status: 'aborted' });\n }\n\n try {\n return Object.freeze({\n status: 'ok',\n value: await handler(validated.value as Form<TValues>['value'], signal),\n });\n } catch (error) {\n if (signal.aborted) return Object.freeze({ status: 'aborted' });\n\n throw error;\n }\n } finally {\n if (!disposed) write((c) => ({ ...c, isSubmitting: false }));\n }\n },\n subscribe(listener, subscribeOptions = {}): Unsubscribe {\n ensureActive('subscribe');\n\n if (subscribeOptions.immediate) notifier.call(() => listener(publicState));\n\n return notifier.add(() => listener(publicState));\n },\n [Symbol.dispose]() {\n form.dispose();\n },\n validate,\n get value() {\n return current.value as Form<TValues>['value'];\n },\n };\n\n return form;\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';
|
|
2
|
-
export { createForm } from './form';
|
|
3
|
-
export * from './types';
|
|
1
|
+
export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors.js';
|
|
2
|
+
export { createForm } from './form.js';
|
|
3
|
+
export * from './types.js';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,UAAU,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,UAAU,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACvH,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,cAAc,YAAY,CAAC"}
|
package/dist/persist.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@vielzeug/arsenal");async function t(t,n,r,i,a,o={}){let s=o.signal?AbortSignal.any([t.disposalSignal,o.signal]):t.disposalSignal;if(s.aborted)throw(0,e.abortError)(s);let c=t.value,l=await(0,e.abortable)(n.get(r,i),s);if(l===void 0||t.value!==c)return!1;let u=a.fromRecord(l);return u===void 0||t.value!==c?!1:(t.reset(u),!0)}async function n(t,n,r,i,a={}){let o=a.signal?AbortSignal.any([t.disposalSignal,a.signal]):t.disposalSignal;if(o.aborted)throw(0,e.abortError)(o);await(0,e.abortable)(n.put(r,i.toRecord(t.value)),o)}exports.loadForm=t,exports.saveForm=n;
|
|
2
|
+
//# sourceMappingURL=persist.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persist.cjs","names":[],"sources":["../src/persist.ts"],"sourcesContent":["import { abortable, abortError } from '@vielzeug/arsenal';\n\nimport type { Form } from './types.js';\n\nexport type FormDraftCodec<TValues extends Record<string, unknown>, RecordValue> = Readonly<{\n fromRecord(record: RecordValue): TValues | undefined;\n toRecord(values: Form<TValues>['value']): RecordValue;\n}>;\n\nexport type FormDraftStore<Table extends string, Key, RecordValue> = {\n get(table: Table, key: Key): Promise<RecordValue | undefined>;\n put(table: Table, value: RecordValue): Promise<void>;\n};\n\nexport type FormPersistenceOptions = Readonly<{ signal?: AbortSignal }>;\n\nexport type FormDraftWriteStore<Table extends string, RecordValue> = {\n put(table: Table, value: RecordValue): Promise<void>;\n};\n\nexport async function loadForm<TValues extends Record<string, unknown>, Table extends string, Key, RecordValue>(\n form: Form<TValues>,\n store: FormDraftStore<Table, Key, RecordValue>,\n table: Table,\n key: Key,\n codec: FormDraftCodec<TValues, RecordValue>,\n options: FormPersistenceOptions = {},\n): Promise<boolean> {\n const signal = options.signal ? AbortSignal.any([form.disposalSignal, options.signal]) : form.disposalSignal;\n if (signal.aborted) throw abortError(signal);\n const snapshot = form.value;\n const record = await abortable(store.get(table, key), signal);\n if (record === undefined || form.value !== snapshot) return false;\n\n const values = codec.fromRecord(record);\n if (values === undefined || form.value !== snapshot) return false;\n\n form.reset(values);\n return true;\n}\n\nexport async function saveForm<TValues extends Record<string, unknown>, Table extends string, RecordValue>(\n form: Form<TValues>,\n store: FormDraftWriteStore<Table, RecordValue>,\n table: Table,\n codec: FormDraftCodec<TValues, RecordValue>,\n options: FormPersistenceOptions = {},\n): Promise<void> {\n const signal = options.signal ? AbortSignal.any([form.disposalSignal, options.signal]) : form.disposalSignal;\n if (signal.aborted) throw abortError(signal);\n await abortable(store.put(table, codec.toRecord(form.value)), signal);\n}\n"],"mappings":"sGAoBA,eAAsB,EACpB,EACA,EACA,EACA,EACA,EACA,EAAkC,CAAC,EACjB,CAClB,IAAM,EAAS,EAAQ,OAAS,YAAY,IAAI,CAAC,EAAK,eAAgB,EAAQ,MAAM,CAAC,EAAI,EAAK,eAC9F,GAAI,EAAO,QAAS,MAAA,EAAM,EAAA,WAAA,CAAW,CAAM,EAC3C,IAAM,EAAW,EAAK,MAChB,EAAS,MAAA,EAAM,EAAA,UAAA,CAAU,EAAM,IAAI,EAAO,CAAG,EAAG,CAAM,EAC5D,GAAI,IAAW,IAAA,IAAa,EAAK,QAAU,EAAU,MAAO,GAE5D,IAAM,EAAS,EAAM,WAAW,CAAM,EAItC,OAHI,IAAW,IAAA,IAAa,EAAK,QAAU,EAAiB,IAE5D,EAAK,MAAM,CAAM,EACV,GACT,CAEA,eAAsB,EACpB,EACA,EACA,EACA,EACA,EAAkC,CAAC,EACpB,CACf,IAAM,EAAS,EAAQ,OAAS,YAAY,IAAI,CAAC,EAAK,eAAgB,EAAQ,MAAM,CAAC,EAAI,EAAK,eAC9F,GAAI,EAAO,QAAS,MAAA,EAAM,EAAA,WAAA,CAAW,CAAM,EAC3C,MAAA,EAAM,EAAA,UAAA,CAAU,EAAM,IAAI,EAAO,EAAM,SAAS,EAAK,KAAK,CAAC,EAAG,CAAM,CACtE"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Form } from './types.js';
|
|
2
|
+
export type FormDraftCodec<TValues extends Record<string, unknown>, RecordValue> = Readonly<{
|
|
3
|
+
fromRecord(record: RecordValue): TValues | undefined;
|
|
4
|
+
toRecord(values: Form<TValues>['value']): RecordValue;
|
|
5
|
+
}>;
|
|
6
|
+
export type FormDraftStore<Table extends string, Key, RecordValue> = {
|
|
7
|
+
get(table: Table, key: Key): Promise<RecordValue | undefined>;
|
|
8
|
+
put(table: Table, value: RecordValue): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
export type FormPersistenceOptions = Readonly<{
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}>;
|
|
13
|
+
export type FormDraftWriteStore<Table extends string, RecordValue> = {
|
|
14
|
+
put(table: Table, value: RecordValue): Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export declare function loadForm<TValues extends Record<string, unknown>, Table extends string, Key, RecordValue>(form: Form<TValues>, store: FormDraftStore<Table, Key, RecordValue>, table: Table, key: Key, codec: FormDraftCodec<TValues, RecordValue>, options?: FormPersistenceOptions): Promise<boolean>;
|
|
17
|
+
export declare function saveForm<TValues extends Record<string, unknown>, Table extends string, RecordValue>(form: Form<TValues>, store: FormDraftWriteStore<Table, RecordValue>, table: Table, codec: FormDraftCodec<TValues, RecordValue>, options?: FormPersistenceOptions): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=persist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../src/persist.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,MAAM,cAAc,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,IAAI,QAAQ,CAAC;IAC1F,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC;CACvD,CAAC,CAAC;AAEH,MAAM,MAAM,cAAc,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,WAAW,IAAI;IACnE,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC;IAC9D,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC;AAExE,MAAM,MAAM,mBAAmB,CAAC,KAAK,SAAS,MAAM,EAAE,WAAW,IAAI;IACnE,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD,CAAC;AAEF,wBAAsB,QAAQ,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,WAAW,EAC5G,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,EACnB,KAAK,EAAE,cAAc,CAAC,KAAK,EAAE,GAAG,EAAE,WAAW,CAAC,EAC9C,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,cAAc,CAAC,OAAO,EAAE,WAAW,CAAC,EAC3C,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,OAAO,CAAC,CAYlB;AAED,wBAAsB,QAAQ,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,SAAS,MAAM,EAAE,WAAW,EACvG,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,EACnB,KAAK,EAAE,mBAAmB,CAAC,KAAK,EAAE,WAAW,CAAC,EAC9C,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,cAAc,CAAC,OAAO,EAAE,WAAW,CAAC,EAC3C,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,IAAI,CAAC,CAIf"}
|
package/dist/persist.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { abortError as e, abortable as t } from "@vielzeug/arsenal";
|
|
2
|
+
//#region src/persist.ts
|
|
3
|
+
async function n(n, r, i, a, o, s = {}) {
|
|
4
|
+
let c = s.signal ? AbortSignal.any([n.disposalSignal, s.signal]) : n.disposalSignal;
|
|
5
|
+
if (c.aborted) throw e(c);
|
|
6
|
+
let l = n.value, u = await t(r.get(i, a), c);
|
|
7
|
+
if (u === void 0 || n.value !== l) return !1;
|
|
8
|
+
let d = o.fromRecord(u);
|
|
9
|
+
return d === void 0 || n.value !== l ? !1 : (n.reset(d), !0);
|
|
10
|
+
}
|
|
11
|
+
async function r(n, r, i, a, o = {}) {
|
|
12
|
+
let s = o.signal ? AbortSignal.any([n.disposalSignal, o.signal]) : n.disposalSignal;
|
|
13
|
+
if (s.aborted) throw e(s);
|
|
14
|
+
await t(r.put(i, a.toRecord(n.value)), s);
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { n as loadForm, r as saveForm };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=persist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persist.js","names":[],"sources":["../src/persist.ts"],"sourcesContent":["import { abortable, abortError } from '@vielzeug/arsenal';\n\nimport type { Form } from './types.js';\n\nexport type FormDraftCodec<TValues extends Record<string, unknown>, RecordValue> = Readonly<{\n fromRecord(record: RecordValue): TValues | undefined;\n toRecord(values: Form<TValues>['value']): RecordValue;\n}>;\n\nexport type FormDraftStore<Table extends string, Key, RecordValue> = {\n get(table: Table, key: Key): Promise<RecordValue | undefined>;\n put(table: Table, value: RecordValue): Promise<void>;\n};\n\nexport type FormPersistenceOptions = Readonly<{ signal?: AbortSignal }>;\n\nexport type FormDraftWriteStore<Table extends string, RecordValue> = {\n put(table: Table, value: RecordValue): Promise<void>;\n};\n\nexport async function loadForm<TValues extends Record<string, unknown>, Table extends string, Key, RecordValue>(\n form: Form<TValues>,\n store: FormDraftStore<Table, Key, RecordValue>,\n table: Table,\n key: Key,\n codec: FormDraftCodec<TValues, RecordValue>,\n options: FormPersistenceOptions = {},\n): Promise<boolean> {\n const signal = options.signal ? AbortSignal.any([form.disposalSignal, options.signal]) : form.disposalSignal;\n if (signal.aborted) throw abortError(signal);\n const snapshot = form.value;\n const record = await abortable(store.get(table, key), signal);\n if (record === undefined || form.value !== snapshot) return false;\n\n const values = codec.fromRecord(record);\n if (values === undefined || form.value !== snapshot) return false;\n\n form.reset(values);\n return true;\n}\n\nexport async function saveForm<TValues extends Record<string, unknown>, Table extends string, RecordValue>(\n form: Form<TValues>,\n store: FormDraftWriteStore<Table, RecordValue>,\n table: Table,\n codec: FormDraftCodec<TValues, RecordValue>,\n options: FormPersistenceOptions = {},\n): Promise<void> {\n const signal = options.signal ? AbortSignal.any([form.disposalSignal, options.signal]) : form.disposalSignal;\n if (signal.aborted) throw abortError(signal);\n await abortable(store.put(table, codec.toRecord(form.value)), signal);\n}\n"],"mappings":";;AAoBA,eAAsB,EACpB,GACA,GACA,GACA,GACA,GACA,IAAkC,CAAC,GACjB;CAClB,IAAM,IAAS,EAAQ,SAAS,YAAY,IAAI,CAAC,EAAK,gBAAgB,EAAQ,MAAM,CAAC,IAAI,EAAK;CAC9F,IAAI,EAAO,SAAS,MAAM,EAAW,CAAM;CAC3C,IAAM,IAAW,EAAK,OAChB,IAAS,MAAM,EAAU,EAAM,IAAI,GAAO,CAAG,GAAG,CAAM;CAC5D,IAAI,MAAW,KAAA,KAAa,EAAK,UAAU,GAAU,OAAO;CAE5D,IAAM,IAAS,EAAM,WAAW,CAAM;CAItC,OAHI,MAAW,KAAA,KAAa,EAAK,UAAU,IAAiB,MAE5D,EAAK,MAAM,CAAM,GACV;AACT;AAEA,eAAsB,EACpB,GACA,GACA,GACA,GACA,IAAkC,CAAC,GACpB;CACf,IAAM,IAAS,EAAQ,SAAS,YAAY,IAAI,CAAC,EAAK,gBAAgB,EAAQ,MAAM,CAAC,IAAI,EAAK;CAC9F,IAAI,EAAO,SAAS,MAAM,EAAW,CAAM;CAC3C,MAAM,EAAU,EAAM,IAAI,GAAO,EAAM,SAAS,EAAK,KAAK,CAAC,GAAG,CAAM;AACtE"}
|
package/dist/schema.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@vielzeug/arsenal");var t=e=>{let t=[];for(let n of e??[]){let e=typeof n==`object`&&n&&`key`in n?n.key:n;if(typeof e==`string`)t.push(e);else if(typeof e==`number`&&Number.isSafeInteger(e)&&e>=0)t.push(e);else return[]}return t};function n(n){return async(r,i)=>{if(i.aborted)return;let a=await(0,e.abortable)(Promise.resolve(n[`~standard`].validate(r)),i);if(!(i.aborted||`value`in a))return a.issues.map(e=>({message:e.message,path:t(e.path)}))}}exports.schemaValidator=n;
|
|
2
|
+
//# sourceMappingURL=schema.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.cjs","names":[],"sources":["../src/schema.ts"],"sourcesContent":["import { abortable } from '@vielzeug/arsenal';\n\nimport type { FormValidator, ValidationIssue } from './types.js';\n\nexport type StandardSchemaLike<Output> = {\n readonly '~standard': {\n readonly validate: (\n value: unknown,\n ) =>\n | PromiseLike<\n | { readonly issues: readonly { readonly message: string; readonly path?: readonly unknown[] }[] }\n | { readonly value: Output }\n >\n | { readonly issues: readonly { readonly message: string; readonly path?: readonly unknown[] }[] }\n | { readonly value: Output };\n };\n};\n\nconst toPath = (path: readonly unknown[] | undefined): ValidationIssue['path'] => {\n const result: Array<string | number> = [];\n\n for (const part of path ?? []) {\n const key = typeof part === 'object' && part !== null && 'key' in part ? part.key : part;\n if (typeof key === 'string') result.push(key);\n else if (typeof key === 'number' && Number.isSafeInteger(key) && key >= 0) result.push(key);\n else return [];\n }\n\n return result;\n};\n\nexport function schemaValidator<TValues extends Record<string, unknown>>(\n schema: StandardSchemaLike<TValues>,\n): FormValidator<TValues> {\n return async (values, signal) => {\n if (signal.aborted) return undefined;\n\n const result = await abortable(Promise.resolve(schema['~standard'].validate(values)), signal);\n\n if (signal.aborted || 'value' in result) return undefined;\n\n return result.issues.map((issue) => ({ message: issue.message, path: toPath(issue.path) }));\n };\n}\n"],"mappings":"sGAkBA,IAAM,EAAU,GAAkE,CAChF,IAAM,EAAiC,CAAC,EAExC,IAAK,IAAM,KAAQ,GAAQ,CAAC,EAAG,CAC7B,IAAM,EAAM,OAAO,GAAS,UAAY,GAAiB,QAAS,EAAO,EAAK,IAAM,EACpF,GAAI,OAAO,GAAQ,SAAU,EAAO,KAAK,CAAG,OACvC,GAAI,OAAO,GAAQ,UAAY,OAAO,cAAc,CAAG,GAAK,GAAO,EAAG,EAAO,KAAK,CAAG,OACrF,MAAO,CAAC,CACf,CAEA,OAAO,CACT,EAEA,SAAgB,EACd,EACwB,CACxB,OAAO,MAAO,EAAQ,IAAW,CAC/B,GAAI,EAAO,QAAS,OAEpB,IAAM,EAAS,MAAA,EAAM,EAAA,UAAA,CAAU,QAAQ,QAAQ,EAAO,YAAY,CAAC,SAAS,CAAM,CAAC,EAAG,CAAM,EAExF,OAAO,SAAW,UAAW,GAEjC,OAAO,EAAO,OAAO,IAAK,IAAW,CAAE,QAAS,EAAM,QAAS,KAAM,EAAO,EAAM,IAAI,CAAE,EAAE,CAC5F,CACF"}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FormValidator } from './types.js';
|
|
2
|
+
export type StandardSchemaLike<Output> = {
|
|
3
|
+
readonly '~standard': {
|
|
4
|
+
readonly validate: (value: unknown) => PromiseLike<{
|
|
5
|
+
readonly issues: readonly {
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly path?: readonly unknown[];
|
|
8
|
+
}[];
|
|
9
|
+
} | {
|
|
10
|
+
readonly value: Output;
|
|
11
|
+
}> | {
|
|
12
|
+
readonly issues: readonly {
|
|
13
|
+
readonly message: string;
|
|
14
|
+
readonly path?: readonly unknown[];
|
|
15
|
+
}[];
|
|
16
|
+
} | {
|
|
17
|
+
readonly value: Output;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare function schemaValidator<TValues extends Record<string, unknown>>(schema: StandardSchemaLike<TValues>): FormValidator<TValues>;
|
|
22
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,YAAY,CAAC;AAEjE,MAAM,MAAM,kBAAkB,CAAC,MAAM,IAAI;IACvC,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,CAAC,QAAQ,EAAE,CACjB,KAAK,EAAE,OAAO,KAEZ,WAAW,CACP;YAAE,QAAQ,CAAC,MAAM,EAAE,SAAS;gBAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;aAAE,EAAE,CAAA;SAAE,GAChG;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAC7B,GACD;YAAE,QAAQ,CAAC,MAAM,EAAE,SAAS;gBAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;aAAE,EAAE,CAAA;SAAE,GAChG;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;KAChC,CAAC;CACH,CAAC;AAeF,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrE,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,GAClC,aAAa,CAAC,OAAO,CAAC,CAUxB"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { abortable as e } from "@vielzeug/arsenal";
|
|
2
|
+
//#region src/schema.ts
|
|
3
|
+
var t = (e) => {
|
|
4
|
+
let t = [];
|
|
5
|
+
for (let n of e ?? []) {
|
|
6
|
+
let e = typeof n == "object" && n && "key" in n ? n.key : n;
|
|
7
|
+
if (typeof e == "string") t.push(e);
|
|
8
|
+
else if (typeof e == "number" && Number.isSafeInteger(e) && e >= 0) t.push(e);
|
|
9
|
+
else return [];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
function n(n) {
|
|
14
|
+
return async (r, i) => {
|
|
15
|
+
if (i.aborted) return;
|
|
16
|
+
let a = await e(Promise.resolve(n["~standard"].validate(r)), i);
|
|
17
|
+
if (!(i.aborted || "value" in a)) return a.issues.map((e) => ({
|
|
18
|
+
message: e.message,
|
|
19
|
+
path: t(e.path)
|
|
20
|
+
}));
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { n as schemaValidator };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","names":[],"sources":["../src/schema.ts"],"sourcesContent":["import { abortable } from '@vielzeug/arsenal';\n\nimport type { FormValidator, ValidationIssue } from './types.js';\n\nexport type StandardSchemaLike<Output> = {\n readonly '~standard': {\n readonly validate: (\n value: unknown,\n ) =>\n | PromiseLike<\n | { readonly issues: readonly { readonly message: string; readonly path?: readonly unknown[] }[] }\n | { readonly value: Output }\n >\n | { readonly issues: readonly { readonly message: string; readonly path?: readonly unknown[] }[] }\n | { readonly value: Output };\n };\n};\n\nconst toPath = (path: readonly unknown[] | undefined): ValidationIssue['path'] => {\n const result: Array<string | number> = [];\n\n for (const part of path ?? []) {\n const key = typeof part === 'object' && part !== null && 'key' in part ? part.key : part;\n if (typeof key === 'string') result.push(key);\n else if (typeof key === 'number' && Number.isSafeInteger(key) && key >= 0) result.push(key);\n else return [];\n }\n\n return result;\n};\n\nexport function schemaValidator<TValues extends Record<string, unknown>>(\n schema: StandardSchemaLike<TValues>,\n): FormValidator<TValues> {\n return async (values, signal) => {\n if (signal.aborted) return undefined;\n\n const result = await abortable(Promise.resolve(schema['~standard'].validate(values)), signal);\n\n if (signal.aborted || 'value' in result) return undefined;\n\n return result.issues.map((issue) => ({ message: issue.message, path: toPath(issue.path) }));\n };\n}\n"],"mappings":";;AAkBA,IAAM,KAAU,MAAkE;CAChF,IAAM,IAAiC,CAAC;CAExC,KAAK,IAAM,KAAQ,KAAQ,CAAC,GAAG;EAC7B,IAAM,IAAM,OAAO,KAAS,YAAY,KAAiB,SAAS,IAAO,EAAK,MAAM;EACpF,IAAI,OAAO,KAAQ,UAAU,EAAO,KAAK,CAAG;OACvC,IAAI,OAAO,KAAQ,YAAY,OAAO,cAAc,CAAG,KAAK,KAAO,GAAG,EAAO,KAAK,CAAG;OACrF,OAAO,CAAC;CACf;CAEA,OAAO;AACT;AAEA,SAAgB,EACd,GACwB;CACxB,OAAO,OAAO,GAAQ,MAAW;EAC/B,IAAI,EAAO,SAAS;EAEpB,IAAM,IAAS,MAAM,EAAU,QAAQ,QAAQ,EAAO,YAAY,CAAC,SAAS,CAAM,CAAC,GAAG,CAAM;EAExF,QAAO,WAAW,WAAW,IAEjC,OAAO,EAAO,OAAO,KAAK,OAAW;GAAE,SAAS,EAAM;GAAS,MAAM,EAAO,EAAM,IAAI;EAAE,EAAE;CAC5F;AACF"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
export type Unsubscribe = () => void;
|
|
2
2
|
export type MaybePromise<T> = T | PromiseLike<T>;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `Date`, `File`, and `Blob` are built-in atomic leaves. Dates are cloned and exposed
|
|
5
|
+
* without mutator methods; browser binary values preserve identity. Other values must be
|
|
6
|
+
* finite JSON primitives, plain objects, or dense arrays.
|
|
7
|
+
*/
|
|
8
|
+
export type Atomic = Date | File | Blob;
|
|
9
|
+
type DateMutator = Extract<keyof Date, `set${string}`>;
|
|
10
|
+
export type ReadonlyDate = Omit<Date, DateMutator>;
|
|
11
|
+
export type ReadonlyDeep<T> = T extends Date ? ReadonlyDate : T extends File | Blob ? T : T extends readonly (infer Item)[] ? readonly ReadonlyDeep<Item>[] : T extends Record<string, unknown> ? {
|
|
4
12
|
readonly [K in keyof T]: ReadonlyDeep<T[K]>;
|
|
5
13
|
} : T;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
fields?: FormErrors<TValues>;
|
|
11
|
-
formError?: string;
|
|
14
|
+
/** A single flat validation issue. `path: []` denotes a form-level error. */
|
|
15
|
+
export type ValidationIssue = Readonly<{
|
|
16
|
+
path: readonly (string | number)[];
|
|
17
|
+
message: string;
|
|
12
18
|
}>;
|
|
13
|
-
export type FormValidator<TValues extends Record<string, unknown>> = (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<
|
|
19
|
+
export type FormValidator<TValues extends Record<string, unknown>> = (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<readonly ValidationIssue[] | undefined>;
|
|
14
20
|
export type FormOptions<TValues extends Record<string, unknown>> = Readonly<{
|
|
15
21
|
initialValues: TValues;
|
|
16
22
|
onSubscriberError?: (error: unknown) => void;
|
|
@@ -25,31 +31,29 @@ export type FieldState<V> = Readonly<{
|
|
|
25
31
|
touched: boolean;
|
|
26
32
|
value: ReadonlyDeep<V>;
|
|
27
33
|
}>;
|
|
28
|
-
export type FormState
|
|
29
|
-
errors: FormErrors<TValues> | undefined;
|
|
34
|
+
export type FormState = Readonly<{
|
|
30
35
|
formError: string | undefined;
|
|
31
36
|
hasErrors: boolean;
|
|
37
|
+
issues: readonly ValidationIssue[] | undefined;
|
|
32
38
|
submitCount: number;
|
|
33
39
|
submitting: boolean;
|
|
34
40
|
touched: boolean;
|
|
35
41
|
validity: 'invalid' | 'unknown' | 'valid';
|
|
36
42
|
validating: boolean;
|
|
37
43
|
}>;
|
|
38
|
-
export type ValidationResult
|
|
44
|
+
export type ValidationResult = Readonly<{
|
|
39
45
|
status: 'aborted';
|
|
40
46
|
}> | Readonly<{
|
|
41
47
|
status: 'valid';
|
|
42
48
|
}> | Readonly<{
|
|
43
|
-
|
|
44
|
-
formError: string | undefined;
|
|
49
|
+
issues: readonly ValidationIssue[];
|
|
45
50
|
status: 'invalid';
|
|
46
51
|
}>;
|
|
47
|
-
export type SubmitResult<TResult = void
|
|
52
|
+
export type SubmitResult<TResult = void> = Readonly<{
|
|
48
53
|
status: 'aborted';
|
|
49
54
|
}> | Readonly<{
|
|
55
|
+
issues: readonly ValidationIssue[];
|
|
50
56
|
status: 'invalid';
|
|
51
|
-
errors: FormErrors<TValues> | undefined;
|
|
52
|
-
formError: string | undefined;
|
|
53
57
|
}> | Readonly<{
|
|
54
58
|
status: 'ok';
|
|
55
59
|
value: TResult;
|
|
@@ -78,10 +82,10 @@ export type Form<TValues extends Record<string, unknown>> = {
|
|
|
78
82
|
field<K extends keyof TValues & string>(key: K): Field<TValues[K]>;
|
|
79
83
|
reset(next?: TValues): void;
|
|
80
84
|
set(next: TValues | ((previous: ReadonlyDeep<TValues>) => TValues)): void;
|
|
81
|
-
readonly state: FormState
|
|
82
|
-
submit<TResult = void>(handler: (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<TResult>, signal?: AbortSignal): Promise<SubmitResult<TResult
|
|
83
|
-
subscribe(listener: (state: FormState
|
|
84
|
-
validate(signal?: AbortSignal): Promise<ValidationResult
|
|
85
|
+
readonly state: FormState;
|
|
86
|
+
submit<TResult = void>(handler: (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<TResult>, signal?: AbortSignal): Promise<SubmitResult<TResult>>;
|
|
87
|
+
subscribe(listener: (state: FormState) => void, options?: SubscribeOptions): Unsubscribe;
|
|
88
|
+
validate(signal?: AbortSignal): Promise<ValidationResult>;
|
|
85
89
|
readonly value: ReadonlyDeep<TValues>;
|
|
86
90
|
};
|
|
87
91
|
export {};
|