Come creare una spa con October e Vue - Parte 3

Luca Benati

Creiamo una single page application in pochissimo tempo con October e Vue parte 3


Pubblicato da Luca Benati il 29 ottobre 2019

vue, spa

Eccoci dunque arrivati alla terza parte di questa serie di articoli sull'uso sinergico di October CMS e Vue Js.

In questo articolo andremo ad arricchire con qualche funzionalità dinamica il frontend della nostra Task List che avevamo cominciato nella seconda parte di questa serie di articoli, in modo da vedere più da vicino come far lavorare insieme i due framework, October e Vue, in modo molto semplice.

Andiamo innanzitutto ad aggiungere, oltre al nome, anche un campo di stato denominato 'satus' ai nostri task in modo che sia possibile settarli come completati ed anche la possibilità di aggiungerli ed eliminarli.

Incominicamo quindi con aggiungere il campo status nella tabella dei task, cosa che tramite l'intefaccia del builder è un attimo. Se non ricordi come fare ripassa la prima parte.

Ora apriamo il nostro componenete Task.vue e sostituiamo il template attuale con il seguente, con il quale andiamo ad aggiungere ad ogni elemento della nostra lista due icone, una sulla sinistra per poter settare il task come completato o meno e una sulla destra per poter cancellare il task.


<template>

        <li class="list-group-item">
            <i
                @mouseover="isHovered=true"
                @mouseleave="isHovered=false"
                v-on:click="toggleStatus(task)"
                class="fa fa-fw"
                :class="{
                    'fa-check green': task.status == 1 && !isHovered,
                    'fa-times red': task.status == 1 && isHovered,
                    'fa-square-o blue': task.status == 0 && !isHovered,
                    'fa-check-square-o green': task.status == 0 && isHovered
                }">
            </i> {{ task.name }}
            <i
                @click="deleteTask(task)"
                class="fa fa-fw fa-trash-o light-grey float-right">
            </i>
        </li>

</template>

Come possiamo vedere abbiamo introdotto molti nuovi concetti di Vue; le due direttive @mouseover e @mouseleave indicano l'azione da compiere al verificarsi dell'evento degli eventi mouseover e mouseleave.

Per chi viene da Jquery sono il corrispettivo di:


Jquery(this).on('mouseover', function(){});

e


Jquery(this).on('mouseleave', function(){});

Il simbolo chiocciola è la scorciatoia della direttiva v-on: ad esempio scrivere v-on:click='miaFunzione' e @click='miaFunzione' è esattamente la stessa cosa

Quindi significa che la proprietà isHovered verrà impostata a true quando il puntatore del mouse sarà sopra all'icona e a false quando ne uscirà.

Utilizzeremo questa proprietà assieme allo stato definito nel singolo oggetto Task per determinare l'icona da visualizzare utilizzando la direttiva :class di Vue.

La direttiva in questione è un'oggetto dove il nome della proprietà sarà la classe applicata nel caso in cui il valore dell'espressione che la segue risulterà vera, come vediamo è possibile concatenare più classi condizionate separandole con la virgola.

il nome della class richiede gli apici solo perchè in questo caso sono presenti i caratteri '-' diversamente non sono necessari.

In questo modo possiamo agilmente gestire tutti e quattro gli stati possibili delle icone dei nostri task con un'unica definizione. Vue è adorabile.

Per poter visualizzare le icone utilizziamo le icone di FontAwesome aggiungendo nella head del file index.html il tag per importarle da cdn: <link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">

Oltre ai due eventi precedenti sull'icona vediamo anche la direttiva v-on:click="toggleStatus(task)" per richiamare al click sull'icona il metodo toggleStatus passandogli come argomento l'oggetto task corrente, questo metodo andrà a invertire lo stato del task.

Allo stesso modo abbiamo inserito sull'icona del cestino di destra presente in ogni elemento della lista la chiamata al metodo deleteTask, da eseguirsi al click, passandogli come argomento l'oggetto task corrente.

Andiamo a vedere ora come modificare la parte script sempre del file del componente Task con il seguente codice


<script>

