Vue transparent wrapping: come estendere le funzionalità di un componente Vue

Parole Federico Di Filitto
Immagini Michael Reali
Tempo di lettura 5

Aggiungere intere funzionalità senza dover riscrivere tutto il codice da capo? Certo! Ecco come

V

Partiamo col costruire un wrapper del tag “input”. Io l’ho chiamato “MyInput”:

<template>
    <input>
</template>
<script>
export default {
    name: "MyInput",
};
</script>

Riflettere v-model

La prima cosa da fare è permettere l’utilizzo del v-model, quindi il componente MyInput deve essere in grado di accettare un v-model dal padre e “rifletterlo” sul v-model dell’input.

Il v-model di Vue in realtà è una semplice scorciatoia delle altre due direttive v-bind e v-on. Va a creare una v-bind del dato a una prop di nome “value” e in aggiunta si mette in ascolto su un evento di nome “input”, che quando si verifica va ad aggiornare il dato.

<input v-model=”myData”>

è equivalente a:

<input
  v-bind:value="myData"
  v-on:input="myData = $event.target.value"
>

 

Se non si è pratici con l’uso del v-model con componenti custom, consiglio altamente la lettura di questo paragrafo della documentazione di Vue.

Proviamo adesso a implementare un sistema che permetta di riflettere il v-model:

<template>
    <input v-bind:value="value"  v-on:input="emitInput" />
</template>
<script>
export default {
   name: "MyInput",
   props: {
       value: String,
    },
    methods: {
  	emitInput(event) {
    	   this.$emit("input", event.target.value);
  	},
   },
};
</script>

 

Il componente MyInput accetta una prop di nome value, che, come abbiamo specificato, rappresenta la prop impiegata dal v-model. Questa prop viene riflessa nel tag input, quindi qualsiasi valore il padre passa al MyInput viene a sua volta passato al tag input.

Il metodo “emitInput” viene eseguito ogni volta che si digita nel campo input (grazie alla direttiva v-on inserita nel tag input). Dentro questo metodo emetto un evento con allegato il nuovo valore digitato. Questo evento notifica il padre di aggiornarsi con il valore inviato.

Allacciare listeners e props

Ottimo, siamo riusciti a riflettere il v-model. Ma ancora non basta per creare un transparent wrapping come si deve. Oltre al v-model, il componente che stiamo cercando di “wrappare” può accettare tante prop e/o può notificare diversi eventi al padre. Significa che dobbiamo andare a studiare ogni singola prop ed evento e rifletterli manualmente nel nostro wrapper come abbiamo fatto per il v-model? Per nostra fortuna, la risposta è no. Il che è un enorme vantaggio perché significa che siamo in grado di wrappare ed estendere funzionalità di componenti di cui non conosciamo tutti gli aspetti e le funzionalità, risparmiandoci così molto tempo.

Vue ci viene incontro mettendo a disposizione degli oggetti che facilitano questo processo.
Ogni istanza di Vue possiede molti oggetti di cui possiamo usufruire. In questo articolo ne andiamo a studiare due in particolare: $attrs e $listeners.

Cosa succede se proviamo a passare a un componente delle prop che non ha definito? A prima vista nulla. Sembra come se i componenti di Vue ignorino le props che non sono definite specificatamente. In verità queste prop vengono comunque salvate all’interno del componente e le possiamo trovare nell’oggetto $attrs.

Analogamente, quando ci si pone in ascolto di un componente attraverso la direttiva v-on, andiamo a creare per così dire un “listener” che possiamo trovare nell’oggetto $listeners all’interno del componente.

Vediamo come riflettere i listeners e le prop:

<template>
<input
  v-bind:value="value"
  v-on:input="emitInput"
  v-on="listeners"
  v-bind="attrs"
/>
</template>
<script>
export default {
    name: "MyInput",
    props: {
        value: String,
    },
    computed: {
        listeners() {
            const listeners = { ...this.$listeners };
            delete listeners["input"];
            return listeners;
        },
        attrs() {
            const attrs = { ...this.$attrs };
            delete attrs["value"];
            return attrs;
        },
    },
    methods: {
        emitInput(event) {
            this.$emit("input", event.target.value);
        },
    },
};
</script>

 

