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
idetclass - 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.
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-siteL’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 devOuvrez l’adresse affichée dans le terminal, en général http://localhost:4321.
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.
.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.
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 HTMLinnerHTML 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 finUn é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 |
function () { } 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 :
- Quel est le seul élément visible de la page qui n’existe pas dans
index.html? D’où vient-il ? - 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.
- Renommez
id="btn-incrementer"enid="bouton-incrementer"dansindex.htmlseulement. 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'afficheprepend 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 :
- afficher dans le paragraphe
Bonjour <nom> !au clic sur le bouton ; - mettre ce paragraphe en gras et sur fond gris clair à ce moment-là ;
- si le champ est vide, afficher
Entrez un nomen 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, jamaisapp.ts - Astro transpile et recharge automatiquement ;
npm run devpuislocalhost:4321 getElementByIdpeut renvoyernull: testez avant d’utiliser,asne vérifie rientextContentpar défaut,innerHTMLseulement sur du HTML que vous maîtrisez (XSS)- Les propriétés de style s’écrivent en camelCase :
backgroundColor, pasbackground-color createElementne suffit pas : sansappendChildouprepend, rien ne s’affiche