Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
JavaScript

TP 9 : Gérer un carnet de contacts avec les objets et JSON

Gérer un carnet de contacts avec les objets et JSON. Dans cette leçon du cours JavaScript, vous apprendrez à : représenter des données réelles par des objets et des tableaux d'objets ; transformer des...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Gérer un carnet de contacts avec les objets et JSON. Dans cette leçon du cours JavaScript, vous apprendrez à : représenter des données réelles par des objets et des tableaux d'objets ; transformer des données sans les modifier (spread, map, filter) ; utiliser un dictionnaire, un Set et le format JSON ; valider des données et gérer les erreurs d'import.

Chapitre 9 — Les objets littéraux, JSON et les collections · TP 9 : Gérer un carnet de contacts avec les objets et JSON

🎯 Objectifs de la leçon
• représenter des données réelles par des objets et des tableaux d'objets ;
• transformer des données sans les modifier (spread, map, filter) ;
• utiliser un dictionnaire, un Set et le format JSON ;
• valider des données et gérer les erreurs d'import.

Présentation

Vous allez écrire le « moteur » d'un carnet de contacts: création et vérification des contacts, recherche, filtre par catégorie, favoris, statistiques par ville, tri, et échange des données au format JSON. L'interface (formulaire, liste, boutons) est fournie dans app.js, car la manipulation de la page n'est vue qu'au chapitre 12 : vous écrivez uniquement les fonctions de contacts.js, qui reçoivent des données et renvoient des données. Une page de 21 tests automatiques vous indique ce qui fonctionne. Les questions vous guident ; aucune solution n'est donnée.

Le carnet au départ, vide
Figure 1 — Au départ, le carnet est vide et les tests sont tous rouges.

Le carnet terminé
Figure 2 — L'objectif : contacts triés (favori en tête), erreurs de saisie, statistiques.

L'export JSON
Figure 3 — L'export du carnet au format JSON.

Fichiers de départ

⬇ Télécharger les fichiers de départ du TP (tp9-depart.zip)

Téléchargez le dossier de départ (bouton ci-dessous). Un contact est un objet { id, nom, telephone, email, ville, categorie, favori }. Lisez tests.js: chaque test est un exemple précis du résultat attendu.

contacts.js (à compléter)
// =====================================================================
// TP 9 — Carnet de contacts : VOS FONCTIONS (les données uniquement)
// Un contact est un objet : { id, nom, telephone, email, ville, categorie, favori }
// Ces fonctions ne touchent pas à la page : elles reçoivent des données et
// renvoient des données. L'affichage (app.js) et les tests (tests.js) sont fournis.
// =====================================================================

// 1. Créer un contact propre à partir des données saisies.
//    - nom : espaces inutiles retirés, chaque mot avec une majuscule ("amina  tazi" → "Amina Tazi")
//    - telephone : tous les espaces retirés ; email : en minuscules, sans espaces autour
//    - ville : espaces retirés autour ; categorie : "amis" par défaut ; favori : false
function creerContact(donnees, id) {
  // À COMPLÉTER
}

// 2. Vérifier un contact : renvoie un TABLEAU de messages d'erreur (vide si tout va bien)
//    - "Le nom doit contenir au moins 2 caractères."
//    - "Le téléphone doit contenir 10 chiffres et commencer par 0."
//    - "L'e-mail n'est pas valide." (seulement s'il est renseigné et ne contient pas @)
function verifierContact(contact) {
  // À COMPLÉTER
}

// 3. Rechercher : contacts dont le nom, la ville ou l'e-mail contient le texte (sans tenir compte des majuscules)
function rechercher(contacts, texte) {
  // À COMPLÉTER
}

// 4. Filtrer par catégorie ("toutes" renvoie tous les contacts)
function filtrerParCategorie(contacts, categorie) {
  // À COMPLÉTER
}

// 5. Basculer le favori du contact d'identifiant id.
//    Renvoie un NOUVEAU tableau : le tableau et les contacts d'origine ne doivent pas être modifiés.
function basculerFavori(contacts, id) {
  // À COMPLÉTER
}

