back to top

Come creare e usare delle direttive personalizzate in Vue.js

Nelle lezioni precedenti abbiamo più volte incontrato direttive come v-bind, v-on, v-if ecc… che rappresentano alcuni degli strumenti indispensabili senza i quali non potremmo realizzare delle applicazioni in Vue.js con estrema semplicità e seguendo un approccio dichiarativo.

Ma Vue.js offre anche la possibilità di creare delle direttive personalizzate in modo simile a quanto abbiamo già visto per la definizione dei componenti.

Cosa sono le direttive in Vue.js

Le direttive predefinite e personalizzate arricchiscono o modificano il comportamento dell’elemento su cui sono applicate utilizzando una sintassi simile a quella dei normali attributi HTML.

Abbiamo visto che in Vue.js i componenti rappresentano le unità base in cui suddividere un’applicazione. In alcuni casi abbiamo però necessità di effettuare operazioni di più basso livello direttamente sul DOM. In situazioni del genere le direttive rappresentano lo strumento migliore per andare ad agire sugli elementi HTML.

Direttive personalizzate

In modo del tutto analogo a quanto già visto per i componenti, possiamo definire una direttiva personalizzata attraverso un oggetto di opzioni.

È poi necessario registrare la direttiva a livello globale.

Vue.directive('directive-name', {/* oggetto per definire la direttiva */})

Oppure possiamo registrarla localmente all’interno di un componente grazie all’opzione directives.

export default {
  /* altre opzioni */
  directives: {
    directiveName: {
      /* oggetto per definire la direttiva */
    }
  }
}

Per evidenziare che si tratta di direttive, al loro nome dobbiamo aggiungere il prefisso v- quando le applichiamo ad un elemento all’interno di un template.

<div v-directive-name></div>

Quella appena mostrata è la sintassi più semplice per una direttiva che in realtà può anche avere degli argomenti, può cambiare comportamento attraverso dei modificatori e ricevere un valore. Vediamo allora qual è la sintassi da usare in ciascuno di questi casi.

<div v-directive-name:arg.modifier1.modifier2="value"></div>

Fra doppie virgolette passiamo un valore alla direttiva. È bene notare che nella forma riportata sopra value è il nome di una proprietà del componente che usa la direttiva. Supponendo che value sia pari a 'ciao', la direttiva riceverà tale stringa come valore.

Le direttive possono però avere anche un argomento opzionale. La sintassi da utilizzare è v-directive-name:argument. Nelle precedenti lezioni abbiamo utilizzato diverse direttive con un argomento. Basta pensare a v-bind:prop o v-on:event.

Per conferire maggiore flessibilità ad una direttiva, possiamo infine impiegare uno o più modificatori opzionali. In questo caso dovremo concatenare il nome dei modificatori separati dal carattere ‘.’ (es. v-dir.modifier oppure v-dir.mod1.mod2.mod3).

Funzioni speciali per le direttive (Hooks)

Per definire il comportamento di una direttiva, Vue mette a disposizione una serie di metodi speciali e opzionali simili ai Lifecycle hooks dei componenti. In questi metodi avremo accesso al valore, all’argomento e ai modificatori passati alla direttiva. I metodi in questione sono i seguenti:

  • bind() viene invocato una sola volta quando la direttiva viene associata all’elemento su cui è stata applicata. Viene solitamente usato per le operazioni di inizializzazione della direttiva.
  • inserted() viene chiamato quando l’elemento su cui è applicata la direttiva viene inserito nel nodo genitore.
  • update() si ripete quando l’elemento viene aggiornato e preferibilmente prima dell’aggiornamento degli elementi discendenti. A voler essere più precisi, il metodo viene invocato ogni volta che viene aggiornato il nodo virtuale (VNode) associato al componente, ovvero la rappresentazione dell’elemento nel Virtual DOM.
  • componentUpdated() viene chiamato dopo che i VNode del componente e di tutti i suoi discendenti sono stati aggiornati.
  • unbind() infine viene invocato quando la direttiva viene dissociata dall’elemento.

