@madojs/mado 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/AGENTS.md +15 -8
  2. package/CHANGELOG.md +66 -0
  3. package/README.md +25 -3
  4. package/dist/src/component.d.ts +3 -1
  5. package/dist/src/component.d.ts.map +1 -0
  6. package/dist/src/component.js +65 -30
  7. package/dist/src/component.js.map +1 -1
  8. package/dist/src/context.d.ts +13 -32
  9. package/dist/src/context.d.ts.map +1 -0
  10. package/dist/src/context.js +73 -48
  11. package/dist/src/context.js.map +1 -1
  12. package/dist/src/css.d.ts +1 -0
  13. package/dist/src/css.d.ts.map +1 -0
  14. package/dist/src/css.js.map +1 -1
  15. package/dist/src/devtools-hook.d.ts +14 -0
  16. package/dist/src/devtools-hook.d.ts.map +1 -0
  17. package/dist/src/devtools-hook.js +27 -0
  18. package/dist/src/devtools-hook.js.map +1 -0
  19. package/dist/src/devtools.d.ts +24 -22
  20. package/dist/src/devtools.d.ts.map +1 -0
  21. package/dist/src/devtools.js +230 -58
  22. package/dist/src/devtools.js.map +1 -1
  23. package/dist/src/diagnostics.d.ts +12 -0
  24. package/dist/src/diagnostics.d.ts.map +1 -0
  25. package/dist/src/diagnostics.js +68 -8
  26. package/dist/src/diagnostics.js.map +1 -1
  27. package/dist/src/each.d.ts +1 -5
  28. package/dist/src/each.d.ts.map +1 -0
  29. package/dist/src/each.js +0 -8
  30. package/dist/src/each.js.map +1 -1
  31. package/dist/src/forms.d.ts +27 -96
  32. package/dist/src/forms.d.ts.map +1 -0
  33. package/dist/src/forms.js +184 -394
  34. package/dist/src/forms.js.map +1 -1
  35. package/dist/src/head.d.ts +1 -0
  36. package/dist/src/head.d.ts.map +1 -0
  37. package/dist/src/head.js +2 -1
  38. package/dist/src/head.js.map +1 -1
  39. package/dist/src/html/bindings.d.ts +1 -0
  40. package/dist/src/html/bindings.d.ts.map +1 -0
  41. package/dist/src/html/bindings.js.map +1 -1
  42. package/dist/src/html/parser.d.ts +3 -4
  43. package/dist/src/html/parser.d.ts.map +1 -0
  44. package/dist/src/html/parser.js +63 -14
  45. package/dist/src/html/parser.js.map +1 -1
  46. package/dist/src/html/template-types.d.ts +1 -0
  47. package/dist/src/html/template-types.d.ts.map +1 -0
  48. package/dist/src/html/template-types.js.map +1 -1
  49. package/dist/src/html/template.d.ts +5 -1
  50. package/dist/src/html/template.d.ts.map +1 -0
  51. package/dist/src/html/template.js +78 -3
  52. package/dist/src/html/template.js.map +1 -1
  53. package/dist/src/index.d.ts +7 -8
  54. package/dist/src/index.d.ts.map +1 -0
  55. package/dist/src/index.js +5 -7
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/json.d.ts +3 -0
  58. package/dist/src/json.d.ts.map +1 -0
  59. package/dist/src/json.js +14 -0
  60. package/dist/src/json.js.map +1 -0
  61. package/dist/src/lifecycle.d.ts +1 -0
  62. package/dist/src/lifecycle.d.ts.map +1 -0
  63. package/dist/src/lifecycle.js +3 -2
  64. package/dist/src/lifecycle.js.map +1 -1
  65. package/dist/src/page.d.ts +1 -0
  66. package/dist/src/page.d.ts.map +1 -0
  67. package/dist/src/page.js.map +1 -1
  68. package/dist/src/persisted.d.ts +6 -1
  69. package/dist/src/persisted.d.ts.map +1 -0
  70. package/dist/src/persisted.js +17 -11
  71. package/dist/src/persisted.js.map +1 -1
  72. package/dist/src/resource.d.ts +2 -1
  73. package/dist/src/resource.d.ts.map +1 -0
  74. package/dist/src/resource.js +126 -49
  75. package/dist/src/resource.js.map +1 -1
  76. package/dist/src/router/base.d.ts +1 -0
  77. package/dist/src/router/base.d.ts.map +1 -0
  78. package/dist/src/router/base.js.map +1 -1
  79. package/dist/src/router/manifest.d.ts +1 -0
  80. package/dist/src/router/manifest.d.ts.map +1 -0
  81. package/dist/src/router/manifest.js +89 -148
  82. package/dist/src/router/manifest.js.map +1 -1
  83. package/dist/src/router/match.d.ts +1 -0
  84. package/dist/src/router/match.d.ts.map +1 -0
  85. package/dist/src/router/match.js +19 -5
  86. package/dist/src/router/match.js.map +1 -1
  87. package/dist/src/router/navigation.d.ts +1 -0
  88. package/dist/src/router/navigation.d.ts.map +1 -0
  89. package/dist/src/router/navigation.js.map +1 -1
  90. package/dist/src/signal.d.ts +1 -0
  91. package/dist/src/signal.d.ts.map +1 -0
  92. package/dist/src/signal.js +46 -17
  93. package/dist/src/signal.js.map +1 -1
  94. package/dist/src/static-runtime.d.ts +1 -0
  95. package/dist/src/static-runtime.d.ts.map +1 -0
  96. package/dist/src/static-runtime.js.map +1 -1
  97. package/dist/src/vite/index.d.ts +1 -0
  98. package/dist/src/vite/index.d.ts.map +1 -0
  99. package/dist/src/vite/index.js +4 -3
  100. package/dist/src/vite/index.js.map +1 -1
  101. package/docs/architecture/v1-roadmap.md +19 -0
  102. package/docs/en/10-pages-and-components.md +9 -1
  103. package/docs/en/13-data.md +8 -6
  104. package/docs/en/14-forms.md +66 -219
  105. package/docs/en/15-static-snapshots.md +4 -4
  106. package/docs/en/16-app-architecture.md +3 -2
  107. package/docs/en/20-deployment.md +21 -8
  108. package/docs/en/21-error-handling.md +9 -6
  109. package/docs/en/24-devtools-and-diagnostics.md +45 -0
  110. package/docs/en/30-api-freeze-map.md +8 -9
  111. package/docs/en/32-v1-stability.md +2 -2
  112. package/docs/en/33-migration-0.12-0.13.md +56 -0
  113. package/docs/en/40-llm-guide.md +7 -9
  114. package/docs/en/41-for-backenders.md +13 -9
  115. package/docs/en/42-why-mado.md +2 -2
  116. package/docs/en/README.md +2 -0
  117. package/docs/recipes/nginx/nginx.conf +2 -2
  118. package/llms.txt +25 -7
  119. package/package.json +37 -16
  120. package/scripts/clean.mjs +8 -0
  121. package/scripts/cli/generate.mjs +11 -14
  122. package/scripts/cli/help.mjs +2 -0
  123. package/scripts/cli/index.mjs +14 -6
  124. package/scripts/cli/init.mjs +13 -18
  125. package/scripts/cli/release.mjs +22 -19
  126. package/scripts/cli/run.mjs +5 -3
  127. package/scripts/logger.mjs +117 -0
  128. package/scripts/output-guard.mjs +128 -0
  129. package/scripts/preview.mjs +11 -19
  130. package/scripts/static/browser.mjs +58 -8
  131. package/scripts/static/discover.mjs +4 -1
  132. package/scripts/static.mjs +21 -7
  133. package/starters/default/package.json +4 -4
  134. package/starters/default/tsconfig.node.json +13 -0
  135. package/starters/default/vite.config.ts +0 -3
  136. package/starters/modular/README.md +4 -4
  137. package/starters/modular/package.json +4 -4
  138. package/starters/modular/src/layouts/app-shell.layout.ts +1 -2
  139. package/starters/modular/src/modules/auth/login.page.ts +5 -12
  140. package/starters/modular/src/modules/billing/pages/invoice-detail.page.ts +3 -4
  141. package/starters/modular/src/modules/billing/pages/invoices-list.page.ts +1 -1
  142. package/starters/modular/src/shared/styles/reset.css +21 -1
  143. package/starters/modular/tsconfig.json +10 -1
  144. package/starters/modular/tsconfig.node.json +13 -0
  145. package/starters/modular/vite.config.ts +6 -5
  146. package/dist/src/lazy.d.ts +0 -38
  147. package/dist/src/lazy.js +0 -73
  148. package/dist/src/lazy.js.map +0 -1
  149. package/scripts/bake.mjs +0 -5
  150. package/scripts/docs-lint.mjs +0 -170
  151. package/scripts/package-smoke.mjs +0 -135
  152. package/scripts/release-notes.mjs +0 -66
  153. package/scripts/size-budget.mjs +0 -95
  154. package/starters/modular/src/shared/ui/x-button.component.ts +0 -49