export default {
    name: 'task',
    props: ['task'],
    data: function(){
        return {
            isHovered: false
        }
    },
    methods: {
        toggleStatus: function(task){
            this.$emit('toggle-status', task)
        },
        deleteTask: function(task){
            if(confirm("Sei sicuro di vole cancellare questo task?")){
                this.$emit('delete-task', task)
            }
        }
    }
}

</script>

A differenza dalla volta scorsa abbiamo aggiunto la proprietà data che ci rende disponibile nel template la variabile 'isHovered', che di default è impostata a false, e due metodi; toggleStatus e deleteTask.

Queste sono le due fuzioni che verranno chiamate al click sull'icona del task e del cestino che abbiamo visto poco fà nel template.

La funzione toogleStatus utilizza la funzione this.$emit per emettere un evento di tipo 'toggle-status' passandogli come argomento il task.

Allo stesso modo la funzione deleteTask sfrutta la funzione this.$emit per propagare un evento di tipo 'delete-task'.

Entrami questi eventi, che abbiamo determinato in modo del tutto aleatorio, potranno essere utilizzati nel nostro componenete genitore TaskList.vue per triggare le funzioni vere e proprie che si occuperanno di fare le chiamate alle API del nostro backend di October, rispettivamente per cambiare lo stato del task o per eliminarlo.

Aggiungiamo sempre nel file Task.vue qualche regola di stile per colorare le icone


<style>

i.fa {
    cursor: pointer;
}
.green{
    color: #28a745;
}
.blue {
    color: #b3d7ff;
}
.red {
    color: #dc3545;
}
.light-grey {
    color: #adb5bd;
}
.fa-trash-o:hover {
    color: #af0616;
}

</style>

Ora apriamo il componenete TaskList.vue e sostituiamo il template attuale con il seguente con il quale andremo a mostrare non più una lista di task ma due liste distinte una con i task ancora da portare a termine e l'altra con quelli completati.

Apriamo quindi il file del componente TaskList.vue e aggiungiamo


<template>
    <div class="col">
        <div class="row">
            <div class="offset-md-2 col-md-4">
                <h3 class="text-center">Cose da fare ({{uncompletedTasks.length}})</h3>
                <ul class="list-group">
                    <task v-for="task in uncompletedTasks" v-bind:task="task" v-bind:key="task.id" @delete-task="deleteTask" @toggle-status="toggleStatus"></task>
                </ul>
            </div>

            <div class="col-md-4">
                <h3 class="text-center">Cose fatte ({{completedTasks.length}})</h3>
                <ul class="list-group">
                    <task v-for="task in completedTasks" v-bind:task="task" v-bind:key="task.id" @delete-task="deleteTask" @toggle-status="toggleStatus"></task>
                </ul>
            </div>
        </div>
    </div>
</template>

Possiamo subito notare i due eventi @toggle-status="toggleStatus" e @delete-task="deleteTask" che in modo del tutto analogo a @click quando triggati dal componente Task tramite this.$emit eseguono il metodo toggleStatus e deleteStatus passando come argomento il task.

I due metodi sfruttano axios per fare una chiamta alle API di October, che dopo andremo a definire, per portare a termine l'operazione richiesta.



import axios from 'axios';

import task from './Task'

export default {
    name: 'TaskList',
    props: ['tasks'],
    components: {
        task
    },
    methods: {

        toggleStatus: function(task){

            task.status = ((task.status == 1 ) ? 0 : 1)

            axios.post('http://dominio.com/task/toggle-status', task).then(function (response){
                console.log(response);
            }).catch(function(error) {
                console.log(error);
            })
        },

        deleteTask: function(task){

            var taskIndex = this.tasks.indexOf(task)

            this.tasks.splice(taskIndex, 1)

            axios.post('http://dominio.com/task/delete', task).then(function (response){
                console.log(response);
            }).catch(function(error) {
                console.log(error);
            })
        }
    },
    computed: {
            completedTasks: function () {
                return this.tasks.filter(function(task){
                    return task.status == 1;
                })
            },
            uncompletedTasks: function () {
                return this.tasks.filter(function(task){
                    return task.status == 0;
                })
            }
        },
}

