Guida tutorial a Vue.js – 4° parte – componenti

I componenti sono uno degli aspetti più interessanti di Vue.js perchè ti consentono di suddividere il codice html in moduli riutilizzabili, ognuno con le sue proprietà, metodi e anche stili css.

Per poter utilizzare un componente è necessario innanzi tutti registrarlo prima della creazione dell’istanza Vue. La registrazione può essere globale o locale, la prima consente di utilizzare il componente in tutta l’applicazione, mentre le seconda ne consente l’utilizzo soltanto all’interno di un altro componente.

La registrazione globale richiede questa sintassi:


Vue.component('nuovo-componente', { 
   template: '<div>Contenuto del componente</div>',
   // altre opzioni
})

La registrazione locale dichiara il componente all’interno del componente genitore, come nell’esempio che segue (in questo caso il componente è figlio dell’istanza Vue):


new Vue({ 
   // opzioni dell'istanza Vue
   components: { 
      'mio-componente': { 
         // opzioni del componente
      }
   } 
});

Secondo le linee guida di Vue, i componenti dovrebbero avere sempre un nome composto, come ad esempio mio-componente piuttosto che un nome singolo, per evitare conflitti con altri elementi html, anche futuri, che invece sono sempre formati da una singola parola.

Per visualizzare il contenuto del componente sullo schermo, si può utilizzare un tag personalizzato, che nel nostro esempio sarà:

<nuovo-componente></nuovo-componente>

oppure un normale tag HTML dotato dell’attributo “is”, ad esempio:

<div is="nuovo-componente"></div>

Nel caso di alcuni tag HTML come ul ol table e select, destinati ad avere al proprio interno degli elementi predefiniti (ad esempio l’elemento select che contiene il tag option) quest’ultima modalità è preferibile per evitare errori nell’output.
I componenti hanno al loro interno una serie di opzioni, che in buona parte rispecchiano quelle presenti nelle istanze di Vue, ma con alcune particolarità, vediamo quali:

La proprietà Template

Abbiamo detto che i componenti sono blocchi di codice riutilizzabile all’interno di una pagina web. La proprietà template ha lo scopo di definire il codice html da mostrare, come nell’esempio sotto

Vue.component('nuovo-componente', { 
    template: '<h1>{{ messaggio }}</h1>',
})

Ovviamente il codice può essere molto più elaborato di quello appena visto, anche sviluppato in più righe di codice. In questo caso ricordati che ogni riga deve terminare con il simbolo \ altrimenti il codice javascript segnalerà un errore. Inoltre ogni template deve avere un unico tag contenitore, per cui se ad esempio il template deve contenere due paragrafi, non devi definirlo così

Vue.component('nuovo-componente', { 
    template: '<p>{{ messaggio1 }}</p><p>{{messaggio2}}</p>',
})

perchè verrebbe visualizzato soltanto il primo paragrafo, piuttosto includilo in un tag contenitore, come sotto (da notare l’utilizzo di \ alla fine di ogni linea)

Vue.component('nuovo-componente', {
    template: '<div>\
    <p>{{ messaggio1 }}</p>\
    <p>{{messaggio2}}</p>\
    <div>', 
})

  

La proprietà data nei componenti

Una particolarità molto importante è rappresentata della proprietà data che all’interno dei componenti non è più un oggetto, bensì una funzione. Un esempio di sintassi potrebbe essere il seguente: 

Vue.component('nuovo-componente', { 
    template: '<h1>{{ messaggio }}</h1>',
    data: function(){
        return { messaggio: 'data diventa una funzione' }
    }
}) 

Il motivo di questa differenza è legato alla possibilità di riutilizzare più volte i componenti all’interno dell’applicazione. Come già sappiamo, le proprietà all’interno dell’oggetto data sono quelle che consentono lo scambio di dati tra l’istanza ed il template. Se queste proprietà rimanessero statiche all’interno dei componenti, in presenza di più istanze del medesimo componente, ad ogni modifica di una proprietà questa si riperquoterebbe anche sulle altre istanze. Per evitare ciò si utilizza una funzione, in modo da creare un nuovo scambio di dati ad ogni istanza del componente.