package/dist/src/forms.js CHANGED
@@ -1,456 +1,246 @@
1
1
  /**
2
- * Forms without pain. Built on native `<form>` submit/input events plus a
3
- * small schema validator whose rules mirror common HTML constraints.
4
- *
5
- * const f = useForm({
6
- * email: { required: true, type: 'email' },
7
- * age: { min: 18 },
8
- * });
9
- *
10
- * html`
11
- * <form @submit=${f.onSubmit(async values => { await save(values) })}>
12
- * <input name="email" .value=${() => f.values().email ?? ''}
13
- * @input=${f.onInput} />
14
- * ${() => f.errors().email ? html`<err>${f.errors().email}</err>` : null}
15
- *
16
- * <input name="age" type="number" .value=${() => f.values().age ?? ''}
17
- * @input=${f.onInput} />
18
- *
19
- * <button ?disabled=${() => !f.isValid() || f.submitting()}>Save</button>
20
- * </form>
21
- * `;
22
- *
23
- * What's inside:
24
- * - validation rules mirror common HTML constraints
25
- * (required, min, max, pattern, type=email/url/number, etc.);
26
- * - custom rules via a validate(values) function returning
27
- * { field: 'msg' } or null;
28
- * - all fields are automatically "touched" on blur — errors
29
- * are shown only after user interaction;
30
- * - submit() is aborted if the form is invalid.
31
- *
32
- * No Yup/Zod/Formik dependencies. If you really want them —
33
- * pass your own validate(values) and wire it to anything.
2
+ * Typed form state over the browser's native form and constraint-validation
3
+ * model. HTML owns required/type/min/max/pattern; Mado owns reactive state,
4
+ * touched/dirty flags, async validation cancellation and submit state.
34
5
  */
