back to top

Inizializzare l’ambiente di lavoro con Vue CLI

Finora abbiamo creato delle applicazioni scaricando i file da una CDN e abbiamo registrato i vari componenti localmente o globalmente creando contestualmente un’istanza base di tipo Vue.

Nell’esempio visto nella precedente lezione ci siamo immediatamente accorti che questo approccio inizia a mostrare tutti i suoi limiti all’aumentare delle dimensioni e della complessità di un progetto.

In particolare avevamo riportato tra i problemi più evidenti che:

  • per ciascun componente definiamo la struttura in HTML e la logica tramite Javascript, ma non esiste un metodo per limitare la visibilità delle regole CSS ed inserirle direttamente nella definizione del componente stesso.
  • le sole stringhe template sono poco indicate per strutture HTML complicate.

Per le ragioni esposte sopra e per meglio organizzare l’ambiente di lavoro, sono stati introdotti i cosiddetti Single-file components, ovvero dei file con estensione .vue che vengono poi processati da strumenti come Webpack per creare un file bundle finale. Per ciascun componente dovremo creare un file con estensione .vue.

Dal momento che la configurazione di Webpack è abbastanza tediosa e si richia di trascorrere più tempo a perfezionare il contenuto del file webpack.config.js che ad occuparsi della realizzazione di un’applicazione, è stata introdotta Vue CLI che rende estremamente semplice ed intuitivo il processo di inizializzazione dell’ambiente di lavoro di un nuovo progetto.

Ma procediamo a piccoli passi e vediamo in dettaglio come sono strutturati i file con estensione .vue.

Nel nostro editor di testo preferito (noi useremo Visual Studio Code con l’estensione Vue VS Code Extension Pack) creiamo allora un nuovo file Counter.vue come mostrato sotto. (Per chi fosse interessato la versione non annotata dell’immagine sottostante è stata generata con l’ausilio dell’estensione per VsCode Polacode)

frammento di codice single-file component

In ciascun file sono presenti 3 distinte sezioni che definiscono completamente il componente. Per la struttura del componente ci affidiamo al linguaggio HTML, la logica del componente è realizzata in Javascript e lo stile è ottenuto grazie a delle regole CSS.

<template>
  <!-- markup HTML -->
</tempalte>

<script>
// codice javascript
</script>

<style>
/* regole CSS */
</style>

La sezione <template> consente di definire la struttura di un componente utilizzando il linguaggio HTML. In alternativa possiamo usare un’estensione di quest’ultimo, come Pug, specificando l’opportuno attributo lang="pug". Prima della versione 3 di Vue.js non è consentito avere più di un elemento base nei template. Per questo motivo due o più elementi adiacenti devono essere inseriti in un solo elemento base come mostrato sotto.

<template>
<div>
  <div>1</div>
  <div>2</div>
</div>
</template>

Questo tipo di limitazione è stata finalmente rimossa nella versione 3 di Vue.js.

La logica di un componente sarà racchiusa fra tag <script>. In questo caso possiamo sostituire Javascript con TypeScript specificando l’attributo lang="ts" sull’elemento <script>. Sempre tra i tag <script> possiamo importare altri componenti o funzioni esterne. Esportiamo poi un oggetto di opzioni del tutto identico a quello che nelle precedenti lezioni abbiamo passato al metodo Vue.component().

Rispetto all’approccio visto in esempi passati, abbiamo un elemento <style> nel quale andremo a specificare delle regole CSS. La loro visibilità può essere ristretta al singolo componente attraverso l’attributo scoped. In questo modo le regole CSS avranno valore solo per il singolo componente e non influenzeranno minimamente neanche i componenti discendenti.

Sempre tramite l’attributo lang è possibile indicare che si ha intenzione di utilizzare un preprocessore come SASS o Less oppure uno strumento come PostCSS.

Vue CLI permette di lavorare con i file con estensione .vue occupandosi di tutte le operazioni di elaborazione e compilazione. Possiamo anche visualizzare un solo file grazie ad una funzione che prende il nome di Instant Prototyping per la quale dovremo però installare un package aggiuntivo.