// 6. Compter les contacts par ville : { Rabat: 2, Fès: 1, … }
function compterParVille(contacts) {
  // À COMPLÉTER
}

// 7. Liste des villes distinctes, triées par ordre alphabétique
function villesDistinctes(contacts) {
  // À COMPLÉTER
}

// 8. Trier par nom (ordre alphabétique français), favoris en premier ; sans modifier le tableau reçu
function trier(contacts) {
  // À COMPLÉTER
}

// 9. Exporter en texte JSON lisible (indentation de 2 espaces)
function exporterJSON(contacts) {
  // À COMPLÉTER
}

// 10. Importer un texte JSON : renvoie un tableau de contacts recréés avec creerContact
//     (identifiants 1, 2, 3…) ; lance une Error("JSON invalide") si le texte n'est pas
//     du JSON ou n'est pas un tableau.
function importerJSON(texte) {
  // À COMPLÉTER
}
tests.js (fourni, à lire)
// =====================================================================
// Tests automatiques (fourni — ne pas modifier)
// =====================================================================
(function () {
  const liste = document.querySelector("#tests");
  let reussis = 0, total = 0;
  function test(description, calcul, attendu) {
    total++;
    const li = document.createElement("li");
    let obtenu;
    try { obtenu = calcul(); } catch (e) { obtenu = "Erreur : " + e.message; }
    const ok = JSON.stringify(obtenu) === JSON.stringify(attendu);
    if (ok) reussis++;
    li.className = ok ? "ok" : "ko";
    li.textContent = (ok ? "✓ " : "✗ ") + description;
    if (!ok) {
      const d = document.createElement("small");
      d.textContent = `attendu ${JSON.stringify(attendu)}, obtenu ${JSON.stringify(obtenu)}`;
      li.append(d);
    }
    liste.append(li);
  }
  const c = (nom, ville, categorie = "amis", favori = false, id = 1) =>
    ({ id, nom, telephone: "0600000000", email: "", ville, categorie, favori });
  const base = [c("Salma Idrissi", "Rabat", "travail", false, 1), c("Karim Benali", "Fès", "amis", true, 2), c("Élodie Martin", "Rabat", "famille", false, 3)];

  test("creerContact nettoie le nom", () => creerContact({ nom: "  amina   TAZI ", telephone: "06 12 34 56 78", email: " Amina@ISTA.ma ", ville: " Casablanca " }, 7).nom, "Amina Tazi");
  test("creerContact : objet complet", () => creerContact({ nom: "amina tazi", telephone: "06 12 34 56 78", email: " Amina@ISTA.ma ", ville: " Casablanca " }, 7),
    { id: 7, nom: "Amina Tazi", telephone: "0612345678", email: "amina@ista.ma", ville: "Casablanca", categorie: "amis", favori: false });
  test("creerContact garde la catégorie choisie", () => creerContact({ nom: "a b", telephone: "", email: "", ville: "", categorie: "travail" }, 1).categorie, "travail");
  test("verifierContact : contact correct → []", () => verifierContact(c("Salma Idrissi", "Rabat")), []);
  test("verifierContact : nom trop court et téléphone faux", () => verifierContact({ ...c("A", "Rabat"), telephone: "12345" }).length, 2);
  test("verifierContact : e-mail sans @", () => verifierContact({ ...c("Salma", "Rabat"), email: "salma.ista.ma" }), ["L'e-mail n'est pas valide."]);
  test("rechercher(\"rab\") trouve les 2 contacts de Rabat", () => rechercher(base, "rab").map((x) => x.id), [1, 3]);
  test("rechercher(\"KARIM\") ignore les majuscules", () => rechercher(base, "KARIM").length, 1);
  test("rechercher(\"\") renvoie tout", () => rechercher(base, "").length, 3);
  test("filtrerParCategorie(\"famille\")", () => filtrerParCategorie(base, "famille").map((x) => x.nom), ["Élodie Martin"]);
  test("filtrerParCategorie(\"toutes\")", () => filtrerParCategorie(base, "toutes").length, 3);
  test("basculerFavori : le contact 1 devient favori", () => basculerFavori(base, 1).find((x) => x.id === 1).favori, true);
  test("basculerFavori ne modifie pas l'original", () => { const r = basculerFavori(base, 1); return [base[0].favori, r !== base, r[1] === base[1]]; }, [false, true, true]);
  test("compterParVille", () => compterParVille(base), { Rabat: 2, "Fès": 1 });
  test("villesDistinctes triées", () => villesDistinctes(base), ["Fès", "Rabat"]);
  test("trier : favori d'abord, puis Élodie avant Salma", () => trier(base).map((x) => x.nom), ["Karim Benali", "Élodie Martin", "Salma Idrissi"]);
  test("trier ne modifie pas le tableau reçu", () => { const r = trier(base); return [Array.isArray(r), base.map((x) => x.id)]; }, [true, [1, 2, 3]]);
  test("exporterJSON produit un texte indenté", () => exporterJSON([base[0]]).split("\n")[2], '    "id": 1,');
  test("importerJSON relit un export", () => importerJSON(exporterJSON(base)).map((x) => [x.id, x.nom]), [[1, "Salma Idrissi"], [2, "Karim Benali"], [3, "Élodie Martin"]]);
  test("importerJSON refuse un texte invalide", () => { try { importerJSON("{nom: 'x'}"); return "aucune erreur"; } catch (e) { return e.message; } }, "JSON invalide");
  test("importerJSON refuse un JSON qui n'est pas un tableau", () => { try { importerJSON('{"nom": "x"}'); return "aucune erreur"; } catch (e) { return e.message; } }, "JSON invalide");

  const score = document.querySelector("#score");
  score.textContent = `${reussis} / ${total}`;
  score.classList.toggle("complet", reussis === total);
})();
app.js (fourni)
// =====================================================================
// Interface du carnet (fourni — ne pas modifier). Utilise vos fonctions.
// =====================================================================
const $ = (s) => document.querySelector(s);
let prochainId = 1;
let contacts = [
  { nom: "salma idrissi", telephone: "06 61 22 33 44", email: "salma@ista.ma", ville: "Rabat", categorie: "travail" },
  { nom: "KARIM benali", telephone: "0670112233", email: "", ville: "Fès", categorie: "amis" },
  { nom: "élodie martin", telephone: "0522 33 44 55", email: "Elodie.Martin@mail.fr", ville: "Rabat", categorie: "famille" },
  { nom: "yassine alaoui", telephone: "0655443322", email: "yassine@ista.ma", ville: "Casablanca", categorie: "travail" },
].map((d) => creerContact(d, prochainId++)).filter(Boolean);

