Passer au contenu

Principes fondamentaux des composants

Les composants nous permettent de fractionner l'UI en morceaux indépendants et réutilisables, sur lesquels nous pouvons réfléchir de manière isolée. Il est courant pour une application d'être organisée en un arbre de composants imbriqués.

Component Tree

Cette approche est très similaire à celle d'imbriquer des éléments HTML natifs, mais Vue implémente son propre modèle de composant, nous permettant d'encapsuler du contenu et de la logique au sein de chaque composant. Vue fonctionne également bien avec les Web Components natifs. Pour en savoir plus sur la relation entre les composants Vue et les Web Components natifs, lisez ceci.

Définir un composant

Lorsqu'on utilise des outils de build, on définit généralement chaque composant Vue dans un fichier dédié en utilisant l'extension .vue - aussi appelé Composant monofichier (ou Single-File Components en anglais, abrégé SFC) :

vue
<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>
vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>

Sans outils de build, un composant Vue peut être défini comme un simple objet JavaScript contenant des options spécifiques à Vue :

js
export default {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
}
js
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return { count }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
  // Peut également cibler un template dans le DOM :
  // `template: '#my-template-element'`
}

Le template est écrit telle une chaîne de caractère JavaScript que Vue va compiler à la volée. Vous pouvez aussi utiliser un sélecteur d'ID pointant sur un élément (généralement des éléments natifs <template>) - Vue utilisera son contenu comme la source du template.

L'exemple ci-dessus définit un composant et l'exporte comme l'export par défaut d'un fichier .js, mais vous pouvez utiliser les exports nommés pour exporter plusieurs composants à partir d'un même fichier.

Utiliser un composant

TIP

Nous allons utiliser la syntaxe SFC dans le reste de ce guide - les concepts des composants sont les mêmes que vous utilisiez des outils de build ou non. La section Exemples illustre l'utilisation des composants dans les deux scénarios.

Afin d'utiliser un composant enfant, nous devons l'importer dans le composant parent. En supposant que nous ayons placé notre composant compteur dans un fichier nommé ButtonCounter.vue, le composant apparaîtra comme l'export par défaut du fichier :

vue
<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  }
}
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>

Pour exposer le composant importé à notre template, nous devons l'enregistrer via l'option components. Le composant sera alors utilisable grâce à une balise portant la clé utilisée lors de l'enregistrement.

vue
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>

Avec <script setup>, les composants importés sont directement rendus accessibles au template.

Il est également possible d'enregistrer globalement un composant, le rendant alors accessible à tous les composants d'une application sans avoir à l'importer. Les pour et contre d'un enregistrement global vs. local sont abordés dans la section Enregistrement des Composants dédiée.

Vous pouvez réutiliser les composants autant de fois que vous voulez :

template
<h1>Here are many child components!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

Notez que lorsque vous cliquez sur les boutons, chacun d'entre eux maintient son propre count individuel. Cela s'explique par le fait que chaque fois que vous utilisez un composant, une nouvelle instance de ce dernier est créée.

Dans les SFC, il est recommandé d'utiliser des noms de balise en casse Pascal (PascalCase) pour les composants enfants afin de les différencier des éléments HTML natifs. Bien que les noms des balises HTML natifs soient insensibles à la casse, les SFC de Vue sont un format compilé, donc nous pouvons y utiliser des noms de balise sensibles à la casse. Nous pouvons également utiliser /> pour fermer une balise.

