Formation Vue 3 | Transmettre des Données entre Composants

Apprenez à transmettre des données entre composants Vue grâce aux props, événements (emits) et slots.

Icône de calendrier
Intermédiaire
10 chapitres

Props

Principe

S’il n’était pas possible de passer des informations sur-mesure à un composant, alors leur utilisation serait limitée…

Imaginons avoir par exemple un composant SuperAlert dont le rôle est d’afficher un message d’alerte sur une page.

SuperAlert.vue
copié !
<template>
	<div class="super-alert">
		Mot de passe erroné
	</div>
</template>

<style scoped>
	.super-alert {
		padding: .75rem 1rem;
		background-color: red;
		border-radius: 5px;
	}
</style>
copié !
<script>
	import SuperAlert from './SuperAlert.vue'

	export default {
		components: {
			SuperAlert
		},
	}
</script>

<template>
	<SuperAlert>
</template>

Ici, le message de l’alerte est écrit en dur, il restera ainsi toujours Mot de passe erroné… De même pour la couleur de fond qui sera toujours red. L’exploitation et la réutilisation de ce composant est très discutable !

Pour pallier à cela, nous allons pouvoir transmettre des données au composant lors de son appel via des props.

Les props sont des attributs spécifiques aux composants qui permettent de transférer des données au composant.

Envoi de props (parent)

Pour envoyer des données statiques (chaîne de caractères), il suffit de déclarer votre prop comme un attribut classique et de lui associer la valeur à transmettre au composant.

ParentComponent.vue
copié !
<template>
	<SuperAlert
		message="Vous avez un nouveau message."
		bg-color="green"
		text-color="#fff"
	/>
	<SuperAlert
		message="Mauvais mot de passe."
		bg-color="red"
		text-color="#fff"
	/>
</template>

Pour envoyer des données dynamiques (expression JavaScript), il faudra alors déclarer votre attribut en le « bindant » avec v-bind ou :.

ParentComponent.vue
copié !
<template>
	<SuperAlert
		:message="alert.message"
		:bg-color="alert.bgColor"
		:text-color="alert.textColor"
	/>
</template>

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

	export default {
		data() {
			return {
				alert: {
					message: "Test",
					bgColor: "green",
					textColor: "#fff"
				}
			}
		},
		components: {
			SuperAlert
		},
	}
</script>

Réception des props (enfant)

Pour réceptionner les données passées en props, il suffit de déclarer dans les options du composant la nouvelle propriété props. Cette propriété peut simplement contenir dans un tableau de chaînes de caractères les props passées.

SuperAlert.vue
copié !
<script>
	export default {
		props: ['message', 'bgColor', 'textColor'],
	}
</script>

<template>
	<div class="super-alert" :style="{ backgroundColor: bgColor, color: textColor }">
		{{ message }}
	</div>
</template>

<style scoped>
	.super-alert {
		padding: .75rem 1rem;
		border-radius: 5px;
	}
</style>

En revanche, il est préférable d’être plus rigoureux en détaillant pour chaque prop le type de valeur attendu. On parle de validation.

SuperAlert.vue
copié !
<script>
	export default {
		props: {
			message: String,
			bgColor: String,
			textColor: String
		},
	}
</script>

<template>
	<div class="super-alert" :style="{ backgroundColor: bgColor, color: textColor }">
		{{ message }}
	</div>
</template>

<style scoped>
	.super-alert {
		padding: .75rem 1rem;
		border-radius: 5px;
	}
</style>
Aller plus loin avec les validations

Check de type :

copié !
	export default {
	props: {
		//  (`null` and `undefined` values will allow any type)
		propA: String,
		// Multiple possible types
		propB: [String, Number],
	}
}

Prop requise :

copié !
export default {
	props: {
		// Required string
		propC: {
			type: String,
			required: true
		},
	}
}

Valeur par défaut :

copié !
export default {
	props: {
		// Number with a default value
		propD: {
			type: Number,
			default: 100
		},
		// Object with a default value
		propE: {
			type: Object,
			// Object or array defaults must be returned from
			// a factory function. The function receives the raw
			// props received by the component as the argument.
			default(rawProps) {
				return { message: 'hello' }
			}
		}
	}
}

