Stephen Build ← Roadmap
0/0
Roadmap des Builders · Étape 4 sur 7

Le JavaScript l'interaction

Le JavaScript rend ton site vivant : un menu qui s'ouvre, une liste qui se souvient de tes tâches, des données qui arrivent d'internet. C'est l'étape la plus longue de la roadmap, c'est normal. Prends ton temps, chaque mission compte.

Durée 3 à 4 semaines Niveau étapes 1 à 3 faites Projet une to-do list, puis une appli météo Mis à jour

À la fin, tu sauras

Les missions

Les missions 0 à 4 se font sur ton portfolio, les suivantes dans deux nouveaux dossiers. Tape le code au lieu de le copier, et garde la console ouverte.

Mission 0

Le JavaScript dit ce que la page fait : réagir à un clic, afficher des données, retenir des infos. Dans ton dossier mon-site, crée un script.js à côté de index.html et branche-le dans le <head> :

index.html
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Alex, apprenti dev</title>
  <link rel="stylesheet" href="style.css">
  <script src="script.js" defer></script>
</head>
script.js
console.log("Le script est bien branché !");
console.log(2 + 2);

Ouvre la page avec Live Server, puis les outils de développement (F12, ou ⌘ + Option + I sur Mac), onglet Console.

  • defer : lance le script une fois tout le HTML lu. Sans lui, ton code chercherait des éléments qui n'existent pas encore.
  • console.log(...) : affiche une valeur dans la console. Tes erreurs s'y affichent aussi, en rouge, avec le numéro de ligne.
Erreur fréquente

Rien dans la console ? Vérifie le nom du fichier (script.js, pas Script.js), qu'il est à côté de index.html, et que t'as sauvegardé.

Résultat attendu
Console
Le script est bien branché !
4

Les deux lignes apparaissent dans l'onglet Console, sans rien en rouge.

Mission 1

Une variable range une valeur sous un nom. Une condition choisit quoi faire, une boucle répète, une fonction range du code pour le réutiliser. Efface script.js et tape :

script.js
const prenom = "Alex";
let age = 27;
const aimeLeCafe = true;

// Les backticks permettent de glisser des variables dans le texte
console.log(`Salut, moi c'est ${prenom} et j'ai ${age} ans.`);

age = age + 1;     // OK : age est un let
// prenom = "Sam"; // Erreur : prenom est un const

const heure = new Date().getHours();

if (heure < 12) {
  console.log("Bonjour !");
} else if (heure < 18) {
  console.log("Bon après-midi !");
} else {
  console.log("Bonsoir !");
}

if (aimeLeCafe && age >= 18) {
  console.log("Un café, Alex ?");
}

// Boucle sur une liste : un tour par élément
const outils = ["VS Code", "Chrome", "Live Server"];
for (const outil of outils) {
  console.log(`J'utilise ${outil}`);
}

// Une fonction : un bout de code avec un nom, qu'on réutilise
function direBonjour(nom) {
  return `Bonjour ${nom} !`;
}

console.log(direBonjour("Alex"));

// La même idée, écrite en fonction fléchée
const double = (nombre) => nombre * 2;
console.log(double(21)); // 42
  • const : ne se réassigne pas. let : peut changer. Prends const par défaut, et oublie var.
  • `...${prenom}...` : un texte entre backticks, où tu glisses des variables.
  • if / else : exécute un bloc si la condition est vraie. && veut dire « et », || « ou ».
  • for (const outil of outils) : un tour par élément. return renvoie le résultat d'une fonction, => l'écrit en plus court.
Erreur fréquente

Comparer avec == : '5' == 5 vaut true. Prends toujours === (et !==), qui compare aussi le type.

Résultat attendu
Console
Salut, moi c'est Alex et j'ai 27 ans.
Bon après-midi !
Un café, Alex ?
J'utilise VS Code
J'utilise Chrome
J'utilise Live Server
Bonjour Alex !
42

La 2e ligne dépend de l'heure : « Bonjour ! » avant midi, « Bon après-midi ! » entre midi et 18 h, « Bonsoir ! » après.

Mission 2

Un tableau est une liste dans l'ordre, un objet une fiche avec des propriétés nommées. Tu vas surtout manipuler des tableaux d'objets.

script.js
// Un objet : une fiche avec des propriétés
const projet = {
  titre: "Site de la boulangerie",
  annee: 2026,
  enLigne: false,
};