function afficher() {
  let visibles = filtrerParCategorie(contacts, $("#filtre").value) ?? contacts;
  visibles = rechercher(visibles, $("#recherche").value) ?? visibles;
  visibles = trier(visibles) ?? visibles;

  const ul = $("#liste");
  ul.replaceChildren();
  for (const ct of visibles) {
    const li = document.createElement("li");
    const fav = document.createElement("button");
    fav.className = "favori" + (ct.favori ? " oui" : "");
    fav.textContent = "★";
    fav.title = "Favori";
    fav.addEventListener("click", () => { contacts = basculerFavori(contacts, ct.id) ?? contacts; afficher(); });
    const infos = document.createElement("div");
    const nom = document.createElement("div"); nom.className = "nom"; nom.textContent = ct.nom;
    const details = document.createElement("div"); details.className = "details";
    details.textContent = [ct.telephone, ct.email, ct.ville].filter(Boolean).join(" · ");
    infos.append(nom, details);
    const cat = document.createElement("span"); cat.className = "categorie " + ct.categorie; cat.textContent = ct.categorie;
    const suppr = document.createElement("button"); suppr.className = "supprimer"; suppr.textContent = "×"; suppr.title = "Supprimer";
    suppr.addEventListener("click", () => { contacts = contacts.filter((x) => x.id !== ct.id); afficher(); });
    li.append(fav, infos, cat, suppr);
    ul.append(li);
  }
  $("#compteur").textContent = `${visibles.length} contact(s) affiché(s) sur ${contacts.length}`;

  const villes = villesDistinctes(contacts);
  $("#villes").textContent = villes ? `${villes.length} ville(s) : ${villes.join(", ")}` : "";
  $("#parVille").replaceChildren(...Object.entries(compterParVille(contacts) ?? {}).map(([v, n]) => {
    const li = document.createElement("li"); li.textContent = `${v} : ${n}`; return li;
  }));
}

