# Atelier 04 — `<template>`, Slots Avancés & Patterns de Rendu

> **Prérequis :** Atelier 03 — Shadow DOM et encapsulation maîtrisés  
> **Durée estimée :** 1 à 2 jours  
> **Ce que tu sauras faire :** Optimiser le rendu avec l'élément `<template>`, gérer des mises à jour ciblées du DOM, et choisir la bonne stratégie de rendu

---

## Introduction

Jusqu'ici, tu rendais tes composants en assignant à `innerHTML`. C'est simple et ça fonctionne. Mais à mesure que tes composants grandissent, deux problèmes apparaissent :

1. **Performance** : parser et sérialiser du HTML à chaque render est coûteux quand ça arrive souvent
2. **Pertes d'état** : `innerHTML = ...` recrée tout le DOM — un `<input>` perd son focus, une animation repart de zéro

Cet atelier couvre deux techniques qui adressent ces problèmes.

---

## 1. L'élément `<template>`

L'élément `<template>` est du HTML inerte : le navigateur le parse **une seule fois** au chargement de la page, mais **n'affiche pas** son contenu et **n'exécute pas** ses scripts.

```html
<!-- index.html -->
<template id="task-card-tmpl">
  <style>
    .card { padding: 1rem; border: 1px solid #ddd; }
    h3 { margin: 0; }
  </style>
  <div class="card">
    <h3 class="card__title"></h3>
    <p class="card__desc"></p>
  </div>
</template>
```

```js
// components/task-card.js
class TaskCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    // Cloner le template — ça ne le retire PAS du document
    const tmpl = document.getElementById('task-card-tmpl');
    this.shadowRoot.appendChild(tmpl.content.cloneNode(true));

    // Maintenant mettre à jour les données sans parser de HTML
    this.shadowRoot.querySelector('.card__title').textContent =
      this.getAttribute('title') ?? '';
  }
}
```

### Template vs `innerHTML`