Procediamo quindi con l’installazione e vediamo come visualizzare un’anteprima del file creato sopra nel browser. Per questo motivo apriamo la shell dei comandi ed installiamo i package necessari.

npm install -g @vue/cli @vue/cli-service-global

Il package @vue/cli-service-global è necessario per la funzione di Instant Prototyping.

A questo punto possiamo visualizzare il contatore visto in precedenza spostandoci nella cartella in cui è presente il file Counter.vue ed eseguendo il seguente comando.

vue serve Counter.vue
vue cli instant prototyping

Verrà avviato un server locale. Possiamo allora aprire il browser all’indirizzo indicato e visualizzare il componente Counter con cui potremo poi interagire ed ispezionare con vue-dev-tools.

Creare un nuovo progetto con Vue CLI

Le possibilità offerte da Vue CLI sono numerose e non si limitano alla sola funzione di Instant Prototyping.

Possiamo infatti inizializzare l’ambiente di lavoro con il comando vue create <nome-applicazione> in cui il nome dell’applicazione sarà anche assegnato alla directory base che contiene tutti i file del progetto e al campo name del file package.json dell’applicazione.

Vediamo dunque come sfruttare Vue CLI per creare una semplice applicazione per la gestione degli impegni sulla base del wireframe riportato sotto.

esempio applicazione todo list

Il primo passo da compiere è quello di individuare i diversi componenti in cui intendiamo suddividere l’applicazione.

esempio todo list suddivisa in componenti

Nel caso specifico abbiamo individuato 6 diversi tipi di componenti.

  • Un componente base <App>(rettangolo rosso).
  • Un componente per l’intestazione che contiene soltanto il logo ed è un componente statico.
  • Un componente che racchiude il campo di testo ed il pulsante che permetterà all’utente di aggiungere nuovi impegni alla lista.
  • Un componente per selezionare quali impegni devono essere visualizzati (completati, non completati ecc…).
  • Un componente che rappresenta l’intera lista degli impegni.
  • Un componente per ciascun impegno che presenta anche un checkbox per indicare se un certo task è stato completato ed un pulsante per eliminarlo definitivamente dalla lista.

Anche se non sono stati rappresentati nel wireframe, aggiungiamo anche un componente statico <Footer> e un altro da mostrare quando la lista dei task non completati è vuota.

Siamo quindi pronti a inizializzare l’ambiente di lavoro con il seguente comando che dovremo lanciare dopo esserci spostati nella directory da noi preferita.

vue create todo-list-example

Dopo aver lanciato il comando ci viene chiesto di selezionare in che modo procedere. Per il momento selezioniamo la voce Default che consente di creare soltanto i file strettamente necessari senza dover configurare nulla.

opzione per selezionare il tipo di preset in vue cli

Se tutto procede correttamente, entro qualche minuto verrà completata la procedura di inizializzazione e verrà creata una nuova cartella con il nome dell’applicazione da noi scelto.

In alternativa Vue CLI presenta anche una modalità con interfaccia grafica che possiamo avviare con il comando vue ui. Si aprirà una nuova pagina nel browser in cui potremo selezionare le varie opzioni in modo abbastanza intuitivo.

schermata iniziale di vue cli in modalità gui
creazione di un nuovo progetto in vue cli in modalità gui
dettagli progetto in vue cli in modalità gui
selezione preset in vue cli in modalità gui

Selezionando poi il preset ‘manual’ avremo la possibilità di andare a modificare le diverse funzioni e configurazioni aggiuntive nelle rispettive schede.

scelta funzioni in vue cli in modalità gui
configurazioni aggiuntive in vue cli in modalità gui

Tornando alla nostra applicazione inizializzata via linea di comando o tramite interfaccia grafica, spostiamoci ora nella cartella base e lanciamo il comando tree per visualizzare la struttura dell’applicazione creata da Vue CLI.

tree -aF --dirsfirst -I node_modules 
.
├── public/
│   ├── favicon.ico
│   └── index.html
├── src/
│   ├── assets/
│   │   └── logo.png
│   ├── components/
│   │   └── HelloWorld.vue
│   ├── App.vue
│   └── main.js
├── .gitignore
├── README.md
├── babel.config.js
├── package-lock.json
└── package.json