Ai metodi bind(), inserted() e unbind() vengono passati tre argomenti: el, binding e vnode. A parte el, gli altri due argomenti vanno usati in sola lettura e non devono essere mai modificati.

  • el rappresenta l’elemento del DOM su cui viene applicata la direttiva. Useremo tale riferimento per apportare le modifiche necessarie direttamente al DOM.
  • binding è un oggetto che contiene le seguenti proprietà:
    • name rappresenta il nome della direttiva senza il prefisso v-;
    • value è il valore (risultato dell’espressione) passato alla direttiva;
    • oldValue è presente solo nei metodi update() e componentUpdated(). Rappresenta il precedente valore della direttiva anche se quest’ultimo non ha subito variazioni;
    • expression è l’espressione assegnata alla direttiva. Nel seguente caso <input v-dir-name="name.toUpperCase()" >, expression è pari alla stringa "name.toUpperCase()";
    • arg è l’argomento passato alla direttiva. In <input v-dir-name:my-arg="name.toUpperCase()" >, la proprietà arg è pari alla stringa "my-arg";
    • modifiers è un oggetto che, se non vuoto, contiene come proprietà il nome dei modificatori. Per <input v-dir-name.mod1.mod2="name.toUpperCase()" >, i diversi hooks avranno accesso all’oggetto {mod1: true, mod2: true}.
  • vnode rappresenta il nodo del Virtual Node prodotto dal compilatore di Vue.js. (Il compilatore passa in rassegna tutti i template dei componenti e genera il Virtual DOM che è una rappresentazione in memoria – un oggetto javascript – del DOM corrente.)

Ai metodi update() e componentUpdated(), oltre agli argomenti appena elencati, viene anche passato un quarto argomento oldVnode che rappresenta il nodo del Virtual DOM relativo all’elemento su cui è applicata la direttiva prima dell’ultimo aggiornamento.

Metodo semplificato per definire una direttiva

I due hooks più utili e usati più frequentemente sono bind() e update(), ma, quando il loro comportamento è il medesimo e non abbiamo neanche bisogno degli altri metodi descritti sopra, possiamo allora definire una direttiva utilizzando semplicamente una funzione.

<template>
  <input 
    type="text" 
    placeholder="Inserisci del testo..." 
    v-fat-border="color"
  >
</template>

<script>
  export default {
    directives: {
      fatBorder(el, binding) {
        el.style.border = `4px solid ${binding.value}`;
      }
    },
    data() {
      return {
        color: 'hsl(86, 60%, 50%)' // green
      }
    }
  }
</script>
esempio di direttiva definita solo con una funzione

Esempio di direttiva personalizzata

Vediamo allora come creare una semplice direttiva da applicare ad un campo di testo. La direttiva aggiunge un bordo all’elemento. Il colore del bordo viene definito tramite un gradiente lineare. La direttiva si occuperà quindi di modificare l’angolo del gradiente man mano che vengono digitati dei caratteri.

Per semplicità registriamo la direttiva localmente con il nome border. La direttiva sarà quindi applicata ad un elemento aggiungendo il prefisso ‘v-‘ al suo nome.

Al fine di illustrare le diverse funzioni disponibili, la nostra direttiva supporterà un argomento per indicare la direzione del gradiente. Tale argomento può assumere uno di tre valori: ‘vertical’, ‘horizontal’ e ‘diagonal’. La direttiva supporta anche un modificatore '.invert' per invertire la direzione del gradiente lungo l’asse stabilito dall’argomento. Il valore passato alla direttiva deve essere nella forma 'color1:color2'. La direttiva si occuperà di prelevare i due colori dalla stringa e di utilizzarli per costruire un gradiente lineare.

Possiamo poi applicare la direttiva nel seguente formato.

<input v-border:vertical.invert="gradient">

Procediamo allora a creare un nuovo file con estensione .vue contenente il seguente frammento di codice per definire un componente che utilizza la direttiva appena descritta la quale viene registrata localmente.

<template>
  <div>
    <input 
      type="text" 
      placeholder="Inserisci del testo..." 
      v-border:horizontal.invert="gradient"
    >
  </div>
</template>

<script>
  export default {
    directives: {
      border: {
        bind(el, binding) {

          const initialAngle = {
            diagonal: '45deg',
            horizontal: '90deg',
            vertical: '0deg'
          }

          let defaultBorder = {
            width: '4px',
            style: 'solid',
            color: '#000'
          };

          const getGradient = (angle, color1, color2) => 
            `linear-gradient(${angle}, ${color1}, ${color2})`;

          // incrementa l'angolo e riparte da capo ogni 360 gradi
          // 360%360 === 0  400 % 360 === 40
          const incrementAngle = (angle, increment) =>
            (parseInt(angle) + parseInt(increment)) % 360 + 'deg';

          let gradient = getGradient('0deg', '#000', '#000');

          // un array di colori a partire da una stringa
          // es. '090979:00d4ff' => ['#090979','#00d4ff']
          const colors = binding.value.split(':')
            .map(color => color.charAt(0) !== '#' ? '#' + color : color);
          
          // angolo iniziale del gradiente
          let angle = initialAngle[binding.arg] || initialAngle.vertical;

          // inverte la direzione del gradiente se richiesto
          if (binding.modifiers && binding.modifiers.invert) {
            angle = incrementAngle(angle, 180);
          }
          // setta spessore e stile del bordo
          el.style.border = Object.values(defaultBorder).join(' ');

          if (colors.length == 2) {
            gradient = getGradient(angle, colors[0], colors[1]);

            // impostazioni per colorare il bordo
            // usando un gradiente
            el.style.borderImageSlice = 1;
            el.style.borderImageSource = gradient;

            // ogni volta che viene digitato un carattere nel campo <input>
            // viene incrementato l'angolo di 45 gradi
            el.addEventListener('input', () => {

              angle = incrementAngle(angle, 45);

              // aggiorna la direzione del gradiente secondo il nuovo angolo
              gradient = getGradient(angle, colors[0], colors[1]);

              el.style.borderImageSource = gradient;
            });
          }
        }
      }
    },
    data() {
      return {
        gradient: '090979:00d4ff'
      }
    },
  }
