@vielzeug/forge 2.1.0 → 2.2.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.
Files changed (53) hide show
  1. package/README.md +2 -1
  2. package/dist/_field.cjs +2 -0
  3. package/dist/_field.cjs.map +1 -0
  4. package/dist/_field.d.ts +18 -0
  5. package/dist/_field.d.ts.map +1 -0
  6. package/dist/_field.js +64 -0
  7. package/dist/_field.js.map +1 -0
  8. package/dist/_notify.cjs +2 -0
  9. package/dist/_notify.cjs.map +1 -0
  10. package/dist/_notify.d.ts +8 -0
  11. package/dist/_notify.d.ts.map +1 -0
  12. package/dist/_notify.js +39 -0
  13. package/dist/_notify.js.map +1 -0
  14. package/dist/core/path.cjs +1 -1
  15. package/dist/core/path.cjs.map +1 -1
  16. package/dist/core/path.d.ts +14 -9
  17. package/dist/core/path.d.ts.map +1 -1
  18. package/dist/core/path.js +84 -50
  19. package/dist/core/path.js.map +1 -1
  20. package/dist/forge.cjs +1 -1
  21. package/dist/forge.cjs.map +1 -1
  22. package/dist/forge.iife.js +1 -1
  23. package/dist/forge.iife.js.map +1 -1
  24. package/dist/forge.js +1 -1
  25. package/dist/forge.js.map +1 -1
  26. package/dist/form-data.cjs +2 -0
  27. package/dist/form-data.cjs.map +1 -0
  28. package/dist/form-data.d.ts.map +1 -0
  29. package/dist/form-data.js +32 -0
  30. package/dist/form-data.js.map +1 -0
  31. package/dist/form.cjs +1 -1
  32. package/dist/form.cjs.map +1 -1
  33. package/dist/form.d.ts.map +1 -1
  34. package/dist/form.js +107 -152
  35. package/dist/form.js.map +1 -1
  36. package/dist/index.cjs +1 -1
  37. package/dist/index.d.ts +0 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -3
  40. package/dist/spell.cjs +1 -1
  41. package/dist/spell.cjs.map +1 -1
  42. package/dist/spell.d.ts.map +1 -1
  43. package/dist/spell.js +17 -25
  44. package/dist/spell.js.map +1 -1
  45. package/dist/types.d.ts +14 -12
  46. package/dist/types.d.ts.map +1 -1
  47. package/package.json +19 -8
  48. package/dist/adapters/form-data.cjs +0 -2
  49. package/dist/adapters/form-data.cjs.map +0 -1
  50. package/dist/adapters/form-data.d.ts.map +0 -1
  51. package/dist/adapters/form-data.js +0 -14
  52. package/dist/adapters/form-data.js.map +0 -1
  53. /package/dist/{adapters/form-data.d.ts → form-data.d.ts} +0 -0
package/dist/form.js CHANGED
@@ -1,232 +1,187 @@
1
1
  import { ForgeConfigError as e, ForgeDisposedError as t, ForgeSubmitError as n, ForgeValidationError as r } from "./errors.js";
2
- import { assertSafeKey as i, hasAtPath as a, immutable as o, isRecord as s, normalizeErrors as c, readAtPath as l, readError as u, readMeta as d, resetAtPath as f, touchAll as p, writeAtPath as m, writeMeta as h } from "./core/path.js";
2
+ import { assertSafeKey as i, immutable as a, isRecord as o, normalizeErrors as s, resetAtPath as c, touchAll as l, writeAtPath as u, writeMeta as d } from "./core/path.js";
3
+ import { createField as f } from "./_field.js";
4
+ import { createNotifier as p } from "./_notify.js";
3
5
  //#region src/form.ts
