1.2 · Une page web avec Astro

Les diapositives de la séance 1

  • créer un projet Astro et démarrer son serveur de développement
  • identifier les balises HTML structurantes et le rôle des attributs id et class
  • appliquer du style CSS à une page, depuis une feuille de style ou depuis TypeScript
  • sélectionner, modifier et créer des éléments du DOM en TypeScript
  • réagir aux actions de l’utilisateur avec addEventListener

Transpiler et exécuter un fichier TypeScript : 1.1 · Hello world TypeScript. Node.js et npm installés : Démarrage.

Du terminal au navigateur

Au chapitre 1.1, node hello.js exécutait le code dans un terminal. Ici, le même code va s’exécuter dans un navigateur, et il pourra donc lire et modifier la page affichée. La porte d’entrée vers cette page s’appelle document.

Conséquence immédiate : document n’existe que dans le navigateur. Un fichier qui manipule document et que vous lancez avec node plante avec ReferenceError: document is not defined. Ce n’est pas une erreur de type, c’est une erreur d’environnement d’exécution.

Le code d’exemple

Le dossier exemples/astro/ contient index.html (la page) et app.ts (le script qui la manipule).

1. Transpiler le script :

tsc app.ts

2. Ouvrir index.html dans un navigateur (double-clic sur le fichier).

3. Ouvrir la console du navigateur avec F12, onglet Console. C’est là que console.log écrit, et c’est là que s’affichent les erreurs.

Le lien entre les deux fichiers tient à une seule ligne, tout en bas de index.html :

<script src="app.js"></script>

On lie bien app.js, et jamais app.ts : un navigateur ne sait pas lire du TypeScript. Sans l’étape tsc, la page s’affiche mais aucun bouton ne réagit.

Créer un projet Astro

Le double-clic suffit pour une page isolée. Dès qu’un projet a plusieurs pages, des modules (1.3 · Modules) ou des appels réseau, il faut un vrai serveur de développement. Astro joue ce rôle et transpile le TypeScript tout seul.

npm create astro@latest mon-site

L’assistant pose quatre questions. Voici quoi répondre :

Question Réponse Pourquoi
How would you like to start your new project? Empty on part d’une page vide, sans exemple à déblayer
Install dependencies? Yes sinon rien ne démarre
Initialize a new git repository? Yes ou No sans importance pour le cours, voir l’encadré ci-dessous
Add AI agent files? (si posée) No fichiers de configuration pour agents IA, inutiles ici

Puis :

cd mon-site
npm run dev

Ouvrez l’adresse affichée dans le terminal, en général http://localhost:4321.

NoteLa question sur git

Répondez ce que vous voulez : rien n’est à rendre sur un dépôt dans ce cours, et git n’est au programme d’aucune évaluation. Si vous tenez à sauvegarder votre travail ailleurs que sur votre machine, voyez Configurer ses outils.

Astro n’a plus de question sur la rigueur TypeScript : le tsconfig.json généré étend déjà astro/tsconfigs/strict.

Deux dossiers suffisent pour l’instant :

mon-site/
├── src/
│   ├── pages/      # une page = un fichier .astro ; index.astro est la racine du site
│   └── scripts/    # vos fichiers .ts : à créer, Astro ne le fait pas
└── package.json

Dans src/pages/index.astro, on charge directement le TypeScript :

<script src="../scripts/app.ts"></script>

Astro le transpile à la volée et recharge la page à chaque enregistrement : vous n’avez plus à lancer tsc vous-même.

NoteLe frontmatter d’un fichier .astro

Un fichier .astro commence par un bloc délimité par deux lignes ---. Ce bloc contient du code exécuté sur le serveur, avant l’envoi de la page. Il peut rester vide, et c’est le cas dans tous les exemples de ce chapitre.

Sur les machines de l’école

npm create astro@latest installe tout dans le dossier du projet : aucun droit administrateur n’est nécessaire. Si le port 4321 est déjà occupé par un camarade, Astro en choisit un autre : lisez l’URL affichée dans le terminal plutôt que de la taper de mémoire.

Le HTML dont vous avez besoin

Une page HTML est un arbre de balises. Voici celles que vous rencontrerez dans index.html.