</script>

<style scoped>
  input {
    padding: .5rem;
  }
  
  input:active, input:focus {
    outline: none;
    box-shadow: 0px 4px 8px rgba(0, 0, 0, .26);
  }
</style>

https://vimeo.com/462644000

Argomenti dinamici per le direttive

Gli argomenti delle direttive possono essere dinamici e cambiare in base al valore di una certa proprietà di un componente.

Per indicare che si tratta di un argomento dinamico, dovremo racchiudere quest’ultimo fra parentesi quadre, per esempio: v-dir-name:[dynamic-argument]

Creiamo allora un altro semplice esempio in cui sfruttiamo un argomento dinamico per decidere se colorare lo sfondo o il bordo di un elemento utilizzando il valore della direttiva.

<template>
  <div>
    <p v-colorize:[selected]="color">
      Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, 
      vel scelerisque nisl consectetur et. Nullam id dolor id nibh ultricies vehicula ut id elit. 
      Nullam id dolor id nibh ultricies vehicula ut id elit. 
      Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
    </p>
    <div>
      <input 
        type="radio" 
        name="background-color" 
        id="background-color" 
        value="background-color" 
        v-model="selected">
      <label for="background-color">Background</label>
      <input 
        type="radio" 
        name="border" 
        id="border" 
        value="border" 
        v-model="selected">
      <label for="border">Border</label>
    </div>
  </div>
</template>

<script>
  export default {
    directives: {
      colorize(el, binding) {
        const cssProperty = binding.arg || 'background-color';
        if (cssProperty === 'border') {
          binding.value = `2px solid ${binding.value}`;
          el.style['background-color'] = '';
        } else if (cssProperty === 'background-color') {
          el.style['border'] = '' 
        }
        el.style[cssProperty] = binding.value;
      }
    },
    data() {
      return {
        color: 'orange',
        selected: 'background-color'
      }
    },
  }
</script>

<style lang="css" scoped>
  p {
    padding: 1rem;
  }
</style>

Nell’esempio riportato sopra, creiamo una direttiva colorize che registriamo localmente ed applichiamo all’unico paragrafo presente nel template del componente. Attraverso dei pulsanti di tipo radio modifichiamo la proprietà selected la quale può assumere come valore ‘background-color’ o ‘color’. Quando questa cambia, viene aggiornato il componente e viene passato il nuovo argomento alla direttiva. La direttiva sfrutta l’argomento dinamico per definire il colore dello sfondo o del bordo del paragrafo. Il colore da usare viene passato come valore della direttiva. Visto che il corpo delle funzioni bind() e update() è identico, definiamo il comportamento della direttiva attraverso una funzione.

https://vimeo.com/462643987

Prima di definire una nuova direttiva da zero, può essere utile cercare su NPM se ne esiste già una perché è possibile che qualcuno abbia dovuto risolvere uno stesso problema prima di noi.

Sul sito di Telerik, potete trovare un articolo interessante in cui sono elencate 15 utili direttive da usare in Vue.js.

Riepilogo

In questa lezione abbiamo spiegato che cosa sono le direttive in Vue.js e abbiamo visto come funzionano internamente. Come per i componenti esistono dei metodi speciali per ogni direttiva che vengono invocati da Vue.js quando la direttiva viene associata ad un componente, nelle varie fasi di inserimento nel DOM ed aggiornamento del componente stesso. Per concludere abbiamo creato una semplice direttiva personalizzata utilizzando i concetti incontrati nei paragrafi precedenti. Nella prossima lezione parleremo dei ‘filtri’ e vedremo come possiamo utilizzarli all’interno dei template dei componenti.

Pubblicitร 
Articolo precedente
Articolo successivo