4 directories, 11 files

Al contrario di altri framework, Vue permette di creare un ambiente di lavoro snello con le strette funzioni necessarie. Come possiamo notare, al di là della cartella node_modules che contiene i package scaricati da NPM e che abbiamo eliminato volutamente dall’elenco riportato sopra, abbiamo soltanto due cartelle public e src oltre ad alcuni file di configurazione.

La cartella public contiene un file index.html che sarà servito dal server locale in fase di sviluppo. Non sarà necessario apportare nessuna modifica a questo file e nella maggior parte dei casi possiamo tranquillamente trascurare l’intera cartella.

Per lanciare il server locale in fase di sviluppo, basterà eseguire il comando npm run serve all’interno della cartella base del progetto.

Prima di passare alla directory src, su cui concentreremo principalmente la nostra attenzione, spieghiamo almeno superficialmente cosa sono gli altri file.

Il file README.md contiene in formato markdown un elenco dei comandi che possiamo lanciare all’interno del progetto. Tali comandi sono definiti nella sezione script del file package.json. che presenta però anche altre configurazioni. Per esempio troviamo quelle per eslint. Potremmo decidere in alternativa di inserirle in un file separato selezionando il preset manual durante la fase di creazione di un nuovo progetto. Nel file package.json sono state anche inserite le liste delle dipendenze e la configurazione dei browser che intendiamo supportare tramite browserlist.

È anche presente il file di configurazione babel.conf.js che Vue CLI inizializza opportunamente (Babel è un compilatore javascript che consente di scrivere del codice con funzionalità non pienamente supportate da tutti i browser. In base al file di configurazione Babel provvederà a convertire il codice in una versione compatibile con le versioni precedenti di JavaScript che anche i browser precedenti riescono a capire).

Il file .gitignore ci assicura che non vengano aggiunti nel repository git dei file non necessari.

Per il momento possiamo tranquillamente trascurare le configurazioni dei file sopra elencati e concentrare la nostra attenzione sullo sviluppo della nostra semplice applicazione.

Ed arriviamo finalmente alla cartella src che è il cuore dell’applicazione e presenta tutti i file su cui andremo a lavorare. Nella cartella assets possiamo inserire eventuali immagini in formato raster o vettoriale. Nel nostro caso aggiungeremo un logo in formato SVG ed un’illustrazione da usare quando la lista degli elementi non completati è vuota. Per l’illustrazione ci siamo affidati ad una di quelle presenti sul sito unDraw che contiene un’ampia collezione di immagini vettoriali personalizzabili con "licenza open-source" senza obbligo di attribuzione. È una sorta di Unsplash per SVG in cui è possibile trovare delle illustrazioni per ogni esigenza.

Nella cartella components inseriremo i file relativi ai diversi componenti che costituiscono la nostra applicazione. Il componente base è invece definito in App.vue.

Il punto di ingresso dell’applicazione è però il file main.js su cui è bene soffermarsi un momento per analizzare il breve frammento di codice in esso presente.

import Vue from 'vue';
import App from './App.vue';

Vue.config.productionTip = false; // 0 

new Vue({
  render: h => h(App) // 1
}).$mount('#app'); // 2

Nel file main.js importiamo Vue e il componente base App. Al punto // 0 chiediamo a Vue di non stampare nella console, all’avvio dell’applicazione, il messaggio che ci ricorda che siamo in development mode.

Creiamo quindi una nuova istanza base di Vue, ma, al contrario di quanto visto nelle precedenti lezioni, in questo caso non usiamo un oggetto di opzioni per la configurazione. Invece ci serviamo della funzione render(). Può forse risultare poco intuitivo qual è il ruolo di questa funzione e cosa fa esattamente. La funzione render() riceve in ingresso una funzione, che viene eseguita, e restituisce il suo risultato. Il nome della funzione che render() riceve come argomento è semplicemente ‘h’ che sta per Hyperscript, ovvero "Script che genera una struttura in formato HTML".