Nel metodo toggleSatus invertiamo il valore di status del task in modo che anche la nostra lista a video venga aggiornata in tempo reale, poi passiamo l'oggetto task alla chiamata post che axios effettuerà alla API dedicata.

Nel metodo deleteTask prima recuperiamo l'indice della posizione del task in questione all'interno dell'array tasks in modo da elimimarlo dalla lista così da aggiornare la vista di frontend, poi in modo analogo al precedente passiamo l'oggetto task alla chiamata post che axios effettuerà alla API dedicata.

Oltre ai metodi appena illustrati abbiamo dichiarato due variabili sotto la sezione 'computed' qui è possibile creare variabili con valori calcolati sulla base dei valori dell'oggetto istanziato.

Praticamente andiamo a definire due variabili che come valore hanno il risultato la lista dei task filtrati per stato, così facendo abbiamo uan lista dei task ancora da svolgere e una con i task completati, in questo modo le liste rimarranno aggiornate dinamicamente quando andremo a cambiare stato o eliminare i task.

Il metodo filter applicato all'array task è una funzione nativa di javascript che ritorna un nuovo array contenente tutti gli elementi che hanno passato positivamente il test implementato nella funzione.

Passiamo ora al file routes.php del nostro plugin di October e creiamo le API necessarie


Route::post('task/toggle-status', function(Request $request){
    $data = $request->input();
    try{
        $task = Task::where('id', $data['id'])->update(['status' => $data['status']]);
    }
    catch(Exception $e){
        return $e->getMessage();
    }
});

Route::post('task/delete', function(Request $request){
    $data = $request->input();
    try{
        Task::destroy($data['id']);
    }
    catch(Exception $e){
        return $e->getMessage();
    }
});

Se incontri dei problemi di risposta dal server prova ad utilizzare la seguente sintassi in modo da intercettare anche le richieste di tipo OPTIONS utilizzate per le preflight request utilizzate dal browser per verificare la validità della cross origin request. Per approfindire leggi qui


Route::match(['options', 'post'],'task/toggle-status', function(Request $request){
    $data = $request->input();
    try{
        $task = Task::where('id', $data['id'])->update(['status' => $data['status']]);
    }
    catch(Exception $e){
        return $e->getMessage();
    }
});

Route::match(['options', 'post'],'task/delete', function(Request $request){
    $data = $request->input();
    try{
        Task::destroy($data['id']);
    }
    catch(Exception $e){
        return $e->getMessage();
    }
});

Le due routes tramite i metodi standard aggiornano o eliminano il task passato identificandolo per id

Per l'hinting della Request è necessario importarla in testa al file aggiungendo: use Illuminate\Http\Request;

Ora non ci resta che da implementare la possibilità di aggiungere un nuovo task, per raggiungere questo obbiettivo andremo a creare un nuovo componente che si occuperà di questo e lo importeremo nel componente principale App.vue.

Apriamo quindi il file App.vue e modifichiamone il template in questo modo


<template>
    <div id="app" class="container">

        <div class="row">
            <TaskList v-bind:tasks="tasks"></TaskList>
        </div>
        <div class="row">
            <CreateTask v-on:create-task="addTask"></CreateTask>
        </div>
    </div>
</template>

e nella sezione script andiamo ad aggiungere l'importazione del componente


<script>

import axios from 'axios';

import TaskList from './components/TaskList'

import CreateTask from './components/CreateTask'