| Critère | `innerHTML` | `<template>` |
|---|---|---|
| Parsing | À chaque instanciation | Une seule fois au chargement |
| Contenu inerte | Non | Oui (pas de rendu, pas d'exécution) |
| Clonage | N/A | `content.cloneNode(true)` |
| Localisation | Dans le JS | Dans le HTML |
| Lisibilité | Chaînes de caractères | HTML natif |

### Template via `document.createElement`

Tu peux aussi créer un template entièrement en JS (utile pour les composants en fichiers séparés sans HTML associé) :

```js
// Créer le template une seule fois au niveau du module
const template = document.createElement('template');
template.innerHTML = `
  <style>
    :host { display: block; }
    .card { padding: 1rem; }
  </style>
  <div class="card">
    <h3 class="title"></h3>
    <slot></slot>
  </div>
`;

class TaskCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // Cloner et attacher une seule fois dans le constructeur
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }

  connectedCallback() {
    // Mettre à jour les données sans tout re-parser
    this.shadowRoot.querySelector('.title').textContent =
      this.getAttribute('title') ?? '';
  }
}
```

C'est la pattern la plus courante dans les composants Vanilla Web en fichiers séparés : **template au scope du module, clone dans le constructeur, mises à jour ciblées dans les callbacks**.

---

## 2. Mises à jour ciblées du DOM

Au lieu de tout re-rendre, mets à jour uniquement ce qui change.

### Stocker des références

```js
class TaskCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(template.content.cloneNode(true));

    // Stocker les références une seule fois
    this._titleEl = this.shadowRoot.querySelector('.title');
    this._statusEl = this.shadowRoot.querySelector('.status');
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue === newValue) return;

    // Mise à jour ciblée — pas de re-render complet
    if (name === 'title') this._titleEl.textContent = newValue ?? '';
    if (name === 'status') this._statusEl.textContent = newValue ?? '';
  }
}
```

### Quand re-rendre tout vs. mise à jour ciblée

| Situation | Approche |
|---|---|
| Structure du DOM change (ajout/suppression d'éléments) | Re-render complet |
| Seules des valeurs changent (textes, attributs) | Mise à jour ciblée |
| Composant simple, peu d'instances | `innerHTML` suffit |
| Composant listé (50+ instances) | Template + mises à jour ciblées |
| `<input>` avec focus ou animation en cours | **Toujours** mise à jour ciblée |

---

## 3. Slots avancés

### Écouter les changements de contenu slotté

```js
connectedCallback() {
  this.shadowRoot.appendChild(template.content.cloneNode(true));

  // L'événement 'slotchange' se déclenche quand le contenu slotté change
  const slot = this.shadowRoot.querySelector('slot');
  slot.addEventListener('slotchange', () => {
    const nodes = slot.assignedNodes({ flatten: true });
    console.log('Contenu du slot mis à jour :', nodes);
  });
}
```

### Accéder aux éléments slottés

```js
// Depuis l'intérieur du composant :
const slot = this.shadowRoot.querySelector('slot[name="title"]');
const assignedElements = slot.assignedElements(); // HTMLElement[] dans ce slot
```

### Contenu de repli (fallback)

```html
<template id="card-tmpl">
  <slot name="title">
    <!-- Contenu affiché si rien n'est fourni dans slot="title" -->
    <span class="untitled">Sans titre</span>
  </slot>
</template>
```

---

## 4. `::slotted()` — styler le contenu slotté

Le CSS du shadow root ne peut pas cibler directement le contenu slotté (il est dans le Light DOM). Le pseudo-élément `::slotted()` est l'exception :

```css
/* Dans le shadow root */
::slotted(p) {
  margin: 0;
  color: #666;
}

::slotted([slot="title"]) {
  font-size: 1.2rem;
  font-weight: bold;
}

/* Limitation : pas de descendants — ::slotted(p strong) ne fonctionne pas */
```

---

## 5. Choisir sa stratégie de rendu

Voici un guide de décision pour tes outils :

```
Composant créé une seule fois sur la page ?
  └─ Oui → innerHTML dans connectedCallback suffit largement
  └─ Non → combien d'instances ?
       └─ < 20 → innerHTML OK, template si tu préfères la lisibilité
       └─ > 20 → template au scope du module + mises à jour ciblées

Le composant a-t-il des inputs / animations internes ?
  └─ Oui → évite tout re-render, mets à jour les nœuds ciblés
  └─ Non → tu peux te permettre innerHTML avec parcimonie
```

---

## Exercices

### Exercice 4.1 — Template au scope du module

**Objectif :** Implémenter le pattern template-module sur un composant réel.

Crée un composant `<user-badge>` (badge d'utilisateur : avatar + nom + rôle) en utilisant :
- Un `document.createElement('template')` au scope du module (pas dans `index.html`)
- Un clone dans le `constructor`
- Des mises à jour ciblées dans `attributeChangedCallback` pour `name` et `role`

**HTML d'utilisation :**
```html
<user-badge name="Alice Martin" role="Admin"></user-badge>
<user-badge name="Bob Dupont" role="Utilisateur"></user-badge>
```

**Critère de succès :** 50 instances sur la page ne causent qu'un seul parsing du template (vérifie en ajoutant un `console.log` dans le template HTML — il ne doit jamais s'exécuter).

---

> **Indice 1** — `template.innerHTML = \`...\`` est le seul moment où le HTML est parsé. Place un `console.log('PARSE')` dans une balise `<script>` dans le template — il ne s'exécutera jamais car le contenu est inerte. C'est un bon moyen de vérifier.

> **Indice 2** — Dans le `constructor`, après `attachShadow` : `this.shadowRoot.appendChild(template.content.cloneNode(true))`. Ensuite, dans le constructor toujours, sauvegarde les références : `this._nameEl = this.shadowRoot.querySelector('.name')`.

> **Indice 3** — `attributeChangedCallback` peut être appelé avant `connectedCallback`. Si tu stockes les références dans le `constructor`, elles sont disponibles dès le premier callback. C'est l'avantage de cette organisation.

---

### Exercice 4.2 — Mise à jour ciblée vs. re-render

**Objectif :** Mesurer et comprendre les conséquences d'un re-render complet.

Crée un composant `<live-input>` qui contient :
- Un `<input type="text">` dont la valeur est modifiable par l'utilisateur
- Un compteur en-dessous (attribut `count`) mis à jour depuis l'extérieur

**Scénario de test :**
1. L'utilisateur tape quelque chose dans l'input
2. Le code extérieur change `count` via `setAttribute`

**Implémente les deux versions :**

**Version A :** `attributeChangedCallback` fait `this.shadowRoot.innerHTML = ...` (re-render complet)  
**Version B :** `attributeChangedCallback` met à jour uniquement `this._countEl.textContent`

**Critère de succès :** En version A, observer que la valeur tapée dans l'input est perdue à chaque changement du `count`. En version B, elle est préservée. Documenter ce que tu observes dans un commentaire dans le code.

---

> **Indice 1** — Pour déclencher le changement de `count` depuis l'extérieur après que l'utilisateur a tapé : `setInterval(() => el.setAttribute('count', ++n), 1000)` dans `main.js`.

> **Indice 2** — La perte de focus (input vide quand on re-rend) est visible. Mais il y a aussi une perte plus subtile : la position du curseur dans l'input est réinitialisée. Teste les deux.

> **Indice 3** — La version B nécessite que la référence `this._countEl` soit établie avant que `attributeChangedCallback` ne soit appelé. Établis-la dans le constructor, pas dans `connectedCallback`.

---

### Exercice 4.3 — `slotchange` et composition dynamique

**Objectif :** Réagir aux changements de contenu slotté.

Crée un composant `<tab-container>` qui :
- Contient un slot par défaut
- Détecte automatiquement les éléments enfants avec `data-tab-label`
- Génère un onglet par élément enfant dans une barre de navigation dans le shadow root
- Affiche/masque les panneaux selon l'onglet actif

**HTML d'utilisation :**
```html
<tab-container>
  <section data-tab-label="Infos">Contenu info...</section>
  <section data-tab-label="Paramètres">Contenu paramètres...</section>
  <section data-tab-label="Aide">Contenu aide...</section>
</tab-container>
```

**Comportement :** Les onglets sont générés automatiquement. Ajouter une `<section>` dynamiquement en JS doit créer un nouvel onglet.

---

> **Indice 1** — Dans `connectedCallback`, écoute `slotchange` sur le slot. Quand il se déclenche, utilise `slot.assignedElements()` pour obtenir les éléments et regénère la barre d'onglets.

> **Indice 2** — Pour afficher/masquer les panneaux : ne les déplace pas. Ils restent dans le Light DOM. Utilise `style.display` sur les éléments directement : `assignedElements().forEach(el => el.style.display = el === activeEl ? '' : 'none')`.

> **Indice 3** — `slotchange` se déclenche lors du premier `connectedCallback` aussi. Tu peux donc initialiser les onglets uniquement dans ce handler plutôt que d'avoir une logique séparée.

---

## Checklist de sortie

- [ ] Tu comprends la différence entre un `<template>` et du HTML rendu
- [ ] Tu sais créer un template au scope du module (sans `index.html`)
- [ ] Tu sais stocker des références DOM et faire des mises à jour ciblées
- [ ] Tu sais choisir entre re-render et mise à jour ciblée selon le contexte
- [ ] Tu comprends le rôle de `slotchange` et `assignedElements()`
- [ ] Tu sais utiliser `::slotted()` pour styler le contenu projeté

---

## Références

| Sujet | URL |
|---|---|
| `<template>` — MDN | https://developer.mozilla.org/fr/docs/Web/HTML/Element/template |
| Templates et Slots — MDN | https://developer.mozilla.org/fr/docs/Web/API/Web_components/Using_templates_and_slots |
| `HTMLSlotElement.assignedElements` — MDN | https://developer.mozilla.org/fr/docs/Web/API/HTMLSlotElement/assignedElements |
| `::slotted()` — MDN | https://developer.mozilla.org/fr/docs/Web/CSS/::slotted |
| web.dev — Slots | https://web.dev/articles/shadowdom-v1#slots__composing_the_shadow_dom |