# Atelier 01 — Custom Elements

> **Prérequis :** Atelier 00 complété — classes JS, ES Modules, serveur local opérationnel  
> **Durée estimée :** 2 à 3 jours  
> **Ce que tu sauras faire :** Créer des éléments HTML personnalisés avec leur propre comportement et cycle de vie

---

## Introduction

Un Custom Element, c'est simplement une **classe JavaScript qui hérite de `HTMLElement`**, enregistrée sous un nom HTML de ton choix. Une fois enregistrée, tu peux l'utiliser comme n'importe quelle balise native :

```html
<task-card title="Écrire les tests" status="en-cours"></task-card>
```

La règle des deux mots (avec un tiret) n'est pas arbitraire : elle garantit que tes noms de balises ne rentrent jamais en collision avec les éléments HTML actuels ou futurs définis par le W3C.

---

## 1. Anatomie d'un Custom Element

```js
// components/task-card.js

export class TaskCard extends HTMLElement {

  // Déclare quels attributs doivent déclencher attributeChangedCallback
  static get observedAttributes() {
    return ['title', 'status'];
  }

  constructor() {
    super(); // Toujours en premier
    // Initialisation de l'état interne uniquement
    // Ne pas accéder au DOM ici : l'élément n'est pas encore dans la page
  }

  // Appelé quand l'élément est inséré dans le document
  connectedCallback() {
    this.render();
  }

  // Appelé quand l'élément est retiré du document
  disconnectedCallback() {
    // Nettoyage : retirer les event listeners, annuler les timers...
  }

  // Appelé quand un attribut listé dans observedAttributes change
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render(); // Re-rendre si nécessaire
    }
  }

  render() {
    this.innerHTML = `
      <div class="card card--${this.getAttribute('status') ?? 'default'}">
        <h3>${this.getAttribute('title') ?? 'Sans titre'}</h3>
      </div>
    `;
  }
}

// Enregistrement de la balise
customElements.define('task-card', TaskCard);
```

```html
<!-- index.html -->
<script type="module">
  import './components/task-card.js';
</script>

<task-card title="Lire la doc" status="en-cours"></task-card>
```

---

## 2. Le cycle de vie en détail

Comprendre **quand** chaque callback s'exécute est critique pour éviter des bugs difficiles à diagnostiquer.

### `constructor()`

