Rendu de liste
Introduction
Dans une application, on doit souvent afficher une liste d'éléments (les produits d'un catalogue, les messages d'une boîte de réception, les lignes d'une facture…) La directive v-for permet de parcourir un tableau ou un objet et de répéter une portion de code pour chaque élément. Chaque occurrence est rendue avec ses propres données, ce qui évite de répéter plusieurs fois une même structure HTML.
Syntaxe de base
La directive v-for s'utilise dans le <template> pour répéter un bloc. On l'emploie le plus souvent avec un tableau ou un objet, sous la forme :
- v-for="valeurElement in tableau"
- v-for="valeurPropriete in objet"
En JavaScript classique, le mot-clé in renvoie les clés ou indices tandis que of renvoie directement les valeurs. En Vue, cette distinction disparaît car que l'on utilise in ou of, c'est toujours la valeur de l'élément ou de la propriété qui est exploitée. Dans ce cours, nous emploierons in, conformément à la documentation officielle de Vue.
Exemple : afficher une liste de produits
<script setup>
const produits = [
{ id: 1, nom: 'Casque audio', prix: 79 },
{ id: 2, nom: 'Clavier mécanique', prix: 120 },
{ id: 3, nom: 'Souris sans fil', prix: 45 }
];
</script>
<template>
<ul>
<!-- Rendu de liste sans clé -->
<li v-for="produit in produits">
{{ produit.nom }} : {{ produit.prix }} €
</li>
</ul>
</template>
La directive v-for parcourt ici le tableau produits. Pour chaque élément, Vue génère une balise <li> qui affiche son nom et son prix. On obtient ainsi une liste dynamique, entièrement construite à partir des données du tableau, sans avoir besoin de répéter le HTML à la main.
Identifiant stable
Dans l'exemple d'affichage des produits de la section précédente, tout fonctionne correctement tant que l'ordre du tableau restent identiques.
Lorsque l'on modifie l'ordre du tableau, par exemple en inversant les deux premiers éléments, Vue ne déplace pas les balises <li>, il se contente d'ajuster leur contenu.
Lorsque l'on insère un nouvel élément au début du tableau, Vue adopte la même logique. La première balise existante est réutilisée et son contenu est remplacé par la nouvelle donnée. Les autres balises sont décalées et reçoivent à leur tour le contenu d'un autre élément du tableau. Enfin, une nouvelle balise est ajoutée à la fin pour que le nombre total corresponde à la nouvelle taille du tableau.
Ce mécanisme de réutilisation rend le rendu rapide, mais il peut introduire des incohérences. Un champ de saisie peut conserver la valeur d'un autre produit, un focus clavier peut se déplacer de manière inattendue ou encore l'état interne d'un composant enfant peut se retrouver associé au mauvais élément.
Dans l'exemple ci-dessous, lorsqu'on coche la case du premier produit puis qu'on clique sur le bouton « Inverser l'ordre », l'ordre de la liste change correctement, mais la case cochée reste en haut alors qu'elle ne correspond plus au même élément.
<script setup>
import { ref } from 'vue';
const produits = ref([
{ id: 1, nom: 'Casque audio', prix: 79 },
{ id: 2, nom: 'Clavier mécanique', prix: 120 },
{ id: 3, nom: 'Souris sans fil', prix: 45 }
]);
const inverserOrdre = () =>
{
produits.value = produits.value.reverse();
}
</script>
<template>
<button @click="inverserOrdre">Inverser ordre</button>
<ul>
<!-- Rendu de liste sans clé -->
<li v-for="produit in produits">
<input type="checkbox">
{{ produit.nom }} : {{ produit.prix }} €
</li>
</ul>
</template>
Pour résoudre ce problème, on utilise l'attribut spécial :key. Chaque élément de la boucle reçoit ainsi un identifiant unique et stable, généralement issu des données comme produit.id. Vue peut alors associer correctement chaque nœud à la bonne donnée, créer ou détruire uniquement ce qui change et éviter les confusions.
<script setup>
import { ref } from 'vue';
const produits = ref([
{ id: 1, nom: 'Casque audio', prix: 79 },
{ id: 2, nom: 'Clavier mécanique', prix: 120 },
{ id: 3, nom: 'Souris sans fil', prix: 45 }
]);
const inverserOrdre = () =>
{
produits.value = produits.value.reverse();
}
</script>
<template>
<button @click="inverserOrdre">Inverser ordre</button>
<ul>
<!-- Rendu de liste avec clé unique -->
<li v-for="produit in produits" :key="produit.id">
<input type="checkbox">
{{ produit.nom }} : {{ produit.prix }} €
</li>
</ul>
</template>
Pour garantir une cohérence stable, il est recommandé d'utiliser une clé issue directement des données, par exemple :
- id : Identifiant numérique généralement fourni par la base de données.
- uuid (Universally Unique Identifier) : Identifiant standardisé de 128 bits conçu pour être unique sans autorité centrale. C'est-à-dire sans serveur d'assignation ni registre global.
- slug : Chaîne unique dérivée d'un texte, souvent utilisée dans les URL.
Attention, dans le cas des tableaux, il ne faut jamais utiliser l'index de boucle comme valeur de :key. Cet index dépend uniquement de la position de l'élément dans le tableau et peut donc changer après une mutation comme un ajout, une suppression ou un tri. Vue risque alors de réutiliser les mauvaises balises du DOM, ce qui entraîne des incohérences dans l'affichage et le comportement.
Notez que Les clés (:key) n'ont besoin d'être uniques qu'à l'intérieur d'une même boucle v-for, pas à travers tout le template.
Pour une liste statique qui ne change jamais, l'attribut :key n'est pas strictement obligatoire, car Vue peut rendre les éléments sans risque de confusion. Il reste toutefois conseillé de l'ajouter par défaut, afin de préserver la cohérence si la liste devient interactive (champs, états ouverts, focus, etc.) et d'assurer un comportement prévisible en cas d'évolution ultérieure.
Récupérer l'index ou la clé
La directive v-for ne se limite pas à fournir la valeur de l'élément courant. Elle peut également donner une information complémentaire selon le type de donnée parcouru :
- dans un tableau, on peut récupérer l"index de l'élément (0 pour le premier, 1 pour le deuxième, etc.)
- dans un objet, on peut récupérer la clé, c'est-à-dire le nom de la propriété
Pour cela, on écrit deux variables entre parenthèses dans la syntaxe de la boucle. La première variable reçoit toujours la valeur, et la seconde reçoit l'index ou la clé selon le type de donnée parcouru :
- v-for="(valeurElement, index) in tableau"
- v-for="(valeurPropriete, nomPropriete) in objet"
Voici un exemple avec un objet utilisateur où l'on affiche dynamiquement les informations disponibles :
<script setup>
const utilisateur = {
nom: 'Dupont',
age: 32,
email: 'dupont@example.com'
};
</script>
<template>
<ul>
<li v-for="(valeur, cle) in utilisateur" :key="cle">
{{ cle }} : {{ valeur }}
</li>
</ul>
</template>
Dans cet exemple, la variable cle prend successivement les valeurs nom, age et email, tandis que valeur contient la donnée correspondante. Cela permet d'afficher facilement toutes les propriétés d'un objet sans devoir les écrire une par une.
Listes imbriquées
Parfois l'affichage implique plusieurs niveaux de données. On veut lister des éléments puis, pour chacun, lister leurs sous-éléments. Vue permet cela avec des v-for imbriqués. Chaque v-for gère son propre niveau de données et ses clés. Limite la profondeur si possible pour garder le code lisible.
Prenons l'exemple d'un tableau d'utilisateurs où chaque utilisateur est représenté par un objet contenant des informations comme son nom, son âge et son email. On peut d'abord parcourir la liste des utilisateurs, puis afficher pour chacun d'eux la liste de ses propriétés.
<script setup>
const utilisateurs = [
{ id: 1, nom: 'Dupont', age: 32, email: 'dupont@example.com' },
{ id: 2, nom: 'Martin', age: 27, email: 'martin@example.com' }
];
</script>
<template>
<ul>
<!-- Niveau 1 : chaque utilisateur -->
<li v-for="utilisateur in utilisateurs" :key="utilisateur.id">
<b>Utilisateur #{{ utilisateur.id }}</b>
<!-- Niveau 2 : propriétés de l'utilisateur -->
<ul>
<li v-for="(valeur, cle) in utilisateur" :key="cle">
{{ cle }} {{ valeur }}
</li>
</ul>
</li>
</ul>
</template>
Dans la liste principale, on parcourt le tableau d'utilisateurs. Chaque élément rendu doit recevoir une clé stable et unique parmi ses voisins. La candidate naturelle est l'identifiant :key="utilisateur.id".
Dans la sous-liste, on parcourt les propriétés de l'utilisateur courant. La balise qui porte v-for définit le groupe où les clés doivent être uniques. Ici, :key="cle" suffit, car l'objet utilisateur expose toujours les mêmes champs (id, nom, age, email). Pour l'utilisateur suivant, un autre groupe est généré, on peut réutiliser les mêmes clés sans conflit.
Si besoin d'expliciter l'appartenance, compose une clé à partir du parent et du nom de propriété :key="`${utilisateur.id}-${cle}`". (Éviter :key="valeur" si la valeur peut changer).
Combiner v-if et v-for
Il arrive de vouloir filtrer certains éléments directement dans une boucle v-for. Cependant, placer v-if sur la même balise provoque une erreur lorsque la condition fait référence à la variable d'itération, car cette variable n'existe pas encore au moment où la condition est évaluée.
Pour éviter ce problème, il est préférable de placer v-for sur un <template> et v-if sur l'élément enfant. Ces solutions garantissent un rendu stable et un code plus clair.
L'exemple suivant affiche une liste d'utilisateurs en ne montrant que ceux qui sont actifs dans les données. Les utilisateurs inactifs ne figurent donc pas dans le rendu final.
<script setup>
const utilisateurs = [
{ id: 1, nom: 'Dupont', actif: true },
{ id: 2, nom: 'Martin', actif: false },
{ id: 3, nom: 'Durand', actif: true }
];
</script>
<template>
<ul>
<template v-for="u in utilisateurs">
<li v-if="u.actif" :key="u.id">
{{ u.nom }}
</li>
</template>
</ul>
</template>
Ici, la boucle v-for est portée par une balise <template>, et la condition v-if est appliquée sur la balise <li>. Chaque itération crée un nouvel élément, mais seule celle qui remplit la condition (u.actif) est rendue. Cette structure évite tout conflit entre les deux directives.
Notez qu'il est souvent préférable de filtrer les données en amont à l'aide d'une propriété calculée (dans le bloc <script>), surtout lorsque le résultat du filtrage doit être réutilisé ailleurs dans le composant. Nous y reviendrons plus en détail dans un chapitre dédié.
Interactions dans une boucle v-for
Avec v-for, on peut afficher aussi bien des valeurs statiques que des données dynamiques. Dans ce dernier cas, toute modification met automatiquement l'affichage à jour. Il est également possible d'associer des écouteurs d'événements à chaque élément généré pour réagir aux actions de l'utilisateur.
Exemple simple avec passage de l'identifiant
<script setup>
import { ref } from 'vue';
const produits = ref([
{ id: 1, nom: 'Casque audio', prix: 79 },
{ id: 2, nom: 'Clavier mécanique', prix: 120 },
{ id: 3, nom: 'Souris sans fil', prix: 45 }
]);
const retirer = (id) =>
{
// Filtrer les données existantes en excluant l'élément
// dont l'identifiant stable correspond à celui passé en argument.
produits.value = produits.value.filter(p => p.id !== id);
};
</script>
<template>
<ul>
<li v-for="produit in produits" :key="produit.id">
{{ produit.nom }} : {{ produit.prix }} €
<button type="button" @click="retirer(produit.id)">Retirer</button>
</li>
</ul>
</template>
La fonction retirer reçoit l"identifiant stable du produit (produit.id). Le rendu reste cohérent même si l'ordre change ou si l'on supprime des éléments.
Récupérer les éléments du DOM générés par v-for
Les refs de template servent à récupérer une référence vers un élément du DOM ou une instance de composant. Lorsqu'une même ref est utilisée dans une boucle v-for, le comportement est légèrement différent de celui d'une ref utilisée sur un élément unique.
Ref sur des éléments répétés : tableau de références
Lorsque la même ref est utilisée sur un élément répété avec v-for, Vue ne stocke pas une seule référence mais un tableau de références. Chaque entrée de ce tableau correspond à l'un des éléments du DOM générés.
<script setup>
import { ref } from 'vue';
const produits = ref([
{ id: 1, nom: 'Casque audio', prix: 79 },
{ id: 2, nom: 'Clavier mécanique', prix: 120 },
{ id: 3, nom: 'Souris sans fil', prix: 45 }
]);
// Contiendra les références vers les éléments <li> générés.
const lignesProduit = ref([]);
const retirerDernierProduit = () =>
{
produits.value.pop();
};
const afficherReferences = () =>
{
console.log('Nombre de produits', produits.value.length);
console.log('Nombre de références DOM', lignesProduit.value.length);
};
</script>
<template>
<button
type="button"
@click="afficherReferences"
>
Afficher résultat dans la console
</button>
<button
type="button"
@click="retirerDernierProduit"
:disabled="produits.length === 0"
>
Retirer le dernier produit
</button>
<ul>
<li
v-for="produit in produits"
:key="produit.id"
ref="lignesProduit"
>
{{ produit.nom }} - {{ produit.prix }} €
</li>
</ul>
</template>
Au moment où l'utilisateur clique sur le bouton, les éléments <li> ont déjà été générés dans le DOM. La propriété lignesProduit.value contient donc un tableau de références vers ces éléments.
Si la liste affichée change, Vue met également à jour les références afin qu'elles correspondent aux éléments actuellement présents dans le DOM.
Attention : il ne faut pas supposer que l'ordre des références dans lignesProduit.value correspond toujours à l'ordre des données dans produits. Si l'on doit retrouver précisément l'élément associé à une donnée particulière, il est préférable d'utiliser une ref fonction.
Ref fonctionnelle avec v-for : une fonction appelée pour chaque élément
Lorsque l'on utilise une fonction avec l'attribut :ref dans une boucle v-for, Vue appelle cette fonction pour chaque élément généré et lui transmet le nœud DOM correspondant.
Cette technique permet de choisir nous-mêmes comment stocker les références. On peut, par exemple, utiliser un simple objet JavaScript pour associer l'identifiant de chaque produit à son élément du DOM.
<script setup>
import { ref, reactive } from 'vue';
const produits = ref([
{ id: 1, nom: 'Casque audio', prix: 79 },
{ id: 2, nom: 'Clavier mécanique', prix: 120 },
{ id: 3, nom: 'Souris sans fil', prix: 45 }
]);
// Associer chaque identifiant de produit à son élément DOM.
const elementsParId = {};
const enregistrerRefProduit = (el, id) =>
{
if (el !== null)
{
// Enregistrer l'élément DOM sous l'identifiant du produit.
elementsParId[id] = el;
}
else
{
// Supprimer la référence lorsque l'élément disparaît du DOM.
delete elementsParId[id];
}
};
const retirerDernierProduit = () =>
{
produits.value.pop();
};
const afficherReferences = () =>
{
console.log('Nombre de produits', produits.value.length);
// Object.keys() retourne un tableau contenant les noms des propriétés.
console.log('Nombre de références DOM', Object.keys(elementsParId).length);
};
</script>
<template>
<button
type="button"
@click="afficherReferences"
>
Afficher résultat dans la console
</button>
<button
type="button"
@click="retirerDernierProduit"
:disabled="produits.length === 0"
>
Retirer le dernier produit
</button>
<ul>
<li
v-for="produit in produits"
:key="produit.id"
:ref="el => enregistrerRefProduit(el, produit.id)"
>
{{ produit.nom }} - {{ produit.prix }} €
</li>
</ul>
</template>
L'expression suivante fournit à Vue une fonction qui reçoit le nœud DOM dans le paramètre el :
el => enregistrerRefProduit(el, produit.id)
Comme cette fonction est créée à l'intérieur de la boucle v-for, elle a également accès au produit correspondant et donc à son id.
La fonction enregistrerRefProduit() peut ainsi associer chaque identifiant à son élément du DOM :
elementsParId[id] = el;
Après le rendu, l'objet elementsParId contient donc conceptuellement des associations de ce type :
{
1: /* élément <li> du casque */,
2: /* élément <li> du clavier */,
3: /* élément <li> de la souris */
}
Il devient alors possible de retrouver directement l'élément associé à un produit à partir de son identifiant :
const el = elementsParId[3];
Lorsque Vue retire un élément du DOM, la fonction utilisée par :ref est appelée avec null. On peut alors supprimer la référence correspondante de l'objet avec l'opérateur delete :
delete elementsParId[id];
L'objet elementsParId n'a pas besoin d'être créé avec ref(), car ses modifications ne servent pas à mettre à jour l'affichage. Il est simplement utilisé en interne pour retrouver des éléments du DOM.
Cette approche permet donc d'associer directement une donnée à son élément du DOM sans dépendre de sa position dans la liste. Elle est particulièrement utile lorsqu'on veut cibler un élément précis, par exemple pour lui donner le focus ou faire défiler la page jusqu'à lui.
Itérer un nombre de fois
La directive v-for peut aussi recevoir un entier. Dans ce cas, Vue répète le bloc autant de fois que la valeur indiquée, en fournissant à chaque répétition un compteur (n) qui commence à 1. Cette technique est utile dès qu'on souhaite générer un certain nombre d'éléments en fonction d'une valeur numérique, comme par exemple dans un système de notation par étoiles :
<script setup>
const nbEtoilesMax = 5;
const note = 3;
</script>
<template>
<!--
On répète l'étoile 5 fois (de 1 à 5).
Pour chaque position (n), on compare à la note :
- si n est inférieur ou égal à la note alors on affiche une étoile pleine
- sinon on affiche une étoile vide
-->
<span v-for="n in nbEtoilesMax" :key="n">
{{ n <= note ? '★' : '☆' }}
</span>
</template>
Dans cet exemple, Vue génère 5 balises <span>, avec des valeurs de n allant de 1 à 5. La condition n <= note permet de remplir seulement les 3 premières avec une étoile pleine. Les deux autres deviennent des étoiles vides.
Déstructuration
La déstructuration permet d'extraire directement les morceaux utiles d'une donnée au moment où on la parcourt. Le code devient plus lisible, on évite les accès du type item[0] ou item.propriete répétés, et on limite les erreurs en nommant clairement ce que l'on utilise.
Déstructuration d'un tableau
Dans l'exemple suivant, on parcourt un tableau dans lequel chaque élément stocke des coordonnées [x, y]. La déstructuration rend l'affichage beaucoup plus clair.
<script setup>
const points = [
[10, 20],
[30, 50],
[60, 15]
];
</script>
<template>
<svg width="120" height="80" style="border:1px solid #ccc">
<circle
v-for="[x, y] in points"
:key="x + '-' + y"
:cx="x"
:cy="y"
r="3"
/>
</svg>
</template>
La syntaxe [x, y] in points déstructure chaque entrée du tableau en deux variables claires. On place ensuite ces valeurs dans les attributs du SVG. L'écriture reste courte et expressive, ce qui aide à se concentrer sur l'intention plutôt que sur l'indexation.
Déstructuration d'un objet
Passons à un exemple avec des objets. On déstructure ici les propriétés dont on a besoin directement dans la boucle, sans récupérer de clé de propriété.
<script setup>
const utilisateurs = [
{ id: 1, nom: 'Dupont', age: 32, email: 'dupont@example.com' },
{ id: 2, nom: 'Martin', age: 27, email: 'martin@example.com' }
];
</script>
<template>
<ul>
<li v-for="{ id, nom, age } in utilisateurs" :key="id">
Utilisateur #{{ id }} : {{ nom }} - {{ age }} ans
</li>
</ul>
</template>
La déstructuration { id, nom, age } lit seulement les champs utiles. On garde une clé stable avec :key="id" pour un rendu fiable, et on évite les répétitions du style utilisateur.nom.
Renommer une propriété
Il est possible de renommer une propriété au moment de la déstructuration. Cela peut être pratique pour éviter un conflit de nom ou pour rendre le code plus lisible.
<template>
<ul>
<li v-for="{ id, nom: nomUtilisateur } in utilisateurs" :key="id">
Utilisateur #{{ id }} : {{ nomUtilisateur }}
</li>
</ul>
</template>
Ici, la propriété nom est lue dans l'objet, mais elle est assignée à une variable locale appelée nomUtilisateur. On garde ainsi une écriture claire et on évite une confusion éventuelle avec un autre champ nom.
Valeur par défaut
On peut aussi donner une valeur par défaut lors de la déstructuration. Cela permet d'éviter les erreurs si une propriété est absente de l'objet.
<template>
<ul>
<li v-for="{ id, nom = 'Anonyme', email } in utilisateurs" :key="id">
Utilisateur #{{ id }} : {{ nom }} - {{ email }}
</li>
</ul>
</template>
Dans cet exemple, si l'objet ne contient pas de propriété nom, la variable prend automatiquement la valeur Anonyme. On sécurise ainsi l'affichage sans avoir besoin de condition supplémentaire.
Combiner avec l'index
Enfin, la déstructuration peut se combiner avec l'index courant du tableau. On récupère à la fois certaines propriétés de l'objet et la position de l'élément dans le tableau.
<template>
<ul>
<li v-for="({ id, nom }, index) in utilisateurs" :key="id">
Ligne n°{{ index + 1 }} : #{{ id }} - {{ nom }}
</li>
</ul>
</template>
Ici, chaque objet est déstructuré pour récupérer id et nom, tandis que la variable index fournit la position de l'élément dans l'affichage de la liste. On combine donc la lisibilité de la déstructuration avec la souplesse de l'index.
Exercice 01 : Liste de notifications
Objectif : Afficher une liste de notifications à partir d'un tableau et présenter pour chacune un titre, un court extrait et une date de réception. Montrer le nombre total de notifications et afficher un message clair lorsqu'il n'y en a aucune. Utiliser v-for pour le rendu.
Instructions :
- Dans la partie <script>, importer reactive depuis Vue.
-
Créer une constante réactive notifications initialisée avec un tableau de 3 objets.
Chaque objet représente une notification avec au minimum :
- un identifiant numérique id,
- un titre court,
- un extrait de quelques mots,
- une chaîne date pour la date (ex. : 2025-08-24 09:15).
- Créer une fonction viderNotifications() qui supprime tous les éléments du tableau.
- Dans la partie <template>, ajouter un sous-titre <h2> avec le texte "Exo 01".
- Afficher un paragraphe indiquant le nombre total de notifications avec des moustaches (ex. : "Total : [nombre de notifications]").
- Ajouter un bouton "Vider" qui appelle la fonction viderNotifications() au clic.
- Créer une liste non ordonnée <ul> rendue seulement lorsqu'il existe au moins une notification. À l'intérieur, répéter des éléments <li> avec v-for pour chaque notification.
-
Dans chaque élément de la liste, afficher :
- Une balise <div> avec le titre en évidence.
- Une balise <div> avec l"extrait.
- Une balise <div> avec la date.
- Sous la liste, afficher un paragraphe "Aucune notification" lorsqu'il n'y a plus d'éléments à rendre
-
Dans la partie <style>, définir un style minimal pour améliorer la lisibilité :
- un léger espacement entre les items,
- un titre un peu plus gras,
- un texte de date plus discret.
-
Tester :
- Vérifier que les 3 notifications s'affichent correctement avec titre, extrait et date.
- Cliquer sur "Vider" et vérifier que le message "Aucune notification" apparaît et que le total passe à 0.
Exercice 02 : Fiches produit avec détails
Objectif : Rendre une liste de produits où chaque ligne possède une zone de détails repliable. Vous commencerez sans l'attribut :key pour constater un comportement indésirable lors d'un changement d'ordre (l'état ouvert se déplace vers le mauvais produit). Vous ajouterez ensuite une clé stable pour corriger le problème et vérifier que l'état reste associé au bon élément.
Instructions :
- Dans la partie <script>, importer reactive depuis Vue.
-
Créer une constante réactive produits initialisée avec un tableau de 4 objets.
Chaque objet contient au minimum :
- un identifiant unique numérique id,
- un nom,
- une desc courte (description).
- Créer une fonction inverser() qui inverse l'ordre du tableau produits.
- Dans la partie <template>, ajouter un sous-titre <h2> avec le texte "Exo 02".
- Placer un bouton "Inverser l'ordre" qui déclenche la fonction inverser() au clic.
- Créer une liste <ul> et, à l'intérieur, répéter un élément <li> pour chaque produit avec v-for. Pour cette première phase, ne pas ajouter l'attribut :key.
-
Dans chaque élément répété, créer une structure
<details> contenant :
- un <summary> qui affiche le nom du produit,
- un paragraphe avec la description du produit.
-
Dans la partie <style>, ajouter un style minimal pour améliorer la lisibilité :
- retirer les puces de la liste,
- un léger bord et un peu de marge entre les éléments,
- curseur pointeur sur <summary> pour indiquer qu'il est cliquable.
-
Tester et analyser :
- Ouvrir la section de détails du deuxième produit en cliquant sur son <summary>.
- Cliquer sur "Inverser l'ordre".
- Constater que la section restée ouverte ne correspond plus au même produit, elle s'est déplacée sur une autre ligne. Le navigateur a conservé l'état ouvert/fermé sur le nœud DOM réutilisé, ce qui révèle l'absence d'identifiant stable.
- Ajouter maintenant une clé stable. Sur l'élément répété <li>, lier l'attribut :key à l'identifiant unique du produit. Ne pas utiliser l'index de boucle.
-
Refaire exactement le même test et vérifier que :
- la section ouverte reste associée au bon produit après l'inversion,
- l'interface ne présente plus de décalage d'état entre les lignes.
Exercice 03 : FAQ groupée par thèmes
Objectif : Afficher une foire aux questions organisée par thèmes. Chaque thème affiche son titre et la liste de ses questions/réponses en retrait. Utiliser deux boucles v-for imbriquées (thèmes > questions).
Instructions :
- Dans la partie <script>, importer ref depuis Vue.
-
Créer une constante réactive themes initialisée avec un tableau d'au moins 3 objets.
Chaque objet représente un thème et contient :
- un identifiant unique numérique id,
- un titre de thème,
- un tableau questions composé d'au moins 2 objets chacun,
contenant :
- un identifiant numérique id,
- un q (intitulé de la question),
- un r (réponse courte).
- Dans la partie <template>, ajouter un sous-titre <h2> avec le texte "Exo 03".
- Créer une liste principale <ul> qui parcourt la constante réactive themes avec une boucle v-for.
-
Dans chaque élément de thème, afficher :
- le titre du thème,
- le nombre de questions entre parenthèses à l'aide de la propriété length du tableau questions.
- Sous le titre du thème, créer une <ul> secondaire pour lister les questions du thème avec une seconde boucle v-for qui parcourt questions.
- Dans chaque élément de question, afficher l'intitulé q en évidence et la réponse r sur la ligne suivante.
- Gérer le cas d'un thème sans question en affichant un court message (ex. : 'Aucune question pour ce thème') à l'aide d'un rendu conditionnel sous le titre du thème lorsque le tableau questions est vide.
-
Dans la partie <style>, écrire un style minimal :
- retirer les puces et marges par défaut des listes,
- un espacement vertical entre les thèmes,
- un léger retrait (indentation) pour les questions d'un thème.
-
Tester :
- Vérifier que chaque thème affiche bien son titre et le nombre de questions attendu.
- Vérifier que chaque question affiche son intitulé et sa réponse.
- Simuler un thème sans question dans les données et confirmer que le message d'absence s'affiche correctement.
Exercice 04 : Marquer des produits en favori avec un coeur
Objectif : Afficher une liste de produits avec un bouton par ligne permettant de marquer/démarquer un produit comme favori. Le bouton affiche un coeur vide "♡" quand le produit n'est pas favori et un coeur plein "♥" quand il l'est. Utiliser v-for pour le rendu de la liste, des écouteurs d'événements pour réagir aux clics et des clés stables avec l'attribut :key pour garder des états cohérents.
Instructions :
- Dans la partie <script>, importer ref depuis Vue.
-
Créer une constante réactive produits initialisée avec un tableau d'au moins 4 objets.
Chaque objet doit contenir au minimum :
- un identifiant unique numérique id,
- un nom (libellé du produit),
- un prix en nombre,
- un booléen favori initialisé à false.
- Créer une fonction basculerFavori(id) qui retrouve le produit correspondant dans le tableau et inverse la valeur de sa propriété favori.
- Créer une fonction retirer(id) qui supprime le produit correspondant du tableau.
- Dans la partie <template>, ajouter un sous-titre <h2> avec le texte "Exo 04".
-
Au-dessus de la liste, afficher :
- le nombre total de produits à l'aide de produits.length,
- le nombre de favoris en comptant les éléments dont la propriété favori vaut true.
- Créer une liste <ul> qui parcourt la constante réactive produits avec v-for. Sur l'élément répété, lier l'attribut :key à l'identifiant unique du produit.
-
Dans chaque élément de la liste, afficher :
- le nom du produit,
- le prix (format simple, ex. : 19.90 €),
- un bouton coeur qui affiche "♡" si le produit n'est pas favori et "♥" s'il est favori. Au clic, ce bouton appelle la fonction basculerFavori(id) avec l'identifiant du produit.
- un bouton "Retirer" qui appelle la fonction retirer(id).
- Ajouter une classe CSS statique pour structurer la ligne et, en plus, une classe optionnelle appliquée uniquement quand la propriété favori vaut true afin de mettre la ligne en évidence (utiliser :class si vous le souhaitez).
-
Dans la partie <style>, écrire un style minimal :
- retirer les puces de la liste,
- aligner proprement nom, prix et actions,
- donner une apparence de bouton au coeur pour indiquer l'interaction.
-
Tester :
- Cliquer sur plusieurs coeurs et vérifier que le symbole passe bien de "♡" à "♥" et inversement.
- Vérifier que le compteur de favoris s'actualise correctement.
- Cliquer sur "Retirer" et confirmer que la ligne disparaît et que les compteurs sont mis à jour.
Exercice 05 : Navigation clavier dans une liste avec v-for et ref
Objectif : Mettre en place une liste d'éléments rendue avec v-for et permettre à l'utilisateur de déplacer une surbrillance au clavier avec les flèches haut et bas. Découvrir l'intérêt d'une ref utilisée dans une boucle pour récupérer les éléments du DOM à partir d'un identifiant stable.
Avant de commencer, il est utile de comprendre comment fonctionne la structure Map qui servira à associer chaque identifiant d'item à son élément du DOM.
La structure Map en pratique
Une Map se comporte comme un petit dictionnaire de paires clé → valeur. Dans cet exercice, la clé sera l'identifiant stable de l'item (id) et la valeur sera l'élément du DOM rendu pour cette ligne.
Les opérations dont vous aurez besoin sont les suivantes.
- Créer une Map vide const refsParId = new Map()
- Ajouter ou remplacer une entrée refsParId.set(id, element)
- Lire une entrée refsParId.get(id)
- Supprimer une entrée refsParId.delete(id)
Vue appelle automatiquement la fonction passée à :ref à chaque fois qu'un élément de la liste est rendu, mis à jour ou retiré. Cette fonction reçoit l'élément du DOM ou la valeur null. Cela permet de tenir la Map à jour en fonction de ce qui est réellement affiché dans la liste.
Instructions :
- Dans le bloc <script setup>, importer ref depuis Vue.
-
Créer une constante réactive
items en utilisant directement le tableau suivant :
const items = ref([ { id: 1, label: 'Tableau de bord' }, { id: 2, label: 'Commandes' }, { id: 3, label: 'Clients' }, { id: 4, label: 'Statistiques' }, { id: 5, label: 'Paramètres' } ]); - Créer une constante réactive ligneActive initialisée à 0 pour représenter la ligne sélectionnée.
- Créer une constante réactive refsParId initialisée avec new Map(). Cette structure servira à stocker, pour chaque identifiant d'item, l'élément du DOM rendu correspondant.
-
Définir une fonction
enregistrerRefItem(el, id) qui :
- enregistre l'élément dans la Map s'il est fourni,
- supprime l'entrée correspondante si l'élément vaut null.
- Créer une fonction mettreLeFocusSur(id) qui récupère dans la Map l'élément lié à l'identifiant et lui applique le focus.
- Définir une fonction deplacer(direction) qui reçoit une direction en argument. Cette fonction ajuste l'index sélectionné en fonction de la direction fournie, reste dans les limites du tableau, met à jour ligneActive puis déclenche le focus sur l'élément correspondant.
- Ajouter un écouteur d'événement clavier sur window. Utiliser une fonction fléchée qui vérifie la touche enfoncée et appelle deplacer() avec une valeur différente selon la direction souhaitée. Par exemple, appeler deplacer('haut') lorsque l'utilisateur appuie sur la flèche haut et deplacer('bas') lorsqu'il appuie sur la flèche bas.
- Dans le bloc <template>, ajouter un titre "Exo 05".
-
Rendre une liste
<ul> avec
v-for pour parcourir les éléments.
Pour chaque ligne (<li>) :
- Associer une clé stable basée sur l'identifiant de l'item.
- Lier l'attribut :ref à une fonction fléchée qui reçoit l'élément rendu par le v-for (il est fourni automatiquement en paramètre) et transmet cet élément ainsi que l'identifiant stable de l'item à enregistrerRefItem. Cela permet d'associer chaque id à son élément DOM.
- Utiliser l'attribut tabindex de manière dynamique (:tabindex) afin de garantir la cohérence entre la ligne mise en évidence par la classe CSS et la ligne réellement focusée par le navigateur. La ligne active reçoit tabindex="0" pour pouvoir recevoir le focus. Les autres lignes utilisent -1 pour empêcher le navigateur de les cibler automatiquement. Cette organisation évite qu'un élément semble être la sélection visuelle alors que le focus clavier se trouve en réalité ailleurs.
- Ajouter une classe dynamique avec :class pour appliquer la classe active à la ligne sélectionnée. Cette classe doit apparaître uniquement lorsque l'index de la ligne affichée correspond à la valeur stockée dans ligneActive. Cette mise en évidence visuelle permet d'indiquer clairement quelle ligne est actuellement sélectionnée et garantit une bonne cohérence avec le focus clavier.
-
Dans la partie
<style>, prévoir :
- une liste sans puces,
- un léger espacement vertical,
- une classe active définissant un fond clair et du texte en gras,
- un style visible lorsqu'un élément du DOM reçoit le focus.
-
Tester le fonctionnement :
- la première ligne est active au chargement,
- les flèches haut et bas déplacent la sélection,
- le focus suit correctement la ligne sélectionnée,
- la Map contient bien les associations identifiant → élément du DOM.
Exercice 06 : Indicateurs de pagination (v-for sur un nombre)
Objectif : Afficher une rangée de pastilles de pages et permettre de changer la page courante en cliquant sur une pastille ou sur des boutons « Précédent / Suivant ». Le nombre de pastilles est calculé à partir d'un total d'éléments et d'une taille de page. Utiliser v-for avec un nombre pour générer les indicateurs, et des classes dynamiques pour marquer la page active.
Instructions :
- Dans la partie <script>, importer ref depuis Vue.
-
Définir :
- Une constante totalElements avec une valeur correspondant au nombre d'articles total (ex.: 87).
- Une constante taillePage avec une valeur correspondants au nombre d'articles par page (ex.: 10).
- Une constante pagesTotal calculée avec Math.ceil(totalElements / taillePage) (arrondir au plus haut pour s'assurer de compter toutes les pages nécessaires, même si la dernière n'est pas remplie complètement).
- Une constante réactive pageCourante initialisée à 1 via ref().
-
Créer trois fonctions :
- allerA(n) : qui assigne la valeur de n à la constante réactive pageCourante pour atteindre directement la page désirée si possible (pageCourante > 1 et pageCourante < pageTotal).
- precedent() : qui décrémente pageCourante pour passer à la page précédente si possible (pageCourante > 1).
- suivant() : qui incrémente pageCourante pour passer à la page suivante si possible (pageCourante < pageTotal).
- Dans la partie <template>, ajouter un sous-titre <h2> avec le texte "Exo 05".
-
Créer un conteneur de pagination (ex.: <nav>) contenant :
- Un bouton "Précédent" lié à la fonction precedent() et désactivé (:disable) quand pageCourante vaut 1.
- Une liste de pastilles rendue avec v-for="n in pagesTotal".
- Un bouton "Suivant" lié à la fonction suivant() et désactivé (:disable) quand pageCourante vaut pagesTotal.
-
Pour chaque pastille de page :
- Afficher le numéro n.
- Déclencher allerA(n) au clic.
- Ajouter une classe dynamique (:class) quand n correspond à pageCourante afin de marquer la pastille active (ex.: bord/contraste différents).
- Sous les pastilles, afficher une ligne d'information du type : "Page [pageCourante] sur [pagesTotal]".
-
Dans la partie <style>, écrire un style minimal :
- Pastilles alignées horizontalement avec un léger espacement.
- Apparence de bouton pour chaque pastille.
- État actif visuellement distinct.
- État désactivé pour les boutons Précédent/Suivant aux bornes (lorsque pageCourante vaut 1 ou pagesTotal).
-
Tester :
- cliquer sur différentes pastilles et vérifier que la page active change,
- utiliser Les boutons "Précédent" puis "suivant" et vérifier les bornes (les boutons doivent se désactiver aux extrémités),
- modifier les valeurs de totalElements et taillePage pour constater l'impact sur pagesTotal et sur les pastilles rendues.