@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.
@@ -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>${() => items().map(t => html`<li>${t.name}</li>`)}</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>${() => each(items(), t => t.id, t => html`<li>${t.name}</li>`)}</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() // lecture
200
- count.set(5) // écriture
201
- count.update(n => n + 1)
202
- count.peek() // lecture sans abonnement
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); // signal<valeur>
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: { required: true, type: "number", min: 18 },
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("x-admin-page", () => () => html`
292
- <section class="bg-white shadow-lg rounded-lg p-4">
293
- ...
294
- </section>
295
- `, { shadow: false });
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>.panel { padding: 1rem; }</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("x-admin-panel", () => () => html`
362
- <section class="panel">...</section>
363
- `, {
364
- styles: css`
365
- .panel { padding: 1rem; }
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(() => "tickets", () => api.listTickets());
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(() => "tickets", () => api.listTickets());
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("x-dashboard", () => () => html`
448
- <header class="page-head">...</header>
449
- <div class="metric-grid">...</div>
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("x-dashboard", () => () => html`
454
- <header class="page-head">...</header>
455
- <div class="metric-grid">...</div>
456
- `, { shadow: false });
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… | Correct dans Mado |
470
- |---|---|
471
- | `useState(0)` | `signal(0)` |
472
- | `useEffect(() => {...}, [a, b])` | `effect(() => {...})` (auto-dépendances) |
473
- | `useEffect(() => return cleanup, [])` | `ctx.onDispose(cleanup)` |
474
- | `useMemo(() => x, [a])` | `computed(() => x)` |
475
- | `useCallback(fn, [])` | fonction ordinaire |
476
- | `useContext(Ctx)` | `inject(host, Ctx)` |
477
- | `useQuery(['key'], fn)` | `resource(() => 'key', fn)` |
478
- | `useMutation(fn)` | `mutation(fn, { invalidates: [...] })` |
479
- | `useRouter().push('/')` | `navigate('/')` |
480
- | `useRouter().query.q` | `queryParam('q')` |
481
- | `<input value={v} onChange={...}>` | `<input .value=${v} @input=${...}>` |
482
- | `{items.map(x => ...)}` | `${() => each(items, x => x.id, x => ...)}` |
483
- | `useForm({ resolver: zodResolver })` | `useForm({...}, { validate: (v) => ... })` |
484
- | `class extends HTMLElement` | `component('x-name', setup)` |
485
- | `@customElement('x')` | `component('x-name', setup)` |
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 | 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
- | Architecture d'application | [10-app-architecture.md](./10-app-architecture.md) |
18
- | 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
- | Bake cookbook | [16-bake-cookbook.md](./16-bake-cookbook.md) |
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) |