- S'exécute lors de `new TaskCard()` ou quand le parser HTML rencontre `<task-card>` pour la première fois
- L'élément **n'a pas encore de parent** dans le DOM
- Les attributs peuvent ne pas encore être disponibles (selon le mode d'instanciation)
- **Règle :** n'initialiser que l'état interne (`this.etat = ...`), jamais le DOM

### `connectedCallback()`

- S'exécute quand l'élément **est inséré** dans un document
- C'est ici que tu rends le HTML, attaches les écouteurs d'événements
- Peut s'exécuter plusieurs fois si l'élément est déplacé dans le DOM

### `disconnectedCallback()`

- S'exécute quand l'élément est **retiré** du document
- Indispensable pour nettoyer les `setInterval`, `addEventListener` sur `document` ou `window`, etc.
- Si tu n'as pas de ressources à nettoyer, tu peux l'omettre

### `attributeChangedCallback(name, oldValue, newValue)`

- S'exécute **uniquement** pour les attributs listés dans `observedAttributes`
- `oldValue` vaut `null` au premier appel (quand l'attribut est ajouté)
- **Timing important :** ce callback peut s'exécuter _avant_ `connectedCallback` si les attributs sont définis dans le HTML statique

### Ordre d'exécution pour `<task-card title="X">` dans le HTML :

```
1. constructor()
2. attributeChangedCallback('title', null, 'X')
3. connectedCallback()
```

---

## 3. Attributs vs Propriétés

C'est une distinction que beaucoup négligent au début et qui cause des bugs subtils.

### Attributs (strings dans le HTML)

```js
// Lecture
this.getAttribute('status');    // retourne toujours une string ou null

// Écriture
this.setAttribute('status', 'terminé');

// Présence
this.hasAttribute('disabled');  // booléen
```

Les attributs sont toujours des **chaînes de caractères**. Si tu veux stocker un objet ou un tableau, tu dois passer par les **propriétés**.

### Propriétés (JS sur l'objet)

```js
// Définition avec getter/setter pour synchroniser avec les attributs
get items() {
  return this._items ?? [];
}

set items(value) {
  this._items = value;
  this.render();
}
```

```js
// Utilisation depuis JS
const card = document.querySelector('task-card');
card.items = [{ id: 1, texte: 'Faire X' }]; // passe un objet JS, pas une string
```

### Règle pratique

| Tu veux... | Utilise |
|---|---|
| Configurer depuis le HTML | Attributs |
| Passer des objets / tableaux | Propriétés JS |
| Réagir aux changements externes | `attributeChangedCallback` ou setter |

---

## 4. Sécurité : XSS et `innerHTML`

Injecter directement des valeurs dans `innerHTML` est dangereux si ces valeurs viennent de l'utilisateur.

```js
// DANGEREUX si titre vient d'un input utilisateur
this.innerHTML = `<h3>${titre}</h3>`;
```

Pour du contenu textuel, utilise toujours `textContent` :

```js
const h3 = this.querySelector('h3');
h3.textContent = titre; // échappe automatiquement le HTML
```

Ou pré-nettoie avec :

```js
function escapeHTML(str) {
  const p = document.createElement('p');
  p.textContent = str;
  return p.innerHTML;
}
```

Pour les exercices de cet atelier, les données sont hardcodées — le risque XSS est nul. Mais prends l'habitude de distinguer les deux cas.

---

## 5. Organisation des fichiers

Dans l'approche Vanilla Web, **un composant = un fichier**. Voici la convention que tu suivras dans tous les ateliers :

```
mon-outil/
├── index.html
├── main.js            ← point d'entrée, importe et initialise
└── components/
    ├── task-card.js   ← export class TaskCard
    └── status-badge.js
```

`main.js` se contente d'importer les composants pour déclencher leur enregistrement :

```js
// main.js
import './components/task-card.js';
import './components/status-badge.js';
// Le HTML peut maintenant utiliser <task-card> et <status-badge>
```

---

## Exercices

### Exercice 1.1 — Premier Custom Element

**Objectif :** Créer et utiliser un élément personnalisé minimal.

Crée un élément `<hello-world>` qui affiche `"Bonjour, [nom] !"` où `nom` est passé via un attribut.

**Structure :**
```
exercice-01/
├── index.html
├── main.js
└── components/
    └── hello-world.js
```

**Comportement attendu :**
```html
<hello-world name="Alice"></hello-world>
<!-- Affiche : Bonjour, Alice ! -->

<hello-world></hello-world>
<!-- Affiche : Bonjour, inconnu ! (valeur par défaut) -->
```

**Critère de succès :** Les deux balises s'affichent correctement sans erreur console.

---

> **Indice 1** — La classe doit s'appeler avec un tiret dans le `define` : `customElements.define('hello-world', HelloWorld)`. Le nom de la classe peut être ce que tu veux.

> **Indice 2** — Dans `connectedCallback`, utilise `this.getAttribute('name')` pour lire l'attribut. Si `getAttribute` retourne `null`, l'attribut est absent — utilise l'opérateur `??` pour fournir une valeur par défaut.

> **Indice 3** — N'oublie pas d'importer le composant dans `main.js` et de charger `main.js` dans `index.html` avec `type="module"`.

---

### Exercice 1.2 — Réactivité aux attributs

**Objectif :** Comprendre le rôle de `observedAttributes` et `attributeChangedCallback`.

Reprends `<hello-world>` et rends-le réactif : si l'attribut `name` change en JavaScript, l'affichage doit se mettre à jour automatiquement.

**Test dans la console Chrome :**
```js
document.querySelector('hello-world').setAttribute('name', 'Bob');
// L'affichage doit changer immédiatement sans rechargement
```

**Critère de succès :** La modification de l'attribut depuis la console met à jour l'affichage.

---

> **Indice 1** — Le callback ne se déclenche **que** pour les attributs déclarés dans `observedAttributes`. C'est une méthode statique : `static get observedAttributes() { return ['name']; }`.

> **Indice 2** — Dans `attributeChangedCallback`, appelle `this.render()`. Assure-toi que `render()` est bien une méthode de ta classe.

> **Indice 3** — Vérifie l'ordre d'exécution en ajoutant des `console.log` dans chaque callback. Tu verras peut-être `attributeChangedCallback` avant `connectedCallback` selon comment le composant est initialisé.

---

### Exercice 1.3 — Composant avec état interne

**Objectif :** Distinguer configuration externe (attributs) et état interne (propriétés).

Crée un composant `<click-counter>` :
- Affiche un bouton avec un libellé passé en attribut (`label`)
- Affiche le nombre de clics, initialisé à `0`
- Chaque clic incrémente le compteur et met à jour l'affichage

**Structure HTML attendue (rendue par le composant) :**
```html
<button>Cliques : 3</button>
```

**Critère de succès :** Plusieurs `<click-counter label="Test">` sur la même page ont chacun leur propre compteur indépendant.

---

> **Indice 1** — L'état du compteur (`count`) est une propriété interne : `this._count = 0` dans le `constructor`. Ce n'est pas un attribut — tu n'as pas besoin de `observedAttributes` pour lui.

> **Indice 2** — Dans `connectedCallback`, après avoir rendu le HTML avec `innerHTML`, récupère le bouton avec `this.querySelector('button')` et attache un `addEventListener`.

> **Indice 3** — Si tu appelles `render()` dans `attributeChangedCallback`, attention : `innerHTML` recrée le DOM entier à chaque appel, donc tu perds l'état du compteur. Réfléchis : faut-il tout re-rendre, ou seulement mettre à jour le texte du bouton ?

> **Indice 4** — Pour mettre à jour seulement le texte sans détruire le DOM : `this.querySelector('button').textContent = ...` plutôt que de tout réinitialiser.

---

### Exercice 1.4 — Composant de liste

**Objectif :** Travailler avec des propriétés complexes (tableau) et comprendre la différence attribut/propriété.

Crée un composant `<task-list>` qui :
- Expose une **propriété JS** `tasks` (un tableau d'objets)
- Rend une `<ul>` avec une `<li>` par tâche
- Se met à jour automatiquement quand `tasks` est modifié depuis l'extérieur

**Utilisation depuis `main.js` :**
```js
import './components/task-list.js';

const liste = document.querySelector('task-list');
liste.tasks = [
  { id: 1, titre: 'Apprendre les Custom Elements' },
  { id: 2, titre: 'Pratiquer le Shadow DOM' },
];
```

**Critère de succès :** Assigner un nouveau tableau à `liste.tasks` met à jour l'affichage.

---

> **Indice 1** — Un tableau ne peut pas être passé comme attribut HTML (les attributs sont des strings). La solution est un **setter** JS : `set tasks(value) { this._tasks = value; this.render(); }`.

> **Indice 2** — Dans `render()`, génère le HTML de la liste avec `this._tasks.map(t => \`<li>${t.titre}</li>\`).join('')`.

> **Indice 3** — Le setter est appelé **après** `connectedCallback` dans ce cas. Assure-toi que `render()` fonctionne même si `_tasks` est vide ou undefined — utilise `this._tasks ?? []`.

---

### Exercice 1.5 — Cycle de vie complet

**Objectif :** Observer et comprendre le cycle de vie complet en pratique.

Crée un composant `<auto-clock>` qui :
- Affiche l'heure courante, mise à jour chaque seconde via `setInterval`
- Démarre le timer dans `connectedCallback`
- **Arrête** le timer dans `disconnectedCallback`

**Test manuel :**
```js
// Dans la console Chrome :
const clock = document.querySelector('auto-clock');
clock.remove(); // disconnectedCallback doit arrêter le timer
document.body.appendChild(clock); // connectedCallback redémarre le timer
```

**Critère de succès :** Après `remove()`, les logs cessent (pas de fuite mémoire). Après `appendChild`, les logs reprennent.

---

> **Indice 1** — Stocke la référence du timer : `this._timer = setInterval(...)`. Sans stocker la référence, tu ne peux pas appeler `clearInterval`.

> **Indice 2** — `new Date().toLocaleTimeString()` retourne l'heure locale sous forme lisible.

> **Indice 3** — Ajoute un `console.log('Timer démarré')` dans `connectedCallback` et `console.log('Timer arrêté')` dans `disconnectedCallback` pour valider le comportement lors du `remove()` / `appendChild`.

---

## Checklist de sortie

- [ ] Tu sais créer et enregistrer un Custom Element
- [ ] Tu comprends l'ordre d'exécution des callbacks du cycle de vie
- [ ] Tu distingues attributs (strings, HTML) et propriétés (JS, objets)
- [ ] Tu sais quand utiliser `observedAttributes` et `attributeChangedCallback`
- [ ] Tu sais comment éviter les fuites mémoire avec `disconnectedCallback`
- [ ] Tu comprends pourquoi le nom de la balise doit contenir un tiret

---

## Références

| Sujet | URL |
|---|---|
| Custom Elements — MDN | https://developer.mozilla.org/fr/docs/Web/API/Web_components/Using_custom_elements |
| Spec HTML — Custom Elements | https://html.spec.whatwg.org/multipage/custom-elements.html |
| web.dev — Custom Elements v1 | https://web.dev/articles/custom-elements-v1 |
| javascript.info — Classes | https://fr.javascript.info/class |