Directives Vue et leurs modificateurs
Modificateurs pour événements clavier
L'écouteur d'événement @keyup.enter permet de réagir spécifiquement à l'appui sur la touche Entrée. Voici un exemple où l'on traite une saisie utilisateur :
<div id="app">
<label>Nom d'utilisateur :</label>
<input @keyup.enter="validerFormulaire" v-model="utilisateur" type="text">
<p>Saisie actuelle : {{ utilisateur }}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
utilisateur: ''
},
methods: {
validerFormulaire() {
console.log('Formulaire soumis avec la valeur :', this.utilisateur)
}
}
})
</script>
D'autres modificateurs courants incluent :
.trim: supprime les espaces inutiles des entrées liées à v-model..number: convertit automatiquement la saisie en nombre..stop: empêche la propagation d'un événement (stopPropagation)..prevent: annule le comportement par défaut d'un événement (preventDefault).
<div id="app">
<div @click="clicParent" class="conteneur">
<div @click.stop="clicEnfant" class="sous-conteneur">Cliquer ici</div>
</div>
<a @click.prevent href="https://example.com">Lien non fonctionnel</a>
</div>
<script>
new Vue({
el: '#app',
methods: {
clicParent() {
alert('Événement parent déclenché')
},
clicEnfant() {
alert('Événement enfant déclenché sans propagation')
}
}
})
</script>
Gestion dynamique des classes et styles avec v-bind
Liaison de classes CSS
La directive v-bind:class accepte un objet ou un tableau pour basculer des classes CSS dynamiquement :
<style>
.boite { width: 150px; height: 150px; border: 1px solid #333; }
.rouge { background-color: #ffcccc; }
.grand { transform: scale(1.5); }
</style>
<div id="app">
<div class="boite" :class="{ rouge: estActif, grand: estAgrandi }"></div>
<button @click="estActif = !estActif">Changer couleur</button>
<button @click="estAgrandi = !estAgrandi">Changer taille</button>
</div>
<script>
new Vue({
el: '#app',
data: {
estActif: false,
estAgrandi: false
}
})
</script>
Application pratique : navigation avec surbrillance
Créons une barre de navigation où l'élément sélectionné est mis en évidence grâce à une classe dynamique :
<ul id="nav">
<li v-for="(onglet, idx) in onglets" :key="onglet.id"
@click="indexActif = idx">
<a :class="{ actif: idx === indexActif }" href="#">{{ onglet.titre }}</a>
</li>
</ul>
<script>
new Vue({
el: '#nav',
data: {
indexActif: 0,
onglets: [
{ id: 101, titre: 'Accueil' },
{ id: 102, titre: 'Services' },
{ id: 103, titre: 'Contact' }
]
}
})
</script>
Liaison de styles en ligne
On peut aussi utiliser v-bind:style pour appliquer des styles CSS dynamiques, par exemple pour une barre de progression :
<div id="app">
<div class="barre-fond">
<div class="barre-progression" :style="{ largeur: progression + '%' }">
{{ progression }}%
</div>
</div>
<button @click="progression = Math.min(progression + 25, 100)">Augmenter</button>
<button @click="progression = Math.max(progression - 25, 0)">Réduire</button>
</div>
<script>
new Vue({
el: '#app',
data: {
progression: 30
}
})
</script>
Propriétés calculées (computed)
Syntaxe de base
Les propriétés calculées permettent de dériver des valeurs à partir de données réactives, avec mise en cache automatique :
<div id="app">
<ul>
<li v-for="article in articles" :key="article.ref">
{{ article.libelle }} : {{ article.quantite }}
</li>
</ul>
<p>Quantité totale : {{ totalQuantite }}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
articles: [
{ ref: 'A1', libelle: 'Livre', quantite: 3 },
{ ref: 'A2', libelle: 'Stylo', quantite: 10 },
{ ref: 'A3', libelle: 'Cahier', quantite: 5 }
]
},
computed: {
totalQuantite() {
return this.articles.reduce((somme, item) => somme + item.quantite, 0)
}
}
})
</script>
Comparaison avec les méthodes
Contrairement aux méthodes, les propriétés calculées sont mises en cache : leur fonction n'est exécutée que lorsque les données dépendantes changent, ce qui améliore les performances pour les calculs coûteux.
Écriture complète avec getter et setter
Une propriété calculée peut inclure un setter pour réagir aux modifications :
<div id="app">
<input v-model="prenom" placeholder="Prénom">
<input v-model="nom" placeholder="Nom">
<p>Nom complet : {{ nomComplet }}</p>
<input v-model="nomComplet" placeholder="Modifier le nom complet">
</div>
<script>
new Vue({
el: '#app',
data: {
prenom: 'Jean',
nom: 'Dupont'
},
computed: {
nomComplet: {
get() {
return this.prenom + ' ' + this.nom
},
set(valeur) {
const parties = valeur.split(' ')
this.prenom = parties[0] || ''
this.nom = parties.slice(1).join(' ') || ''
}
}
}
})
</script>
Observateurs (watch)
Syntaxe fondamentale
Les observateurs permettent d'exécuter du code en réaction à un changement de données :
<div id="app">
<textarea v-model="texteOriginal" placeholder="Texte à traduire"></textarea>
<p>Résultat : {{ texteTraduit }}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
texteOriginal: '',
texteTraduit: ''
},
watch: {
texteOriginal(nouvelleValeur) {
// Simuler un appel API avec un délai
clearTimeout(this.timerId)
this.timerId = setTimeout(() => {
this.texteTraduit = nouvelleValeur.toUpperCase() // Transformation simple
}, 500)
}
}
})
</script>
Utilisation pour des appels asynchrones
Dans un cas réel, on pourrait appeler une API de traduction. Voici une structure typique avec gestion du debounce :
watch: {
texteOriginal: {
handler(nouvelleValeur) {
clearTimeout(this.timeoutId)
this.timeoutId = setTimeout(async () => {
const reponse = await fetch(`/api/translate?text=${encodeURIComponent(nouvelleValeur)}`)
const donnees = await reponse.json()
this.texteTraduit = donnees.result
}, 300)
},
immediate: false // Ne pas exécuter au chargement initial
}
}
Observation profonde d'objets
Pour surveiller les changements internes d'un objet, on utilise l'option deep: true :
data: {
configuration: {
langue: 'en',
theme: 'clair'
}
},
watch: {
configuration: {
deep: true,
handler(nouvelleConfig) {
localStorage.setItem('config', JSON.stringify(nouvelleConfig))
}
}
}
Cas pratique : panier d'achat de fruits
Structure de données et affichage
<div id="panier">
<div v-for="fruit in listeFruits" :key="fruit.id" class="article">
<img :src="fruit.image" :alt="fruit.nom">
<span>{{ fruit.nom }} - {{ fruit.prixUnitaire }} €</span>
<div class="controle-quantite">
<button @click="decrementer(fruit.id)" :disabled="fruit.quantite <= 1">-</button>
<span>{{ fruit.quantite }}</span>
<button @click="incrementer(fruit.id)">+</button>
</div>
<input type="checkbox" v-model="fruit.selectionne">
</div>
</div>
<script>
new Vue({
el: '#panier',
data: {
listeFruits: [
{ id: 1, nom: 'Pomme', image: '/img/pomme.png', prixUnitaire: 2, quantite: 3, selectionne: true },
{ id: 2, nom: 'Banane', image: '/img/banane.png', prixUnitaire: 1.5, quantite: 6, selectionne: false },
{ id: 3, nom: 'Orange', image: '/img/orange.png', prixUnitaire: 3, quantite: 2, selectionne: true }
]
},
methods: {
incrementer(id) {
const fruit = this.listeFruits.find(f => f.id === id)
if (fruit) fruit.quantite++
},
decrementer(id) {
const fruit = this.listeFruits.find(f => f.id === id)
if (fruit) fruit.quantite--
}
}
})
</script>
Fonctionnalités avancées avec propriétés calculées et observateurs
Ajoutons la gestion de la sélection totale, du calcul des totaux et de la sauvegarde locale :
computed: {
tousSelectionnes: {
get() {
return this.listeFruits.every(f => f.selectionne)
},
set(val) {
this.listeFruits.forEach(f => f.selectionne = val)
}
},
montantTotal() {
return this.listeFruits
.filter(f => f.selectionne)
.reduce((total, f) => total + (f.quantite * f.prixUnitaire), 0)
},
articlesSelectionnes() {
return this.listeFruits.filter(f => f.selectionne).length
}
},
watch: {
listeFruits: {
deep: true,
handler(nouvelleListe) {
localStorage.setItem('panier', JSON.stringify(nouvelleListe))
}
}
},
created() {
const sauvegarde = localStorage.getItem('panier')
if (sauvegarde) {
this.listeFruits = JSON.parse(sauvegarde)
}
}