console.log(projet.titre);
projet.enLigne = true;

// Un tableau d'objets : une liste de fiches
const projets = [
  { titre: "Page Qui suis-je", enLigne: true },
  { titre: "Portfolio", enLigne: true },
  { titre: "To-do list", enLigne: false },
];

console.log(projets.length);    // 3
console.log(projets[0].titre);  // "Page Qui suis-je" : on compte à partir de 0

projets.push({ titre: "Appli météo", enLigne: false });

// forEach : faire quelque chose pour chaque élément
projets.forEach((p) => console.log(p.titre));

// map : fabriquer un nouveau tableau en transformant chaque élément
const titres = projets.map((p) => p.titre);

// filter : garder seulement les éléments qui passent le test
const enLigne = projets.filter((p) => p.enLigne);

console.log(titres);
console.log(enLigne);
  • projet.titre : lit une propriété de l'objet.
  • .length : le nombre d'éléments. [0] : le premier.
  • .push(...) : ajoute à la fin. Ça marche sur un const : il empêche de remplacer la boîte, pas de changer son contenu.
  • forEach agit sur chaque élément, map les transforme, filter les trie.
Demande à l'IA
Explique-moi la différence entre forEach, map et filter en JavaScript, avec un exemple simple pour chacun. Dis-moi quand utiliser l'un plutôt que l'autre.
Résultat attendu
Console
Site de la boulangerie
3
Page Qui suis-je
Page Qui suis-je
Portfolio
To-do list
Appli météo
(4) ['Page Qui suis-je', 'Portfolio', 'To-do list', 'Appli météo']
(2) [{titre: 'Page Qui suis-je', enLigne: true}, {titre: 'Portfolio', enLigne: true}]

« Page Qui suis-je » apparaît deux fois : avec projets[0].titre, puis dans le forEach.

Mission 3

Le DOM, c'est ta page vue par le JavaScript : un arbre d'éléments que tu peux lire et modifier. Ajoute une liste vide dans ta section à propos :

index.html
<section id="apropos">
  <h2>À propos</h2>
  <p>J'apprends à coder des sites.</p>
  <ul id="competences"></ul>
</section>

Puis remplace le contenu de script.js :

script.js
// 1. Attraper un élément de la page
const titre = document.querySelector("h1");
console.log(titre.textContent);

// 2. Changer son texte et ses classes
titre.textContent = "Alex, apprenti dev";
titre.classList.add("surligne");

// 3. Créer des éléments et les ajouter à la page
const competences = ["HTML", "CSS", "Responsive", "JavaScript"];
const liste = document.querySelector("#competences");

