# Atelier 02 — Custom Events & Communication entre Composants

> **Prérequis :** Atelier 01 — tu sais créer des Custom Elements avec leurs callbacks  
> **Durée estimée :** 2 à 3 jours  
> **Ce que tu sauras faire :** Faire communiquer des composants indépendants via le système d'événements natif du DOM

---

## Introduction

Dans une application avec plusieurs composants, la question qui se pose immédiatement est : **comment font-ils pour se parler ?**

L'article que tu as lu résume le pattern en une phrase :

> *"Data down through properties and attributes, events up through the bubbling system."*

Ce pattern a un nom : **flux de données unidirectionnel**. Il est simple, prévisible, et il repose entièrement sur des mécanismes natifs du navigateur — aucune bibliothèque nécessaire.

---

## 1. Rappel : les événements du DOM

Avant de créer des événements personnalisés, rappelons comment les événements natifs fonctionnent.

### Émettre un événement

```js
const btn = document.querySelector('button');
btn.dispatchEvent(new Event('click'));
```

### Écouter un événement

```js
document.addEventListener('click', (event) => {
  console.log('Clic détecté sur :', event.target);
});
```

### La propagation (bubbling)

Par défaut, la plupart des événements natifs **remontent** dans le DOM. Un clic sur un `<button>` est aussi visible sur le `<div>` parent, le `<body>`, et `document`.

```
<body>          ← l'événement arrive ici aussi
  <div>         ← puis ici
    <button>    ← événement déclenché ici
```

C'est ce mécanisme de remontée que tu exploiteras pour la communication entre composants.

---

## 2. `CustomEvent` : événements avec données

`CustomEvent` étend `Event` et permet d'attacher des données arbitraires dans la propriété `detail`.

```js
// Émission avec données
this.dispatchEvent(new CustomEvent('task-selected', {
  detail: {
    taskId: 42,
    titre: 'Apprendre les Custom Events',
  },
  bubbles: true,    // L'événement remonte dans le DOM
  composed: false,  // L'événement NE traverse PAS les Shadow DOM boundaries
}));
```

```js
// Écoute (n'importe où dans les ancêtres)
document.addEventListener('task-selected', (event) => {
  console.log('Tâche sélectionnée :', event.detail.taskId);
});
```

### Les trois options de configuration

| Option | Type | Défaut | Signification |
|---|---|---|---|
| `bubbles` | boolean | `false` | L'événement remonte-t-il vers les parents ? |
| `composed` | boolean | `false` | L'événement traverse-t-il les Shadow DOM boundaries ? |
| `cancelable` | boolean | `false` | Peut-on appeler `preventDefault()` dessus ? |

**Règle pratique :** Mets toujours `bubbles: true` pour les événements de communication entre composants. `composed: true` ne sera nécessaire qu'à partir de l'atelier 3 (Shadow DOM).

---

## 3. Le pattern : Data Down, Events Up

C'est le cœur de cet atelier. Visualise l'architecture ainsi :

```
┌─────────────────────────────────────┐
│          <app-shell>                │
│   Reçoit les événements             │
│   Distribue les données             │
│                                     │
│  ┌──────────────┐ ┌──────────────┐  │
│  │ <filter-bar> │ │ <task-list>  │  │
│  │              │ │              │  │
│  │  [filtre    ]│ │ [tâche 1 ]   │  │
│  │              │ │ [tâche 2 ]   │  │
│  └──────┬───────┘ └──────────────┘  │
│         │ ↑ événements remontent    │
│         │   données descendent ↓   │
└─────────────────────────────────────┘
```

- `<filter-bar>` **émet** un événement quand le filtre change
- `<app-shell>` **écoute** cet événement et **pousse** les nouvelles données vers `<task-list>`
- `<task-list>` **reçoit** les données via une propriété et re-rend

Les composants `<filter-bar>` et `<task-list>` **ne se connaissent pas**. C'est là l'élégance du pattern.

---

## 4. Nommage des événements

**Conventions à respecter :**

- Utilise des noms en **kebab-case** : `task-selected`, `filter-changed`, `form-submitted`
- Sois descriptif : préfère `user-logged-in` à `login`
- Inclus le composant émetteur si l'événement peut être ambigu : `filter-bar:filter-changed`

**Évite :**
- Les noms d'événements natifs (`click`, `change`, `input`) — tu pourrais créer des conflits
- Les noms trop génériques (`update`, `change`)

---

## 5. Délégation d'événements

Quand un composant contient une liste d'éléments, attacher un écouteur sur chaque élément est coûteux. La **délégation** consiste à attacher un seul écouteur sur le parent et à identifier l'élément ciblé via `event.target`.