Rôle Balises
Structure sémantique <header>, <nav>, <main>, <section>, <article>, <footer>
Titres et texte <h1> à <h6>, <p>, <span>, <br>, <hr>
Mise en forme <strong> (important), <em> (emphase), <b>, <i>, <u>
Conteneur générique <div> (bloc), <span> (en ligne)
Listes <ul> (à puces), <ol> (numérotée), <li> (élément)
Tableaux <table>, <thead>, <tbody>, <tr> (ligne), <th> (en-tête), <td> (cellule)
Formulaires <form>, <label>, <input>, <textarea>, <select>, <option>, <button>
Liens et images <a>, <img>

<strong> et <b> donnent le même gras, mais <strong> dit pourquoi le texte est en gras. C’est cette information que lisent les moteurs de recherche et les lecteurs d’écran : préférez toujours la balise sémantique.

Les attributs

Attribut Rôle
id identifiant unique dans la page : c’est par lui que TypeScript retrouve un élément
class étiquette réutilisable, partagée par plusieurs éléments, ciblée par le CSS
href destination d’un lien <a>
src source d’une <img> ou d’un <script>
alt description d’une image, lue si l’image ne charge pas
placeholder texte grisé dans un champ vide
for relie un <label> au champ portant cet id

Le CSS en trois façons

Le CSS décrit l’apparence : couleurs, tailles, espacements, mise en page.

<!-- 1. En ligne : rapide, mais impossible à réutiliser -->
<p style="color: red;">Texte rouge</p>

<!-- 2. Dans le <head> : pour une page unique -->
<style> p { color: red; } </style>

<!-- 3. Fichier externe : la bonne méthode dès qu'il y a plus d'une page -->
<link rel="stylesheet" href="styles.css">

Un sélecteur désigne les éléments à styler : p cible toutes les balises <p>, .important cible tous les éléments ayant class="important", #titre cible l’élément ayant id="titre".

.important { color: red; font-weight: bold; }
#titre-principal { text-align: center; }

Les propriétés les plus courantes : color, background-color, font-size, font-weight, padding (espace intérieur), margin (espace extérieur), border, border-radius, width, height, text-align.

AstuceApprendre la mise en page en jouant

Flexbox Froggy enseigne le positionnement flexible en une vingtaine de minutes. C’est la technique de mise en page utilisée partout aujourd’hui.

Le DOM depuis TypeScript

Le DOM (Document Object Model) est la représentation de la page sous forme d’objets manipulables. Quatre opérations suffisent à tout faire.

1. Sélectionner un élément

app.ts
const titrePrincipal = document.getElementById("titre-principal") as HTMLHeadingElement;
if (titrePrincipal) {
    console.log("Élément trouvé :", titrePrincipal.textContent);
}

as HTMLHeadingElement est une assertion de type : vous affirmez au compilateur que cet élément est bien un titre, ce qu’il ne peut pas deviner à partir d’une simple chaîne. En échange, il vous donne l’autocomplétion des propriétés d’un titre.

Les types les plus fréquents : HTMLElement (générique), HTMLHeadingElement (<h1>…<h6>), HTMLParagraphElement, HTMLDivElement, HTMLSpanElement, HTMLButtonElement, HTMLInputElement, HTMLUListElement, HTMLLIElement.

getElementById renvoie null si l’id n’existe pas, et une faute de frappe dans l’id suffit. L’assertion as ne vous protège pas : elle ment au compilateur, elle ne vérifie rien à l’exécution. D’où le if (element) systématique avant d’utiliser l’élément, sous peine de Cannot read properties of null.

2. Modifier le contenu

element.textContent = "Nouveau texte";      // sûr : tout est traité comme du texte
element.innerHTML = "<b>Texte en gras</b>"; // interprète le HTML

innerHTML interprète ce qu’on lui donne. Si la chaîne vient de l’utilisateur (champ de formulaire, paramètre d’URL, réponse d’une API), elle peut contenir du code qui s’exécutera dans la page : c’est une faille XSS (Cross-Site Scripting). Utilisez textContent par défaut, et ne réservez innerHTML qu’à du HTML que vous avez écrit.

3. Modifier le style

element.style.color = "purple";
element.style.backgroundColor = "#f0f0f0";
element.style.padding = "10px";
element.style.borderRadius = "5px";

En CSS on écrit background-color ; en TypeScript, le tiret est interdit dans un nom de propriété, on écrit donc backgroundColor en camelCase. Même règle pour font-size → fontSize, border-radius → borderRadius.

4. Créer et insérer

