@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.
- package/AGENTS.md +15 -8
- package/CHANGELOG.md +66 -0
- package/README.md +25 -3
- package/dist/src/component.d.ts +3 -1
- package/dist/src/component.d.ts.map +1 -0
- package/dist/src/component.js +65 -30
- 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.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 +1 -0
- package/dist/src/head.d.ts.map +1 -0
- package/dist/src/head.js +2 -1
- 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 +3 -4
- package/dist/src/html/parser.d.ts.map +1 -0
- package/dist/src/html/parser.js +63 -14
- 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 +78 -3
- package/dist/src/html/template.js.map +1 -1
- package/dist/src/index.d.ts +7 -8
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +5 -7
- 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 +1 -0
- package/dist/src/page.d.ts.map +1 -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 +126 -49
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/base.d.ts +1 -0
- package/dist/src/router/base.d.ts.map +1 -0
- package/dist/src/router/base.js.map +1 -1
- 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 +89 -148
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +1 -0
- package/dist/src/router/match.d.ts.map +1 -0
- package/dist/src/router/match.js +19 -5
- 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.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 +1 -0
- package/dist/src/static-runtime.d.ts.map +1 -0
- package/dist/src/static-runtime.js.map +1 -1
- package/dist/src/vite/index.d.ts +1 -0
- package/dist/src/vite/index.d.ts.map +1 -0
- package/dist/src/vite/index.js +4 -3
- package/dist/src/vite/index.js.map +1 -1
- package/docs/architecture/v1-roadmap.md +19 -0
- package/docs/en/10-pages-and-components.md +9 -1
- package/docs/en/13-data.md +8 -6
- package/docs/en/14-forms.md +66 -219
- package/docs/en/15-static-snapshots.md +4 -4
- package/docs/en/16-app-architecture.md +3 -2
- package/docs/en/20-deployment.md +21 -8
- package/docs/en/21-error-handling.md +9 -6
- package/docs/en/24-devtools-and-diagnostics.md +45 -0
- package/docs/en/30-api-freeze-map.md +8 -9
- package/docs/en/32-v1-stability.md +2 -2
- package/docs/en/33-migration-0.12-0.13.md +56 -0
- package/docs/en/40-llm-guide.md +7 -9
- package/docs/en/41-for-backenders.md +13 -9
- package/docs/en/42-why-mado.md +2 -2
- package/docs/en/README.md +2 -0
- package/docs/recipes/nginx/nginx.conf +2 -2
- package/llms.txt +25 -7
- package/package.json +37 -16
- package/scripts/clean.mjs +8 -0
- package/scripts/cli/generate.mjs +11 -14
- package/scripts/cli/help.mjs +2 -0
- package/scripts/cli/index.mjs +14 -6
- package/scripts/cli/init.mjs +13 -18
- package/scripts/cli/release.mjs +22 -19
- package/scripts/cli/run.mjs +5 -3
- package/scripts/logger.mjs +117 -0
- package/scripts/output-guard.mjs +128 -0
- package/scripts/preview.mjs +11 -19
- package/scripts/static/browser.mjs +58 -8
- package/scripts/static/discover.mjs +4 -1
- package/scripts/static.mjs +21 -7
- package/starters/default/package.json +4 -4
- package/starters/default/tsconfig.node.json +13 -0
- package/starters/default/vite.config.ts +0 -3
- package/starters/modular/README.md +4 -4
- package/starters/modular/package.json +4 -4
- package/starters/modular/src/layouts/app-shell.layout.ts +1 -2
- package/starters/modular/src/modules/auth/login.page.ts +5 -12
- package/starters/modular/src/modules/billing/pages/invoice-detail.page.ts +3 -4
- package/starters/modular/src/modules/billing/pages/invoices-list.page.ts +1 -1
- package/starters/modular/src/shared/styles/reset.css +21 -1
- package/starters/modular/tsconfig.json +10 -1
- package/starters/modular/tsconfig.node.json +13 -0
- package/starters/modular/vite.config.ts +6 -5
- package/dist/src/lazy.d.ts +0 -38
- package/dist/src/lazy.js +0 -73
- package/dist/src/lazy.js.map +0 -1
- package/scripts/bake.mjs +0 -5
- package/scripts/docs-lint.mjs +0 -170
- package/scripts/package-smoke.mjs +0 -135
- package/scripts/release-notes.mjs +0 -66
- package/scripts/size-budget.mjs +0 -95
- package/starters/modular/src/shared/ui/x-button.component.ts +0 -49
package/docs/en/14-forms.md
CHANGED
|
@@ -1,244 +1,91 @@
|
|
|
1
1
|
# Forms
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
`useForm()` adds reactive state to the browser's form and constraint-validation
|
|
4
|
+
model. HTML remains the source of truth for `required`, `type`, `min`, `max`,
|
|
5
|
+
`minlength`, `maxlength` and `pattern`.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
-
import { html,
|
|
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()`
|
|
8
|
+
import { html, useForm } from "@madojs/mado";
|
|
93
9
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
invalidates: ["/api/users*"],
|
|
10
|
+
const form = useForm({
|
|
11
|
+
initial: { email: "", age: "" as number | "", newsletter: false },
|
|
12
|
+
validate: async (values, { signal }) => {
|
|
13
|
+
const available = await api.emailAvailable(values.email, { signal });
|
|
14
|
+
return available ? null : { email: "Email is already registered" };
|
|
15
|
+
},
|
|
101
16
|
});
|
|
102
17
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<form @submit=${f.onSubmit(async (v) => {
|
|
107
|
-
await save.run(v);
|
|
108
|
-
navigate("/users");
|
|
18
|
+
html`
|
|
19
|
+
<form @submit=${form.onSubmit(async (values, event) => {
|
|
20
|
+
await api.save(values, new FormData(event.currentTarget as HTMLFormElement));
|
|
109
21
|
})}>
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
22
|
+
<label>
|
|
23
|
+
Email
|
|
24
|
+
<input name="email" type="email" required
|
|
25
|
+
@input=${form.onInput} @blur=${form.onBlur} />
|
|
26
|
+
</label>
|
|
27
|
+
${() => form.touched().email && form.errors().email
|
|
28
|
+
? html`<small role="alert">${form.errors().email}</small>`
|
|
29
|
+
: null}
|
|
30
|
+
|
|
31
|
+
<input name="age" type="number" min="18" max="120" @input=${form.onInput} />
|
|
32
|
+
<label><input name="newsletter" type="checkbox" @input=${form.onInput} /> Subscribe</label>
|
|
33
|
+
<button type="submit" ?disabled=${() => form.submitting() || form.validating()}>
|
|
34
|
+
Save
|
|
35
|
+
</button>
|
|
113
36
|
</form>
|
|
114
37
|
`;
|
|
115
38
|
```
|
|
116
39
|
|
|
117
|
-
|
|
118
|
-
`onSubmit` handler runtime. Usually only one of them is interesting.
|
|
119
|
-
|
|
120
|
-
## Shadow DOM inputs — when you need them
|
|
40
|
+
## API
|
|
121
41
|
|
|
122
|
-
|
|
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.
|
|
42
|
+
`useForm<T>({ initial, validate? })` returns:
|
|
126
43
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
44
|
+
| Member | Meaning |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `values()` | current typed values |
|
|
47
|
+
| `errors()` | native and custom errors by field; `$form` is available for form-wide errors |
|
|
48
|
+
| `touched()` | fields that have blurred or participated in submit |
|
|
49
|
+
| `dirty()` | whether values differ from the current initial values |
|
|
50
|
+
| `isValid()` | whether the merged error map is empty |
|
|
51
|
+
| `submitting()` / `validating()` | current async activity |
|
|
52
|
+
| `setField(name, value)` | update one field programmatically |
|
|
53
|
+
| `validate(form?)` | run native and custom validation; returns `Promise<boolean>` |
|
|
54
|
+
| `reset(nextInitial?)` | abort validation, clear state and reset native controls |
|
|
55
|
+
| `onInput`, `onBlur`, `onSubmit(handler)` | DOM event handlers |
|
|
130
56
|
|
|
131
|
-
|
|
132
|
-
|
|
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>`.
|
|
57
|
+
Custom validation receives `{ signal, form }`. Starting another validation or
|
|
58
|
+
calling `reset()` aborts the previous run. Treat abort as normal cancellation.
|
|
138
59
|
|
|
139
|
-
|
|
140
|
-
custom input forces you to. For everything else, see "Light-DOM
|
|
141
|
-
inputs" further down.
|
|
60
|
+
## Native controls
|
|
142
61
|
|
|
143
|
-
|
|
62
|
+
- Checkbox groups produce an array when the initial field value is an array;
|
|
63
|
+
a single checkbox produces a boolean.
|
|
64
|
+
- Radio groups produce the checked value.
|
|
65
|
+
- `<select multiple>` produces a string array.
|
|
66
|
+
- Number/range inputs produce a number, or `""` when empty.
|
|
67
|
+
- File inputs produce `File | null`, or `File[]` when `multiple` is present.
|
|
68
|
+
- The submit callback receives the native `SubmitEvent`; construct `FormData`
|
|
69
|
+
from its form when the backend expects multipart or repeated fields.
|
|
144
70
|
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
```
|
|
71
|
+
Use a native `<button type="submit">`. A button inside another element's
|
|
72
|
+
Shadow DOM is not a submit control for the outer form and keyboard submission
|
|
73
|
+
will not discover it.
|
|
170
74
|
|
|
171
|
-
|
|
172
|
-
(`composed: true` by default), so `<form @input=${f.onInput}>` keeps
|
|
173
|
-
working.
|
|
174
|
-
|
|
175
|
-
### Custom submit button — `requestSubmit()`
|
|
75
|
+
## Mutations
|
|
176
76
|
|
|
177
77
|
```ts
|
|
178
|
-
|
|
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
|
-
`;
|
|
78
|
+
const save = mutation((values: Profile) => api.save(values), {
|
|
79
|
+
invalidates: ["/api/profile"],
|
|
195
80
|
});
|
|
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.
|
|
81
|
+
const form = useForm({ initial: { displayName: "" } });
|
|
208
82
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"
|
|
212
|
-
|
|
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
|
-
);
|
|
83
|
+
html`<form @submit=${form.onSubmit((values) => save.run(values))}>
|
|
84
|
+
<input name="displayName" required minlength="2" @input=${form.onInput} />
|
|
85
|
+
<button type="submit" ?disabled=${() => form.submitting()}>Save</button>
|
|
86
|
+
</form>`;
|
|
223
87
|
```
|
|
224
88
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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.
|
|
89
|
+
Write failures belong to the mutation; field and cross-field validation belong
|
|
90
|
+
to the form. See the [0.12 → 0.13 migration guide](./33-migration-0.12-0.13.md)
|
|
91
|
+
for the removed schema and field-array APIs.
|
|
@@ -151,9 +151,9 @@ same shape the snapshot captured.
|
|
|
151
151
|
resource(), custom element definition) fails the snapshot. A small
|
|
152
152
|
allow-list covers `/favicon.ico`, `/favicon.svg`, `/robots.txt`
|
|
153
153
|
and `data:` URLs.
|
|
154
|
-
- **All network calls
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
- **All network calls must target the internal capture origin** serving the
|
|
155
|
+
freshly built `out/` tree. Undeclared external HTTP(S) requests are blocked;
|
|
156
|
+
provide deterministic `static.initialData()` instead.
|
|
157
157
|
|
|
158
158
|
## Comparison
|
|
159
159
|
|
|
@@ -178,4 +178,4 @@ multi-locale builds live in [23-cookbook.md](./23-cookbook.md).
|
|
|
178
178
|
- Declare `static: true | { paths, initialData }` on `page({ ... })`.
|
|
179
179
|
- Run `mado release`.
|
|
180
180
|
- Ship `out/` to any static host.
|
|
181
|
-
- The live SPA takes over atomically on first paint.
|
|
181
|
+
- The live SPA takes over atomically on first paint.
|
|
@@ -190,8 +190,9 @@ Reusable leaf components keep their own styles in ``css`...` `` inside
|
|
|
190
190
|
`component()` options. They should depend on tokens (`var(--color-text)`)
|
|
191
191
|
rather than global classes.
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
Vite 8's standard CSS pipeline owns transforms and minification. Mado adds no
|
|
194
|
+
CSS transformer; applications can configure Vite directly when they have a
|
|
195
|
+
measured need.
|
|
195
196
|
|
|
196
197
|
## Growth
|
|
197
198
|
|
package/docs/en/20-deployment.md
CHANGED
|
@@ -62,7 +62,7 @@ Key lines of the recipe `nginx.conf`:
|
|
|
62
62
|
- `gzip_static on;` — serves the precompressed `.gz` files written by
|
|
63
63
|
`mado release`. Zero CPU at request time.
|
|
64
64
|
- `/assets/*` should be cached immutable; Vite filenames are content hashed.
|
|
65
|
-
- `try_files $uri $uri/ /
|
|
65
|
+
- `try_files $uri $uri/ /_mado/spa.html;` — SPA fallback so deep links work
|
|
66
66
|
after a hard refresh.
|
|
67
67
|
|
|
68
68
|
Enable HTTPS with Let's Encrypt / Certbot. Add HSTS once you have it.
|
|
@@ -76,7 +76,7 @@ mado release
|
|
|
76
76
|
npx wrangler pages deploy out --project-name=myapp
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
- The generated `_redirects` (`/* /
|
|
79
|
+
- The generated `_redirects` (`/* /_mado/spa.html 200`) gives you SPA fallback.
|
|
80
80
|
- The generated `_headers` (immutable cache for `/assets/*`, `no-cache` for
|
|
81
81
|
HTML) is honored by CF Pages.
|
|
82
82
|
- Baked routes are promoted to real route files (`out/<route>/index.html`),
|
|
@@ -116,16 +116,15 @@ aws s3 sync out/ s3://my-bucket/ \
|
|
|
116
116
|
--cache-control "no-cache, must-revalidate" --include '*.html'
|
|
117
117
|
```
|
|
118
118
|
Configure CloudFront's "Default root object" to `index.html` and add a custom
|
|
119
|
-
error response: 403/404 → `/
|
|
119
|
+
error response: 403/404 → `/_mado/spa.html` with status 200 (SPA fallback).
|
|
120
120
|
|
|
121
121
|
**GitHub Pages**
|
|
122
122
|
```bash
|
|
123
123
|
mado release
|
|
124
124
|
# Push out/ into the gh-pages branch (or use actions/upload-pages-artifact)
|
|
125
125
|
```
|
|
126
|
-
Pages handles `index.html` automatically.
|
|
127
|
-
|
|
128
|
-
[`spa-github-pages`](https://github.com/rafgraph/spa-github-pages) trick.
|
|
126
|
+
Pages handles route `index.html` files automatically. `mado release` also writes
|
|
127
|
+
`404.html` from the noindex SPA shell for client-only routes.
|
|
129
128
|
|
|
130
129
|
---
|
|
131
130
|
|
|
@@ -177,7 +176,7 @@ jobs:
|
|
|
177
176
|
|
|
178
177
|
- **404 on hard refresh of a deep link.** Your host did not pick up SPA
|
|
179
178
|
fallback. nginx: check `try_files`. CF/Netlify: `_redirects` is present?
|
|
180
|
-
S3+CloudFront: configure the 404 → `/
|
|
179
|
+
S3+CloudFront: configure the 404 → `/_mado/spa.html` (200) error response.
|
|
181
180
|
- **HTML is cached forever.** Either your host sent a default
|
|
182
181
|
`Cache-Control: public, max-age=...` or you are sitting behind a CDN that
|
|
183
182
|
ignores `no-cache`. Add an explicit rule mirroring the matrix above.
|
|
@@ -189,6 +188,20 @@ jobs:
|
|
|
189
188
|
field. If you still see it, upgrade `@madojs/mado` and re-run
|
|
190
189
|
`mado release`.
|
|
191
190
|
|
|
191
|
+
## CSP and trusted content
|
|
192
|
+
|
|
193
|
+
Mado serialises JSON-LD and static seed JSON with `<`, U+2028 and U+2029
|
|
194
|
+
escaped, so data cannot close its script element. `unsafeHTML()` is different:
|
|
195
|
+
it is an explicit trust boundary and must receive only audited or sanitised
|
|
196
|
+
markup.
|
|
197
|
+
|
|
198
|
+
Start with a restrictive policy such as `default-src 'self'; script-src
|
|
199
|
+
'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'`
|
|
200
|
+
and tighten styles with nonces/hashes when your deployment supports them.
|
|
201
|
+
Trusted Types can enforce the same application boundary around any code that
|
|
202
|
+
produces trusted HTML. The devtools overlay is development-only and should not
|
|
203
|
+
be imported in production entrypoints.
|
|
204
|
+
|
|
192
205
|
## Sub-path deployments and `base`
|
|
193
206
|
|
|
194
207
|
A deployment under a sub-path (`https://your.site/docs/`, GitHub
|
|
@@ -226,7 +239,7 @@ html`<a data-link href=${routeUrl("/guides/intro")}>Intro</a>`;
|
|
|
226
239
|
|
|
227
240
|
On the production host:
|
|
228
241
|
|
|
229
|
-
- **nginx** — keep `try_files $uri $uri/ /
|
|
242
|
+
- **nginx** — keep `try_files $uri $uri/ /_mado/spa.html;` but mount the
|
|
230
243
|
app under the matching prefix (`location /docs/ { ... }`).
|
|
231
244
|
- **Cloudflare Pages / Netlify** — `_redirects` is generated with the
|
|
232
245
|
base prefix already baked in.
|
|
@@ -24,7 +24,7 @@ For a specific page, `page({ errorView })` wins over the global route boundary.
|
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
26
|
export default page({
|
|
27
|
-
load:
|
|
27
|
+
load: () => resource(() => "/reports", jsonFetcher<Report[]>()),
|
|
28
28
|
errorView: (err) => html`<x-report-error .error=${err}></x-report-error>`,
|
|
29
29
|
view: ({ data }) => html`<x-report .data=${data}></x-report>`,
|
|
30
30
|
});
|
|
@@ -53,10 +53,10 @@ Validation errors belong in `useForm()`. Server errors from writes belong near
|
|
|
53
53
|
the submit button.
|
|
54
54
|
|
|
55
55
|
```ts
|
|
56
|
-
const form = useForm(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
);
|
|
56
|
+
const form = useForm({
|
|
57
|
+
initial: { email: "" },
|
|
58
|
+
validate: (values, { signal }) => api.validateUser(values, { signal }),
|
|
59
|
+
});
|
|
60
60
|
|
|
61
61
|
const save = mutation((values) => api.post("/users", values), {
|
|
62
62
|
invalidates: ["/api/users*"],
|
|
@@ -66,6 +66,7 @@ html`
|
|
|
66
66
|
<form @submit=${form.onSubmit(async values => {
|
|
67
67
|
await save.run(values);
|
|
68
68
|
})}>
|
|
69
|
+
<input name="email" type="email" required @input=${form.onInput} />
|
|
69
70
|
<button ?disabled=${() => form.validating() || form.submitting()}>
|
|
70
71
|
Save
|
|
71
72
|
</button>
|
|
@@ -97,4 +98,6 @@ component("x-online", (ctx) => {
|
|
|
97
98
|
|
|
98
99
|
Log once at the boundary that owns recovery. Avoid logging the same failure in
|
|
99
100
|
the API client, resource, page and component. The user should get one visible
|
|
100
|
-
message and developers should get one
|
|
101
|
+
message and developers should get one structured diagnostic. Runtime records
|
|
102
|
+
are visible in the devtools Timeline/Errors panel and as `mado:diagnostic`
|
|
103
|
+
events; CLI automation can select `--log-format=json`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Devtools and diagnostics
|
|
2
|
+
|
|
3
|
+
Development instrumentation is opt-in and compiled out of production builds.
|
|
4
|
+
Import the stable devtools subpath before application startup:
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { devtools } from "@madojs/mado/devtools.js";
|
|
8
|
+
|
|
9
|
+
devtools.open();
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`Alt+Shift+M` toggles the Shadow DOM overlay. Its Overview, Reactivity,
|
|
13
|
+
Components, Router/Data and Timeline/Errors views are fed by a versioned
|
|
14
|
+
internal hook. The controller also exposes `close()`, `toggle()`, `clear()`,
|
|
15
|
+
`setLogLevel()` and `snapshot()`. Snapshots contain safe previews rather than
|
|
16
|
+
live application objects.
|
|
17
|
+
|
|
18
|
+
`localStorage.madoDebug = "1"` is accepted during 0.13 migration only and emits
|
|
19
|
+
a deprecation warning.
|
|
20
|
+
|
|
21
|
+
## Runtime diagnostics
|
|
22
|
+
|
|
23
|
+
Every record has `level`, `scope`, `code`, `message`, optional `data`, and a
|
|
24
|
+
timestamp. Set browser verbosity with:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
localStorage.setItem("mado:log-level", "debug");
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Allowed levels are `debug`, `info`, `warn`, `error` and `silent`. Records are
|
|
31
|
+
also dispatched as `mado:diagnostic` events and appear in devtools.
|
|
32
|
+
|
|
33
|
+
## CLI diagnostics
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
mado release --log-level=debug --log-format=pretty
|
|
37
|
+
mado release --log-format=plain
|
|
38
|
+
mado release --log-format=json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The CLI honours `MADO_LOG_LEVEL`, `MADO_LOG_FORMAT` and `NO_COLOR`. Non-TTY
|
|
42
|
+
output automatically becomes plain. JSON mode writes one structured record per
|
|
43
|
+
line, suitable for CI ingestion.
|
|
44
|
+
|
|
45
|
+
The logger is internal infrastructure, not a public logging API for apps.
|
|
@@ -9,11 +9,11 @@ package root:
|
|
|
9
9
|
import { component, html, resource, routes, signal } from "@madojs/mado";
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
The public package subpaths are the
|
|
13
|
-
|
|
12
|
+
The public package subpaths are the devtools controller and the Vite tooling
|
|
13
|
+
integration:
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
import "@madojs/mado/devtools.js";
|
|
16
|
+
import { devtools } from "@madojs/mado/devtools.js";
|
|
17
17
|
import { mado } from "@madojs/mado/vite";
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -26,7 +26,7 @@ These names are public and protected by SemVer once v1 ships:
|
|
|
26
26
|
|
|
27
27
|
- Reactivity: `signal`, `computed`, `effect`, `untracked`, `batch`,
|
|
28
28
|
`flushSync`.
|
|
29
|
-
- Templates and directives: `html`, `render`, `
|
|
29
|
+
- Templates and directives: `html`, `render`, `unmount`, `each`, `unsafeHTML`,
|
|
30
30
|
`ref`, `classMap`, `styleMap`.
|
|
31
31
|
- Components and CSS: `component`, `css`, `cssVars`.
|
|
32
32
|
- Routing and pages: `routes`, `router`, `page`, `layout`,
|
|
@@ -37,9 +37,8 @@ These names are public and protected by SemVer once v1 ships:
|
|
|
37
37
|
- Context: `createContext`, `provide`, `inject`.
|
|
38
38
|
- Advanced lifecycle helpers: `createLifecycle`, `runInLifecycle`,
|
|
39
39
|
`getCurrentLifecycle`.
|
|
40
|
-
- Public TypeScript types exported from `@madojs/mado`, including
|
|
41
|
-
`
|
|
42
|
-
and `HeadMeta` (the head metadata shape returned by pages).
|
|
40
|
+
- Public TypeScript types exported from `@madojs/mado`, including `FormApi`,
|
|
41
|
+
`Resource`, `ComponentContext`, `StaticPageConfig` and `HeadMeta`.
|
|
43
42
|
|
|
44
43
|
## Internal or unstable
|
|
45
44
|
|
|
@@ -66,8 +65,8 @@ Application code should not.
|
|
|
66
65
|
|
|
67
66
|
## What can change
|
|
68
67
|
|
|
69
|
-
Patch and minor releases may add
|
|
70
|
-
|
|
68
|
+
Patch and minor releases may add compatible options, diagnostics, docs or
|
|
69
|
+
starter files. New root exports require an explicit API review. Releases may change internals, emitted bundle shape, and
|
|
71
70
|
implementation details as long as the stable API and documented behavior remain
|
|
72
71
|
compatible.
|
|
73
72
|
|
|
@@ -17,7 +17,7 @@ After v1, Mado treats these as SemVer-protected:
|
|
|
17
17
|
|
|
18
18
|
- Public exports from `@madojs/mado`.
|
|
19
19
|
- Public TypeScript types exported from `@madojs/mado`.
|
|
20
|
-
- The `@madojs/mado/devtools.js`
|
|
20
|
+
- The `@madojs/mado/devtools.js` controller subpath and `@madojs/mado/vite`.
|
|
21
21
|
- Template binding syntax: child `${}`, `@event`, `.prop`, `?boolean`,
|
|
22
22
|
attribute bindings, directives and `each()`.
|
|
23
23
|
- Signal semantics documented in the reactivity ordering guide.
|
|
@@ -33,7 +33,7 @@ Breaking these requires a major version.
|
|
|
33
33
|
|
|
34
34
|
Minor releases may add:
|
|
35
35
|
|
|
36
|
-
- New root exports.
|
|
36
|
+
- New root exports after explicit API review and golden-file update.
|
|
37
37
|
- New options on existing APIs.
|
|
38
38
|
- New diagnostics and warnings.
|
|
39
39
|
- New starters, examples, docs and CLI flags.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Migrating from 0.12 to 0.13
|
|
2
|
+
|
|
3
|
+
0.13 is the final planned pre-v1 breaking release. It narrows the root API and
|
|
4
|
+
makes lifecycle, forms, resources and router behaviour explicit.
|
|
5
|
+
|
|
6
|
+
## Forms
|
|
7
|
+
|
|
8
|
+
<!-- docs-lint:allow-legacy-mention -->
|
|
9
|
+
|
|
10
|
+
The 0.12 schema-shaped call:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
useForm({ email: { required: true, type: "email" } });
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
becomes:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
useForm({ initial: { email: "" } });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Move `required`, `type`, `min`, `max` and `pattern` to the native controls.
|
|
23
|
+
Replace `setValue` with `setField`. The separate field-array helper was removed;
|
|
24
|
+
use ordinary array values and `setField`. Custom/async validation now belongs
|
|
25
|
+
to `validate(values, { signal, form })` in the options object.
|
|
26
|
+
|
|
27
|
+
<!-- /docs-lint:allow-legacy-mention -->
|
|
28
|
+
|
|
29
|
+
## Removed root exports
|
|
30
|
+
|
|
31
|
+
`lazy`, `list`, `instantiate`, `isHtmlDirective`, `adopt`, `scopeStyles`,
|
|
32
|
+
`isPage` and `isLayoutGroup` are no longer root exports. Use dynamic route
|
|
33
|
+
imports, `each()`, `render()` and the documented high-level APIs instead.
|
|
34
|
+
Only the package root, `/vite` and `/devtools.js` are public subpaths.
|
|
35
|
+
|
|
36
|
+
## Behaviour changes
|
|
37
|
+
|
|
38
|
+
- `render()` returns an idempotent disposer; `unmount(container)` explicitly
|
|
39
|
+
releases bindings, events, nested templates and child lifecycles.
|
|
40
|
+
- `page.load` is synchronous. Return a value or a `Resource`; returning a
|
|
41
|
+
Promise throws a diagnostic error. Put async work in `resource()`.
|
|
42
|
+
- `resource().refresh()` returns `Promise<T>`. `staleTime: 0` deduplicates only
|
|
43
|
+
an in-flight request and never reuses completed data.
|
|
44
|
+
- Cache identity includes the fetcher, and `invalidate()` reaches cached and
|
|
45
|
+
uncached live resources.
|
|
46
|
+
- Persisted signals separate `dispose()` from explicit storage deletion.
|
|
47
|
+
- Context now uses the Web Components `context-request` protocol and cleans up
|
|
48
|
+
subscriptions with the current lifecycle.
|
|
49
|
+
- Static capture blocks undeclared external requests and `mado build` writes
|
|
50
|
+
the `out/` artifact consumed by `mado static`.
|
|
51
|
+
|
|
52
|
+
## Devtools and logging
|
|
53
|
+
|
|
54
|
+
Import `@madojs/mado/devtools.js` in development and use `Alt+Shift+M`.
|
|
55
|
+
`localStorage.madoDebug` is deprecated; use `mado:log-level` or the controller.
|
|
56
|
+
CLI automation should prefer `--log-format=json`.
|