Composants
Dans ce chapitre, on continue de construire notre Show Watchlist en découpant le composant du chapitre précédent en composants réutilisables : ShowCard et ShowList.
Le fichier de types
Avant de créer des composants, on centralise les interfaces TypeScript partagées dans un fichier dédié. Cela évite de redéfinir Show dans chaque composant.
// src/types/index.ts
export interface Show {
id: number
title: string
genre: string
year: number
seen: boolean
}
Le type Show est partagé entre tous les composants. Les types de props, eux, sont définis directement dans le fichier du composant concerné — ils lui sont propres et n'ont pas à être partagés.
import type { Show } from '@/types'
Création et utilisation d'un composant
Les composants sont des éléments réutilisables qui permettent de structurer une application Vue en morceaux indépendants. Chaque composant est un fichier .vue qui encapsule son template, sa logique et ses styles.
Reprenons l'exemple précédent et extrayons l'affichage d'une série dans un composant dédié.
<!-- src/components/ShowCard.component.vue -->
<template>
<div>
<p>{{ show.title }} ({{ show.year }}) — {{ show.genre }}</p>
<span v-if="show.seen">✓ Vu</span>
<span v-else>À voir</span>
<button @click="emit('toggle-seen', show)">Basculer</button>
</div>
</template>
<script setup lang="ts">
import type { Show } from '@/types'
interface ShowCardProps {
show: Show
}
defineProps<ShowCardProps>()
const emit = defineEmits<{ 'toggle-seen': [show: Show] }>()
</script>
<!-- src/App.vue -->
<template>
<div>
<ShowCard :show="shows[0]" @toggle-seen="onToggleSeen" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { Show } from '@/types'
import ShowCard from './components/ShowCard.component.vue'
const shows = ref<Show[]>([
{ id: 1, title: 'Inception', genre: 'Sci-Fi', year: 2010, seen: true },
])
const onToggleSeen = (show: Show) => {
show.seen = !show.seen
}
</script>
Remarque : Vue 3 avec
<script setup>importe automatiquement les composants dans le template dès qu'ils sont importés dans le script. Pas besoin de les déclarer danscomponents: {}.
Props : passer des données au composant enfant
Les props sont aux composants ce que les paramètres sont aux fonctions : elles permettent de passer des données depuis l'extérieur pour personnaliser le comportement.
Tout comme on ne peut pas modifier un paramètre de fonction depuis l'intérieur et s'attendre à ce que ça change la variable de l'appelant, un composant enfant ne doit jamais modifier directement une prop — c'est le parent qui en est propriétaire.
Composant enfant (ShowCard.component.vue)
<template>
<div>
<h3>{{ show.title }}</h3>
<p>{{ show.genre }} — {{ show.year }}</p>
<span v-if="show.seen">✓ Vu</span>
<span v-else>À voir</span>
</div>
</template>
<script setup lang="ts">
import type { Show } from '@/types'
interface ShowCardProps {
show: Show
}
defineProps<ShowCardProps>()
</script>
Composant parent (HomePage.vue)
<template>
<div>
<ShowCard
v-for="show in shows"
:key="show.id"
:show="show"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { Show } from '@/types'
import ShowCard from './components/ShowCard.component.vue'
const shows = ref<Show[]>([
{ id: 1, title: 'Inception', genre: 'Sci-Fi', year: 2010, seen: true },
{ id: 2, title: 'The Dark Knight', genre: 'Action', year: 2008, seen: false },
{ id: 3, title: 'Interstellar', genre: 'Sci-Fi', year: 2014, seen: false },
])
</script>
TypeScript : Définir les props avec
defineProps<ShowCardProps>()permet à TypeScript de valider les types à la compilation. Si vous devez donner des valeurs par défaut, utilisezwithDefaults(defineProps<ShowCardProps>(), { ... }).
Émission d'événements : du composant enfant vers le parent
Lorsqu'un composant enfant doit communiquer avec son parent, on utilise defineEmits<{}>() pour déclarer les événements émis.
Composant enfant (ShowCard.component.vue)
<template>
<div>
<h3>{{ show.title }}</h3>
<p>{{ show.genre }} — {{ show.year }}</p>
<span v-if="show.seen">✓ Vu</span>
<span v-else>À voir</span>
<button @click="emit('toggle-seen', show)">Basculer</button>
</div>
</template>
<script setup lang="ts">
import type { Show } from '@/types'
interface ShowCardProps {
show: Show
}
defineProps<ShowCardProps>()
const emit = defineEmits<{
'toggle-seen': [show: Show]
}>()
</script>
Composant parent (HomePage.vue)
<template>
<div>
<p v-if="lastToggled">Dernière série modifiée : {{ lastToggled.title }}</p>
<ShowCard
v-for="show in shows"
:key="show.id"
:show="show"
@toggle-seen="onToggleSeen"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { Show } from '@/types'
import ShowCard from './components/ShowCard.component.vue'
const shows = ref<Show[]>([
{ id: 1, title: 'Inception', genre: 'Sci-Fi', year: 2010, seen: true },
{ id: 2, title: 'The Dark Knight', genre: 'Action', year: 2008, seen: false },
])
const lastToggled = ref<Show | null>(null)
const onToggleSeen = (show: Show) => {
show.seen = !show.seen
lastToggled.value = show
}
</script>
Convention de nommage : Les événements sont déclarés en
kebab-case(toggle-seen) et écoutés enkebab-casedans le template parent (@toggle-seen).
Slots : injecter du contenu dans un composant
Les slots permettent au composant parent d'injecter du contenu HTML dans un emplacement défini par l'enfant.
Composant enfant avec slot (ShowList.component.vue)
<template>
<div>
<slot></slot>
<ul>
<li
v-for="show in shows"
:key="show.id"
:class="{ seen: show.seen }"
@click="emit('toggle-seen', show)"
>
{{ show.title }} — {{ show.year }}
<span v-if="show.seen">✓</span>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import type { Show } from '@/types'
interface ShowListProps {
shows: Show[]
}
defineProps<ShowListProps>()
const emit = defineEmits<{
'toggle-seen': [show: Show]
}>()
</script>
Composant parent utilisant le slot
<template>
<div>
<ShowList :shows="seenShows" @toggle-seen="onToggleSeen">
<h2>Séries vues ({{ seenShows.length }})</h2>
</ShowList>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { Show } from '@/types'
import ShowList from './components/ShowList.component.vue'
const shows = ref<Show[]>([
{ id: 1, title: 'Inception', genre: 'Sci-Fi', year: 2010, seen: true },
{ id: 2, title: 'The Dark Knight', genre: 'Action', year: 2008, seen: false },
{ id: 3, title: 'Interstellar', genre: 'Sci-Fi', year: 2014, seen: false },
])
const seenShows = computed(() =>
shows.value.filter((m) => m.seen)
)
const onToggleSeen = (show: Show) => {
show.seen = !show.seen
}
</script>
Exemple complet
Le même Show Watchlist qu'au chapitre précédent, refactorisé en composants. Chaque fichier a une responsabilité claire.
src/components/ShowCard.component.vue
<template>
<li :class="{ seen: show.seen }">
{{ show.title }} ({{ show.year }}) — {{ show.genre }}
<span v-if="show.seen">✓ Vu</span>
<span v-else>À voir</span>
<button @click="emit('toggle-seen', show)">Basculer</button>
</li>
</template>
<script setup lang="ts">
import type { Show } from '@/types'
interface ShowCardProps {
show: Show
}
defineProps<ShowCardProps>()
const emit = defineEmits<{
'toggle-seen': [show: Show]
}>()
</script>
src/components/ShowList.component.vue
<template>
<div>
<slot></slot>
<p v-if="shows.length === 0">Aucune série vue pour l'instant.</p>
<ul>
<ShowCard
v-for="show in shows"
:key="show.id"
:show="show"
@toggle-seen="emit('toggle-seen', $event)"
/>
</ul>
</div>
</template>
<script setup lang="ts">
import type { Show } from '@/types'
import ShowCard from './ShowCard.component.vue'
interface ShowListProps {
shows: Show[]
}
defineProps<ShowListProps>()
const emit = defineEmits<{
'toggle-seen': [show: Show]
}>()
</script>
src/pages/HomePage.vue
<template>
<div>
<ShowList :shows="shows" @toggle-seen="onToggleSeen">
<input v-model="search" placeholder="Rechercher une série..."/>
<p>{{ seenShows.length }} série(s) vue(s) sur {{ shows.length }}</p>
</ShowList>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import type { Show } from '@/types'
import ShowList from '../components/ShowList.component.vue'
const search = ref<string>('')
const shows = ref<Show[]>([
{ id: 1, title: 'Inception', genre: 'Sci-Fi', year: 2010, seen: true },
{ id: 2, title: 'The Dark Knight', genre: 'Action', year: 2008, seen: false },
{ id: 3, title: 'Interstellar', genre: 'Sci-Fi', year: 2014, seen: false },
{ id: 4, title: 'Parasite', genre: 'Thriller', year: 2019, seen: true },
{ id: 5, title: 'Dune', genre: 'Sci-Fi', year: 2021, seen: false },
{ id: 6, title: 'Everything Everywhere All at Once', genre: 'Action', year: 2022, seen: true },
])
const seenShows = computed(() =>
shows.value.filter((m) => m.seen)
)
watch(search, (newVal) => {
console.log('Recherche :', newVal)
})
onMounted(() => {
console.log('Application prête')
})
const onToggleSeen = (show: Show) => {
show.seen = !show.seen
}
</script>
Organisation des fichiers
src/
├── components/
│ ├── ShowCard.component.vue
│ └── ShowList.component.vue
├── pages/
│ └── HomePage.vue
├── types/
│ └── index.ts ← Show (modèles partagés)
├── App.vue
└── main.ts
Convention : Les modèles de données partagés (comme
Show) vont danssrc/types/index.ts. Les types de props (ShowCardProps,ShowListProps) se définissent directement dans le fichier du composant — ils lui appartiennent.