Validateur personnalisé :

copié !
export default {
	props: {
		// Custom validator function
		propF: {
			validator(type) {
				// The type must match one of these strings
				return ['success', 'warning', 'danger'].includes(type)
			}
		}
	}
}

Quelques remarques :

  • Toutes les props sont facultatives par défaut, sauf si required: true est spécifié.
  • Une prop facultative aura une valeur undefined (hormis Boolean qui vaudra false).

Evènements (emits)

Principe

Parfois, il peut être nécessaire pour un composant enfant de communiquer avec son composant parent : cette mécanique est rendue possible par ce que l’on appelle les « emits ».

Cette communication se traduit la plupart du temps par le besoin de transmettre des données à un composant parent.

Concrètement :

  1. Un composant enfant émet un évènement
  2. Le composant parent est notifié lors de l’émission d’un évènement

Emettre un évènement (enfant)

Pour émettre un évènement depuis le composant enfant on utilise la méthode $emit(), dans laquelle on renseigne le nom d’un évènement sur mesure que l’on souhaite écouter.

Cela peut se faire directement depuis le template :

ChildComponent.vue
copié !
<template>
	<button @click="$emit('myCustomEvent')">Cliquez moi</button>
</template>

Cela peut aussi se faire depuis un script avec this :

ChildComponent.vue
copié !
<template>
	<button @click="demo()">Cliquez moi</button>
</template>

<script>
export default {
  methods: {
    demo() {
      this.$emit('myCustomEvent')
    }
  }
}
</script>

À chaque click sur le bouton, l’évènement myCustomEvent sera émis au composant parent. Le composant parent sera notifié que l’évènement a été déclenché dans le composant enfant et pourra réagir en fonction.

Dans le cas où vous souhaiteriez également transmettre au composant parent des données, il sera possible de les passer via des arguments additionnels à la méthode $emit().

copié !
$emit('myCustomEvent', data1, data2, ...)

Ecouter l’évènement émis (parent)

Du côté du composant parent, on écoute l’évènement émis par le composant enfant et on exécute la logique voulue lorsqu’il est déclenché.

ParentComponent.vue
copié !
<template>
	<ChildComponent @my-custom-event="..."/>
</template>

<script>
import ChildComponent from '@/components/ChildComponent.vue'

export default {
	components: {
		ChildComponent
	},
	data() {
		return {
			clicks: 0
		}
	}
}
</script>

Si vous souhaitez récupérer des données transmises par l’enfant avec par exemple $emit('myCustomEvent', 1), vous avez le choix entre :

1. Une fonction fléchée au sein du template
ParentComponent.vue
copié !
<ChildComponent @my-custom-event="(value) => clicks += value" />
2. Une méthode au sein des scripts
ParentComponent.vue
copié !
<ChildComponent @my-custom-event="maMethode" />

Ensuite, la valeur sera automatiquement passée en tant que paramètre de cette méthode :

ParentComponent.vue
copié !
methods: {
  maMethode(value) {
    this.clicks += value
  }
}

Slots

Principe

Si nous devions faire passer l’intégralité des informations à notre composant, comme par exemple son contenu, via des props, cela s’avèrerait parfois fastidieux et/ou limitant. Reprenons l’exemple de notre composant d’alerte :

ParentComponent.vue
copié !
<SuperAlert
	message="Vous avez un nouveau message."
	bg-color="green"
	text-color="#fff"
/>

Les slots consistent à délimiter des zones avec les balises <slot></slot> dans un composant.

ParentComponent.vue
copié !
<template>
	<div class="super-alert" :style="{ backgroundColor: bgColor, color: textColor }">
		<slot></slot>
	</div>
</template>

Désormais, lors de l’usage de ce composant, il est possible de passer du contenu (textuel ou HTML) au slot, en l’insérant au sein des balises paires.

ParentComponent.vue
copié !
<SuperAlert
	bg-color="green"
	text-color="#fff"
>
	<i class="fas fa-exclamation-circle"></i>
	Vous avez un nouveau message.
</SuperAlert>

Cela nous permet ainsi de nous passer de la prop message et d’insérer du contenu dynamiquement au sein du composant. C’est finalement la manière naturelle de transmettre du contenu à une balise HTML, et ça, c’est plus propre !