competences.forEach((competence) => {
  const li = document.createElement("li");
  li.textContent = competence;
  liste.append(li);
});
style.css
.surligne {
  color: tomato;
}
  • querySelector("h1") : renvoie le premier élément qui correspond, avec les sélecteurs du CSS (#id, .classe).
  • .textContent : lit ou remplace le texte d'un élément.
  • .classList.add(...) : ajoute une classe. Le JS change les classes, le CSS fait le style.
  • createElement("li") crée un élément, .append(...) le place dans la page.
Erreur fréquente

Cannot read properties of null : ton querySelector n'a rien trouvé. Vérifie le sélecteur (# ou .) et le defer.

Résultat attendu
Console
Salut, moi c'est Alex
index.html

La console affiche l'ancien titre, la page le nouveau, en tomate, avec les 4 compétences sous « À propos ».

Mission 4

Un événement, c'est un clic, une touche, un formulaire envoyé. Tu dis au navigateur : « quand ça arrive, lance cette fonction ». On s'en sert pour un menu qui s'ouvre sur téléphone. Adapte ton <header> (garde tes liens) :

index.html
<header>
  <p class="logo">Alex</p>
  <button class="menu-btn" type="button" aria-expanded="false" aria-controls="menu">Menu</button>
  <nav id="menu" class="menu">
    <ul>
      <li><a href="#apropos">À propos</a></li>
      <li><a href="#projets">Projets</a></li>
      <li><a href="#contact">Contact</a></li>
    </ul>
  </nav>
</header>

Ajoute ces règles à la fin de ton CSS (adapte les classes si t'avais déjà un menu) :

style.css
/* Mobile d'abord : menu caché, bouton visible */
.menu {
  display: none;
}

.menu.ouvert {
  display: block;
}

/* Écran large : menu toujours visible, bouton caché */
@media (min-width: 600px) {
  .menu-btn {
    display: none;
  }

  .menu {
    display: block;
  }
}

Et ceci à la fin de script.js :

script.js
const menuBtn = document.querySelector(".menu-btn");
const menu = document.querySelector("#menu");

menuBtn.addEventListener("click", () => {
  const ouvert = menu.classList.toggle("ouvert");
  menuBtn.setAttribute("aria-expanded", ouvert);
});
  • addEventListener("click", ...) : la fonction s'exécute à chaque clic.
  • classList.toggle("ouvert") : ajoute la classe ou l'enlève, et renvoie true si elle vient d'être ajoutée.
  • aria-expanded : dit aux lecteurs d'écran si le menu est ouvert.
À toi

Fais refermer le menu au clic sur un lien. Indice : menu.querySelectorAll('a'), une boucle, classList.remove, et aria-expanded à false.

Résultat attendu
index.html

En largeur téléphone, le bouton « Menu » ouvre et ferme les 3 liens. En écran large, il disparaît.

Mission 5

Premier projet. Crée un dossier todo avec index.html, style.css (vide pour l'instant) et script.js.

index.html
<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Ma to-do list</title>
    <link rel="stylesheet" href="style.css">
    <script src="script.js" defer></script>
  </head>
  <body>
    <main>
      <h1>À faire</h1>
      <form id="todo-form">
        <label for="todo-input">Nouvelle tâche</label>
        <input id="todo-input" type="text" placeholder="Ex : finir la mission 6" autocomplete="off">
        <button type="submit">Ajouter</button>
      </form>
      <p id="todo-error" class="error" hidden></p>
      <ul id="todo-list"></ul>
    </main>
  </body>
</html>
script.js
const form = document.querySelector("#todo-form");
const input = document.querySelector("#todo-input");
const list = document.querySelector("#todo-list");
const errorMsg = document.querySelector("#todo-error");

let tasks = [];

// Redessine toute la liste à partir du tableau tasks
function render() {
  list.textContent = "";
  for (const task of tasks) {
    const li = document.createElement("li");
    li.textContent = task.text;
    list.append(li);
  }
}

form.addEventListener("submit", (event) => {
  event.preventDefault();

  const text = input.value.trim();
  if (text === "") {
    errorMsg.textContent = "Écris une tâche avant de l'ajouter.";
    errorMsg.hidden = false;
    return;
  }

  errorMsg.hidden = true;
  tasks.push({ id: Date.now(), text: text, done: false });
  input.value = "";
  input.focus();
  render();
});
  • tasks : la source de vérité. On modifie le tableau, puis render() redessine toute la liste.
  • "submit" : marche avec le bouton et la touche Entrée.
  • .trim() : enlève les espaces autour, pour refuser une tâche vide.
  • Date.now() : un nombre qui change à chaque milliseconde, pratique comme identifiant unique.
Erreur fréquente

La tâche apparaît puis disparaît ? T'as oublié event.preventDefault() : la page se recharge et repart de zéro.

Résultat attendu
todo/index.html

Essaie dans l'aperçu : chaque tâche s'ajoute en puce, avec le bouton ou Entrée. Un champ vide affiche le message d'erreur.

Mission 6

Recharge la page : tes tâches ont disparu. Pour les garder, on utilise localStorage, un petit stockage dans le navigateur. On ajoute aussi une case à cocher et un bouton supprimer. Voici le script.js complet :

script.js
const form = document.querySelector("#todo-form");
const input = document.querySelector("#todo-input");
const list = document.querySelector("#todo-list");
const errorMsg = document.querySelector("#todo-error");

const STORAGE_KEY = "todo-tasks";

function save() {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}

function load() {
  const saved = localStorage.getItem(STORAGE_KEY);
  return saved ? JSON.parse(saved) : [];
}

let tasks = load();

function render() {
  list.textContent = "";

  for (const task of tasks) {
    const li = document.createElement("li");

    // La case à cocher et le texte, dans un label cliquable
    const label = document.createElement("label");
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.checked = task.done;
    checkbox.addEventListener("change", () => {
      task.done = checkbox.checked;
      save();
      render();
    });

    const text = document.createElement("span");
    text.textContent = task.text;
    if (task.done) {
      text.classList.add("done");
    }
    label.append(checkbox, text);

    // Le bouton supprimer
    const del = document.createElement("button");
    del.type = "button";
    del.textContent = "Supprimer";
    del.addEventListener("click", () => {
      tasks = tasks.filter((t) => t.id !== task.id);
      save();
      render();
    });

    li.append(label, del);
    list.append(li);
  }
}

form.addEventListener("submit", (event) => {
  event.preventDefault();

  const text = input.value.trim();
  if (text === "") {
    errorMsg.textContent = "Écris une tâche avant de l'ajouter.";
    errorMsg.hidden = false;
    return;
  }

  errorMsg.hidden = true;
  tasks.push({ id: Date.now(), text: text, done: false });
  save();
  input.value = "";
  input.focus();
  render();
});

render();
style.css
.done {
  text-decoration: line-through;
  opacity: 0.6;
}

.error {
  color: crimson;
}
  • localStorage.setItem et getItem : écrire et lire une valeur qui survit au rechargement.
  • JSON.stringify : transforme le tableau en texte, seul format accepté. JSON.parse fait l'inverse.
  • La case met à jour task.done, le bouton retire la tâche avec filter.
  • Le schéma ne change jamais : modifier tasks, save(), render().
Demande à l'IA
Explique-moi pourquoi on doit utiliser JSON.stringify et JSON.parse avec localStorage en JavaScript, avec un exemple de ce qui se passe si on les oublie.
Résultat attendu
todo/index.html
Outils de développement > Application > Local Storage
Key          Value
todo-tasks   [{"id":1759755600000,"text":"finir la mission 6","done":true},{"id":1759755660000,"text":"Relire le cours sur fetch","done":false}]

Après un rechargement, les tâches sont toujours là et la tâche cochée est barrée. Dans Local Storage, une clé todo-tasks contient ton tableau en texte (tes id seront différents).

Mission 7

Une API te renvoie des données quand tu appelles son adresse. Open-Meteo donne la météo, sans inscription ni clé. Crée un dossier meteo avec index.html et script.js, et teste l'appel seul :

script.js
const URL_METEO = "https://api.open-meteo.com/v1/forecast?latitude=48.85&longitude=2.35&current=temperature_2m,wind_speed_10m";

async function test() {
  const response = await fetch(URL_METEO);
  const data = await response.json();
  console.log(data);
  console.log(data.current.temperature_2m);
}

test();

Déplie l'objet dans la console : la température est dans current. Maintenant, affiche-la dans la page. Remplace tout :

index.html
<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Ma météo</title>
    <script src="script.js" defer></script>
  </head>
  <body>
    <main>
      <h1>Météo</h1>
      <p id="status"></p>
      <div id="result" hidden>
        <h2 id="place"></h2>
        <p>Température : <strong id="temp"></strong></p>
        <p>Vent : <strong id="wind"></strong></p>
      </div>
    </main>
  </body>
</html>
script.js
const statusEl = document.querySelector("#status");
const result = document.querySelector("#result");
const placeEl = document.querySelector("#place");
const tempEl = document.querySelector("#temp");
const windEl = document.querySelector("#wind");

// Demande la météo actuelle à Open-Meteo pour des coordonnées
async function getWeather(latitude, longitude) {
  const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,wind_speed_10m`;
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Erreur HTTP ${response.status}`);
  }
  const data = await response.json();
  return data.current;
}

// Gère l'affichage : chargement, résultat ou erreur
async function showWeather(latitude, longitude, name) {
  statusEl.textContent = "Chargement...";
  result.hidden = true;

  try {
    const current = await getWeather(latitude, longitude);
    placeEl.textContent = name;
    tempEl.textContent = `${current.temperature_2m} °C`;
    windEl.textContent = `${current.wind_speed_10m} km/h`;
    result.hidden = false;
    statusEl.textContent = "";
  } catch (error) {
    console.error(error);
    statusEl.textContent = "Impossible de récupérer la météo. Vérifie ta connexion et réessaie.";
  }
}

showWeather(48.85, 2.35, "Paris");
  • fetch(url) : va chercher l'adresse et renvoie une promesse de résultat.
  • await : attend le résultat avant la ligne suivante. Il ne marche que dans une fonction async.
  • response.json() : transforme la réponse en objet JavaScript.
  • try / catch : si ça plante (pas de réseau, response.ok à false), on affiche un message au lieu de casser la page. Teste-le en passant en « Hors ligne » dans l'onglet Réseau.
Demande à l'IA
Explique-moi simplement ce qu'est une promesse en JavaScript, et ce que font async et await, avec une analogie de la vie de tous les jours. N'écris pas mon appli à ma place.
Résultat attendu
Console (test, objet déplié)
{latitude: 48.86, longitude: 2.3399997, generationtime_ms: 0.03, utc_offset_seconds: 0, timezone: 'GMT', …}
  current: {time: '2026-10-06T14:00', interval: 900, temperature_2m: 16.2, wind_speed_10m: 11.5}
  current_units: {time: 'iso8601', interval: 'seconds', temperature_2m: '°C', wind_speed_10m: 'km/h'}
  elevation: 43
  generationtime_ms: 0.03
  latitude: 48.86
  longitude: 2.3399997
  timezone: "GMT"
  timezone_abbreviation: "GMT"
  utc_offset_seconds: 0
16.2
meteo/index.html

Valeurs d'exemple : chez toi, ce sera la météo de Paris au moment du test, après un bref « Chargement... ».

Mission 8

L'API météo veut des coordonnées, pas un nom de ville. L'API de géocodage d'Open-Meteo fait la conversion. Ajoute un formulaire sous le titre de index.html :

index.html
<h1>Météo</h1>
<form id="city-form">
  <label for="city-input">Ville</label>
  <input id="city-input" type="text" placeholder="Ex : Lyon" autocomplete="off">
  <button type="submit">Voir la météo</button>
</form>
<p id="status"></p>

Puis ajoute ce code à la fin de script.js, sous celui de la mission 7 :

script.js
const cityForm = document.querySelector("#city-form");
const cityInput = document.querySelector("#city-input");
const cityBtn = cityForm.querySelector("button");

// Transforme un nom de ville en coordonnées (API de géocodage d'Open-Meteo)
async function findCity(name) {
  const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1&language=fr`;
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Erreur HTTP ${response.status}`);
  }
  const data = await response.json();
  if (!data.results || data.results.length === 0) {
    return null;
  }
  return data.results[0];
}

