Vue.js

Composants

Créer et organiser des composants Vue 3 réutilisables avec props, événements et slots

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 dans components: {}.

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, utilisez withDefaults(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 en kebab-case dans 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 dans src/types/index.ts. Les types de props (ShowCardProps, ShowListProps) se définissent directement dans le fichier du composant — ils lui appartiennent.