Contenu par défaut

Il est également possible de spécifier dans le composant un contenu par défaut qui s’affichera si aucun contenu n’est spécifié à l’utilisation d’un composant :

ParentComponent.vue
copié !
<SuperAlert
	bg-color="green"
	text-color="#fff"
>
</SuperAlert>
SuperAlert.vue
copié !
<template>
	<div class="super-alert" :style="{ backgroundColor: bgColor, color: textColor }">
		<slot>Contenu par défaut</slot>
	</div>
</template>

Slots nommés

Il est tout à fait possible de déclarer plusieurs slot au sein d’un même composant. Prenons l’exemple d’un composant de type accordion utilisé dans le cadre d’une FAQ. On donne à l’utilisateur du composant la possibilité de spécifier un titre ainsi qu’une description de FAQ en faisant usage de 2 slots distincts.

Chaque slot devra alors être identifié. Cette identification se fera via l’usage d’un attribut name.

SuperAccordion.vue
copié !
<template>
	<div class="super-accordion">
		<header class="super-accordion-header" @click="opened=!opened">
			<h3><slot name="title"></slot></h3>
		</header>
		<div class="super-accordion-body" v-if="opened">
			<p><slot name="description"></slot></p>
		</div>
	</div>
</template>

<script>
	export default {
		data() {
			return {
				opened: false
			}
		}	
	}
</script>

Ainsi, côté composant parent, nous avons la possibilité de préciser quel contenu doit s’insérer dans quelle zone en combinant la balise <template> avec la directive v-slot.

ParentComponent.vue
copié !
<template>
	<SuperAccordion>
		<template v-slot:title>Peut-on retourner un article ?</template>
		<template v-slot:description>Vous avez 30 jours à compter de la réception de votre commande pour... blablabla.</template>
	</SuperAccordion>
</template>

La directive v-slot: est généralement raccourcie par le caractère #, donc <template v-slot:title> peut être simplifié par juste <template #title>.

Lorsqu’un composant accepte un slot par défaut et des slots nommés, tous les nœuds HTML n’étant pas situés au sein d’une balise <template> seront considérés comme destinés au slot par défaut.

SuperAccordion.vue
copié !
<template>
	<div class="super-accordion">
		<header class="super-accordion-header" @click="opened=!opened">
			<h3><slot name="title"></slot></h3>
		</header>
		<div class="super-accordion-body" v-if="opened">
			<p><slot></slot></p>
		</div>
	</div>
</template>

<script>
export default {
	data() {
		return {
			opened: false
		}
	}	
}
</script>

<style scoped>
	.super-accordion {
		border: solid #000 1px;
	}
	.super-accordion-header {
		background-color: #eee;
	}
	.super-accordion-header, .super-accordion-body {
		padding: .5rem 1rem;
	}
</style>
ParentComponent.vue
copié !
<template>
	<SuperAccordion>
		<template #title>Peut-on retourner un article ?</template>
		Vous avez 30 jours à compter de la réception... <!-- Va dans le second slot, non nommé -->
	</SuperAccordion>
</template>

Préprocesseurs, supersets et moteurs de templates

Qu’il s’agisse de HTML, CSS ou encore JS, les langages front web se voient souvent “dopés” par l’utilisation de technologies qui simplifient, dynamisent ou encore sécurisent leur écriture.

  • Pour HTML, on parle de moteurs de templates tel que Pug.
  • Pour CSS, on parle de préprocesseurs tels que Sass, Less ou encore Stylus.
  • Pour JavaScript, on parle de superset tel que TypeScript.

Pour activer le support de la technologie en question, il faut :

  1. Installer et configurer les préprocesseurs en question via le gestionnaire de paquet et l’outil de build de votre choix (Vite, Webpack…).
  2. Ajouter l’attribut lang au sein des balises <template>, <script> ou <style> avec pour valeur, le préprocesseur à utiliser.

Quelques exemples :

Support de Pug
copié !
<template lang="pug">
	...
</template>
Support de Sass
copié !
<style lang="scss">
	...
</style>
Support de TypeScript
copié !
<script lang="ts">
	...
</script>