$("#formulaire").addEventListener("submit", (ev) => {
  ev.preventDefault();
  const nouveau = creerContact({ nom: $("#nom").value, telephone: $("#telephone").value, email: $("#email").value, ville: $("#ville").value, categorie: $("#categorie").value }, prochainId);
  const erreurs = nouveau ? verifierContact(nouveau) ?? [] : ["creerContact n'est pas encore écrite."];
  $("#erreurs").replaceChildren(...erreurs.map((e) => { const li = document.createElement("li"); li.textContent = e; return li; }));
  if (erreurs.length > 0) return;
  contacts = [...contacts, nouveau];
  prochainId++;
  $("#formulaire").reset();
  afficher();
});
$("#recherche").addEventListener("input", afficher);
$("#filtre").addEventListener("change", afficher);
$("#exporter").addEventListener("click", () => {
  $("#json").value = exporterJSON(contacts) ?? "";
  $("#messageJson").textContent = "Carnet exporté.";
});
$("#importer").addEventListener("click", () => {
  try {
    const importes = importerJSON($("#json").value);
    if (!Array.isArray(importes)) throw new Error("importerJSON n'est pas encore écrite.");
    contacts = importes;
    prochainId = contacts.length + 1;
    $("#messageJson").textContent = `${contacts.length} contact(s) importé(s).`;
  } catch (e) {
    $("#messageJson").textContent = "Import impossible : " + e.message;
  }
  afficher();
});
afficher();

🛠 TP · Partie 1 — Créer et vérifier un contact

Mission : Complétez creerContact et verifierContact (tests 1 à 6).

Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :

  1. Dans creerContact, comment récupérez-vous les champs de donnees en une ligne, avec "amis" comme catégorie par défaut ?
  2. Pour transformer «  amina   TAZI » en « Amina Tazi », découpez le travail : retirer les espaces autour, découper en mots, mettre chaque mot en forme, recoller. Quelles méthodes des chaînes et des tableaux utilisez-vous ?
  3. Comment retirez-vous tous les espaces du téléphone, et pas seulement ceux du début et de la fin ?
  4. verifierContact renvoie un tableau de messages. Pourquoi un tableau plutôt qu'un simple true/false?
  5. Comment vérifiez-vous qu'un téléphone contient exactement 10 chiffres et commence par 0 ? (une expression régulière, leçon 5.4, ou une boucle)

Je vérifie / je rends :

  • ☐ Tests 1 à 6 au vert ; les 4 contacts de départ s'affichent avec des noms propres.

🛠 TP · Partie 2 — Rechercher, filtrer, mettre en favori

Mission : Complétez rechercher, filtrerParCategorie et basculerFavori (tests 7 à 13).

Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :

  1. Pour la recherche, comment comparez-vous sans tenir compte des majuscules ? Comment testez-vous les trois champs (nom, ville, e-mail) sans écrire trois conditions ? (indice : some)
  2. Que doit renvoyer rechercher pour un texte vide ? Vérifiez que votre code le fait sans cas particulier.
  3. basculerFavori ne doit modifier ni le tableau ni les objets reçus. Pourquoi contacts.find(…).favori = true est-il interdit ici ? Quelle combinaison de map et de spread utilisez-vous ?
  4. Le test 13 vérifie aussi que les contacts non modifiés sont les mêmes objets qu'avant. Pourquoi est-ce une bonne chose (mémoire, comparaison rapide) ?

Je vérifie / je rends :

  • ☐ Tests 7 à 13 au vert ; la recherche, le filtre et l'étoile fonctionnent dans la page.

🛠 TP · Partie 3 — Statistiques et tri