Si vous éditez vos templates directement dans un DOM (par exemple comme le contenu d'un élément natif <template>), le template sera soumis au parsing HTML par défaut du navigateur. Dans ces cas de figure, vous aurez besoin d'utiliser la casse kebab (kebab-case) et de fermer explicitement les balises pour vos composants :

template
<!-- Si le template est écrit dans le DOM -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

Voir les mises en garde concernant l'analyse du template DOM pour plus de détails.

Passer des props

Si nous construisons un blog, il est probable que nous ayons besoin d'un composant pour représenter un article du blog. Nous voulons que tous les articles partagent la même mise en page, mais avec un contenu différent. Un tel composant ne sera utile que si vous pouvez lui passer des données, comme le titre et le contenu d'un article spécifique que l'on voudrait afficher. C'est là que les props entrent en jeu.

Les props sont des attributs personnalisés que l'on peut enregistrer sur un composant. Pour passer un titre au composant article de notre blog, nous devons le déclarer dans la liste des props que ce composant accepte, en utilisant l'option props.definePropsune macro :

vue
<!-- BlogPost.vue -->
<script>
export default {
  props: ['title']
}
</script>

<template>
  <h4>{{ title }}</h4>
</template>

Lorsqu'une valeur est passée à un attribut prop, il devient une propriété de l'instance du composant. La valeur de cette propriété est accessible à l'intérieur du template et dans le contexte this du composant, tout comme n'importe quelle autre de ses propriétés.

vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>

defineProps est une macro de compilation qui est seulement accessible à l'intérieur de <script setup> et ne nécessite pas d'être explicitement importée. Les props déclarées sont automatiquement exposées au template. defineProps retourne également un objet contenant toutes les propriétés passées au composant, de manière à ce que l'on puisse y accéder en JavaScript si nécessaire :

js
const props = defineProps(['title'])
console.log(props.title)

Voir aussi : Typer les props d'un composant

Si vous n'utilisez pas <script setup>, les propriétés doivent être déclarées via l'option props, et l'objet props sera passée à setup() en premier argument :

js
export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

Un composant peut avoir autant de props que vous voulez, et par défaut, n'importe quelle valeur peut être passée à une prop.

Une fois qu'une prop est enregistrée, vous pouvez lui passer des données via un attribut personnalisé, de cette manière :

template
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

Toutefois, dans une application classique, vous auriez sûrement un tableau d'article dans votre composant parent :

js
export default {
  // ...
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}
js
const posts = ref([
  { id: 1, title: 'My journey with Vue' },
  { id: 2, title: 'Blogging with Vue' },
  { id: 3, title: 'Why Vue is so fun' }
])

Puis vous voudriez rendre un composant pour chacun d'entre eux, grâce à v-for :

template
<BlogPost
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
 />

Remarquez comment v-bind est utilisé pour passer des valeurs de props dynamiques. Cela est particulièrement utile lorsque vous ne connaissez pas le contenu exact que vous allez rendre au fur et à mesure du temps.

Pour le moment, c'est tout ce dont vous avez besoin concernant les props, mais une fois que vous aurez terminé de lire cette page et vous sentirez à l'aise avec son contenu, nous vous recommandons de revenir afin de lire le guide complet sur les Props.

Écouter des événements

Au fur et à mesure que nous développons notre composant <BlogPost>, certaines fonctionnalités peuvent nécessiter de communiquer avec le parent. Par exemple, on peut décider d'inclure une fonctionnalité d'accessibilité permettant d'agrandir le texte des articles du blog, tout en laissant la taille par défaut sur le reste de la page.

Dans le parent, nous pouvons développer cette fonctionnalité en ajoutant une propriété de donnéesref postFontSize :

js
data() {
  return {
    posts: [
      /* ... */
    ],
    postFontSize: 1
  }
}
js
const posts = ref([
  /* ... */
])

const postFontSize = ref(1)

Qui pourra être utilisée dans le template afin de contrôler la taille de police de tous les articles du blog :

template
<div :style="{ fontSize: postFontSize + 'em' }">
  <BlogPost
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
   />
</div>

Maintenant ajoutons un bouton dans le template du composant <BlogPost> :

vue
<!-- BlogPost.vue, omitting <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button>Enlarge text</button>
  </div>
</template>

Pour le moment le bouton ne fait rien - nous voulons que le clique communique au parent qu'il doit agrandir le texte de tous les articles. Pour résoudre ce problème, les composants fournissent un système personnalisé d'événements. Le parent peut choisir d'écouter n'importe quel événement de l'instance du composant enfant grâce à v-on ou @, comme nous le ferions avec un événement natif du DOM :

template
<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

Ensuite le composant enfant peut émettre lui-même un événement en appelant la méthode intégrée $emit, et en lui passant le nom de l'événement :

vue
<!-- BlogPost.vue, en omettant <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

Grâce à l'écouteur @enlarge-text="postFontSize += 0.1", le parent va recevoir l'événement et mettre à jour la valeur de postFontSize.

Nous pouvons, de manière facultative, déclarer les événements émis en utilisant l'option emitsdefineEmitsune macro :

vue
<!-- BlogPost.vue -->
<script>
export default {
  props: ['title'],
  emits: ['enlarge-text']
}
</script>
vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

Cela documente tous les événements qu'un composant émet et peut éventuellement les valider. Cela permet également à Vue d'éviter de les appliquer implicitement en tant qu'écouteurs natifs à l'élément racine du composant enfant.

Comme c'est le cas pour defineProps, defineEmits n'est utilisable que dans <script setup> et ne nécessite pas d'être importé. Une fonction emit est retournée, similaire à la méthode $emit. Cela peut être utilisé pour émettre des événements dans la section <script setup> d'un composant, où $emit n'est pas directement accessible :

vue
<script setup>
const emit = defineEmits(['enlarge-text'])

emit('enlarge-text')
</script>

Voir aussi : Typer les emits d'un composant

Dans le cas où vous n'utilisez pas <script setup>, vous pouvez déclarer les événements émis en utilisant l'option emits. Vous pouvez accéder à la fonction emit via une propriété du contexte du setup (passé à setup() en deuxième argument) :

js
export default {
  emits: ['enlarge-text'],
  setup(props, ctx) {
    ctx.emit('enlarge-text')
  }
}

Pour le moment, c'est tout ce dont vous avez besoin concernant les événements personnalisés de composants, mais une fois que vous aurez terminé de lire cette page et vous sentirez à l'aise avec son contenu, nous vous recommandons de revenir afin de lire le guide complet sur la Gestion des événements.

Distribution de contenu avec les slots

Comme pour les éléments HTML, il est souvent utile de pouvoir passer du contenu à un composant, de cette manière :

template
<AlertBox>
  Quelque chose de grave s'est produit.
</AlertBox>

Ce qui devrait rendre :

Il s'agit d'une Erreur à des Fins de Démonstration

Quelque chose de grave s'est produit.

Cela peut être réalisé en utilisant l'élément personnalisé de Vue <slot> :

vue
<template>
  <div class="alert-box">
    <strong>Il s'agit d'une Erreur à des Fins de Démonstration</strong>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  /* ... */
}
</style>