```js
connectedCallback() {
  this.innerHTML = `
    <ul>
      <li data-id="1">Tâche A</li>
      <li data-id="2">Tâche B</li>
    </ul>
  `;

  // Un seul écouteur sur le composant lui-même
  this.addEventListener('click', (event) => {
    const li = event.target.closest('li[data-id]');
    if (!li) return; // Clic ailleurs que sur un li

    this.dispatchEvent(new CustomEvent('task-clicked', {
      detail: { id: li.dataset.id },
      bubbles: true,
    }));
  });
}
```

`closest()` remonte dans le DOM à partir de `event.target` et retourne le premier ancêtre (ou l'élément lui-même) correspondant au sélecteur.

---

## 6. Nettoyage des écouteurs

Si tu attaches des écouteurs sur `document` ou sur des éléments externes au composant dans `connectedCallback`, **tu dois les retirer dans `disconnectedCallback`**.

```js
connectedCallback() {
  // Stocker la référence pour pouvoir la retirer
  this._onFilterChanged = (e) => this.applyFilters(e.detail);
  document.addEventListener('filter-changed', this._onFilterChanged);
}

disconnectedCallback() {
  document.removeEventListener('filter-changed', this._onFilterChanged);
}
```

Les écouteurs sur `this` (le composant lui-même) sont automatiquement nettoyés quand l'élément est retiré du DOM — tu n'as pas à t'en occuper.

---

## Exercices

### Exercice 2.1 — Premier CustomEvent

**Objectif :** Émettre et recevoir un événement personnalisé entre deux composants.

Crée deux composants :

- `<mood-button>` : un bouton qui émet un événement `mood-changed` avec `detail: { mood: 'happy' | 'sad' }` selon lequel des deux boutons est cliqué
- `<mood-display>` : affiche l'humeur courante reçue via l'événement

**HTML de la page :**
```html
<mood-button></mood-button>
<mood-display></mood-display>
```

La page elle-même (dans `main.js`) joue le rôle de coordinateur :

```js
document.addEventListener('mood-changed', (e) => {
  document.querySelector('mood-display').mood = e.detail.mood;
});
```

**Critère de succès :** Cliquer sur un bouton met à jour l'affichage dans `<mood-display>` sans que les deux composants se référencent mutuellement.

---

> **Indice 1** — Dans `<mood-button>`, génère deux boutons dans `connectedCallback`. Utilise un seul écouteur avec délégation sur `this`, et identifie le bouton cliqué avec `event.target.dataset.mood`.

> **Indice 2** — L'événement doit avoir `bubbles: true` pour remonter jusqu'au `document` où `main.js` l'écoute.

> **Indice 3** — Dans `<mood-display>`, la propriété `mood` est un setter : `set mood(value) { this._mood = value; this.render(); }`. N'oublie pas que le setter peut être appelé avant `connectedCallback`.

---

### Exercice 2.2 — Coordinateur central

**Objectif :** Implémenter le pattern complet "data down, events up" avec un shell coordinateur.

Crée une mini-application avec trois composants et un coordinateur :

**Composants :**
- `<search-input>` : un champ de recherche qui émet `search-changed` avec `detail: { query: string }` à chaque frappe (utilise l'événement `input` du champ)
- `<result-count>` : affiche "X résultats" où X est mis à jour via une propriété `count`
- `<result-list>` : affiche une liste filtrée via une propriété `items`

**Données initiales (dans `main.js`) :**
```js
const ITEMS = [
  'Apprendre les Custom Elements',
  'Maîtriser le Shadow DOM',
  'Comprendre les Custom Events',
  'Déployer sur GitLab Pages',
  'Construire un vrai outil',
];
```

**Coordinateur dans `main.js` :**
```js
document.addEventListener('search-changed', (e) => {
  const filtered = ITEMS.filter(item =>
    item.toLowerCase().includes(e.detail.query.toLowerCase())
  );
  document.querySelector('result-list').items = filtered;
  document.querySelector('result-count').count = filtered.length;
});
```

**Critère de succès :** Taper dans le champ filtre la liste en temps réel. Les composants ne se connaissent pas.

---

> **Indice 1** — Dans `<search-input>`, écoute l'événement `input` natif sur le `<input>` que tu rends : `this.querySelector('input').addEventListener('input', ...)`. Mais attention : si tu re-rends le composant, l'écouteur est perdu.

> **Indice 2** — Pour éviter le problème de l'indice 1 : attache l'écouteur **directement sur `this`** (le composant) avec délégation, comme dans la section 5. L'événement `input` bulle.

> **Indice 3** — `<result-count>` et `<result-list>` ont besoin de setters. Pense à appeler `render()` si l'élément est déjà dans le DOM au moment du set, et à ne rien faire sinon (le `connectedCallback` appelera `render()` plus tard).

---

### Exercice 2.3 — Communication bidirectionnelle

**Objectif :** Implémenter un scénario où des données descendent ET remontent.

Crée un composant `<editable-item>` qui :
- Reçoit un `label` via attribut — c'est son état initial
- Affiche le label avec un bouton "Modifier"
- Au clic "Modifier", affiche un `<input>` pré-rempli avec le label actuel et un bouton "Sauvegarder"
- Au clic "Sauvegarder", émet un événement `item-updated` avec `detail: { oldLabel, newLabel }` et revient à l'affichage normal

**HTML de la page :**
```html
<editable-item label="Apprendre les Web Components"></editable-item>
<editable-item label="Déployer sur GitLab Pages"></editable-item>

<div id="log"></div>
```

**Coordinateur dans `main.js` :**
```js
document.addEventListener('item-updated', (e) => {
  document.getElementById('log').innerHTML +=
    `<p>Modifié : "${e.detail.oldLabel}" → "${e.detail.newLabel}"</p>`;
});
```

**Critère de succès :** Plusieurs `<editable-item>` sur la même page fonctionnent indépendamment. Les modifications sont loggées.

---

> **Indice 1** — Le composant a deux états internes : `editing` (boolean) et `currentLabel` (string). Utilise `this._editing` et `this._label`.

> **Indice 2** — Dans `connectedCallback`, initialise `this._label = this.getAttribute('label')`. À partir de là, `_label` est la source de vérité, pas l'attribut.

> **Indice 3** — Deux modes de rendu : `renderView()` (label + bouton Modifier) et `renderEdit()` (input + bouton Sauvegarder). Appelle l'un ou l'autre selon `this._editing` dans ta méthode `render()`.

> **Indice 4** — Dans le mode édition, pour lire la valeur de l'input avant de l'émettre dans l'événement : `this.querySelector('input').value`.

---

### Exercice 2.4 — Délégation avancée et `closest()`

**Objectif :** Gérer une liste dynamique d'items avec un seul écouteur.

Crée un composant `<action-list>` qui :
- Expose une propriété `items` (tableau de `{ id, label }`)
- Rend chaque item avec deux boutons : "✓ Fait" et "✗ Supprimer"
- Utilise **un seul** `addEventListener` sur le composant
- Émet `item-done` et `item-deleted` avec `detail: { id }` selon le bouton cliqué

**HTML des items rendu par le composant :**
```html
<li data-id="1">
  Apprendre les Custom Elements
  <button data-action="done">✓</button>
  <button data-action="delete">✗</button>
</li>
```

**Critère de succès :** Avec 10 items dans la liste, il n'y a qu'un seul écouteur attaché. Les événements remontent correctement.

---

> **Indice 1** — Dans l'écouteur unique : `const btn = event.target.closest('button[data-action]')`. Si `btn` est `null`, le clic n'était pas sur un bouton d'action — `return` immédiatement.

> **Indice 2** — Pour trouver l'`id` de l'item parent du bouton : `btn.closest('li[data-id]').dataset.id`.

> **Indice 3** — Pour vérifier qu'il n'y a qu'un seul écouteur : dans les DevTools Chrome, sélectionne le composant dans l'onglet Elements, puis regarde l'onglet "Event Listeners". Tu ne dois voir qu'un seul écouteur `click`.

---

## Checklist de sortie

- [ ] Tu sais créer un `CustomEvent` avec `detail`, `bubbles` et `composed`
- [ ] Tu comprends pourquoi `bubbles: true` est nécessaire pour la communication inter-composants
- [ ] Tu as implémenté le pattern "data down, events up" avec un coordinateur
- [ ] Tu sais utiliser la délégation d'événements avec `closest()`
- [ ] Tu sais nettoyer les écouteurs attachés à l'extérieur du composant
- [ ] Tu comprends la différence entre `composed: false` et `composed: true` (anticipation atelier 3)

---

## Références

| Sujet | URL |
|---|---|
| CustomEvent — MDN | https://developer.mozilla.org/fr/docs/Web/API/CustomEvent |
| Event bubbling — MDN | https://developer.mozilla.org/fr/docs/Learn_web_development/Core/Scripting/Events |
| Element.closest() — MDN | https://developer.mozilla.org/fr/docs/Web/API/Element/closest |
| EventTarget.addEventListener — MDN | https://developer.mozilla.org/fr/docs/Web/API/EventTarget/addEventListener |
| web.dev — Custom Events | https://web.dev/articles/custom-elements-v1#react_to_changes |