createElement, textContent et append
document.createElement('h2') crée un élément. textContent lui affecte du texte. append l’ajoute dans un conteneur. Cette construction garde les données textuelles séparées du balisage.
const titre = document.createElement('h2');
titre.textContent = projet.title;
const carte = document.createElement('article');
carte.append(titre);
Si projet.title contient <script>, textContent affiche ces caractères comme du texte ; innerHTML les interpréterait comme du balisage. La construction DOM n’est pas un contrôle universel : une URL doit encore être validée pour son usage et le serveur doit contrôler les données reçues.
| Partie de la carte | Origine | Vérification |
|---|---|---|
| Titre et description. | Texte du JSON. | Affichage textuel et hiérarchie correcte. |
| Média. | Champ d’image valide ou repli prévu. | Alternative adaptée et image absente traitée. |
| Lien. | URL autorisée par le contrat. | Destination et intitulé explicites. |
| Date. | Champ prévu dans le contrat. | Date de fin absente clairement présentée. |
Chargement, liste vide et erreur de lecture
Avant la réponse, affichez le chargement. Après réussite, affichez les cartes ou un message d’état vide. Après échec, affichez une erreur et une possibilité de reprise. L’absence de projet n’est pas un écran blanc inexpliqué.
Images absentes et textes longs
Une fonction creerCarte(projet) ne doit pas supposer que tous les titres ont la même longueur. Testez trois projets : titre court, description longue et image absente. Utilisez la même structure pour chacun. Un nouveau projet doit devenir une nouvelle carte sans ajouter une branche spécifique à son nom.
La fonction de rendu d’une carte
L’API livre des données ; le navigateur construit l’interface. Une fonction de rendu commune évite d’écrire à la main une carte par projet. Le contenu peut varier en longueur et certains champs sont absents : un composant doit traiter ces cas sans laisser des liens vides.
function creerCarte(project) {
const article = document.createElement('article');
const title = document.createElement('h2');
const description = document.createElement('p');
title.textContent = project.title;
description.textContent = project.description;
article.append(title, description);
return article;
}
liste.replaceChildren(...projects.map(creerCarte));
Résultat attendu : autant de cartes que de projets, sans interpréter un titre comme HTML. Le remplacement vide d’abord le conteneur, donc une actualisation ne duplique pas les cartes. La carte utilise article, son titre reste dans une hiérarchie cohérente avec le h1 de page.
Comparer données, cartes et états de liste
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 20px;
font-family: system-ui, sans-serif;
color: #18324c;
background: #f1f5f9;
}
button,
input,
select,
textarea {
font: inherit;
max-width: 100%;
}
button {
padding: 8px 12px;
cursor: pointer;
}
input,
select,
textarea {
padding: 8px;
}
:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 3px;
}
article,
.panel {
padding: 16px;
background: white;
border-radius: 12px;
min-width: 0;
}
.cards {
display: grid;
gap: 16px;
}
label {
display: block;
margin-block: 12px 4px;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.15rem;
}
p,
h2,
a {
overflow-wrap: anywhere;
}
.note {
font-size: 0.9rem;
}
@media (min-width: 700px) {
.cards {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1000px) {
.cards {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
</style>
<h1>Portfolio : une carte par objet</h1>
<p class="note">
Prototype en mémoire ; la version PHP/MySQL du TP conserve les données.
</p>
<p id="etat" role="status"></p>
<section id="projets" class="cards"></section>
<script>
const projects = [
{
id: 1,
title: "Festival étudiant",
description: "Une identité et un site.",
technologies: "HTML, CSS",
},
{
id: 2,
title: "Galerie photo",
description: "Un catalogue de portraits.",
technologies: "PHP, CSS",
},
{
id: 3,
title: "Tournoi du campus",
description: "Une page événement.",
technologies: "HTML",
},
];
function render() {
const results = projects;
const cards = results.map((p) => {
const article = document.createElement("article");
const h2 = document.createElement("h2");
h2.textContent = p.title;
const description = document.createElement("p");
description.textContent = p.description;
const technologies = document.createElement("p");
technologies.textContent = p.technologies;
article.append(h2, description, technologies);
return article;
});
document.querySelector("#projets").replaceChildren(...cards);
document.querySelector("#etat").textContent = results.length
? `${results.length} projet(s)`
: "Aucun résultat pour ces filtres.";
}
render();
</script>Image, lien et état vide
Une image informative reçoit un alt utile ; ne recopiez pas mécaniquement le nom de fichier. Laissez une place stable avec aspect-ratio et object-fit: cover pour éviter une mise en page qui saute au chargement. Un lien « Voir Festival étudiant » possède une destination réelle. Si project_url est vide, n’affichez pas de lien cliquable vers #.
Une liste vide affiche « Aucun projet enregistré ». Une erreur affiche un message d’échec et un moyen de réessayer. Le chargement affiche un état provisoire : ces trois messages ne sont pas interchangeables. Le rendu doit rester utilisable avec les seules touches Tab et Entrée.
Tableau de référence — notions et opérations
Exemple court — un cadre, une image et un texte
La carte possède une unité de contenu et un niveau de titre. Le cadre réserve une proportion régulière ; cover peut recadrer l’image, mais ne doit pas couper l’information importante. Le rendu dynamique réutilise cette structure à partir des données.
<article>
<img src="/images/learning/landscape.svg" alt="Illustration des montagnes du projet">
<h2>Regards sur la montagne</h2>
<p>Un projet photographique.</p>
</article>
<style>
article {
max-width: 18rem;
}
img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
</style>