export default {
    name: 'App',
    components: {
        TaskList,
        CreateTask
    },
    data: function(){
        return {
            tasks: []
            }
    },
    .....

Vediamo che utilizzando la stessa tecnica precedente andremo ad esegure una funzione (addTask) allo scaturire dell'evento 'create-task' che andremo a triggare nel componente CrateTask all'atto di inviare il nuovo task.

La sezione script di App.vue completa sarà dunque


<script>

import axios from 'axios';

import TaskList from './components/TaskList'

import CreateTask from './components/CreateTask'

export default {
    name: 'App',
    components: {
        TaskList,
        CreateTask
    },
    data: function(){
        return {
            tasks: []
            }
    },
    mounted: function(){
        var _self = this
        axios.get('http://127.0.0.1/democita/tasks/').then(function(response){
            _self.tasks = response.data
        })
    },
    methods: {
        addTask: function(task) {
            if(task.name){

                this.tasks.push(task);

                axios.post('http://127.0.0.1/democita/task/add', task).then(function (response){
                    console.log(response);
                }).catch(function(error) {
                    console.log(error);
                })
            }
        }

    }
}
</script>

Il metodo addTask, una volta verificato che il nome del nuovo task sia impostato, aggiunge il nuovo task alla lista tasks in modo da aggiornare l'interfaccia e poi tramite axios invierà via post i dati del nuovo task alla nostra API che sarà


Route::match(['options', 'post'],'/task/add', function(Request $request){

    $data = $request->input();

    try{
        if($data['name']){
            Task::create([
                'name' => $data['name']
                'status' => $data['status']
            ]);
        }
    }
    catch(Exception $e){
        return $e->getMessage();
    }

});

Anche qui, verificata l'esistenza del nome del task, creiamo un nuovo task.

Per poter inserire i campi name e status utilizzando il metodo create del model Task dobbiamo aggiungere una proprietà protetta nel file del model Task del Plugin


protected $fillable = ['name', 'status'];

Vedi https://octobercms.com/docs/database/model#mass-assignment

Creiamo il nuovo componente CreateTask.vue nella cartella components del progetto Vue e nella sezione template inseriamo il seguente codice


<template>
    <div class="offset-md-2 col-md-4 mt-2 ">
        <button
            class="btn btn-info btn-sm float-right"
            v-on:click="openForm"
            v-show="!isCreating">Aggiungi Task
        </button>
        <div class="card"  v-show="isCreating">
            <div class="card-body">
                <div class="form-group" >
                    <label for="" class="control-label">Nome Task</label>
                    <input
                        type="text"
                        class="form-control"
                        placeholder="Inserisci nome del task"
                        v-model="nameTask"
                        ref="title"
                />
                </div>
                <div class="form-group" >
                    <button
                        class="btn btn-success btn-sm"
                        v-on:click="sendForm">Aggiungi
                    </button>
                    <button
                        class="btn btn-danger btn-sm"
                        v-on:click="closeForm">Annulla
                    </button>
                </div>
            </div>
        </div>
    </div>
</template> 

In modo analogo ai casi visti prima sfruttiamo gli eventi click sui bottoni per gestire la visualizzazione del form di inserimento e per l'invio del form triggando l'evento 'create-task' visto prima in App.vue.

L'unica nuova direttiva che troviamo qui è v-show che determina se l'elemento deve essere visualizzato se impostato a true o nascosto se false, in questo caso il valore è determinato dal valore di isCreating in modo molto simile come fatto prima per determinare lo stato di hover delle icone del singolo task.

Nella sezione script inseriremo perciò questo codice


<script>

export default{
    name: 'CreateTask',
    data: function(){
        return {
            isCreating: false,
            nameTask: ''
        }
    },

    methods: {
        openForm: function() {
            this.isCreating = true;
        },
        closeForm: function() {
            this.isCreating = false;
        },

        sendForm: function() {

            var name = this.nameTask;

            this.$emit('create-task', {
                name,
                status: 0
            });

            this.isCreating = false;
            this.nameTask ='';
        }
    }
}

</script>

Ottimo, abbiamo terminato, se non abbiamo commesso errori dovremo trovarci un risultato come il seguente

Non c'è che dire, lavorare con October e Vue è veramente piacevole e produttivo.

Questa accoppiata può risultare un'ottima scelta sia in ambienti piccoli che medio/grandi data la curva d'apprendimento relativamente semplice e l'ecosistema offerto da Vue Js.

Ricordiamo che Vue è adottabile in modo incrementale il chè significa che può scalare dalla semplice libreria ad un Framework completo di tutte le funzionalità a seconda del tipo di progetto di cui abbiamo bisogno, come di fatto si dichiara lui stesso nella prima riga della guida ufficiale

Vue is a progressive framework for building user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable.

Happy coding!


Lunga vita e prosperità

Ti interessa un argomento non trattato?