1. Créer et enregistrer un composant
Un composant Vue est un bloc réutilisable qui encapsule template, logique et style. Il existe deux façons de l'enregistrer : globalement (disponible partout) ou localement (disponible uniquement dans le composant parent).
Enregistrement global — app.component()
const app = Vue.createApp({});
// Enregistrement global : disponible dans toute l'app
app.component('MonBouton', {
template: `<button class="btn">
<slot />
</button>`
});
app.mount('#app');
<!-- Utilisation dans n'importe quel template -->
<MonBouton>Cliquez-moi</MonBouton>
<!-- ou en kebab-case -->
<mon-bouton>Cliquez-moi</mon-bouton>
MonBouton), kebab-case dans le HTML (<mon-bouton>). Vue accepte les deux dans les templates en-ligne.
Enregistrement local — components
const MonBouton = {
template: `<button class="btn"><slot /></button>`
};
const MaCard = {
components: { MonBouton }, // enregistrement local
template: `
<div class="card">
<MonBouton>OK</MonBouton>
</div>`
};
Structure complète d'un composant
const MonComposant = {
// Données réactives locales
data() {
return { compteur: 0 };
},
// Propriétés reçues du parent
props: ['titre'],
// Événements émis vers le parent
emits: ['incrementer'],
// Méthodes
methods: {
incrementer() {
this.compteur++;
this.$emit('incrementer', this.compteur);
}
},
// Template HTML
template: `
<div>
<h3>{{ titre }}</h3>
<p>Compteur : {{ compteur }}</p>
<button @click="incrementer">+</button>
</div>`
};
2. Props — passer des données vers l'enfant
Les props permettent au composant parent de transmettre des données vers l'enfant. C'est le flux de données descendant (parent → enfant).
Définition simple (tableau de strings)
const Carte = {
props: ['titre', 'description', 'actif'],
template: `
<div :class="['card', { active: actif }]">
<h3>{{ titre }}</h3>
<p>{{ description }}</p>
</div>`
};
Définition avancée (objet avec types)
const Carte = {
props: {
// Type simple
titre: String,
// Type + required
id: {
type: Number,
required: true
},
// Type + valeur par défaut
couleur: {
type: String,
default: 'blue'
},
// Plusieurs types acceptés
valeur: {
type: [String, Number],
default: 0
},
// Objet avec valeur par défaut (factory function !)
config: {
type: Object,
default: () => ({ taille: 'md', arrondi: true })
},
// Validation personnalisée
statut: {
type: String,
validator: (val) => ['actif', 'inactif', 'brouillon'].includes(val)
}
},
template: `<div>{{ titre }}</div>`
};
Passage de props depuis le parent
<!-- Valeurs statiques (string) -->
<Carte titre="Mon titre" couleur="green" />
<!-- Valeurs dynamiques (v-bind) -->
<Carte :titre="article.titre" :id="article.id" :actif="true" />
<!-- Passage de tout un objet avec v-bind -->
<Carte v-bind="article" />
<!-- équivalent à :titre="article.titre" :id="article.id" etc. -->
data() ou utilisez les emits.
// ❌ Mauvais : mutater une prop
props: ['valeur'],
methods: {
reset() { this.valeur = 0; } // Erreur Vue !
},
// ✅ Bon : copier en data locale
props: ['valeurInitiale'],
data() {
return { valeurLocale: this.valeurInitiale };
},
methods: {
reset() { this.valeurLocale = 0; } // OK
}
3. Emits — communiquer vers le parent
Les emits permettent à l'enfant d'envoyer des messages/événements vers le parent. C'est le flux de données remontant (enfant → parent).
Déclarer et émettre un événement
const BoutonAimer = {
emits: ['aimer'], // Déclaration explicite (bonne pratique)
data() {
return { aime: false };
},
methods: {
basculer() {
this.aime = !this.aime;
this.$emit('aimer', this.aime); // Émission avec payload
}
},
template: `
<button @click="basculer">
{{ aime ? '❤️' : '🤍' }} Aimer
</button>`
};
Écouter dans le parent
<BoutonAimer @aimer="gererAimer" />
<!-- ou avec une expression inline -->
<BoutonAimer @aimer="(val) => console.log('Aimé ?', val)" />
// Dans le parent
methods: {
gererAimer(estAime) {
console.log('Aimé :', estAime);
this.articles[0].aime = estAime;
}
}
Déclaration avec validation
const Formulaire = {
emits: {
// Pas de validation
annuler: null,
// Avec validation du payload
soumettre: (donnees) => {
if (!donnees.email) {
console.warn('Email manquant dans l\'événement soumettre');
return false; // Avertissement (n'empêche pas l'émission)
}
return true;
}
},
methods: {
onSubmit() {
this.$emit('soumettre', { email: this.email, nom: this.nom });
}
}
};
mise-a-jour, element-supprime. Évitez les noms déjà utilisés par le DOM natif comme click, input.
4. Slots — projeter du contenu
Les slots permettent au composant parent d'injecter du contenu HTML dans le template de l'enfant. C'est le mécanisme de composition en Vue.
Slot par défaut
const Alerte = {
props: ['type'],
template: `
<div :class="['alerte', 'alerte-' + type]">
<slot /> <!-- Contenu injecté ici -->
</div>`
};
<Alerte type="succes">
<strong>Bravo !</strong> Votre formulaire a été envoyé.
</Alerte>
Contenu par défaut du slot
const Bouton = {
template: `
<button class="btn">
<slot>Cliquer ici</slot> <!-- Contenu de repli -->
</button>`
};
// <Bouton /> → "Cliquer ici"
// <Bouton>Valider</Bouton> → "Valider"
Slots nommés
const Modal = {
template: `
<div class="modal">
<div class="modal-header">
<slot name="header">Titre par défaut</slot>
</div>
<div class="modal-body">
<slot /> <!-- slot par défaut -->
</div>
<div class="modal-footer">
<slot name="footer" />
</div>
</div>`
};
<Modal>
<template #header>
<h2>Confirmation</h2>
</template>
<!-- Slot par défaut (contenu direct) -->
<p>Voulez-vous vraiment supprimer cet élément ?</p>
<template #footer>
<button @click="fermer">Annuler</button>
<button @click="confirmer" class="btn-danger">Supprimer</button>
</template>
</Modal>
Scoped slots — données de l'enfant vers le parent
const ListeItems = {
data() {
return { items: ['Pomme', 'Banane', 'Cerise'] };
},
template: `
<ul>
<li v-for="(item, i) in items" :key="i">
<slot :item="item" :index="i" />
</li>
</ul>`
};
<ListeItems>
<!-- On récupère les données du slot -->
<template #default="{ item, index }">
<span>{{ index + 1 }}. {{ item }}</span>
</template>
</ListeItems>
5. v-model sur composant
Pour créer un composant qui supporte v-model (liaison bidirectionnelle), il faut implémenter le contrat :modelValue + @update:modelValue.
Comment fonctionne v-model
<!-- Ces deux lignes sont équivalentes : -->
<MonInput v-model="texte" />
<MonInput :modelValue="texte" @update:modelValue="texte = $event" />
Implémenter le composant
const MonInput = {
props: {
modelValue: String // Reçoit la valeur depuis le parent
},
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
class="form-input"
/>`
};
<!-- Utilisation -->
<MonInput v-model="nomUtilisateur" />
<p>Bonjour, {{ nomUtilisateur }} !</p>
Plusieurs v-model (Vue 3 uniquement)
const NomComplet = {
props: {
prenom: String,
nom: String
},
emits: ['update:prenom', 'update:nom'],
template: `
<div class="form-row">
<input
:value="prenom"
@input="$emit('update:prenom', $event.target.value)"
placeholder="Prénom"
/>
<input
:value="nom"
@input="$emit('update:nom', $event.target.value)"
placeholder="Nom"
/>
</div>`
};
<NomComplet
v-model:prenom="utilisateur.prenom"
v-model:nom="utilisateur.nom"
/>
6. provide / inject — partage sans prop drilling
Le prop drilling est le problème de passer des props à travers plusieurs niveaux de composants. provide/inject résout cela en créant un "canal" direct entre ancêtre et descendant.
Sans provide/inject (prop drilling)
// ❌ Prop drilling : A → B → C → D juste pour passer "theme"
const ComposantA = { /* fournit theme */ };
const ComposantB = { props: ['theme'], /* passe à C */ };
const ComposantC = { props: ['theme'], /* passe à D */ };
const ComposantD = { props: ['theme'], /* ENFIN utilise theme */ };
Avec provide/inject
// ✅ ComposantA fournit directement à ComposantD
const ComposantA = {
data() {
return { theme: 'sombre', utilisateur: { nom: 'Alice' } };
},
provide() {
// IMPORTANT : utiliser une fonction pour accéder à this.data
return {
theme: this.theme,
utilisateur: this.utilisateur
};
},
template: `<div><ComposantB /></div>`
};
const ComposantD = {
inject: ['theme', 'utilisateur'],
template: `<p>Thème: {{ theme }}, User: {{ utilisateur.nom }}</p>`
};
provide ne maintient pas la réactivité ! Si theme change dans A, D ne sera pas mis à jour. Pour conserver la réactivité, fournissez une référence réactive ou utilisez computed().
provide réactif avec computed
// Vue 3 — provide réactif
const app = Vue.createApp({
data() {
return { themeActuel: 'sombre' };
},
provide() {
return {
// Vue.computed() maintient la réactivité
theme: Vue.computed(() => this.themeActuel)
};
}
});
// Inject avec valeur par défaut
const ComposantD = {
inject: {
theme: {
default: 'clair' // Valeur de repli si rien n'est fourni
}
}
};