Adloun

Communication entre composants

Cours · Vue : construire une application web (école d'ingénieur), chapitre 7 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun

Dans la Librairie du chapitre précédent, un clic sur « Ajouter au panier » fait un long voyage : la carte émet ajouter, le catalogue l'écoute et le réémet, l'application l'écoute enfin et modifie le panier. Le catalogue ne fait rien de cet événement ; il le relaie parce que c'est la seule façon de le faire monter. Dans l'autre sens, le nombre d'articles du panier doit descendre jusqu'à l'en-tête, et demain jusqu'à la page de commande, de prop en prop. Plus l'arbre des composants grandit, plus ces relais s'allongent et rendent le code fragile : déplacer un composant oblige à retoucher tous ses ancêtres.

Les props et les événements restent la base de la communication entre composants. Ce chapitre présente les outils que Vue fournit pour les cas où ils ne suffisent plus, ou deviennent lourds : le v-model d'un composant, l'injection de dépendances (provide et inject), les emplacements nommés et à portée, les composants dynamiques, et la téléportation d'un morceau de gabarit hors de son composant. À la fin, la Librairie aura un panier partagé sans relais, une recherche et un tri réutilisables, et une fenêtre modale accessible qui affiche le contenu du panier.

Objectifs du chapitre

  • Rendre un composant compatible avec v-model, à la main puis avec defineModel, y compris pour plusieurs valeurs.
  • Fournir une donnée à tout un sous-arbre avec provide et la recevoir avec inject, sans perdre la maîtrise de ses modifications.
  • Écrire des composants à emplacements nommés et à portée.
  • Afficher un composant choisi à l'exécution, et garder son état avec <KeepAlive>.
  • Rendre un contenu ailleurs dans le DOM avec <Teleport>, et construire une fenêtre modale accessible.

Les exemples de ce chapitre sont réunis dans le projet Vite demos du chapitre, une page par exemple ; la Librairie est dans le projet librairie. Les sorties ont été produites dans Chromium, en simulant les actions décrites.

7.1 v-model sur un composant

Sur un champ de formulaire, v-model est un raccourci (chapitre 4) qui lie la valeur et écoute la saisie : les deux lignes suivantes sont équivalentes.


<input v-model="texte">
<input :value="texte" @input="texte = $event.target.value">

Sur un composant, le même raccourci existe, avec d'autres noms.

Proposition 7.1v-model sur un composant

<ChampRecherche v-model="recherche" /> équivaut à


<ChampRecherche :model-value="recherche" @update:model-value="(v) => (recherche = v)" />

Le composant reçoit la valeur dans la prop modelValue, et demande sa modification en émettant update:modelValue avec la nouvelle valeur. Le parent reste propriétaire de la donnée : c'est le flux descendant du chapitre 5, écrit en une seule directive.

Voici un champ de recherche qui respecte cette convention, écrit à la main :


<script setup>
// Ce que v-model attend d'un composant, écrit à la main
defineProps({ modelValue: { type: String, default: "" } });
const emit = defineEmits(["update:modelValue"]);
</script>

<template>
  <input
    :value="modelValue"
    type="search"
    aria-label="Recherche (à la main)"
    @input="emit('update:modelValue', $event.target.value)"
  >
</template>

Le champ ne modifie pas la prop : il affiche modelValue et, à chaque saisie, émet la nouvelle valeur. Vue 3.4 a ajouté une macro qui écrit tout cela d'un coup :


<script setup>
// La même chose avec defineModel
const texte = defineModel({ type: String, default: "" });
</script>

<template>
  <input
    v-model="texte"
    type="search"
    aria-label="Recherche (defineModel)"
  >
</template>
Définition 7.2defineModel

const texte = defineModel(options) déclare la prop modelValue (avec les options d'une prop : type, défaut…) et l'événement update:modelValue, et rend une ref : lire texte.value donne la prop, écrire texte.value = … émet l'événement. On peut donc l'utiliser telle quelle avec le v-model d'un champ intérieur.

Les deux composants sont branchés sur la même donnée du parent :


<script setup>
import { ref, watch } from "vue";
import ChampManuel from "./ChampManuel.vue";
import ChampModele from "./ChampModele.vue";

const recherche = ref("");
watch(recherche, (v) => console.log("recherche :", JSON.stringify(v)));
</script>

<template>
  <p><ChampManuel v-model="recherche" /></p>
  <p><ChampModele v-model="recherche" /></p>
  <p>Dans le parent : « {{ recherche }} »</p>
</template>

On tape « Ja » dans le premier champ, puis « va » à la fin du second :


recherche : "J"
recherche : "Ja"
recherche : "Jav"
recherche : "Java"
Dans le parent : « Java »

Les deux champs affichent « Java » : chacun reçoit la valeur du parent, et la modification de l'un, passée par le parent, se voit dans l'autre.

Proposition 7.3Plusieurs v-model

Un composant peut exposer plusieurs valeurs : defineModel("min") déclare la prop min et l'événement update:min, que le parent lie avec v-model:min="prixMin" (exercice 7.3). La fenêtre modale de la Librairie s'ouvre ainsi avec v-model:ouverte.

Une ref de defineModel n'est pas une copie

texte.value = "…" émet l'événement update:modelValue : c'est le parent qui détient la valeur. Si le parent passe la prop sans brancher v-model (ou s'il ignore l'événement), le champ garde la saisie mais la donnée du parent ne change pas : les deux divergent, sans aucun avertissement (vérifié avec Vue 3.5.43). Pour un brouillon local, indépendant du parent, on utilise une ref ordinaire.

Dans la Librairie, la recherche et le tri deviennent deux composants à v-model :


<script setup>
// v-model sur un composant : defineModel() crée la prop modelValue et
// l'événement update:modelValue, et les relie à une ref locale.
const texte = defineModel({ type: String, default: "" });

defineProps({
  id: { type: String, required: true },
  etiquette: { type: String, required: true },
});
</script>

<template>
  <p class="filtre">
    <label :for="id">{{ etiquette }}</label>
    <input
      :id="id"
      v-model="texte"
      type="search"
      autocomplete="off"
    >
  </p>
</template>

<script setup>
const tri = defineModel({ type: String, required: true });

defineProps({
  // [{ valeur: "titre", libelle: "Titre" }, …]
  options: { type: Array, required: true },
});
</script>

<template>
  <p class="tri">
    <label for="tri">Trier par</label>
    <select
      id="tri"
      v-model="tri"
    >
      <option
        v-for="option in options"
        :key="option.valeur"
        :value="option.valeur"
      >
        {{ option.libelle }}
      </option>
    </select>
  </p>
</template>

7.2 Fournir et injecter

Le panier est utilisé par l'en-tête (le compteur), par chaque carte (le bouton d'ajout) et par la fenêtre qui le détaille. Le faire passer par des props et des événements obligerait chaque composant intermédiaire à le transporter. Vue permet à un composant de fournir une valeur à tous ses descendants, à quelque profondeur qu'ils soient.

La suite du cours est sur Adloun

Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.

Lire la suite du cours

En accès libre, sans inscription.

Exercices corrigés de ce chapitre

Lire la suite du cours →