La proprietà Props

Poichè i componenti sono blocchi di codice riutilizzabile, è normale che in un’applicazione ne vengano utilizzati più di uno contemporaneamente, anche all’interno di componenti-genitori. In questi casi si pone il problema dello scambio di dati e di variabili dal componente genitore al componente figlio e vice-versa. Questo passaggio avviene attraverso la proprietà props, con la quale il componente figlio dichiara le proprietà che si aspetta di ricevere dal componente genitore.

Per capirci megio, facciamo un esempio. Supponi di avere un componente come quello che segue:

Vue.component('nuovo-componente', { 
    template: '<h1>{{ messaggio }}</h1>',
})

Si tratta di un semplice componente che deve visualizzare la proprietà {{messaggio}} all’interno di un tag h1. Ma quale sarà il contenuto di questo messaggio? Il contenuto può essere trasferito attraverso il template, come un suo attributo:

<nuovo-componente messaggio="Questo è il testo da visualizzare"></nuovo-componente>

In pratica quando inserisci il template all’interno del codice html, indichi al suo interno il contenuto della proprietà messaggio. Ma questo non basta, affinchè il codice funzioni correttamente devi indicare quale o quali proprietà verranno trasferite al componente. Il codice visto prima deve diventare così:

Vue.component('nuovo-componente', { 
    template: '<h1>{{ messaggio }}</h1>',
    props: {
        messaggio: {
            type: String
        }
    }
})

Ti potrà capitare di vedere l’opzione props in una sintassi abbreviata, sotto forma di array, così: props: [‘messaggio’], ma secondo le linee guida di Vue.js è bene dettagliare il più possibile le proprietà al suo interno, specificandone almeno il tipo di valore.
Fai attenzione alle proprietà che hanno un nome composto e che utilizzano il così detto camelCase, come ad esempio mioValore. In questo caso per trasferire un valore attraverso il template non potrai scrivere

<template mioValore="contenuto da definire"></template>

perchè mioValore in questo caso funge da attributo html, e notoriamente il linguaggio html è “case unsensitive” cioè non distingue tra maiuscole e minuscole, per cui mioValore e miovalore sarebbero praticamente uguali. Per evitare questo inconveniente la sintassi corretta è la seguente:

<template mio-valore="contenuto da definire"></template>

Se il passaggio di dati fosse limitato ad una stringa da digitare all’interno di un attributo del tag dei componenti, questi non sarebbero particolarmente utili. Invece, è possibile trasferire al componente anche il valore di proprietà dell’istanza di Vue o di componenti ai componenti figli. Supponi di avere il codice che segue, dove le opzioni props sono costituite da due oggetti, “messaggio2” e “miaVariabile”.

Vue.component('nuovo-componente', { 
    template: '<h1>{{ messaggio2 }}<span>{{miaVariabile}}</span></h1>',
    props: {
        messaggio2: {
            type: String
        },
        miaVariabile: {
            type: String
        }
    }
})
            
var vm = new Vue({
    el: '#main-app',
    data: {
        altro: 'bla bla bla',
    }
})

Supponi di volere assegnare a quest’ultima il valore della proprietà “altro” dell’oggetto data dell’istanza Vue. Per fare questo dobbiamo utilizzare la direttiva v-bind all’interno del tag del componente, come nel codice sotto (in questo caso abbiamo utilizzato la sintassi abbreviata di v-bind, con i doppi punti)

<nuovo-componente messaggio2="questo il testo da visualizzare" :mia-variabile="altro"></nuovo-componente>

Inseriamo i componenti nel nostro sito

Riprendiamo il file che abbiamo usato dall’inizio della guida, che dovrebbe avere attualmente il contenuto che segue:

<!DOCTYPE html>
<html>
    <head>
        <title>Vue Tutorial</title>
        <link rel="stylesheet" href="foundation.min.css">
        <link href="https://fonts.googleapis.com/css?family=Open+Sans|Oswald:700" rel="stylesheet">
        <link rel="stylesheet" href="style.css">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    </head>
     
    <body>
        <div class="wrapper row">
            <header class="site-header">
                    <div class="site-branding small-12 large-3 columns" id="site-branding">
                        <a href="#" title="Vue Tutorial">
                            <h1 class="site-title">Vue Tutorial</h1>
                        </a>
                        <p class="site-description">Il sito per creare con Vue.js</p>
                    </div><!-- .site-branding -->
                    <div class="site-header-menu small-12 large-9 columns">
                        <nav class="main-navigation" role="navigation" aria-label="Primary Menu">
                            <ul id="menu-primary" class="menu">
                                <li class="menu-item">
                                    <a href="#">Home</a>
                                </li>
                                <li class="menu-item">
                                    <a href="#">Chi siamo</a>
                                </li>
                                <li class="menu-item">
                                    <a href="#">Blog</a>
                                </li>
                                <li class="menu-item">
                                    <a href="#">Contatti</a>
                                </li>
                            </ul>
                        </nav><!-- .main-navigation -->
                    </div><!-- .site-header-menu -->
            </header>
            <section id="main-app">
                <form method="post" action="/">
                    <fieldset>
                        <legend>Seleziona la tipologia di annuncio</legend>
                            <input type="radio" v-model="tipologia" value="vendita"> Vendite
                            <input type="radio" v-model="tipologia" value="affitto"> Affitti
                    </fieldset>
                </form>
                <h1>immobili in {{tipologia}}</h1>
                <ul v-if="annunci.length > 0">
                    <li class="small-12 large-4 columns" v-for="(annuncio, i) in annunci" v-bind:key="i">
                        <img :src="annuncio.acf.immagine_1">
                    <p>{{annuncio.title.rendered}}</p>
                    <p>{{annuncio.acf.indirizzo}} - {{annuncio.acf.citta}}</p>
                    <p>&euro; {{annuncio.acf.richiesta}} - $ {{Number(annuncio.acf.richiesta) * 1.22717}}</p> 
                 </li>
                </ul>
                <p v-else>Siamo spiacenti, non ci sono annunci al momento</p>
                
            </section>
            <footer class="site-footer">
                <ul>
                    <li><h3>Contatti</h3></li>
                    <li>Facebook</li>
                    <li>LinkedIn</li>
                    <li>Google+</li>
                </ul>
            </footer>
        </div>
        <script>
            var affitti = [
                {
                    "title": {
                      "rendered": "Appartamento arredato zona centro"
                    },
                    "acf": {
                      "indirizzo": "Via Vattelappesca n. 4",
                      "citta": "Venezia",
                      "vani": "2",
                      "mq": "80",
                      "richiesta": "1500",
                      "descrizione": "Appartamento arredato, 2 vani + cucina abitabile, ottimo stato",
                      "immagine_1": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/venice-2451047_640.jpg",
                      "immagine_2": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/venice-3057446_640.jpg",
                      "immagine_3": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/venice-2928217_640.jpg",
                      "tipologia": "affitto"
                    }
                  },
                  {
                  "title": {
                    "rendered": "Attico ristrutturato"
                  },
                  "acf": {
                    "indirizzo": "Via Vattelappesca n. 5",
                    "citta": "Firenze",
                    "vani": "3",
                    "mq": "75",
                    "richiesta": "1400",
                    "descrizione": "Delizioso attico zona centro, completamente ristrutturato",
                    "immagine_1": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/florence-2493041_640.jpg",
                    "immagine_2": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/street-2921554_640.jpg",
                    "immagine_3": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/dome-541170_640.jpg",
                    "tipologia": "affitto"
                  }
                },
                {
                  "title": {
                    "rendered": "Villetta in residence"
                  },
                  "acf": {
                    "indirizzo": "Via Vattelappesca n. 6",
                    "citta": "Marsala",
                    "vani": "3",
                    "mq": "80",
                    "richiesta": "400",
                    "descrizione": "Villetta in residence, superficie 80 mq, giardino e spazi esterni.",
                    "immagine_1": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/interior-1026454_640.jpg",
                    "immagine_2": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/marsala.jpg",
                    "immagine_3": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/sicily-1191436_640.jpg",
                    "tipologia": "affitto"
                  }
                },
            ];

            var vendite = [
                {
                    "title": {
                      "rendered": "Monolocale da ristrutturare"
                    },
                    "acf": {
                      "indirizzo": "Via Vattelappesca n. 3",
                      "citta": "albenga",
                      "vani": "1",
                      "mq": "40",
                      "richiesta": "50000",
                      "descrizione": "Monolocale piano terra da ristrutturare",
                      "immagine_1": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/albenga-673212_640.jpg",
                      "immagine_2": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/albenga-673259_640.jpg",
                      "immagine_3": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/centa-673151_640.jpg",
                      "tipologia": "vendita"
                    }
                },
                {
                    "title": {
                      "rendered": "Trullo"
                    },
                    "acf": {
                      "indirizzo": "Via Vattelappesca n. 2",
                      "citta": "Alberobello",
                      "vani": "2",
                      "mq": "50",
                      "richiesta": "800000",
                      "descrizione": "Caratteristico ed affascinante trullo",
                      "immagine_1": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/puglia-2740078_640.jpg",
                      "immagine_2": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/alberobello-1816422_640.jpg",
                      "immagine_3": "https://www.alessandrocosta.pro/wp-content/uploads/2018/01/house-552083_640.jpg",
                      "tipologia": "vendita"
                    }
                },
                {
                    "title": {
                      "rendered": "Appartamento zona centro"
                    },
                    "acf": {
                      "indirizzo": "Via Vattelappesca n. 1",
                      "citta": "Roma",
                      "vani": "5",
                      "mq": "140",
                      "richiesta": "900000",
                      "descrizione": "Appartamento prestigioso completamente ristrutturato fronte Colosseo. Non ti accorgerai di averlo acquistato!",
                      "immagine_1": "https://www.alessandrocosta.pro/wp-content/uploads/2017/12/building-2942786_1280-e1515500315125.jpg",
                      "immagine_2": "https://www.alessandrocosta.pro/wp-content/uploads/2017/12/rome-2030648_640.jpg",
                      "immagine_3": "https://www.alessandrocosta.pro/wp-content/uploads/2017/12/rome-2093608_640.jpg",
                      "tipologia": "vendita"
                    }
                }
            ];
            var vm = new Vue({
            el: '#main-app',
            data: {
              tipologia: 'vendita'
            },
            computed: {
                annunci: function () {
                  if (this.tipologia === 'affitto') {
                    return affitti;
                  } else if(this.tipologia === 'vendita') {
                    return vendite
                  }
                }
            },
        })
        </script>
    </body>