Per rendere il tutto più semplice possiamo procedere al refactor della funzione in una forma del tutto equivalente, ma che è probabilmente più familiare, ovvero la tipica funzione anonima di ES5.

{
render: function(createElement) {
  return createElement(App);
}
}

A questo punto è forse più chiaro qual è il ruolo della funzione render() a cui Vue passa un argomento che per maggiore chiarezza abbiamo rinominato createElement in quanto si occupa di creare un nuovo elemento base ed i relativi elementi discendenti a partire dal modello fornito dal componente App.

Abbiamo quindi creato una nuova istanza base di Vue, ma dobbiamo indicare qual è l’elemento base sul quale deve avere il controllo l’applicazione. Nelle precedenti lezioni avevamo usato la proprietà el dell’oggetto delle opzioni. In questo caso viene invece usato il metodo predefinito vm.$mount() a cui passiamo come argomento il selettore CSS '#app' relativo all’elemento <div id="app"> che è presente nel file index.html all’interno della cartella public.

Il componente App è il componente base e si occupa di mantenere lo stato dell’intera applicazione. In questo caso si tratterà di un oggetto con due proprietà, ovvero un array todos con l’elenco di tutti i task aggiunti dall’utente e una stringa currentView che stabilisce quali todos devono essere mostrati. Il motivo per cui affidiamo al componente base il ruolo di conservare lo stato dell’applicazione è da ricondurre al fatto che due suoi componenti discendenti, che sono fra loro adiacenti, hanno necessità di accedere alla lista dei todos.

Prima di vedere il contenuto del componente App, rimuoviamo i file predefiniti non necessari ed aggiungiamo dei nuovi file relativi ai componenti che andremo poi ad importare in App.vue nella cartella components .

tree components                     
components
├── TheFooter.vue
├── TheHeader.vue
├── TodoInputField.vue
├── TodoList.vue
├── TodoListFilter.vue
├── TodoListItem.vue
└── TodoListNoItems.vue

0 directories, 7 files

Ciascuno di questi file conterrà per il momento il seguente frammento di codice a partire dal quale andremo poi a definire completamente il comportamento di ogni componente.

<template>

</template>

<script>
export default {}
</script>


<style scoped>

</style>

Tornando al componente App, definiamo la struttura dell’applicazione fra i tag <template>.

<template>
  <div class="wrapper">
    <div class="container">
      <TheHeader />
      <TodoInputField @new-todo="onNewTodo" />
      <div v-if="displayTodos">
        <TodoListFilter @change-view="onChangeView" :picked="currentView" />
        <div
          class="no-completed-todos"
          v-if="currentView === 'completed' && !noOfCompletedTodos"
        >
          Non ci sono task completati 🤔
        </div>
        <TodoList
          @delete-todo="deleteTodo"
          @todo-status-update="updateTodoStatus"
          :todos="visibleTodos"
        />
      </div>
      <TodoListNoItems
        :no-of-completed-todos="noOfCompletedTodos"
        @change-view="currentView = $event"
        v-else
      />
    </div>
    <TheFooter />
  </div>
</template>

Notiamo la presenza di Header e Footer che occuperanno rispettivamente la parte superiore ed inferiore della pagina. Si tratta di due semplici componenti statici che riportiamo sotto.

// file TheHeader.vue
<template>
  <h1><img src="../assets/logo-dark-flat.svg" alt="todo list"></h1>
</template>

<style scoped>
/* regole CSS omesse per brevità */
</style>

Nel file TheHeader.vue abbiamo omesso la sezione <script> dal momento che si tratta di un componente statico che non importa nessun altro componente. Non avendo proprietà o metodi, non necessita infatti di un oggetto javascript per definirlo. Abbiamo invece un elemento <style> con attributo scoped in modo che tutte le regole presenti vengano applicate soltanto al componente Header.

Le considerazioni fatte per Header valgono anche per il componente Footer.

<template>
  <footer>Made with 💻 and Vue.js</footer>
</template>

<style scoped>
/* regole CSS omesse per brevità */
</style>

Sempre nel template del componente App abbiamo un altro componente TodoInputField che presenta un campo di input ed un pulsante. Questo componente emette un evento personalizzato 'new-todo' al click del pulsante.