Qui ho aggiunto due computed props: attrs e listeners. Queste restituiscono degli oggetti che contengono le prop e i listener da passare al nostro child component.
Come mai non possiamo passare direttamente gli oggetti $attrs e $listeners? Il motivo è che all’interno degli $attrs è presente la prop “value” e nei $listeners è presente il listener dell’evento “input”. Questi gli abbiamo già gestiti nella prima parte dell’articolo, quindi non vanno riflessi una seconda volta.
Lo scopo di queste computed props è quindi restituire un oggetto identico a $attrs e $listeners ma senza gli elementi “value” e “input”.

Aggiungere funzionalità al componente

Adesso il componente MyInput è un transparent wrapper del tag input e possiamo divertirci ad aggiungere tutte le feature che vogliamo.

Ad esempio io ho implementato la possibilità di aggiungere un'icona al campo input:

<template>
  <div :class="{container: hasIcon}">
	<i  class="glyphicon" :class="iconClass"></i>
	<input v-on="listeners" v-bind="attrs" v-on:input="emitInput" v-bind:value=”value”/>
  </div>
</template>
<script>
export default {
    name: "MyInput",
    props: {
        icon: String,
        value: String,
    },
    computed: {
        listeners() {
            const listeners = { ...this.$listeners };
            delete listeners["input"];
            return listeners;
    },
        attrs() {
            const attrs = { ...this.$attrs };
            delete attrs["value"];
            return attrs;
        },
        iconClass(){
            return {
                    [`glyphicon-${this.icon}`] : this.hasIcon
            }
        },
        hasIcon() {
            return !!this.icon
        },
    },
 
    methods: {
        emitInput(event) {
            this.$emit("input", event.target.value);
        },
    },
};
</script>
<style scoped>
@import url("//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css");

.container {
  position: relative;
}
.container input{
	padding-left: 30px;
}

input {
  width: 100%;
}

/* style icon */
.container .glyphicon {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
  left: 7px;
}
</style>

 

Ho aggiunto un paio di computed props per aiutarmi ad applicare le classi css in modo dinamico. Infine MyInput ha un'altra prop “icon” che permette di selezionare l’icona desiderata.

A questo punto potrebbe sorgere una domanda: come mai abbiamo dovuto gestire separatamente il v-model?
Il problema sta nel fatto che a volte i dati hanno bisogno di essere manipolati. Ad esempio possiamo considerare il metodo “emitInput”. Come possiamo vedere, non andiamo ad emettere il dato tale e quale, “event” è un oggetto e non una stringa. Infatti in realtà emettiamo solamente la stringa che ha scritto l’utente “event.target.value”. Se provassimo a trasmettere l’evento tale e quale, verrebbe mostrato l’oggetto “event” nel nostro campo input e non ciò che l’utente sta digitando.
In questo caso non era necessario andare a gestire separatamente la prop “value”, lo scopo era solamente di rendere la guida più semplice da seguire.
Quindi in base al componente che stiamo wrappando potrebbe essere necessario dover gestire separatamente il v-model.

 

Qui trovate l'esempio completo realizzato su codepen:

Conclusione

Il transparent wrapping ci dà la possibilità di estendere tutti i componenti che vogliamo, dai più semplici ai più complessi. Lo permette in modo sorprendentemente veloce e senza neanche conoscere a fondo i componenti che stiamo wrappando. Non siamo costretti a riscrivere due volte lo stesso codice per componenti simili, favorendo così un progetto più pulito e con meno ridondanze.
Questa tecnica è un vero e proprio superpotere, un componente che richiederebbe molto lavoro per essere sviluppato, possiamo invece basarci su altri componenti più o meno simili, anche appartenenti ad altre librerie.
Insomma, l’unico limite è la vostra immaginazione.

Federico Di Filitto

Junior Web Developer