app.ts
const message: string = "Hello, hello TYPESCRIPT World!";
const titre: HTMLHeadingElement = document.createElement("h1");  // créé, mais invisible
titre.textContent = message;
titre.style.color = "purple";
document.body.prepend(titre);   // prepend : au début ; appendChild : à la fin

Un élément créé par createElement n’apparaît nulle part tant qu’il n’a pas été inséré dans le document. C’est l’oubli le plus fréquent : le code tourne sans erreur, et rien ne s’affiche.

Réagir aux événements

addEventListener attache une fonction à un événement. Cette fonction n’est pas appelée par vous : c’est le navigateur qui l’appellera, plus tard, quand l’événement se produira. On parle de fonction de rappel (callback).

app.ts
let compteur: number = 0;
const affichage = document.getElementById("demo-compteur") as HTMLParagraphElement;
const bouton = document.getElementById("btn-incrementer") as HTMLButtonElement;

if (bouton && affichage) {
    bouton.addEventListener("click", function () {
        compteur = compteur + 1;
        affichage.textContent = "Compteur : " + compteur.toString();
    });
}

Certains événements transportent une information ; on la récupère par le paramètre de la fonction de rappel :

app.ts
const champ = document.getElementById("demo-input") as HTMLInputElement;
const sortie = document.getElementById("demo-output") as HTMLParagraphElement;

if (champ && sortie) {
    champ.addEventListener("input", function (event: Event) {
        const cible = event.target as HTMLInputElement;   // l'élément qui a déclenché
        sortie.textContent = "Vous avez tapé : " + cible.value;
    });
}
Événement Déclencheur
click clic sur l’élément
input saisie, à chaque caractère tapé
change valeur modifiée, une fois le champ quitté
submit envoi d’un formulaire
mouseenter / mouseleave entrée / sortie du survol
keydown / keyup touche pressée / relâchée
Notefunction () { } ou () => { } ?

Vous croiserez les deux écritures ; elles font ici la même chose. La seconde, la fonction fléchée, est détaillée au chapitre 2.1 · Fonctions fléchées.

Exercices

Exercice 1.4 : Explorer la page d’exemple

essentiel

Transpilez app.ts, ouvrez index.html, et répondez :

  1. Quel est le seul élément visible de la page qui n’existe pas dans index.html ? D’où vient-il ?
  2. Combien de fois pouvez-vous cliquer sur Changer la couleur avant que la couleur se répète ? Retrouvez dans le code ce qui l’explique.
  3. Renommez id="btn-incrementer" en id="bouton-incrementer" dans index.html seulement. Rechargez : que fait le bouton, et que dit la console ?

Réussite : vous savez dire pour chaque comportement de la page s’il vient du HTML ou du TypeScript.

1. Le grand titre violet Hello, hello TYPESCRIPT World! tout en haut de la page. Cherchez <h1>Hello dans index.html : il n’y est pas. Il est fabriqué de toutes pièces par les quatre lignes du début d’app.ts :

const my_title: HTMLHeadingElement = document.createElement("h1");
my_title.textContent = my_hello_message;
my_title.style.color = "purple";
document.body.prepend(my_title);   // sans cette ligne, rien ne s'affiche

prepend explique sa position : il est inséré au début du <body>, avant la zone de démonstration. Avec appendChild, il serait apparu tout en bas.

À la réflexion, la liste de fruits mérite la même remarque : index.html ne contient qu’un <div id="demo-liste"></div> vide, et c’est la section 6 d’app.ts qui crée le <ul> et ses quatre <li>. La différence avec le titre est que le conteneur, lui, existe bien dans le HTML : le TypeScript le remplit, il ne le crée pas.

2. Cinq clics. Le sixième ramène à la première couleur. La raison tient en deux endroits du code :

const couleurs: string[] = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7"];
// …
indexCouleur = indexCouleur + 1;
if (indexCouleur >= couleurs.length) {
    indexCouleur = 0;   // retour au début : le cycle fait exactement 5 clics
}

Le tableau contient cinq couleurs, et le compteur repart à zéro dès qu’il atteint couleurs.length. Ajoutez une couleur au tableau : le cycle passe à six, sans toucher au reste du code. C’est le .length qui rend cela possible : avec un 5 écrit en dur, il aurait fallu penser à le changer.

3. Le bouton ne fait rien, et la console reste muette. C’est cette combinaison qui est instructive.