Vediamo allora come è fatto il componente TodoInputField analizzando l’omonimo file presente nella cartella components.

// TodoInputField.vue
<template>
  <form @submit.prevent="onSubmit">
    <input 
      type="text"
      placeholder="Aggiungi un nuovo task..."
      name="new-todo" 
      id="new-todo"
      autofocus
      autocomplete="off"
      v-model="currentTodo">
    <button 
      type="submit"
      class="btn no-highlights"
      id="submit"
    >
      <span class="btn-value--md">Aggiungi Task</span>
      <span class="btn-value--sm">➔</span>
    </button>
  </form>
</template>

<script>
export default {
  name: 'TodoInputField',
  data() {
    return {
      currentTodo: ''
    }
  },
  methods: {
    onSubmit() {
      const value = this.currentTodo && this.currentTodo.trim();
      if (value) {
        this.$emit('new-todo', value);
        this.currentTodo = '';
      }
    }
  }
}
</script>

<style scoped>
/* regole CSS omesse */
</style>

Il componente TodoInputField contiene un form in cui sono presenti un campo di testo ed un pulsante (Il valore del pulsante cambia a seconda della larghezza della finestra. Per i dispositivi con viewport più stretto useremo semplicemente una freccia).

Qui intercettiamo l’evento submit e tramite modificatore .prevent blocchiamo il comportamento standard. In caso contrario verrebbe ricaricata l’intera pagina.

Per il campo di input usiamo la direttiva v-model che non abbiamo mai incontrato finora. Tale direttiva consente di implementare velocemente la tecnica che prende il nome di two-way data binding. Ciò significa che al campo di testo associamo una proprietà, nel caso specifico è la proprietà currentTodo. Non appena il campo di testo cambia valore, la proprietà currentTodo viene aggiornata, viceversa se modifichiamo currentTodo, Vue provvede a settare il valore del campo di input col nuovo valore.

Se tutto ciò può sembrare astratto, consideriamo il seguente campo di input.

<input type="text" v-model="currentTodo">

Possiamo creare un campo di input equivalente utilizzando le conoscenze che abbiamo acquisito nelle precedenti lezioni. Possiamo quindi tradurre l’esempio di sopra nel seguente modo.

<input 
  type="text" 
  @input="currentTodo = $event.target.value" 
  :value="currentTodo">

O immaginando di avere definito un metodo OnIput.

onInput(event) { this.currentTodo = event.target.value; }
<input type="text" @input="onInput" :value="currentTodo">

Il componente TodoInputField mantiene quindi al proprio interno il valore corrente del campo di input e, non appena viene premuto il pulsante ‘Aggiungi Task’ (o ‘->’ nelle finestre più strette), emette un evento new-todo a cui associa il valore del campo di input dopo aver eliminato spazi superflui. Procede poi a resettare il campo assegnando alla proprietà currentTodo il valore ”.

Il componente App intercetta l’evento 'new-todo' con il metodo onNewTodo.

methods: {
  onNewTodo(newTodoValue) {
    if (newTodoValue) {
      this.todos.push(this.createNewTodo(newTodoValue));
    }
  }
}

Al verificarsi di tale evento viene invocato l’altro metodo createNewTodo() che si occupa di creare un nuovo oggetto, con la struttura riportata sotto, che viene aggiunto all’array todos mantenuto da App.

{
  id: uniqid(),
  date: new Date(),
  completed: false,
  value
}

Riportiamo allora il codice javascript del componente App per capire meglio il suo funzionamento.

<script>
import uniqid from 'uniqid';

import TheHeader from '@/components/TheHeader.vue';
import TheFooter from '@/components/TheFooter.vue';
import TodoInputField from '@/components/TodoInputField.vue';
import TodoListFilter from '@/components/TodoListFilter.vue';
import TodoList from '@/components/TodoList.vue';
import TodoListNoItems from '@/components/TodoListNoItems.vue';