4
- function g(e) {
5
- queueMicrotask(() => {
6
- throw e;
7
- });
8
- }
9
- function _(e, t) {
10
- let n = l(e.value, t), r = l(e.baseline, t);
11
- return Object.freeze({
12
- dirty: n !== r || a(e.value, t) !== a(e.baseline, t),
13
- error: u(e.errors, t),
14
- touched: d(e.touched, t),
15
- value: n
16
- });
17
- }
18
- function v(e) {
6
+ function m(e) {
19
7
  return Object.freeze({
20
- error: e.error,
21
8
  errors: e.errors,
9
+ formError: e.formError,
10
+ hasErrors: e.formError !== void 0 || e.errors !== void 0,
22
11
  submitCount: e.submitCount,
23
12
  submitting: e.isSubmitting,
24
13
  touched: Object.keys(e.touched).length > 0,
25
- valid: e.error === void 0 && e.errors === void 0,
26
- validating: e.isValidating
14
+ validating: e.isValidating,
15
+ validity: e.validity
27
16
  });
28
17
  }
29
- function y(a) {
30
- let u = o(a.initialValues);
31
- function d(e) {
32
- if (!a.onSubscriberError) {
33
- g(e);
34
- return;
35
- }
36
- try {
37
- a.onSubscriberError(e);
38
- } catch (e) {
39
- g(e);
40
- }
41
- }
42
- let y = {
43
- baseline: u,
44
- error: void 0,
18
+ function h(h) {
19
+ let g = a(h.initialValues), _ = p(h.onSubscriberError), v = new AbortController(), y = !1, b, x = {
20
+ baseline: g,
45
21
  errors: void 0,
22
+ formError: void 0,
46
23
  isSubmitting: !1,
47
24
  isValidating: !1,
48
25
  submitCount: 0,
49
26
  touched: {},
50
- value: u
51
- }, b = /* @__PURE__ */ new Set(), x = new AbortController(), S = !1, C;
52
- function w(e) {
53
- if (S) throw new t(e);
27
+ validity: "unknown",
28
+ value: g
29
+ };
30
+ function S(e) {
31
+ if (y) throw new t(e);
54
32
  }
55
- function T() {
56
- for (let e of [...b]) try {
57
- e();
58
- } catch (e) {
59
- d(e);
60
- }
33
+ function C(e) {
34
+ x = Object.freeze(e(x)), _.notify();
61
35
  }
62
- function E(e) {
63
- y = Object.freeze(e(y)), T();
36
+ function w() {
37
+ b?.abort();
64
38
  }
65
- async function D(e) {
66
- w("validate"), C?.abort();
39
+ let T = {
40
+ abortValidation: w,
41
+ addListener: _.add,
42
+ ensureActive: S,
43
+ readState: () => x,
44
+ resetValue(e) {
45
+ C((t) => ({
46
+ ...t,
47
+ touched: d(t.touched, e, !1),
48
+ validity: "unknown",
49
+ value: c(t.value, t.baseline, e)
50
+ }));
51
+ },
52
+ setTouched(e, t) {
53
+ C((n) => ({
54
+ ...n,
55
+ touched: d(n.touched, e, t)
56
+ }));
57
+ },
58
+ setValue(e, t) {
59
+ C((n) => ({
60
+ ...n,
61
+ validity: "unknown",
62
+ value: u(n.value, e, t)
63
+ }));
64
+ }
65
+ };
66
+ async function E(e) {
67
+ S("validate"), w();
67
68
  let t = new AbortController(), n = e ? AbortSignal.any([t.signal, e]) : t.signal;
68
- C = t, E((e) => ({
69
+ b = t, C((e) => ({
69
70
  ...e,
70
71
  isValidating: !0
71
72
  }));
72
73
  try {
73
- let e = a.validate ? await a.validate(y.value, n) : void 0;
74
- if (n.aborted || C !== t) return Object.freeze({ status: "aborted" });
75
- let r = e?.fields ? c(o(e.fields)) : void 0, i = e?.formError;
76
- return E((e) => ({
74
+ let e = h.validate ? await h.validate(x.value, n) : void 0;
75
+ if (n.aborted || b !== t) return Object.freeze({ status: "aborted" });
76
+ let r = e?.fields ? s(a(e.fields)) : void 0, i = e?.formError, o = r === void 0 && i === void 0 ? "valid" : "invalid";
77
+ return C((e) => ({
77
78
  ...e,
78
- error: i,
79
- errors: r
80
- })), Object.freeze(r === void 0 && i === void 0 ? { status: "valid" } : {
79
+ errors: r,
80
+ formError: i,
81
+ validity: o
82
+ })), Object.freeze(o === "valid" ? { status: "valid" } : {
81
83
  errors: r,
82
84
  formError: i,
83
85
  status: "invalid"
84
86
  });
85
87
  } catch (e) {
86
- if (n.aborted || C !== t) return Object.freeze({ status: "aborted" });
88
+ if (n.aborted || b !== t) return Object.freeze({ status: "aborted" });
87
89
  throw new r("Form validation failed.", { cause: e });
88
90
  } finally {
89
- C === t && (C = void 0, S || E((e) => ({
91
+ b === t && (b = void 0, y || C((e) => ({
90
92
  ...e,
91
93
  isValidating: !1
92
94
  })));
93
95
  }
94
96
  }
95
- function O(t) {
96
- return Object.assign({
97
- get dirty() {
98
- return _(y, t).dirty;
99
- },
100
- get error() {
101
- return _(y, t).error;
102
- },
103
- reset() {
104
- w("field().reset"), C?.abort(), E((e) => ({
105
- ...e,
106
- touched: h(e.touched, t, !1),
107
- value: f(e.value, e.baseline, t)
108
- }));
109
- },
110
- set(e) {
111
- w("field().set"), C?.abort();
112
- let n = l(y.value, t), r = typeof e == "function" ? e(n) : e;
113
- E((e) => ({
114
- ...e,
115
- value: m(e.value, t, r)
116
- }));
117
- },
118
- subscribe(e, n = {}) {
119
- w("field().subscribe");
120
- let r = _(y, t);
121
- n.immediate && e(r);
122
- let i = () => {
123
- let n = _(y, t);
124
- (n.value !== r.value || n.error !== r.error || n.touched !== r.touched || n.dirty !== r.dirty) && (r = n, e(n));
125
- };
126
- return b.add(i), () => b.delete(i);
127
- },
128
- touch() {
129
- w("field().touch"), E((e) => ({
130
- ...e,
131
- touched: h(e.touched, t, !0)
132
- }));
133
- },
134
- get touched() {
135
- return _(y, t).touched;
136
- },
137
- get value() {
138
- return _(y, t).value;
139
- }
140
- }, { field(n) {
141
- i(n);
142
- let r = l(y.value, t);
143
- if (r !== void 0 && !s(r)) throw new e(`Cannot select '${n}' because the current field value is not an object.`);
144
- return O([...t, n]);
145
- } });
146
- }
147
- let k = {
97
+ let D = {
148
98
  get disposalSignal() {
149
- return x.signal;
99
+ return v.signal;
150
100
  },
151
101
  dispose() {
152
- S || (S = !0, x.abort(), C?.abort(), b.clear());
102
+ y || (y = !0, v.abort(), w(), _.clear());
153
103
  },
154
104
  get disposed() {
155
- return S;
105
+ return y;
156
106
  },
157
- field(e) {
158
- return i(e), O([e]);
107
+ field(t) {
108
+ if (i(t), !o(x.value)) throw new e("Form value must be an object.");
109
+ return f([t], T);
159
110
  },
160
111
  reset(e) {
161
- w("reset"), C?.abort();
162
- let t = e === void 0 ? y.baseline : o(e);
163
- E((e) => ({
112
+ S("reset"), w();
113
+ let t = e === void 0 ? x.baseline : a(e);
114
+ C((e) => ({
164
115
  ...e,
165
116
  baseline: t,
166
- error: void 0,
167
117
  errors: void 0,
118
+ formError: void 0,
168
119
  touched: {},
120
+ validity: "unknown",
169
121
  value: t
170
122
  }));
171
123
  },
172
124
  set(e) {
173
- w("set"), C?.abort();
174
- let t = typeof e == "function" ? e(k.value) : e;
175
- E((e) => ({
125
+ S("set"), w();
126
+ let t = typeof e == "function" ? e(D.value) : e;
127
+ C((e) => ({
176
128
  ...e,
177
- error: void 0,
178
129
  errors: void 0,
179
- value: o(t)
130
+ formError: void 0,
131
+ validity: "unknown",
132
+ value: a(t)
180
133
  }));
181
134
  },
182
135
  get state() {
183
- return v(y);
136
+ return m(x);
184
137
  },
185
- async submit(e) {
186
- if (w("submit"), y.isSubmitting) throw new n("submit() called while a submission is already in progress");
187
- E((e) => ({
138
+ async submit(e, t) {
139
+ if (S("submit"), x.isSubmitting) throw new n("submit() called while a submission is already in progress");
140
+ let r = t ? AbortSignal.any([v.signal, t]) : v.signal;
141
+ C((e) => ({
188
142
  ...e,
189
143
  isSubmitting: !0,
190
144
  submitCount: e.submitCount + 1,
191
- touched: p(e.value)
145
+ touched: l(e.value)
192
146
  }));
193
147
  try {
194
- let t = await D();
195
- return t.status === "aborted" ? Object.freeze({
196
- ok: !1,
197
- type: "aborted"
198
- }) : t.status === "invalid" ? Object.freeze({
199
- errors: t.errors,
200
- formError: t.formError,
201
- ok: !1,
202
- type: "validation"
203
- }) : Object.freeze({
204
- ok: !0,
205
- value: await e(k.value)
148
+ let n = await E(t);
149
+ if (n.status === "aborted") return Object.freeze({ status: "aborted" });
150
+ if (n.status === "invalid") return Object.freeze({
151
+ errors: n.errors,
152
+ formError: n.formError,
153
+ status: "invalid"
206
154
  });
155
+ try {
156
+ return Object.freeze({
157
+ status: "ok",
158
+ value: await e(D.value, r)
159
+ });
160
+ } catch (e) {
161
+ if (r.aborted) return Object.freeze({ status: "aborted" });
162
+ throw e;
163
+ }
207
164
  } finally {
208
- S || E((e) => ({
165
+ y || C((e) => ({
209
166
  ...e,
210
167
  isSubmitting: !1
211
168
  }));
212
169
  }
213
170
  },
214
171
  subscribe(e, t = {}) {
215
- w("subscribe"), t.immediate && e(v(y));
216
- let n = () => e(v(y));
217
- return b.add(n), () => b.delete(n);
172
+ return S("subscribe"), t.immediate && e(m(x)), _.add(() => e(m(x)));
218
173
  },
219
174
  [Symbol.dispose]() {
220
- k.dispose();
175
+ D.dispose();
221
176
  },
222
- validate: D,
177
+ validate: E,
223
178
  get value() {
224
- return y.value;
179
+ return x.value;
225
180
  }
226
181
  };
227
- return k;
182
+ return D;
228
183
  }
229
184
  //#endregion
230
- export { y as createForm };
185
+ export { h as createForm };
231
186
 
232
187
  //# 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 {\n assertSafeKey,\n hasAtPath,\n immutable,\n isRecord,\n type MetaRoot,\n normalizeErrors,\n readAtPath,\n readError,\n readMeta,\n resetAtPath,\n touchAll,\n writeAtPath,\n writeMeta,\n} from './core/path';\nimport { ForgeConfigError, ForgeDisposedError, ForgeSubmitError, ForgeValidationError } from './errors';\nimport type {\n Field,\n FieldState,\n Form,\n FormErrors,\n FormOptions,\n FormState,\n MaybePromise,\n ReadonlyDeep,\n SubmitResult,\n SubscribeOptions,\n Unsubscribe,\n ValidationErrors,\n ValidationResult,\n} from './types';\n\ntype InternalState<TValues extends Record<string, unknown>> = Readonly<{\n baseline: TValues;\n error: string | undefined;\n errors: FormErrors<TValues> | undefined;\n isSubmitting: boolean;\n isValidating: boolean;\n submitCount: number;\n touched: MetaRoot;\n value: TValues;\n}>;\n\nfunction rethrowAsync(error: unknown): void {\n queueMicrotask(() => {\n throw error;\n });\n}\n\nfunction makeFieldState<V>(state: InternalState<Record<string, unknown>>, path: readonly string[]): FieldState<V> {\n const value = readAtPath<V>(state.value, path);\n const baseline = readAtPath<V>(state.baseline, path);\n\n return Object.freeze({\n dirty: value !== baseline || hasAtPath(state.value, path) !== hasAtPath(state.baseline, path),\n error: readError(state.errors, path),\n touched: readMeta(state.touched, path),\n value: value as ReadonlyDeep<V>,\n });\n}\n\nfunction makeFormState<TValues extends Record<string, unknown>>(current: InternalState<TValues>): FormState<TValues> {\n return Object.freeze({\n error: current.error,\n errors: current.errors,\n submitCount: current.submitCount,\n submitting: current.isSubmitting,\n touched: Object.keys(current.touched).length > 0,\n valid: current.error === undefined && current.errors === undefined,\n validating: current.isValidating,\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\n function reportListenerError(error: unknown): void {\n if (!options.onSubscriberError) {\n rethrowAsync(error);\n\n return;\n }\n\n try {\n options.onSubscriberError(error);\n } catch (reporterError) {\n rethrowAsync(reporterError);\n }\n }\n\n let current: InternalState<TValues> = {\n baseline: initial,\n error: undefined,\n errors: undefined,\n isSubmitting: false,\n isValidating: false,\n submitCount: 0,\n touched: {},\n value: initial,\n };\n const listeners = new Set<() => void>();\n const disposalController = new AbortController();\n let disposed = false;\n let validationController: AbortController | undefined;\n\n function ensureActive(operation: string): void {\n if (disposed) throw new ForgeDisposedError(operation);\n }\n\n function notify(): void {\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch (error) {\n reportListenerError(error);\n }\n }\n }\n\n function write(update: (current: InternalState<TValues>) => InternalState<TValues>): void {\n current = Object.freeze(update(current));\n notify();\n }\n\n async function validate(externalSignal?: AbortSignal): Promise<ValidationResult<TValues>> {\n ensureActive('validate');\n validationController?.abort();\n\n const controller = new AbortController();\n const signal = externalSignal ? AbortSignal.any([controller.signal, externalSignal]) : controller.signal;\n\n validationController = controller;\n write((current) => ({ ...current, 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\n write((current) => ({ ...current, error: formError, errors }));\n\n return errors === undefined && formError === undefined\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((current) => ({ ...current, isValidating: false }));\n }\n }\n }\n\n function createField<V>(path: readonly string[]): Field<V> {\n const common = {\n get dirty() {\n return makeFieldState<V>(current as InternalState<Record<string, unknown>>, path).dirty;\n },\n get error() {\n return makeFieldState<V>(current as InternalState<Record<string, unknown>>, path).error;\n },\n reset() {\n ensureActive('field().reset');\n validationController?.abort();\n write((current) => ({\n ...current,\n touched: writeMeta(current.touched, path, false),\n value: resetAtPath(current.value, current.baseline, path),\n }));\n },\n set(next: V | ((previous: ReadonlyDeep<V>) => V)) {\n ensureActive('field().set');\n validationController?.abort();\n\n const previous = readAtPath<V>(current.value, path) as ReadonlyDeep<V>;\n const value = typeof next === 'function' ? (next as (current: ReadonlyDeep<V>) => V)(previous) : next;\n\n write((current) => ({ ...current, value: writeAtPath(current.value, path, value) }));\n },\n subscribe(listener: (state: FieldState<V>) => void, subscribeOptions: SubscribeOptions = {}): Unsubscribe {\n ensureActive('field().subscribe');\n\n let previous = makeFieldState<V>(current as InternalState<Record<string, unknown>>, path);\n\n if (subscribeOptions.immediate) listener(previous);\n\n const filtered = () => {\n const next = makeFieldState<V>(current as InternalState<Record<string, unknown>>, path);\n\n if (\n next.value === previous.value &&\n next.error === previous.error &&\n next.touched === previous.touched &&\n next.dirty === previous.dirty\n ) {\n return;\n }\n\n previous = next;\n listener(next);\n };\n\n listeners.add(filtered);\n\n return () => listeners.delete(filtered);\n },\n touch() {\n ensureActive('field().touch');\n write((current) => ({ ...current, touched: writeMeta(current.touched, path, true) }));\n },\n get touched() {\n return makeFieldState<V>(current as InternalState<Record<string, unknown>>, path).touched;\n },\n get value() {\n return makeFieldState<V>(current as InternalState<Record<string, unknown>>, path).value;\n },\n };\n\n return Object.assign(common, {\n field<K extends keyof NonNullable<V> & string>(key: K): Field<NonNullable<V>[K]> {\n assertSafeKey(key);\n\n const currentVal = readAtPath(current.value, path);\n\n if (currentVal !== undefined && !isRecord(currentVal)) {\n throw new ForgeConfigError(`Cannot select '${key}' because the current field value is not an object.`);\n }\n\n return createField<NonNullable<V>[K]>([...path, key]);\n },\n }) as Field<V>;\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 validationController?.abort();\n listeners.clear();\n },\n get disposed() {\n return disposed;\n },\n field(key) {\n assertSafeKey(key);\n\n return createField<TValues[typeof key]>([key]);\n },\n reset(next) {\n ensureActive('reset');\n validationController?.abort();\n\n const baseline = next === undefined ? current.baseline : immutable(next);\n\n write((current) => ({\n ...current,\n baseline,\n error: undefined,\n errors: undefined,\n touched: {},\n value: baseline,\n }));\n },\n set(next) {\n ensureActive('set');\n validationController?.abort();\n\n const value =\n typeof next === 'function' ? (next as (current: ReadonlyDeep<TValues>) => TValues)(form.value) : next;\n\n write((current) => ({ ...current, error: undefined, errors: undefined, value: immutable(value) }));\n },\n get state() {\n return makeFormState(current);\n },\n async submit<TResult = void>(\n handler: (values: ReadonlyDeep<TValues>) => MaybePromise<TResult>,\n ): Promise<SubmitResult<TResult, TValues>> {\n ensureActive('submit');\n\n if (current.isSubmitting) throw new ForgeSubmitError('submit() called while a submission is already in progress');\n\n write((current) => ({\n ...current,\n isSubmitting: true,\n submitCount: current.submitCount + 1,\n touched: touchAll(current.value),\n }));\n\n try {\n const result = await validate();\n\n if (result.status === 'aborted') return Object.freeze({ ok: false, type: 'aborted' });\n\n if (result.status === 'invalid') {\n return Object.freeze({\n errors: result.errors,\n formError: result.formError,\n ok: false,\n type: 'validation',\n });\n }\n\n return Object.freeze({ ok: true, value: await handler(form.value) });\n } finally {\n if (!disposed) write((current) => ({ ...current, isSubmitting: false }));\n }\n },\n subscribe(listener, subscribeOptions: SubscribeOptions = {}): Unsubscribe {\n ensureActive('subscribe');\n\n if (subscribeOptions.immediate) listener(makeFormState(current));\n\n const formListener = () => listener(makeFormState(current));\n\n listeners.add(formListener);\n\n return () => listeners.delete(formListener);\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":"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"}
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":"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"}
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./errors.cjs"),t=require("./adapters/form-data.cjs"),n=require("./form.cjs");exports.ForgeConfigError=e.ForgeConfigError,exports.ForgeDisposedError=e.ForgeDisposedError,exports.ForgeError=e.ForgeError,exports.ForgeSubmitError=e.ForgeSubmitError,exports.ForgeValidationError=e.ForgeValidationError,exports.createForm=n.createForm,exports.toFormData=t.toFormData;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./errors.cjs"),t=require("./form.cjs");exports.ForgeConfigError=e.ForgeConfigError,exports.ForgeDisposedError=e.ForgeDisposedError,exports.ForgeError=e.ForgeError,exports.ForgeSubmitError=e.ForgeSubmitError,exports.ForgeValidationError=e.ForgeValidationError,exports.createForm=t.createForm;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export { toFormData } from './adapters/form-data';
2
1
  export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';
3
2
  export { createForm } from './form';
4
3
  export * from './types';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,UAAU,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACpH,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,cAAc,SAAS,CAAC"}
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,UAAU,CAAC;AACpH,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,cAAc,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
1
  import { ForgeConfigError as e, ForgeDisposedError as t, ForgeError as n, ForgeSubmitError as r, ForgeValidationError as i } from "./errors.js";
2
- import { toFormData as a } from "./adapters/form-data.js";
3
- import { createForm as o } from "./form.js";
4
- export { e as ForgeConfigError, t as ForgeDisposedError, n as ForgeError, r as ForgeSubmitError, i as ForgeValidationError, o as createForm, a as toFormData };
2
+ import { createForm as a } from "./form.js";
3
+ export { e as ForgeConfigError, t as ForgeDisposedError, n as ForgeError, r as ForgeSubmitError, i as ForgeValidationError, a as createForm };
package/dist/spell.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./core/path.cjs");function t(e){return e.code===`invalid_union`}function n(e,t){return t.aborted?Promise.resolve(void 0):new Promise((n,r)=>{let i=()=>n(void 0);t.addEventListener(`abort`,i,{once:!0}),e.then(e=>{t.removeEventListener(`abort`,i),n(e)},e=>{t.removeEventListener(`abort`,i),r(e)})})}function r(e){let t=[];for(let n of e){if(typeof n==`number`)break;t.push(n)}return t}function i(e,t){return t.map(t=>({...t,path:[...e,...t.path]}))}function a(a){return async(o,s)=>{let c=await n(a.safeParseAsync(o),s);if(!c||c.success)return;let l=c.error.issues.flatMap(e=>{if(!t(e))return[e];let n=c.error.bestMatch(e);return n?i(e.path,n):[e]}),u={},d;for(let t of l){if(t.path.length===0){d??=t.message;continue}u=e.writeError(u,r(t.path),t.message)}let f={fields:Object.keys(u).length===0?void 0:u,formError:d};return f.fields===void 0&&f.formError===void 0?void 0:f}}exports.customValidator=a;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./core/path.cjs");function t(e){return e.code===`invalid_union`}function n(e,t){return t.aborted?Promise.resolve(void 0):new Promise((n,r)=>{let i=()=>n(void 0);t.addEventListener(`abort`,i,{once:!0}),e.then(e=>{t.removeEventListener(`abort`,i),n(e)},e=>{t.removeEventListener(`abort`,i),r(e)})})}function r(e,t){return t.map(t=>({...t,path:[...e,...t.path]}))}function i(i){return async(a,o)=>{let s=await n(i.safeParseAsync(a),o);if(!s||s.success)return;let c=s.error.issues.flatMap(e=>{if(!t(e))return[e];let n=s.error.bestMatch(e);return n?r(e.path,n):[e]}),l={},u;for(let t of c){if(t.path.length===0){u??=t.message;continue}l=e.writeError(l,t.path,t.message)}let d={fields:Object.keys(l).length===0?void 0:l,formError:u};return d.fields===void 0&&d.formError===void 0?void 0:d}}exports.customValidator=i;
2
2
  //# sourceMappingURL=spell.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"spell.cjs","names":[],"sources":["../src/spell.ts"],"sourcesContent":["import type { Issue, Schema, SchemaMode } from '@vielzeug/spell';\nimport { writeError } from './core/path';\nimport type { FormErrors, FormValidator, ValidationErrors } from './types';\n\ntype ErrorTree = Record<string, unknown>;\ntype UnionIssue = Extract<Issue, { code: 'invalid_union' }>;\n\nfunction isUnionIssue(issue: Issue): issue is UnionIssue {\n return issue.code === 'invalid_union';\n}\n\nfunction abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T | undefined> {\n if (signal.aborted) return Promise.resolve(undefined);\n\n return new Promise((resolve, reject) => {\n const abort = () => resolve(undefined);\n\n signal.addEventListener('abort', abort, { once: true });\n void promise.then(\n (value) => {\n signal.removeEventListener('abort', abort);\n resolve(value);\n },\n (error: unknown) => {\n signal.removeEventListener('abort', abort);\n reject(error);\n },\n );\n });\n}\n\nfunction fieldPath(path: readonly (string | number)[]): string[] {\n const segments: string[] = [];\n\n for (const segment of path) {\n if (typeof segment === 'number') break;\n\n segments.push(segment);\n }\n\n return segments;\n}\n\nfunction prefixIssues(path: readonly (string | number)[], issues: readonly Issue[]): Issue[] {\n return issues.map((issue) => ({ ...issue, path: [...path, ...issue.path] }));\n}\n\n/** Adapts a Spell Schema into Forge's full-form validator without transforming form values. */\nexport function customValidator<TValues extends Record<string, unknown>>(\n schema: Schema<unknown, TValues, SchemaMode>,\n): FormValidator<TValues> {\n return async (values, signal) => {\n const result = await abortable(schema.safeParseAsync(values), signal);\n\n if (!result || result.success) return undefined;\n\n const issues = result.error.issues.flatMap((issue) => {\n if (!isUnionIssue(issue)) return [issue];\n\n const bestMatch = result.error.bestMatch(issue);\n\n return bestMatch ? prefixIssues(issue.path, bestMatch) : [issue];\n });\n let fields: ErrorTree = {};\n let formError: string | undefined;\n\n for (const issue of issues) {\n if (issue.path.length === 0) {\n formError ??= issue.message;\n continue;\n }\n\n fields = writeError(fields, fieldPath(issue.path), issue.message);\n }\n\n const errors: ValidationErrors<TValues> = {\n fields: Object.keys(fields).length === 0 ? undefined : (fields as FormErrors<TValues>),\n formError,\n };\n\n return errors.fields === undefined && errors.formError === undefined ? undefined : errors;\n };\n}\n"],"mappings":"sGAOA,SAAS,EAAa,EAAmC,CACvD,OAAO,EAAM,OAAS,eACxB,CAEA,SAAS,EAAa,EAAqB,EAA6C,CAGtF,OAFI,EAAO,QAAgB,QAAQ,QAAQ,IAAA,EAAS,EAE7C,IAAI,SAAS,EAAS,IAAW,CACtC,IAAM,MAAc,EAAQ,IAAA,EAAS,EAErC,EAAO,iBAAiB,QAAS,EAAO,CAAE,KAAM,EAAK,CAAC,EACtD,EAAa,KACV,GAAU,CACT,EAAO,oBAAoB,QAAS,CAAK,EACzC,EAAQ,CAAK,CACf,EACC,GAAmB,CAClB,EAAO,oBAAoB,QAAS,CAAK,EACzC,EAAO,CAAK,CACd,CACF,CACF,CAAC,CACH,CAEA,SAAS,EAAU,EAA8C,CAC/D,IAAM,EAAqB,CAAC,EAE5B,IAAK,IAAM,KAAW,EAAM,CAC1B,GAAI,OAAO,GAAY,SAAU,MAEjC,EAAS,KAAK,CAAO,CACvB,CAEA,OAAO,CACT,CAEA,SAAS,EAAa,EAAoC,EAAmC,CAC3F,OAAO,EAAO,IAAK,IAAW,CAAE,GAAG,EAAO,KAAM,CAAC,GAAG,EAAM,GAAG,EAAM,IAAI,CAAE,EAAE,CAC7E,CAGA,SAAgB,EACd,EACwB,CACxB,OAAO,MAAO,EAAQ,IAAW,CAC/B,IAAM,EAAS,MAAM,EAAU,EAAO,eAAe,CAAM,EAAG,CAAM,EAEpE,GAAI,CAAC,GAAU,EAAO,QAAS,OAE/B,IAAM,EAAS,EAAO,MAAM,OAAO,QAAS,GAAU,CACpD,GAAI,CAAC,EAAa,CAAK,EAAG,MAAO,CAAC,CAAK,EAEvC,IAAM,EAAY,EAAO,MAAM,UAAU,CAAK,EAE9C,OAAO,EAAY,EAAa,EAAM,KAAM,CAAS,EAAI,CAAC,CAAK,CACjE,CAAC,EACG,EAAoB,CAAC,EACrB,EAEJ,IAAK,IAAM,KAAS,EAAQ,CAC1B,GAAI,EAAM,KAAK,SAAW,EAAG,CAC3B,IAAc,EAAM,QACpB,QACF,CAEA,EAAS,EAAA,WAAW,EAAQ,EAAU,EAAM,IAAI,EAAG,EAAM,OAAO,CAClE,CAEA,IAAM,EAAoC,CACxC,OAAQ,OAAO,KAAK,CAAM,CAAC,CAAC,SAAW,EAAI,IAAA,GAAa,EACxD,WACF,EAEA,OAAO,EAAO,SAAW,IAAA,IAAa,EAAO,YAAc,IAAA,GAAY,IAAA,GAAY,CACrF,CACF"}
1
+ {"version":3,"file":"spell.cjs","names":[],"sources":["../src/spell.ts"],"sourcesContent":["import type { Issue, Schema, SchemaMode } from '@vielzeug/spell';\nimport type { ErrorTree } from './core/path';\nimport { writeError } from './core/path';\nimport type { FormErrors, FormValidator, ValidationErrors } from './types';\n\ntype UnionIssue = Extract<Issue, { code: 'invalid_union' }>;\n\nfunction isUnionIssue(issue: Issue): issue is UnionIssue {\n return issue.code === 'invalid_union';\n}\n\nfunction abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T | undefined> {\n if (signal.aborted) return Promise.resolve(undefined);\n\n return new Promise((resolve, reject) => {\n const abort = () => resolve(undefined);\n\n signal.addEventListener('abort', abort, { once: true });\n void promise.then(\n (value) => {\n signal.removeEventListener('abort', abort);\n resolve(value);\n },\n (error: unknown) => {\n signal.removeEventListener('abort', abort);\n reject(error);\n },\n );\n });\n}\n\nfunction prefixIssues(path: readonly (string | number)[], issues: readonly Issue[]): Issue[] {\n return issues.map((issue) => ({ ...issue, path: [...path, ...issue.path] }));\n}\n\n/** Adapts a Spell Schema into Forge's full-form validator without transforming form values. */\nexport function customValidator<TValues extends Record<string, unknown>>(\n schema: Schema<unknown, TValues, SchemaMode>,\n): FormValidator<TValues> {\n return async (values, signal) => {\n const result = await abortable(schema.safeParseAsync(values), signal);\n\n if (!result || result.success) return undefined;\n\n const issues = result.error.issues.flatMap((issue) => {\n if (!isUnionIssue(issue)) return [issue];\n\n const bestMatch = result.error.bestMatch(issue);\n\n return bestMatch ? prefixIssues(issue.path, bestMatch) : [issue];\n });\n let fields: ErrorTree = {};\n let formError: string | undefined;\n\n for (const issue of issues) {\n if (issue.path.length === 0) {\n formError ??= issue.message;\n continue;\n }\n\n fields = writeError(fields, issue.path, issue.message);\n }\n\n const errors: ValidationErrors<TValues> = {\n fields: Object.keys(fields).length === 0 ? undefined : (fields as FormErrors<TValues>),\n formError,\n };\n\n return errors.fields === undefined && errors.formError === undefined ? undefined : errors;\n };\n}\n"],"mappings":"sGAOA,SAAS,EAAa,EAAmC,CACvD,OAAO,EAAM,OAAS,eACxB,CAEA,SAAS,EAAa,EAAqB,EAA6C,CAGtF,OAFI,EAAO,QAAgB,QAAQ,QAAQ,IAAA,EAAS,EAE7C,IAAI,SAAS,EAAS,IAAW,CACtC,IAAM,MAAc,EAAQ,IAAA,EAAS,EAErC,EAAO,iBAAiB,QAAS,EAAO,CAAE,KAAM,EAAK,CAAC,EACtD,EAAa,KACV,GAAU,CACT,EAAO,oBAAoB,QAAS,CAAK,EACzC,EAAQ,CAAK,CACf,EACC,GAAmB,CAClB,EAAO,oBAAoB,QAAS,CAAK,EACzC,EAAO,CAAK,CACd,CACF,CACF,CAAC,CACH,CAEA,SAAS,EAAa,EAAoC,EAAmC,CAC3F,OAAO,EAAO,IAAK,IAAW,CAAE,GAAG,EAAO,KAAM,CAAC,GAAG,EAAM,GAAG,EAAM,IAAI,CAAE,EAAE,CAC7E,CAGA,SAAgB,EACd,EACwB,CACxB,OAAO,MAAO,EAAQ,IAAW,CAC/B,IAAM,EAAS,MAAM,EAAU,EAAO,eAAe,CAAM,EAAG,CAAM,EAEpE,GAAI,CAAC,GAAU,EAAO,QAAS,OAE/B,IAAM,EAAS,EAAO,MAAM,OAAO,QAAS,GAAU,CACpD,GAAI,CAAC,EAAa,CAAK,EAAG,MAAO,CAAC,CAAK,EAEvC,IAAM,EAAY,EAAO,MAAM,UAAU,CAAK,EAE9C,OAAO,EAAY,EAAa,EAAM,KAAM,CAAS,EAAI,CAAC,CAAK,CACjE,CAAC,EACG,EAAoB,CAAC,EACrB,EAEJ,IAAK,IAAM,KAAS,EAAQ,CAC1B,GAAI,EAAM,KAAK,SAAW,EAAG,CAC3B,IAAc,EAAM,QACpB,QACF,CAEA,EAAS,EAAA,WAAW,EAAQ,EAAM,KAAM,EAAM,OAAO,CACvD,CAEA,IAAM,EAAoC,CACxC,OAAQ,OAAO,KAAK,CAAM,CAAC,CAAC,SAAW,EAAI,IAAA,GAAa,EACxD,WACF,EAEA,OAAO,EAAO,SAAW,IAAA,IAAa,EAAO,YAAc,IAAA,GAAY,IAAA,GAAY,CACrF,CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"spell.d.ts","sourceRoot":"","sources":["../src/spell.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEjE,OAAO,KAAK,EAAc,aAAa,EAAoB,MAAM,SAAS,CAAC;AA6C3E,+FAA+F;AAC/F,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,GAC3C,aAAa,CAAC,OAAO,CAAC,CAgCxB"}
1
+ {"version":3,"file":"spell.d.ts","sourceRoot":"","sources":["../src/spell.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGjE,OAAO,KAAK,EAAc,aAAa,EAAoB,MAAM,SAAS,CAAC;AAgC3E,+FAA+F;AAC/F,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,GAC3C,aAAa,CAAC,OAAO,CAAC,CAgCxB"}
package/dist/spell.js CHANGED
@@ -13,44 +13,36 @@ function n(e, t) {
13
13
  });
14
14
  });
15
15
  }
16
- function r(e) {
17
- let t = [];
18
- for (let n of e) {
19
- if (typeof n == "number") break;
20
- t.push(n);
21
- }
22
- return t;
23
- }
24
- function i(e, t) {
16
+ function r(e, t) {
25
17
  return t.map((t) => ({
26
18
  ...t,
27
19
  path: [...e, ...t.path]
28
20
  }));
29
21
  }
30
- function a(a) {
31
- return async (o, s) => {
32
- let c = await n(a.safeParseAsync(o), s);
33
- if (!c || c.success) return;
34
- let l = c.error.issues.flatMap((e) => {
22
+ function i(i) {
23
+ return async (a, o) => {
24
+ let s = await n(i.safeParseAsync(a), o);
25
+ if (!s || s.success) return;
26
+ let c = s.error.issues.flatMap((e) => {
35
27
  if (!t(e)) return [e];
36
- let n = c.error.bestMatch(e);
37
- return n ? i(e.path, n) : [e];
38
- }), u = {}, d;
39
- for (let t of l) {
28
+ let n = s.error.bestMatch(e);
29
+ return n ? r(e.path, n) : [e];
30
+ }), l = {}, u;
31
+ for (let t of c) {
40
32
  if (t.path.length === 0) {
41
- d ??= t.message;
33
+ u ??= t.message;
42
34
  continue;
43
35
  }
44
- u = e(u, r(t.path), t.message);
36
+ l = e(l, t.path, t.message);
45
37
  }
46
- let f = {
47
- fields: Object.keys(u).length === 0 ? void 0 : u,
48
- formError: d
38
+ let d = {
39
+ fields: Object.keys(l).length === 0 ? void 0 : l,
40
+ formError: u
49
41
  };
50
- return f.fields === void 0 && f.formError === void 0 ? void 0 : f;
42
+ return d.fields === void 0 && d.formError === void 0 ? void 0 : d;
51
43
  };
52
44
  }
53
45
  //#endregion
54
- export { a as customValidator };
46
+ export { i as customValidator };
55
47
 
56
48
  //# sourceMappingURL=spell.js.map
package/dist/spell.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"spell.js","names":[],"sources":["../src/spell.ts"],"sourcesContent":["import type { Issue, Schema, SchemaMode } from '@vielzeug/spell';\nimport { writeError } from './core/path';\nimport type { FormErrors, FormValidator, ValidationErrors } from './types';\n\ntype ErrorTree = Record<string, unknown>;\ntype UnionIssue = Extract<Issue, { code: 'invalid_union' }>;\n\nfunction isUnionIssue(issue: Issue): issue is UnionIssue {\n return issue.code === 'invalid_union';\n}\n\nfunction abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T | undefined> {\n if (signal.aborted) return Promise.resolve(undefined);\n\n return new Promise((resolve, reject) => {\n const abort = () => resolve(undefined);\n\n signal.addEventListener('abort', abort, { once: true });\n void promise.then(\n (value) => {\n signal.removeEventListener('abort', abort);\n resolve(value);\n },\n (error: unknown) => {\n signal.removeEventListener('abort', abort);\n reject(error);\n },\n );\n });\n}\n\nfunction fieldPath(path: readonly (string | number)[]): string[] {\n const segments: string[] = [];\n\n for (const segment of path) {\n if (typeof segment === 'number') break;\n\n segments.push(segment);\n }\n\n return segments;\n}\n\nfunction prefixIssues(path: readonly (string | number)[], issues: readonly Issue[]): Issue[] {\n return issues.map((issue) => ({ ...issue, path: [...path, ...issue.path] }));\n}\n\n/** Adapts a Spell Schema into Forge's full-form validator without transforming form values. */\nexport function customValidator<TValues extends Record<string, unknown>>(\n schema: Schema<unknown, TValues, SchemaMode>,\n): FormValidator<TValues> {\n return async (values, signal) => {\n const result = await abortable(schema.safeParseAsync(values), signal);\n\n if (!result || result.success) return undefined;\n\n const issues = result.error.issues.flatMap((issue) => {\n if (!isUnionIssue(issue)) return [issue];\n\n const bestMatch = result.error.bestMatch(issue);\n\n return bestMatch ? prefixIssues(issue.path, bestMatch) : [issue];\n });\n let fields: ErrorTree = {};\n let formError: string | undefined;\n\n for (const issue of issues) {\n if (issue.path.length === 0) {\n formError ??= issue.message;\n continue;\n }\n\n fields = writeError(fields, fieldPath(issue.path), issue.message);\n }\n\n const errors: ValidationErrors<TValues> = {\n fields: Object.keys(fields).length === 0 ? undefined : (fields as FormErrors<TValues>),\n formError,\n };\n\n return errors.fields === undefined && errors.formError === undefined ? undefined : errors;\n };\n}\n"],"mappings":";;AAOA,SAAS,EAAa,GAAmC;CACvD,OAAO,EAAM,SAAS;AACxB;AAEA,SAAS,EAAa,GAAqB,GAA6C;CAGtF,OAFI,EAAO,UAAgB,QAAQ,QAAQ,KAAA,CAAS,IAE7C,IAAI,SAAS,GAAS,MAAW;EACtC,IAAM,UAAc,EAAQ,KAAA,CAAS;EAGrC,AADA,EAAO,iBAAiB,SAAS,GAAO,EAAE,MAAM,GAAK,CAAC,GACtD,EAAa,MACV,MAAU;GAET,AADA,EAAO,oBAAoB,SAAS,CAAK,GACzC,EAAQ,CAAK;EACf,IACC,MAAmB;GAElB,AADA,EAAO,oBAAoB,SAAS,CAAK,GACzC,EAAO,CAAK;EACd,CACF;CACF,CAAC;AACH;AAEA,SAAS,EAAU,GAA8C;CAC/D,IAAM,IAAqB,CAAC;CAE5B,KAAK,IAAM,KAAW,GAAM;EAC1B,IAAI,OAAO,KAAY,UAAU;EAEjC,EAAS,KAAK,CAAO;CACvB;CAEA,OAAO;AACT;AAEA,SAAS,EAAa,GAAoC,GAAmC;CAC3F,OAAO,EAAO,KAAK,OAAW;EAAE,GAAG;EAAO,MAAM,CAAC,GAAG,GAAM,GAAG,EAAM,IAAI;CAAE,EAAE;AAC7E;AAGA,SAAgB,EACd,GACwB;CACxB,OAAO,OAAO,GAAQ,MAAW;EAC/B,IAAM,IAAS,MAAM,EAAU,EAAO,eAAe,CAAM,GAAG,CAAM;EAEpE,IAAI,CAAC,KAAU,EAAO,SAAS;EAE/B,IAAM,IAAS,EAAO,MAAM,OAAO,SAAS,MAAU;GACpD,IAAI,CAAC,EAAa,CAAK,GAAG,OAAO,CAAC,CAAK;GAEvC,IAAM,IAAY,EAAO,MAAM,UAAU,CAAK;GAE9C,OAAO,IAAY,EAAa,EAAM,MAAM,CAAS,IAAI,CAAC,CAAK;EACjE,CAAC,GACG,IAAoB,CAAC,GACrB;EAEJ,KAAK,IAAM,KAAS,GAAQ;GAC1B,IAAI,EAAM,KAAK,WAAW,GAAG;IAC3B,MAAc,EAAM;IACpB;GACF;GAEA,IAAS,EAAW,GAAQ,EAAU,EAAM,IAAI,GAAG,EAAM,OAAO;EAClE;EAEA,IAAM,IAAoC;GACxC,QAAQ,OAAO,KAAK,CAAM,CAAC,CAAC,WAAW,IAAI,KAAA,IAAa;GACxD;EACF;EAEA,OAAO,EAAO,WAAW,KAAA,KAAa,EAAO,cAAc,KAAA,IAAY,KAAA,IAAY;CACrF;AACF"}
1
+ {"version":3,"file":"spell.js","names":[],"sources":["../src/spell.ts"],"sourcesContent":["import type { Issue, Schema, SchemaMode } from '@vielzeug/spell';\nimport type { ErrorTree } from './core/path';\nimport { writeError } from './core/path';\nimport type { FormErrors, FormValidator, ValidationErrors } from './types';\n\ntype UnionIssue = Extract<Issue, { code: 'invalid_union' }>;\n\nfunction isUnionIssue(issue: Issue): issue is UnionIssue {\n return issue.code === 'invalid_union';\n}\n\nfunction abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T | undefined> {\n if (signal.aborted) return Promise.resolve(undefined);\n\n return new Promise((resolve, reject) => {\n const abort = () => resolve(undefined);\n\n signal.addEventListener('abort', abort, { once: true });\n void promise.then(\n (value) => {\n signal.removeEventListener('abort', abort);\n resolve(value);\n },\n (error: unknown) => {\n signal.removeEventListener('abort', abort);\n reject(error);\n },\n );\n });\n}\n\nfunction prefixIssues(path: readonly (string | number)[], issues: readonly Issue[]): Issue[] {\n return issues.map((issue) => ({ ...issue, path: [...path, ...issue.path] }));\n}\n\n/** Adapts a Spell Schema into Forge's full-form validator without transforming form values. */\nexport function customValidator<TValues extends Record<string, unknown>>(\n schema: Schema<unknown, TValues, SchemaMode>,\n): FormValidator<TValues> {\n return async (values, signal) => {\n const result = await abortable(schema.safeParseAsync(values), signal);\n\n if (!result || result.success) return undefined;\n\n const issues = result.error.issues.flatMap((issue) => {\n if (!isUnionIssue(issue)) return [issue];\n\n const bestMatch = result.error.bestMatch(issue);\n\n return bestMatch ? prefixIssues(issue.path, bestMatch) : [issue];\n });\n let fields: ErrorTree = {};\n let formError: string | undefined;\n\n for (const issue of issues) {\n if (issue.path.length === 0) {\n formError ??= issue.message;\n continue;\n }\n\n fields = writeError(fields, issue.path, issue.message);\n }\n\n const errors: ValidationErrors<TValues> = {\n fields: Object.keys(fields).length === 0 ? undefined : (fields as FormErrors<TValues>),\n formError,\n };\n\n return errors.fields === undefined && errors.formError === undefined ? undefined : errors;\n };\n}\n"],"mappings":";;AAOA,SAAS,EAAa,GAAmC;CACvD,OAAO,EAAM,SAAS;AACxB;AAEA,SAAS,EAAa,GAAqB,GAA6C;CAGtF,OAFI,EAAO,UAAgB,QAAQ,QAAQ,KAAA,CAAS,IAE7C,IAAI,SAAS,GAAS,MAAW;EACtC,IAAM,UAAc,EAAQ,KAAA,CAAS;EAGrC,AADA,EAAO,iBAAiB,SAAS,GAAO,EAAE,MAAM,GAAK,CAAC,GACtD,EAAa,MACV,MAAU;GAET,AADA,EAAO,oBAAoB,SAAS,CAAK,GACzC,EAAQ,CAAK;EACf,IACC,MAAmB;GAElB,AADA,EAAO,oBAAoB,SAAS,CAAK,GACzC,EAAO,CAAK;EACd,CACF;CACF,CAAC;AACH;AAEA,SAAS,EAAa,GAAoC,GAAmC;CAC3F,OAAO,EAAO,KAAK,OAAW;EAAE,GAAG;EAAO,MAAM,CAAC,GAAG,GAAM,GAAG,EAAM,IAAI;CAAE,EAAE;AAC7E;AAGA,SAAgB,EACd,GACwB;CACxB,OAAO,OAAO,GAAQ,MAAW;EAC/B,IAAM,IAAS,MAAM,EAAU,EAAO,eAAe,CAAM,GAAG,CAAM;EAEpE,IAAI,CAAC,KAAU,EAAO,SAAS;EAE/B,IAAM,IAAS,EAAO,MAAM,OAAO,SAAS,MAAU;GACpD,IAAI,CAAC,EAAa,CAAK,GAAG,OAAO,CAAC,CAAK;GAEvC,IAAM,IAAY,EAAO,MAAM,UAAU,CAAK;GAE9C,OAAO,IAAY,EAAa,EAAM,MAAM,CAAS,IAAI,CAAC,CAAK;EACjE,CAAC,GACG,IAAoB,CAAC,GACrB;EAEJ,KAAK,IAAM,KAAS,GAAQ;GAC1B,IAAI,EAAM,KAAK,WAAW,GAAG;IAC3B,MAAc,EAAM;IACpB;GACF;GAEA,IAAS,EAAW,GAAQ,EAAM,MAAM,EAAM,OAAO;EACvD;EAEA,IAAM,IAAoC;GACxC,QAAQ,OAAO,KAAK,CAAM,CAAC,CAAC,WAAW,IAAI,KAAA,IAAa;GACxD;EACF;EAEA,OAAO,EAAO,WAAW,KAAA,KAAa,EAAO,cAAc,KAAA,IAAY,KAAA,IAAY;CACrF;AACF"}
package/dist/types.d.ts CHANGED
@@ -3,7 +3,7 @@ export type MaybePromise<T> = T | PromiseLike<T>;
3
3
  export type ReadonlyDeep<T> = T extends (...args: never[]) => unknown ? T : T extends readonly (infer Item)[] ? readonly ReadonlyDeep<Item>[] : T extends Record<string, unknown> ? {
4
4
  readonly [K in keyof T]: ReadonlyDeep<T[K]>;
5
5
  } : T;
6
- export type FormErrors<T> = T extends readonly unknown[] ? string : T extends Record<string, unknown> ? string | {
6
+ export type FormErrors<T> = T extends readonly (infer Item)[] ? string | readonly (FormErrors<Item> | undefined)[] : T extends Record<string, unknown> ? string | {
7
7
  readonly [K in keyof T]?: FormErrors<T[K]>;
8
8
  } : string;
9
9
  export type ValidationErrors<TValues extends Record<string, unknown>> = Readonly<{
@@ -26,12 +26,13 @@ export type FieldState<V> = Readonly<{
26
26
  value: ReadonlyDeep<V>;
27
27
  }>;
28
28
  export type FormState<TValues extends Record<string, unknown>> = Readonly<{
29
- error: string | undefined;
30
29
  errors: FormErrors<TValues> | undefined;
30
+ formError: string | undefined;
31
+ hasErrors: boolean;
31
32
  submitCount: number;
32
33
  submitting: boolean;
33
34
  touched: boolean;
34
- valid: boolean;
35
+ validity: 'invalid' | 'unknown' | 'valid';
35
36
  validating: boolean;
36
37
  }>;
37
38
  export type ValidationResult<TValues extends Record<string, unknown>> = Readonly<{
@@ -44,18 +45,18 @@ export type ValidationResult<TValues extends Record<string, unknown>> = Readonly
44
45
  status: 'invalid';
45
46
  }>;
46
47
  export type SubmitResult<TResult = void, TValues extends Record<string, unknown> = Record<string, unknown>> = Readonly<{
47
- ok: true;
48
- value: TResult;
49
- }> | Readonly<{
50
- ok: false;
51
- type: 'aborted';
48
+ status: 'aborted';
52
49
  }> | Readonly<{
50
+ status: 'invalid';
53
51
  errors: FormErrors<TValues> | undefined;
54
52
  formError: string | undefined;
55
- ok: false;
56
- type: 'validation';
53
+ }> | Readonly<{
54
+ status: 'ok';
55
+ value: TResult;
57
56
  }>;
58
- type ChildField<V> = NonNullable<V> extends readonly unknown[] ? Record<never, never> : NonNullable<V> extends Record<string, unknown> ? {
57
+ type ChildField<V> = NonNullable<V> extends readonly (infer Item)[] ? {
58
+ field(index: number): Field<Item>;
59
+ } : NonNullable<V> extends Record<string, unknown> ? {
59
60
  field<K extends keyof NonNullable<V> & string>(key: K): Field<NonNullable<V>[K]>;
60
61
  } : Record<never, never>;
61
62
  export type Field<V> = ChildField<V> & {
@@ -63,6 +64,7 @@ export type Field<V> = ChildField<V> & {
63
64
  readonly error: string | undefined;
64
65
  reset(): void;
65
66
  set(next: V | ((previous: ReadonlyDeep<V>) => V)): void;
67
+ readonly state: FieldState<V>;
66
68
  subscribe(listener: (state: FieldState<V>) => void, options?: SubscribeOptions): Unsubscribe;
67
69
  touch(): void;
68
70
  readonly touched: boolean;
@@ -77,7 +79,7 @@ export type Form<TValues extends Record<string, unknown>> = {
77
79
  reset(next?: TValues): void;
78
80
  set(next: TValues | ((previous: ReadonlyDeep<TValues>) => TValues)): void;
79
81
  readonly state: FormState<TValues>;
80
- submit<TResult = void>(handler: (values: ReadonlyDeep<TValues>) => MaybePromise<TResult>): Promise<SubmitResult<TResult, TValues>>;
82
+ submit<TResult = void>(handler: (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<TResult>, signal?: AbortSignal): Promise<SubmitResult<TResult, TValues>>;
81
83
  subscribe(listener: (state: FormState<TValues>) => void, options?: SubscribeOptions): Unsubscribe;
82
84
  validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;
83
85
  readonly value: ReadonlyDeep<TValues>;