Dopo la parentesi teorica della lezione precedente, nel seguito implementeremo da zero una piccola applicazione di checklist dove sarà possibile segnare come completate attività appartenenti a diversi gruppi.
Per capire meglio che applicazione implementeremo, ecco uno screenshot:
Il codice sorgente è inoltre allegati a questo articolo.
L'applicazione è implementata tramite file Vue, compilati grazie Babel e Webpack, scaricati tramite Yarn.
Store
Il cuore dell'applicazione è lo store, che incapsula il modello dati reso disponibile da Vuex nei vari componenti. Esso viene definito all'interno del file src/vuex/store.js.
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
function findItemByIds(state, checklistId, itemId) {
let checklist = state.checklists.find(checklist => checklist.id === checklistId);
if(!checklist) return null;
return checklist.items.find(item => item.id === itemId);
}
export default new Vuex.Store({
state: {
checklists: [{
id: 1,
name: 'Wishlist',
items: [{
id: 1,
name: 'TV 50 inches',
done: false
}, {
....
}]
} , {
.....
}]
},
getters: {
checklistCount(state) {
return state.checklists.length;
},
completedChecklistCount(state) {
return state.checklists.filter(checklist => {
return checklist.items.every(item => item.done);
}).length;
},
itemsCount(state) {
return state.checklists.reduce((count, checklist) => {
return count + checklist.items.length;
}, 0);
},
completedItemsCount(state) {
return state.checklists.reduce((count, checklist) => {
return checklist.items.reduce((count, item) => {
return count + (item.done ? 1 : 0);
}, count);
}, 0);
}
},
mutations: {
toggle(state, payload) {
let item = findItemByIds(state, payload.checklistId, payload.itemId);
if(item) {
item.done = !item.done;
}
}
},
actions: {
toggle(context, payload) {
context.commit('toggle', payload);
}
}
});
Il file parte con l'inclusione di Vue e di Vuex, con l'installazone di Vuex tramite Vue.use
e con la definizione di una funzione privata utilizzata in una mutation. Successivamente viene inizializzato l'oggetto Vuex.Store a partire da un set di opzioni:
state
: rappresenta il modello dati iniziale dell'applicazione. Nel nostro esempio, per semplicità, esso viene scritto a mano all'interno del file .js, ma in un contesto reale esso potrebbe arrivare dal server tramite una API dedicata;getters
: sono funzioni specifiche che permettono di ritornare informazioni complesse a partire dallostate
. Igetters
vengono definiti direttamente all'interno dello store in modo da poter essere riutilizzati. In particolare abbiamo quattro getters che permettono di ritornare informazioni di sintesi sullo stato attuale delle checklist;mutations
: sono le operazioni che mutano lo stato. Nel caso specifico abbiamo un'unica mutationtoggle
che permette di modificare lo stato di un singolo item;actions
: sono le azioni che vengono "dispatchate" sullo store, l'interfaccia pubblica esterna dellemutations
.
Inizializzazione dell'app e template base
L'inizializzazione dell'app avviene nel file src/app.js. In questo file vengono incluse tutte le dipendenze necessarie, vengono registrati i 4 componenti globali e viene inizializzato l'oggetto Vue al quale viene passato, oltre all'opzione el
, anche lo store descritto in precedenza.
import Vue from 'vue';
import store from './vuex/store.js';
import 'bootstrap/dist/css/bootstrap.min.css';
import VuexChecklistApp from './components/VuexChecklistApp.vue';
import VuexChecklistWrapper from './components/VuexChecklistWrapper.vue';
import VuexChecklistSummary from './components/VuexChecklistSummary.vue';
import VuexChecklist from './components/VuexChecklist.vue';
Vue.component('vuex-checklist-app', VuexChecklistApp);
Vue.component('vuex-checklist-wrapper', VuexChecklistWrapper);
Vue.component('vuex-checklist-summary', VuexChecklistSummary);
Vue.component('vuex-checklist', VuexChecklist);
new Vue({
el: '#app',
store: store
});
Il template iniziale (index.html) non presenta nulla di particolarmente interessante:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vuex checklist workshop</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" >Vuex checklist workshop</a>
</nav>
<div id="app" style="margin-top: 20px">
<vuex-checklist-app />
</div>
<script src="app.js"></script>
</body>
</html>
Il componente vuex-checklist-app
Il componente Vue principale è vuex-checklist-app
, inserito nel file src/components/VuexChecklistApp.js. Esso viene istanziato direttamente dal template globale dell'applicazione e ha come unico scopo quello di definire il layout base e di istanziare due componenti più specifici: vuex-checklist-wrapper
e vuex-checklist-summary
.
<template>
<div class="container">
<div class="row">
<div class="col-md-10">
<vuex-checklist-wrapper />
</div>
<div class="col-md-2">
<vuex-checklist-summary />
</div>
</div>
</div>
</template>
export default {
name: 'vuex-checklist-app'
}
Il componente vuex-checklist-summary
Questo componente, incluso nella spalla destra del sito e definito nel file src/components/VuexChecklistSummary.js, permette di avere un riassunto della situazione attuale delle checklist. Esso sfrutta i getters definiti all'interno dello store e già discussi in precedenza.
<template>
<div>
<p>
Checklists: <strong>{{ checklistCount }}</strong>
</p>
<p>
Done checklists: <strong>{{ completedChecklistCount }}</strong>
</p>
<p>
Items: <strong>{{ itemsCount }}</strong>
</p>
<p>
Done items: <strong>{{ completedItemsCount }}</strong>
</p>
</div>
</template>
import { mapGetters } from 'vuex'
export default {
name: 'vuex-checklist-summary',
computed: mapGetters([ 'checklistCount', 'completedChecklistCount', 'itemsCount', 'completedItemsCount' ])
}
Nel codice Javascript viene inclusa la funzione mapGetters
dal modulo vuex. Questa funzione permette di accedere con semplicità ai metodi getters dello store e di linkarli come proprietà computed del componente Vue. In questo modo possiamo utilizzare le property dello store passate come parametro, esattamente come se fossero proprie del componente. Nel template HTML è possibile vedere il loro agnostico utilizzo.
Il componente vuex-checklist-wrapper
Il wrapper rappresenta la parte principale del layout e permette di istanziare le varie checklist. Esso è definito nel file src/components/VueChecklistWrapper.vue.
<template>
<div class="row">
<div v-for="checklist in checklists" class="col-md-4">
<vuex-checklist :checklist="checklist" />
</div>
</div>
</template>
import { mapState } from 'vuex'
export default {
name: 'vuex-checklist-wrapper',
computed: mapState([ 'checklists' ])
}
Anche in questo componente usiamo una funzione del modulo vuex mapState
. Anch'essa offre una scorciatoia per accedere ai dati presenti nello store, ma in questo caso permette di utilizzare i dati grezzi direttamente dallo store senza dei particolari getters. Grazie ad essa possiamo accedere all'elenco delle checklist come fossero dati propri del componente Vue.
Il componente vuex-checklist
L'ultimo componente rappresenta la singola checklist, istanziato all'interno di un ciclo dal wrapper. Esso è definito nel file src/components/VueChecklist.vue.
<template>
<div class="card">
<img class="card-img-top" :src="image">
<div class="card-body">
<h5 class="card-title">{{ checklist.name }}</h5>
<p class="card-text">{{ checklist.description }}</p>
<ul class="list-group list-group-flush">
<li v-for="item in checklist.items" class="list-group-item" :class="{ 'list-group-item-success' : item.done }" @click="click(item)">{{ item.name }}</li>
</ul>
</div>
</div>
</template>
import store from '../vuex/store.js';
export default {
name: 'vuex-checklist',
props: {
checklist: {
required: true
}
},
computed: {
image: function() {
return "https://placeimg.com/200/70/tech?" + Math.random();
}
},
methods: {
click(item) {
store.dispatch({
type: 'toggle',
checklistId: this.checklist.id,
itemId: item.id
});
}
}
}
La parte principale è sicuramente quella dedicata all'evento click, invocato appunto da un click dell'utente sul singolo elemento. Esso scatena un'azione di tipo toggle
sullo store passando come parametri l'id della checklist e dell'elemento selezionato. L'invocazione dell'azione, come visto prima, scatenerà una mutation sullo store che altererà lo stato e che, grazie alle varie computed property, verrà renderizzata in tempo reale all'utente
Conclusioni
Per quanto l'applicazione sia abbastanza semplice e poco funzionale, essa ci permette di approfondire praticamente tutti gli aspetti di una applicazione Vuex, introducendo non solamente actions e mutations, ma anche dei getters custom. Grazie all'accesso globale delle informazioni importanti, i componenti risultano più snelli in quanto non c'è la necessità di un continuo passaggio di informazioni.
Ovviamente un'implementazione che non prevede l'integrazione con un server e relativo database, ha poco senso e non offre appunto persistenza. La mancanza di questo aspetto è da intendersi esclusivamente a scopo didattico e per potersi concentrare sugli aspetti frontend.