export default {
  name: 'App',
  components: {
    TheHeader,
    TodoInputField,
    TodoList,
    TodoListFilter,
    TodoListNoItems,
    TheFooter
  },
  data() {
    return {
      todos: [],
      currentView: 'all'
    };
  },
  computed: {
    noOfCompletedTodos() {
      return this.completedTodos().length;
    },
    displayTodos() {
      const active = this.activeTodos();
      const completed = this.completedTodos();
      return (
        active.length ||
        (
          !active.length && completed.length && 
          this.currentView === 'completed'
        )
      );
    },
    visibleTodos() {
      if (this[this.currentView + 'Todos']) {
        return this[this.currentView + 'Todos']();
      }
      return this.todos;
    }
  },
  methods: {
    deleteTodo(todoId) {
      const foundIndex = this.todos.findIndex(obj => obj.id === todoId);
      if (foundIndex < this.todos.length) {
        this.todos.splice(foundIndex, 1);
      }
    },
    updateTodoStatus(todoId) {
      const foundElement = this.todos.find(obj => obj.id === todoId);
      foundElement.completed = !foundElement.completed;
    },
    onChangeView(picked) {
      this.currentView = picked;
    },
    onNewTodo(newTodoValue) {
      if (newTodoValue) {
        this.todos.push(this.createNewTodo(newTodoValue));
      }
    },
    createNewTodo(value) {
      return {
        id: uniqid(),
        date: new Date(),
        completed: false,
        value
      };
    },
    activeTodos() {
      return this.todos.filter(todo => {
        return !todo.completed;
      });
    },
    allTodos() {
      return [...this.activeTodos(), ...this.completedTodos()];
    },
    completedTodos() {
      return this.todos.filter(todo => {
        return todo.completed;
      });
    }
  }
};
</script>

Aprimo una breve parentesi e, prima di continuare con il resto dell’applicazione, analizziamo il file App.vue in cui importiamo il package uniqid che abbiamo precedentemente installato con il comando npm i uniqid. Grazie a quest’ultimo generiamo degli identificativi univoci. Importiamo poi tutti i componenti della cartella components che registriamo localmente nel componente App. Notiamo che il percorso di ciascun componente inizia con il carattere ‘@’. Vue CLI infatti utilizza Webpack che, tramite la configurazione resolve.alias, provvede a sostituire al carattere ‘@’ il percorso completo della cartella ‘src’. In questo modo useremo sempre un percorso del tipo @/<cartella-dentro-directory-src>.

Continuando con l’analisi dell’intera applicazione, quando aggiungiamo un nuovo elemento all’array degli impegni, vengono ricalcolate tutte le computed properties che dipendono dalla proprietà todos. Per questo motivo displayTodos assumerà valore true e verranno mostrati sullo schermo il selettore e la lista di task da completare. A quest’ultima passeremo una prop todos a cui assegniamo il valore della computed property visibleTodos.

E per calcolare visibleTodos invochiamo in modo dinamico uno di tre possibili metodi: activeTodos(), allTodos e completedTodos. I loro nomi sono composti da uno dei tre valori che può assumere la proprietà currentView (active, all, completed) e dalla parola ‘Todos’.

Sia activeTodos() che completedTodos() usano il metodo Array.prototype.filter() che restituisce un nuovo array in base alla funzione che riceve in ingresso. Nel caso specifico selezioniamo gli oggetti la cui proprietà completed è rispettivamente pari a false e true.

Per il metodo allTodos() restituiamo un nuovo array in cui anteponiamo i task attivi a quelli completati in modo da visualizzarli prima nella lista.

Attraverso la direttiva v-on, il componente App registra inoltre i metodi deleteTodo e updateTodoStatus in risposta ai due eventi personalizzati delete-todo e todo-status-update che vengono lanciati dai singoli componenti TodoListItem per segnalare che un oggetto presente nell’array todos deve essere rimosso o ha cambiato stato (completato o attivo). Per questo entrambi i metodi ricevono in ingresso l’identificativo dell’oggetto.

Il principio di funzionamento è quello che abbiamo più volte descritto. I componenti figli si limitano a segnalare tramite eventi che deve essere aggiornato lo stato dell’applicazione. Il componente, che mantiene tale stato e che solitamente è la radice dell’albero dei componenti, si occupa di aggiornare le corrette informazioni. Questa operazione avvia poi il processo di aggiornamento e distribuzione delle nuove informazioni ai componenti discendenti.

