@madojs/mado 0.11.1 → 0.13.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/AGENTS.md +116 -32
- package/CHANGELOG.md +216 -1
- package/README.md +158 -173
- package/dist/src/component.d.ts +6 -3
- package/dist/src/component.d.ts.map +1 -0
- package/dist/src/component.js +163 -36
- package/dist/src/component.js.map +1 -1
- package/dist/src/context.d.ts +13 -32
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +73 -48
- package/dist/src/context.js.map +1 -1
- package/dist/src/css.d.ts +1 -0
- package/dist/src/css.d.ts.map +1 -0
- package/dist/src/css.js +34 -5
- package/dist/src/css.js.map +1 -1
- package/dist/src/devtools-hook.d.ts +14 -0
- package/dist/src/devtools-hook.d.ts.map +1 -0
- package/dist/src/devtools-hook.js +27 -0
- package/dist/src/devtools-hook.js.map +1 -0
- package/dist/src/devtools.d.ts +24 -22
- package/dist/src/devtools.d.ts.map +1 -0
- package/dist/src/devtools.js +230 -58
- package/dist/src/devtools.js.map +1 -1
- package/dist/src/diagnostics.d.ts +12 -0
- package/dist/src/diagnostics.d.ts.map +1 -0
- package/dist/src/diagnostics.js +68 -8
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/each.d.ts +1 -5
- package/dist/src/each.d.ts.map +1 -0
- package/dist/src/each.js +0 -8
- package/dist/src/each.js.map +1 -1
- package/dist/src/forms.d.ts +27 -96
- package/dist/src/forms.d.ts.map +1 -0
- package/dist/src/forms.js +184 -394
- package/dist/src/forms.js.map +1 -1
- package/dist/src/head.d.ts +7 -6
- package/dist/src/head.d.ts.map +1 -0
- package/dist/src/head.js +8 -7
- package/dist/src/head.js.map +1 -1
- package/dist/src/html/bindings.d.ts +1 -0
- package/dist/src/html/bindings.d.ts.map +1 -0
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/html/parser.d.ts +4 -5
- package/dist/src/html/parser.d.ts.map +1 -0
- package/dist/src/html/parser.js +64 -15
- package/dist/src/html/parser.js.map +1 -1
- package/dist/src/html/template-types.d.ts +1 -0
- package/dist/src/html/template-types.d.ts.map +1 -0
- package/dist/src/html/template-types.js.map +1 -1
- package/dist/src/html/template.d.ts +5 -1
- package/dist/src/html/template.d.ts.map +1 -0
- package/dist/src/html/template.js +102 -13
- package/dist/src/html/template.js.map +1 -1
- package/dist/src/index.d.ts +15 -12
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -10
- package/dist/src/index.js.map +1 -1
- package/dist/src/json.d.ts +3 -0
- package/dist/src/json.d.ts.map +1 -0
- package/dist/src/json.js +14 -0
- package/dist/src/json.js.map +1 -0
- package/dist/src/lifecycle.d.ts +1 -0
- package/dist/src/lifecycle.d.ts.map +1 -0
- package/dist/src/lifecycle.js +3 -2
- package/dist/src/lifecycle.js.map +1 -1
- package/dist/src/page.d.ts +63 -28
- package/dist/src/page.d.ts.map +1 -0
- package/dist/src/page.js +5 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/persisted.d.ts +6 -1
- package/dist/src/persisted.d.ts.map +1 -0
- package/dist/src/persisted.js +17 -11
- package/dist/src/persisted.js.map +1 -1
- package/dist/src/resource.d.ts +2 -1
- package/dist/src/resource.d.ts.map +1 -0
- package/dist/src/resource.js +128 -50
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/base.d.ts +66 -0
- package/dist/src/router/base.d.ts.map +1 -0
- package/dist/src/router/base.js +168 -0
- package/dist/src/router/base.js.map +1 -0
- package/dist/src/router/manifest.d.ts +1 -0
- package/dist/src/router/manifest.d.ts.map +1 -0
- package/dist/src/router/manifest.js +156 -171
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +17 -2
- package/dist/src/router/match.d.ts.map +1 -0
- package/dist/src/router/match.js +60 -6
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.d.ts +1 -0
- package/dist/src/router/navigation.d.ts.map +1 -0
- package/dist/src/router/navigation.js +58 -9
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/signal.d.ts +1 -0
- package/dist/src/signal.d.ts.map +1 -0
- package/dist/src/signal.js +46 -17
- package/dist/src/signal.js.map +1 -1
- package/dist/src/static-runtime.d.ts +82 -0
- package/dist/src/static-runtime.d.ts.map +1 -0
- package/dist/src/static-runtime.js +209 -0
- package/dist/src/static-runtime.js.map +1 -0
- package/dist/src/vite/index.d.ts +32 -3
- package/dist/src/vite/index.d.ts.map +1 -0
- package/dist/src/vite/index.js +49 -17
- package/dist/src/vite/index.js.map +1 -1
- package/docs/README.md +5 -8
- package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
- package/docs/architecture/v1-roadmap.md +19 -0
- package/docs/en/00-the-mado-way.md +1 -1
- package/docs/en/01-quickstart.md +183 -0
- package/docs/en/10-pages-and-components.md +279 -0
- package/docs/en/11-templates-and-signals.md +211 -0
- package/docs/en/12-routing.md +229 -0
- package/docs/en/13-data.md +227 -0
- package/docs/en/14-forms.md +91 -0
- package/docs/en/15-static-snapshots.md +181 -0
- package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +56 -4
- package/docs/en/{13-deployment.md → 20-deployment.md} +79 -17
- package/docs/en/{15-error-handling.md → 21-error-handling.md} +9 -6
- package/docs/en/{14-testing.md → 22-testing.md} +6 -5
- package/docs/en/23-cookbook.md +292 -0
- package/docs/en/24-devtools-and-diagnostics.md +45 -0
- package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +17 -9
- package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +5 -5
- package/docs/en/33-migration-0.12-0.13.md +56 -0
- package/docs/en/40-llm-guide.md +774 -0
- package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +28 -25
- package/docs/en/{05-why-mado.md → 42-why-mado.md} +3 -3
- package/docs/en/README.md +99 -27
- package/docs/recipes/nginx/Containerfile +26 -0
- package/docs/recipes/nginx/nginx.conf +42 -0
- package/llms.txt +263 -210
- package/package.json +51 -23
- package/scripts/clean.mjs +8 -0
- package/scripts/cli/generate.mjs +57 -10
- package/scripts/cli/help.mjs +13 -5
- package/scripts/cli/index.mjs +31 -7
- package/scripts/cli/init.mjs +19 -19
- package/scripts/cli/release.mjs +38 -83
- package/scripts/cli/run.mjs +5 -3
- package/scripts/logger.mjs +117 -0
- package/scripts/output-guard.mjs +128 -0
- package/scripts/preview.mjs +105 -24
- package/scripts/static/browser.mjs +695 -0
- package/scripts/static/discover.mjs +284 -0
- package/scripts/static/output.mjs +141 -0
- package/scripts/static/serialize.mjs +212 -0
- package/scripts/static/server.mjs +167 -0
- package/scripts/static.mjs +205 -0
- package/starters/default/README.md +57 -55
- package/starters/default/package.json +6 -12
- package/starters/default/src/app.routes.ts +20 -33
- package/starters/default/src/components/feature-card.component.ts +40 -0
- package/starters/default/src/components/live-counter.component.ts +57 -0
- package/starters/default/src/content/guides.ts +55 -0
- package/starters/default/src/main.ts +9 -13
- package/starters/default/src/pages/app.page.ts +41 -0
- package/starters/default/src/pages/guide.page.ts +40 -0
- package/starters/default/src/pages/home.page.ts +49 -0
- package/starters/default/src/pages/not-found.page.ts +18 -0
- package/starters/default/src/styles/document.css +39 -0
- package/starters/default/src/styles/reset.css +37 -0
- package/starters/default/src/styles/tokens.css +30 -0
- package/starters/default/src/styles.d.ts +3 -1
- package/starters/default/src/vite-env.d.ts +1 -1
- package/starters/default/tsconfig.node.json +13 -0
- package/starters/default/vite.config.ts +14 -4
- package/starters/modular/README.md +142 -0
- package/starters/modular/index.html +13 -0
- package/starters/modular/package.json +30 -0
- package/starters/modular/src/app.routes.ts +39 -0
- package/starters/{default → modular}/src/layouts/app-shell.layout.ts +9 -7
- package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
- package/starters/modular/src/main.ts +16 -0
- package/starters/{default → modular}/src/modules/auth/login.page.ts +5 -12
- package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +3 -4
- package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +4 -4
- package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
- package/starters/modular/src/modules/home/not-found.page.ts +14 -0
- package/starters/{default → modular}/src/shared/styles/reset.css +21 -1
- package/starters/modular/src/styles.d.ts +1 -0
- package/starters/modular/src/vite-env.d.ts +1 -0
- package/starters/modular/tsconfig.json +33 -0
- package/starters/modular/tsconfig.node.json +13 -0
- package/starters/modular/vite.config.ts +132 -0
- package/TODO.md +0 -79
- package/dist/src/lazy.d.ts +0 -38
- package/dist/src/lazy.js +0 -73
- package/dist/src/lazy.js.map +0 -1
- package/docs/en/01-routing.md +0 -152
- package/docs/en/02-project-layout.md +0 -124
- package/docs/en/03-static-bake.md +0 -249
- package/docs/en/04-ide-setup.md +0 -162
- package/docs/en/07-llm-pitfalls.md +0 -724
- package/docs/en/08-llm-zero-history-test.md +0 -56
- package/docs/en/09-shadow-vs-light-dom.md +0 -174
- package/docs/en/11-layouts.md +0 -113
- package/docs/en/12-auth-and-api.md +0 -124
- package/docs/en/16-bake-cookbook.md +0 -100
- package/docs/en/17-shadow-dom-forms.md +0 -192
- package/docs/fr/00-the-mado-way.md +0 -85
- package/docs/fr/01-routing.md +0 -120
- package/docs/fr/02-project-layout.md +0 -84
- package/docs/fr/03-static-bake.md +0 -289
- package/docs/fr/04-ide-setup.md +0 -162
- package/docs/fr/05-why-mado.md +0 -193
- package/docs/fr/06-for-backenders.md +0 -438
- package/docs/fr/07-llm-pitfalls.md +0 -625
- package/docs/fr/08-llm-zero-history-test.md +0 -38
- package/docs/fr/09-shadow-vs-light-dom.md +0 -65
- package/docs/fr/10-app-architecture.md +0 -138
- package/docs/fr/11-layouts.md +0 -47
- package/docs/fr/12-auth-and-api.md +0 -76
- package/docs/fr/13-deployment.md +0 -57
- package/docs/fr/14-testing.md +0 -41
- package/docs/fr/15-error-handling.md +0 -50
- package/docs/fr/16-bake-cookbook.md +0 -88
- package/docs/fr/17-shadow-dom-forms.md +0 -196
- package/docs/fr/18-api-freeze-map.md +0 -63
- package/docs/fr/19-reactivity-ordering.md +0 -97
- package/docs/fr/20-v1-stability.md +0 -88
- package/docs/fr/README.md +0 -27
- package/docs/ru/00-the-mado-way.md +0 -84
- package/docs/ru/01-routing.md +0 -119
- package/docs/ru/02-project-layout.md +0 -84
- package/docs/ru/03-static-bake.md +0 -250
- package/docs/ru/04-ide-setup.md +0 -144
- package/docs/ru/05-why-mado.md +0 -193
- package/docs/ru/06-for-backenders.md +0 -428
- package/docs/ru/07-llm-pitfalls.md +0 -624
- package/docs/ru/08-llm-zero-history-test.md +0 -57
- package/docs/ru/09-shadow-vs-light-dom.md +0 -63
- package/docs/ru/10-app-architecture.md +0 -152
- package/docs/ru/11-layouts.md +0 -47
- package/docs/ru/12-auth-and-api.md +0 -75
- package/docs/ru/13-deployment.md +0 -66
- package/docs/ru/14-testing.md +0 -50
- package/docs/ru/15-error-handling.md +0 -56
- package/docs/ru/16-bake-cookbook.md +0 -95
- package/docs/ru/17-shadow-dom-forms.md +0 -193
- package/docs/ru/18-api-freeze-map.md +0 -64
- package/docs/ru/19-reactivity-ordering.md +0 -95
- package/docs/ru/20-v1-stability.md +0 -82
- package/docs/ru/README.md +0 -25
- package/docs/uk/00-the-mado-way.md +0 -82
- package/docs/uk/01-routing.md +0 -76
- package/docs/uk/02-project-layout.md +0 -73
- package/docs/uk/03-static-bake.md +0 -48
- package/docs/uk/04-ide-setup.md +0 -26
- package/docs/uk/05-why-mado.md +0 -34
- package/docs/uk/06-for-backenders.md +0 -55
- package/docs/uk/07-llm-pitfalls.md +0 -145
- package/docs/uk/08-llm-zero-history-test.md +0 -34
- package/docs/uk/09-shadow-vs-light-dom.md +0 -58
- package/docs/uk/10-app-architecture.md +0 -97
- package/docs/uk/11-layouts.md +0 -47
- package/docs/uk/12-auth-and-api.md +0 -70
- package/docs/uk/13-deployment.md +0 -40
- package/docs/uk/14-testing.md +0 -34
- package/docs/uk/15-error-handling.md +0 -32
- package/docs/uk/16-bake-cookbook.md +0 -36
- package/docs/uk/17-shadow-dom-forms.md +0 -193
- package/docs/uk/18-api-freeze-map.md +0 -61
- package/docs/uk/19-reactivity-ordering.md +0 -95
- package/docs/uk/20-v1-stability.md +0 -83
- package/docs/uk/README.md +0 -27
- package/scripts/bake.mjs +0 -570
- package/scripts/package-smoke.mjs +0 -77
- package/scripts/release-notes.mjs +0 -66
- package/scripts/size-budget.mjs +0 -91
- package/starters/default/src/modules/home/not-found.page.ts +0 -11
- package/starters/default/src/shared/ui/x-button.component.ts +0 -49
- /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
- /package/starters/{default → modular}/.editorconfig +0 -0
- /package/starters/{default → modular}/eslint.config.mjs +0 -0
- /package/starters/{default → modular}/public/favicon.svg +0 -0
- /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
- /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
- /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
package/dist/src/forms.js
CHANGED
|
@@ -1,456 +1,246 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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 {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
-
...
|
|
73
|
-
...
|
|
17
|
+
...nativeErrors(),
|
|
18
|
+
...customErrors(),
|
|
74
19
|
}));
|
|
20
|
+
const dirty = computed(() => !sameValues(values(), initial));
|
|
75
21
|
const isValid = computed(() => Object.keys(errors()).length === 0);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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 (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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 (
|
|
191
|
-
|
|
192
|
-
|
|
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(
|
|
247
|
-
const
|
|
248
|
-
if (!
|
|
89
|
+
onInput(event) {
|
|
90
|
+
const control = controlFromEvent(event);
|
|
91
|
+
if (!control?.name)
|
|
249
92
|
return;
|
|
250
|
-
|
|
251
|
-
|
|
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(
|
|
254
|
-
const
|
|
255
|
-
if (!
|
|
99
|
+
onBlur(event) {
|
|
100
|
+
const control = controlFromEvent(event);
|
|
101
|
+
if (!control?.name)
|
|
256
102
|
return;
|
|
257
|
-
|
|
103
|
+
rememberForm(event, control);
|
|
104
|
+
touched.update((current) => ({ ...current, [control.name]: true }));
|
|
258
105
|
},
|
|
259
106
|
onSubmit(handler) {
|
|
260
|
-
return (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
for (const
|
|
267
|
-
|
|
268
|
-
touched.set(
|
|
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
|
|
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 (
|
|
277
|
-
|
|
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
|
-
|
|
130
|
+
activeSubmits--;
|
|
131
|
+
submitting.set(activeSubmits > 0);
|
|
282
132
|
}
|
|
283
133
|
})();
|
|
284
134
|
};
|
|
285
135
|
},
|
|
286
|
-
setField
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
160
|
+
return isFormControl(event.target) ? event.target : null;
|
|
374
161
|
}
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
385
|
-
return
|
|
169
|
+
function isForm(value) {
|
|
170
|
+
return !!value && typeof value === "object" &&
|
|
171
|
+
String(value.tagName).toLowerCase() === "form";
|
|
386
172
|
}
|
|
387
|
-
function
|
|
388
|
-
if (
|
|
389
|
-
return
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
403
|
-
|
|
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
|
|
406
|
-
|
|
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
|
|
409
|
-
|
|
410
|
-
return [pattern];
|
|
411
|
-
return expandParts(values, splitPath(pattern), []);
|
|
213
|
+
function isInput(control) {
|
|
214
|
+
return String(control.tagName).toLowerCase() === "input";
|
|
412
215
|
}
|
|
413
|
-
function
|
|
414
|
-
|
|
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
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
448
|
-
const next = { ...
|
|
449
|
-
|
|
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
|