Chapitre 1530 minDébutant
Exercices JavaScript corrigés type examen
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- résoudre des exercices qui mélangent toutes les notions du cours ;
- lire, corriger et faire évoluer un code existant comme à l'E6 ;
- repérer les erreurs de sécurité et d'accessibilité dans un code.
10 exercices corrigés
Ces exercices corrigés mélangent toutes les notions du cours, comme les cas que tu rencontreras en évaluation, en atelier de professionnalisation ou pendant l'heure de réalisation de l'E6 : lire du code et prévoir son résultat, afficher des données dans une page, valider un formulaire, appeler une API, calculer des statistiques, corriger un code bogué, faire évoluer une application existante. Ils sont classés du plus guidé au plus ouvert. Chaque corrigé a été exécuté ; cherche d'abord, ouvre ensuite.
Les exercices 2 à 5 portent sur une autre application de Réparinfo, le suivi des interventions : chaque appareil confié par un client donne lieu à une intervention, avec une date, un client, un appareil, un technicien, une durée en minutes et un statut ('en attente', 'en cours' ou 'terminée').
const interventions = [
{ id: 1, technicien: 'Julie', client: 'Martin', duree: 45, statut: 'terminée' },
{ id: 2, technicien: 'Karim', client: 'Benali', duree: 90, statut: 'terminée' },
{ id: 3, technicien: 'Julie', client: 'Roux', duree: 30, statut: 'en cours' },
{ id: 4, technicien: 'Sofiane', client: 'Petit', duree: 120, statut: 'terminée' },
{ id: 5, technicien: 'Karim', client: 'Garnier', duree: 70, statut: 'en attente' },
{ id: 6, technicien: 'Julie', client: 'Faure', duree: 75, statut: 'terminée' },
];
Exercice 1 : lire du code
Exercice 1 sur 10 Prévoir l'affichage Réussi À revoir
Sans exécuter, donne ce qu'affiche chaque console.log, en justifiant.
const notes = [12, 7, 15, 9, 18];
const resultat = notes.filter((n) => n >= 10).map((n) => n * 2).reduce((a, b) => a + b, 0);
let compteur = 0;
for (const n of notes) {
if (n < 10) continue;
compteur++;
if (compteur === 2) break;
}
console.log(resultat, compteur); // (a)
const f = (x = 2) => (y) => x * y;
console.log(f()(5), f(3)(5), [1, 2, 3].map(f())); // (b)
const o = { a: 1, b: { c: 2 } };
const p = { ...o, a: 9 };
p.b.c = 7;
console.log(o.a, o.b.c, p.a); // (c)
console.log('3' * '4' + '1'); // (d)
Voir la réponseMasquer la réponse de l'exercice 1
(a) 90 2. filter garde 12, 15 et 18 ; map donne 24, 30 et 36 ; reduce les additionne : 90. Dans la boucle : 12 donne compteur = 1, 7 est sauté par continue, 15 donne compteur = 2 et break arrête la boucle.
(b) 10 15 [2, 4, 6]. f renvoie une fonction (une closure qui retient x). f() utilise la valeur par défaut x = 2, donc f()(5) vaut 10. f(3)(5) vaut 15. [1, 2, 3].map(f()) multiplie chaque élément par 2.
(c) 1 7 9. La décomposition { ...o } est une copie superficielle : p.a est indépendant (9, et o.a reste 1), mais p.b et o.b désignent le même sous-objet, donc o.b.c vaut aussi 7.
(d) 121, une chaîne. '3' * '4' convertit en nombres et donne 12 ; 12 + '1' concatène et donne '121'.
Tu avais trouvé ?
Exercice 2 : afficher un tableau de données
Exercice 2 sur 10 Le tableau des interventions Réussi À revoir
La page contient <table><tbody id="interventions"></tbody></table>. Les interventions reçues ont cette forme : { date: '2026-10-28', client: 'Martin', appareil: 'Portable HP', statut: 'terminée' }.
Écris la fonction afficherInterventions(tbody, interventions) qui :
- affiche une ligne par intervention avec, dans l'ordre, la date au format
28/10/2026, le client, l'appareil et le statut ; - trie les lignes de la plus récente à la plus ancienne, sans modifier le tableau reçu ;
- donne à chaque ligne la classe
ok,encoursouattenteselon le statut ; - reste sûre si un nom de client contient du HTML.
Voir la réponseMasquer la réponse de l'exercice 2
const dateCourte = new Intl.DateTimeFormat('fr-FR', { dateStyle: 'short', timeZone: 'UTC' });
const CLASSES = { 'terminée': 'ok', 'en cours': 'encours', 'en attente': 'attente' };
function ligneIntervention(i) {
const tr = document.createElement('tr');
tr.classList.add(CLASSES[i.statut] ?? 'inconnu');
for (const texte of [dateCourte.format(new Date(i.date)), i.client, i.appareil, i.statut]) {
const td = document.createElement('td');
td.textContent = texte;
tr.append(td);
}
return tr;
}
function afficherInterventions(tbody, interventions) {
const triees = interventions.toSorted((a, b) => b.date.localeCompare(a.date));
tbody.replaceChildren(...triees.map(ligneIntervention));
}
Les points qui comptent :
- Le tri : des dates au format
AAAA-MM-JJse trient correctement comme des chaînes ;bavantadonne l'ordre décroissant.toSortedlaisse le tableau reçu intact. - La date :
new Date('2026-10-28')est minuit UTC. Formatée sans précaution, elle pourrait s'afficher la veille dans un fuseau en retard sur UTC (aux Antilles, par exemple). L'optiontimeZone: 'UTC'affiche la date telle qu'elle est écrite. - La sécurité : chaque texte passe par
textContent. Un client nomméRoux <b>VIP</b>s'affiche tel quel, balises comprises, sans être interprété. - L'objet de correspondance
CLASSESremplace unswitch, avec une classeinconnupour un statut imprévu.
Tu avais trouvé ?
Exercice 3 : valider un formulaire
Exercice 3 sur 10 Le formulaire de prise en charge Réussi À revoir
Quand un client dépose un appareil, le technicien remplit un formulaire : nom du client, téléphone, e-mail, numéro de série de l'appareil, description de la panne. Les règles :
- nom : au moins 2 caractères ;
- au moins un moyen de contact : téléphone ou e-mail ;
- téléphone, s'il est saisi : format français, espaces facultatifs (
06 12 34 56 78) ; - e-mail, s'il est saisi : forme
texte@domaine.extension; - numéro de série : 8 à 12 lettres ou chiffres, sans espace ni tiret, la saisie en minuscules étant acceptée ;
- panne : au moins 10 caractères.
Écris la fonction pure validerPriseEnCharge(saisie) qui renvoie un objet { champ: message } (vide si tout est valide), puis donne son résultat pour les deux saisies suivantes.
const saisie1 = { nom: 'R', telephone: '', email: '', serie: 'ABC-123', panne: 'HS' };
const saisie2 = { nom: 'Lucas Girard', telephone: '0612', email: 'lucas@exemple', serie: '5CG1234XYZ', panne: 'Batterie gonflée' };
Voir la réponseMasquer la réponse de l'exercice 3
const TEL = /^0[1-9](\s?\d{2}){4}$/;
const MAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const SERIE = /^[A-Z0-9]{8,12}$/;
function validerPriseEnCharge(s) {
const erreurs = {};
if (s.nom.trim().length < 2) erreurs.nom = 'Nom obligatoire (2 caractères minimum).';
const tel = s.telephone.trim();
const mail = s.email.trim();
if (tel === '' && mail === '') {
erreurs.contact = 'Indique au moins un téléphone ou un e-mail.';
}
if (tel !== '' && !TEL.test(tel)) erreurs.telephone = 'Téléphone au format 06 12 34 56 78.';
if (mail !== '' && !MAIL.test(mail)) erreurs.email = 'Adresse e-mail invalide.';
if (!SERIE.test(s.serie.trim().toUpperCase())) {
erreurs.serie = 'Numéro de série : 8 à 12 lettres ou chiffres.';
}
if (s.panne.trim().length < 10) erreurs.panne = 'Décris la panne en 10 caractères au moins.';
return erreurs;
}
saisie1 : quatre erreurs, nom, contact (ni téléphone ni e-mail), serie (le tiret est interdit et il n'y a que 6 caractères utiles) et panne.
saisie2 : deux erreurs, telephone (0612 est trop court) et email (pas d'extension après le domaine). Le numéro de série et la panne (16 caractères) sont valides.
Les champs facultatifs ne sont testés que s'ils sont remplis ; la règle « au moins un contact » est une règle à part. Le numéro de série est passé en majuscules avant le test, pour accepter la saisie en minuscules sans compliquer le motif. Les deux motifs sont encadrés par ^ et $.
Tu avais trouvé ?
Exercice 4 : appeler une API
Exercice 4 sur 10 Charger les interventions en cours Réussi À revoir
L'API de l'atelier répond à GET /api/interventions?statut=en-cours par un tableau JSON d'objets { id, client, appareil }. La page contient <p id="etat-chargement" role="status"></p> et <ul id="liste-interventions"></ul>.
Écris la fonction async chargerInterventions(url) qui affiche « Chargement des interventions… » pendant la requête, puis :
- la liste (un
lipar intervention, au formatRoux : MacBook Air) et le message « 2 intervention(s) chargée(s). » ; - ou « Aucune intervention en cours. » si le tableau est vide ;
- ou « Impossible de charger les interventions : » suivi de la raison, en cas d'erreur réseau ou de statut HTTP d'erreur.
La requête doit abandonner au bout de 5 secondes.
Voir la réponseMasquer la réponse de l'exercice 4
const zoneEtat = document.querySelector('#etat-chargement');
const liste = document.querySelector('#liste-interventions');
async function chargerInterventions(url) {
zoneEtat.textContent = 'Chargement des interventions…';
liste.replaceChildren();
try {
const reponse = await fetch(url, {
headers: { Accept: 'application/json' },
signal: AbortSignal.timeout(5000),
});
if (!reponse.ok) throw new Error(`le serveur a répondu ${reponse.status}`);
const interventions = await reponse.json();
if (interventions.length === 0) {
zoneEtat.textContent = 'Aucune intervention en cours.';
return;
}
liste.replaceChildren(...interventions.map((i) => {
const li = document.createElement('li');
li.textContent = `${i.client} : ${i.appareil}`;
return li;
}));
zoneEtat.textContent = `${interventions.length} intervention(s) chargée(s).`;
} catch (erreur) {
zoneEtat.textContent = `Impossible de charger les interventions : ${erreur.message}.`;
}
}
chargerInterventions('/api/interventions?statut=en-cours');
Résultats obtenus dans le navigateur : avec deux interventions, « 2 intervention(s) chargée(s). » ; avec une adresse fausse, « Impossible de charger les interventions : le serveur a répondu 404. » ; avec un serveur arrêté, « … : Failed to fetch. » (message de Chrome) ; avec un tableau vide, « Aucune intervention en cours. ».
Les quatre états de l'interface (chargement, erreur, vide, données) sont tous traités, le test de reponse.ok transforme un 404 en erreur, et la zone role="status" annonce chaque état aux lecteurs d'écran.
Tu avais trouvé ?
Exercice 5 : statistiques avec reduce
Exercice 5 sur 10 Le tableau de bord de l'atelier Réussi À revoir
Avec le tableau interventions donné en début de chapitre, écris les expressions qui calculent :
- la durée totale des interventions terminées ;
- un objet qui donne, pour chaque technicien, le total de ses minutes ;
- le technicien qui a le plus de minutes, et son total ;
- la durée moyenne d'une intervention, arrondie à la minute ;
- la liste triée des clients dont l'intervention n'est pas terminée.
Donne la valeur de chaque résultat.
Voir la réponseMasquer la réponse de l'exercice 5
// 1.
interventions
.filter((i) => i.statut === 'terminée')
.reduce((total, i) => total + i.duree, 0);
// 330 (45 + 90 + 120 + 75)
// 2.
const parTechnicien = interventions.reduce((acc, i) => {
acc[i.technicien] = (acc[i.technicien] ?? 0) + i.duree;
return acc;
}, {});
// { Julie: 150, Karim: 160, Sofiane: 120 }
// 3.
const [plusCharge] = Object.entries(parTechnicien).toSorted((a, b) => b[1] - a[1]);
// ['Karim', 160]
// 4.
Math.round(interventions.reduce((total, i) => total + i.duree, 0) / interventions.length);
// 72 (430 / 6 = 71,67)
// 5.
interventions
.filter((i) => i.statut !== 'terminée')
.map((i) => i.client)
.toSorted((a, b) => a.localeCompare(b, 'fr'));
// ['Garnier', 'Roux']
Pour la question 3, Object.entries transforme l'objet en tableau de paires [nom, minutes], qu'on trie par minutes décroissantes ; la déstructuration [plusCharge] garde la première paire. Une boucle for…of qui retient le maximum serait aussi juste. Pour la moyenne, attention à diviser par le nombre de toutes les interventions (6), pas des seules terminées.
Tu avais trouvé ?
Exercice 6 : stockage local
Exercice 6 sur 10 Les pièces favorites Réussi À revoir
Dans le catalogue de pièces, chaque pièce a un bouton étoile qui l'ajoute aux favoris du technicien ou l'en retire. Les favoris doivent être retrouvés à la visite suivante, sous la clé reparinfo.favoris.
- Écris
lireFavoris(), qui renvoie unSetde références, vide si rien n'est enregistré ou si les données sont illisibles. - Écris
enregistrerFavoris(favoris). - Écris l'écouteur délégué de la liste
<ul id="catalogue">, dont chaque bouton portedata-ref, et qui tient à jour l'attributaria-presseddu bouton.
Voir la réponseMasquer la réponse de l'exercice 6
const CLE = 'reparinfo.favoris';
function lireFavoris() {
try {
const donnees = JSON.parse(localStorage.getItem(CLE));
return new Set(Array.isArray(donnees) ? donnees : []);
} catch {
return new Set();
}
}
function enregistrerFavoris(favoris) {
try {
localStorage.setItem(CLE, JSON.stringify([...favoris]));
} catch {
// Stockage indisponible : les favoris marchent pendant la visite, sans être retenus.
}
}
const favoris = lireFavoris();
const catalogue = document.querySelector('#catalogue');
catalogue.addEventListener('click', (event) => {
const b = event.target.closest('button[data-ref]');
if (!b) return;
const { ref } = b.dataset;
if (favoris.has(ref)) favoris.delete(ref);
else favoris.add(ref);
enregistrerFavoris(favoris);
b.setAttribute('aria-pressed', String(favoris.has(ref)));
});
Un Set est le bon outil : une référence ne peut pas être deux fois favorite. Mais JSON ne sait pas représenter un Set (JSON.stringify(new Set(['a'])) donne {}) : on le convertit en tableau avec [...favoris] avant d'enregistrer, et on le reconstruit avec new Set(tableau) à la lecture. JSON.parse(null) (rien d'enregistré) renvoie null, d'où le test Array.isArray ; des données corrompues lèvent une SyntaxError, attrapée par le catch. Le bouton étoile est un bouton bascule : aria-pressed dit aux lecteurs d'écran s'il est actif, et son aria-label (« Favori : SSD NVMe 512 Go ») dit de quelle pièce il s'agit.
Tu avais trouvé ?
Exercice 7 : délégation d'événements
Exercice 7 sur 10 La liste des tâches de l'atelier Réussi À revoir
Le gérant veut une petite liste de tâches : un formulaire <form id="form-tache"> avec un champ name="tache" pour en ajouter une, et une liste <ul id="taches"> où chaque tâche a un bouton « Terminer » (qui devient « Rouvrir » et barre la tâche) et un bouton « Supprimer ».
Écris le code complet : l'état (un tableau de tâches { id, texte, terminee }), la fonction d'affichage, l'écouteur submit (qui ignore une saisie vide) et un seul écouteur pour tous les boutons de la liste. Utilise crypto.randomUUID() pour les identifiants.
Voir la réponseMasquer la réponse de l'exercice 7
let taches = [];
const listeTaches = document.querySelector('#taches');
const formTache = document.querySelector('#form-tache');
function bouton(texte, action, id) {
const b = document.createElement('button');
b.type = 'button';
b.textContent = texte;
b.dataset.action = action;
b.dataset.id = id;
return b;
}
function afficherTaches() {
listeTaches.replaceChildren(...taches.map((t) => {
const li = document.createElement('li');
li.classList.toggle('terminee', t.terminee);
const texte = document.createElement('span');
texte.textContent = t.texte;
li.append(
texte, ' ',
bouton(t.terminee ? 'Rouvrir' : 'Terminer', 'basculer', t.id), ' ',
bouton('Supprimer', 'supprimer', t.id),
);
return li;
}));
}
formTache.addEventListener('submit', (event) => {
event.preventDefault();
const texte = formTache.tache.value.trim();
if (texte === '') return;
taches = [...taches, { id: crypto.randomUUID(), texte, terminee: false }];
formTache.reset();
afficherTaches();
});
listeTaches.addEventListener('click', (event) => {
const b = event.target.closest('button[data-action]');
if (!b) return;
const { action, id } = b.dataset;
if (action === 'supprimer') {
taches = taches.filter((t) => t.id !== id);
} else {
taches = taches.map((t) => (t.id === id ? { ...t, terminee: !t.terminee } : t));
}
afficherTaches();
});
li.terminee span { text-decoration: line-through; }
L'état est la seule source de vérité : chaque action le remplace (filter pour supprimer, map pour basculer) puis réaffiche. L'écouteur délégué fonctionne pour les tâches créées après sa pose. formTache.tache donne le champ dont le name est tache. crypto.randomUUID() produit un identifiant unique ; il n'est disponible que sur une page servie en HTTPS ou sur localhost.
Tu avais trouvé ?
Exercice 8 : trouver les erreurs
Exercice 8 sur 10 Revue de code Réussi À revoir
Un stagiaire a écrit ce code pour afficher les pièces d'une catégorie, triées par prix, à partir de l'API. Il « marche à peu près ». Relève au moins six problèmes (bugs, sécurité, bonnes pratiques), puis réécris la fonction.
var pieces = [];
function afficherCategorie(categorie) {
const reponse = fetch('/api/pieces');
pieces = reponse.json();
var liste = document.querySelector('#liste');
liste.innerHTML = '';
for (var i = 0; i <= pieces.length; i++) {
if (pieces[i].categorie == categorie) {
liste.innerHTML += '<li>' + pieces[i].nom + ' : ' + pieces[i].prix + ' €</li>';
}
}
pieces.sort((a, b) => a.prix > b.prix);
}
Voir la réponseMasquer la réponse de l'exercice 8
awaitmanquant :fetchrenvoie une promesse, etreponse.jsonn'existe pas sur une promesse. La fonction doit êtreasyncet attendre les deux étapes.- Statut non vérifié : une réponse 404 ou 500 serait traitée comme un succès. Il faut tester
reponse.ok. - Aucune gestion d'erreur : pas de
try…catch, l'utilisateur ne saura jamais pourquoi la liste est vide. - Faille XSS : le nom de la pièce est inséré avec
innerHTML. Il fauttextContentetcreateElement. - Boucle d'un tour de trop :
i <= pieces.lengthlitpieces[pieces.length], qui vautundefined, et plante sur.categorie. - Comparaison large
==au lieu de===. - Tri inutile et faux : il est fait après l'affichage, la fonction de comparaison renvoie un booléen au lieu d'un nombre (il faut
a.prix - b.prix), etsortmodifie le tableau global. varet variable globalepiecesmodifiée par la fonction ;innerHTML +=dans une boucle reconstruit la liste à chaque tour ; le prix n'est pas formaté.
const euros = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });
async function afficherCategorie(categorie) {
const liste = document.querySelector('#liste');
try {
const reponse = await fetch('/api/pieces', { signal: AbortSignal.timeout(5000) });
if (!reponse.ok) throw new Error(`Le serveur a répondu ${reponse.status}.`);
const pieces = await reponse.json();
const items = pieces
.filter((p) => p.categorie === categorie)
.toSorted((a, b) => a.prix - b.prix)
.map((p) => {
const li = document.createElement('li');
li.textContent = `${p.nom} : ${euros.format(p.prix)}`;
return li;
});
liste.replaceChildren(...items);
} catch (erreur) {
const li = document.createElement('li');
li.textContent = `Impossible d'afficher les pièces : ${erreur.message}`;
liste.replaceChildren(li);
}
}
Tu avais trouvé ?
Exercice 9 : faire évoluer Gestock comme à l'E6
Exercice 9 sur 10 Le bon de commande fournisseur Réussi À revoir
Le gérant de Réparinfo demande une évolution de Gestock : un bouton « Préparer la commande » qui affiche, sous le tableau, le bon de commande des pièces en alerte. Pour chaque pièce en alerte, la quantité à commander remonte le stock au double du seuil, avec au moins une unité ; le bon affiche la référence, le nom, la quantité à commander et le montant, puis le total.
- Écris dans
stock.jsla fonction purebonDeCommande(pieces)qui renvoie{ lignes, total }. - Donne son résultat sur le stock de départ.
- Écris deux tests Vitest : le stock de départ, et une pièce en rupture dont le seuil vaut 0.
- Écris l'écouteur du bouton, qui affiche le bon dans
<section id="bon-commande">sous forme de liste.
Voir la réponseMasquer la réponse de l'exercice 9
1. La règle métier
export function bonDeCommande(pieces) {
const lignes = pieces
.filter(estEnAlerte)
.map((p) => {
const aCommander = Math.max(1, p.seuil * 2 - p.quantite);
return { ref: p.ref, nom: p.nom, aCommander, montant: aCommander * p.prix };
});
const total = lignes.reduce((t, l) => t + l.montant, 0);
return { lignes, total };
}
filter(estEnAlerte) passe directement la fonction existante, sans la réécrire. Math.max(1, …) traite le cas d'une pièce en rupture dont le seuil vaut 0 : sans lui, on commanderait 0 unité.
2. Sur le stock de départ, deux pièces sont en alerte : RAM-16 (8 − 3 = 5 unités, 5 × 38,50 = 192,50 €) et CHG-65 (4 − 0 = 4 unités, 4 × 24 = 96 €). Le total vaut 288,50 €.
3. Les tests
// piecesDeDepart : le tableau des cinq pièces du chapitre 1, déclaré en haut du fichier de test.
test('commande les pièces en alerte au double du seuil', () => {
const { lignes, total } = bonDeCommande(piecesDeDepart);
expect(lignes.map((l) => [l.ref, l.aCommander])).toEqual([['RAM-16', 5], ['CHG-65', 4]]);
expect(total).toBeCloseTo(288.5);
});
test('commande au moins une unité si le seuil vaut 0', () => {
const { lignes } = bonDeCommande([{ ref: 'VEN-12', nom: 'Ventilateur', quantite: 0, seuil: 0, prix: 7.9 }]);
expect(lignes[0].aCommander).toBe(1);
});
4. L'affichage
const euros = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });
const zoneBon = document.querySelector('#bon-commande');
document.querySelector('#preparer-commande').addEventListener('click', () => {
const { lignes, total } = bonDeCommande(etat.pieces);
const titre = document.createElement('h2');
titre.textContent = 'Bon de commande';
if (lignes.length === 0) {
const p = document.createElement('p');
p.textContent = 'Aucune pièce à commander.';
zoneBon.replaceChildren(titre, p);
return;
}
const ul = document.createElement('ul');
ul.append(...lignes.map((l) => {
const li = document.createElement('li');
li.textContent = `${l.ref} · ${l.nom} · ${l.aCommander} unité(s) · ${euros.format(l.montant)}`;
return li;
}));
const pTotal = document.createElement('p');
pTotal.textContent = `Total : ${euros.format(total)}`;
zoneBon.replaceChildren(titre, ul, pTotal);
});
C'est la démarche attendue à l'E6 : la règle métier d'abord, isolée et testée, puis l'affichage qui l'utilise, en réutilisant ce qui existe (estEnAlerte, etat.pieces, le formateur de prix). Lors de l'entretien, tu peux justifier le Math.max(1, …) par le cas limite qu'il traite et montrer le test qui le prouve.
Tu avais trouvé ?
Exercice 10 : un calcul en direct
Exercice 10 sur 10 Le devis de réparation Réussi À revoir
Un formulaire de devis propose quatre prestations, chacune avec une case à cocher dans <fieldset id="prestations"> : diagnostic (29 € HT), nettoyage (19 €), pose d'un SSD (49 €), réinstallation du système (59 €). Le paragraphe <p id="total-devis" aria-live="polite"></p> doit afficher en permanence Total : 78,00 € HT, TVA 15,60 €, 93,60 € TTC (avec les vraies valeurs), et se mettre à jour dès qu'une case change. Écris le code, avec une fonction pure pour le calcul. Quel total TTC obtient-on avec les quatre prestations ?
Voir la réponseMasquer la réponse de l'exercice 10
<label><input type="checkbox" name="prestation" value="diagnostic"> Diagnostic (29 €)</label>
<!-- … une case par prestation … -->
const PRESTATIONS = { diagnostic: 29, nettoyage: 19, ssd: 49, systeme: 59 };
const euros = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });
function totalDevis(choix, tauxTva = 0.2) {
const ht = choix.reduce((t, c) => t + PRESTATIONS[c], 0);
const tva = Math.round(ht * tauxTva * 100) / 100;
return { ht, tva, ttc: ht + tva };
}
const zonePrestations = document.querySelector('#prestations');
const zoneTotal = document.querySelector('#total-devis');
function majDevis() {
const choix = [...zonePrestations.querySelectorAll('input:checked')].map((c) => c.value);
const { ht, tva, ttc } = totalDevis(choix);
zoneTotal.textContent = `Total : ${euros.format(ht)} HT, TVA ${euros.format(tva)}, ${euros.format(ttc)} TTC`;
}
zonePrestations.addEventListener('change', majDevis);
majDevis();
Un seul écouteur change sur le fieldset suffit : l'événement remonte depuis chaque case. Le sélecteur input:checked ne garde que les cases cochées. Avec diagnostic et SSD : 78 € HT, 15,60 € de TVA, 93,60 € TTC. Avec les quatre prestations : 156 € HT, 31,20 € de TVA, 187,20 € TTC. L'appel final à majDevis() affiche « Total : 0,00 € HT, TVA 0,00 €, 0,00 € TTC » dès le chargement, et aria-live annonce chaque nouveau total.
Tu avais trouvé ?