Code

Nos 4 astuces pour le développement avec Vue.js 3

Retour au blog

Chez Atipik, on adore Vue, et c’est tout naturellement qu’on a commencé à développer sur la dernière version Vue3 pour nos nouveaux projets front. Dans cet article, on a regroupé nos trucs et astuces que l’on considère extrêmement utiles pour un développement efficace et un code maintenable. Profitez-en, c’est cadeau 🎁 !


#1 Vue a (déjà) inventé la téléportation 💺

Lien vers la doc officielle: https://vuejs.org/guide/built-ins/teleport.html#teleport

Tout développeur front s’est déjà retrouvé dans une situation où un élément du DOM ne s’affiche pas correctement à cause de son positionnement dans l’arbre du fait d’un position: relative ou d’un z-index: 9999 d’un de ses parents.

L’exemple le plus parlant c’est celui de la modal : imaginons que l’on veuille afficher une modal de confirmation dans un SFC (Single File Component) très bas dans l’arbre, comme ceci :

<template>
  <div class="relative w-full h-48 border-2 border-red-500">
    <h1>Hello World</h1>
    <button @click="$refs.confirmationModal.show()">Click me!</button>
    <Modal ref="confirmationModal">
      <template #header>Confirmed!</template>
      It's all good
    </Modal>
  </div>
</template>

<script>
...
</script>

Comme on peut le voir, la Modal est incluse dans un div avec un positionnement relatif. Du coup, bah, ça donne ça :

Problem_modal.gif

Le problème c’est qu’on aimerait garder cette modal exactement au même endroit tout en l’affichant en fullscreen.

C’est là que le nouveau component Teleport s’avère particulièrement pratique. Il suffit de wrapper notre modal à l’intérieur et le tour est joué !

Niveau props, c’est on ne peut plus simple, il suffit de spécifier un tag ou un sélecteur CSS dans le prop to . Dans notre cas, “body” semble assez naturel.

<template>
  <div>
    ...
    <Teleport to="body">
      <Modal ref="confirmationModal">
        ...
      </Modal>
     </Teleport>
  </div>
</template>

<script>
...
</script>
Ok_modal.gif

Évidemment, le mieux est d’insérer le Teleport au niveau du component Modal directement.

#2 Des v-model(s) en veux-tu en voilà 🤩

Si vous ne savez pas à quoi sert les v-model, l’exemple le plus parlant c’est sur un input :

 <input
  v-model="message"
  placeholder="edit me"
/>

Ce petit bout de code permet d’avoir un binding dans les 2 sens :

  • La valeur de l’input est remplie par message
  • À chaque changement de valeur de l’input, message est mis à jour

Le v-model n’est rien d’autre qu’un raccourci, qui peut également s’écrire comme ceci dans le cas de l’ input :

 <input
  :value="message"
  @input="event => message = event.target.value"
>

Utilisation du v-model sur des composants custom

Et en fait, c’est très simple d’implémenter un v-model sur un composant custom, en utilisant la même technique avec quelques ajustements.

Voilà un exemple avec un CustomSelect :

Screen Shot 2022-06-21 at 09.59.36.png

Deux points importants à retenir sur ce bout de code :

  1. On déclare une prop value , chose obligatoire pour l’utilisation d’un v-model
  2. On émet un event update:modelValue avec la valeur de notre choix. Attention, le nom de l’event est important pour que le v-model soit correctement mis à jour dans le parent.

Dans Vue2, on ne peut avoir qu’un seul attribut v-model. C’est bien, mais c’est un peu limitant, il faut bien l’avouer.

Une infinité de v-model avec Vue3

Avec Vue3, on a maintenant la possibilité d’avoir une infinité de v-model 🎉

Prenons un exemple pour illustrer l’utilisation de 2 v-model sur un composant. Supposons que nous ayons besoin d’un composant qui nous permette la saisie d’un champ text et nous retourne l’état de validation de ce champ.

D’abord, le composant parent :

import CustomInput from "./form/CustomInput"

export default {
  components: {
    CustomInput,
  }
  data() {
    return {
      message: '',
      isMessageValid: true
    };
  },
};

Passons maintenant à l’implémentation du composant CustomInput.vue :

import { computed } from "vue";

export default {
  props: {
    input: {
      type: String,
    },
    valid: {
      type: Boolean,
    },
    validator: {
      type: Function,
      required: true,
    },
  },
  setup(props, { emit }) {
    const inputValue = computed({
      get: () => props["input"],
      set: (value) => emit("update:input", value)
    });
    const validValue = computed({
      get: () => props["valid"],
      set: (value) => emit("update:valid", value)
    });
    return {
      inputValue,
      validValue,
    };
  },
  methods: {
    setValidity() {
      this.validValue = this.validator(this.input);
    },
  },
};