Comme vous le verrez, nous utilisons <slot> comme un espace réservé où nous voulons que le contenu aille – et c'est tout. Nous avons terminé !

Pour le moment, c'est tout ce dont vous avez besoin concernant les slots, mais une fois que vous aurez terminé de lire cette page et vous sentirez à l'aise avec son contenu, nous vous recommandons de revenir afin de lire le guide complet sur les Slots.

Composants dynamiques

Parfois, il peut être utile d'alterner dynamiquement entre des composants, comme dans une interface avec des onglets :

Cela est rendu possible par l'élément <component> de Vue avec l'attribut spécial is :

template
<!-- Le composant change lorsque currentTab change -->
<component :is="currentTab"></component>
template
<!-- Le composant change lorsque currentTab change -->
<component :is="tabs[currentTab]"></component>

Dans l'exemple ci-dessus, la valeur passée à :is peut contenir au choix :

  • une chaîne de caractères représentant le nom d'un composant enregistré, OU
  • le véritable objet composant importé

Vous pouvez également utiliser l'attribut is pour créer des éléments HTML classiques.

Lorsqu'on alterne entre plusieurs composants avec <component :is="...">, seul celui sélectionné par :is reste monté. On peut forcer les composants inactifs à rester "en vie" grâce au composant intégré <KeepAlive>.

