In questa lezione realizzeremo un’applicazione per la raccolta di citazioni e frasi celebri utilizzando Vuex. Per semplicità useremo JSON Server per simulare il salvataggio delle citazioni su un finto server locale. JSON Server fornisce una REST API che permetterà di prelevare e salvare sul server le citazioni aggiunte attraverso un semplice form. Dietro le quinte verrà usato un file db.json
come database.
Dopo aver installato globalmente JSON Server (npm i -g json-server
), spostiamoci in una nuova cartella e lanciamo il comando vue create vuex-example
. Selezioniamo attraverso i tasti freccia Manually select features e premiamo il tasto INVIO.
Seguiamo la medesima procedura dell’esempio visto nella precedente lezione, scegliendo le stesse funzionalità aggiuntive, ovvero Babel, Vuex e Linter/Formatter.
A questo punto possiamo spostarci all’interno della cartella base del nostro progetto per aggiungere un file db.json
che sarà utilizzato come database da JSON server.
{
"quotes": [
{
"id": "1kb6ktl63",
"quoteText": "Sometimes it is the people no one can imagine anything of who do the things no one can imagine.",
"quoteAuthor": "Alan Turing",
"lang": "en"
},
{
"id": "1tb4ktl23",
"quoteText": "Well done is better than well said.",
"quoteAuthor": "Benjamin Franklin",
"lang": "en"
},
{
"id": "1kb6l41ib",
"quoteText": "Bite off more than you can chew, then chew it.",
"quoteAuthor": "Ella Williams",
"lang": "en"
},
{
"id": "1kb6l41m0",
"quoteText": "Never give up. Today is hard, tomorrow will be worse, but the day after tomorrow will be sunshine.",
"quoteAuthor": "Jack Ma",
"lang": "en"
},
{
"id": "1kb6l41mf",
"quoteText": "Nothing in life is to be feared, it is only to be understood. Now is the time to understand more, so that we may fear less.",
"quoteAuthor": "Marie Curie",
"lang": "en"
},
{
"id": "1la4l51me",
"quoteText": "I have not failed. I've just found 10,000 ways that won't work.",
"quoteAuthor": "Thomas A. Edison",
"lang": "en"
},
{
"id": "1kb6l41lz",
"quoteText": "Don't sit down and wait for the opportunities to come. Get up and make them.",
"quoteAuthor": "Madam C. J. Walker",
"lang": "en"
},
{
"id": "1kb6l41m3",
"quoteText": "Compi ogni azione come se fosse l'ultima della vita.",
"quoteAuthor": "Marco Aurelio",
"lang": "it"
},
{
"id": "1kb6l41m4",
"quoteText": "È impossibile per un uomo imparare ciò che crede di sapere già.",
"quoteAuthor": "Epiteto",
"lang": "it"
},
{
"id": "1kb6l41m5",
"quoteText": "Le persone perfette non combattono, non mentono, non commettono errori e non esistono.",
"quoteAuthor": "Aristotele",
"lang": "it"
}
]
}
JSON server si occuperà di mettere a disposizione una REST API completa. Basterà spostarci nella cartella vuex-example
in una nuova finestra del terminale e lanciare il comando json-server -p 5555 -d 2000 --watch db.json
. In questo modo verrà avviato un server locale sulla porta 555 che potremo interpellare dalla nostra applicazione per scaricare in fase di avvio l’elenco delle citazioni già presenti nel database. Con l’opzione -d 2000
introduciamo un ritardo di due secondi nella risposta del server.
Una volta lanciato JSON server, procediamo alla realizzazione della nostra applicazione.
Prima di analizzare il codice dei vari componenti, riportiamo un breve video in cui illustriamo rapidamente il funzionamento dell’applicazione terminata.
Per questa applicazione abbiamo suddiviso lo store Vuex in moduli e abbiamo creato 7 componenti che analizzeremo nel resto della lezione. La struttura finale dell’applicazione è la seguente:
tree vuex-example -aF --dirsfirst -I node_modules
vuex-example
├── public/
│ ├── favicon.ico
│ └── index.html
├── src/
│ ├── assets/
│ │ ├── error.svg
│ │ ├── global.css
│ │ ├── logo.svg
│ │ └── ok.svg
│ ├── components/
│ │ ├── LoadingSpinner.vue
│ │ ├── QuoteFilter.vue
│ │ ├── QuoteForm.vue
│ │ ├── QuoteList.vue
│ │ ├── QuoteListItem.vue
│ │ ├── TheFooter.vue
│ │ └── TheHeader.vue
│ ├── services/
│ │ └── QuoteService.js
│ ├── store/
│ │ ├── modules/
│ │ │ ├── currentViewState.js
│ │ │ ├── editingModeState.js
│ │ │ ├── errorState.js
│ │ │ ├── hasTriedToUploadState.js
│ │ │ ├── loadingState.js
│ │ │ ├── quotesState.js
│ │ │ └── uploadingState.js
│ │ └── index.js
│ ├── App.vue
│ └── main.js
├── .browserslistrc
├── .env.development
├── .eslintrc.js
├── .gitignore
├── .prettierrc.js
├── README.md
├── babel.config.js
├── db.json
├── package-lock.json
└── package.json
7 directories, 34 files
Nel file main.js
ci limitiamo ad importare global.css
che contiene una serie di regole CSS valide per l’intera applicazione.
import Vue from 'vue';
import App from './App.vue';
import store from './store';
Vue.config.productionTip = false;
import '@/assets/global.css';
new Vue({
store,
render: (h) => h(App)
}).$mount('#app');
Sempre nel file main.js
importiamo lo store esportato dal file store/index.js
e lo inseriamo nell’oggetto delle opzioni usato per creare una nuova istanza di Vue. Spostiamoci allora nella cartella store/
ed analizziamo i diversi moduli che compongono lo store Vuex.
import Vue from 'vue';
import Vuex from 'vuex';
import currentViewModule from '@/store/modules/currentViewState';
import editingModeModule from '@/store/modules/editingModeState';
import errorModule from '@/store/modules/errorState';
import hasTriedToUploadModule from '@/store/modules/hasTriedToUploadState';
import loadingModule from '@/store/modules/loadingState';
import quoteModule from '@/store/modules/quoteState';
import uploadingModule from '@/store/modules/uploadingState';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
currentViewModule,
editingModeModule,
errorModule,
hasTriedToUploadModule,
loadingModule,
quoteModule,
uploadingModule
}
});
Nel file store/index.js
importiamo i diversi moduli e li passiamo alla proprietà modules
dell’oggetto di configurazione dello store.
Per ciascun modulo abbiamo usato namespaced: true
per limitare la visibilità di mutations
, actions
, state
e getters
. In questo modo per accedere alle funzionalità definite nel modulo, dovremo sempre scrivere l’intero nome del loro namespace.
// file: store/modules/currentViewState.js
export default {
namespaced: true,
state: () => ({
currentView: 1
}),
mutations: {
SET_CURRENT_VIEW(state, currentView) {
state.currentView = currentView;
}
},
actions: {
setCurrentView({ commit }, currentView) {
commit('SET_CURRENT_VIEW', currentView);
}
}
};
In store/modules/currentViewState.js
definiamo una sola proprietà per lo state, ovvero currentView
. Si tratta di un indice numerico che impieghiamo per decidere quali citazioni devono essere caricate nella lista. In particolare usiamo il valore 0 per le sole citazioni in inglese, 1 per tutte le citazioni e 2 per quelle in italiano.
Definiamo poi un’action e una mutation per settare il valore di state.currentView
. Ricordiamo che alle Actions viene passato un primo argomento context
che espone varie proprietà fra cui la funzione commit()
e state
. Alle actions e alle mutations possiamo passare un argomento quando invochiamo rispettivamente le funzioni dispatch()
e commit()
.
Il secondo file che esploriamo è store/modules/editingModeState
.
// file: store/modules/editingModeState.js
export default {
namespaced: true,
state: () => ({
editingMode: false
}),
mutations: {
SET_EDITING_MODE(state, editingMode) {
state.editingMode = editingMode;
}
},
actions: {
setEditingMode({ commit }, value) {
commit('SET_EDITING_MODE', value);
}
}
};
In questo secondo modulo, lo stato contiene pure una sola proprietà. Si tratta di un valore booleano che utilizzeremo per mostrare o nascondere il form col quale aggiungiamo una nuova citazione al database. Per entrare in modalità ‘editing’, lanceremo quindi l’Action editingModeModule/setEditingMode
con l’istruzione this.$store.dispatch('editingModeModule/setEditingMode', true)
. Vedremo che sarà il componente TheHeader
a lanciare tale Action tramite un apposito pulsante.
Il modulo store/modules/errorState
mantiene le informazioni in merito ad eventuali errori. Presenta un oggetto errors
con due proprietà nelle quali manteniamo eventuali messaggi di errore in fase di upload o download. Attraverso la mutazione ADD_ERROR
, assegniamo un messaggio di errore ad una delle due proprietà che poi resettiamo con la mutazione CLEAR_ERROR
.
// file: store/modules/errorState.js
export default {
namespaced: true,
state: () => ({
errors: {
upload: null,
download: null
}
}),
mutations: {
ADD_ERROR(state, error) {
state.errors[error.type] = error.msg;
},
CLEAR_ERROR(state, type) {
state.errors[type] = null;
}
}
};
Nel file store/modules/hasTriedToUploadState.js
abbiamo invece definito un altro modulo in cui è presente una proprietà booleana hasTriedToUpload
che usiamo poi nel componente QuoteForm
per mostrare dei messaggi di feedback all’utente dopo aver cliccato il pulsante per salvare la citazione nel database. Setteremo hasTriedToUpload
al valore true
non appena viene cliccato il pulsante di invio del form e per farlo ci serviremo della mutazione SET_TRIED_TO_UPLOAD
.
export default {
namespaced: true,
state: () => ({
hasTriedToUpload: false
}),
mutations: {
SET_TRIED_TO_UPLOAD(state, value) {
state.hasTriedToUpload = value;
}
}
};
Passiamo ora all’altro modulo store/modules/loadingModule
. Nello stato abbiamo una proprietà booleana che consente di decidere se mostrare o meno il componente LoadingSpinner
mentre si stanno scaricando le citazioni dal server.
export default {
namespaced: true,
state: () => ({
loading: false
}),
mutations: {
SET_LOADING_STATUS(state, value) {
state.loading = value;
}
},
actions: {
setLoadingStatus({ commit }, value) {
commit('ET_LOADING_STATUS', value);
}
}
};
In store/modules/uploadingState.js
definiamo un altro modulo simile. In questo caso la proprietà booleana dello stato verrà usata per segnalare che è in corso il salvataggio di una citazione nel database.
export default {
namespaced: true,
state: () => ({
uploading: false
}),
mutations: {
SET_UPLOADING_STATUS(state, uploadingStatus) {
state.uploading = uploadingStatus;
}
}
};
Infine nel file store/module/quoteState.js
abbiamo definito il modulo che incapsula tutte le funzionalità per la gestione della lista di citazioni.
import QuoteService from '@/services/QuoteService.js';
export default {
namespaced: true,
state: () => ({
quotes: []
}),
mutations: {
ADD_QUOTE(state, quote) {
state.quotes.push(quote);
},
SET_QUOTES(state, quotes) {
state.quotes = quotes;
}
},
actions: {
async addQuote({ commit }, quote) {
try {
commit('uploadingModule/SET_UPLOADING_STATUS', true, { root: true });
commit('hasTriedToUploadModule/SET_TRIED_TO_UPLOAD', true, {
root: true
});
const response = await QuoteService.postQuote(quote);
if (response.status !== 201) {
throw new Error('It was not possible to save the quote');
}
commit('ADD_QUOTE', quote);
} catch (error) {
commit(
'errorModule/ADD_ERROR',
{ type: 'upload', msg: error.message },
{ root: true }
);
} finally {
commit('uploadingModule/SET_UPLOADING_STATUS', false, { root: true });
setTimeout(() => {
commit('hasTriedToUploadModule/SET_TRIED_TO_UPLOAD', false, {
root: true
});
commit('errorModule/CLEAR_ERROR', 'upload', { root: true });
}, 4000);
}
},
async getQuotes({ commit }) {
try {
commit('loadingModule/SET_LOADING_STATUS', true, { root: true });
commit('errorModule/CLEAR_ERROR', 'download', { root: true });
const response = await QuoteService.getAllQuotes();
commit('SET_QUOTES', response.data);
} catch (error) {
commit(
'errorModule/ADD_ERROR',
{ type: 'download', msg: error.message },
{ root: true }
);
} finally {
commit('loadingModule/SET_LOADING_STATUS', false, { root: true });
}
}
},
getters: {
getCurrentViewQuotes(state, getters, rootState) {
const filters = [
(quote) => quote.lang === 'en',
(quote) => quote,
(quote) => quote.lang === 'it'
];
const filter = filters[rootState.currentViewModule.currentView];
return state.quotes.filter(filter);
}
}
};
Abbiamo definito un’action addQuote
che esegue inizialmente le due Mutations uploadingModule/SET_UPLOADING_STATUS
e hasTriedToUploadModule/SET_TRIED_TO_UPLOAD
per segnalare che sta per effettuare una chiamata al server e potrebbe essere necessario del tempo prima di completare un’operazione asincrona. Invoca quindi la funzione postQuote()
che abbiamo definito nel servizio QuoteService per salvare una nuova citazione sul server locale. Resta quindi in attesa che la promise restituita venga completata. Se non si verificano errori, esegue la mutazione commit('ADD_QUOTE', quote)
per aggiungere la citazione alla proprietà quotes
dello stato locale. In caso contrario invoca una mutazione per segnalare la presenza di un errore. In entrambi i casi, viene invocata una mutazione per indicare che il processo di salvataggio è stato completato e dopo 4 secondi viene settato il valore di hasTriedToUploadModule/hasTriedToUpload
al valore false
per indicare che un tentativo di invio di una citazione è stato definitivamente terminato (Vedremo come usare le varie proprietà booleane nel componente QuoteForm
). Viene inoltre resettata la proprietà dello stato che registra eventuali errori.
L’altra Action è getQuotes()
. Anche in questo caso usiamo async/await per gestire la promise restituita da QuoteService.getAllQuotes()
che consente di recuperare tutte le citazioni dal server.
async getQuotes({ commit }) {
try {
commit('loadingModule/SET_LOADING_STATUS', true, { root: true });
commit('errorModule/CLEAR_ERROR', 'download', { root: true });
const response = await QuoteService.getAllQuotes();
commit('SET_QUOTES', response.data);
} catch (error) {
commit(
'errorModule/ADD_ERROR',
{ type: 'download', msg: error.message },
{ root: true }
);
} finally {
commit('loadingModule/SET_LOADING_STATUS', false, { root: true });
}
}
Resettiamo eventuali errori precedenti ed eseguiamo una mutazione per indicare che è in corso un’operazione che richiede del tempo ('loadingModule/SET_LOADING_STATUS'
). In questo modo, potremo poi mostrare un indicatore di caricamento all’interno dell’applicazione. Inviamo una richiesta al server per recuperare tutte le citazioni e se il processo viene concluso in maniera corretta, assegniamo un array di mutazioni alla proprietà quotes
dello stato locale. In caso di errori eseguiamo la mutazione 'errorModule/ADD_ERROR'
passando un oggetto col tipo di operazione eseguita ed un messaggio di errore. Alla fine del processo chiamiamo la funzione commit per invocare la mutazione loadingModule/SET_LOADING_STATUS
con valore false
. Ricordiamo che per le mutazioni che non appartengono al modulo corrente, dobbiamo specificare il loro nome completo e passare alla funzione commit()
un terzo argomento { root: true }
.
Nel modulo appena descritto, abbiamo usato un servizio che abbiamo definito nel file services/QuoteService.js
.
import axios from 'axios';
const { VUE_APP_REMOTE_ADDRESS: ADDRESS, VUE_APP_PORT: PORT } = process.env;
const axiosInstance = axios.create({
baseURL: `//${ADDRESS}:${PORT}`,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
export default {
getAllQuotes() {
return axiosInstance.get('/quotes');
},
getQuote(id) {
return axiosInstance.get(`/quotes/${id}`);
},
postQuote(quote) {
return axiosInstance.post(`/quotes`, quote);
}
};
Nel file services/QuoteService.js
creiamo una nuova istanza di Axios. Si tratta di un popolare client HTTP per il browser e Node.js che abbiamo installato con il comando npm i axios
. Grazie ad Axios possiamo effettuare richieste al server locale con estrema semplicità. Abbiamo definito una configurazione comune per tutte le richieste passando le opportune opzioni al metodo axios.create()
per indicare al server il tipo del contenuto del corpo delle richieste e dei dati attesi dall’applicazione. Abbiamo poi esportato tre funzioni dal modulo javascript. In particolare, in getAllQuotes()
chiediamo al server di restituire tutte le citazioni del database. Per recuperarne una specifica, usiamo invece getQuote(id)
a cui passiamo l’identificativo della citazione. Infine postQuote()
consente di inviare una nuova citazione in formato JSON al server. Tutte le funzioni restituiscono una Promise.
Per quanto riguarda i componenti, ne abbiamo definiti ben 7 nella cartella components
a cui si aggiunge App
nella directory base.
Alcuni di questi sono abbastanza semplici e non presentano particolari dettagli di interesse.
Per esempio nel file TheFooter.vue
abbiamo un componente che usiamo per mostrare delle banali informazioni nel footer dell’applicazione.
<template>
<footer>
<p>Quotes DB Example. Made with Vue & Vuex.</p>
</footer>
</template>
<style scoped>
footer {
text-align: center;
margin-top: 4rem;
}
p {
font-size: var(--font-size-xs);
color: var(--secondary-color-600);
padding: 1rem;
}
</style>
I componenti QuoteList
e QuoteListItem
si limitano ad elencare la lista delle citazioni.
// file: components/QuoteList.vue
<template>
<div class="quote-list-container">
<h2 id="quotes">Citazioni</h2>
<div class="quote-filter-wrapper">
<QuoteFilter />
</div>
<transition-group tag="ul" name="list">
<QuoteListItem v-for="quote in quotes" :key="quote.id" :quote="quote" />
</transition-group>
</div>
</template>
<script>
import QuoteListItem from '@/components/QuoteListItem.vue';
import QuoteFilter from '@/components/QuoteFilter.vue';
export default {
name: 'QuoteList',
components: {
QuoteFilter,
QuoteListItem
},
props: {
quotes: {
type: Array,
required: true
}
}
};
</script>
QuoteList
importa i due componenti QuoteListItem
e QuoteFilter
ed aspetta di ricevere le citazioni attraverso la Prop quotes
.
Nel suo template troviamo il componente QuoteFilter
che permette di filtrare le citazioni da visualizzare in base alla lingua scelta. Per ciascuna citazione creiamo un’istanza di QuoteListItem
usando la direttiva v-for
. Per migliorare l’esperienza utente, racchiudiamo i diversi componenti di tipo QuoteListItem
fra i tag <transition-group>
in modo da poter applicare una transizione che definiamo nella sezione CSS del file.
// file: QuoteListItem.vue
<template>
<li>
<blockquote>
<p>{{ quote.quoteText }}</p>
<footer>{{ quote.quoteAuthor }}</footer>
</blockquote>
</li>
</template>
<script>
export default {
name: 'QuoteListItem',
props: {
quote: {
type: Object,
required: true
}
}
};
</script>
QuoteListItem
si limita poi a mostrare le informazioni presenti all’interno dell’oggetto che riceve attraverso la Prop quote
.
Molto più interessante è invece App
che è il componente base.
<template>
<div id="app">
<TheHeader />
<div class="body-container">
<div class="form-container" ref="formContainer">
<QuoteForm />
</div>
<div class="content-wrapper" ref="contentWrapper">
<transition name="fade" mode="out-in">
<LoadingSpinner v-if="isLoading" color="hsl(158, 58%, 62%)" />
<div v-else-if="downloadError" class="download-error">
<ErrorIcon />
<p>
Oops... Si è verificato un errore <br />
Non è stato possibile scaricare <br />
le citazioni dal server :-(
</p>
</div>
<div v-else class="quote-list-wrapper">
<QuoteList :quotes="quotes" />
</div>
</transition>
</div>
</div>
<TheFooter />
</div>
</template>
<script>
import TheHeader from '@/components/TheHeader.vue';
import TheFooter from '@/components/TheFooter.vue';
import LoadingSpinner from '@/components/LoadingSpinner.vue';
import QuoteForm from '@/components/QuoteForm.vue';
import QuoteList from '@/components/QuoteList.vue';
import ErrorIcon from '@/assets/error.svg?inline';
export default {
name: 'App',
components: {
ErrorIcon,
TheHeader,
TheFooter,
LoadingSpinner,
QuoteForm,
QuoteList
},
data() {
return {
formContainer: null,
contentWrapper: null
};
},
computed: {
downloadError() {
return this.$store.state.errorModule.errors.download;
},
isLoading() {
return this.$store.state.loadingModule.loading;
},
quotes() {
return this.$store.getters['quoteModule/getCurrentViewQuotes'];
}
},
watch: {
'$store.state.editingModeModule.editingMode': function() {
this.formContainer.style.opacity = 1;
this.toggleForm();
}
},
created() {
this.$store.dispatch('quoteModule/getQuotes');
},
mounted() {
this.formContainer = this.$refs.formContainer;
this.contentWrapper = this.$refs.contentWrapper;
this.toggleForm();
},
methods: {
toggleForm() {
const { height: y } = this.formContainer.getBoundingClientRect();
const editMode = this.$store.state.editingModeModule.editingMode;
this.formContainer.style.transform = `scaleY(${+editMode})`;
this.contentWrapper.style.transform = `translateY(-${y}px)`;
}
}
};
</script>
In App.vue
importiamo ben 5 dei componenti da noi definiti. Per usare poi l’icona ErrorIcon
come se fosse un componente, sfruttiamo il plugin vue cli plugin svg che abbiamo precedentemente installato lanciando il comando vue add svg
nella cartella base.
Nel template del componente App
inseriamo <TheHeader>
in alto, <TheFooter>
in basso. Aggiungiamo il componente <QuoteForm>
racchiuso in un elemento <div class="form-container">
del quale salviamo un riferimento accessibile tramite this.$refs
. All’interno di un altro <div>
, al quale applichiamo l’attributo ref="contentWrapper"
, inseriamo un elemento <transition>
che racchiude tre possibili elementi. Se l’applicazione sta scaricando le citazioni, visualilzziamo il componente <LoadingSpinner>
, in caso contrario visualizziamo il componente <QuoteList>
.
Se invece si verifica un errore mostriamo un messaggio il quale comunica all’utente che non è stato possibile scaricare le citazioni dal server.
Al componente QuoteList
passiamo una Prop quotes
il cui valore è una computed property ottenuta dallo store Vuex tramite il getter 'quoteModule/getCurrentViewQuotes'
. Ciò vuol dire che se vengono aggiunte delle citazioni, il componente App riceve in automatico il nuovo array aggiornato che passa poi al componente QuoteList
. Per recuperare tutte le citazioni del database lanciamo un’Action nel lifecycle hook created()
. In mounted()
otteniamo invece i riferimenti ai due <div>
sui quali avevamo applicato l’attributo ref
. Invochiamo poi il metodo toggleForm()
che utilizza il metodo getBoundingClintRect()
per ottenere l’altezza del <div>
che contiene il form. In base al valore di editingModeModule.editingMode
scegliamo se nascondere o mostrare il form e traslare <div class="content-wrapper">
verso l’altro o verso il basso. Per nascondere <div class="form-container">
riduciamo la sua altezza tramite this.formContainer.style.transform = `scaleY(${+editMode})`;
(editMode è un valore booleano che trasformiamo in valore numerico 0 o 1 anteponendo il segno ‘+’, +true === 1, +false === 0). Nella sezione CSS, che abbiamo omesso, abbiamo spostato l’origine di eventuali transizioni nella parte superiore dell’elemento tramite la regola (.form-container {transform-origin: top center;}
).
Registriamo anche un watcher per osservare eventuali modifiche della proprietà $store.state.editingModeModule.editingMode
dello store Vuex. Ogni volta che questa cambia, invochiamo toggleForm()
per mostrare (editingModeModule.editingMode === true
) o nascondere il form a seconda della modalità in cui si trova l’applicazione.
Per modificare il valore di $store.state.editingModeModule.editingMode
, lanciamo un’Action dal componente TheHeader
.
// file: src/components/TheHeader.vue
<template>
<header>
<h1>
<a href="/" title="back to Homepage">
<QuotesLogo />
</a>
</h1>
<transition name="fade">
<button
v-if="isLoading"
@click="onClick"
class="btn btn-secondary"
:class="editingClass"
>
{{ buttonLabel }}
</button>
</transition>
</header>
</template>
<script>
import QuotesLogo from '@/assets/logo.svg?inline';
export default {
name: 'TheHeader',
components: {
QuotesLogo
},
computed: {
editingClass() {
return { editing: this.$store.state.editingModeModule.editingMode };
},
isLoading() {
return !this.$store.state.loadingModule.loading;
},
buttonLabel() {
return this.$store.state.editingModeModule.editingMode
? 'nascondi form'
: 'crea nuova citazione';
}
},
methods: {
onClick() {
this.$store.dispatch(
'editingModeModule/setEditingMode',
!this.$store.state.editingModeModule.editingMode
);
}
}
};
</script>
Nel componente TheHeader inseriamo un pulsante che servirà per mostrare o nascondere il form. Per far ciò, viene lanciata un’Action che mira ad invertire il valore corrente di $store.state.editingModeModule.editingMode
.
Quando $store.state.editingModeModule.editingMode
è pari a true
, aggiungiamo anche un’ulteriore classe CSS .editing usando la direttiva v-bind
applicata all’attributo class
Nel file QuoteFilter.vue
inseriamo tre pulsanti di tipo radio. Selezionando uno dei tre, viene invocata la funzione onChange()
che lancia un’Action currentViewModule/setCurrentView
passando come valore l’indice associato al pulsante. In questo modo viene cambiato l’indice della view corrente e viene quindi automaticamente ricalcolata la computed property quotes del componente App
il cui valore è pari a quello restituito dal Getter quoteModule/getCurrentViewQuotes
.
// file: src/components/QuoteFilter.vue
<template>
<div class="filter-container">
<div class="filter-wrapper">
<span class="highlight" :style="translate"></span>
<div class="label-wrapper">
<template v-for="(filter, index) in filters">
<input
type="radio"
:id="filter"
:value="index"
@change="onChange"
:checked="picked === index"
:key="'radio' + index"
/>
<label :for="filter" :key="'label' + index">{{ filter }}</label>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
picked: 1,
filters: ['inglese', 'tutte', 'italiano']
};
},
computed: {
translate() {
let distance = this.picked * 100;
return `transform: translateX(${distance}px)`;
}
},
methods: {
onChange(event) {
this.picked = +event.target.value;
this.$store.dispatch('currentViewModule/setCurrentView', this.picked);
}
}
};
</script>
Selezionando uno dei pulsanti radio viene anche ricalcolata la computed propery translate
che usiamo per traslare orizzontalmente l’elemento <span>
con classe highlight
. Così facendo, evidenziamo l’elemento che è attualmente attivo.
Dal momento che vogliamo lanciare un’Action al verificarsi dell’evento change
dei vari pulsanti di tipo radio, abbiamo utilizzato le direttive v-on
e v-bind
per registrare una funzione in risposta dell’evento change e per settare il valore dell’attributo checked
, applicato al pulsante selezionato, pari a true
.
Per quanto riguarda il componente QuoteForm
, nel suo template inseriamo un form che contiene un campo per il nome dell’autore il quale è associato alla proprietà quoteAuthor
tramite la direttiva v-model
. È anche presente una textarea per il testo dela citazione (associata a quoteText
sempre tramite v-model
) e due pulsanti di tipo radio per selezionare la lingua anch’essi collegati alla proprietà quoteLanguage
.
<template>
<form @submit.prevent="formHandler">
<div class="input-wrapper">
<label class="text-field" for="quoteText">testo</label>
<textarea
placeholder="Inserisci il testo della citazione"
name="quoteText"
id="quoteText"
v-model="quoteText"
></textarea>
</div>
<div class="input-wrapper">
<label class="text-field" for="quoteAuthor">autore</label>
<input
placeholder="Inserisci il nome dell'autore"
type="text"
name="quoteAuthor"
id="quoteAuthor"
v-model="quoteAuthor"
/>
</div>
<div class="input-wrapper">
<input type="radio" id="en" value="en" v-model="quoteLanguage" />
<label class="radio-label" for="en">EN</label>
<input type="radio" id="it" value="it" v-model="quoteLanguage" />
<label class="radio-label" for="it">IT</label>
</div>
<transition name="fade" mode="out-in">
<div key="uploading" v-if="isUploading" class="uploading-wrapper">
<LoadingSpinner color="hsl(210, 31%, 80%)" />
<p>Salvataggio in corso...</p>
</div>
<div key="not-uploading" v-else class="button-wrapper">
<transition name="fade" mode="out-in">
<div v-if="hasTriedToUpload" class="form-feedback">
<div v-if="uploadError" class="feedback-error">
<ErrorIcon />
<span>Impossibile salvare le informazioni.</span>
</div>
<div v-else class="feedback-ok">
<SuccessIcon />
<span>Salvataggio completato</span>
</div>
</div>
<button
v-else
:disabled="!isValidForm"
type="submit"
class="btn btn-primary"
>
Aggiungi al database
</button>
</transition>
</div>
</transition>
</form>
</template>
<script>
import uniqid from 'uniqid';
import LoadingSpinner from '@/components/LoadingSpinner.vue';
import SuccessIcon from '@/assets/ok.svg?inline';
import ErrorIcon from '@/assets/error.svg?inline';
export default {
components: {
ErrorIcon,
LoadingSpinner,
SuccessIcon
},
data() {
return {
quoteText: '',
quoteAuthor: '',
quoteLanguage: 'en'
};
},
computed: {
isValidForm() {
return this.quoteText && this.quoteAuthor;
},
isUploading() {
return this.$store.state.uploadingModule.uploading;
},
hasTriedToUpload() {
return this.$store.state.hasTriedToUploadModule.hasTriedToUpload;
},
uploadError() {
return this.$store.state.errorModule.errors.upload;
}
},
methods: {
formHandler() {
if (this.quoteText && this.quoteAuthor) {
const quote = {
id: uniqid(),
quoteText: this.quoteText,
quoteAuthor: this.quoteAuthor,
lang: this.quoteLanguage
};
this.$store.dispatch('quoteModule/addQuote', quote);
this.quoteText = '';
this.quoteAuthor = '';
}
}
}
};
</script>
Se è stato cliccato il pulsante per inviare le informazioni al server, si verifica l’evento submit
del form in risposta del quale eseguiamo il metodo formHandler
. All’elemento form
abbiamo infatti applicato la direttiva v-on:submit
con modificatore prevent
per evitare il comportamento predefinito che causerebbe un nuovo caricamento dell’intera pagina. Nel metodo formHandler
creiamo un nuovo oggetto con le informazioni relative alla citazione che vogliamo aggiungere al database e lanciamo l’Action quoteModule/addQuote
che setta isUploading
al valore true
. Per assegnare un identificativo univoco all’oggetto della citazione, abbiamo usato il package uniqid. Mentre è in corso la procedura di salvataggio, mostriamo il componente LoadingSpinner
. Non appena la computed property isUploading
è pari a false
, ovvero quando l’Action quoteModule/addQuote
sta per terminare ed esegue la Mutation che setta $store.state.uploadingModule.uploading
al valore false
, viene mostrato un messaggio il quale comunica se l’operazione di salvataggio è avvenuta correttamente o meno (in caso di errore l’Action quoteModule/addQuote
esegue una mutazione per assegnare a state.errorModule.errors.upload
un messaggio di errore). Ricordiamo che nell’Action quoteModule/addQuote
invocavamo alla fine la funzione setTimeout()
in modo da cambiare state.hasTriedToUploadModule.hasTriedToUpload
che diventa pari a false
. Così facendo, la computed property hasTriedToUpload
diventa pari a false
e vengono quindi nascosti i messaggi di feedback per far posto nuovamente al pulsante del form.
La versione finale dell’applicazione è disponibile su Bitbucket.
Riepilogo
In questa lezione abbiamo realizzato un esempio un po’ più articolato usando Vuex.
Finora abbiamo visto soltanto applicazioni con una sola view. Nella prossima lezione illustreremo invece il funzionamento di Vue Router che rappresenta un altro argomento fondamentale per lo sviluppo di applicazioni SPA (single-page application) nel mondo reale.