document.getElementById("btn-incrementer") ne trouve plus rien et renvoie null. Le as HTMLButtonElement n’y change rien : une assertion de type n’est qu’une promesse faite au compilateur, elle ne vérifie strictement rien à l’exécution. La variable btnIncrementer vaut donc null, et le garde-fou fait son office :

if (btnIncrementer && affichageCompteur) {   // false : tout le bloc est sauté
    btnIncrementer.addEventListener("click", …);
}

L’addEventListener n’est jamais posé. Aucun écouteur, donc aucune réaction au clic, et aucune erreur non plus.

Supprimez le if pour voir l’autre scénario :

Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')

Le point clé : le if (element) est indispensable, mais il a un coût qu’il faut connaître : il transforme un plantage bruyant en panne silencieuse. Une faute de frappe dans un id ne produit alors aucun signal, ni à la compilation (le compilateur ne lit pas votre HTML) ni à l’exécution. C’est l’un des bugs les plus coûteux à trouver en développement web. D’où l’habitude, pendant la mise au point, d’écrire plutôt :

if (!btnIncrementer) {
    console.error("Bouton introuvable : vérifiez l'id dans le HTML");
} else {
    // …
}

Exercice 1.5 : Une fiche interactive dans un projet Astro

essentiel transfert

Créez un projet Astro, une page src/pages/fiche.astro et un script src/scripts/fiche.ts. La page doit contenir un champ texte, un bouton et un paragraphe vide, tous munis d’un id.

Votre script doit :

  1. afficher dans le paragraphe Bonjour <nom> ! au clic sur le bouton ;
  2. mettre ce paragraphe en gras et sur fond gris clair à ce moment-là ;
  3. si le champ est vide, afficher Entrez un nom en rouge à la place.

Réussite : la page réagit sans rechargement, et vous n’avez jamais lancé tsc à la main.

src/pages/fiche.astro
---
---
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Fiche interactive</title>
</head>
<body>
    <h1>Fiche</h1>
    <input type="text" id="champ-nom" placeholder="Entrez votre nom">
    <button id="btn-saluer">Saluer</button>
    <p id="message"></p>

    <script src="../scripts/fiche.ts"></script>
</body>
</html>
src/scripts/fiche.ts
const champNom = document.getElementById("champ-nom") as HTMLInputElement;
const btnSaluer = document.getElementById("btn-saluer") as HTMLButtonElement;
const message = document.getElementById("message") as HTMLParagraphElement;

if (champNom && btnSaluer && message) {
    btnSaluer.addEventListener("click", function () {
        const nom: string = champNom.value.trim();

        if (nom === "") {
            message.textContent = "Entrez un nom";
            message.style.color = "red";
            message.style.fontWeight = "normal";
            message.style.backgroundColor = "transparent";
        } else {
            message.textContent = `Bonjour ${nom} !`;
            message.style.color = "black";
            message.style.fontWeight = "bold";
            message.style.backgroundColor = "#f0f0f0";
        }
    });
}

Lancez npm run dev, ouvrez http://localhost:4321/fiche : le nom de la page vient du nom du fichier, sans l’extension.

Le point clé : les deux branches du if doivent remettre toutes les propriétés de style à leur valeur, pas seulement celles qui les intéressent. Sinon les styles s’accumulent : saisissez un nom (gras, fond gris), videz le champ, recliquez. Le message d’erreur apparaît en rouge, mais toujours en gras sur fond gris. Une propriété style fixée en TypeScript reste sur l’élément jusqu’à ce qu’on la change explicitement.

L’alternative, plus propre dès que les styles se multiplient, est de définir deux classes CSS (.succes et .erreur) et de basculer de l’une à l’autre avec message.className = "erreur" : une seule affectation remplace tout l’ancien style.

Deux détails qui comptent : .value renvoie toujours une string, même pour un <input type="number"> : champ.value * 2 ne fait pas ce que vous croyez. Et le .trim() évite qu’une saisie composée de trois espaces soit considérée comme un nom valide.

Enfin, tsc n’est jamais lancé : npm run dev transpile à la volée et recharge la page à chaque enregistrement. Si un changement semble ne rien produire, regardez le terminal d’Astro : c’est là que s’affichent les erreurs de transpilation, pas dans la console du navigateur.

Exercice 1.6 : Liste construite en TypeScript

entraînement

Dans la même page, ajoutez un <div id="liste-noms"></div> vide. À chaque clic sur le bouton, ajoutez le nom saisi comme nouvel élément <li> dans une liste <ul> créée en TypeScript : la liste doit être créée une seule fois, pas à chaque clic.

