Skip to content

5. Observateurs

Présentation

Les observateurs nous permettent de facilement associer une action à la modification d’une donnée dans nos composants.

Dans ce chapitre, nous allons voir comment les utiliser. Nous nous en servirons pour implémenter le stockage de nos données en tant que données locales.

Watch

Pour définir un observateur, il faut utiliser la champ watch de la configuration de nos composants. Ce champ prend un objet dont le nom des champs doit renvoyer à l’une des données ou props de notre composant. Chaque champ a pour valeur la fonction à exécuter lorsque la variable est modifiée :

jsx
<script>
export default {
  ...
  watch: {
    username: function(newUsername, oldUsername) {
      console.log("Username " + newUsername + " has been replace by " + oldUsername)
    }
  },
  data() {
    return {
      username: "John Doe" 
    }
  }
}
</script>

La fonction observatrice récupère en argument la nouvelle et l’ancienne valeur de la variable concernée. Il est possible d’utiliser des observateurs sur des props, des données et même des données calculées.

TIP

👉 Les observateurs servent généralement à gérer des opérations n’étant pas directement liées à l’interface de nos composants, comme des échanges avec une API et du stockage de données. Pour des modifications impactant directement les éléments affichés sur le site, il sera souvent préférable de gérer les changements à l’aide des données calculées.

Dans le cas de notre composant DogsGallery, nous pouvons utiliser un observateur pour modifier les données locales à chaque modification des données search et dogsSortType :

jsx
<script>
export default {
  name: 'DogsGallery',
  ...
  watch: {
    search: function(newSearch) {
      localStorage.setItem("search", newSearch)
    },
    dogsSortType: function(newDogsSortType) {
      localStorage.setItem("dogsSortType", newDogsSortType)
    }
  },
  data() {
    return {
      dogsData: [],
      search: "",
      dogsSortType: "AZName"
    }
  }
  ...
}
</script>

Nous pouvons maintenant récupérer les données stockées pour les mettre par défaut dans nos données à la création du composant. Attention toutefois, il se peut que les données locales n’existent pas encore ou aient été supprimées. Il faut donc conserver une valeur par défaut dans le cas où les données ne sont pas trouvées :

jsx
  data() {
    return {
      dogsData: [],
      search: localStorage.getItem("search") || "",
      dogsSortType: localStorage.getItem("dogsSortType") || "AZName"
    }
  }

TIP

💡 Nous utilisons ici l’opérateur logique OU (||) pour déterminer la valeur à prendre. Il permet dans notre cas, si la première valeur est null, de récupérer la seconde valeur à la place.

Conclusion

Nous stockons maintenant les options de notre utilisateur sur notre navigateur. Si nous quittons notre page et revenons plus tard, notre recherche et notre option de tri seront conservés.

Mais notre composant DogsGallery est devenu, au fil des derniers chapitres, relativement gros :

jsx
<template>
  <div class="dogs-gallery">
    <div class="gallery-options">
        <input type="text" v-model="search" placeholder="Chercher un chien">
        <button v-if="search" @click="cleanSearch">X</button>
        <label for="dog-sort">Trier par : </label>
        <select v-model="dogsSortType" id="dog-sort">
          <option value="AZName">Noms de A à Z</option>
          <option value="ZAName">Noms de Z à A</option>
          <option value="AZBreed">Espèces de A à Z</option>
          <option value="ZABreed">Espèces de Z à A</option>
        </select>
    <div>
    <div class="gallery">
      <DogCard 
        v-for="dog in dogsOrganizedData"
        :key="dog.id" 
        :firstname="dog.name" 
        :breed="dog.breed" 
        :pictureUrl="dog.picture"/>
    </div>
  </div>
</template>

<script>
import DogCard from './components/DogCard.vue'
import getDogsData from '@/services/api/dogsRepository.js'

export default {
  name: 'DogsGallery',
  components: {
    DogCard
  },
  watch: {
    search: function(newSearch) {
      localStorage.setItem("search", newSearch)
    },
    dogsSortType: function(newDogsSortType) {
      localStorage.setItem("dogsSortType", newDogsSortType)
    }
  },
  created: function() {
    this.retrieveDogsData()
  },
  computed: {
    dogsOrganizedData: function() {
      const field = ["AZName", "ZAName"].includes(this.dogsSortType) ? "name" : "breed"
      const reversed = ["ZAName", "ZABreed"].includes(this.dogsSortType)
      const filterFunc = (a) => a.name.toLowerCase().includes(this.search.toLowerCase())
      const comparator = (a, b) => a[field].localeCompare(b[field]) 
      let data = this.dogsData.filter(filterFunc)
      data = data.sort(comparator)
      if (reversed) data = data.reverse()
      return data
    }
  },
  data() {
    return {
      dogsData: [],
      search: localStorage.getItem("search") || "",
      dogsSortType: localStorage.getItem("dogsSortType") || "AZName"
    }
  },
  methods: {
    async retrieveDogsData() {
      this.dogsData = await getDogsData()
    },
    cleanSearch: function() {
      this.search = ""
    }  
  }
}
</script>

Notre composant s’occupe désormais de gérer beaucoup de choses : si la présentation des chiens est assez bien confiée au composant DogCard, notre galerie se charge elle-même de la gestion de notre barre d’options.

Cette situation est assez courante lors du développement d’une application, mais elle entre en conflit avec le respect de notre principe de séparation des préoccupations. Nous pourrions encapsuler notre barre d’options dans un nouveau composant enfant.

Mais alors comment récupérer les données search et dogsSortType pour trier notre galerie si elles sont déplacées dans un composant enfant ?

Nous verrons dans le prochain chapitre comment gérer cette réorganisation à l’aide des événements personnalisés.

Aller plus loin