Aller au contenu

Objets et JSON

Objectif : ranger les informations d'une chose dans un seul objet, le lire et le modifier, comprendre ce qu'est un fichier JSON, puis afficher une liste d'objets dans Vue.

1. Pourquoi les objets ?

Imagine un petit bestiaire : une galerie de créatures inventées. Voici la première, avec ce que tu connais déjà :

const nom = 'Grolem'
const niveau = 12
const pv = 180
const vivant = true

Quatre variables pour une seule créature. À la huitième, tu jongles avec nom8, niveau8, pv8… 😵

Un objet regroupe ces informations sous un seul nom :

const creature = {
  nom: 'Grolem',
  niveau: 12,
  pv: 180,
  vivant: true,
}

Une variable, quatre propriétés. Sur ton portfolio, ce sera pareil : un projet a un titre, une image, une description, des tags.

2. Créer un objet

Un objet, c'est un sac à variables. Des accolades { } l'ouvrent et le ferment.

Chaque ligne est une propriété : une clé (son nom, sans guillemets), deux-points, puis sa valeur. Une virgule sépare deux propriétés ; celle après la dernière est facultative, Prettier l'ajoute pour toi.

Anatomie d'un objet JavaScript : l'objet creature de Grolem annoté, avec le nom de la variable, les accolades, chaque paire clé : valeur, les virgules et le type de chaque valeur

La valeur peut être de n'importe quel type :

Type Exemple
Chaîne de caractères nom: 'Grolem'
Nombre niveau: 12
Booléen vivant: true
Tableau faiblesses: ['eau', 'mousse']
Objet stats: { pv: 180, attaque: 25 }

Un objet peut donc en contenir un autre : on y vient à la section 5. Il peut aussi ranger des fonctions, et tu en utilises déjà : tableau.push() ou texte.toUpperCase().

Note

Les clés de ce chapitre n'ont ni accent ni espace : capacites, degats, defense. C'est une convention : une clé sans accent ne s'écrit que d'une seule façon. Les valeurs, elles, peuvent en contenir : 'Éboulement'.

3. Lire une propriété

Pour lire une propriété : le nom de l'objet, un point, la clé. C'est la notation point, celle que tu utiliseras presque toujours.

creature.nom    // 'Grolem'
creature.niveau // 12

Le commentaire montre ce que répond la console. Pour essayer, ouvre les DevTools (F12, ou Cmd + Option + I sur Mac), onglet Console, et colles-y l'objet. Au premier collage, le navigateur peut te demander de taper allow pasting : c'est une protection, tape-le.

Les crochets font la même chose, avec la clé entre apostrophes. Ils servent quand la clé est dans une variable :

creature['niveau'] // 12

const champ = 'pv'
creature[champ] // 180, comme creature.pv

Écrire creature.champ chercherait une clé qui s'appelle littéralement champ, et répondrait undefined.

La règle : le point par défaut, les crochets quand la clé est dans une variable.

4. Modifier, ajouter, supprimer

Pour changer une valeur, un =. Si la clé n'existe pas, JavaScript la crée. delete la retire.

creature.pv = 150
creature.surnom = 'Le Têtu'
delete creature.surnom

Mais creature est déclarée avec const : comment peut-on la modifier ? const verrouille l'étiquette du sac, pas son contenu.

Remplacer l'objet entier (creature = {}) provoque l'erreur TypeError: Assignment to constant variable. Changer ce qu'il contient ne pose aucun problème.

5. Objets dans des objets, tableaux d'objets

Une vraie fiche contient plus que quatre informations. Voici Grolem au complet, l'objet de tout le reste du chapitre :

const creature = {
  nom: 'Grolem',
  espece: 'Golem de galets',
  type: 'pierre',
  niveau: 12,
  rarete: 'rare',
  description: "Un tas de galets qui s'est mis debout un jour de pluie et n'a jamais voulu se recoucher.",
  image: '/creatures/grolem.png',
  stats: { pv: 180, attaque: 25, defense: 40, vitesse: 2 },
  capacites: [
    { nom: 'Éboulement', type: 'physique', degats: 30 },
    { nom: 'Mur de pierre', type: 'soutien', degats: 0 },
  ],
  faiblesses: ['eau', 'mousse'],
  habitat: { region: 'Carrières de Quenast', climat: 'sec' },
}

stats et habitat sont des objets dans l'objet. faiblesses est un tableau, capacites un tableau d'objets.

La description est entre guillemets doubles parce qu'elle contient une apostrophe. Prettier fait ce choix tout seul.

La fiche de Grolem décomposée en arbre : stats et habitat sont des sous-objets, capacites et faiblesses des tableaux de deux éléments, avec à côté la chaîne d'accès qui mène à chaque valeur

Pour descendre dans l'objet, tu enchaînes les accès :

creature.nom              // 'Grolem'
creature.stats.pv         // 180
creature.capacites.length // 2
creature.capacites[0].nom // 'Éboulement'
creature.faiblesses[1]    // 'mousse'
creature.habitat.region   // 'Carrières de Quenast'
creature.boss             // undefined (pas d'erreur)
creature.boss.nom         // ❌ TypeError: Cannot read properties of undefined (reading 'nom')

Lis creature.capacites[0].nom de gauche à droite, comme une adresse : dans creature, prends capacites, puis l'élément 0, puis son nom.

Un tableau se compte à partir de zéro : [0] est le premier élément. .length donne le nombre d'éléments.

Les deux dernières lignes : creature.boss n'existe pas, JavaScript répond undefined, sans erreur. Mais creature.boss.nom demande le nom de… rien. Là, ça plante.

Attention

Dans un projet Vue, Cannot read properties of undefined vient presque toujours d'une faute de frappe dans la chaîne : creature.stat.pv au lieu de creature.stats.pv. Relis-la lettre par lettre, en partant de la gauche.

6. Exercices de lecture