Réussite : trois clics successifs produisent une liste de trois éléments, et index.html ne contient toujours aucun <li>.

Dans fiche.astro, ajoutez le conteneur vide sous le paragraphe :

<div id="liste-noms"></div>

Dans fiche.ts, complétez le script de l’exercice 1.5 :

src/scripts/fiche.ts
const conteneurListe = document.getElementById("liste-noms") as HTMLDivElement;

// Le <ul> est créé UNE SEULE FOIS, au chargement du script, hors de tout écouteur
const listeNoms: HTMLUListElement = document.createElement("ul");
conteneurListe.appendChild(listeNoms);

if (champNom && btnSaluer && message) {
    btnSaluer.addEventListener("click", function () {
        const nom: string = champNom.value.trim();
        if (nom === "") {
            return;   // rien à ajouter à la liste
        }

        // Seul le <li> est créé à chaque clic
        const element: HTMLLIElement = document.createElement("li");
        element.textContent = nom;
        listeNoms.appendChild(element);

        champNom.value = "";   // on vide le champ pour la saisie suivante
    });
}

Le point clé : tout l’exercice tient dans le placement des deux createElement. Celui du <ul> est à l’extérieur de l’écouteur : il s’exécute une fois, au chargement de la page. Celui du <li> est à l’intérieur : il s’exécute à chaque clic. Déplacez le premier à l’intérieur du addEventListener et vous obtiendrez trois listes de un élément au lieu d’une liste de trois : visuellement presque identique, structurellement faux. Vérifiez-le dans l’inspecteur (F12, onglet Éléments) : c’est le bon réflexe dès qu’on construit du DOM.

La variable listeNoms reste accessible depuis l’intérieur de la fonction de rappel bien qu’elle soit déclarée à l’extérieur : c’est une fermeture, le mécanisme disponible dès maintenant et expliqué au chapitre 2.1 · Fonctions fléchées.

À ne pas faire : conteneurListe.innerHTML += "<li>" + nom + "</li>". C’est plus court, et c’est doublement mauvais : le nom vient de l’utilisateur, donc une saisie comme <img src=x onerror=alert(1)> s’exécuterait dans la page (faille XSS) ; et innerHTML += redétruit puis reconstruit tout le contenu du conteneur, ce qui efface au passage les écouteurs posés sur les éléments existants.

QCM

1. Quel est le type de titre ?

const titre = document.getElementById("titre-principal");

A. HTMLElement B. HTMLElement | null C. HTMLHeadingElement D. string

B. getElementById renvoie null si aucun élément ne porte cet id : il faut tester avant d’utiliser. Le type précis (HTMLHeadingElement) ne s’obtient qu’avec as ou instanceof.

2. Ce code s’exécute sans erreur, et pourtant rien n’apparaît dans la page. Pourquoi ?

const item = document.createElement("li");
item.textContent = "Pomme";

A. textContent ne fonctionne pas sur un <li>. B. L’élément n’a jamais été inséré dans la page. C. Il fallait utiliser innerHTML. D. Il faut recharger la page pour voir le changement.

B. createElement fabrique un élément détaché. Il n’apparaît qu’après appendChild ou prepend sur un élément de la page.

3. Un nom saisi par l’utilisateur doit s’afficher dans un paragraphe sortie. Quelle ligne choisir ?

A. sortie.innerHTML = nom; B. sortie.textContent = nom; C. sortie.style.content = nom; D. document.write(nom);

B. textContent traite la saisie comme du texte. Avec innerHTML, une saisie contenant du HTML s’exécuterait dans la page (faille XSS).

La suite : découper le script

Tout le code de ce chapitre tient dans un seul app.ts. Dès qu’il grossit, on le découpe en plusieurs fichiers qui s’échangent des fonctions : c’est l’objet du chapitre court qui termine la séance, 1.3 · Modules.

À retenir

  • Le navigateur exécute du JavaScript : on lie toujours app.js, jamais app.ts
  • Astro transpile et recharge automatiquement ; npm run dev puis localhost:4321
  • getElementById peut renvoyer null : testez avant d’utiliser, as ne vérifie rien
  • textContent par défaut, innerHTML seulement sur du HTML que vous maîtrisez (XSS)
  • Les propriétés de style s’écrivent en camelCase : backgroundColor, pas background-color
  • createElement ne suffit pas : sans appendChild ou prepend, rien ne s’affiche