Mission : Complétez compterParVille, villesDistinctes et trier (tests 14 à 17).

Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :

  1. compterParVille est un dictionnaire de comptage (leçon 9.2). Comment ignorez-vous un contact sans ville ?
  2. Pour les villes distinctes, quelle collection supprime les doublons ? Comment la transformez-vous en tableau trié ?
  3. trier place les favoris en premier, puis trie par nom. Écrivez la fonction de comparaison en deux étapes. Comment soustraire deux booléens donne-t-il un nombre utilisable ?
  4. Pourquoi « Élodie » doit-elle être placée avant « Salma » ? Quelle méthode des chaînes compare en tenant compte des accents ?
  5. sort modifie le tableau sur place : comment trier une copie ?

Je vérifie / je rends :

  • ☐ Tests 14 à 17 au vert ; les statistiques s'affichent.

🛠 TP · Partie 4 — Échanger en JSON

Mission : Complétez exporterJSON et importerJSON (tests 18 à 21).

Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :

  1. Comment obtenez-vous un JSON indenté de 2 espaces ?
  2. Dans importerJSON, quelles sont les deux situations d'erreur à détecter ? Comment transformez-vous la SyntaxError de JSON.parse en Error("JSON invalide")?
  3. Pourquoi repasser chaque contact importé dans creerContact plutôt que de garder les objets tels quels ? Pensez à un fichier modifié à la main.
  4. Le favori doit être conservé à l'import, mais creerContact le remet à false. Comment le restaurez-vous sans modifier creerContact?
  5. Exportez votre carnet, modifiez le JSON à la main dans la zone de texte (ajoutez un contact), puis importez-le. Introduisez ensuite une virgule en trop : que se passe-t-il ?

Je vérifie / je rends :

  • ☐ 21 / 21 ; export et import fonctionnent dans la page.

🛠 TP · Partie 5 — Bonus

Mission : Allez plus loin.

Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :

  1. Rendez la recherche insensible aux accents : « elodie » doit trouver « Élodie ». (indice : normalize("NFD") et une expression régulière)
  2. Refusez l'ajout d'un contact dont le téléphone existe déjà, avec un Set des téléphones.
  3. Ajoutez une fonction exporterCSV(contacts) qui produit une ligne par contact, séparateur « ; », avec une ligne d'en-tête.
  4. Au chapitre 20, vous enregistrerez le carnet dans le localStorage: quelles deux fonctions de ce TP réutiliserez-vous ?

Je vérifie / je rends :

  • ☐ Les bonus réalisés, décrits dans le compte rendu.

Scénarios de recette

Le corrigé de l'enseignant réussit tous ces scénarios :

  1. La page de tests affiche 21 / 21.
  2. Au chargement : 4 contacts aux noms nettoyés, triés : Élodie Martin, Karim Benali, Salma Idrissi, Yassine Alaoui ; « 3 ville(s) : Casablanca, Fès, Rabat » ; « Rabat : 2 ».
  3. Recherche « rab » : 2 contacts ; filtre « Travail » : 2 contacts.
  4. Étoile sur Yassine : il passe en tête de liste.
  5. Saisie « x » / « 123 » : deux messages d'erreur, aucun contact ajouté.
  6. Saisie «  nadia   el amrani », « 06 11 22 33 44 », Tanger, Famille : « Nadia El Amrani » est ajoutée, 4 villes.
  7. Export : le JSON indenté contient "nom": "Nadia El Amrani".
  8. Import de {pas du json}: « Import impossible : JSON invalide », le carnet est intact.
  9. Import d'un tableau d'un contact favori : 1 contact affiché, avec son étoile.

Grille d'évaluation indicative

CritèrePoints
Partie 1 — création (déstructuration, nettoyage) et vérification5
Partie 2 — recherche, filtre, favori sans modifier les données4
Partie 3 — dictionnaire, Set et tri4
Partie 4 — export et import JSON avec gestion des erreurs4
Qualité du code et compte rendu (scénarios de recette)3
Total (bonus : jusqu'à +3)20

← Les collections Map et Set