Vérifie que tu sais lire la fiche de Grolem. Réponds dans ta tête (ou dans la console), puis retourne la carte.

const creature = {
  nom: 'Grolem',
  espece: 'Golem de galets',
  type: 'pierre',
  niveau: 12,
  rarete: 'rare',
  description: "Un tas de galets qui s'est mis debout un jour de pluie et n'a jamais voulu se recoucher.",
  image: '/creatures/grolem.png',
  stats: { pv: 180, attaque: 25, defense: 40, vitesse: 2 },
  capacites: [
    { nom: 'Éboulement', type: 'physique', degats: 30 },
    { nom: 'Mur de pierre', type: 'soutien', degats: 0 },
  ],
  faiblesses: ['eau', 'mousse'],
  habitat: { region: 'Carrières de Quenast', climat: 'sec' },
}

Comment lis-tu le nom de la créature ?

creature.nom, qui vaut 'Grolem'.

Et ses points de vie ?

creature.stats.pv, qui vaut 180. Les PV sont rangés dans le sous-objet stats.

Comment obtiens-tu le nom de sa première capacité ?

creature.capacites[0].nom, qui vaut 'Éboulement'. Le premier élément d'un tableau porte le numéro 0.

Combien a-t-elle de faiblesses, et comment le savoir sans compter ?

creature.faiblesses.length, qui vaut 2.

Comment lis-tu sa dernière faiblesse sans connaître la longueur du tableau à l'avance ?

creature.faiblesses[creature.faiblesses.length - 1], ou plus court : creature.faiblesses.at(-1). Les deux valent 'mousse'.

Que vaut creature.habitat.climat ?

'sec'.

Que vaut creature.boss ? Et creature.boss.nom ?

creature.boss vaut undefined : la clé n'existe pas, sans erreur. creature.boss.nom plante : TypeError: Cannot read properties of undefined (reading 'nom').

Comment rends-tu la créature légendaire ?

creature.rarete = 'légendaire'. Ça marche même si creature est déclarée avec const : tu modifies le contenu, pas la variable.

Comment lui ajoutes-tu une propriété cri qui vaut 'Grrrolem !' ?

creature.cri = 'Grrrolem !'. La clé n'existait pas : JavaScript la crée.

Combien de dégâts fait sa deuxième capacité ?

creature.capacites[1].degats, qui vaut 0.

Un deuxième paquet, sur le JSON et Vue, t'attend à la fin de la section 9.

7. JSON : du texte qui ressemble à un objet

JSON (JavaScript Object Notation) est un format de texte pour écrire des données. La créature de la section 1, en JSON :

{
  "nom": "Grolem",
  "niveau": 12,
  "pv": 180,
  "vivant": true
}

On dirait un objet JavaScript. Ce n'en est pas un : c'est du texte, rangé dans un fichier ou envoyé par un serveur, avec des règles plus strictes.

Objet JavaScript JSON
Ce que c'est Une valeur, dans ton code Du texte, dans un fichier ou sur le réseau
Clés Sans guillemets : nom: Toujours entre guillemets doubles : "nom":
Chaînes Apostrophes ou guillemets Guillemets doubles uniquement
Virgule après le dernier élément Acceptée Interdite
Fonctions, commentaires, undefined Acceptés Interdits

Objet JavaScript et JSON côte à côte : clés sans guillemets et apostrophes d'un côté, guillemets doubles partout de l'autre, avec JSON.stringify et JSON.parse pour passer de l'un à l'autre

C'est le format d'échange universel : fichiers de configuration, réponses des serveurs, contenus d'un site. Tu en as déjà ouvert un : package.json.

JSON.parse transforme un texte JSON en objet :

const texte = '{"nom":"Grolem","niveau":12}'
const grolem = JSON.parse(texte)
grolem.niveau // 12

JSON.stringify fait l'inverse, d'un objet vers du texte :

JSON.stringify(grolem) // '{"nom":"Grolem","niveau":12}'

Tu les écriras rarement à la main : pour un fichier, Vite s'en charge.

8. Importer un JSON dans un projet Vue

Les données d'un site n'ont rien à faire au milieu du code. On les range dans src/data/ :

mon-projet/
├── public/
│   └── creatures/
│       ├── grolem.png
│       └── …
└── src/
    ├── data/
    │   └── creatures.json
    └── App.vue

creatures.json contient un tableau de huit objets sur le modèle de Grolem, chacun avec un id unique en plus. Le fichier complet est à l'exercice 2.

Dans le <script setup> d'un composant, une ligne suffit pour l'utiliser :

import creatures from '@/data/creatures.json'

console.log(creatures.length) // 8
console.log(creatures[1].nom) // Grolem

@ désigne src/, comme au chapitre Structure d'un projet.

Vite parse pour toi : il lit le texte, fait le JSON.parse et te donne directement le tableau d'objets.

Les images suivent la règle de public/. Le JSON indique /creatures/grolem.png : le fichier va donc dans public/creatures/grolem.png.

Attention

Cet import marche parce que Vite est là. Sans Vite, le navigateur refuse d'importer un fichier .json écrit tel quel.

9. Et dans Vue ?

Afficher un objet

Un objet déclaré dans le <script setup> s'utilise dans le template avec la même chaîne d'accès que dans la console :

<script setup>
  const creature = {
    nom: 'Grolem',
    niveau: 12,
    image: '/creatures/grolem.png',
    stats: { pv: 180, attaque: 25 },
  }
</script>

<template>
  <img :src="creature.image" :alt="creature.nom" class="w-48">
  <h1>{{ creature.nom }}</h1>
  <p>Niveau {{ creature.niveau }} · {{ creature.stats.pv }} PV</p>
</template>

Deux endroits, deux écritures :

  • entre deux balises, les moustaches : {{ creature.stats.pv }} ;
  • dans un attribut, les deux-points devant : :src="creature.image". Sans les deux-points, le navigateur chercherait une image qui s'appelle littéralement creature.image. C'est la règle des props du chapitre Components : une expression, pas un texte.