Abbiamo detto che ciascun elemento della lista emette un evento. Ci si può quindi chiedere come mai abbiamo intercettato gli eventi personalizzati sull’intera lista.

La ragione è che, utilizzando gli eventi e le props come mezzo di comunicazione fra componenti, siamo obbligati a passare le informazioni un passo alla volta.

Il componente TodoList si occuperà dunque di trasmettere i dati di ciascun oggetto dell’array todos ai singoli TodoListItem. Viceversa resterà in ascolto dei due eventi delete-todo e todo-status-update e si limiterà semplicemente a rilanciarli verso il componente App.

<template>
  <ul>
    <TodoListItem 
      v-for="todo in todos" 
      :key="todo.id"  
      :todo="todo" 
      @todo-status-update="forward"
      @delete-todo="forward" />
  </ul>
</template>

<script>
import TodoListItem from './TodoListItem';

export default {
  name: 'TodoList',
  components: {
    TodoListItem
  },
  props: {
    todos: Array
  },
  methods: {
    forward(payload) {
      if (payload && payload.event) {
        this.$emit(payload.event, payload.todoId);
      }
    }
  }
}
</script>

<style scoped>
ul {
  list-style: none;
  padding: 0;
  max-width: 768px;
  margin: 0 auto;
}
</style>

Ciascun componente TodoListItem riceve le informazioni da TodoList attraverso la prop todo. Emette invece un evento quando viene selezionato il checkbox per indicare che il task è stato completato ed un altro evento per segnalare che un task deve essere rimosso dall’array todos mantenuto dal componente App. A ciascun evento viene associato un oggetto di dati che presenta due proprietà: la prima indica che tipo di evento si è verificato, la seconda rappresenta l’identificativo del task. Queste informazioni sono poi usate dal componente TodoList per passare le informazioni verso l’alto usando il solo metodo forward().

<template>
  <li :class="{completed: todo.completed}">
    <div class="info-container">
      <div class="todo-container">
        <label class="checkbox-label">
          <input 
            type="checkbox" 
            class="checkbox" 
            name="status" 
            id="status" 
            :checked="todo.completed"
            @change="onChange(todo.id)">
          <span class="checkbox-custom"></span>
        </label>
        <span>{{ todo.value }}</span>
      </div>
      <button @click="onClick(todo.id)" class="no-highlights" title="Delete task">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
      </button>
    </div>
    <small>{{ date }}</small>
  </li>
</template>

<script>
import { today, yesterday, sameDay } from '@/utilities/time';

export default {
  name: 'TodoListItem',
  props: {
    todo: {
      type: Object,
      required: true
    }
  },
  computed: {
    date() {
      const todoDate = this.todo.date;

      const isToday = sameDay(todoDate, today);

      const wasYesterday = sameDay(todoDate, yesterday);
      
      if (isToday) {
        return 'Oggi';
      }

      if (wasYesterday) {
        return 'Ieri';
      }
      
      return this.todo.date.toLocaleDateString();
    }
  },
  methods: {
    onClick(todoId) {
      this.$emit('delete-todo', {event: 'delete-todo' , todoId});
    },
    onChange(todoId) {
      this.$emit('todo-status-update', {event: 'todo-status-update', todoId });
    }
  }
}
</script>

<style scoped>
/* omesso per brevità */
</style>

Il componente TodoListItem emette due eventi per segnalare il completamento di un task o la volontà di cancellarlo.

Per ciascun elemento mostriamo anche la data anche se per ora alla chiusura della finestra del browser l’array todos viene resettato.

Per determinare quando è stato creato il task e mostrare le informazioni corrette tramite la computed property date, creiamo una funzione sameDay() e due costanti today e yesterday in un nuovo file utilities/time.js. Per il tipo di metodi messi a disposizione dall’oggetto Date, usiamo una funzione anonima auto-eseguibile per calcolare la costante yesterday.

export const today = new Date();
export const yesterday = 
  ( d => new Date(d.setDate(d.getDate()-1)) )(new Date());

