Appearance
6. Evènements personnalisés
Présentation
Dans le dialogue entre les composants de notre application, il arrive que nous ayons besoin de remonter des données à un composant parent, ou de signaler un évènement particulier se produisant dans un composant enfant.
Dans ce chapitre, nous verrons comment remonter des informations à des composants parents dans le respect du principe de flux de données unidirectionnel défendu par Vue.js. Nous appliquerons cela en réorganisant une partie de notre composant DogsGallery à l’aide d’un nouveau composant enfant.
Principe de flux de données unidirectionel
Afin de transmettre des données d’un composant enfant vers un parent, nous pourrions être tentés d’exploiter les props.
En effet, dans notre composant parent, nous pouvons fournir la valeur d’une donnée à la props d’un composant enfant, et utiliser la directive v-bind pour les connecter dynamiquement :
jsx
<template>
<div>
<ChildComponent v-bind:myProps="myData"/>
</div>
</template>
<script>
export default {
...
data() {
return {
myData: "Hello World"
}
}
}
</script>Ainsi, nous pourrions être tentés de modifier la valeur de la props depuis le composant enfant, dans l’espoir que la modification se propagera au niveau composant parent.
Toutefois, cette manière de faire n’est pas autorisée. Elle induit en effet que nos props peuvent faire circuler la donnée dans les deux sens en même temps, du parent vers l’enfant, et inversement.
Cela complexifie tout d’abord la compréhension du composant enfant. Rappelez-vous, les props sont une forme d’interface dans l’usage d’un composant. Elles vous permettent de rapidement implémenter un composant, que vous n’avez peut-être pas développé vous-même, sans avoir à connaître le détail de son code interne. Un développeur s’attend donc généralement à fournir des informations via les props, et non à en recevoir.
Par ailleurs, la circulation bidirectionnelle des données peut provoquer dans certains cas d’importants conflits entre composants parent et enfant, en plus de provoquer un couplage fort des deux composants. La modification d’une donnée peut notamment entraîner dans certaines configurations une boucle de retro-action dans laquelle il sera difficile de prédire l’ordre des exécutions, voire entraînant une boucle infinie.
Pour éviter ces problématiques, il est donc recommandé de maintenir un flux de données unidirectionnel entre les composants, allant toujours des parents vers les enfants.
Dans le cas où une information doit être remontée, il est nécessaire de passer par un système d’évènements, comme le mettent déjà en place les balises natives HTML. Pour cela, il est possible de créer nos propres évènements, qui répondront aux besoins propres à notre application.
Créer des évènements personnalisés
Pour émettre un évènement personnalisé dans un composant, il vous faut d’abord déclarer son nom dans le champ emit de la configuration du composant:
jsx
<script>
export default {
name: "ChildComponent",
emit: ["eventName"],
...
}
</script>Il vous est bien sûr possible d’avoir plusieurs évènements personnalisés pour un même composant en les listant tous dans le champ emit.
REMARQUE
👉 Notez que si le nom de votre évènement personnalisé est le même que celui d’un évènement natif, comme click par exemple, alors l’évènement personnalisé remplacera l’évènement natif. Cela signifie que le composant n’emmétra plus l’évènement au moment où il le faut nativement.
Ensuite, il vous suffit d’utiliser la méthode $emit de votre composant :
jsx
<script>
export default {
name: "ChildComponent",
emit: ["eventName"],
methods: {
emitCustomEvent: function() {
this.$emit("eventName", "Hello World")
}
}
}
</script>Cette commande émettra un évènement qui pourra être capté par un composant parent via le nom spécifié en premier argument :
jsx
<template>
<div>
<ChildComponent v-on:eventName="print"/>
</div>
</template>
<script>
export default {
name: "ParentComponent",
...
methods: {
print: function(value) {
console.log(value) // Hello World
}
}
}
</script>La fonction associée à l’évènement par le composant parent récupèrera en argument la donnée fournie lors du $emit par le composant enfant, ici “Hello World” .
REMARQUE
👉 Il est possible de ne pas fournir de données lors de l’émission d’un évènement personnalisé, ou d’en fournir plusieurs. Généralement, les données émises sont présentées sous la forme d’un objet unique (l’équivalent de l’élément $event, des évènements Javascript natifs).
L’usage de v-model
La directive v-model permet la synchronisation d’une donnée présente dans un élément de formulaire avec une donnée de nos composants :
jsx
<input v-model="myData"/>Mais il est également possible d’utiliser cette directive pour nos propres composants afin de permettre de la même façon la synchronisation d’une donnée entre un composant parent et enfant.
Contrairement aux props, cette syntaxe permet d’explicitement indiquer au composant parent que la valeur de donnée pourra être modifiée par son composant enfant. Cette échange de données est bien supporté car en arrière plan Vue.js redéfinit un évènement personnalisé qui permettra de faire remonter au composant parent que la donnée doit être modifiée, sans altérer le flux unidirectionnel des échanges.
Pour définir un v-model personnalisé, il faut à la fois définir une props et un évènement personnalisé du même nom :
jsx
<script>
export default {
name: "ChildComponent",
props: ['myModel'],
emits: ["update:myModel"],
...
}
</script>On voit ici que dans le cas de l’évènement personnalisé, il est nécessaire d’ajouter le préfixe update: pour signifier la liaison avec la props du même nom.
Lorsque l’on souhaite modifier la valeur associée au v-model, il suffit ensuite d’appeler l’évènement personnalisé avec $emit et de fournir la nouvelle valeur en second paramètre :
jsx
<script>
export default {
name: "ChildComponent",
props: ['myModel'],
emits: ["update:myModel"],
methods: {
emitCustomEvent: function() {
this.$emit("update:myModel", "Hello World")
}
}
}
</script>Il ne reste plus qu’à déclarer le v-model au niveau du composant parent pour profiter de la fonctionnalité :
jsx
<template>
<div>
<ChildComponent v-model:myModel="myData"/>
</div>
</template>
<script>
export default {
name: "ParentComponent",
...
data() {
return {
myData: ""
}
}
}
</script>La valeur de myData sera maintenue synchronisée entre le composant enfant et parent, qu’elle soit modifiée dans l’un ou l’autre des composants.
REMARQUE
👉 Un même composant pourra proposer plusieurs v-model en leur donnant simplement des noms différents lors de leur déclaration dans props et emits. Vous pouvez aussi imiter la directive v-model native, en déclarant un props et un emits nommés modelValue. Dans ce cas particulier, il sera possible d’appeler directement la directive v-model="myData" sans avoir à ajouter un suffixe particulier.
Conclusion
Grâce aux évènements personnalisés et la directive v-model, il nous est désormais possible de créer un composant enfant GalleryOptions à notre composant DogsGallery, qui se chargera de gérer notre menu d’options :
jsx
<template>
<div class="gallery-options">
<input type="text" :value="search" @input="onSearchChanged" placeholder="Chercher un chien">
<button v-if="search" @click="cleanSearch">X</button>
<label for="dog-sort">Trier par : </label>
<select :value="dogsSortType" @input="onDogsSortTypeChanged" id="dog-sort">
<option value="AZName">Noms de A à Z</option>
<option value="ZAName">Noms de Z à A</option>
<option value="AZBreed">Espèces de A à Z</option>
<option value="ZABreed">Espèces de Z à A</option>
</select>
</div>
</template>
<script>
export default {
name: 'GalleryOptions',
props: {
search: String,
dogsSortType: String
},
emits: ["update:search", "update:dogsSortType"]
watch: {
search: function(newSearch) {
localStorage.setItem("search", newSearch)
},
dogsSortType: function(newDogsSortType) {
localStorage.setItem("dogsSortType", newDogsSortType)
}
},
methods: {
cleanSearch: function() {
this.$emit('update:search', "")
},
onSearchChanged(event) {
this.$emit('update:search', event.target.value)
},
onDogsSortTypeChanged(event) {
this.$emit('update:dogsSortType', event.target.value)
},
}
}
</script>Nous allégeons ainsi le contenu de notre composant DogsGallery et respectons ainsi mieux le principe de séparation des préoccupations :
jsx
<template>
<div class="dogs-gallery">
<GalleryOptions v-model:search="search" v-model:dogsSortType="dogsSortType"/>
<div class="gallery">
<DogCard
v-for="dog in dogsOrganizedData"
:key="dog.id"
:firstname="dog.name"
:breed="dog.breed"
:pictureUrl="dog.picture"/>
</div>
</div>
</template>
<script>
import DogCard from './components/DogCard.vue'
import GalleryOptions from './components/GalleryOptions.vue'
import getDogsData from '@/services/api/dogsRepository.js'
export default {
name: 'DogsGallery',
components: {
DogCard,
GalleryOptions
},
created: function() {
this.retrieveDogsData()
},
computed: {
dogsOrganizedData: function() {
const field = ["AZName", "ZAName"].includes(this.dogsSortType) ? "name" : "breed"
const reversed = ["ZAName", "ZABreed"].includes(this.dogsSortType)
const filterFunc = (a) => a.name.toLowerCase().includes(this.search.toLowerCase())
const comparator = (a, b) => a[field].localeCompare(b[field])
let data = this.dogsData.filter(filterFunc)
data = data.sort(comparator)
if (reversed) data = data.reverse()
return data
}
},
data() {
return {
dogsData: [],
search: localStorage.getItem("search") || "",
dogsSortType: localStorage.getItem("dogsSortType") || "AZName"
}
},
methods: {
async retrieveDogsData() {
this.dogsData = await getDogsData()
}
}
}
</script>Nous pourrions encore subdiviser notre composant GalleryOptions en deux, entre la partie gérant la recherche, et l’autre l’ordonnancement. Cela séparerait encore davantage les responsabilités et les flux d‘informations dans notre application.
Nous en avons maintenant fini avec notre application pour les chiens du chenil. Il serait bien sûr possible d’enrichir de bien des façons notre application en se servant des nombreux concepts fondamentaux de Vue.js que nous avons vus.
Dans la prochaine partie, nous complèterons ce cours en abordant quelques notions de développement front-end plus avancées, comme la création d’un système de routage afin d’avoir plusieurs pages, ou encore d’éléments de data-visualisation.
Aller plus loin
- Sur les évènements personnalisés :