Pour l'instant, on affiche. Modifier l'objet et voir la page suivre toute seule, c'est le chapitre suivant, Ref.

Afficher un tableau d'objets avec v-for

Huit créatures, ce n'est pas huit <li> : tu en écris un, et v-for le répète.

<script setup>
  import creatures from '@/data/creatures.json'
</script>

<template>
  <ul>
    <li v-for="creature in creatures" :key="creature.id">
      {{ creature.nom }} — niveau {{ creature.niveau }}
    </li>
  </ul>
</template>

Lis : « pour chaque creature dans creatures ». creatures est le tableau importé, creature l'élément du tour en cours.

:key donne à chaque élément un identifiant unique, ici son id. Vue s'en sert pour suivre chaque ligne quand la liste change. Le chapitre v-for détaillera le reste : :key, l'index, v-for sur un objet.

Un sous-tableau ? Un deuxième v-for, placé dans le premier, liste les capacités de chaque créature :

<ul>
  <li v-for="capacite in creature.capacites" :key="capacite.nom">
    {{ capacite.nom }} ({{ capacite.degats }} dégâts)
  </li>
</ul>

Passer un objet entier à un composant

Au chapitre Components, ProjectCard recevait une prop par champ. Tu peux aussi passer l'objet entier, avec une prop de type Object :

<!-- src/components/CreatureCard.vue -->
<script setup>
  defineProps({
    creature: {
      type: Object,
      required: true,
    },
  })
</script>

<template>
  <article class="bg-white border border-zinc-200 overflow-hidden">
    <img :src="creature.image" :alt="creature.nom" class="w-full h-48 object-cover">
    <div class="p-4">
      <h3 class="text-xl font-bold text-zinc-900">{{ creature.nom }}</h3>
      <p class="text-sm text-zinc-500">{{ creature.espece }} · niveau {{ creature.niveau }}</p>
    </div>
  </article>
</template>
<!-- src/App.vue -->
<script setup>
  import creatures from '@/data/creatures.json'
  import CreatureCard from '@/components/CreatureCard.vue'
</script>

<template>
  <main class="min-h-screen bg-stone-100 p-8">
    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
      <CreatureCard v-for="creature in creatures" :key="creature.id" :creature="creature" />
    </div>
  </main>
</template>

Dans :creature="creature", à gauche le nom de la prop, à droite la variable du v-for.

Une prop par champ (:title="creature.nom") ou l'objet entier (:creature="creature") : la deuxième façon est plus courte quand la carte a besoin de presque tout l'objet.

Du fichier à la page : creatures.json est importé, devient un tableau de huit objets, que v-for transforme en huit cartes

Exercices de lecture : JSON et Vue

Deuxième paquet, sur ce texte et sur la liste v-for vue plus haut :

const texte = '{"nom":"Brumelle","niveau":8,"faiblesses":["vent","soleil"]}'
<script setup>
  import creatures from '@/data/creatures.json'
</script>

<template>
  <ul>
    <li v-for="creature in creatures" :key="creature.id">
      {{ creature.nom }} — niveau {{ creature.niveau }}
    </li>
  </ul>
</template>

texte est-il un objet JavaScript ?

Non. C'est du texte (une chaîne entre apostrophes) écrit au format JSON. Pour t'en servir comme d'un objet, il faut d'abord le convertir.

Comment le convertis-tu en objet ?

const brumelle = JSON.parse(texte)

Une fois converti, comment lis-tu le niveau ?

brumelle.niveau, qui vaut 8.

Quelle différence entre { nom: 'Brumelle' } et { "nom": "Brumelle" } ?

Le premier est un objet JavaScript : clé sans guillemets, apostrophes acceptées. Le second est aussi du JSON valide : en JSON, les clés et les chaînes sont toujours entre guillemets doubles.

Cite deux choses qu'un objet JavaScript peut contenir, et un JSON jamais.

Des fonctions et des commentaires. Aussi : undefined, et des chaînes entre apostrophes.

Comment transformes-tu l'objet brumelle en texte JSON ?

JSON.stringify(brumelle)

Dans un projet Vite, comment charges-tu src/data/creatures.json ?

import creatures from '@/data/creatures.json'. Pas de JSON.parse : Vite le fait pour toi.

Le JSON contient 8 créatures. Combien de <li> le v-for crée-t-il ?

8, un par élément du tableau.

À quoi sert :key="creature.id" ?

À donner à Vue un identifiant unique et stable pour chaque élément, pour qu'il retrouve chaque ligne quand la liste change. Détail au chapitre v-for.

Pour afficher le portrait, src="creature.image" ou :src="creature.image" ?

:src="creature.image". Avec les deux-points, Vue évalue l'expression et lit l'objet ; sans, le navigateur reçoit le texte creature.image et cherche une image qui s'appelle comme ça.

10. Quand mettre les données où ?

Trois endroits possibles, selon la taille et la vie des données :

Où Quand Exemple
Dans le <script setup> C'est petit, ça change rarement, c'est propre au composant Les liens d'une navbar, les options d'un <select>
Dans un fichier .json Une liste qui grandit, des données séparées du code, plusieurs composants qui la lisent Tes projets, le bestiaire, des témoignages
Sur un serveur ou une API Plus tard dans l'année : quand les données doivent être partagées ou modifiées par d'autres Les messages d'un formulaire, un catalogue tenu à jour par un client

Sur un site de designer, tout ce qui se répète avec la même forme a sa place dans un JSON :

  • les projets d'un portfolio ;
  • une galerie d'images légendées ;
  • des témoignages clients ;
  • un catalogue de produits ;
  • les articles d'un blog.

11. Exercices pratiques