</html>

Adesso trasformeremo questo file in modo da ottenere lo stesso risultato finale attraverso l’uso dei componenti.
Iniziamo creando il nostro componente, prima dell’istanza Vue inserisci questo codice:

Vue.component('elenco-annunci', {
    template: '',
    props: {
        immobili: {
            type: Array
        }
}
});

All’interno del template dobbiamo inserire la lista di annunci che attualmente troviamo all’interno della section id=”main-app” per cui taglia e incolla tutto il codice che va dall’apertura del tag ul alla sua chiusura, ed incollalo come contenuto del template del componente elenco-annunci. Ricorda di aggiungere alla fine di ogni riga il simbolo \. A questo punto il componente dovrebbe avere questo aspetto:

Vue.component('elenco-annunci', {
    template: '<ul v-if="annunci.length > 0">\
        <li class="small-12 large-4 columns" v-for="(annuncio, i) in annunci" v-bind:key="i">\
            <img :src="annuncio.acf.immagine_1">\
            <p>{{annuncio.title.rendered}}</p>\
            <p>{{annuncio.acf.indirizzo}} - {{annuncio.acf.citta}}</p>\
            <p>&euro; {{annuncio.acf.richiesta}} - $ {{Number(annuncio.acf.richiesta) * 1.2187}}</p>\
        </li>\
    </ul>',
    props: {
        immobili: {
            type: Array
        }
}
});