export const sameDay = (d1, d2) => d1.getDate() === d2.getDate() &&
  d1.getMonth() === d2.getMonth() &&
  d1.getFullYear() === d2.getFullYear();

Per visualizzare i diversi elementi dell’array todos in base al loro stato di completamento, utilizziamo infine il componente TodoListFilter.

<template>
  <div class="todo-list-filter">
    <template v-for="filter in filters">
      <input 
        type="radio" 
        name="filter" 
        :key="'input_' + filter" 
        :id="filter" 
        :value="filter"
        :checked="picked === filter"
        @change="onChange(filter)"
        >
      <label 
        :key="'label_' + filter" 
        class="no-highlights" 
        :for="filter">{{ filter.charAt(0).toUpperCase() + filter.substring(1) }}</label>
    </template>
  </div>
</template>

<script>
export default {
  name: 'TodoListFilter',
  props: {
    picked: {
      type: String,
      default: 'all'
    }
  },
  data() {
    return {
      filters: ['all', 'completed', 'active']
    }
  },
  methods: {
    onChange(filter) {
      this.$emit('change-view', filter);
    }
  }
}
</script>

<style scoped>
</style>

Il componente TodoListFilter registra un metodo da eseguire quando viene selezionato uno dei pulsanti radio costruiti tramite la direttiva v-for. Viene quindi lanciato un nuovo evento personalizzato ‘change-view’ con il nome del tipo di task da visualizzare che può essere uno dei seguenti valori: ‘all’, ‘completed’, ‘active’.

Nel componente base App ricordiamo che avevamo registrato una funzione da valutare in risposta dell’evento ‘change-view’ in seguito al quale viene cambiato il valore della proprietà currentView.

Concludiamo dicendo che, se la lista dei task è vuota o se sono presenti solo todos completati, mostriamo il componente TodoListNoItems a cui passiamo il numero di task completati attualmente presenti nell’array todos in modo da mostrare eventualmente un link che permetta di visualizzarli.

<template>
  <div class="illustration-container">
    <HappyMusic class="illustration" />
    <p class="message">
      Grandioso! Non hai nuovi task da completare!<br> 
      Rilassati e goditi la giornata 😎
    </p>
    <a 
      v-if="noOfCompletedTodos"
      @click.prevent="$emit('change-view', 'completed')"
      href="">
        Mostra Tasks completati
      </a>
  </div>
</template>

<script>
import HappyMusic from '@/assets/undraw_happy_music.svg?inline';

export default {
  name: 'TodoListNoItems',
  components: {
    HappyMusic
  },
  props: {
    noOfCompletedTodos: Number
  }
}
</script>

<style scoped>
</style>

In questo componente importiamo il file SVG dell’illustrazione che vogliamo inserire direttamente nella pagina. Notiamo che in questo caso il file SVG è importato come se si trattasse di un altro componente. Per ottenere questo comportamento abbiamo usato un plugin di Vue CLI che prende il nome di vue-cli-plugin-svg. Per installarlo abbiamo precedentemente lanciato il comando vue add svg.

Possiamo quindi vedere nell’immagine sottostante la schermata iniziale…

schermata iniziale todo list

… E il video dell’intera applicazione.

https://vimeo.com/462643581

Volendo poi preparare l’applicazione appena creata per essere messa in produzione, ci basterà eseguire il comando npm run build che procederà all’ottimizzazione del bundle finale e copierà i file da pubblicare all’interno di una cartella dist nella directory base.

La versione finale dell’applicazione è disponibile su Bitbucket.

Riepilogo

In questa lezione abbiamo visto un diverso metodo per configurare l’ambiente di lavoro e creare delle applicazioni attraverso Vue CLI. Così facendo possiamo incapsulare all’interno di ciascun componente le tre diverse parti che lo caratterizzano. Possiamo infatti definire la struttura tramite HTML, la logica grazie a Javascript e lo stile tramite regole CSS di cui possiamo limitare la visibilità al solo componente (neanche i componenti discendenti saranno affetti dalle regole definite nel genitore) grazie all’attributo scoped da applicare sull’elemento <style>.

Pubblicitร