cityForm.addEventListener("submit", async (event) => {
  event.preventDefault();

  const city = cityInput.value.trim();
  if (city === "") {
    statusEl.textContent = "Tape le nom d'une ville.";
    return;
  }

  cityBtn.disabled = true;
  statusEl.textContent = "Recherche de la ville...";
  result.hidden = true;

  try {
    const place = await findCity(city);
    if (place === null) {
      statusEl.textContent = `Aucune ville trouvée pour « ${city} ».`;
      return;
    }
    const name = place.country ? `${place.name}, ${place.country}` : place.name;
    await showWeather(place.latitude, place.longitude, name);
  } catch (error) {
    console.error(error);
    statusEl.textContent = "La recherche a échoué. Vérifie ta connexion et réessaie.";
  } finally {
    cityBtn.disabled = false;
  }
});
  • encodeURIComponent(name) : protège les espaces et les accents dans l'adresse.
  • Pas de data.results : aucune ville trouvée, on renvoie null.
  • cityBtn.disabled bloque le bouton pendant la recherche, finally le débloque dans tous les cas.
  • On réutilise showWeather : chargement, résultat et erreur sont déjà gérés.
Erreur fréquente

statusEl is not defined ? Ce code doit être dans le même script.js, sous les const de la mission 7.

Résultat attendu
meteo/index.html
meteo/index.html

Valeurs d'exemple. Teste « Lyon », « New York », puis « azertyuiop » : une météo ou un message clair, jamais d'erreur rouge dans la console.

Le projet

Une to-do list, puis une appli météo

Refais tes deux applis toutes les deux, dans de nouveaux dossiers, sans regarder le code des missions. Si tu bloques, relis l'explication, referme-la et écris toi-même. Donne-leur un vrai style, utilisable sur téléphone.

Ton projet est validé quand :

Bonus

Pour la to-do, ajoute des filtres « Toutes / À faire / Faites ». Pour la météo, affiche les prévisions des prochains jours : regarde le paramètre daily dans la doc d'Open-Meteo.

Pour aller plus loin

Étape 4 bouclée ?

Si tes deux applis cochent tous les critères, ton site sait faire des choses. Prochaine étape : sauvegarder ton code avec Git et le mettre en ligne, pour que tout le monde puisse le voir.

Étape 5 : Git et mise en ligne →