Directives Vue, propriétés calculées et observateurs avec illustrations pratiques

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)
  }
}

Étiquettes: Vue.js directives Vue propriétés calculées observateurs JavaScript

Publié le 24 juillet à 06h18