Tutorial: Backbone web app responsive con Bootstrap 3
In un precedente articolo ho sottolineato il ruolo fondamentale che può rappresentare Backbone per la realizzazione di una MVC javascript web application, elencando anche i vari componenti che la libreria mette a disposizione, oltre ad alcune applicazioni con essa realizzate.
Su lobaedesign.com invece, ho introdotto l’indipendenza della view dagli altri componenti con un tutorial su come realizzare un orologio analogico CSS3 e JavaScript con animazione, e su come la stessa view aggiorna gli elementi dell’interfaccia “osservando” il cambiamento di un model.
In questo post voglio introdurre invece altri due componenti per la creazione di una backbone web app:
- la collection, ovvero la lista secondo la quale organizzare i model
- il sistema di templating della libreria underscore.js, per la generazione del markup HTML
Lo farò attraverso un semplice tutorial che implementa il framework bootstrap 3 nel documento e usa due view diverse legate allo stesso model ed una alla lista.
Il nostro intento sarà quindi quello di creare una backbone web app responsive che mostra una lista di persone, ciascuna con informazioni proprie come nome, sesso, occupazione, e descrizione, e con possibilità da parte dell’utente di creare, modificare, eliminare una persona, oltre che poter visualizzare una modal con delle informazioni aggiuntive cliccando nel nome della lista.
Impostazione del documento
Come prima operazione, aggiungiamo al nostro documento, prima della chiusura del tag body, le librerie di cui abbiamo bisogno:
1 2 3 4 5 6 7 |
<script src="js/jquery-1.10.2.min.js"></script> <!-- jquery --> <script src="js/bootstrap.min.js"></script> <!-- bootstrap per il modal--> <script src="js/underscore-min.js"></script> <!-- libreria di supporto per backbone e per il sistema di templating --> <script src="js/backbone-min.js"></script><!-- backbone.js --> <script src="js/demo.js"></script> <!-- il file js della nostra applicazione --> |
Backbone.js e Undersore.js sono scaricabili dai rispettivi link:
Il markup HTML
Andiamo a vedere il markup HTML della view principale dell’applicazione, ovvero quello che comprende il form di inserimento e la lista:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<!-- form di creazione dell'individuo --> <div class="col-md-4 col-lg-4 well well-sm"> <legend><i class="glyphicon glyphicon-user"></i> Aggiungi individuo</legend> <form action="#" id="add-form" method="post" class="form" role="form"> <div class="row"> <div class="col-xs-8 col-md-8"> <input class="form-control" name="name" placeholder="Nome" type="text" /> </div> <div class="col-xs-4 col-md-4"> <select name="gender" class="form-control"> <option value="M">Maschio</option> <option value="F">Femmina</option> </select> </div> </div> <input class="form-control" name="occupation" placeholder="Occupazione" type="text" /> <label for="notes">Note</label> <textarea name="notes" id="notes" class="form-control" rows="3"></textarea> <button class="btn btn-block button add-person" type="submit">Aggiungi</button> </form> </div> <!-- la lista degli individui --> <div class="col-md-8 col-lg-8"> <ul class="list-group"> </ul> </div> |
I template di Underscore.js
Impostiamo ora il markup HTML dei template delle view secondo il sistema di templating di underscore.js; i metodi per comunicare alla view backbone quale template utilizzare per il rendering del markup da inserire poi nel documento sono principalmente tre:
- Specificare il template sottoforma di variabile string nello script
- Specificare il rifermento al template il cui markup è stato “posizionato” nel documento
- Utilizzare librerie come require.js per “leggere” in modo asincrono il contenuto del template da un file esterno (solitamente di usa un file con estensione .html)
Personalmente preferisco utilizzare l’ultima opzione, soprattutto nel caso di web app ad architettura molto complessa, per due motivi principali:
- lasciare il nostro documento più pulito possibile
- migliore organizzazione del progetto per quanto riguarda la struttura dei file
In questo tutorial tuttavia prenderemo in considerazione il secondo metodo.
Per saperne di più sui sistemi di javascript templating leggi:
Per l’esperimento definiremo 3 view, quella dell’applicazione principale (che comprende il form di inserimento e l’ul della lista), quella di ogni singolo elemento del lista, e quella della modal delle informazioni aggiuntive.
Poiché per la prima delle tre imposteremo come elemento di riferimento il body stesso del documento, andiamo a definire il markup dei template delle altre due view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
<!-- *********** Template delle Views ************** --> <!-- modal dell'elemento della lista --> <script type="text/template" id="person-template"> <div class="view"> <div class="action pull-right"> <button type="button" class="btn btn-xs button open-modal" title="View Modal"> <span class="glyphicon glyphicon-eye-open"></span> </button> <button type="button" class="btn btn-xs button edit-view" title="Edit"> <span class="glyphicon glyphicon-pencil"></span> </button> <button type="button" class="btn btn-xs button person-delete" title="Delete"> <span class="glyphicon glyphicon-trash"></span> </button> </div> <h4><%=name%></h4> <div class="gender-info"> Sesso: <strong><%=gender%></strong> - Occupazione: <strong><%=occupation%></strong> </div> </div> <div class="edit"> <form action="#" class="form" role="form"> <div class="row"> <div class="col-xs-3 col-md-3"> <input class="form-control" name="name" value="<%=name%>" placeholder="Nome" type="text" /> </div> <div class="col-xs-3 col-md-3"> <input class="form-control" value="<%=occupation%>" name="occupation" placeholder="Occupazione" type="text" /> </div> <div class="col-xs-2 col-md-2"> <select name="gender" class="form-control"> <option value="M" <%if(gender=='M'){%>selected<%}%>>Maschio</option> <option value="F" <%if(gender=='F'){%>selected<%}%>>Femmina</option> </select> </div> <div class="col-xs-2 col-md-2"> <button class="btn btn-sm btn-block button save" type="submit">Salva</button> </div> <div class="col-xs-2 col-md-2"> <a href="#" class="btn btn-sm btn-default btn-block cancel">Annulla</a> </div> </div> <textarea name="notes" id="notes" class="form-control" rows="3"><%=notes%></textarea> </form> </div> </script> <!-- modal delle informazioni aggiuntive--> <script type="text/template" id="modal-template"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title"><%=name%></h4> </div> <div class="modal-body"> <div class="gender-info"> Sesso: <strong><%=gender%></strong> - Occupazione: <strong><%=occupation%></strong> </div> <p class="gender-notes"> <%=notes%> </p> </div> <div class="modal-footer"> <button type="button" class="btn button" data-dismiss="modal">Chiudi</button> </div> </div> </script> |
Dal codice puoi notare che ogni template è stato definito all’interno dell tag script in cui è stato impostato un attributo type con valore text/template. Così facendo il browser ignorerà questa porzione di codice che invece sarà raggiungibile da Javascript tramite l’attribuito id.
Il file JavaScript
Passiamo ora alla definizione dei vari componenti backbone della nostra applicazione, in primis occupiamoci dell’astrazione dei dati, il model della persona:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//definiamo il modello dell'individuo var PersonModel = Backbone.Model.extend({ // Attributi predefiniti per il modello, che vengono usati //se non passati al momento dell'inizializzazione defaults: { gender: "M", createdat: new Date() }, initialize: function() { }, //elimina l'istanza del model clear: function() { this.destroy(); } }); |
Definiamo ora la collection in cui tutte le istanze del model appena definito verranno organizzate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//definiamo la collection degli individui var PersonList = Backbone.Collection.extend({ // Impostiamo la referenza al model per questa collection. model: PersonModel, url: "#", // Filtra tutti gli individui Maschi o Femmina (in base al parametro passato). getGender: function(gender) { return this.where({gender: gender}); } }); |
Ora che abbiamo definito la nostra (semplicissima) struttura dei dati, vediamo l’impostazione delle view.
La view dell’elemento della lista
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
//impostiamo la view della persona per come elemento nella lista var PersonView = Backbone.View.extend({ // diciamo a backbone che il tag deve essere una li tagName: "li", //settiamo la classe da impostare al tag li per usare gli stili bootstrap e l'animazione in entrata className: "list-group-item animated", // Coumunichiamo alla view a quale template far riferimento. template: '#person-template', // Bindiamo gli eventi agli elementi di questa view (i selettori funzionano come jQuery). events: { "click .person-delete" : "deletePerson", "click .edit-view" : "toggleView", "click .cancel" : "toggleView", "submit form" : "savePerson", "click .open-modal" : "openModal" }, //questa funzione viene eseguita ogni volta che viene creata //una nuova istanza per questa view initialize: function() { //compiliamo il template in una variabile this.template = _.template($(this.template).html()); //osserviamo l'evento change del model dell'individuo per fare in modo che //quando ciò accade anche il markup della relativa view venga aggiornato this.listenTo(this.model, 'change', this.toggleView); //osserviamo l'evento destroy del model dell'individuo per fare in modo che //quando ciò accade anche la relativa view venga rimossa this.listenTo(this.model, 'destroy', this.removeFromDOM); }, // Effettua il render dei contenuti del markup della view nella lista. render: function() { $(this.el).html(this.template(this.model.toJSON())); return this; }, //visualizza il modal per le informazioni dell'individuo openModal: function(){ var model = this.model; var view = new ModalView({model: model}); $("body").append(view.render().el); ///richiamiamo la funzione open della view appena creata view.open(); }, // Visualizza o nasconde i campi di editing per questa view. toggleView: function() { //riaggiorniamo la vista e cambiamo la classe per la visualizzazione con jQuery this.render(); this.$el.toggleClass("edit"); }, //salva i dati modificati dell'indivuduo savePerson: function(e){ e.preventDefault(); //riprendiamo i dati del nuovo individuo var name = $.trim(this.$("[name='name']").val()), gender = this.$("[name='gender']").val(), occupation = $.trim(this.$("[name='occupation']").val()), notes = $.trim(this.$("[name='notes']").val()); //se non tutti i campi sono stati compilati, interrompiamo l'operazione if(!name.length || ! occupation.length || !notes.length) return; //impostiamo i nuovi dati per il model this.model.set({ name: name, gender: gender, occupation: occupation, notes: notes }); //salviamo il model this.model.save(); }, removeFromDOM: function(){ //aggiungiamo la classe per l'animazione di uscita, //e alla fine dell'animazione (sappiamo che dura 1 secondo) //rimuoviamo l'elemento DOM dal documento var that = this; that.$el.addClass("hinge"); setTimeout(function(){ that.remove(); }, 1010); }, // Rimuove la persona distruggendo il relativo model. deletePerson: function() { this.model.clear(); } }); |
La view del modal delle informazioni aggiuntive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
var ModalView = Backbone.View.extend({ // diciamo a backbone che il tag deve essere una div tagName: "div", //settiamo la classe da impostare al tag li per usare gli stili bootstrap e l'animazione in entrata className: "modal fade", template: "#modal-template", initialize: function() { //compiliamo il template in una variabile this.template = _.template($(this.template).html()); //rimuoviamo il modal dal DOM quando esso viene nascosto this.$el.on("hidden.bs.modal", function(){ this.remove(); }); }, // Effettua il render dei contenuti del markup della view nella lista. render: function() { $(this.el).html(this.template(this.model.toJSON())); return this; }, open: function(){ this.$el.modal("show"); } }); |
La view dell’applicazione
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
//definiamo la view dell'app intera var AppView = Backbone.View.extend({ //a differenza della view dell'individuo, dove impostavamo //quale elemento generare al'inizializzazione, qui comunichiamo //quale oggetto già aesistente utilizzare come markup el: "body", // Deleghiamo gli eventi per la creazione di un nuovo //individuo (il funzionamento ed i selettori sono come quelli di jQuery). events: { "submit #add-form": "create" }, // Nella funzione di inizializzazione osserviamo l'evento add //della collection legata a questa view, per richiamare la funzione che // inserisce un nuovo elemento nella lista DOM initialize: function() { var todos = this.collection; //deleghiamo gli eventi della collection alle relative funzioni //evento che si verifica quando un nuovo model viene aggiunto alla collection // l'ultimo parametro è una funzione alla quale viene passata l'istanza del model appena aggiunto // alla collection this.listenTo(this.collection, 'add', this.addPerson); //richiamiamo la funzione che aggiunge gli elementi all alla lista this.addElements(); }, // funzione che ogni crea una nuova view ed agigunge // l'elemento generato dal render alla lista addPerson: function(personModel) { //creaiamo una nuova istanza della view della persona passando come model quello appena creato var view = new PersonView({model: personModel}); this.$(".list-group").append(view.render().el); //aggiungiamo la classe per l'animazione view.$el.addClass("fadeInLeftBig"); }, // Aggiunge nella lista tutti gli elementi della collection di individui. addElements: function() { this.collection.each(this.addPerson); }, // funzione che aggiunge un nuovo model con i dati del form alla collection create: function(e) { e.preventDefault(); //riprendiamo i dati del nuovo individuo var name = $.trim(this.$("[name='name']").val()), gender = this.$("[name='gender']").val(), occupation = $.trim(this.$("[name='occupation']").val()), notes = $.trim(this.$("[name='notes']").val()); //se non tutti i campi sono stati compilati, interrompiamo l'operazione if(!name.length || ! occupation.length || !notes.length) return; //aggiungiamo il model alla collection this.collection.create({ name: name, gender: gender, occupation: occupation, notes: notes }); //resettiamo il form dopo la creazione this.$("input, textarea").val(''); this.$("[name='gender']").val('M'); } }); |
Avviamo la nostra javascript web application!
Infine, per instanziare i nostri componenti ed avviare l’applicazione, ci serviremo del seguente codice javascript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// nel momento in cui jQuery è pronto, inizializiamo i nostri componenti jQuery(function ($) { //creiamo una nuova istanza per la collection degli individui con due models var persons = new PersonList([ { name: "Mark Zuckerberg", gender: "M", occupation: "Sviluppatore", notes: "Il creatore di Facebook" }, { name: "Giacomo Freddi", gender: "M", occupation: "Sviluppatore", notes: "Web Designer Freelance e Developer, si occupa del design e dello sviluppo di applicazioni "+ "web dal 2008, come molti freelance è abituato a gestire più ruoli e spaziare su più campi,"+ " ma la sua passione principale è quella della creazione di interfacce front-end e back-end"+ " utilizzando codice html5 e css3. Adora usare pattern MVC per i suoi Javascript." } ]); //creiamo una nuova istanza per la vire dell'applicazione //principale associandogli la collection var app = new AppView({ collection : persons }); }); |
Stili e CSS
Come è possible notare scaricando i sorgenti, al fine di rendere il tutto un po più piacevole all’utente, abbiamo inserito degli effetti CSS3 per animare l’inserimento e l’eliminazione di ogni record dalla lista; il codice utilizzato proviene dalla libreria animate.css, e prevede un effetto di entrata con fade da sinistra nel momento dell’inserimento, mentre un elemento di “caduta” nel momento della eliminazione.
Abbiamo inoltre reso il tutto responsive grazie al sistema di griglie di Boostrap 3.
Divertiti con i sorgenti!
Giacomo Freddi
Web Designer Freelance e Developer, si occupa del design e dello sviluppo di applicazioni web dal 2008, come molti freelance è abituato a gestire più ruoli e spaziare su più campi, ma la sua passione principale è quella della creazione di interfacce front-end e back-end utilizzando codice html5 e css3. Adora usare pattern MVC per i suoi Javascript.