@madojs/mado 0.11.1 → 0.12.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 +102 -25
- package/CHANGELOG.md +158 -1
- package/README.md +135 -172
- package/dist/src/component.d.ts +3 -2
- package/dist/src/component.js +98 -6
- package/dist/src/component.js.map +1 -1
- package/dist/src/css.js +34 -5
- package/dist/src/css.js.map +1 -1
- package/dist/src/head.d.ts +6 -6
- package/dist/src/head.js +6 -6
- package/dist/src/html/parser.d.ts +1 -1
- package/dist/src/html/parser.js +1 -1
- package/dist/src/html/template.js +24 -10
- package/dist/src/html/template.js.map +1 -1
- package/dist/src/index.d.ts +8 -4
- package/dist/src/index.js +13 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/page.d.ts +62 -28
- package/dist/src/page.js +5 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/resource.js +2 -1
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/base.d.ts +65 -0
- package/dist/src/router/base.js +168 -0
- package/dist/src/router/base.js.map +1 -0
- package/dist/src/router/manifest.js +75 -31
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +16 -2
- package/dist/src/router/match.js +41 -1
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.js +58 -9
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/static-runtime.d.ts +81 -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 +31 -3
- package/dist/src/vite/index.js +49 -18
- 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/en/00-the-mado-way.md +1 -1
- package/docs/en/01-quickstart.md +183 -0
- package/docs/en/10-pages-and-components.md +271 -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 +225 -0
- package/docs/en/14-forms.md +244 -0
- package/docs/en/15-static-snapshots.md +181 -0
- package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
- package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
- package/docs/en/{14-testing.md → 22-testing.md} +6 -5
- package/docs/en/23-cookbook.md +292 -0
- package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
- package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
- package/docs/en/40-llm-guide.md +776 -0
- package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
- package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
- package/docs/en/README.md +97 -27
- package/docs/recipes/nginx/Containerfile +26 -0
- package/docs/recipes/nginx/nginx.conf +42 -0
- package/llms.txt +246 -211
- package/package.json +17 -10
- package/scripts/bake.mjs +4 -569
- package/scripts/cli/generate.mjs +55 -5
- package/scripts/cli/help.mjs +11 -5
- package/scripts/cli/index.mjs +20 -4
- package/scripts/cli/init.mjs +7 -2
- package/scripts/cli/release.mjs +20 -68
- package/scripts/docs-lint.mjs +170 -0
- package/scripts/package-smoke.mjs +67 -9
- package/scripts/preview.mjs +95 -6
- package/scripts/size-budget.mjs +5 -1
- package/scripts/static/browser.mjs +654 -0
- package/scripts/static/discover.mjs +281 -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 +191 -0
- package/starters/default/README.md +57 -55
- package/starters/default/package.json +3 -9
- 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/vite.config.ts +14 -1
- 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 +8 -5
- 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/billing/pages/invoices-list.page.ts +3 -3
- 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/modular/src/styles.d.ts +1 -0
- package/starters/modular/src/vite-env.d.ts +1 -0
- package/starters/modular/tsconfig.json +24 -0
- package/starters/modular/vite.config.ts +131 -0
- package/TODO.md +0 -79
- 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/starters/default/src/modules/home/not-found.page.ts +0 -11
- /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
- /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/auth/login.page.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/modules/billing/pages/invoice-detail.page.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/reset.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-button.component.ts +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Forms
|
|
2
|
+
|
|
3
|
+
> `useForm()` is a thin signal-wrapper around native HTML5 form
|
|
4
|
+
> validation. For 90% of cases you write a `<form>`, attach
|
|
5
|
+
> `onInput / onBlur / onSubmit` and you are done.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { html, page, useForm } from "@madojs/mado";
|
|
9
|
+
|
|
10
|
+
export default page({
|
|
11
|
+
view: () => {
|
|
12
|
+
const f = useForm({
|
|
13
|
+
email: { required: true, type: "email" },
|
|
14
|
+
age: { required: true, type: "number", min: 18 },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return html`
|
|
18
|
+
<form @submit=${f.onSubmit(async (v) => { await api.save(v); f.reset(); })}>
|
|
19
|
+
<input name="email" type="email"
|
|
20
|
+
@input=${f.onInput} @blur=${f.onBlur} />
|
|
21
|
+
${() =>
|
|
22
|
+
f.touched().email && f.errors().email
|
|
23
|
+
? html`<small class="err">${f.errors().email}</small>`
|
|
24
|
+
: null}
|
|
25
|
+
|
|
26
|
+
<input name="age" type="number" @input=${f.onInput} />
|
|
27
|
+
|
|
28
|
+
<button ?disabled=${() => !f.isValid() || f.submitting()}>Save</button>
|
|
29
|
+
</form>
|
|
30
|
+
`;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## The signals it exposes
|
|
36
|
+
|
|
37
|
+
| Reader | Value |
|
|
38
|
+
| ---------------------- | ---------------------------------------------------------------- |
|
|
39
|
+
| `f.values()` | object of current field values |
|
|
40
|
+
| `f.errors()` | object of `field → error string` (validators + HTML5 constraints)|
|
|
41
|
+
| `f.touched()` | object of `field → boolean` (true after first blur) |
|
|
42
|
+
| `f.isValid()` | true when `errors()` is empty |
|
|
43
|
+
| `f.submitting()` | true while the `onSubmit` async handler is running |
|
|
44
|
+
| `f.dirty()` | true when any value differs from initial |
|
|
45
|
+
|
|
46
|
+
Writers: `f.setValue(name, v)`, `f.setError(name, msg)`,
|
|
47
|
+
`f.reset(values?)`, `f.touch(name)`.
|
|
48
|
+
|
|
49
|
+
## Schema
|
|
50
|
+
|
|
51
|
+
The schema mirrors HTML5 constraints, with a few extras:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
useForm({
|
|
55
|
+
email: { required: true, type: "email" },
|
|
56
|
+
age: { type: "number", min: 18, max: 120 },
|
|
57
|
+
name: { required: true, minLength: 2, maxLength: 80 },
|
|
58
|
+
website: { type: "url" },
|
|
59
|
+
tags: { custom: (v) => Array.isArray(v) && v.length > 0 ? null : "pick at least one" },
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For cross-field or async rules use `validate`:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
useForm({
|
|
67
|
+
password: { required: true, minLength: 8 },
|
|
68
|
+
confirm: { required: true },
|
|
69
|
+
}, {
|
|
70
|
+
validate: (values) =>
|
|
71
|
+
values.password !== values.confirm
|
|
72
|
+
? { confirm: "passwords do not match" }
|
|
73
|
+
: null,
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Return `null` (everything is valid) or a partial `field → error`
|
|
78
|
+
object.
|
|
79
|
+
|
|
80
|
+
## Field arrays
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const f = useForm({ tags: { custom: (v) => v.length > 0 ? null : "required" } });
|
|
84
|
+
const tags = f.fieldArray<string>("tags");
|
|
85
|
+
|
|
86
|
+
tags.append("rust");
|
|
87
|
+
tags.remove(0);
|
|
88
|
+
tags.move(0, 2);
|
|
89
|
+
tags.items(); // current array (signal)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## With `mutation()`
|
|
93
|
+
|
|
94
|
+
Forms compose with `mutation()` for typed write-paths:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { mutation, navigate } from "@madojs/mado";
|
|
98
|
+
|
|
99
|
+
const save = mutation((u: User) => api.save(u), {
|
|
100
|
+
invalidates: ["/api/users*"],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// inside view()
|
|
104
|
+
const f = useForm({ name: { required: true } });
|
|
105
|
+
return html`
|
|
106
|
+
<form @submit=${f.onSubmit(async (v) => {
|
|
107
|
+
await save.run(v);
|
|
108
|
+
navigate("/users");
|
|
109
|
+
})}>
|
|
110
|
+
<input name="name" @input=${f.onInput} />
|
|
111
|
+
<button ?disabled=${() => !f.isValid() || save.loading()}>Create</button>
|
|
112
|
+
${() => save.error() ? html`<p class="err">${save.error()!.message}</p>` : null}
|
|
113
|
+
</form>
|
|
114
|
+
`;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`save.loading()` reflects the request; `f.submitting()` reflects the
|
|
118
|
+
`onSubmit` handler runtime. Usually only one of them is interesting.
|
|
119
|
+
|
|
120
|
+
## Shadow DOM inputs — when you need them
|
|
121
|
+
|
|
122
|
+
Most apps build forms from native `<input>` / `<select>` /
|
|
123
|
+
`<textarea>` directly inside the `<form>`. `useForm()` reads
|
|
124
|
+
`e.target.name` and `e.target.value` on the events, which works
|
|
125
|
+
out-of-the-box for native controls.
|
|
126
|
+
|
|
127
|
+
You only need a custom Web Component when the input has its own
|
|
128
|
+
shadow tree (a date picker, a tag editor, a rich text field, …). Two
|
|
129
|
+
browser-level facts make that path slightly different:
|
|
130
|
+
|
|
131
|
+
1. **Event retargeting** — events that bubble out of an open shadow
|
|
132
|
+
root have their `target` retargeted to the host. `useForm()` reads
|
|
133
|
+
`e.target.name` and `e.target.value` — the host element must
|
|
134
|
+
expose both.
|
|
135
|
+
2. **Form association** — a `<button type="submit">` inside a shadow
|
|
136
|
+
root is NOT part of the form-owner algorithm. Clicking it does
|
|
137
|
+
not submit the surrounding `<form>`.
|
|
138
|
+
|
|
139
|
+
The two patterns below cover both. Reach for them **only** if a real
|
|
140
|
+
custom input forces you to. For everything else, see "Light-DOM
|
|
141
|
+
inputs" further down.
|
|
142
|
+
|
|
143
|
+
### Custom input — proxy `name` / `value`
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { component, html } from "@madojs/mado";
|
|
147
|
+
|
|
148
|
+
component("x-input", ({ host, attr }) => {
|
|
149
|
+
const type = attr("type", "text");
|
|
150
|
+
|
|
151
|
+
// After Shadow DOM retargets e.target from the inner <input> to
|
|
152
|
+
// <x-input>, useForm reads e.target.name / e.target.value. These
|
|
153
|
+
// getters bridge the gap.
|
|
154
|
+
Object.defineProperty(host, "name", {
|
|
155
|
+
get: () => host.getAttribute("name") ?? "",
|
|
156
|
+
configurable: true,
|
|
157
|
+
});
|
|
158
|
+
Object.defineProperty(host, "value", {
|
|
159
|
+
get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
|
|
160
|
+
set: (v: string) => {
|
|
161
|
+
const input = host.shadowRoot?.querySelector("input");
|
|
162
|
+
if (input) input.value = v;
|
|
163
|
+
},
|
|
164
|
+
configurable: true,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return () => html`<input type=${type} />`;
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The inner `input` event already bubbles through the shadow boundary
|
|
172
|
+
(`composed: true` by default), so `<form @input=${f.onInput}>` keeps
|
|
173
|
+
working.
|
|
174
|
+
|
|
175
|
+
### Custom submit button — `requestSubmit()`
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { component, html } from "@madojs/mado";
|
|
179
|
+
|
|
180
|
+
component("x-button", ({ host, attr }) => {
|
|
181
|
+
const disabled = attr("disabled");
|
|
182
|
+
|
|
183
|
+
const onClick = () => {
|
|
184
|
+
const t = host.getAttribute("type");
|
|
185
|
+
if (t === "button" || t === "reset") return;
|
|
186
|
+
if (host.hasAttribute("disabled")) return;
|
|
187
|
+
host.closest("form")?.requestSubmit();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return () => html`
|
|
191
|
+
<button ?disabled=${() => disabled() !== ""} @click=${onClick}>
|
|
192
|
+
<slot></slot>
|
|
193
|
+
</button>
|
|
194
|
+
`;
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`host.closest("form")` works because the host itself lives in the
|
|
199
|
+
light DOM. `requestSubmit()` triggers HTML5 validation and the
|
|
200
|
+
`submit` event exactly like a native click.
|
|
201
|
+
|
|
202
|
+
### Light-DOM input — the simpler alternative
|
|
203
|
+
|
|
204
|
+
If a custom element is just a styled wrapper around a native control
|
|
205
|
+
and does not need style encapsulation, use `{ shadow: false }`. The
|
|
206
|
+
native `<input>` is then part of the document; no retargeting, no
|
|
207
|
+
proxy properties, no submit bridge.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
component(
|
|
211
|
+
"x-field",
|
|
212
|
+
({ attr }) => {
|
|
213
|
+
const label = attr("label", "");
|
|
214
|
+
return () => html`
|
|
215
|
+
<label>
|
|
216
|
+
<span>${label}</span>
|
|
217
|
+
<slot></slot>
|
|
218
|
+
</label>
|
|
219
|
+
`;
|
|
220
|
+
},
|
|
221
|
+
{ shadow: false },
|
|
222
|
+
);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
This is the only situation where `shadow: false` is the recommended
|
|
226
|
+
default — and even then a `page()` is usually a better fit if the
|
|
227
|
+
wrapper is layout-shaped rather than reusable.
|
|
228
|
+
|
|
229
|
+
## Summary
|
|
230
|
+
|
|
231
|
+
| Concern | Native form | Custom component with Shadow DOM |
|
|
232
|
+
| -------------------------- | ------------------------------------ | ----------------------------------------- |
|
|
233
|
+
| `useForm` integration | works out of the box | proxy `name` / `value` on the host |
|
|
234
|
+
| Submit button | `<button type="submit">` works | `host.closest("form")?.requestSubmit()` |
|
|
235
|
+
| Reactive attributes | not needed | `ctx.attr(name)` returns a Signal |
|
|
236
|
+
| Style encapsulation | global CSS | Shadow DOM CSS via `css\`\`` |
|
|
237
|
+
|
|
238
|
+
## Further reading
|
|
239
|
+
|
|
240
|
+
- [10-pages-and-components.md](./10-pages-and-components.md) — when
|
|
241
|
+
to reach for `component({ shadow: false })`.
|
|
242
|
+
- [13-data.md](./13-data.md) — `mutation()` for typed writes.
|
|
243
|
+
- [21-error-handling.md](./21-error-handling.md) — surfacing form
|
|
244
|
+
errors from network failures.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Static snapshots (`mado static`)
|
|
2
|
+
|
|
3
|
+
> Browser-rendered static HTML for SEO and first paint. No SSR, no
|
|
4
|
+
> hydration, no template-language bake.
|
|
5
|
+
|
|
6
|
+
`mado static` is a *snapshot capture*: at release time Mado runs your
|
|
7
|
+
app in a real headless Chromium, lets the page render to the DOM
|
|
8
|
+
(including any Shadow DOM), then serialises the result — Declarative
|
|
9
|
+
Shadow DOM and all — to one HTML file per route. On first paint the
|
|
10
|
+
live SPA atomically replaces the static tree; there is no hydration
|
|
11
|
+
protocol and no node reconciliation.
|
|
12
|
+
|
|
13
|
+
It is the right tool for content that is the same for every visitor:
|
|
14
|
+
public landing pages, product / catalogue pages, documentation, blog
|
|
15
|
+
posts. It is intentionally **not** an SSR runtime for personalised
|
|
16
|
+
data.
|
|
17
|
+
|
|
18
|
+
## When `static` is suitable
|
|
19
|
+
|
|
20
|
+
- Public pages with a single canonical representation per URL.
|
|
21
|
+
- Pages whose data is known at build time (CMS dump, file system,
|
|
22
|
+
Markdown, JSON, build-time API call).
|
|
23
|
+
- Pages that need to appear fully rendered in `view-source:`, in
|
|
24
|
+
social preview crawlers, and to JS-disabled clients.
|
|
25
|
+
- First paint of a page that *later* upgrades into a live SPA route.
|
|
26
|
+
|
|
27
|
+
## When `static` is **not** suitable
|
|
28
|
+
|
|
29
|
+
- Personalised content per visitor (auth, geo, A/B). The capture sees
|
|
30
|
+
one DOM and would ship it to everyone.
|
|
31
|
+
- Real-time data (chat, dashboards, prices). The snapshot is frozen
|
|
32
|
+
the moment of capture.
|
|
33
|
+
- Routes behind guards. Static pages are public by definition; the
|
|
34
|
+
discovery pipeline refuses to capture guarded routes.
|
|
35
|
+
|
|
36
|
+
For those cases keep the route SPA-only — Mado still serves a clean
|
|
37
|
+
`_mado/spa.html` fallback for them.
|
|
38
|
+
|
|
39
|
+
## Declaring a static page
|
|
40
|
+
|
|
41
|
+
A page opts in through the `static` field on `page({ ... })`. Two
|
|
42
|
+
shapes are accepted:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// Single static page (no params)
|
|
46
|
+
import { html, page } from "@madojs/mado";
|
|
47
|
+
|
|
48
|
+
export default page({
|
|
49
|
+
static: true,
|
|
50
|
+
title: "Pricing",
|
|
51
|
+
head: () => ({ description: "Plans and pricing." }),
|
|
52
|
+
view: () => html`<main><h1>Pricing</h1></main>`,
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// Dynamic static route — one capture per `paths()` entry
|
|
58
|
+
import { html, page } from "@madojs/mado";
|
|
59
|
+
|
|
60
|
+
export default page<{ slug: string }, Guide>({
|
|
61
|
+
static: {
|
|
62
|
+
paths: async () => guides.map((g) => ({ slug: g.slug })),
|
|
63
|
+
initialData: async ({ slug }) => loadGuide(slug),
|
|
64
|
+
},
|
|
65
|
+
title: ({ slug }) => guides.find((g) => g.slug === slug)?.title ?? slug,
|
|
66
|
+
head: ({ slug }, seed) => ({
|
|
67
|
+
description: seed?.summary ?? "",
|
|
68
|
+
}),
|
|
69
|
+
view: (ctx) => html`<article>${ctx.data.body}</article>`,
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The seed produced by `initialData()` is JSON-serialised into the
|
|
74
|
+
captured HTML as `<script type="application/json" data-mado-static-data="…">`.
|
|
75
|
+
On first client boot Mado consumes it once so `page.load()` and
|
|
76
|
+
`page.head()` see the same data the snapshot saw — no extra fetch.
|
|
77
|
+
|
|
78
|
+
## Running
|
|
79
|
+
|
|
80
|
+
In normal use `mado static` is invoked through `mado release`:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
mado release # vite build + mado static + deployment files
|
|
84
|
+
mado preview # serve out/ like a real static host
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
You can also invoke `mado static` on its own (for tighter loops in
|
|
88
|
+
CI), but it operates on the existing `out/` artefact produced by
|
|
89
|
+
`vite build`, so the canonical command is `release`.
|
|
90
|
+
|
|
91
|
+
A public origin is required so static documents carry absolute
|
|
92
|
+
canonical URLs. Set it in `vite.config.ts`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { defineConfig } from "vite";
|
|
96
|
+
import { mado } from "@madojs/mado/vite";
|
|
97
|
+
|
|
98
|
+
export default defineConfig({
|
|
99
|
+
plugins: [mado({ site: "https://your-app.example" })],
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
…or override per run:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
mado release --base-url https://staging.example
|
|
107
|
+
MADO_SITE=https://staging.example mado release
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## What the generated HTML contains
|
|
111
|
+
|
|
112
|
+
For every captured route:
|
|
113
|
+
|
|
114
|
+
- The full rendered light DOM.
|
|
115
|
+
- Every open shadow root serialised through Declarative Shadow DOM
|
|
116
|
+
(`<template shadowrootmode="open">`).
|
|
117
|
+
- Adopted stylesheets materialised into `<style>` tags inside the
|
|
118
|
+
shadow root.
|
|
119
|
+
- A `<link rel="canonical">` and `<meta property="og:url">` derived
|
|
120
|
+
from `site + base + pathname` (if the page did not declare its own).
|
|
121
|
+
Both are marked `data-mado-head="static"` so the runtime `applyHead`
|
|
122
|
+
removes them on SPA navigation into a page without explicit head
|
|
123
|
+
metadata.
|
|
124
|
+
- The seed `<script type="application/json" data-mado-static-data="…">`
|
|
125
|
+
for routes that declared `initialData()`.
|
|
126
|
+
- Vite assets resolved through the active `base`.
|
|
127
|
+
|
|
128
|
+
`<form>`, `<input>`, `<select>`, `<textarea>` and `<details>` state
|
|
129
|
+
is serialised through attributes so a JS-disabled browser sees the
|
|
130
|
+
same shape the snapshot captured.
|
|
131
|
+
|
|
132
|
+
## Constraints and gotchas
|
|
133
|
+
|
|
134
|
+
- **`paths()` and `initialData()` run during discovery AND ship in the
|
|
135
|
+
client bundle.** Keep them browser-safe. Never read secrets, never
|
|
136
|
+
call private services.
|
|
137
|
+
- **Static pages cannot use guards** — route-level or layout-level.
|
|
138
|
+
- **Wildcard routes (`*`) cannot be static.** They are the SPA
|
|
139
|
+
fallback.
|
|
140
|
+
- **A compatible Chromium is required.** CI should install it through
|
|
141
|
+
Playwright:
|
|
142
|
+
```bash
|
|
143
|
+
npx playwright install --with-deps chromium
|
|
144
|
+
```
|
|
145
|
+
…or via the framework's installed `playwright-core` for revision
|
|
146
|
+
parity:
|
|
147
|
+
```bash
|
|
148
|
+
node node_modules/playwright-core/cli.js install --with-deps chromium
|
|
149
|
+
```
|
|
150
|
+
- **Capture is strict by default.** Any failed fetch (script, style,
|
|
151
|
+
resource(), custom element definition) fails the snapshot. A small
|
|
152
|
+
allow-list covers `/favicon.ico`, `/favicon.svg`, `/robots.txt`
|
|
153
|
+
and `data:` URLs.
|
|
154
|
+
- **All network calls happen against an internal capture server** that
|
|
155
|
+
serves the freshly built `out/` tree. There is no real network at
|
|
156
|
+
capture time.
|
|
157
|
+
|
|
158
|
+
## Comparison
|
|
159
|
+
|
|
160
|
+
| Approach | Mado static snapshot |
|
|
161
|
+
| ------------------------------------- | -------------------- |
|
|
162
|
+
| Next-style SSR + hydration | not used |
|
|
163
|
+
| Astro-style island runtime | not used |
|
|
164
|
+
| Build-time HTML from templates only | not used |
|
|
165
|
+
| Browser-rendered DOM + DSD + takeover | **used** |
|
|
166
|
+
|
|
167
|
+
The result is a plain HTML file. Search engines, social preview
|
|
168
|
+
crawlers and `curl` see exactly what your app renders on first paint;
|
|
169
|
+
no JavaScript step is required to make the document meaningful.
|
|
170
|
+
|
|
171
|
+
## Cookbook
|
|
172
|
+
|
|
173
|
+
Concrete recipes for blog, product catalogue, documentation site and
|
|
174
|
+
multi-locale builds live in [23-cookbook.md](./23-cookbook.md).
|
|
175
|
+
|
|
176
|
+
## TL;DR
|
|
177
|
+
|
|
178
|
+
- Declare `static: true | { paths, initialData }` on `page({ ... })`.
|
|
179
|
+
- Run `mado release`.
|
|
180
|
+
- Ship `out/` to any static host.
|
|
181
|
+
- The live SPA takes over atomically on first paint.
|
|
@@ -4,7 +4,58 @@ The official starter is the canonical production shape for Mado apps. It is
|
|
|
4
4
|
not a demo architecture and not a framework inside the framework: it is plain
|
|
5
5
|
files, imports, Mado primitives, and ESLint boundaries.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Project layout (universal starter)
|
|
8
|
+
|
|
9
|
+
Both starters share the same top-level shape. The universal starter is the
|
|
10
|
+
minimum:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
my-app/
|
|
14
|
+
├── package.json # exactly one runtime dep: @madojs/mado
|
|
15
|
+
├── tsconfig.json # strict TS, ES2022, Bundler resolution
|
|
16
|
+
├── vite.config.ts # mado() from @madojs/mado/vite
|
|
17
|
+
├── index.html # Vite entry + SPA shell
|
|
18
|
+
├── public/ # static assets (favicons, images, robots.txt)
|
|
19
|
+
└── src/
|
|
20
|
+
├── main.ts # entry: mount router into #app
|
|
21
|
+
├── app.routes.ts # one app map (default + named `manifest`)
|
|
22
|
+
├── pages/ # *.page.ts files
|
|
23
|
+
├── components/ # reusable <x-tag> components
|
|
24
|
+
└── shared/ # http/, lib/, styles/, ui/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`index.html` belongs at the project root because Vite treats it as an entry
|
|
28
|
+
template, not a static public file. Put only copy-as-is files in `public/`.
|
|
29
|
+
|
|
30
|
+
### The three artifact states
|
|
31
|
+
|
|
32
|
+
| Folder | What it is | Who writes | Who reads | Deploy? |
|
|
33
|
+
| --------- | ----------------------------------------------------------- | --------------- | ----------------------- | ------------- |
|
|
34
|
+
| `src/` | your TypeScript source | you | Vite, `tsc --noEmit` | ❌ no |
|
|
35
|
+
| `public/` | static assets copied as-is | you | Vite build | ✅ via `out/` |
|
|
36
|
+
| `out/` | **the deploy artifact**: SPA shell + assets + snapshots | `mado release` | nginx / CDN / CF Pages | ✅ **yes** |
|
|
37
|
+
|
|
38
|
+
One-liner: develop with `mado dev`, ship with `mado release`, upload `out/`.
|
|
39
|
+
|
|
40
|
+
### Naming rules
|
|
41
|
+
|
|
42
|
+
| What | Style | Example |
|
|
43
|
+
| -------------------------- | ------------------ | ---------------------- |
|
|
44
|
+
| File | kebab-case | `user-profile.ts` |
|
|
45
|
+
| Component tag | `x-` + kebab | `<x-user-profile>` |
|
|
46
|
+
| Context | PascalCase + `Ctx` | `ThemeCtx`, `AuthCtx` |
|
|
47
|
+
| Signal | camelCase | `userId`, `isLoggedIn` |
|
|
48
|
+
| Page-internal element | `x-<route>-page` | `<x-posts-page>` |
|
|
49
|
+
|
|
50
|
+
### What does NOT belong in `src/`
|
|
51
|
+
|
|
52
|
+
- ❌ Build-tool configs beyond `vite.config.ts` with `mado()`.
|
|
53
|
+
- ❌ `.env` files — read env in `src/shared/lib/config.ts` from
|
|
54
|
+
`import.meta.env` and import that one module everywhere.
|
|
55
|
+
- ❌ Tests mixed with code — put them in `test/`.
|
|
56
|
+
- ❌ `examples/` folder — keep large demos outside the app repo.
|
|
57
|
+
|
|
58
|
+
## File tree (modular reference starter)
|
|
8
59
|
|
|
9
60
|
```txt
|
|
10
61
|
src/
|
|
@@ -68,7 +119,7 @@ export default routes(manifest);
|
|
|
68
119
|
|
|
69
120
|
Rules:
|
|
70
121
|
|
|
71
|
-
- Export `manifest` so `mado
|
|
122
|
+
- Export `manifest` so `mado static` can discover bakeable pages.
|
|
72
123
|
- Modules never call `layout()`.
|
|
73
124
|
- Layouts describe app zones, not domains.
|
|
74
125
|
- Do not hide the router inside a custom element or a second shell in
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
out/
|
|
11
|
-
├── index.html ←
|
|
11
|
+
├── index.html ← captured snapshot for / (or SPA shell)
|
|
12
12
|
├── assets/ ← Vite hashed assets
|
|
13
13
|
│ ├── *.gz ← precompressed gzip (gzip_static / Accept-Encoding)
|
|
14
14
|
│ └── *.br ← precompressed brotli (brotli_static / Accept-Encoding)
|
|
15
|
-
├── <route>/index.html ←
|
|
15
|
+
├── <route>/index.html ← captured snapshot per static route
|
|
16
|
+
├── _mado/spa.html ← SPA fallback shell (noindex)
|
|
16
17
|
├── sitemap.xml ← generated sitemap
|
|
17
18
|
├── favicon.svg ← your public/ assets copied verbatim
|
|
18
19
|
├── _redirects ← Cloudflare Pages / Netlify SPA fallback
|
|
@@ -89,8 +90,9 @@ Build command: npm ci && npx mado release
|
|
|
89
90
|
Output directory: out
|
|
90
91
|
```
|
|
91
92
|
|
|
92
|
-
For catalogs too big to
|
|
93
|
-
the external examples workspace rather than in the core
|
|
93
|
+
For catalogs too big to snapshot at release time, keep on-demand snapshot
|
|
94
|
+
experiments in the external examples workspace rather than in the core
|
|
95
|
+
package.
|
|
94
96
|
|
|
95
97
|
---
|
|
96
98
|
|
|
@@ -182,10 +184,57 @@ jobs:
|
|
|
182
184
|
- **`/assets/*` files change but the browser keeps the old one.** They
|
|
183
185
|
should not — the filename is hashed by Vite during `mado release`. If you bypassed
|
|
184
186
|
build and shipped your own unhashed JS, give it a hash or short cache.
|
|
185
|
-
- **
|
|
186
|
-
|
|
187
|
-
upgrade `@madojs/mado` and re-run
|
|
187
|
+
- **Captured snapshot shows `[object Object]`.** `mado static` rejects
|
|
188
|
+
non-JSON-serialisable seeds at build time and points at the bad
|
|
189
|
+
field. If you still see it, upgrade `@madojs/mado` and re-run
|
|
190
|
+
`mado release`.
|
|
191
|
+
|
|
192
|
+
## Sub-path deployments and `base`
|
|
193
|
+
|
|
194
|
+
A deployment under a sub-path (`https://your.site/docs/`, GitHub
|
|
195
|
+
Pages project URL, an admin panel mounted under `/admin/`, …) only
|
|
196
|
+
requires two changes:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// vite.config.ts
|
|
200
|
+
import { defineConfig } from "vite";
|
|
201
|
+
import { mado } from "@madojs/mado/vite";
|
|
202
|
+
|
|
203
|
+
export default defineConfig({
|
|
204
|
+
base: "/docs/", // ← active prefix
|
|
205
|
+
plugins: [mado({ site: "https://your.site" })],
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Then make every internal anchor go through `routeUrl()` + `data-link`
|
|
210
|
+
so the URL is correct under any base:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { html, routeUrl } from "@madojs/mado";
|
|
214
|
+
|
|
215
|
+
html`<a data-link href=${routeUrl("/")}>Home</a>`; // → "/docs/"
|
|
216
|
+
html`<a data-link href=${routeUrl("/guides/intro")}>Intro</a>`;
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`mado release` honours `base` end-to-end:
|
|
220
|
+
|
|
221
|
+
- captured snapshots load assets through `/docs/assets/...`;
|
|
222
|
+
- sitemap URLs are `https://your.site/docs/...`;
|
|
223
|
+
- canonicals and `og:url` include the prefix;
|
|
224
|
+
- `mado preview` redirects bare `/` to `/docs/` and serves SPA
|
|
225
|
+
fallback under `/docs/<anything>`.
|
|
226
|
+
|
|
227
|
+
On the production host:
|
|
228
|
+
|
|
229
|
+
- **nginx** — keep `try_files $uri $uri/ /index.html;` but mount the
|
|
230
|
+
app under the matching prefix (`location /docs/ { ... }`).
|
|
231
|
+
- **Cloudflare Pages / Netlify** — `_redirects` is generated with the
|
|
232
|
+
base prefix already baked in.
|
|
233
|
+
- **GitHub Pages (project site)** — set `base: "/<repo>/"` and use
|
|
234
|
+
the `gh-pages` deploy path; canonical URLs land at
|
|
235
|
+
`https://<user>.github.io/<repo>/...`.
|
|
188
236
|
|
|
189
|
-
See also: [`
|
|
190
|
-
`src
|
|
191
|
-
for the
|
|
237
|
+
See also: [`16-app-architecture.md`](./16-app-architecture.md) for the
|
|
238
|
+
`src/` / `public/` / `out/` model and
|
|
239
|
+
[`15-static-snapshots.md`](./15-static-snapshots.md) for the static
|
|
240
|
+
snapshot mechanics.
|
|
@@ -48,7 +48,7 @@ Test behavior, not internal implementation details:
|
|
|
48
48
|
- route guards, redirects, scroll/focus behavior, error boundaries;
|
|
49
49
|
- forms: validation, async validation races, field arrays;
|
|
50
50
|
- resources/mutations: cache keys, invalidation, lifecycle cleanup;
|
|
51
|
-
- CLI flows: `mado release`, `mado
|
|
51
|
+
- CLI flows: `mado release`, `mado static`, `mado preview`.
|
|
52
52
|
|
|
53
53
|
## Browser smoke tests
|
|
54
54
|
|
|
@@ -74,9 +74,10 @@ services from framework tests. For app tests, make the API client injectable via
|
|
|
74
74
|
npm run typecheck
|
|
75
75
|
npm run build
|
|
76
76
|
npm test
|
|
77
|
-
npm run
|
|
77
|
+
npm run release
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
`mado release` runs the production path for an app: typecheck, Vite
|
|
81
|
-
|
|
82
|
-
`npm run build` still emits `dist/src`
|
|
80
|
+
`mado release` runs the production path for an app: typecheck, Vite
|
|
81
|
+
build, static-snapshot capture, compression and deploy helper files.
|
|
82
|
+
In the framework repository, `npm run build` still emits `dist/src`
|
|
83
|
+
for package tests and publishing.
|