Oui, il se passe pas mal de choses 😱, voilà en détail :

  1. Niveau template, on a notre input qui permet à l’utilisateur de saisir son texte
    • On utilise le v-model de l’input pour binder notre prop input
    • À chaque changement, on appelle la fonction setValidity qui se charge de mettre à jour la validité de l’input en fonction du validateur fourni
  2. Niveau props :
    • input : le champ texte à proprement parler qui contient la saisie de l’utilisateur
    • valid : un booléen qui nous donne la validité de la saisie
    • validator : un callback qui permet de déterminer la validité de la saisie
  3. On utilise la fonction setup de la composition API pour construire nos v-model
  4. Dans la méthode setValidity, on assigne simplement le résultat du validateur au computed, qui propagera automatiquement le changement au v-model du parent

#3 La transmission de props, de génération en génération 👴 👨‍🦰 👶

Lien vers la doc officielle : https://vuejs.org/guide/components/provide-inject.html

Généralement, on utilise un store (Vuex pour nommer le plus populaire) pour partager des données à plusieurs composants. C’est parfait dans 99% des cas, sauf que parfois, on aimerait simplement partager une donnée d’un composant parent à tous ses composants enfants, sans passer par un store.

Untitled (1).png

Source : https://vuejs.org/guide/components/provide-inject.html#prop-drilling

Dans Vue2, on n’avait pas le choix que de déclarer le même prop sur l’ensemble des descendants pour que le petit-petit-petit fils y ait accès. Pas vraiment satisfaisant, non ?

Vue3 introduit un nouveau concept, le provide / inject :

Untitled (2).png

Source : https://vuejs.org/guide/components/provide-inject.html#prop-drilling

Le principe est simple :

  1. Depuis le composant parent, on déclare les variables (props, data, computed, …) qui seront accessibles à tous les descendants via la clé provide
  2. Depuis n’importe quel composant enfant, on spécifie la liste des variables injectable via la clé inject

Pour illustrer par un exemple, imaginons avoir une modal qui contient un formulaire. L’idée et de pouvoir accéder au data shown de la modal dans tous les inputs du formulaire pour pouvoir exécuter une routine lorsque sa valeur passe à true.

import { computed } from "vue";
import Input from "./form/Input";

export default {
  components: {
    Input,
  },
  provide() {
    return {
      modalShown: computed(() => this.shown)
    }
  },
  data() {
    return {
      shown: false,
    }
  },
  methods: {
    show() {
      this.shown = true;
    },
    hide() {
      this.shown = false;
    }
  }
}

Attention, pour que la valeur du provide soit dynamique, il faut le wrapper dans un computed.

import Input from "./form/Input";

export default {
  inject: ['modalShown'],
  props: {
    input {
      type: String,
    }
  },
  watch: {
    modalShown(shown) {
      if (shown) {
        // modal shown, do something crazy!
      }
    },
  },
}

#4 L’héritage de composant 🏰

Lien vers la doc officielle : https://vuejs.org/api/options-composition.html#extends

Dans certaines situations, il est intéressant de partager un bout de code pour appliquer un certain comportement à un ensemble de composants ou même tous les composants d’une application Vue. Pour se faire, on peut utiliser les mixins.

Voilà un exemple qui applique un mixin à l’ensemble des composants d’une application Vue :

const mixin = {
  created() {
    console.log(1)
  }
}

createApp({
  created() {
    console.log(2)
  },
  mixins: [mixin]
})

Voici un autre exemple qui n’applique le mixin qu’a certain composants :

// Component1.vue
export default {
  mixins: [MySuperMixin]
}

// Component2.vue
export default {
  mixins: [MySuperMixin]
}

// Component3.vue
export default {}

Cette fonctionnalité s’avère bien utile dans la plupart des cas, mais elle nous contraint en termes d’implémentation :

  • Soit on applique le mixin à TOUS les composants
  • Soit on applique le mixin par composant, nous obligeant à l’importer et le spécifier dans chacun des composants

Pour info, l’utilisation des mixins est désormais déconseillée dans Vue3. A la place, il est préférable d’utiliser la Composition API.

Dans Vue3, l’Options API nous fournit la fonctionnalité extends. Elle ressemble fortement à l’option mixins mais son intention est légèrement différent : mixins se focalise sur la réutilisation de code, alors qu’ extends se concentre sur l’héritage.

Comme son nom l’indique, extends permet d’hériter d’un autre composant, tout en appliquant les mêmes règles d’évaluation que pour les mixins.

Par exemple, cela nous permet de mettre en place un composant générique fournissant une méthode de validation de formulaire :

export default {
  computed: {
    isFormValid() {
      return !_.some(this.inputs, input => !input);
    }
  }
}

Puis, on peut implémenter des composants de formulaire en héritant de FormValidator :

export default {
  extends: FormValidator,
  methods: {
    postForm() {
      if (!this.isFormValid) {
        return;
      }
      
      ...
    }
  }
}

Ces quelques astuces devraient s’avérer utiles pour le développement de vos applications Vue3, en tout cas on l’espère ! Cette liste est loin d’être exhaustive. Vous pouvez retrouver les nouvelles fonctionnalités majeures ici.

Auteur

Publié le 21 juin 2022

Lead Ingénieur Logiciel · Web