Tout se fait dans ton projet Vue des chapitres précédents, avec Tailwind v4 branché.

  1. Télécharge et dézippe bestiaire.zipZIP : un fichier creatures.json et un dossier creatures/ de huit portraits.
  2. Mets creatures/ dans public/, et creatures.json dans src/data/ (crée le dossier).
  3. Copie le contenu actuel de App.vue dans un fichier à part, par exemple src/ExercicesTailwind.vue (ta carte de profil resservira à l'exercice 2). Puis remplace-le par :
<script setup>
  // Les données de l'exercice viennent ici.
</script>

<template>
  <main class="min-h-screen bg-stone-100 p-8">
    <!-- La fiche ou la grille vient ici -->
  </main>
</template>

Lance npm run dev si ce n'est pas déjà fait.

Exercice 1 — Fiche de créature ⭐⭐

Objectif : afficher un objet complexe : sous-objets stats et habitat, tableaux capacites et faiblesses.

Ta mission : colle l'objet Grolem de la section 5 dans le <script setup>, puis construis sa fiche dans le template.

  1. L'image (:src, :alt), le nom en <h1>, l'espèce, le type dans une pastille, le niveau et la rareté.
  2. La description.
  3. Les quatre stats en quatre tuiles (grid grid-cols-4), chacune avec son libellé et sa valeur.
  4. Les capacités avec v-for : nom, type, dégâts. « Mur de pierre » fait 0 dégât : affiche « soutien » plutôt que « 0 dégâts », avec v-if="capacite.degats > 0" sur un <span> et v-else sur le suivant, comme à l'exercice 3 du chapitre Components.
  5. Les faiblesses en pastilles, avec v-for.
  6. L'habitat : région et climat.

Pour l'image : /creatures/grolem.png, ou une image à toi.

Rendu attendu

La fiche de Grolem : son portrait, son nom en titre, une pastille « pierre », quatre tuiles de stats, la liste de ses deux capacités, ses faiblesses en pastilles et son habitat

Critères observables

  • Aucune donnée de Grolem n'est écrite en dur dans le template : tout vient de creature
  • Les quatre stats viennent de creature.stats
  • Les capacités et les faiblesses sont des v-for
  • La page utilise la palette du cours : stone, zinc, un seul accent
  • L'image a un alt

Bonus ⭐ : une barre de vie sous les stats. Un <div> de couleur, de hauteur fixe, dont la largeur dépend des PV : :style="{ width: creature.stats.pv / 3 + '%' }" (300 PV, le maximum du bestiaire, donnent 100 %). Regarde la valeur de :style : un objet, encore un, dont les clés sont des propriétés CSS.

La fiche de Grolem avec, sous les quatre tuiles de stats, une barre de vie remplie aux trois cinquièmes

src/App.vue :

<script setup>
  const creature = {
    nom: 'Grolem',
    espece: 'Golem de galets',
    type: 'pierre',
    niveau: 12,
    rarete: 'rare',
    description:
      "Un tas de galets qui s'est mis debout un jour de pluie et n'a jamais voulu se recoucher.",
    image: '/creatures/grolem.png',
    stats: { pv: 180, attaque: 25, defense: 40, vitesse: 2 },
    capacites: [
      { nom: 'Éboulement', type: 'physique', degats: 30 },
      { nom: 'Mur de pierre', type: 'soutien', degats: 0 },
    ],
    faiblesses: ['eau', 'mousse'],
    habitat: { region: 'Carrières de Quenast', climat: 'sec' },
  }
</script>

<template>
  <main class="min-h-screen bg-stone-100 p-8">
    <article class="mx-auto max-w-3xl bg-white border border-zinc-200 p-8">
      <!-- En-tête : portrait + identité -->
      <div class="flex flex-col gap-6 md:flex-row">
        <img
          :src="creature.image"
          :alt="creature.nom"
          class="w-48 h-48 object-cover border border-zinc-200"
        />
        <div>
          <p class="text-xs uppercase tracking-widest text-zinc-500">{{ creature.espece }}</p>
          <h1 class="mt-1 text-4xl font-bold text-zinc-900">{{ creature.nom }}</h1>
          <div class="mt-3 flex items-center gap-3">
            <span class="rounded-full bg-amber-100 px-3 py-1 text-sm font-medium text-amber-900">
              {{ creature.type }}
            </span>
            <span class="text-sm text-zinc-600">
              Niveau {{ creature.niveau }} · {{ creature.rarete }}
            </span>
          </div>
          <p class="mt-4 text-zinc-600 leading-relaxed">{{ creature.description }}</p>
        </div>
      </div>

      <!-- Les quatre stats -->
      <h2 class="mt-8 text-xs uppercase tracking-widest text-zinc-500">Statistiques</h2>
      <div class="mt-3 grid grid-cols-4 gap-3">
        <div class="bg-stone-100 p-4 text-center">
          <p class="text-2xl font-bold text-zinc-900">{{ creature.stats.pv }}</p>
          <p class="text-sm text-zinc-500">PV</p>
        </div>
        <div class="bg-stone-100 p-4 text-center">
          <p class="text-2xl font-bold text-zinc-900">{{ creature.stats.attaque }}</p>
          <p class="text-sm text-zinc-500">Attaque</p>
        </div>
        <div class="bg-stone-100 p-4 text-center">
          <p class="text-2xl font-bold text-zinc-900">{{ creature.stats.defense }}</p>
          <p class="text-sm text-zinc-500">Défense</p>
        </div>
        <div class="bg-stone-100 p-4 text-center">
          <p class="text-2xl font-bold text-zinc-900">{{ creature.stats.vitesse }}</p>
          <p class="text-sm text-zinc-500">Vitesse</p>
        </div>
      </div>

      <!-- Les capacités : un v-for sur le tableau -->
      <h2 class="mt-8 text-xs uppercase tracking-widest text-zinc-500">Capacités</h2>
      <ul class="mt-3 divide-y divide-zinc-200 border-y border-zinc-200">
        <li
          v-for="capacite in creature.capacites"
          :key="capacite.nom"
          class="flex justify-between py-3"
        >
          <span class="font-medium text-zinc-900">{{ capacite.nom }}</span>
          <span v-if="capacite.degats > 0" class="text-zinc-600">
            {{ capacite.type }} · {{ capacite.degats }} dégâts
          </span>
          <span v-else class="text-zinc-600">soutien</span>
        </li>
      </ul>

      <div class="mt-8 grid gap-8 md:grid-cols-2">
        <div>
          <h2 class="text-xs uppercase tracking-widest text-zinc-500">Faiblesses</h2>
          <div class="mt-3 flex gap-2">
            <span
              v-for="faiblesse in creature.faiblesses"
              :key="faiblesse"
              class="rounded-full border border-zinc-300 px-3 py-1 text-sm text-zinc-700"
            >
              {{ faiblesse }}
            </span>
          </div>
        </div>
        <div>
          <h2 class="text-xs uppercase tracking-widest text-zinc-500">Habitat</h2>
          <p class="mt-3 text-zinc-900">{{ creature.habitat.region }}</p>
          <p class="text-sm text-zinc-600">Climat {{ creature.habitat.climat }}</p>
        </div>
      </div>
    </article>
  </main>
</template>

src/App.vue :

<script setup>
  const creature = {
    nom: 'Grolem',
    espece: 'Golem de galets',
    type: 'pierre',
    niveau: 12,
    rarete: 'rare',
    description:
      "Un tas de galets qui s'est mis debout un jour de pluie et n'a jamais voulu se recoucher.",
    image: '/creatures/grolem.png',
    stats: { pv: 180, attaque: 25, defense: 40, vitesse: 2 },
    capacites: [
      { nom: 'Éboulement', type: 'physique', degats: 30 },
      { nom: 'Mur de pierre', type: 'soutien', degats: 0 },
    ],
    faiblesses: ['eau', 'mousse'],
    habitat: { region: 'Carrières de Quenast', climat: 'sec' },
  }
</script>

<template>
  <main class="min-h-screen bg-stone-100 p-8">
    <article class="mx-auto max-w-3xl bg-white border border-zinc-200 p-8">
      <!-- En-tête : portrait + identité -->
      <div class="flex flex-col gap-6 md:flex-row">
        <img
          :src="creature.image"
          :alt="creature.nom"
          class="w-48 h-48 object-cover border border-zinc-200"
        />
        <div>
          <p class="text-xs uppercase tracking-widest text-zinc-500">{{ creature.espece }}</p>
          <h1 class="mt-1 text-4xl font-bold text-zinc-900">{{ creature.nom }}</h1>
          <div class="mt-3 flex items-center gap-3">
            <span class="rounded-full bg-amber-100 px-3 py-1 text-sm font-medium text-amber-900">
              {{ creature.type }}
            </span>
            <span class="text-sm text-zinc-600">
              Niveau {{ creature.niveau }} · {{ creature.rarete }}
            </span>
          </div>
          <p class="mt-4 text-zinc-600 leading-relaxed">{{ creature.description }}</p>
        </div>
      </div>

      <!-- Les quatre stats -->
      <h2 class="mt-8 text-xs uppercase tracking-widest text-zinc-500">Statistiques</h2>
      <div class="mt-3 grid grid-cols-4 gap-3">
        <div class="bg-stone-100 p-4 text-center">
          <p class="text-2xl font-bold text-zinc-900">{{ creature.stats.pv }}</p>
          <p class="text-sm text-zinc-500">PV</p>
        </div>
        <div class="bg-stone-100 p-4 text-center">
          <p class="text-2xl font-bold text-zinc-900">{{ creature.stats.attaque }}</p>
          <p class="text-sm text-zinc-500">Attaque</p>
        </div>
        <div class="bg-stone-100 p-4 text-center">
          <p class="text-2xl font-bold text-zinc-900">{{ creature.stats.defense }}</p>
          <p class="text-sm text-zinc-500">Défense</p>
        </div>
        <div class="bg-stone-100 p-4 text-center">
          <p class="text-2xl font-bold text-zinc-900">{{ creature.stats.vitesse }}</p>
          <p class="text-sm text-zinc-500">Vitesse</p>
        </div>
      </div>
      <!-- Bonus : la largeur dépend des PV -->
      <div class="mt-3 h-2 bg-stone-200">
        <div class="h-2 bg-amber-500" :style="{ width: creature.stats.pv / 3 + '%' }"></div>
      </div>

      <!-- Les capacités : un v-for sur le tableau -->
      <h2 class="mt-8 text-xs uppercase tracking-widest text-zinc-500">Capacités</h2>
      <ul class="mt-3 divide-y divide-zinc-200 border-y border-zinc-200">
        <li
          v-for="capacite in creature.capacites"
          :key="capacite.nom"
          class="flex justify-between py-3"
        >
          <span class="font-medium text-zinc-900">{{ capacite.nom }}</span>
          <span v-if="capacite.degats > 0" class="text-zinc-600">
            {{ capacite.type }} · {{ capacite.degats }} dégâts
          </span>
          <span v-else class="text-zinc-600">soutien</span>
        </li>
      </ul>

      <div class="mt-8 grid gap-8 md:grid-cols-2">
        <div>
          <h2 class="text-xs uppercase tracking-widest text-zinc-500">Faiblesses</h2>
          <div class="mt-3 flex gap-2">
            <span
              v-for="faiblesse in creature.faiblesses"
              :key="faiblesse"
              class="rounded-full border border-zinc-300 px-3 py-1 text-sm text-zinc-700"
            >
              {{ faiblesse }}
            </span>
          </div>
        </div>
        <div>
          <h2 class="text-xs uppercase tracking-widest text-zinc-500">Habitat</h2>
          <p class="mt-3 text-zinc-900">{{ creature.habitat.region }}</p>
          <p class="text-sm text-zinc-600">Climat {{ creature.habitat.climat }}</p>
        </div>
      </div>
    </article>
  </main>
</template>

Exercice 2 — Le bestiaire ⭐⭐

Objectif : importer un fichier JSON, afficher une liste d'objets en cartes avec v-for.

Le contenu de creatures.json :

[
  {
    "id": 1,
    "nom": "Mousseron",
    "espece": "Champignon lumineux",
    "type": "plante",
    "niveau": 3,
    "rarete": "commune",
    "description": "Un petit champignon qui brille dans le noir et qui éternue des spores quand on le surprend.",
    "image": "/creatures/mousseron.png",
    "stats": { "pv": 40, "attaque": 8, "defense": 12, "vitesse": 6 },
    "capacites": [
      { "nom": "Nuage de spores", "type": "magique", "degats": 12 },
      { "nom": "Lueur apaisante", "type": "soutien", "degats": 0 }
    ],
    "faiblesses": ["feu", "sécheresse"],
    "habitat": { "region": "Forêt de Soignes", "climat": "humide" }
  },
  {
    "id": 2,
    "nom": "Grolem",
    "espece": "Golem de galets",
    "type": "pierre",
    "niveau": 12,
    "rarete": "rare",
    "description": "Un tas de galets qui s'est mis debout un jour de pluie et n'a jamais voulu se recoucher.",
    "image": "/creatures/grolem.png",
    "stats": { "pv": 180, "attaque": 25, "defense": 40, "vitesse": 2 },
    "capacites": [
      { "nom": "Éboulement", "type": "physique", "degats": 30 },
      { "nom": "Mur de pierre", "type": "soutien", "degats": 0 }
    ],
    "faiblesses": ["eau", "mousse"],
    "habitat": { "region": "Carrières de Quenast", "climat": "sec" }
  },
  {
    "id": 3,
    "nom": "Brumelle",
    "espece": "Esprit de brume",
    "type": "spectre",
    "niveau": 8,
    "rarete": "rare",
    "description": "Elle apparaît au petit matin au-dessus des étangs et disparaît dès qu'on prononce son nom.",
    "image": "/creatures/brumelle.png",
    "stats": { "pv": 60, "attaque": 18, "defense": 5, "vitesse": 20 },
    "capacites": [
      { "nom": "Souffle glacé", "type": "magique", "degats": 20 },
      { "nom": "Évanouissement", "type": "soutien", "degats": 0 }
    ],
    "faiblesses": ["vent", "soleil"],
    "habitat": { "region": "Étangs de Genval", "climat": "brumeux" }
  },
  {
    "id": 4,
    "nom": "Krakette",
    "espece": "Petit kraken d'étang",
    "type": "eau",
    "niveau": 6,
    "rarete": "commune",
    "description": "Huit bras, une mauvaise humeur, et une passion pour les bouchons de pêche.",
    "image": "/creatures/krakette.png",
    "stats": { "pv": 70, "attaque": 15, "defense": 10, "vitesse": 9 },
    "capacites": [
      { "nom": "Fouet de tentacule", "type": "physique", "degats": 14 },
      { "nom": "Jet d'encre", "type": "magique", "degats": 10 }
    ],
    "faiblesses": ["électricité", "filet"],
    "habitat": { "region": "Lac de Louvain-la-Neuve", "climat": "tempéré" }
  },
  {
    "id": 5,
    "nom": "Fourmidable",
    "espece": "Fourmi géante",
    "type": "insecte",
    "niveau": 10,
    "rarete": "commune",
    "description": "Elle porte vingt fois son poids, et elle le fait savoir.",
    "image": "/creatures/fourmidable.png",
    "stats": { "pv": 90, "attaque": 22, "defense": 18, "vitesse": 12 },
    "capacites": [
      { "nom": "Morsure", "type": "physique", "degats": 18 },
      { "nom": "Appel de la colonie", "type": "soutien", "degats": 0 }
    ],
    "faiblesses": ["feu", "pluie"],
    "habitat": { "region": "Dunes de La Panne", "climat": "sec" }
  },
  {
    "id": 6,
    "nom": "Pyrouette",
    "espece": "Salamandre de braise",
    "type": "feu",
    "niveau": 9,
    "rarete": "rare",
    "description": "Elle danse sur les cendres chaudes et met le feu aux rideaux par simple distraction.",
    "image": "/creatures/pyrouette.png",
    "stats": { "pv": 65, "attaque": 24, "defense": 8, "vitesse": 16 },
    "capacites": [
      { "nom": "Boule de feu", "type": "magique", "degats": 26 },
      { "nom": "Pirouette", "type": "physique", "degats": 10 }
    ],
    "faiblesses": ["eau", "glace"],
    "habitat": { "region": "Terrils de Charleroi", "climat": "chaud" }
  },
  {
    "id": 7,
    "nom": "Vieux-Chêne",
    "espece": "Tréant",
    "type": "plante",
    "niveau": 20,
    "rarete": "légendaire",
    "description": "Il a vu passer trois siècles et quatre autoroutes. Il n'est pas pressé.",
    "image": "/creatures/vieux-chene.png",
    "stats": { "pv": 300, "attaque": 30, "defense": 35, "vitesse": 1 },
    "capacites": [
      { "nom": "Racines", "type": "physique", "degats": 28 },
      { "nom": "Sève régénérante", "type": "soutien", "degats": 0 },
      { "nom": "Chute de glands", "type": "physique", "degats": 15 }
    ],
    "faiblesses": ["feu", "hache"],
    "habitat": { "region": "Forêt de Meerdael", "climat": "tempéré" }
  },
  {
    "id": 8,
    "nom": "Ronflax",
    "espece": "Blaireau dormeur",
    "type": "bête",
    "niveau": 5,
    "rarete": "commune",
    "description": "Il dort dix-huit heures par jour et il est de très mauvaise humeur les six autres.",
    "image": "/creatures/ronflax.png",
    "stats": { "pv": 110, "attaque": 12, "defense": 20, "vitesse": 4 },
    "capacites": [
      { "nom": "Ronflement sonique", "type": "magique", "degats": 8 },
      { "nom": "Coup de museau", "type": "physique", "degats": 14 }
    ],
    "faiblesses": ["bruit", "lumière"],
    "habitat": { "region": "Bois de Lauzelle", "climat": "tempéré" }
  }
]

Ta mission

  1. Si ce n'est pas déjà fait, télécharge bestiaire.zipZIP : creatures/ va dans public/, creatures.json dans src/data/.
  2. Importe le JSON dans App.vue.
  3. Affiche les huit créatures en grille de cartes : 1 colonne sur mobile, 2 sur tablette, 4 sur grand écran. Chaque carte montre l'image, le nom, l'espèce, le type en pastille, le niveau et deux stats (PV, attaque).
  4. Sous le titre, une ligne indique combien de créatures sont affichées (creatures.length).

Astuce

Ne repars pas de zéro : recopie une carte que tu as déjà faite (ta carte de profil de « Premiers pas avec Tailwind » ou ta ProjectCard) et adapte ses champs.

Rendu attendu

La grille du bestiaire sur grand écran : huit cartes sur deux rangées de quatre, chacune avec le portrait, le nom, l'espèce, une pastille de type, le niveau, les PV et l'attaque de la créature

Critères observables

  • Une seule carte est écrite dans le template, pas huit copies
  • Chaque carte a un :key
  • Les données viennent du JSON, pas du template
  • Le alt de chaque image vient du JSON
  • La grille passe de 1 à 2 puis à 4 colonnes

Bonus 1 ⭐ : extrais la carte dans src/components/CreatureCard.vue, avec une prop creature de type Object (modèle à la section 9).

Bonus 2 ⭐ : dans chaque carte, la liste des capacités de la créature : un v-for dans le v-for, avec le nom et les dégâts (ou « soutien »), comme à la section 9.

Bonus 3 ⭐⭐ : sous la grille, une section « Les plantes » qui n'affiche que les créatures de type plante, avec les mêmes cartes. Indice : dans le script, const plantes = creatures.filter((creature) => creature.type === 'plante') fabrique un second tableau (MDN — Array.filter). Compte-les aussi, avec plantes.length.

Le bestiaire avec les capacités listées dans chaque carte, puis une section « Les plantes » qui ne montre que Mousseron et Vieux-Chêne

src/App.vue :

<script setup>
  import creatures from '@/data/creatures.json'
</script>

<template>
  <main class="min-h-screen bg-stone-100 p-8">
    <h1 class="text-4xl font-bold text-zinc-900">Bestiaire</h1>
    <p class="mt-2 text-zinc-600">{{ creatures.length }} créatures</p>

    <div class="mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
      <!-- Une seule carte, répétée pour chaque créature du JSON -->
      <article
        v-for="creature in creatures"
        :key="creature.id"
        class="bg-white border border-zinc-200 overflow-hidden shadow-sm hover:shadow-xl transition"
      >
        <img :src="creature.image" :alt="creature.nom" class="w-full h-48 object-cover" />
        <div class="p-6">
          <h2 class="text-zinc-900 text-2xl font-bold">{{ creature.nom }}</h2>
          <p class="mt-1 text-xs uppercase tracking-widest text-zinc-500">{{ creature.espece }}</p>
          <div class="mt-4 flex items-center justify-between">
            <span class="rounded-full bg-amber-100 px-3 py-1 text-sm font-medium text-amber-900">
              {{ creature.type }}
            </span>
            <span class="text-sm text-zinc-600">Niveau {{ creature.niveau }}</span>
          </div>
          <p class="mt-4 text-sm text-zinc-600">
            {{ creature.stats.pv }} PV · {{ creature.stats.attaque }} attaque
          </p>
        </div>
      </article>
    </div>
  </main>
</template>

src/components/CreatureCard.vue :

<script setup>
  defineProps({
    creature: {
      type: Object,
      required: true,
    },
  })
</script>

<template>
  <article
    class="bg-white border border-zinc-200 overflow-hidden shadow-sm hover:shadow-xl transition"
  >
    <img :src="creature.image" :alt="creature.nom" class="w-full h-48 object-cover" />
    <div class="p-6">
      <h2 class="text-zinc-900 text-2xl font-bold">{{ creature.nom }}</h2>
      <p class="mt-1 text-xs uppercase tracking-widest text-zinc-500">{{ creature.espece }}</p>
      <div class="mt-4 flex items-center justify-between">
        <span class="rounded-full bg-amber-100 px-3 py-1 text-sm font-medium text-amber-900">
          {{ creature.type }}
        </span>
        <span class="text-sm text-zinc-600">Niveau {{ creature.niveau }}</span>
      </div>
      <p class="mt-4 text-sm text-zinc-600">
        {{ creature.stats.pv }} PV · {{ creature.stats.attaque }} attaque
      </p>
      <ul class="mt-4 space-y-1 text-sm text-zinc-600">
        <li v-for="capacite in creature.capacites" :key="capacite.nom" class="flex justify-between">
          <span>{{ capacite.nom }}</span>
          <span v-if="capacite.degats > 0">{{ capacite.degats }} dégâts</span>
          <span v-else>soutien</span>
        </li>
      </ul>
    </div>
  </article>
</template>

src/App.vue :

<script setup>
  import creatures from '@/data/creatures.json'
  import CreatureCard from '@/components/CreatureCard.vue'

  // Bonus 3 : un second tableau, filtré une fois
  const plantes = creatures.filter((creature) => creature.type === 'plante')
</script>

<template>
  <main class="min-h-screen bg-stone-100 p-8">
    <h1 class="text-4xl font-bold text-zinc-900">Bestiaire</h1>
    <p class="mt-2 text-zinc-600">{{ creatures.length }} créatures</p>

    <div class="mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
      <CreatureCard v-for="creature in creatures" :key="creature.id" :creature="creature" />
    </div>

    <h2 class="mt-12 text-2xl font-bold text-zinc-900">Les plantes</h2>
    <p class="mt-2 text-zinc-600">{{ plantes.length }} créatures</p>

    <div class="mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
      <CreatureCard v-for="creature in plantes" :key="creature.id" :creature="creature" />
    </div>
  </main>
</template>

plantes est calculé une fois, dans le script, puis affiché exactement comme creatures. Pour une autre section, il suffit de changer le type dans le filter.

Exercice 3 — Ma playlist ⭐

Objectif : écrire toi-même un fichier JSON, puis l'afficher.

Ta mission : crée src/data/playlist.json avec au moins cinq morceaux que tu aimes vraiment. Chaque morceau est un objet avec ces clés :

Clé Type Exemple
id Nombre 1
titre Chaîne "Around the World"
artiste Chaîne "Daft Punk"
duree Chaîne "7:09"
album Objet { "nom": "Homework", "annee": 1997 }
genres Tableau de chaînes ["house", "électro"]

Puis, dans App.vue :

  1. Importe le fichier.
  2. Affiche la playlist en liste numérotée : un <ol>, un seul <li> en v-for. Le numéro, c'est l'id. Dans chaque ligne : le titre, l'artiste, l'album et son année (morceau.album.nom, morceau.album.annee), la durée.
  3. Les genres en pastilles, avec un second v-for.
  4. Sous le titre, le nombre de morceaux.

Astuce

Les trois erreurs JSON classiques : une virgule après le dernier morceau, une clé sans guillemets, une apostrophe à la place d'un guillemet. VS Code souligne en rouge, et Vite te donne la ligne dans son message d'erreur.

Rendu attendu (avec une playlist d'exemple : la tienne sera différente)

Exemple de rendu : une playlist de cinq morceaux en liste numérotée, avec pour chacun le titre, l'artiste, l'album et son année, les genres en pastilles et la durée à droite

Critères observables

  • Le JSON est valide : rien en rouge dans VS Code, pas d'erreur dans le terminal
  • Un seul <li> est écrit dans le template
  • L'album et son année viennent du sous-objet album
  • Les genres sortent d'un second v-for
  • Les morceaux sont les tiens

Bonus ⭐ : si tu tiens aux pochettes, ajoute une clé pochette à chaque morceau, une image carrée dans public/pochettes/, et affiche-la en w-16 h-16 object-cover au début de la ligne, avec :src et :alt.

src/data/playlist.json :

[
  {
    "id": 1,
    "titre": "Around the World",
    "artiste": "Daft Punk",
    "duree": "7:09",
    "album": { "nom": "Homework", "annee": 1997 },
    "genres": ["house", "électro"]
  },
  {
    "id": 2,
    "titre": "Alors on danse",
    "artiste": "Stromae",
    "duree": "3:26",
    "album": { "nom": "Cheese", "annee": 2010 },
    "genres": ["pop", "électro"]
  },
  {
    "id": 3,
    "titre": "Blinding Lights",
    "artiste": "The Weeknd",
    "duree": "3:20",
    "album": { "nom": "After Hours", "annee": 2020 },
    "genres": ["synthpop"]
  },
  {
    "id": 4,
    "titre": "Birds of a Feather",
    "artiste": "Billie Eilish",
    "duree": "3:30",
    "album": { "nom": "Hit Me Hard and Soft", "annee": 2024 },
    "genres": ["pop", "indie"]
  },
  {
    "id": 5,
    "titre": "Texas Hold 'Em",
    "artiste": "Beyoncé",
    "duree": "3:53",
    "album": { "nom": "Cowboy Carter", "annee": 2024 },
    "genres": ["country", "pop"]
  }
]

src/App.vue :

<script setup>
  import playlist from '@/data/playlist.json'
</script>

<template>
  <main class="min-h-screen bg-stone-100 p-8">
    <h1 class="text-4xl font-bold text-zinc-900">Ma playlist</h1>
    <p class="mt-2 text-zinc-600">{{ playlist.length }} morceaux</p>

    <ol class="mt-8 max-w-2xl bg-white border border-zinc-200 divide-y divide-zinc-200">
      <li v-for="morceau in playlist" :key="morceau.id" class="flex items-center gap-4 px-6 py-4">
        <span class="w-6 text-zinc-400 tabular-nums">{{ morceau.id }}</span>
        <div class="flex-1">
          <p class="font-medium text-zinc-900">{{ morceau.titre }}</p>
          <p class="text-sm text-zinc-500">
            {{ morceau.artiste }} · {{ morceau.album.nom }}, {{ morceau.album.annee }}
          </p>
          <!-- Un v-for dans le v-for : les genres -->
          <div class="mt-2 flex flex-wrap gap-2">
            <span
              v-for="genre in morceau.genres"
              :key="genre"
              class="rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-amber-900"
            >
              {{ genre }}
            </span>
          </div>
        </div>
        <span class="text-sm text-zinc-500 tabular-nums">{{ morceau.duree }}</span>
      </li>
    </ol>
  </main>
</template>

Récap

  • Un objet regroupe les informations d'une même chose : des paires clé: valeur entre accolades.
  • On lit avec le point (creature.nom), avec des crochets quand la clé est dans une variable.
  • = modifie ou ajoute, delete retire. const n'empêche pas de modifier le contenu.
  • Les accès s'enchaînent de gauche à droite : creature.capacites[0].nom. Une clé absente vaut undefined ; aller plus loin plante.
  • JSON est du texte aux règles strictes : guillemets doubles, pas de virgule finale, pas de fonction. JSON.parse et JSON.stringify font le pont.
  • Dans Vite, import creatures from '@/data/creatures.json' donne directement le tableau d'objets.
  • Dans Vue : {{ creature.nom }} entre deux balises, :src="creature.image" dans un attribut, v-for et :key pour une liste, une prop Object pour passer un objet entier. Modifier l'objet, c'est le chapitre Ref.