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>
Convention de nommage : PascalCase dans le JS (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>`
};
Conseil : Préférez l'enregistrement local pour de meilleures performances (tree-shaking) et une meilleure lisibilité. Utilisez le global uniquement pour des composants véritablement utilisés partout (ex : icônes, boutons de base).

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. -->
Règle d'or : Ne JAMAIS modifier une prop directement dans l'enfant ! C'est une violation du flux de données. Si vous avez besoin d'une valeur modifiable, copiez-la dans 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 });
    }
  }
};
Nommage : Convention kebab-case pour les événements personnalisés : 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>`
};
Réactivité : Par défaut, 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
    }
  }
};
Cas d'usage : provide/inject est idéal pour les configurations globales (thème, locale, données d'authentification) mais pour la gestion d'état complexe, préférez Pinia (V08).
📝 Exercices ▶ Mini-projet 🧠 QCM Suivant →