@madojs/mado 0.6.1 → 0.7.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 +98 -3
- package/dist/src/component.d.ts +17 -4
- package/dist/src/component.js +26 -4
- package/dist/src/component.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
|
@@ -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.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Shadow DOM + formulaires
|
|
2
|
+
|
|
3
|
+
L'utilisation de `useForm()` avec des composants input personnalisés en Shadow DOM
|
|
4
|
+
nécessite de connaître deux comportements au niveau du navigateur :
|
|
5
|
+
|
|
6
|
+
1. **Retargeting des événements** — les événements qui remontent du Shadow DOM ont
|
|
7
|
+
leur `e.target` redirigé vers l'élément host. `useForm().onInput` lit
|
|
8
|
+
`e.target.name` et `e.target.value`, mais un élément host `<x-input>`
|
|
9
|
+
ne possède pas nativement ces propriétés.
|
|
10
|
+
|
|
11
|
+
2. **Association de formulaire** — un `<button type="submit">` à l'intérieur d'un
|
|
12
|
+
Shadow Root ne fait PAS partie de l'algorithme form-owner pour `<form>` dans
|
|
13
|
+
le Light DOM. Cliquer dessus ne déclenche pas le submit du formulaire.
|
|
14
|
+
|
|
15
|
+
Ces deux limitations sont au niveau de la spécification, pas des bugs Mado. Mais
|
|
16
|
+
le framework fournit des patterns qui les rendent indolores.
|
|
17
|
+
|
|
18
|
+
## Pattern : Propriétés proxy sur les composants input
|
|
19
|
+
|
|
20
|
+
En encapsulant un `<input>` dans un composant Shadow DOM, exposez `name` et
|
|
21
|
+
`value` comme propriétés DOM sur le host pour que `useForm().onInput` fonctionne
|
|
22
|
+
après le 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
|
+
// Propriétés proxy pour la compatibilité useForm().
|
|
33
|
+
// Après le retargeting Shadow DOM de e.target : <input> → <x-input>,
|
|
34
|
+
// useForm lit e.target.name / e.target.value — ces getters font le pont.
|
|
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
|
+
L'événement `input` du `<input>` interne a `composed: true` par défaut, donc
|
|
53
|
+
il remonte à travers la frontière shadow. Après le retargeting, `e.target` est
|
|
54
|
+
`<x-input>`, mais maintenant il a les getters `.name` et `.value` → `useForm`
|
|
55
|
+
fonctionne.
|
|
56
|
+
|
|
57
|
+
## Pattern : Submit de formulaire depuis des boutons Shadow DOM
|
|
58
|
+
|
|
59
|
+
Un `<button type="submit">` dans le Shadow DOM ne peut pas déclencher le submit
|
|
60
|
+
d'un `<form>` dans le Light DOM. Pont via `requestSubmit()` :
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { component, css, html } from "@madojs/mado";
|
|
64
|
+
|
|
65
|
+
component("x-button", ({ host, attr }) => {
|
|
66
|
+
const disabled = attr("disabled");
|
|
67
|
+
|
|
68
|
+
const handleClick = () => {
|
|
69
|
+
const typeAttr = host.getAttribute("type");
|
|
70
|
+
if (typeAttr === "button" || typeAttr === "reset") return;
|
|
71
|
+
const form = host.closest("form");
|
|
72
|
+
if (form && !host.hasAttribute("disabled")) form.requestSubmit();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return () => html`
|
|
76
|
+
<button ?disabled=${() => disabled() !== ""} @click=${handleClick}>
|
|
77
|
+
<slot></slot>
|
|
78
|
+
</button>
|
|
79
|
+
`;
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`host.closest("form")` fonctionne parce que l'élément host lui-même vit dans
|
|
84
|
+
le Light DOM (seuls ses éléments internes sont dans l'ombre). `requestSubmit()`
|
|
85
|
+
déclenche la validation et l'événement `submit` exactement comme si l'utilisateur
|
|
86
|
+
avait cliqué sur un bouton submit natif à l'intérieur du formulaire.
|
|
87
|
+
|
|
88
|
+
## Pattern : Attributs réactifs avec ctx.attr()
|
|
89
|
+
|
|
90
|
+
Depuis la v0.7, `ctx.attr(name, defaultValue?)` retourne un `Signal<string>` qui
|
|
91
|
+
se met à jour automatiquement quand l'attribut change sur le host. Plus besoin de
|
|
92
|
+
`MutationObserver` :
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
component("x-badge", ({ attr }) => {
|
|
96
|
+
const variant = attr("variant", "default"); // Signal<string>
|
|
97
|
+
|
|
98
|
+
return () =>
|
|
99
|
+
html`<span class=${() => `badge badge-${variant()}`}>
|
|
100
|
+
<slot></slot>
|
|
101
|
+
</span>`;
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Le parent peut utiliser `?disabled=${() => !form.isValid()}` (attribut booléen)
|
|
106
|
+
ou `.variant=${"danger"}` — le composant se re-rend de manière réactive dans
|
|
107
|
+
les deux cas.
|
|
108
|
+
|
|
109
|
+
## Exemple complet de formulaire
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { page, html, useForm, navigate } from "@madojs/mado";
|
|
113
|
+
import "../components/x-input.js";
|
|
114
|
+
import "../components/x-button.js";
|
|
115
|
+
|
|
116
|
+
export default page({
|
|
117
|
+
title: "Connexion",
|
|
118
|
+
view: () => {
|
|
119
|
+
const form = useForm({
|
|
120
|
+
email: { required: true, type: "email" },
|
|
121
|
+
password: { required: true, minLength: 6 },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const handleLogin = async (values) => {
|
|
125
|
+
await api("/auth/login", { method: "POST", json: values });
|
|
126
|
+
navigate("/admin");
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return html`
|
|
130
|
+
<form @submit=${form.onSubmit(handleLogin)}>
|
|
131
|
+
<x-input
|
|
132
|
+
name="email"
|
|
133
|
+
type="email"
|
|
134
|
+
label="Email"
|
|
135
|
+
required
|
|
136
|
+
@input=${form.onInput}
|
|
137
|
+
@blur=${form.onBlur}
|
|
138
|
+
></x-input>
|
|
139
|
+
${() =>
|
|
140
|
+
form.errors().email
|
|
141
|
+
? html`<small class="err">${form.errors().email}</small>`
|
|
142
|
+
: null}
|
|
143
|
+
|
|
144
|
+
<x-input
|
|
145
|
+
name="password"
|
|
146
|
+
type="password"
|
|
147
|
+
label="Mot de passe"
|
|
148
|
+
required
|
|
149
|
+
@input=${form.onInput}
|
|
150
|
+
@blur=${form.onBlur}
|
|
151
|
+
></x-input>
|
|
152
|
+
|
|
153
|
+
<x-button type="submit" ?disabled=${() => !form.isValid()}>
|
|
154
|
+
Se connecter
|
|
155
|
+
</x-button>
|
|
156
|
+
</form>
|
|
157
|
+
`;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Quand utiliser Light DOM à la place
|
|
163
|
+
|
|
164
|
+
Si votre composant input est juste un wrapper stylisé sans besoin
|
|
165
|
+
d'encapsulation, `shadow: false` évite les deux problèmes (retargeting et
|
|
166
|
+
association de formulaire) :
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
component(
|
|
170
|
+
"x-field",
|
|
171
|
+
({ attr }) => {
|
|
172
|
+
const label = attr("label", "");
|
|
173
|
+
return () => html`
|
|
174
|
+
<label>
|
|
175
|
+
<span>${label}</span>
|
|
176
|
+
<slot></slot>
|
|
177
|
+
</label>
|
|
178
|
+
`;
|
|
179
|
+
},
|
|
180
|
+
{ shadow: false },
|
|
181
|
+
);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Avec Light DOM, le `<input>` natif fait partie de l'arbre du document, les
|
|
185
|
+
événements ne sont pas retargetés, et le submit fonctionne nativement.
|
|
186
|
+
Le compromis : les styles ne sont pas encapsulés (vous devez les scoper
|
|
187
|
+
vous-même).
|
|
188
|
+
|
|
189
|
+
## Résumé
|
|
190
|
+
|
|
191
|
+
| Problème | Solution Shadow DOM | Alternative Light DOM |
|
|
192
|
+
| ------------------------------ | -------------------------------------------- | ------------------------------- |
|
|
193
|
+
| `useForm` + input personnalisé | Proxy `name`/`value` sur le host | `<input>` natif dans un slot |
|
|
194
|
+
| Submit de formulaire | `form.requestSubmit()` dans le click handler | Le bouton natif fonctionne |
|
|
195
|
+
| Attributs réactifs | `ctx.attr()` → signal auto | `ctx.attr()` fonctionne partout |
|
|
196
|
+
| Encapsulation des styles | Oui (automatique) | `@scope` manuel ou BEM |
|
package/docs/fr/README.md
CHANGED
|
@@ -2,22 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Documentation française.
|
|
4
4
|
|
|
5
|
-
| Section
|
|
6
|
-
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
| IDE
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
| LLM
|
|
15
|
-
| LLM
|
|
16
|
-
| Shadow DOM vs Light DOM
|
|
17
|
-
| Architecture d'application
|
|
18
|
-
|
|
|
19
|
-
| Auth et API
|
|
20
|
-
| Déploiement
|
|
21
|
-
| Tests
|
|
22
|
-
| Gestion des erreurs
|
|
23
|
-
|
|
|
5
|
+
| Section | Fichier |
|
|
6
|
+
| ----------------------------- | ------------------------------------------------------------ |
|
|
7
|
+
| La voie Mado | [00-the-mado-way.md](./00-the-mado-way.md) |
|
|
8
|
+
| Routage | [01-routing.md](./01-routing.md) |
|
|
9
|
+
| Structure du projet | [02-project-layout.md](./02-project-layout.md) |
|
|
10
|
+
| Prérendu statique & SEO | [03-static-bake.md](./03-static-bake.md) |
|
|
11
|
+
| Configuration IDE | [04-ide-setup.md](./04-ide-setup.md) |
|
|
12
|
+
| Pourquoi Mado | [05-why-mado.md](./05-why-mado.md) |
|
|
13
|
+
| Pour les développeurs backend | [06-for-backenders.md](./06-for-backenders.md) |
|
|
14
|
+
| Pièges LLM | [07-llm-pitfalls.md](./07-llm-pitfalls.md) |
|
|
15
|
+
| Test LLM sans historique | [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
|
+
| Architecture d'application | [10-app-architecture.md](./10-app-architecture.md) |
|
|
18
|
+
| Mises en page (layouts) | [11-layouts.md](./11-layouts.md) |
|
|
19
|
+
| Auth et API | [12-auth-and-api.md](./12-auth-and-api.md) |
|
|
20
|
+
| Déploiement | [13-deployment.md](./13-deployment.md) |
|
|
21
|
+
| Tests | [14-testing.md](./14-testing.md) |
|
|
22
|
+
| Gestion des erreurs | [15-error-handling.md](./15-error-handling.md) |
|
|
23
|
+
| Guide de recettes bake | [16-bake-cookbook.md](./16-bake-cookbook.md) |
|
|
24
|
+
| Shadow DOM + formulaires | [17-shadow-dom-forms.md](./17-shadow-dom-forms.md) |
|