@madojs/mado 0.6.1 → 0.8.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 +82 -30
- package/CHANGELOG.md +150 -3
- package/dist/src/component.d.ts +17 -4
- package/dist/src/component.js +43 -4
- package/dist/src/component.js.map +1 -1
- package/dist/src/forms.js +4 -1
- package/dist/src/forms.js.map +1 -1
- package/dist/src/page.d.ts +12 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/router/manifest.js +7 -1
- package/dist/src/router/manifest.js.map +1 -1
- package/docs/en/07-llm-pitfalls.md +197 -60
- package/docs/en/08-llm-zero-history-test.md +1 -1
- package/docs/en/17-shadow-dom-forms.md +192 -0
- package/docs/en/README.md +20 -19
- package/docs/fr/07-llm-pitfalls.md +196 -60
- package/docs/fr/17-shadow-dom-forms.md +196 -0
- package/docs/fr/README.md +20 -19
- package/docs/ru/07-llm-pitfalls.md +198 -61
- package/docs/ru/08-llm-zero-history-test.md +39 -38
- package/docs/ru/09-shadow-vs-light-dom.md +97 -81
- package/docs/ru/17-shadow-dom-forms.md +193 -0
- package/docs/ru/README.md +20 -19
- package/docs/uk/07-llm-pitfalls.md +64 -3
- package/docs/uk/17-shadow-dom-forms.md +193 -0
- package/docs/uk/README.md +20 -19
- package/llms.txt +50 -1
- package/package.json +2 -2
- package/scripts/bake.mjs +25 -19
- package/scripts/cli.mjs +22 -33
- package/starters/admin/src/components/x-button.ts +40 -13
- package/starters/admin/src/components/x-input.ts +50 -19
- package/starters/admin/src/lib/api.ts +55 -4
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Shadow DOM + Forms
|
|
2
|
+
|
|
3
|
+
Using `useForm()` with custom input components that have Shadow DOM requires
|
|
4
|
+
awareness of two browser-level behaviours:
|
|
5
|
+
|
|
6
|
+
1. **Event retargeting** — events that bubble from Shadow DOM have their
|
|
7
|
+
`e.target` retargeted to the host element. `useForm().onInput` reads
|
|
8
|
+
`e.target.name` and `e.target.value`, but an `<x-input>` host element
|
|
9
|
+
doesn't natively have these properties.
|
|
10
|
+
|
|
11
|
+
2. **Form association** — a `<button type="submit">` inside a Shadow Root is
|
|
12
|
+
NOT part of the form-owner algorithm for `<form>` in Light DOM. Clicking it
|
|
13
|
+
does not trigger form submit.
|
|
14
|
+
|
|
15
|
+
Both are spec-level limitations, not Mado bugs. But the framework provides
|
|
16
|
+
patterns that make them painless.
|
|
17
|
+
|
|
18
|
+
## Pattern: Proxy Properties on Input Components
|
|
19
|
+
|
|
20
|
+
When wrapping `<input>` in a Shadow DOM component, expose `name` and `value`
|
|
21
|
+
as DOM properties on the host so that `useForm().onInput` works after event
|
|
22
|
+
retargeting:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { component, css, html } from "@madojs/mado";
|
|
26
|
+
|
|
27
|
+
component("x-input", ({ host, attr }) => {
|
|
28
|
+
const name = attr("name", "");
|
|
29
|
+
const type = attr("type", "text");
|
|
30
|
+
const value = attr("value", "");
|
|
31
|
+
|
|
32
|
+
// Proxy properties for useForm() compatibility.
|
|
33
|
+
// After Shadow DOM retargets e.target from <input> to <x-input>,
|
|
34
|
+
// useForm reads e.target.name / e.target.value — these getters bridge the gap.
|
|
35
|
+
Object.defineProperty(host, "name", {
|
|
36
|
+
get: () => host.getAttribute("name") ?? "",
|
|
37
|
+
configurable: true,
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(host, "value", {
|
|
40
|
+
get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
|
|
41
|
+
set: (v: string) => {
|
|
42
|
+
const input = host.shadowRoot?.querySelector("input");
|
|
43
|
+
if (input) input.value = v;
|
|
44
|
+
},
|
|
45
|
+
configurable: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return () => html` <input name=${name} type=${type} .value=${value} /> `;
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The `input` event from the inner `<input>` has `composed: true` by default, so
|
|
53
|
+
it will bubble through the shadow boundary. After retargeting, `e.target` is
|
|
54
|
+
`<x-input>`, but now it has `.name` and `.value` getters → `useForm` works.
|
|
55
|
+
|
|
56
|
+
## Pattern: Form Submit from Shadow DOM Buttons
|
|
57
|
+
|
|
58
|
+
A `<button type="submit">` inside Shadow DOM cannot trigger `<form>` submit in
|
|
59
|
+
Light DOM. Bridge it with `requestSubmit()`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { component, css, html } from "@madojs/mado";
|
|
63
|
+
|
|
64
|
+
component("x-button", ({ host, attr }) => {
|
|
65
|
+
const disabled = attr("disabled");
|
|
66
|
+
|
|
67
|
+
const handleClick = () => {
|
|
68
|
+
const typeAttr = host.getAttribute("type");
|
|
69
|
+
if (typeAttr === "button" || typeAttr === "reset") return;
|
|
70
|
+
const form = host.closest("form");
|
|
71
|
+
if (form && !host.hasAttribute("disabled")) form.requestSubmit();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return () => html`
|
|
75
|
+
<button ?disabled=${() => disabled() !== ""} @click=${handleClick}>
|
|
76
|
+
<slot></slot>
|
|
77
|
+
</button>
|
|
78
|
+
`;
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`host.closest("form")` works because the host element itself lives in Light DOM
|
|
83
|
+
(only its internals are shadowed). `requestSubmit()` triggers validation and the
|
|
84
|
+
`submit` event exactly as if the user had clicked a native submit button inside
|
|
85
|
+
the form.
|
|
86
|
+
|
|
87
|
+
## Pattern: Reactive Attributes with ctx.attr()
|
|
88
|
+
|
|
89
|
+
Since v0.7, `ctx.attr(name, defaultValue?)` returns a `Signal<string>` that
|
|
90
|
+
updates automatically when the attribute changes on the host. No more
|
|
91
|
+
`MutationObserver` boilerplate:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
component("x-badge", ({ attr }) => {
|
|
95
|
+
const variant = attr("variant", "default"); // Signal<string>
|
|
96
|
+
|
|
97
|
+
return () =>
|
|
98
|
+
html`<span class=${() => `badge badge-${variant()}`}>
|
|
99
|
+
<slot></slot>
|
|
100
|
+
</span>`;
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The parent can use `?disabled=${() => !form.isValid()}` (boolean attribute) or
|
|
105
|
+
`.variant=${"danger"}` — the component re-renders reactively either way.
|
|
106
|
+
|
|
107
|
+
## Complete Form Example
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { page, html, useForm, navigate } from "@madojs/mado";
|
|
111
|
+
import "../components/x-input.js";
|
|
112
|
+
import "../components/x-button.js";
|
|
113
|
+
|
|
114
|
+
export default page({
|
|
115
|
+
title: "Login",
|
|
116
|
+
view: () => {
|
|
117
|
+
const form = useForm({
|
|
118
|
+
email: { required: true, type: "email" },
|
|
119
|
+
password: { required: true, minLength: 6 },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const handleLogin = async (values) => {
|
|
123
|
+
await api("/auth/login", { method: "POST", json: values });
|
|
124
|
+
navigate("/admin");
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return html`
|
|
128
|
+
<form @submit=${form.onSubmit(handleLogin)}>
|
|
129
|
+
<x-input
|
|
130
|
+
name="email"
|
|
131
|
+
type="email"
|
|
132
|
+
label="Email"
|
|
133
|
+
required
|
|
134
|
+
@input=${form.onInput}
|
|
135
|
+
@blur=${form.onBlur}
|
|
136
|
+
></x-input>
|
|
137
|
+
${() =>
|
|
138
|
+
form.errors().email
|
|
139
|
+
? html`<small class="err">${form.errors().email}</small>`
|
|
140
|
+
: null}
|
|
141
|
+
|
|
142
|
+
<x-input
|
|
143
|
+
name="password"
|
|
144
|
+
type="password"
|
|
145
|
+
label="Password"
|
|
146
|
+
required
|
|
147
|
+
@input=${form.onInput}
|
|
148
|
+
@blur=${form.onBlur}
|
|
149
|
+
></x-input>
|
|
150
|
+
|
|
151
|
+
<x-button type="submit" ?disabled=${() => !form.isValid()}>
|
|
152
|
+
Sign in
|
|
153
|
+
</x-button>
|
|
154
|
+
</form>
|
|
155
|
+
`;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## When to Use Light DOM Instead
|
|
161
|
+
|
|
162
|
+
If your input component is just a styled wrapper without encapsulation needs,
|
|
163
|
+
`shadow: false` avoids both retargeting and form-association issues entirely:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
component(
|
|
167
|
+
"x-field",
|
|
168
|
+
({ attr }) => {
|
|
169
|
+
const label = attr("label", "");
|
|
170
|
+
return () => html`
|
|
171
|
+
<label>
|
|
172
|
+
<span>${label}</span>
|
|
173
|
+
<slot></slot>
|
|
174
|
+
</label>
|
|
175
|
+
`;
|
|
176
|
+
},
|
|
177
|
+
{ shadow: false },
|
|
178
|
+
);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
With Light DOM, the native `<input>` is part of the document tree, events
|
|
182
|
+
are not retargeted, and form submission works natively. The tradeoff: styles
|
|
183
|
+
are not encapsulated (you must scope them yourself).
|
|
184
|
+
|
|
185
|
+
## Summary
|
|
186
|
+
|
|
187
|
+
| Concern | Shadow DOM Solution | Light DOM Alternative |
|
|
188
|
+
| ------------------------ | --------------------------------------- | ----------------------------- |
|
|
189
|
+
| `useForm` + custom input | Proxy `name`/`value` on host | Use native `<input>` in slot |
|
|
190
|
+
| Form submit | `form.requestSubmit()` in click handler | Native button works |
|
|
191
|
+
| Reactive attributes | `ctx.attr()` → auto-signal | `ctx.attr()` works everywhere |
|
|
192
|
+
| Style encapsulation | Yes (automatic) | Manual `@scope` or BEM |
|
package/docs/en/README.md
CHANGED
|
@@ -2,22 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
English documentation set.
|
|
4
4
|
|
|
5
|
-
| Section
|
|
6
|
-
|
|
7
|
-
| The Mado way
|
|
8
|
-
| Routing
|
|
9
|
-
| Project layout
|
|
10
|
-
| Static bake & SEO
|
|
11
|
-
| IDE setup
|
|
12
|
-
| Why Mado
|
|
13
|
-
| For backenders
|
|
14
|
-
| LLM pitfalls
|
|
15
|
-
| LLM zero-history test
|
|
16
|
-
| Shadow DOM vs Light DOM | [09-shadow-vs-light-dom.md](./09-shadow-vs-light-dom.md)
|
|
17
|
-
| App architecture
|
|
18
|
-
| Layouts
|
|
19
|
-
| Auth and API
|
|
20
|
-
| Deployment
|
|
21
|
-
| Testing
|
|
22
|
-
| Error handling
|
|
23
|
-
| Bake cookbook
|
|
5
|
+
| Section | Source |
|
|
6
|
+
| ----------------------- | ------------------------------------------------------------ |
|
|
7
|
+
| The Mado way | [00-the-mado-way.md](./00-the-mado-way.md) |
|
|
8
|
+
| Routing | [01-routing.md](./01-routing.md) |
|
|
9
|
+
| Project layout | [02-project-layout.md](./02-project-layout.md) |
|
|
10
|
+
| Static bake & SEO | [03-static-bake.md](./03-static-bake.md) |
|
|
11
|
+
| IDE setup | [04-ide-setup.md](./04-ide-setup.md) |
|
|
12
|
+
| Why Mado | [05-why-mado.md](./05-why-mado.md) |
|
|
13
|
+
| For backenders | [06-for-backenders.md](./06-for-backenders.md) |
|
|
14
|
+
| LLM pitfalls | [07-llm-pitfalls.md](./07-llm-pitfalls.md) |
|
|
15
|
+
| LLM zero-history test | [08-llm-zero-history-test.md](./08-llm-zero-history-test.md) |
|
|
16
|
+
| Shadow DOM vs Light DOM | [09-shadow-vs-light-dom.md](./09-shadow-vs-light-dom.md) |
|
|
17
|
+
| App architecture | [10-app-architecture.md](./10-app-architecture.md) |
|
|
18
|
+
| Layouts | [11-layouts.md](./11-layouts.md) |
|
|
19
|
+
| Auth and API | [12-auth-and-api.md](./12-auth-and-api.md) |
|
|
20
|
+
| Deployment | [13-deployment.md](./13-deployment.md) |
|
|
21
|
+
| Testing | [14-testing.md](./14-testing.md) |
|
|
22
|
+
| Error handling | [15-error-handling.md](./15-error-handling.md) |
|
|
23
|
+
| Bake cookbook | [16-bake-cookbook.md](./16-bake-cookbook.md) |
|
|
24
|
+
| Shadow DOM + Forms | [17-shadow-dom-forms.md](./17-shadow-dom-forms.md) |
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
> commettent lors de la génération de code Mado. Et comment les corriger.
|
|
5
5
|
|
|
6
6
|
Ce document s'adresse à **deux publics** :
|
|
7
|
+
|
|
7
8
|
1. **Les agents IA dans l'IDE** qui lisent `AGENTS.md` / `.cursorrules` / `.github/copilot-instructions.md`. Plus de détails sur les pièges typiques sont fournis ici.
|
|
8
9
|
2. **Les humains** qui ont reçu du code d'une IA avec ces erreurs et ne comprennent pas ce qui ne va pas.
|
|
9
10
|
|
|
@@ -17,19 +18,20 @@ Ce document s'adresse à **deux publics** :
|
|
|
17
18
|
const count = signal(0);
|
|
18
19
|
|
|
19
20
|
// ❌ L'IA génère souvent ceci
|
|
20
|
-
html`<div>Compte : ${count() * 2}</div
|
|
21
|
+
html`<div>Compte : ${count() * 2}</div>`;
|
|
21
22
|
// → Affichera "Compte : 0" et ne se mettra plus jamais à jour.
|
|
22
23
|
// count() est lu une seule fois quand le TemplateResult est créé.
|
|
23
24
|
|
|
24
25
|
// ✅ Correct — fonction getter
|
|
25
|
-
html`<div>Compte : ${() => count() * 2}</div
|
|
26
|
+
html`<div>Compte : ${() => count() * 2}</div>`;
|
|
26
27
|
// → Mado créera un effect() pour cette fonction et re-rendra quand count change.
|
|
27
28
|
|
|
28
29
|
// ✅ Aussi correct — le signal lui-même est une fonction
|
|
29
|
-
html`<div>Compte : ${count}</div
|
|
30
|
+
html`<div>Compte : ${count}</div>`;
|
|
30
31
|
```
|
|
31
32
|
|
|
32
33
|
**Règle :**
|
|
34
|
+
|
|
33
35
|
- Si `${...}` contient une **expression** (quelque chose est fait avec le signal) — enveloppez dans `() => ...`.
|
|
34
36
|
- Si `${...}` contient **le signal lui-même** — il peut être utilisé tel quel.
|
|
35
37
|
|
|
@@ -45,10 +47,10 @@ Ceci s'applique aux **bindings enfants** (texte à l'intérieur des tags) et aux
|
|
|
45
47
|
const loading = signal(false);
|
|
46
48
|
|
|
47
49
|
// ❌ C'est setAttribute("disabled", "false") — le DOM traite ça comme disabled
|
|
48
|
-
html`<button disabled=${loading()}>Enregistrer</button
|
|
50
|
+
html`<button disabled=${loading()}>Enregistrer</button>`;
|
|
49
51
|
|
|
50
52
|
// ✅ Correct — binding booléen (basculer l'attribut)
|
|
51
|
-
html`<button ?disabled=${loading}>Enregistrer</button
|
|
53
|
+
html`<button ?disabled=${loading}>Enregistrer</button>`;
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
**Règles pour les attributs :**
|
|
@@ -86,6 +88,7 @@ component("x-counter", () => {
|
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|
**Différences clés :**
|
|
91
|
+
|
|
89
92
|
- Pas de hooks, pas de règles de hooks.
|
|
90
93
|
- `signal()` peut être créé n'importe où — dans le setup, dans un effect, dans un handler.
|
|
91
94
|
- `effect()` voit ce qu'il a lu de lui-même — pas besoin de tableau de dépendances.
|
|
@@ -172,11 +175,20 @@ import { Home } from "./pages/home.js";
|
|
|
172
175
|
|
|
173
176
|
```ts
|
|
174
177
|
// ❌ Fonctionne, mais sans clé : recrée le DOM à chaque changement
|
|
175
|
-
html`<ul
|
|
178
|
+
html`<ul>
|
|
179
|
+
${() => items().map((t) => html`<li>${t.name}</li>`)}
|
|
180
|
+
</ul>`;
|
|
176
181
|
|
|
177
182
|
// ✅ Correct : each() avec une fonction de clé
|
|
178
183
|
import { each } from "@madojs/mado";
|
|
179
|
-
html`<ul
|
|
184
|
+
html`<ul>
|
|
185
|
+
${() =>
|
|
186
|
+
each(
|
|
187
|
+
items(),
|
|
188
|
+
(t) => t.id,
|
|
189
|
+
(t) => html`<li>${t.name}</li>`,
|
|
190
|
+
)}
|
|
191
|
+
</ul>`;
|
|
180
192
|
```
|
|
181
193
|
|
|
182
194
|
**Règle :** utilisez toujours `each()` pour les listes de tableaux avec des IDs stables. Réservez `.map()` uniquement pour les listes statiques.
|
|
@@ -191,15 +203,15 @@ html`<ul>${() => each(items(), t => t.id, t => html`<li>${t.name}</li>`)}</ul>`
|
|
|
191
203
|
const count = signal(0);
|
|
192
204
|
|
|
193
205
|
// ❌ Pas une telle API
|
|
194
|
-
count.value
|
|
195
|
-
count.value = 5
|
|
196
|
-
count.get()
|
|
206
|
+
count.value;
|
|
207
|
+
count.value = 5;
|
|
208
|
+
count.get();
|
|
197
209
|
|
|
198
210
|
// ✅ Correct
|
|
199
|
-
count()
|
|
200
|
-
count.set(5)
|
|
201
|
-
count.update(n => n + 1)
|
|
202
|
-
count.peek()
|
|
211
|
+
count(); // lecture
|
|
212
|
+
count.set(5); // écriture
|
|
213
|
+
count.update((n) => n + 1);
|
|
214
|
+
count.peek(); // lecture sans abonnement
|
|
203
215
|
```
|
|
204
216
|
|
|
205
217
|
---
|
|
@@ -220,7 +232,7 @@ component("x-app", ({ host }) => {
|
|
|
220
232
|
});
|
|
221
233
|
|
|
222
234
|
component("x-child", ({ host }) => {
|
|
223
|
-
const api = inject(host, ApiCtx);
|
|
235
|
+
const api = inject(host, ApiCtx); // signal<valeur>
|
|
224
236
|
return () => html`...`;
|
|
225
237
|
});
|
|
226
238
|
```
|
|
@@ -242,6 +254,7 @@ if (typeof window !== "undefined") { ... } // dans Mado, window est TOUJOURS di
|
|
|
242
254
|
Mado **ne fait pas de SSR avec hydratation**. Le code ne s'exécute pas sur le serveur — il y a uniquement `bake` (prérendu statique au moment du build) et edge-prerender. Les deux remplacent le code utilisateur par un environnement linkedom, mais c'est **uniquement** pour générer du HTML avec des meta tags, pas pour exécuter la logique de page.
|
|
243
255
|
|
|
244
256
|
Cela signifie :
|
|
257
|
+
|
|
245
258
|
- ✅ `window`, `document`, `location`, `fetch` — disponibles sans vérifications.
|
|
246
259
|
- ❌ N'écrivez pas de code qui essaie de "fonctionner universellement sur serveur et client".
|
|
247
260
|
- ❌ N'utilisez pas les patterns Next.js (`getServerSideProps`, `headers()`).
|
|
@@ -259,7 +272,7 @@ const f = useForm({ resolver: zodResolver(schema) });
|
|
|
259
272
|
// ✅ Correct : validation proche du HTML via le schéma useForm
|
|
260
273
|
const f = useForm({
|
|
261
274
|
email: { required: true, type: "email" },
|
|
262
|
-
age:
|
|
275
|
+
age: { required: true, type: "number", min: 18 },
|
|
263
276
|
});
|
|
264
277
|
|
|
265
278
|
// ✅ Ou une fonction personnalisée si HTML5 ne suffit pas
|
|
@@ -288,11 +301,13 @@ Bootstrap-via-classes) **ne fonctionnent qu'en light DOM** (`shadow: false`) :
|
|
|
288
301
|
|
|
289
302
|
```ts
|
|
290
303
|
// Composant page/écran Light-DOM, les classes Tailwind fonctionnent
|
|
291
|
-
component(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
`,
|
|
304
|
+
component(
|
|
305
|
+
"x-admin-page",
|
|
306
|
+
() => () => html`
|
|
307
|
+
<section class="bg-white shadow-lg rounded-lg p-4">...</section>
|
|
308
|
+
`,
|
|
309
|
+
{ shadow: false },
|
|
310
|
+
);
|
|
296
311
|
|
|
297
312
|
// Composant Shadow-DOM (par défaut) — Tailwind ne fonctionne PAS.
|
|
298
313
|
// Utilisez css`` ou ::part() pour le stylage externe.
|
|
@@ -301,7 +316,7 @@ component("x-button", () => () => html`<button><slot></slot></button>`, {
|
|
|
301
316
|
button {
|
|
302
317
|
background: var(--button-bg, #2563eb);
|
|
303
318
|
color: white;
|
|
304
|
-
padding: .5rem 1rem;
|
|
319
|
+
padding: 0.5rem 1rem;
|
|
305
320
|
border-radius: 6px;
|
|
306
321
|
}
|
|
307
322
|
`,
|
|
@@ -334,6 +349,7 @@ Mado.signal(0);
|
|
|
334
349
|
**Symptôme :** l'IA suggère `npm install lodash` / `npm install date-fns` / etc.
|
|
335
350
|
|
|
336
351
|
Mado est **zéro dépendances runtime** par conception. Si l'IA veut ajouter :
|
|
352
|
+
|
|
337
353
|
- **lodash** → utilisez du JS natif (`Object.entries`, `Array.prototype`, `structuredClone`) ;
|
|
338
354
|
- **date-fns** → utilisez `Intl.DateTimeFormat` et `Intl.RelativeTimeFormat` ;
|
|
339
355
|
- **uuid** → `crypto.randomUUID()` ;
|
|
@@ -352,19 +368,27 @@ Toute dépendance runtime est une **violation des principes du framework**. Si v
|
|
|
352
368
|
// ❌ Fonctionne, mais se met à l'échelle difficilement et complique le nettoyage
|
|
353
369
|
page({
|
|
354
370
|
view: () => html`
|
|
355
|
-
<style
|
|
371
|
+
<style>
|
|
372
|
+
.panel {
|
|
373
|
+
padding: 1rem;
|
|
374
|
+
}
|
|
375
|
+
</style>
|
|
356
376
|
<section class="panel">...</section>
|
|
357
377
|
`,
|
|
358
378
|
});
|
|
359
379
|
|
|
360
380
|
// ✅ Correct : styles de composant via css``
|
|
361
|
-
component(
|
|
362
|
-
|
|
363
|
-
`,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
381
|
+
component(
|
|
382
|
+
"x-admin-panel",
|
|
383
|
+
() => () => html` <section class="panel">...</section> `,
|
|
384
|
+
{
|
|
385
|
+
styles: css`
|
|
386
|
+
.panel {
|
|
387
|
+
padding: 1rem;
|
|
388
|
+
}
|
|
389
|
+
`,
|
|
390
|
+
},
|
|
391
|
+
);
|
|
368
392
|
```
|
|
369
393
|
|
|
370
394
|
Pour les écrans route/page d'admin backend, il est souvent approprié d'utiliser `shadow: false`,
|
|
@@ -381,10 +405,10 @@ la page ou n'est pas préchargé.
|
|
|
381
405
|
|
|
382
406
|
```ts
|
|
383
407
|
// ❌ Lien ordinaire : le navigateur effectuera un rechargement complet
|
|
384
|
-
html`<a href="/tickets/42">Ouvrir</a
|
|
408
|
+
html`<a href="/tickets/42">Ouvrir</a>`;
|
|
385
409
|
|
|
386
410
|
// ✅ Navigation SPA : router() interceptera le clic même à travers Shadow DOM
|
|
387
|
-
html`<a href="/tickets/42" data-link>Ouvrir</a
|
|
411
|
+
html`<a href="/tickets/42" data-link>Ouvrir</a>`;
|
|
388
412
|
```
|
|
389
413
|
|
|
390
414
|
Mado trouve le lien via `event.composedPath()`, donc `data-link` fonctionne aussi à l'intérieur
|
|
@@ -400,7 +424,10 @@ entre les pages.
|
|
|
400
424
|
|
|
401
425
|
```ts
|
|
402
426
|
// ❌ Pas de nettoyage du lifecycle, générera un avertissement dev
|
|
403
|
-
const tickets = resource(
|
|
427
|
+
const tickets = resource(
|
|
428
|
+
() => "tickets",
|
|
429
|
+
() => api.listTickets(),
|
|
430
|
+
);
|
|
404
431
|
|
|
405
432
|
component("x-tickets", () => {
|
|
406
433
|
return () => html`${() => tickets.data()?.length ?? 0}`;
|
|
@@ -408,7 +435,10 @@ component("x-tickets", () => {
|
|
|
408
435
|
|
|
409
436
|
// ✅ Créer la resource à l'intérieur du setup du composant
|
|
410
437
|
component("x-tickets", () => {
|
|
411
|
-
const tickets = resource(
|
|
438
|
+
const tickets = resource(
|
|
439
|
+
() => "tickets",
|
|
440
|
+
() => api.listTickets(),
|
|
441
|
+
);
|
|
412
442
|
return () => html`${() => tickets.data()?.length ?? 0}`;
|
|
413
443
|
});
|
|
414
444
|
```
|
|
@@ -427,7 +457,7 @@ nettoyés quand le composant se déconnecte.
|
|
|
427
457
|
const view = signal(html`<x-home></x-home>`);
|
|
428
458
|
|
|
429
459
|
// ✅ Pattern normal : un TemplateResult imbriqué peut être retourné depuis un binding enfant
|
|
430
|
-
html`${view}
|
|
460
|
+
html`${view}`;
|
|
431
461
|
```
|
|
432
462
|
|
|
433
463
|
À partir de v0.3, ceci est garanti par des tests de régression : quand un binding enfant est
|
|
@@ -444,16 +474,23 @@ nettoyer manuellement.
|
|
|
444
474
|
|
|
445
475
|
```ts
|
|
446
476
|
// ❌ .page-head est déclaré globalement, mais x-dashboard utilise Shadow DOM par défaut
|
|
447
|
-
component(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
477
|
+
component(
|
|
478
|
+
"x-dashboard",
|
|
479
|
+
() => () => html`
|
|
480
|
+
<header class="page-head">...</header>
|
|
481
|
+
<div class="metric-grid">...</div>
|
|
482
|
+
`,
|
|
483
|
+
);
|
|
451
484
|
|
|
452
485
|
// ✅ Les composants page/layout/admin-shell doivent souvent être Light DOM
|
|
453
|
-
component(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
486
|
+
component(
|
|
487
|
+
"x-dashboard",
|
|
488
|
+
() => () => html`
|
|
489
|
+
<header class="page-head">...</header>
|
|
490
|
+
<div class="metric-grid">...</div>
|
|
491
|
+
`,
|
|
492
|
+
{ shadow: false },
|
|
493
|
+
);
|
|
457
494
|
```
|
|
458
495
|
|
|
459
496
|
Règle : Shadow DOM — pour les widgets feuilles et les layouts basés sur slot, Light DOM — pour
|
|
@@ -464,24 +501,123 @@ Plus de détails : [`09-shadow-vs-light-dom.md`](./09-shadow-vs-light-dom.md).
|
|
|
464
501
|
|
|
465
502
|
---
|
|
466
503
|
|
|
504
|
+
## Piège #20 : `host.getAttribute()` dans render = pas réactif
|
|
505
|
+
|
|
506
|
+
**Symptôme :** l'apparence du composant ne se met pas à jour quand le parent change un attribut.
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
// ❌ host.getAttribute() dans la fonction render est lu une seule fois.
|
|
510
|
+
// Le render ne se relance que quand ses propres signaux changent.
|
|
511
|
+
component("x-badge", ({ host }) => () => {
|
|
512
|
+
const variant = host.getAttribute("variant") ?? "default";
|
|
513
|
+
return html`<span class=${variant}>...</span>`;
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// ✅ Correct : ctx.attr() — retourne un Signal<string> réactif
|
|
517
|
+
component("x-badge", ({ attr }) => {
|
|
518
|
+
const variant = attr("variant", "default");
|
|
519
|
+
return () => html`<span class=${() => `badge-${variant()}`}>...</span>`;
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**Règle :** n'utilisez jamais `host.getAttribute()` ou `host.hasAttribute()` dans la
|
|
524
|
+
fonction render pour des valeurs qui peuvent changer de l'extérieur. Utilisez `ctx.attr()` —
|
|
525
|
+
il retourne un Signal qui se met à jour via `attributeChangedCallback`.
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Piège #21 : `<button>` Shadow DOM ne soumet pas les formulaires
|
|
530
|
+
|
|
531
|
+
**Symptôme :** cliquer sur `<x-button type="submit">` dans un `<form>` ne fait rien.
|
|
532
|
+
|
|
533
|
+
Un `<button>` dans le Shadow DOM ne participe pas à l'algorithme form-owner pour
|
|
534
|
+
`<form>` dans le Light DOM — c'est une limitation de la spécification.
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
// ❌ Le <button type="submit"> interne ne peut pas déclencher le <form> parent
|
|
538
|
+
component("x-button", ({ host }) => {
|
|
539
|
+
return () => html`<button type="submit"><slot></slot></button>`;
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// ✅ Pont via requestSubmit()
|
|
543
|
+
component("x-button", ({ host, attr }) => {
|
|
544
|
+
const disabled = attr("disabled");
|
|
545
|
+
|
|
546
|
+
const handleClick = () => {
|
|
547
|
+
const typeAttr = host.getAttribute("type");
|
|
548
|
+
if (typeAttr === "button" || typeAttr === "reset") return;
|
|
549
|
+
const form = host.closest("form");
|
|
550
|
+
if (form && !host.hasAttribute("disabled")) form.requestSubmit();
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
return () => html`
|
|
554
|
+
<button ?disabled=${() => disabled() !== ""} @click=${handleClick}>
|
|
555
|
+
<slot></slot>
|
|
556
|
+
</button>
|
|
557
|
+
`;
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Plus de détails : [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md).
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Piège #22 : `useForm()` avec des inputs Shadow DOM personnalisés
|
|
566
|
+
|
|
567
|
+
**Symptôme :** `form.onInput` reçoit `undefined` pour name/value de `<x-input>`.
|
|
568
|
+
|
|
569
|
+
Quand un input Shadow DOM dispatche un événement `input`, le navigateur retarget
|
|
570
|
+
`e.target` du `<input>` interne vers le host `<x-input>`. Mais `<x-input>`
|
|
571
|
+
(HTMLElement) n'a pas `.name` ni `.value` — donc `useForm` ne reçoit rien.
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
// ❌ Pas de propriétés proxy — useForm ignore silencieusement les événements
|
|
575
|
+
component("x-input", ({ host, attr }) => {
|
|
576
|
+
const name = attr("name", "");
|
|
577
|
+
return () => html`<input name=${name} />`;
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// ✅ Ajouter des propriétés proxy pour la compatibilité useForm
|
|
581
|
+
component("x-input", ({ host, attr }) => {
|
|
582
|
+
const name = attr("name", "");
|
|
583
|
+
|
|
584
|
+
Object.defineProperty(host, "name", {
|
|
585
|
+
get: () => host.getAttribute("name") ?? "",
|
|
586
|
+
configurable: true,
|
|
587
|
+
});
|
|
588
|
+
Object.defineProperty(host, "value", {
|
|
589
|
+
get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
|
|
590
|
+
configurable: true,
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
return () => html`<input name=${name} />`;
|
|
594
|
+
});
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Plus de détails : [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md).
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
467
601
|
## Aide-mémoire pour l'IA
|
|
468
602
|
|
|
469
|
-
| Si vous voulez faire…
|
|
470
|
-
|
|
471
|
-
| `useState(0)`
|
|
472
|
-
| `useEffect(() => {...}, [a, b])`
|
|
473
|
-
| `useEffect(() => return cleanup, [])` | `ctx.onDispose(cleanup)`
|
|
474
|
-
| `useMemo(() => x, [a])`
|
|
475
|
-
| `useCallback(fn, [])`
|
|
476
|
-
| `useContext(Ctx)`
|
|
477
|
-
| `useQuery(['key'], fn)`
|
|
478
|
-
| `useMutation(fn)`
|
|
479
|
-
| `useRouter().push('/')`
|
|
480
|
-
| `useRouter().query.q`
|
|
481
|
-
| `<input value={v} onChange={...}>`
|
|
482
|
-
| `{items.map(x => ...)}`
|
|
483
|
-
| `useForm({ resolver: zodResolver })`
|
|
484
|
-
| `class extends HTMLElement`
|
|
485
|
-
| `@customElement('x')`
|
|
603
|
+
| Si vous voulez faire… | Correct dans Mado |
|
|
604
|
+
| ------------------------------------- | ------------------------------------------- |
|
|
605
|
+
| `useState(0)` | `signal(0)` |
|
|
606
|
+
| `useEffect(() => {...}, [a, b])` | `effect(() => {...})` (auto-dépendances) |
|
|
607
|
+
| `useEffect(() => return cleanup, [])` | `ctx.onDispose(cleanup)` |
|
|
608
|
+
| `useMemo(() => x, [a])` | `computed(() => x)` |
|
|
609
|
+
| `useCallback(fn, [])` | fonction ordinaire |
|
|
610
|
+
| `useContext(Ctx)` | `inject(host, Ctx)` |
|
|
611
|
+
| `useQuery(['key'], fn)` | `resource(() => 'key', fn)` |
|
|
612
|
+
| `useMutation(fn)` | `mutation(fn, { invalidates: [...] })` |
|
|
613
|
+
| `useRouter().push('/')` | `navigate('/')` |
|
|
614
|
+
| `useRouter().query.q` | `queryParam('q')` |
|
|
615
|
+
| `<input value={v} onChange={...}>` | `<input .value=${v} @input=${...}>` |
|
|
616
|
+
| `{items.map(x => ...)}` | `${() => each(items, x => x.id, x => ...)}` |
|
|
617
|
+
| `useForm({ resolver: zodResolver })` | `useForm({...}, { validate: (v) => ... })` |
|
|
618
|
+
| `class extends HTMLElement` | `component('x-name', setup)` |
|
|
619
|
+
| `@customElement('x')` | `component('x-name', setup)` |
|
|
620
|
+
| `host.getAttribute('x')` dans render | `ctx.attr('x', default)` (réactif) |
|
|
621
|
+
| `jsonFetcher()` avec auth | `apiFetcher()` (attache le Bearer token) |
|
|
486
622
|
|
|
487
623
|
Si quelque chose ne rentre pas dans cette liste — ouvrez `src/` et **lisez 500 lignes**. Sérieusement. Mado est intentionnellement petit pour être lisible.
|