Mises en garde concernant l'analyse du template DOM

Si vous écrivez vos templates Vue directement dans le DOM, Vue va devoir extraire du DOM la chaîne de caractère représentant le template. Cela entraîne quelques avertissements à cause du comportement d'analyse du HTML natif des navigateurs.

TIP

Il est important de rappeler que les limitations que nous allons aborder ne s'appliquent que lorsque vous écrivez vos templates directement dans le DOM. Elles ne s'appliquent PAS si vous utilisez des templates en chaîne de caractères à partir des sources suivantes :

  • Composants Monofichiers
  • Chaînes de caractères représentant le template écrites en ligne (par exemple template: '...')
  • <script type="text/x-template">

Insensibilité de la casse

Les balises HTML et les noms des attributs sont insensibles à la casse, donc les navigateurs interpréteront une lettre majuscule comme une lettre minuscule. Cela signifie que lorsque vous utilisez des templates dans le DOM, les noms des composants en casse Pascal et les noms des propriétés en casse Camel ou encore les noms des événements v-on doivent tous utiliser leurs équivalent en casse Kebab (séparation par un trait d'union) :

js
// casse Camel en JavaScript
const BlogPost = {
  props: ['postTitle'],
  emits: ['updatePost'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
}
template
<!-- casse Camel en HTML -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

Balises auto-fermantes

Nous avons utilisé des balises auto-fermantes pour les composants dans les exemples de code précédents :

template
<MyComponent />

Ceci s'explique par le fait que l'outil d'analyse d'un template Vue respecte /> comme une indication de fin de balise, peu importe son type.

Dans les templates du DOM, cependant, nous devons toujours inclure des fermetures de balise explicites :

template
<my-component></my-component>

Cela est dû aux spécifications du HTML qui n'autorisent que quelques éléments spécifiques à omettre la fermeture des balises, les plus communs étant <input> et <img>. Pour tous les autres éléments, si vous omettez de fermer les balises, l'outil d'analyse du HTML natif pensera que vous n'avez jamais terminé leur ouverture. Par exemple, le bout de code suivant :

template
<my-component /> <!-- nous voulons fermer la balise ici... -->
<span>hello</span>

Sera analysé comme :

template
<my-component>
  <span>hello</span>
</my-component> <!-- mais le navigateur le fermera ici -->

Restrictions pour le placement des éléments

Certains éléments HTML, comme <ul>, <ol>, <table> et <select> ont des restrictions concernant quels éléments ils peuvent contenir, et certains éléments comme <li>, <tr>, et <option> ne peuvent apparaître qu'à l'intérieur de certains éléments.

Cela va entraîner des problèmes lorsque nous allons utiliser des composants avec des éléments qui ont ce genre de restrictions. Par exemple :

template
<table>
  <blog-post-row></blog-post-row>
</table>

Le composant personnalisé <blog-post-row> sera relevé comme contenu invalide, ce qui peut causer des erreurs dans le résultat rendu final. Nous pouvons utiliser l'attribut spécial is comme solution :

template
<table>
  <tr is="vue:blog-post-row"></tr>
</table>

TIP

Lorsqu'il est utilisé sur des éléments HTML natifs, la valeur de is doit être préfixée de vue: afin d'être interprétée comme un composant Vue. Cela est nécessaire afin d'éviter les confusions avec les éléments personnalisés intégrés.

C'est tout ce que vous avez besoin de savoir à propos des mises en garde concernant l'analyse du template DOM pour le moment - et d'ailleurs, la fin des Essentiels de Vue. Félicitations ! Il y a encore à apprendre, mais d'abord, nous vous recommandons de prendre une pause afin d'expérimenter Vue par vous-même - construisez quelque chose d'amusant, ou découvrez certains des Exemples si ça n'est pas déjà fait.

Dès que vous vous sentez à l'aide avec le savoir que vous venez de digérer, avancez dans le guide pour découvrir les composants en profondeur.

Principes fondamentaux des composantsa chargé