Manipuler les attributs data-*
Présentation
En HTML, les attributs natifs comme src, href ou type ont chacun un rôle défini par la spécification. Le navigateur leur attribue un sens précis et les traite en conséquence.
Les attributs data-* sont différents. Ce sont des attributs personnalisés que le développeur définit librement. Le navigateur ne leur attribue aucun sens particulier et ne les traite pas. Ils servent uniquement à stocker des informations utiles au script JavaScript, directement sur les éléments HTML concernés, sans les afficher dans l'interface.
En JavaScript, interagir avec une page signifie souvent réagir à une action de l'utilisateur sur un élément précis. Pour répondre correctement, le script a souvent besoin d'un contexte : quel produit, quelle catégorie, quel panneau ? Ce contexte peut être stocké directement sur l'élément concerné, sous forme d'attribut data-*.
Prenons un cas concret. Sur un site de vente en ligne, un catalogue affiche plusieurs produits, chacun avec un bouton "Ajouter au panier". Visuellement, seul le libellé du bouton est affiché. Mais pour que l'action fonctionne, le script doit savoir quel produit ajouter. Stocker l'identifiant du produit directement sur le bouton avec data-produit-id="ref-502" est une solution propre et lisible. Au moment du clic, le script lit cet attribut et dispose immédiatement du contexte dont il a besoin.
<!-- Chaque bouton embarque l'identifiant du produit concerné. -->
<button class="js-btn-panier" type="button" data-produit-id="ref-501">Ajouter au panier</button>
<button class="js-btn-panier" type="button" data-produit-id="ref-502">Ajouter au panier</button>
<button class="js-btn-panier" type="button" data-produit-id="ref-503">Ajouter au panier</button>
Il est utile de distinguer le rôle de chaque type d'attribut courant.
- id identifie un élément de manière unique dans la page. Il sert à cibler cet élément précis en JavaScript ou en CSS.
- class regroupe des éléments qui partagent un style ou un comportement. Il est principalement utilisé en CSS.
- data-* associe des informations utiles au script directement sur un élément. Ces informations ne sont ni affichées ni interprétées par le navigateur.
Nommer un attribut data-*
Syntaxe HTML
Un attribut personnalisé commence obligatoirement par le préfixe data-, suivi d'au moins un caractère. La forme minimale valide est data-x, mais en pratique on choisit un nom qui décrit clairement l'information stockée.
Par convention, les noms d'attributs data-* s'écrivent en kebab-case en HTML, c'est-à-dire en minuscules avec les mots séparés par des tirets.
<!-- kebab-case : minuscules, mots séparés par des tirets -->
<article data-produit-id="42" data-categorie="fps" data-est-disponible="true">...</article>
Accès en JavaScript avec dataset
Le DOM expose les attributs data-* d'un élément via sa propriété dataset. Chaque attribut data-* y devient une propriété accessible directement par son nom.
Lors de cette conversion, le préfixe data- est retiré et le nom restant est converti en camelCase. Chaque tiret est supprimé et la lettre qui le suit passe en majuscule. Cette conversion est automatique.
- data-produit-id devient dataset.produitId
- data-categorie devient dataset.categorie
- data-est-disponible devient dataset.estDisponible
Les valeurs sont toujours des chaînes de caractères
Quelle que soit la valeur écrite dans le HTML, dataset la retourne toujours sous forme de chaîne de caractères. C'est un point important à garder en tête dès qu'on lit une valeur numérique ou booléenne.
console.log(element.dataset.produitId); // "42" et non 42
console.log(typeof element.dataset.produitId); // "string"
console.log(element.dataset.estDisponible); // "true" et non true
console.log(typeof element.dataset.estDisponible); // "string"
Si la valeur doit être utilisée comme nombre, il faut la convertir explicitement.
const id = Number(element.dataset.produitId); // 42
const prix = Number(element.dataset.prix); // 9.99
Si la valeur doit être interprétée comme un booléen, la comparaison se fait avec la chaîne de caractères, pas avec le booléen natif.
// data-est-disponible="true"
if (element.dataset.estDisponible === 'true') { ... } // correct
if (element.dataset.estDisponible === true) { ... } // incorrect : ne sera jamais vrai
Conseils de nommage
- Choisir un nom court et explicite qui décrit directement l'information stockée.
- Rester cohérent sur l'ensemble du projet. Si on utilise data-produit-id à un endroit, on ne passe pas à data-id-produit ailleurs.
Manipuler les attributs data-*
Cette section couvre les quatre opérations principales sur les attributs data-* :
- lire une valeur,
- créer ou modifier un attribut,
- supprimer un attribut,
- cibler des éléments par leur valeur d'attribut.
Les quatres opérations s'appuient sur le même extrait HTML de référence, qui évolue au fil des exemples.
<article id="carte-01" data-statut="disponible">Produit A</article>
<article id="carte-02">Produit B</article>
Lire une valeur
Pour lire la valeur d'un attribut data-*, on accède à la propriété correspondante de dataset sur l'élément ciblé. Si l'attribut n'existe pas sur l'élément, la propriété vaut undefined.
const carte01 = document.querySelector('#carte-01');
const carte02 = document.querySelector('#carte-02');
console.log(carte01.dataset.statut); // Affiche : "disponible"
console.log(carte02.dataset.statut); // Affiche : undefined (l'attribut n'existe pas sur cet élément)
Créer ou modifier une valeur
Pour créer ou modifier un attribut data-*, on assigne une valeur à la propriété correspondante de dataset. Si l'attribut n'existe pas encore sur l'élément, il est créé automatiquement. S'il existe déjà, sa valeur est remplacée.
// Modifier la valeur existante de data-statut sur carte-01.
carte01.dataset.statut = 'echoue';
// Créer un nouvel attribut data-statut sur carte-02 qui n'en avait pas.
carte02.dataset.statut = 'disponible';
Après l'exécution de ce code, les deux balises sont mises à jour dans le DOM.
<!-- modifié -->
<article id="carte-01" data-statut="echoue">Produit A</article>
<!-- créé -->
<article id="carte-02" data-statut="disponible">Produit B</article>
Supprimer un attribut
Pour supprimer un attribut data-*, on utilise l'instruction delete sur la propriété correspondante de dataset. L'attribut disparaît complètement du DOM.
En reprenant l'état du DOM tel qu'il était après les modifications précédentes, on supprime l'attribut data-statut sur carte-01.
delete carte01.dataset.statut;
Après l'exécution de ce code, l'attribut n'est plus présent sur la première balise.
<!-- data-statut supprimé -->
<article id="carte-01">Produit A</article>
<!-- inchangé -->
<article id="carte-02" data-statut="disponible">Produit B</article>
La suppression d'un attribut data-* est moins fréquente que sa lecture ou sa modification. Elle devient surtout utile lorsque l'absence même de l'attribut doit avoir un sens, ou lorsqu'on veut empêcher un script générique ou un sélecteur CSS de continuer à considérer l'élément comme porteur de cette information.
Cibler des éléments par valeur d'attribut
Les attributs data-* peuvent être utilisés comme sélecteurs CSS, aussi bien en JavaScript avec querySelector qu'en CSS directement. Cette syntaxe permet de cibler uniquement les éléments dont l'attribut possède une valeur précise.
En Javascript
L'exemple suivant montre un cas parmi d'autres où il peut être utile de sélectionner tous les produits dont l'attribut data-statut vaut dispo, par exemple pour afficher le nombre de produits disponibles.
// Sélectionner tous les éléments dont data-statut vaut "disponible".
const cartesDisponibles = document.querySelectorAll('[data-statut="disponible"]');
Cette technique doit surtout être utilisée lorsque la sélection dépend d'une valeur précise contenue dans un attribut data-*. Dans les autres cas, il est préférable de sélectionner les éléments via un id ou une classe dédiée au JavaScript, comme js-*.
En CSS
L'exemple suivant applique une opacité moindre pour tous les produits épuisés.
/* Appliquer un style aux éléments dont data-statut vaut "epuise". */
[data-statut="epuise"]
{
opacity: 0.5;
}
Cette technique est a privilégier uniquement lorsque la sélection ou le style dépend d'une valeur précise portée par les éléments. On la retrouve notamment dans les cas d'utilisation présentés dans la section suivante.
Cas d'utilisation
Les attributs data-* répondent principalement à deux grandes situations. La première concerne les données contextuelles attachées à un élément interactif. La seconde concerne les états à valeur qui pilotent automatiquement le style CSS.
Données contextuelles sur un élément interactif
Quand un élément interactif doit porter une information que le script utilisera au moment de l'action, un attribut data-* est la solution naturelle. L'information est directement disponible sur l'élément cliqué, sans avoir à la chercher ailleurs dans la page.
C'est le cas le plus fréquent. On stocke typiquement des identifiants, des catégories ou des références. Par exemple :
- data-produit-id sur un bouton "Ajouter au panier",
- data-categorie sur une carte de catalogue,
- data-cible sur un bouton d'onglet.
L'exemple suivant montre un système d'onglets. Un seul écouteur d'événement est placé sur le parent des boutons. Lors d'un clic, le script remonte jusqu'au bouton d'onglet concerné, lit la valeur de son attribut data-cible, puis affiche le panneau correspondant. La même fonction est aussi utilisée au chargement pour appliquer l'état initial.
Code HTML
<nav id="onglets-produit" class="onglets">
<button class="js-btn-onglet" type="button" data-cible="panneau-infos">Infos</button>
<button class="js-btn-onglet" type="button" data-cible="panneau-avis">Avis</button>
<button class="js-btn-onglet" type="button" data-cible="panneau-faq">FAQ</button>
</nav>
<section class="panneau js-panneau" id="panneau-infos">Contenu des informations.</section>
<section class="panneau js-panneau" id="panneau-avis" hidden>Contenu des avis.</section>
<section class="panneau js-panneau" id="panneau-faq" hidden>Contenu de la FAQ.</section>
Code JavaScript
const ongletsElem = document.querySelector('#onglets-produit');
// Au départ, le premier bouton correspond au panneau visible dans le HTML.
let boutonActifElem = ongletsElem.querySelector('.js-btn-onglet');
let panneauActifElem = document.getElementById(boutonActifElem.dataset.cible);
function activerOnglet(boutonOngletElem)
{
// Arrêter si l'onglet déjà actif est recliqué.
if (boutonOngletElem === boutonActifElem)
{
return;
}
// Lire l'id du panneau à afficher.
const idCible = boutonOngletElem.dataset.cible;
const panneauCibleElem = document.getElementById(idCible);
// Masquer l'ancien panneau.
panneauActifElem.hidden = true;
// Afficher le nouveau panneau.
panneauCibleElem.hidden = false;
// Mémoriser le nouvel état actif.
boutonActifElem = boutonOngletElem;
panneauActifElem = panneauCibleElem;
}
ongletsElem.addEventListener('click', (event) =>
{
// Remonter jusqu'au bouton d'onglet le plus proche.
const boutonCliqueElem = event.target.closest('.js-btn-onglet');
// Arrêter si le clic ne vient pas d'un bouton d'onglet.
if (!boutonCliqueElem)
{
return;
}
activerOnglet(boutonCliqueElem);
});
Ici, l'attribut data-cible sert à associer chaque bouton au panneau qu'il doit afficher. Le script reste générique, car il lit cette information directement dans le HTML au lieu de coder une règle différente pour chaque onglet.
Cette approche reste souple. Ajouter un nouvel onglet consiste simplement à ajouter un nouveau bouton avec une valeur dans data-cible et le panneau possédant l' id correspondant.
État à valeur qui pilote le style CSS
Quand un élément peut prendre plusieurs états distincts et que le CSS doit s'adapter à chacun, modifier un attribut data-* dans le DOM permet au CSS de réagir automatiquement via des sélecteurs d'attribut.
Ce cas se distingue de l'état binaire classique (actif / inactif), pour lequel classList.toggle() reste la solution adaptée. L'attribut data-* devient pertinent quand l'état porte une valeur significative, comme une note de 1 à 5 ou un mode d'affichage parmi plusieurs options. Gérer cela avec des classes imposerait de créer autant de classes que de valeurs possibles (.note-1, .note-2, etc.) et de retirer manuellement la classe précédente à chaque changement.
L'exemple suivant montre un widget (widget = élément interactif réutilisable dans une interface) de notation par étoiles. La note choisie est stockée dans data-note sur le conteneur. Le CSS colore automatiquement les bonnes étoiles en fonction de cette valeur, sans qu'il soit nécessaire de manipuler les classes de chaque étoile individuellement.
Code HTML
<div class="notation js-notation" data-note="0">
<button class="etoile js-etoile" type="button" data-valeur="1">★</button>
<button class="etoile js-etoile" type="button" data-valeur="2">★</button>
<button class="etoile js-etoile" type="button" data-valeur="3">★</button>
<button class="etoile js-etoile" type="button" data-valeur="4">★</button>
<button class="etoile js-etoile" type="button" data-valeur="5">★</button>
</div>
Code CSS
.etoile
{
color: #ccc; /* Gris par défaut */
}
/*
:nth-child(-n + x) sélectionne les x premiers enfants d'un même parent.
La formule (-n + x) génère les indices : x, x-1, x-2, ..., 1.
Les enfants sont numérotés à partir de 1.
Tout enfant dont la position est inférieure ou égale à x est donc sélectionné.
Exemple avec .notation[data-note="3"] .etoile:nth-child(-n + 3) :
n=0 → -0 + 3 = 3 → étoile n°3 sélectionnée
n=1 → -1 + 3 = 2 → étoile n°2 sélectionnée
n=2 → -2 + 3 = 1 → étoile n°1 sélectionnée
n=3 → -3 + 3 = 0 → ignoré (aucun enfant n°0)
Résultat : les trois premières étoiles sont colorées en or.
*/
.notation[data-note="1"] .etoile:nth-child(-n + 1),
.notation[data-note="2"] .etoile:nth-child(-n + 2),
.notation[data-note="3"] .etoile:nth-child(-n + 3),
.notation[data-note="4"] .etoile:nth-child(-n + 4),
.notation[data-note="5"] .etoile:nth-child(-n + 5)
{
color: gold;
}
Code JavaScript
const notationElem = document.querySelector('.js-notation');
notationElem.addEventListener('click', (e) =>
{
// closest() remonte dans les ancêtres de l'élément cliqué
// et retourne le premier qui correspond au sélecteur passé en argument.
const etoileElem = e.target.closest('.js-etoile');
if (etoileElem)
{
// Lire la valeur de l'étoile cliquée et la stocker sur le conteneur.
// Le CSS réagit automatiquement : les bonnes étoiles sont colorées en or.
notationElem.dataset.note = etoileElem.dataset.valeur;
}
});
Sans data-note, il faudrait parcourir toutes les étoiles à chaque clic et ajouter ou retirer une classe sur chacune d'elles en fonction de sa position. Stocker la note dans un seul attribut réduit le JavaScript à une opération et délègue entièrement la mise en forme au CSS.
Bonnes pratiques
Les attributs data-* ne sont ni une solution à tout, ni une mauvaise pratique en soi. Leur intérêt dépend surtout du rôle de l'information. Avant d'en ajouter un, il est utile de se poser une question simple est-ce que cette information doit réellement être portée par l'élément HTML lui-même ?
Choisir où stocker l'information
-
Le script doit seulement retrouver un élément ?
Utiliser de préférence une classe dédiée au JavaScript, comme js-..., ou un id si l'élément est unique. -
Il faut représenter un état visuel binaire ?
Utiliser en général une classe CSS, comme actif, ouvert ou selectionne. -
Un élément doit porter une valeur utile au script ?
Utiliser un attribut data-* si cette valeur appartient bien à cet élément et doit pouvoir être relue depuis le DOM. -
L'information n'existe que pour la logique interne du script ?
La stocker de préférence dans une variable, un tableau, un objet, une Map ou une autre structure JavaScript.
Choisir entre une classe CSS et un attribut data-*
Une classe CSS convient bien lorsqu'il faut simplement indiquer un état binaire ou appliquer un style sans transporter de valeur métier. C'est le cas, par exemple, d'un menu ouvert ou fermé, d'un bouton actif ou non, ou d'une carte sélectionnée ou non.
boutonElem.classList.toggle('actif');
Un attribut data-* devient plus pertinent lorsque l'élément doit porter une valeur explicite, par exemple une note, une catégorie, une cible, un identifiant ou un mode. Ce choix prend encore plus de sens lorsque le JavaScript lit cette valeur et que le CSS peut lui aussi réagir en fonction d'elle.
conteneurElem.dataset.note = '4';
Autrement dit, une classe répond bien à la logique oui / non, tandis qu'un attribut data-* répond bien à la logique quelle valeur ?
Choisir entre un attribut data-* et une donnée stockée en JavaScript
Les deux approches sont possibles, mais elles ne répondent pas au même besoin. Un attribut data-* est utile lorsque l'information doit rester attachée à un élément précis dans le DOM. C'est souvent le cas lorsqu'elle sera lue au moment d'un clic, d'un survol, d'une initialisation, ou lorsqu'elle doit rester visible dans le HTML.
À l'inverse, lorsqu'une donnée ne sert qu'au fonctionnement interne du script, qu'elle évolue souvent, qu'elle est calculée à la volée, ou qu'elle n'a pas besoin d'être portée par un élément HTML, il est préférable de la stocker directement en JavaScript.
Par exemple, un attribut comme data-produit-id="42" sur un bouton "Ajouter au panier" a du sens, car cette valeur appartient à ce bouton précis. En revanche, la liste complète des produits chargés depuis une API, le panier courant ou un score calculé en mémoire ont davantage leur place dans des variables ou des objets JavaScript.
Il ne faut donc pas opposer artificiellement les deux approches. La vraie question est plutôt la suivante cette information doit-elle vivre dans le DOM ou seulement dans la logique du script ?
Points d'attention
-
Les valeurs de dataset sont toujours des chaînes de caractères.
Si un attribut contient un nombre ou un booléen, il faudra le convertir explicitement en JavaScript. -
Ne pas stocker de données sensibles.
Tout ce qui se trouve dans le DOM peut être lu depuis l'inspecteur. Un attribut data-role, data-prix-achat ou data-est-admin ne doit jamais servir à sécuriser une action. -
Ne pas y stocker de gros volumes de données.
Les attributs data-* conviennent à des valeurs courtes et ciblées. Un gros objet JSON ou une longue structure de données a davantage sa place dans une variable JavaScript ou dans une réponse d'API. -
Ne pas en faire la source de vérité.
Un attribut data-stock="3" peut refléter un état affiché dans l'interface, mais la donnée de référence reste côté serveur, en base de données ou dans une API. -
Ne pas utiliser data-* quand aucune donnée n'est portée.
Si le script doit seulement retrouver un élément, une classe js-... est généralement plus claire.
Exercices
JS: Attributs HTML data-* - Exo 01
Cet exercice porte sur l'utilisation d'un attribut data-* pour piloter un filtre, et sur la mise à jour de l'affichage avec la propriété JavaScript hidden. Le but est de filtrer un catalogue de jeux selon la catégorie choisie dans un select.
Attendu
Une liste de cartes de jeux est affichée. Chaque carte possède un attribut data-categorie. L'utilisateur choisit une catégorie dans la liste déroulante. Seules les cartes correspondant à la catégorie choisie doivent rester visibles. Si la catégorie Toutes est sélectionnée, toutes les cartes doivent être visibles.
Structure
Créer un dossier nommé Exo-01-attributs-data-filtrer-catalogue, puis organiser les fichiers et dossiers en respectant l'arborescence suivante :
📁 Exo-01-attributs-data-filtrer-catalogue/
├── 📄 index.html
├── 📁 css/
│ └── 📄 style.css
└── 📁 js/
├── 📄 app.js
└── 📁 modules/
└── 📄 filtreCatalogue.js
Fichiers fournis
Copier les fichiers suivants à l'identique.
index.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./css/style.css">
<title>JS: Manipulation Attributs data - Exo 01</title>
</head>
<body>
<main>
<h1>JS: Manipulation Attributs data - Exo 01</h1>
<h2>Catalogue de jeux</h2>
<section class="filtres">
<h3>Filtre</h3>
<label>
Catégorie
<select id="select-categorie">
<option value="toutes" selected>Toutes</option>
<option value="fps">FPS</option>
<option value="rogue-lite">Rogue-lite</option>
</select>
</label>
</section>
<section id="catalogue" class="catalogue">
<article class="jeu js-jeu" data-categorie="fps">
<h3>DOOM Eternal</h3>
<p class="meta">FPS</p>
</article>
<article class="jeu js-jeu" data-categorie="fps">
<h3>Valorant</h3>
<p class="meta">FPS</p>
</article>
<article class="jeu js-jeu" data-categorie="rogue-lite">
<h3>Hades</h3>
<p class="meta">Rogue-lite</p>
</article>
<article class="jeu js-jeu" data-categorie="rogue-lite">
<h3>Dead Cells</h3>
<p class="meta">Rogue-lite</p>
</article>
<article class="jeu js-jeu" data-categorie="rogue-lite">
<h3>Vampire Survivors</h3>
<p class="meta">Rogue-lite</p>
</article>
</section>
</main>
<script src="./js/app.js" type="module"></script>
</body>
</html>
css/style.css
/* VARIABLES */
:root
{
--fond-principal: #0e1319;
--fond-secondaire: #151c24;
--texte-principal: #e7e1ca;
--texte-secondaire: #b9b39f;
--accent-visuel: orange;
--espace-s: 0.75rem;
--espace-m: 2rem;
--police-corps: Arial, sans-serif;
--rayon-s: 0.5rem;
--bordure: 1px solid rgba(231, 225, 202, 0.15);
}
/* RESET */
html, body, main, section, article, h1, h2, h3, p, label, select
{
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* BASE */
html
{
font-family: var(--police-corps);
color: var(--texte-principal);
background-color: var(--fond-principal);
}
body
{
min-height: 100vh;
}
main
{
margin: 0 auto;
padding: var(--espace-m);
width: min(900px, 100%);
display: flex;
flex-direction: column;
gap: var(--espace-m);
}
h1, h2
{
text-align: center;
}
/* FILTRES */
.filtres
{
border: var(--bordure);
border-radius: var(--rayon-s);
padding: var(--espace-m);
background: var(--fond-secondaire);
display: flex;
flex-wrap: wrap;
gap: var(--espace-m);
align-items: end;
}
.filtres h3
{
width: 100%;
font-size: 1.1rem;
color: var(--texte-secondaire);
}
label
{
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 0.95rem;
}
select
{
border: var(--bordure);
border-radius: var(--rayon-s);
padding: 0.6rem 0.8rem;
outline: none;
color: var(--texte-principal);
background: transparent;
}
select option
{
background: var(--fond-principal);
color: var(--texte-principal);
}
select:focus
{
border-color: var(--accent-visuel);
background: var(--fond-principal);
}
/* CATALOGUE */
.catalogue
{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--espace-m);
}
.jeu
{
border: var(--bordure);
border-radius: var(--rayon-s);
padding: var(--espace-m);
background: var(--fond-secondaire);
display: flex;
flex-direction: column;
gap: var(--espace-s);
}
/*
Note sur hidden
Le navigateur applique bien display: none quand l'attribut hidden est present.
Cependant, une regle CSS comme .serie { display: flex; } vient d'un style auteur
et peut l'emporter sur le style par defaut du navigateur.
Cette regle garantit que hidden cache vraiment les cartes dans ce projet.
*/
.jeu[hidden]
{
display: none;
}
.jeu::before
{
content: "";
width: 2.5rem;
height: 0.25rem;
background: var(--accent-visuel);
border-radius: 999px;
}
.jeu h2
{
font-size: 1.05rem;
}
.meta
{
color: var(--texte-secondaire);
font-size: 0.95rem;
}
.desc
{
color: var(--texte-secondaire);
line-height: 1.45;
}
/* RESPONSIVE */
@media (max-width: 520px)
{
main
{
padding: 1rem;
}
.filtres
{
padding: 1rem;
}
}
Instructions
Le travail se fait dans js/app.js et js/modules/filtreCatalogue.js. Le filtre doit être construit progressivement avec des console.log(), afin de valider chaque étape avant de passer à la suivante.
Dans cet exercice, chaque carte possède un attribut HTML data-categorie. En JavaScript, vous lirez cette information via dataset. Vous utiliserez ensuite hidden pour masquer ou afficher les cartes.
Étape 01
- Créer le fichier js/modules/filtreCatalogue.js.
-
En haut du fichier, créer deux variables initialisées à null :
- selectCategorieElem
- carteElements
-
À la suite de ces deux variables, créer une fonction nommée initialiserFiltreCatalogue.
Cette fonction reçoit deux paramètres qui contiennent des sélecteurs CSS sous forme de texte.
Ces sélecteurs seront fournis au moment où app.js appellera la fonction
afin que le module puisse retrouver, dans la page, l'interface du filtre (la balise select)
et les cartes de jeux (les balises article) :
- selecteurCssCategorie
- selecteurCssCartes
- Dans initialiserFiltreCatalogue, sélectionner le filtre (balise select) à l'aide de la méthode querySelector et du sélecteur CSS passé en argument à selecteurCssCategorie, puis stocker la référence dans selectCategorieElem.
- Ensuite, sélectionner toutes les cartes (les informations concernant les jeux, présents dans les balises article) à l'aide de la méthode querySelectorAll et du sélecteur CSS passé en argument à selecteurCssCartes, puis stocker la référence dans carteElements.
- Ajouter un test de sécurité. Attention : contrairement à querySelector qui renvoie null si aucun élément n'est trouvé, querySelectorAll renvoie toujours une NodeList, même vide. Il faut donc vérifier deux choses séparément : si selectCategorieElem vaut null, ou si carteElements.length vaut 0. Dans ces cas, afficher le message "Filtre catalogue non initialisé : sélecteur catégorie ou cartes introuvables." avec console.warn(), puis arrêter la fonction immédiatement avec return. Ce contrôle évite d'exécuter la suite du script sur des éléments inexistants, ce qui provoquerait une erreur et bloquerait tous les autres scripts de la page.
- Exporter la fonction initialiserFiltreCatalogue en utilisant un export nommé, afin qu'elle puisse être importée explicitement dans app.js.
Étape 02
- Créer le fichier js/app.js.
- Dans js/app.js, importer la fonction initialiserFiltreCatalogue depuis ./modules/filtreCatalogue.js. Le chemin commence par ./ car app.js et le dossier modules se trouvent dans le même dossier js.
-
Appeler initialiserFiltreCatalogue en lui passant deux sélecteurs CSS.
Chaque sélecteur doit permettre de retrouver un type d'élément précis dans la page :
- le premier sélecteur doit cibler la liste déroulante select dont l'attribut HTML id vaut select-categorie
- le second sélecteur doit cibler toutes les cartes de jeux situées à l'intérieur de la balise dont l'attribut HTML id vaut catalogue, et qui possèdent la classe CSS js-jeu
-
Tester :
- recharger la page
- ouvrir la console
- vérifier que la fonction initialiserFiltreCatalogue s'exécute bien et que les logs de l'étape 01 confirment que les sélecteurs CSS des deux éléments HTML à stocker étaient bien valides.
Étape 03
- Dans js/modules/filtreCatalogue.js, créer une fonction nommée appliquerFiltre. La fonction ne reçoit aucun argument. Elle utilise les références déjà stockées dans selectCategorieElem et carteElements.
- Dans appliquerFiltre, ajouter provisoirement un console.log() affichant le message appliquerFiltre appelé !. Ce message sert uniquement à vérifier que la fonction est bien appelée lors des tests qui suivent, avant d'implémenter la logique réelle.
- Dans initialiserFiltreCatalogue, appeler appliquerFiltre. Cet appel permet d'afficher, dès l'arrivée sur la page, les bonnes cartes en fonction de la catégorie sélectionnée dans le filtre, sans devoir attendre une action de l'utilisateur.
- Dans initialiserFiltreCatalogue, ajouter un écouteur d'événement change sur selectCategorieElem. À chaque changement de sélection, la fonction appliquerFiltre doit être exécutée afin de mettre à jour l'affichage du catalogue.
-
Tester :
- recharger la page et vérifier dans la console que le message appliquerFiltre appelé ! apparaît une première fois, ce qui confirme que la fonction est bien exécutée au chargement
- changer la catégorie dans la liste déroulante et vérifier que le même message apparaît à chaque changement, ce qui confirme que la fonction est bien appelée lorsque l'utilisateur modifie le filtre
Étape 04
- Dans appliquerFiltre, récupérer la valeur actuellement sélectionnée dans le filtre via la propriété value de la balise référencée par selectCategorieElem. Stocker cette valeur dans une constante nommée categorieDemandee, afin de pouvoir l'utiliser ensuite pour comparer chaque carte et décider lesquelles doivent être affichées.
- Afficher categorieDemandee dans la console. Tester en changeant plusieurs fois la sélection. La valeur doit correspondre aux valeurs des option.
- Utiliser une structure itérative afin de parcourir toutes les cartes stockées dans carteElements. Pour chaque carte, récupérer la valeur de l'attribut HTML data-categorie via dataset. Stocker cette valeur dans une constante nommée categorieCarte.
-
Pour chaque carte, créer une constante nommée estVisible.
Cette constante doit contenir un booléen qui résume la décision
afficher (true) ou cacher (false) la carte.
estVisible doit être true dans deux cas :
- si le choix du filtre categorieDemandee vaut 'toutes'. Cette valeur est un choix spécial du filtre car elle ne correspond à aucune catégorie réelle écrite dans les attributs data-categorie. Elle signifie simplement que l'on veut tout afficher.
- sinon, si la catégorie sélectionnée dans le filtre (categorieDemandee) est identique à la catégorie de la carte courante (categorieCarte), alors la carte doit rester visible car elle correspond au choix de l'utilisateur.
-
Mettre à jour l'affichage de chaque carte en fonction de la valeur de
estVisible.
À ce stade, la décision est déjà prise.
Il ne reste plus qu'à traduire cette décision en une action concrète sur le HTML.
Pour cela, passer la propriété JavaScript hidden
à true ou à false en fonction de la valeur estVisible.
- si estVisible vaut true, la carte doit rester visible, donc hidden doit être false
- si estVisible vaut false, la carte doit être masquée, donc hidden doit être true
-
Tester :
- sélectionner FPS et vérifier que seules les cartes FPS restent visibles
- sélectionner Rogue-lite et vérifier que seules les cartes Rogue-lite restent visibles
- sélectionner Toutes et vérifier que toutes les cartes réapparaissent
- Quand tout fonctionne, supprimer les logs de debug qui ne sont plus utiles.
JS: Attributs HTML data-* - Exo 02
Cet exercice porte sur un filtre piloté par un attribut data-*, et sur la génération de cartes HTML en JavaScript à partir d'une liste de films simulant des données de base de données. Le but est de filtrer un catalogue de films selon le genre choisi dans un select.
Attendu
Un catalogue de films est affiché sous forme de cartes. Les cartes ne sont pas écrites en dur dans le fichier HTML, elles sont générées en JavaScript. L'utilisateur choisit un genre dans la liste déroulante. Seules les cartes correspondant au genre choisi doivent rester visibles. Quand l'option Tous est sélectionnée, toutes les cartes doivent être visibles.
Structure
Créer un dossier nommé Exo-02-attributs-data-filtrer-films, puis organiser les fichiers et dossiers en respectant l'arborescence suivante.
📁 Exo-02-attributs-data-filtrer-films/
├── 📄 index.html
├── 📁 css/
│ └── 📄 style.css
└── 📁 js/
├── 📄 app.js
└── 📁 modules/
├── 📄 donneesFilms.js
└── 📄 catalogueFilms.js
Fichiers fournis
Copier les fichiers suivants à l'identique.
index.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./css/style.css">
<title>JS: Attributs data et filtre - Exo 02</title>
</head>
<body>
<main>
<h1>JS: Attributs data et filtre - Exo 02</h1>
<h2>Catalogue de films</h2>
<section class="filtres">
<h3>Filtre</h3>
<label>
Genre
<select id="select-genre">
<option value="tous" selected>Tous</option>
<option value="action">Action</option>
<option value="science-fiction">Science-fiction</option>
<option value="animation">Animation</option>
<option value="thriller">Thriller</option>
</select>
</label>
</section>
<section id="catalogue" class="catalogue">
<!-- Les cartes de films seront générées en JavaScript. -->
</section>
</main>
<script src="./js/app.js" type="module"></script>
</body>
</html>
css/style.css
/* VARIABLES */
:root
{
--fond-principal: #0e1319;
--fond-secondaire: #151c24;
--texte-principal: #e7e1ca;
--texte-secondaire: #b9b39f;
--accent-visuel: orange;
--espace-s: 0.75rem;
--espace-m: 2rem;
--police-corps: Arial, sans-serif;
--rayon-s: 0.5rem;
--bordure: 1px solid rgba(231, 225, 202, 0.15);
}
/* RESET */
html, body, main, section, article, h1, h2, h3, p, label, select
{
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* BASE */
html
{
font-family: var(--police-corps);
color: var(--texte-principal);
background-color: var(--fond-principal);
}
body
{
min-height: 100vh;
}
main
{
margin: 0 auto;
padding: var(--espace-m);
width: min(900px, 100%);
display: flex;
flex-direction: column;
gap: var(--espace-m);
}
h1, h2
{
text-align: center;
}
/* FILTRES */
.filtres
{
border: var(--bordure);
border-radius: var(--rayon-s);
padding: var(--espace-m);
background: var(--fond-secondaire);
display: flex;
flex-wrap: wrap;
gap: var(--espace-m);
align-items: end;
}
.filtres h3
{
width: 100%;
font-size: 1.1rem;
color: var(--texte-secondaire);
}
label
{
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 0.95rem;
}
select
{
border: var(--bordure);
border-radius: var(--rayon-s);
padding: 0.6rem 0.8rem;
outline: none;
color: var(--texte-principal);
background: transparent;
}
select option
{
background: var(--fond-principal);
color: var(--texte-principal);
}
select:focus
{
border-color: var(--accent-visuel);
background: var(--fond-principal);
}
/* CATALOGUE */
.catalogue
{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--espace-m);
}
.film
{
border: var(--bordure);
border-radius: var(--rayon-s);
padding: var(--espace-m);
background: var(--fond-secondaire);
display: flex;
flex-direction: column;
gap: var(--espace-s);
}
/*
Les cartes utilisent display: flex via la règle .film.
Sans la règle ci-dessous, le display: flex de .film prendrait le dessus,
et une carte marquée hidden pourrait rester visible.
On force donc explicitement display: none lorsque hidden est présent.
*/
.film[hidden]
{
display: none;
}
.film::before
{
content: "";
width: 2.5rem;
height: 0.25rem;
background: var(--accent-visuel);
border-radius: 999px;
}
.titre
{
font-size: 1.05rem;
}
.meta
{
color: var(--texte-secondaire);
font-size: 0.95rem;
}
/* RESPONSIVE */
@media (max-width: 520px)
{
main
{
padding: 1rem;
}
.filtres
{
padding: 1rem;
}
}
js/modules/donneesFilms.js
const films = [
{
id: 1,
titre: 'Mad Max Fury Road',
genre: 'action',
annee: 2015
},
{
id: 2,
titre: 'John Wick',
genre: 'action',
annee: 2014
},
{
id: 3,
titre: 'Interstellar',
genre: 'science-fiction',
annee: 2014
},
{
id: 4,
titre: 'The Matrix',
genre: 'science-fiction',
annee: 1999
},
{
id: 5,
titre: 'Spider-Man Into the Spider-Verse',
genre: 'animation',
annee: 2018
},
{
id: 6,
titre: 'Spirited Away',
genre: 'animation',
annee: 2001
},
{
id: 7,
titre: 'Parasite',
genre: 'thriller',
annee: 2019
}
];
export { films };
Instructions
Le travail se fait dans js/app.js, js/modules/donneesFilms.js et js/modules/catalogueFilms.js. Le catalogue et le filtre doivent être construits progressivement avec des console.log(), afin de valider chaque étape avant de passer à la suivante.
Dans cet exercice, les cartes ne sont pas écrites dans le HTML. Elles sont générées en JavaScript à partir d'une liste de films placée dans un module, afin de simuler des données provenant d'une base de données.
Les cartes seront créées avec innerHTML. Ici, c'est acceptable car les données sont encodées par vos soins, et ne viennent pas d'un utilisateur ni d'une API.
Étape 01
- Créer le fichier js/modules/catalogueFilms.js.
- Importer films depuis ./donneesFilms.js. L'import est un import nommé, il utilise donc le même nom. La constante films deviendra ainsi une constante du module actuel et pourra être utilisée au sein des fonctions de celui-ci.
-
En haut du fichier, créer trois variables initialisées à null.
- selectGenreElem
- catalogueElem
- carteElements
-
À la suite de ces variables, créer une fonction nommée initialiserCatalogueFilms.
Cette fonction reçoit deux paramètres qui contiennent des sélecteurs CSS sous forme de texte.
Ces sélecteurs seront fournis au moment où app.js appellera la fonction
afin que le module puisse retrouver l'interface du filtre (la balise select)
et le conteneur dans lequel les cartes seront injectées :
- selecteurCssGenre
- selecteurCssCatalogue
- Dans initialiserCatalogueFilms, sélectionner le filtre (balise select) à l'aide de la méthode querySelector et du sélecteur CSS passé en argument à selecteurCssGenre, puis stocker la référence dans selectGenreElem.
- Ensuite, sélectionner le conteneur HTML dans lequel les cartes films vont être créées à l'aide de la méthode querySelector et du sélecteur CSS passé en argument à selecteurCssCatalogue, puis stocker la référence dans catalogueElem.
- Ajouter un test de sécurité. Si selectGenreElem vaut null ou si catalogueElem vaut null, afficher le message Catalogue films non initialisé, filtre ou conteneur introuvable. avec console.warn(), puis arrêter la fonction immédiatement avec return.
- Juste après ce test de sécurité, ajouter un console.log() temporaire affichant Tous les éléments HTML ont bien été chargés et stockés. Ce message servira de repère lors du test de l'étape suivante afin de confirmer que les deux sélections ont bien réussi et que les références ont bien été enregistrées dans les variables du module. Ce log pourra être supprimé plus tard, une fois le reste du catalogue fonctionnel.
- Exporter la fonction initialiserCatalogueFilms en utilisant un export nommé, afin qu'elle puisse être importée explicitement dans app.js.
Étape 02
- Créer le fichier js/app.js.
- Dans js/app.js, importer la fonction initialiserCatalogueFilms depuis ./modules/catalogueFilms.js. Le chemin commence par ./ car app.js et le dossier modules se trouvent dans le même dossier.
-
Appeler initialiserCatalogueFilms en lui passant deux sélecteurs CSS.
Chaque sélecteur doit permettre de retrouver un type d'élément précis dans la page :
- le premier sélecteur cible la liste déroulante select dont l'attribut HTML id vaut select-genre
- le second sélecteur cible la balise qui contient le catalogue dont l'attribut HTML id vaut catalogue
-
Tester :
- recharger la page
- ouvrir la console
- vérifier que la fonction initialiserCatalogueFilms s'exécute bien et que les logs de l'étape 01 confirment que les sélecteurs CSS des deux éléments HTML à stocker étaient bien valides.
Étape 03
- Revenir dans le fichier js/modules/catalogueFilms.js.
- À la suite du code déjà présent, créer une fonction nommée afficherCatalogueFilms qui ne reçoit aucun argument. Le rôle de cette fonction est de générer les cartes de films à partir de la constante films importée via son module, puis d'injecter le résultat dans catalogueElem.
- Créer une variable nommée html initialisée avec une chaîne vide. Cette variable sera utilisée pour construire progressivement une seule grande chaîne contenant toutes les cartes, avant de l'injecter dans le conteneur HTML.
- Utiliser une structure itérative pour parcourir le tableau des films (constante importée films). Chaque itération permettra de construire la structure HTML d'une carte film.
-
À chaque itération, déstructurer l'objet film courant pour en extraire les propriétés utiles,
puis construire la carte HTML en concaténant la variable html avec le gabarit suivant :
const { titre, genre, annee } = film; html += ` <article class="film js-film" data-genre="${genre}"> <h3 class="titre">${titre}</h3> <p class="meta">${genre} · ${annee}</p> </article> `; - Une fois la chaîne HTML prête (boucle terminée), l'injecter dans la balise conteneur du catalogue, celle référencée par catalogueElem, en utilisant innerHTML.
- Une fois les films ajoutés au conteneur HTML, les sélectionner à l'aide de la méthode querySelectorAll en les ciblant à partir de leur classe CSS js-film et stocker le résultat dans la variable de module carteElements.
- Dans initialiserCatalogueFilms, à la suite du code déjà présent, appeler afficherCatalogueFilms. Au rechargement, le catalogue doit se remplir sans que vous ayez écrit de cartes dans le HTML.
- Tester : recharger la page et vérifier que les cartes de films ont bien été ajoutées à la page.
Étape 04
- Dans js/modules/catalogueFilms.js, créer une fonction nommée appliquerFiltre. La fonction ne reçoit aucun argument. Elle utilise les références déjà stockées dans selectGenreElem et carteElements.
- Dans appliquerFiltre, ajouter provisoirement un console.log() affichant le message appliquerFiltre appelé !. Ce message sert à vérifier que la fonction est bien exécutée lors des tests qui suivent, avant d'implémenter la logique réelle.
- Dans initialiserCatalogueFilms, juste après l'appel de afficherCatalogueFilms, appeler appliquerFiltre. Cet appel permet d'afficher, dès l'arrivée sur la page, les bonnes cartes en fonction du genre actuellement sélectionné.
- Ensuite, toujours dans initialiserCatalogueFilms, ajouter un écouteur d'événement change sur selectGenreElem. À chaque changement de sélection, la fonction appliquerFiltre est exécutée afin de mettre à jour l'affichage du catalogue.
-
Tester :
- recharger la page et vérifier que appliquerFiltre appelé ! apparaît une première fois
- changer le genre et vérifier que appliquerFiltre appelé ! apparaît à chaque changement
Étape 05
- Dans appliquerFiltre, récupérer la valeur actuellement sélectionnée dans le filtre via la propriété value de selectGenreElem. Stocker cette valeur dans une constante nommée genreDemande.
- Afficher genreDemande dans la console. Tester en changeant plusieurs fois la sélection. La valeur affichée doit correspondre aux valeurs des option.
- Parcourir toutes les cartes stockées dans carteElements. À chaque itération, récupérer la valeur de data-genre de chaque carte film via dataset, puis stocker cette valeur dans une constante nommée genreCarte.
-
À chaque itération, créer une constante nommée estVisible.
Cette constante résume la décision d'affichage.
estVisible vaut true dans deux cas :
- quand genreDemande vaut tous, car cette valeur est un choix spécial du filtre et ne correspond à aucun genre réel écrit dans data-genre
- sinon, quand genreDemande est identique à genreCarte, car la carte correspond au choix de l'utilisateur
-
Mettre à jour l'affichage de chaque carte en fonction de estVisible.
Pour cela, passer la propriété JavaScript hidden à true ou à false.
- quand estVisible vaut true, la carte reste visible, donc hidden vaut false
- quand estVisible vaut false, la carte est masquée, donc hidden vaut true
-
Tester :
- sélectionner Action et vérifier que seules les cartes action restent visibles
- sélectionner Science-fiction et vérifier que seules les cartes science-fiction restent visibles
- sélectionner Animation et vérifier que seules les cartes animation restent visibles
- sélectionner Thriller et vérifier que seules les cartes thriller restent visibles
- sélectionner Tous et vérifier que toutes les cartes réapparaissent
- Quand tout fonctionne, supprimer les logs de debug qui ne sont plus utiles.