Poichè la nostra props si chiama “immobili” dobbiamo modificare leggermente i tag ul e li sostituendo “annunci” con “immobili”, come nel codice sotto:

<ul v-if="immobili.length > 0">\
    <li class="small-12 large-4 columns" v-for="(annuncio, i) in immobili" v-bind:key="i">\

Il resto del codice non richiede variazioni. Torna su nella section id=”main-app”. Avendo tagliato il codice contenuto all’interno del tag ul, la sezione dovrebbe concludersi con il tag h1. Se provi a visualizzare la pagina nel browser puoi vedere che gli annunci sono spariti, e non potrebbe essere altrimenti, perchè abbiamo creato il componente ma non lo abbiamo ancora richiamato all’interno del codice HTML. Per fare questo, torna nella section id=”main-app” e prima della chiusura del tag section inserisci questo codice:

<elenco-annunci :immobili="annunci"></elenco-annunci>

Il codice dovrebbe esserti abbastanza chiaro, il tag elenco-annunci rispecchia il nome che abbiamo dato al componente quando abbiamo scritto Vue.component(‘elenco-annunci’, poichè dobbiamo trasferire al componente i dati contenuti all’interno della proprietà computed annunci, che verranno trasferiti alla propimmobili del componente, utilizziamo la direttiva v-bind nella sua forma abbreviata :immobili=”annunci”. Prova a ricaricare la pagina del browser e tutto dovrebbe funzionare correttamente.

Adesso è il momento di creare un altro componente, dove inseriremo il form necessario a selezionare il tipo di annuncio che ci interessa. Inserisci questo codice subito dopo quello relativo al primo componente

Vue.component('selezione-annunci', {
    template: '',
    data: function() {
        return {
            tipo: 'vendita'
        }
    },
});

Torna all’interno della section id=”main-app”, taglia tutto il codice all’interno del tag form ed incollalo all’interno della proprietà template del componente, ricordando sempre di digitare il simbolo \ alla fine di ogni riga. Racchiudi il form all’interno di un tag div e taglia/incolla al suo interno anche il tag h1 Puoi cancellare il paragrafo con l’attributo v-else che non ci serve più. Il nuovo codice dovrebbe essere il seguente:

Vue.component('selezione-annunci', {
    template: '<div>\
         <form method="post" action="/" ref="input">\
             <fieldset>\
                 <legend>Seleziona la tipologia di annuncio</legend>\
                 <input type="radio" v-model="tipologia" value="vendita"> Vendite
                 <input type="radio" v-model="tipologia" value="affitto"> Affitti
             </fieldset>\
         </form>\
         <h1>Immobili in {{tipologia}}</h1>\
     </div>',
     data: function() {
        return {
            tipo: 'vendita'
        }
     },    
});

Poichè abbiamo inserito all’interno della funzione data la proprietà tipo modifichiamo il valore della direttiva v-model da tipologia a tipo. Anche all’interno del tag h1 modifichiamo il contenuto delle doppie parentesi graffe da tipologia a tipo. Avremmo potuto anche lasciare tutto inalterato nominando “tipologia” la proprietà della funzione data, ma siccome tipologia è anche la proprietà dell’oggetto data dell’istanza di Vue, preferisco chiamarle diversamente per farti avere ben chiaro quale proprietà stiamo modificando e come. A questo punto il tuo codice dovrebbe corrispondere a quello sotto:

Vue.component('selezione-annunci', {
    template: '<div>\
         <form method="post" action="/" ref="input">\
             <fieldset>\
                 <legend>Seleziona la tipologia di annuncio</legend>\
                 <input type="radio" v-model="tipo" value="vendita"> Vendite
                 <input type="radio" v-model="tipo" value="affitto"> Affitti
             </fieldset>\
         </form>\
         <h1>Immobili in {{tipo}}</h1>\
     </div>',
     data: function() {
        return {
            tipo: 'vendita'
        }
     },    
});

Adesso dobbiamo richiamare il componente all’interno del codice html compreso nel tag section id=”main-app”. Al posto del form che hai tagliato prima, inserisci questo codice:

<selezione-annunci></selezione-annunci>

Prova a ricaricare la pagina web, vedrai che il sito funziona… a metà! Cliccando sulle opzioni del form, “affitti” o “vendite”, cambierà in maniera corrispondente il messaggio “Immobili in (affitto o vendita)”, ma gli annunci visualizzati saranno sempre gli stessi. Cosa è successo? Procediamo con ordine: innanzi tutto il messaggio cambia perchè la direttiva v-model esplica i suoi effetti all’interno del componente. La proprietà tipo all’interno del componente, il cui valore è richiamato all’interno del tag h1 tra le doppie parentesi graffe cambia grazie alla direttiva v-model. Poichè queste variazioni si verificano all’interno del singolo componente, tutto procede correttamente. Il problema è un altro, come trasmettere il nuovo valore al di fuori del componente, comunicandolo all’istanza di Vue che in questo momento è il “genitore” del componente selezione-annunci? Prima abbiamo parlato dell’opzione props, ma in questo caso non può esserci utile, perchè tale opzione serve solo per passare i dati dall’istanza di Vue al compenente (oppure da un componente-genitore al componente-figlio). Qui il caso è opposto, i dati devono passare dal figlio al genitore, e per fare questo dobbiamo usare un evento, in particolare l’evento $emit. Per spiegarmi meglio proverò ad utilizzare un semplice schema, che vedi nell’immagine sotto:
infografica che evidenzia il passaggio di dati fra componenti vue
In pratica il nostro obiettivo è quello di trasferire il valore restituito dal form (affitti o vendite) dal componente selezione-annunci all’istanza di Vue, dopodichè dovremo trasferire tale valore dall’istanza di Vue al componente elenco-annunci affinchè visualizzi sullo schermo gli annunci in vendita o in affitto. Quest’ultima parte del procedimanto la conosciamo, dobbiamo usare la prop, per la prima parte dobbiamo usare l’evento $emit. Modifichiamo di conseguenza il nostro codice:

<input type="radio" v-model="tipo" value="vendita" v-on:focus="updateValue($event.target.value)"> Vendite\
<input type="radio" v-model="tipo" value="affitto" v-on:focus="updateValue($event.target.value)"> Affitti\

Per prima cosa modifica i campi di input del componente in modo tale che ad ogni focus venga chiamato il metodo updateValue che ha come argomento il valore del campo di input (affitto o vendita). dopodichè, sempre all’interno del componente selezione-annunci, dopo l’oggetto data, inserisci il metodo updateValue, così:

methods: {
    updateValue: function(value){
        this.$emit('update:tipo', value)
    }
}

Il metodo updateValue è un funzione, che accetta un argomento value, con lo scopo di aggiornare il valore della proprietà tipo con il valore dell’argomento value. Quindi ogni volta che si clicca sul campo di input, il valore del campo viene assegnato alla proprietà tipo.
Non abbiamo ancora finito, dobbiamo fare in modo che il valore della proprietà tipologia dell’istanza di Vue venga aggiornato con il valore della prorietà tipo. Per fare questo dobbiamo modificare il tag selezione-annunci inserendo il modificatore .sync in questo modo:

<selezione-annunci :tipo.sync="tipologia"></selezione-annunci>

Adesso prova a ricaricare la pagina web, tutto funziona correttamente, i dati passano da un componente all’altro ed il risultato a video cambia di conseguenza.
Siamo arrivati alla fine di questo articolo, nel prossimo ci occuperemo ancora di componenti, ma questa volta distribuiti in file separati. Se hai dubbi o considerazioni da aggiungere scrivi un commento!