35
- import { signal, computed } from "./signal.js";
36
- export function useForm(schema, options = {}) {
37
- const defaults = () => {
38
- const out = {};
39
- for (const k in schema) {
40
- const s = schema[k];
41
- if (s.default !== undefined)
42
- out[k] = cloneFormValue(s.default);
43
- }
44
- return out;
45
- };
46
- const values = signal(defaults());
6
+ import { computed, signal } from "./signal.js";
7
+ import { reportError } from "./diagnostics.js";
8
+ export function useForm(options) {
9
+ let initial = cloneValues(options.initial);
10
+ const values = signal(cloneValues(initial));
11
+ const nativeErrors = signal({});
12
+ const customErrors = signal({});
47
13
  const touched = signal({});
48
14
  const submitting = signal(false);
49
- const asyncErrors = signal({});
50
- const validatingFields = signal({});
51
- const validating = computed(() => Object.keys(validatingFields()).length > 0);
52
- const fieldRuns = new Map();
53
- let formRun = 0;
54
- let formAsyncPaths = [];
55
- const syncErrors = computed(() => {
56
- const v = values();
57
- const out = {};
58
- for (const pattern in schema) {
59
- const s = schema[pattern];
60
- const paths = expandSchemaPath(v, pattern);
61
- for (const name of paths)
62
- validateSyncField(out, s, v, name);
63
- }
64
- if (options.validate) {
65
- const custom = options.validate(v);
66
- if (custom)
67
- Object.assign(out, custom);
68
- }
69
- return out;
70
- });
15
+ const validating = signal(false);
71
16
  const errors = computed(() => ({
72
- ...syncErrors(),
73
- ...asyncErrors(),
17
+ ...nativeErrors(),
18
+ ...customErrors(),
74
19
  }));
20
+ const dirty = computed(() => !sameValues(values(), initial));
75
21
  const isValid = computed(() => Object.keys(errors()).length === 0);
76
- function validateSyncField(out, s, v, name) {
77
- const raw = getPath(v, name);
78
- const isEmpty = raw === undefined || raw === "" || raw === null;
79
- if (s.required && isEmpty) {
80
- out[name] = "required field";
81
- return;
82
- }
83
- if (isEmpty)
84
- return;
85
- if (s.type === "email" && typeof raw === "string") {
86
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw))
87
- out[name] = "invalid email";
88
- }
89
- else if (s.type === "url" && typeof raw === "string") {
90
- try {
91
- new URL(raw);
92
- }
93
- catch {
94
- out[name] = "invalid URL";
95
- }
96
- }
97
- else if (s.type === "number") {
98
- const n = Number(raw);
99
- if (Number.isNaN(n))
100
- out[name] = "must be a number";
101
- else {
102
- if (s.min !== undefined && n < s.min)
103
- out[name] = `minimum ${s.min}`;
104
- if (s.max !== undefined && n > s.max)
105
- out[name] = `maximum ${s.max}`;
106
- }
107
- }
108
- if (typeof raw === "string") {
109
- if (s.minLength !== undefined && raw.length < s.minLength) {
110
- out[name] = `minimum ${s.minLength} characters`;
111
- }
112
- if (s.maxLength !== undefined && raw.length > s.maxLength) {
113
- out[name] = `maximum ${s.maxLength} characters`;
114
- }
115
- if (s.pattern && !new RegExp(s.pattern).test(raw)) {
116
- out[name] = "invalid format";
117
- }
118
- }
119
- if (s.validate) {
120
- const custom = s.validate(raw, v, name);
121
- if (custom)
122
- out[name] = custom;
123
- }
124
- }
125
- const setFieldValidating = (name, enabled) => {
126
- validatingFields.update((m) => {
127
- if (enabled)
128
- return { ...m, [name]: true };
129
- const next = { ...m };
130
- delete next[name];
131
- return next;
132
- });
22
+ let lastForm = null;
23
+ let validationController = null;
24
+ let validationRun = 0;
25
+ let activeSubmits = 0;
26
+ const rememberForm = (event, control) => {
27
+ const current = event.currentTarget;
28
+ if (isForm(current))
29
+ lastForm = current;
30
+ else if (control?.form)
31
+ lastForm = control.form;
133
32
  };
134
- const setAsyncErrors = (clearPaths, nextErrors) => {
135
- asyncErrors.update((prev) => {
136
- const next = { ...prev };
137
- for (const path of clearPaths)
138
- delete next[path];
139
- for (const [path, message] of Object.entries(nextErrors)) {
140
- if (message)
141
- next[path] = message;
142
- else
143
- delete next[path];
144
- }
145
- return next;
146
- });
33
+ const setField = (name, value) => {
34
+ values.update((current) => ({ ...current, [name]: value }));
35
+ customErrors.update((current) => withoutKey(current, name));
147
36
  };
148
- const clearAsyncPrefix = (prefix) => {
149
- asyncErrors.update((prev) => clearRecordPrefix(prev, prefix));
150
- };
151
- // Invalidate every in-flight async validation whose path is at or under
152
- // `prefix`. Bumping the generation makes a stale validateField() see
153
- // fieldRuns.get(name) !== run in its finally and skip writing its result —
154
- // otherwise a result for e.g. "items.2.title" would land on a row that an
155
- // array mutation (remove/move/replace) has since shifted. (FABLE_REPORT #5)
156
- const invalidateRunsPrefix = (prefix) => {
157
- const dotted = `${prefix}.`;
158
- for (const name of fieldRuns.keys()) {
159
- if (name === prefix || name.startsWith(dotted)) {
160
- fieldRuns.set(name, (fieldRuns.get(name) ?? 0) + 1);
161
- setFieldValidating(name, false);
162
- }
37
+ const validate = async (form = lastForm) => {
38
+ if (form)
39
+ lastForm = form;
40
+ validationController?.abort();
41
+ const controller = new AbortController();
42
+ validationController = controller;
43
+ const run = ++validationRun;
44
+ const nextNative = {};
45
+ for (const control of controlsOf(form)) {
46
+ const message = nativeValidationMessage(control);
47
+ if (message)
48
+ nextNative[control.name] = message;
163
49
  }
164
- };
165
- const validateField = async (name) => {
166
- const matching = Object.entries(schema).filter(([pattern, s]) => s.validateAsync && schemaPathMatches(pattern, name));
167
- const run = (fieldRuns.get(name) ?? 0) + 1;
168
- fieldRuns.set(name, run);
169
- setAsyncErrors([name], {});
170
- if (matching.length === 0)
171
- return !errors.peek()[name];
172
- setFieldValidating(name, true);
173
- const snapshot = values.peek();
174
- const next = {};
50
+ nativeErrors.set(nextNative);
51
+ if (!options.validate) {
52
+ if (validationController === controller)
53
+ validationController = null;
54
+ return Object.keys(nextNative).length === 0;
55
+ }
56
+ validating.set(true);
175
57
  try {
176
- for (const [, s] of matching) {
177
- const message = await s.validateAsync(getPath(snapshot, name), snapshot, name);
178
- if (message) {
179
- next[name] = message;
180
- break;
181
- }
182
- }
58
+ const result = await options.validate(values.peek(), {
59
+ signal: controller.signal,
60
+ form,
61
+ });
62
+ if (controller.signal.aborted || run !== validationRun)
63
+ return false;
64
+ customErrors.set(result ?? {});
65
+ return Object.keys({ ...nextNative, ...(result ?? {}) }).length === 0;
183
66
  }
184
- catch (err) {
185
- next[name] = "validation failed";
186
- // eslint-disable-next-line no-console
187
- console.error("[mado] field validator threw:", err);
67
+ catch (error) {
68
+ if (controller.signal.aborted || run !== validationRun)
69
+ return false;
70
+ customErrors.set({ $form: "validation failed" });
71
+ reportError("forms", "validator", "form validator threw", error);
72
+ return false;
188
73
  }
189
74
  finally {
190
- if (fieldRuns.get(name) === run) {
191
- setAsyncErrors([name], next);
192
- setFieldValidating(name, false);
193
- }
194
- }
195
- return !errors.peek()[name];
196
- };
197
- const validateAll = async () => {
198
- const run = ++formRun;
199
- if (Object.keys(syncErrors.peek()).length > 0)
200
- return false;
201
- if (options.validateAsync)
202
- setFieldValidating("$form", true);
203
- const snapshot = values.peek();
204
- const paths = concreteSchemaPaths(snapshot, schema);
205
- await Promise.all(paths.map((path) => validateField(path)));
206
- if (options.validateAsync) {
207
- try {
208
- const custom = await options.validateAsync(values.peek());
209
- if (formRun === run) {
210
- setAsyncErrors(formAsyncPaths, custom ?? {});
211
- formAsyncPaths = Object.keys(custom ?? {});
212
- }
75
+ if (run === validationRun) {
76
+ validating.set(false);
77
+ validationController = null;
213
78
  }
214
- catch (err) {
215
- if (formRun === run) {
216
- setAsyncErrors(formAsyncPaths, { $form: "validation failed" });
217
- formAsyncPaths = ["$form"];
218
- }
219
- // eslint-disable-next-line no-console
220
- console.error("[mado] form validator threw:", err);
221
- }
222
- finally {
223
- if (formRun === run)
224
- setFieldValidating("$form", false);
225
- }
226
- }
227
- return Object.keys(errors.peek()).length === 0;
228
- };
229
- const readField = (el) => {
230
- if (el instanceof HTMLInputElement) {
231
- if (el.type === "checkbox")
232
- return el.checked;
233
- if (el.type === "number")
234
- return el.value === "" ? "" : Number(el.value);
235
79
  }
236
- return el.value;
237
80
  };
238
81
  const api = {
239
82
  values,
240
83
  errors,
241
84
  touched,
85
+ dirty,
242
86
  submitting,
243
87
  validating,
244
- validatingFields,
245
88
  isValid,
246
- onInput(e) {
247
- const t = e.target;
248
- if (!t.name)
89
+ onInput(event) {
90
+ const control = controlFromEvent(event);
91
+ if (!control?.name)
249
92
  return;
250
- api.setField(t.name, readField(t));
251
- void validateField(t.name);
93
+ rememberForm(event, control);
94
+ const name = control.name;
95
+ setField(name, readControlValue(control, values.peek()[name], lastForm));
96
+ const message = nativeValidationMessage(control);
97
+ nativeErrors.update((current) => message ? { ...current, [name]: message } : withoutKey(current, name));
252
98
  },
253
- onBlur(e) {
254
- const t = e.target;
255
- if (!t.name)
99
+ onBlur(event) {
100
+ const control = controlFromEvent(event);
101
+ if (!control?.name)
256
102
  return;
257
- touched.update((m) => ({ ...m, [t.name]: true }));
103
+ rememberForm(event, control);
104
+ touched.update((current) => ({ ...current, [control.name]: true }));
258
105
  },
259
106
  onSubmit(handler) {
260
- return (e) => {
261
- e.preventDefault();
262
- // mark all fields as touched to show all errors
263
- const all = {};
264
- for (const k in schema)
265
- all[k] = true;
266
- for (const k of concreteSchemaPaths(values.peek(), schema))
267
- all[k] = true;
268
- touched.set(all);
107
+ return (event) => {
108
+ event.preventDefault();
109
+ const form = isForm(event.currentTarget) ? event.currentTarget : lastForm;
110
+ if (form)
111
+ lastForm = form;
112
+ const allTouched = { ...touched.peek() };
113
+ for (const control of controlsOf(form))
114
+ allTouched[control.name] = true;
115
+ touched.set(allTouched);
269
116
  void (async () => {
270
- if (!(await validateAll()))
117
+ if (!(await validate(form))) {
118
+ form?.reportValidity?.();
271
119
  return;
120
+ }
121
+ activeSubmits++;
272
122
  submitting.set(true);
273
123
  try {
274
- await handler(values.peek());
124
+ await handler(values.peek(), event);
275
125
  }
276
- catch (err) {
277
- // eslint-disable-next-line no-console
278
- console.error("[mado] form submit threw:", err);
126
+ catch (error) {
127
+ reportError("forms", "submit", "form submit threw", error);
279
128
  }
280
129
  finally {
281
- submitting.set(false);
130
+ activeSubmits--;
131
+ submitting.set(activeSubmits > 0);
282
132
  }
283
133
  })();
284
134
  };
285
135
  },
286
- setField(name, value) {
287
- values.update((v) => setPath(v, name, value));
288
- setAsyncErrors([name], {});
289
- },
290
- validateField,
291
- validate: validateAll,
292
- array(name) {
293
- const read = () => {
294
- // Use values() (tracked) — not values.peek() — so that effects and
295
- // computed() that call items() re-run when the array changes via
296
- // append/remove/replace/etc.
297
- const value = getPath(values(), name);
298
- return Array.isArray(value) ? value : [];
299
- };
300
- const write = (items) => {
301
- values.update((v) => setPath(v, name, [...items]));
302
- touched.update((m) => clearRecordPrefix(m, name));
303
- clearAsyncPrefix(name);
304
- // Array indices shifted — invalidate in-flight async validations under
305
- // this array so their stale results don't land on the wrong row.
306
- invalidateRunsPrefix(name);
307
- };
308
- const clamp = (index, length) => Math.max(0, Math.min(index, length));
309
- return {
310
- items: read,
311
- path(index, field) {
312
- return field ? `${name}.${index}.${field}` : `${name}.${index}`;
313
- },
314
- append(value) {
315
- write([...read(), value]);
316
- },
317
- prepend(value) {
318
- write([value, ...read()]);
319
- },
320
- insert(index, value) {
321
- const items = read();
322
- const at = clamp(index, items.length);
323
- write([...items.slice(0, at), value, ...items.slice(at)]);
324
- },
325
- remove(index) {
326
- const items = read();
327
- if (index < 0 || index >= items.length)
328
- return;
329
- write(items.filter((_, i) => i !== index));
330
- },
331
- move(from, to) {
332
- const items = read();
333
- if (from < 0 || from >= items.length)
334
- return;
335
- const next = [...items];
336
- const [item] = next.splice(from, 1);
337
- next.splice(clamp(to, next.length), 0, item);
338
- write(next);
339
- },
340
- replace(items) {
341
- write(items);
342
- },
343
- set(index, value) {
344
- const items = read();
345
- if (index < 0 || index >= items.length)
346
- return;
347
- const next = [...items];
348
- next[index] = value;
349
- write(next);
350
- },
351
- };
352
- },
353
- reset() {
354
- values.set(defaults());
136
+ setField,
137
+ reset(nextInitial) {
138
+ validationRun++;
139
+ validationController?.abort();
140
+ validationController = null;
141
+ validating.set(false);
142
+ if (nextInitial)
143
+ initial = cloneValues(nextInitial);
144
+ values.set(cloneValues(initial));
145
+ nativeErrors.set({});
146
+ customErrors.set({});
355
147
  touched.set({});
356
- asyncErrors.set({});
357
- validatingFields.set({});
358
- formAsyncPaths = [];
359
- submitting.set(false);
148
+ lastForm?.reset?.();
360
149
  },
150
+ validate,
361
151
  };
362
152
  return api;
363
153
  }
364
- function cloneFormValue(value) {
365
- if (Array.isArray(value))
366
- return value.map(cloneFormValue);
367
- if (value && typeof value === "object") {
368
- const out = {};
369
- for (const [k, v] of Object.entries(value))
370
- out[k] = cloneFormValue(v);
371
- return out;
154
+ function controlFromEvent(event) {
155
+ const path = event.composedPath?.() ?? [];
156
+ for (const candidate of path) {
157
+ if (isFormControl(candidate))
158
+ return candidate;
372
159
  }
373
- return value;
160
+ return isFormControl(event.target) ? event.target : null;
374
161
  }
375
- function getPath(value, path) {
376
- let current = value;
377
- for (const part of splitPath(path)) {
378
- if (current == null || typeof current !== "object")
379
- return undefined;
380
- current = current[part];
381
- }
382
- return current;
162
+ function isFormControl(value) {
163
+ if (!value || typeof value !== "object")
164
+ return false;
165
+ const candidate = value;
166
+ return typeof candidate.name === "string" &&
167
+ ("value" in candidate || String(candidate.tagName).toLowerCase() === "select");
383
168
  }
384
- function setPath(root, path, value) {
385
- return setPathParts(root, splitPath(path), value);
169
+ function isForm(value) {
170
+ return !!value && typeof value === "object" &&
171
+ String(value.tagName).toLowerCase() === "form";
386
172
  }
387
- function setPathParts(current, parts, value) {
388
- if (parts.length === 0)
389
- return value;
390
- const key = parts[0];
391
- const rest = parts.slice(1);
392
- const container = Array.isArray(current)
393
- ? [...current]
394
- : current && typeof current === "object"
395
- ? { ...current }
396
- : isArrayIndex(key)
397
- ? []
398
- : {};
399
- container[key] = setPathParts(container[key], rest, value);
400
- return container;
173
+ function controlsOf(form) {
174
+ if (!form)
175
+ return [];
176
+ const elements = form.elements ? Array.from(form.elements) :
177
+ Array.from(form.querySelectorAll("input,select,textarea"));
178
+ return elements.filter(isFormControl);
401
179
  }
402
- function splitPath(path) {
403
- return path.split(".").filter(Boolean);
180
+ function nativeValidationMessage(control) {
181
+ if (typeof control.checkValidity !== "function")
182
+ return null;
183
+ return control.checkValidity()
184
+ ? null
185
+ : control.validationMessage || "Invalid value";
404
186
  }
405
- function isArrayIndex(value) {
406
- return /^(0|[1-9]\d*)$/.test(value);
187
+ function readControlValue(control, previous, form) {
188
+ if (isInput(control)) {
189
+ if (control.type === "checkbox") {
190
+ if (Array.isArray(previous)) {
191
+ return controlsOf(form)
192
+ .filter((item) => isInput(item) && item.type === "checkbox" &&
193
+ item.name === control.name && item.checked)
194
+ .map((item) => item.value);
195
+ }
196
+ return control.checked;
197
+ }
198
+ if (control.type === "radio")
199
+ return control.checked ? control.value : previous;
200
+ if (control.type === "number" || control.type === "range") {
201
+ return control.value === "" ? "" : control.valueAsNumber;
202
+ }
203
+ if (control.type === "file") {
204
+ const files = Array.from(control.files ?? []);
205
+ return control.multiple ? files : files[0] ?? null;
206
+ }
207
+ }
208
+ if (isSelect(control) && control.multiple) {
209
+ return Array.from(control.selectedOptions, (option) => option.value);
210
+ }
211
+ return control.value;
407
212
  }
408
- function expandSchemaPath(values, pattern) {
409
- if (!pattern.includes("*"))
410
- return [pattern];
411
- return expandParts(values, splitPath(pattern), []);
213
+ function isInput(control) {
214
+ return String(control.tagName).toLowerCase() === "input";
412
215
  }
413
- function expandParts(current, parts, prefix) {
414
- if (parts.length === 0)
415
- return [prefix.join(".")];
416
- const [part, ...rest] = parts;
417
- if (part === "*") {
418
- if (!Array.isArray(current))
419
- return [];
420
- return current.flatMap((item, index) => expandParts(item, rest, [...prefix, String(index)]));
421
- }
422
- const next = current && typeof current === "object"
423
- ? current[part]
424
- : undefined;
425
- return expandParts(next, rest, [...prefix, part]);
216
+ function isSelect(control) {
217
+ return String(control.tagName).toLowerCase() === "select";
426
218
  }
427
- function concreteSchemaPaths(values, schema) {
428
- const paths = new Set();
429
- for (const pattern in schema) {
430
- for (const path of expandSchemaPath(values, pattern))
431
- paths.add(path);
432
- }
433
- return [...paths];
219
+ function cloneValues(value) {
220
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
221
+ key,
222
+ Array.isArray(item) ? [...item] : item,
223
+ ]));
434
224
  }
435
- function schemaPathMatches(pattern, path) {
436
- const patternParts = splitPath(pattern);
437
- const pathParts = splitPath(path);
438
- if (patternParts.length !== pathParts.length)
439
- return false;
440
- for (let i = 0; i < patternParts.length; i++) {
441
- const expected = patternParts[i];
442
- if (expected !== "*" && expected !== pathParts[i])
225
+ function sameValues(a, b) {
226
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
227
+ for (const key of keys) {
228
+ const left = a[key];
229
+ const right = b[key];
230
+ if (Array.isArray(left) && Array.isArray(right)) {
231
+ if (left.length !== right.length || left.some((item, i) => !Object.is(item, right[i]))) {
232
+ return false;
233
+ }
234
+ }
235
+ else if (!Object.is(left, right)) {
443
236
  return false;
237
+ }
444
238
  }
445
239
  return true;
446
240
  }
447
- function clearRecordPrefix(record, prefix) {
448
- const next = { ...record };
449
- const dotted = `${prefix}.`;
450
- for (const key of Object.keys(next)) {
451
- if (key === prefix || key.startsWith(dotted))
452
- delete next[key];
453
- }
241
+ function withoutKey(value, key) {
242
+ const next = { ...value };
243
+ delete next[key];
454
244
  return next;
455
245
  }
456
246
  //# sourceMappingURL=forms.js.map