Applicazione singola pagina ToDo con Backbone.js

Backbone.js è un framework JavaScript per la creazione di applicazioni web flessibili. Viene fornito con modelli, collezioni, viste, eventi, router e alcune altre fantastiche funzionalità. In questo articolo svilupperemo una semplice applicazione ToDo che supporta l'aggiunta, la modifica e la rimozione di attività. Dovremmo anche essere in grado di contrassegnare un'attività come fatto e archiviarlo. Al fine di mantenere ragionevole la lunghezza di questo post, non includeremo alcuna comunicazione con un database. Tutti i dati saranno conservati sul lato client.

Impostare

Ecco la struttura del file che utilizzeremo:

css └── styles.css js └── collections └── ToDos.js └── models └── ToDo.js └── vendor └── backbone.js └── jquery-1.10.2.min.js └── underscore.js └── views └── App.js └── index.html 

Ci sono poche cose che sono ovvie, come /css/styles.css e /index.html. Contengono gli stili CSS e il markup HTML. Nel contesto di Backbone.js, il modello è un luogo in cui conserviamo i nostri dati. Quindi, i nostri ToDos saranno semplicemente dei modelli. E poiché avremo più di un compito, li organizzeremo in una raccolta. La logica di business è distribuita tra le viste e il file dell'applicazione principale, App.js. Backbone.js ha una sola dura dipendenza: Underscore.js. Anche il framework funziona molto bene con jQuery, quindi entrambi vanno al venditore directory. Tutto ciò di cui abbiamo bisogno ora è solo un piccolo markup HTML e siamo pronti per partire.

   I miei TODO    

Come puoi vedere, stiamo includendo tutti i file JavaScript esterni verso il basso, poiché è buona pratica farlo alla fine del tag body. Stiamo anche preparando il bootstrap dell'applicazione. C'è un contenitore per il contenuto, un menu e un titolo. La navigazione principale è un elemento statico e non lo cambieremo. Sostituiremo il contenuto del titolo e il div sotto di esso.

Pianificazione dell'applicazione

È sempre bene avere un piano prima di iniziare a lavorare su qualcosa. Backbone.js non ha un'architettura super severa, che dobbiamo seguire. Questo è uno dei vantaggi del framework. Quindi, prima di iniziare con l'implementazione della logica aziendale, parliamo delle basi.

namespacing

Una buona pratica è mettere il tuo codice nel proprio ambito. La registrazione di variabili o funzioni globali non è una buona idea. Quello che creeremo è un modello, una collezione, un router e poche viste Backbone.js. Tutti questi elementi dovrebbero vivere in uno spazio privato. App.js conterrà la classe che contiene tutto.

// App.js var app = (function () var api = visualizzazioni: , modelli: , collezioni: , contenuto: null, router: null, todos: null, init: function ()  this.content = $ ("# content");, changeContent: function (el) this.content.empty (). append (el); return this;, title: function (str) $ ("h1 "). text (str); return this;; var ViewsFactory = ; var Router = Backbone.Router.extend (); api.router = new Router (); return api;) (); 

Sopra è una tipica implementazione del modello di modulo rivelatore. Il api variabile è l'oggetto che viene restituito e rappresenta i metodi pubblici della classe. Il visualizzazioni, Modelli e collezioni le proprietà fungeranno da detentori per le classi restituite da Backbone.js. Il soddisfare è un elemento jQuery che punta al contenitore dell'interfaccia utente principale. Ci sono due metodi di supporto qui. Il primo aggiorna quel contenitore. Il secondo imposta il titolo della pagina. Quindi abbiamo definito un modulo chiamato ViewsFactory. Fornirà le nostre opinioni e alla fine, abbiamo creato il router.

Potresti chiedere, perché abbiamo bisogno di una fabbrica per le opinioni? Bene, ci sono alcuni schemi comuni mentre si lavora con Backbone.js. Uno di questi è legato alla creazione e all'utilizzo delle viste.

var ViewClass = Backbone.View.extend (/ * logic here * /); var view = new ViewClass (); 

È bene inizializzare le viste solo una volta e lasciarle vivi. Una volta che i dati sono cambiati, normalmente chiamiamo i metodi della vista e aggiorniamo il suo contenuto EL oggetto. L'altro approccio molto popolare è quello di ricreare l'intera vista o sostituire l'intero elemento DOM. Tuttavia, questo non è molto buono dal punto di vista delle prestazioni. Quindi, normalmente ci ritroviamo con una classe di utilità che crea un'istanza della vista e la restituisce quando ne abbiamo bisogno.

Definizione dei componenti

Abbiamo uno spazio dei nomi, quindi ora possiamo iniziare a creare componenti. Ecco come appare il menu principale:

// views / menu.js app.views.menu = Backbone.View.extend (initialize: function () , render: function () ); 

Abbiamo creato una proprietà chiamata menu che contiene la classe della navigazione. Successivamente, potremmo aggiungere un metodo nel modulo factory che crea un'istanza di esso.

var ViewsFactory = menu: function () if (! this.menuView) this.menuView = new api.views.menu (el: $ ("# menu"));  restituire this.menuView; ; 

Sopra è come gestiremo tutte le viste e ci assicureremo di ottenere solo una e della stessa istanza. Questa tecnica funziona bene, nella maggior parte dei casi.

Flusso

Il punto di ingresso dell'app è App.js e la sua dentro metodo. Questo è ciò che chiameremo nel onload gestore del finestra oggetto.

window.onload = function () app.init ();  

Successivamente, il router definito prende il controllo. Basato sull'URL, decide quale gestore eseguire. In Backbone.js, non abbiamo la solita architettura Model-View-Controller. Manca il controller e la maggior parte della logica viene inserita nelle visualizzazioni. Quindi, colleghiamo i modelli direttamente ai metodi, all'interno delle viste e otteniamo un aggiornamento istantaneo dell'interfaccia utente, una volta che i dati sono cambiati.

Gestire i dati

La cosa più importante nel nostro piccolo progetto sono i dati. I nostri compiti sono ciò che dovremmo gestire, quindi partiamo da lì. Ecco la nostra definizione del modello.

// models / ToDo.js app.models.ToDo = Backbone.Model.extend (defaults: title: "ToDo", archiviato: false, done: false); 

Solo tre campi. Il primo contiene il testo dell'attività e gli altri due sono flag che definiscono lo stato del record.

Ogni cosa all'interno del framework è in realtà un dispatcher di eventi. E poiché il modello viene modificato con i setter, il framework sa quando i dati vengono aggiornati e può notificare il resto del sistema. Una volta che leghi qualcosa a queste notifiche, la tua applicazione reagirà ai cambiamenti nel modello. Questa è una funzionalità davvero potente in Backbone.js.

Come ho detto all'inizio, avremo molti record e li organizzeremo in una raccolta chiamata ToDos.

// collections / ToDos.js app.collections.ToDos = Backbone.Collection.extend (initialize: function () this.add (title: "Learn JavaScript basics"); this.add (title: "Vai a backbonejs.org "); this.add (title:" Sviluppa un'applicazione Backbone ");, modello: app.models.ToDo up: function (index) if (index> 0) var tmp = this.models [index-1]; this.models [index-1] = this.models [index]; this.models [index] = tmp; this.trigger ("change");, down: function ( indice) if (indice < this.models.length-1)  var tmp = this.models[index+1]; this.models[index+1] = this.models[index]; this.models[index] = tmp; this.trigger("change");  , archive: function(archived, index)  this.models[index].set("archived", archived); , changeStatus: function(done, index)  this.models[index].set("done", done);  ); 

Il inizializzare metodo è il punto di ingresso della raccolta. Nel nostro caso, abbiamo aggiunto alcune attività per impostazione predefinita. Naturalmente nel mondo reale, le informazioni verranno da un database o da qualche altra parte. Ma per mantenerti concentrato, lo faremo manualmente. L'altra cosa che è tipica per le collezioni, è l'impostazione del modello proprietà. Indica alla classe che tipo di dati vengono memorizzati. Il resto dei metodi implementa la logica personalizzata, relativa alle funzionalità nella nostra applicazione. su e giù le funzioni cambiano l'ordine dei ToDos. Per semplificare le cose, identificheremo ogni ToDo con un solo indice nell'array della raccolta. Ciò significa che se vogliamo recuperare un record specifico, dovremmo puntare al suo indice. Quindi, l'ordine sta semplicemente cambiando gli elementi in un array. Come puoi intuire dal codice qui sopra, this.models è la matrice di cui stiamo parlando. archivio e cambiare stato imposta le proprietà dell'elemento specificato. Mettiamo questi metodi qui, perché le viste avranno accesso a ToDos raccolta e non direttamente ai compiti.

Inoltre, non è necessario creare alcun modello dal app.models.ToDo classe, ma abbiamo bisogno di creare un'istanza dal app.collections.ToDos collezione.

// App.js init: function () this.content = $ ("# content"); this.todos = new api.collections.ToDos (); restituiscilo;  

Mostrando la nostra prima vista (navigazione principale)

La prima cosa che dobbiamo mostrare è la navigazione dell'applicazione principale.

// views / menu.js app.views.menu = Backbone.View.extend (template: _.template ($ ("# tpl-menu"). html ()), initialize: function () this.render ();, render: function () this. $ el.html (this.template ());); 

Sono solo nove righe di codice, ma qui stanno accadendo molte cose interessanti. Il primo sta impostando un modello. Se ricordi, abbiamo aggiunto Underscore.js alla nostra app? Useremo il suo motore di template, perché funziona bene ed è abbastanza semplice da usare.

_.template (templateString, [data], [settings]) 

Quello che hai alla fine, è una funzione che accetta un oggetto che tiene le tue informazioni in coppie chiave-valore e il templateString è un codice HTML. Ok, accetta una stringa HTML, ma cos'è $ ( "# TPL-menu"). Html () fare lì? Quando stiamo sviluppando una piccola applicazione a pagina singola, normalmente inseriamo i modelli direttamente nella pagina in questo modo:

// index.html  

E poiché è un tag script, non viene mostrato all'utente. Da un altro punto di vista, è un nodo DOM valido in modo da poter ottenere il suo contenuto con jQuery. Quindi, il breve frammento sopra prende solo il contenuto di quel tag script.

Il rendere il metodo è molto importante in Backbone.js. Questa è la funzione che mostra i dati. Normalmente si associano gli eventi attivati ​​dai modelli direttamente a quel metodo. Tuttavia, per il menu principale, non abbiamo bisogno di un simile comportamento.

. Questo $ el.html (this.template ()); 

questo. $ el è un oggetto creato dal framework e ogni vista lo ha per impostazione predefinita (c'è a $ di fronte a EL perché abbiamo incluso jQuery). E per impostazione predefinita, è un vuoto

. Ovviamente puoi cambiarlo usando il tagName proprietà. Ma ciò che è più importante qui, è che non assegniamo direttamente un valore a quell'oggetto. Non lo stiamo cambiando, stiamo cambiando solo il suo contenuto. C'è una grande differenza tra la riga sopra e la seguente:

questo. $ el = $ (this.template ()); 

Il punto è che se vuoi vedere le modifiche nel browser dovresti chiamare prima il metodo render, per aggiungere la vista al DOM. Altrimenti verrà aggiunto solo il div vuoto. C'è anche un altro scenario in cui hai viste nidificate. E poiché stai modificando la proprietà direttamente, il componente principale non viene aggiornato. Anche gli eventi associati possono essere interrotti e devi collegare nuovamente gli ascoltatori. Quindi, dovresti solo cambiare il contenuto di questo. $ el e non il valore della proprietà.

La vista è ora pronta e dobbiamo inizializzarla. Aggiungiamolo al nostro modulo di fabbrica:

// App.js var ViewsFactory = menu: function () if (! This.menuView) this.menuView = new api.views.menu (el: $ ("# menu"));  restituire this.menuView; ; 

Alla fine basta chiamare il menu metodo nell'area di bootstrap:

// App.js init: function () this.content = $ ("# content"); this.todos = new api.collections.ToDos (); ViewsFactory.menu (); restituiscilo;  

Si noti che mentre stiamo creando una nuova istanza dalla classe di navigazione, stiamo passando un elemento DOM già esistente $ ( "# Menu"). Così la questo. $ el la proprietà all'interno della vista indica effettivamente $ ( "# Menu").

Aggiunta di rotte

Backbone.js supporta il stato di spinta operazioni. In altre parole, puoi manipolare l'URL del browser corrente e spostarti tra le pagine. Ad ogni modo, ad esempio, rimarremo con i buoni vecchi URL di tipo hash / # Modificare / 3.

// App.js var Router = Backbone.Router.extend (routes: "archive": "archive", "new": "newToDo", "edit /: index": "editToDo", "elimina /: index ":" delteToDo "," ":" elenco ", lista: funzione (archivio) , archivio: function () , newToDo: function () , editToDo: function (index) , delteToDo: funzione (indice) ); 

Sopra è il nostro router. Ci sono cinque percorsi definiti in un oggetto hash. La chiave è ciò che digiterà nella barra degli indirizzi del browser e il valore è la funzione che verrà chiamata. Si noti che c'è :indice su due delle rotte. Questa è la sintassi che è necessario utilizzare se si desidera supportare URL dinamici. Nel nostro caso, se digiti # Modifica / 3 il editToDo sarà eseguito con parametro index = 3. L'ultima riga contiene una stringa vuota che significa che gestisce la home page della nostra applicazione.

Mostrare una lista di tutti i compiti

Finora ciò che abbiamo costruito è la visione principale del nostro progetto. Recupererà i dati dalla raccolta e li stamperà sullo schermo. Potremmo usare la stessa vista per due cose: mostrare tutti i ToDos attivi e mostrare quelli che sono archiviati.

Prima di continuare con l'implementazione della visualizzazione elenco, vediamo come viene effettivamente inizializzata.

// in App.js visualizza la lista factory: function () if (! this.listView) this.listView = new api.views.list (model: api.todos);  return this.listView;  

Si noti che stiamo passando nella raccolta. Questo è importante perché lo useremo in seguito questo modello per accedere ai dati memorizzati. La fabbrica restituisce la nostra visualizzazione elenco, ma il router è il ragazzo che deve aggiungerlo alla pagina.

// nella lista dei router di App.js: function (archive) var view = ViewsFactory.list (); api .title (archivio? "Archivio:": "ToDos:") .changeContent (view. $ el); view.setMode (archive? "archive": null) .render ();  

Per ora, il metodo elenco nel router viene chiamato senza alcun parametro. Quindi la vista non è in archivio modalità, mostrerà solo i ToDos attivi.

// views / list.js app.views.list = Backbone.View.extend (mode: null, events: , initialize: function () var handler = _.bind (this.render, this); .model.bind ('change', handler); this.model.bind ('add', handler); this.model.bind ('remove', handler);, render: function () , priorityUp: function (e) , priorityDown: function (e) , archive: function (e) , changeStatus: function (e) , setMode: function (mode) this.mode = mode; ); 

Il modalità la proprietà sarà usata durante il rendering. Se il suo valore è mode = "archivio" quindi verranno mostrati solo i ToDos archiviati. Il eventi è un oggetto che riempiremo immediatamente. Questo è il posto in cui posizioniamo la mappatura degli eventi del DOM. Il resto dei metodi sono le risposte dell'interazione dell'utente e sono direttamente collegati alle funzionalità necessarie. Per esempio, priorityUp e priorityDown cambia l'ordine dei ToDos. archivio sposta l'elemento nell'area di archivio. cambiare stato segna semplicemente l'impegno come fatto.

È interessante cosa sta succedendo nel inizializzare metodo. In precedenza abbiamo detto che normalmente vincolerai i cambiamenti nel modello (la raccolta nel nostro caso) al rendere metodo della vista. Puoi digitare this.model.bind ('change', this.render). Ma molto presto noterai che il Questo parola chiave, nel rendere il metodo non punta alla vista stessa. Questo perché l'ambito è cambiato. Come soluzione alternativa, stiamo creando un gestore con un ambito già definito. Questo è quello di Underscore legare la funzione è usata per.

Ed ecco l'implementazione del rendere metodo.

// views / list.js render: function () ) var html = '
    ', auto = questo; this.model.each (function (todo, index) if (self.mode === "archive"? todo.get ("archived") === true: todo.get ("archived") === false ) var template = _.template ($ ("# tpl-list-item"). html ()); html + = template (title: todo.get ("title"), index: index, archiveLink: self .mode === "archivio"? "unarchive": "archive", fatto: todo.get ("done")? "yes": "no", doneChecked: todo.get ("done")? 'checked = = "checked" ":" ");); html + = '
'; . Questo $ el.html (html); this.delegateEvents (); restituiscilo;

Stiamo collegando tutti i modelli della collezione e generando una stringa HTML, che verrà successivamente inserita nell'elemento DOM della vista. Ci sono alcuni controlli che distinguono il ToDos da archiviato a attivo. L'attività è contrassegnata come fatto con l'aiuto di una casella di controllo. Quindi per indicarlo dobbiamo passare a controllato == "controllato" attributo a quell'elemento Potresti notare che stiamo usando this.delegateEvents (). Nel nostro caso questo è necessario, perché stiamo staccando e attaccando la vista dal DOM. Sì, non sostituiamo l'elemento principale, ma i gestori degli eventi vengono rimossi. Ecco perché dobbiamo dire a Backbone.js di collegarli di nuovo. Il modello utilizzato nel codice sopra riportato è:

// index.html  

Si noti che è stata definita una classe CSS definita fatto sì, che dipinge il ToDo con uno sfondo verde. Oltre a questo, ci sono un sacco di link che utilizzeremo per implementare la funzionalità necessaria. Hanno tutti attributi dei dati. Il nodo principale dell'elemento, Li, ha dati-index. Il valore di questo attributo sta mostrando l'indice dell'attività nella raccolta. Si noti che le espressioni speciali sono state spostate in <%=… %> vengono inviati al modello funzione. Questo è il dato che viene iniettato nel modello.

È ora di aggiungere alcuni eventi alla vista.

// views / list.js events: 'fai clic su [data-up]': 'priorityUp', 'fai clic su [data-down]': 'priorityDown', 'fai clic su [data-archive]': 'archive ',' click input [data-status] ':' changeStatus ' 

In Backbone.js la definizione dell'evento è solo un hash. In primo luogo scrivi il nome dell'evento e poi un selettore. I valori delle proprietà sono in realtà metodi della vista.

// views / list.js priorityUp: function (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.up (indice); , priorityDown: function (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.down (indice); , archive: function (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.archive (this.mode! == "archive", index); , changeStatus: function (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.changeStatus (e.target.checked, index);  

Qui stiamo usando e.target arrivando al conduttore. Indica l'elemento DOM che ha attivato l'evento. Stiamo ottenendo l'indice del fatto clic su ToDo e l'aggiornamento del modello nella raccolta. Con queste quattro funzioni abbiamo terminato la nostra lezione e ora i dati vengono mostrati sulla pagina.

Come accennato in precedenza, useremo la stessa vista per il Archivio pagina.

list: function (archive) var view = ViewsFactory.list (); api .title (archivio? "Archivio:": "ToDos:") .changeContent (view. $ el); view.setMode (archive? "archive": null) .render (); , archive: function () this.list (true);  

Sopra è lo stesso gestore di route di prima, ma questa volta con vero come parametro.

Aggiunta e modifica di ToDos

Seguendo il primer della visualizzazione elenco, è possibile crearne un altro che mostri un modulo per aggiungere e modificare attività. Ecco come viene creata questa nuova classe:

// App.js / views factory form: function () if (! This.formView) this.formView = new api.views.form (model: api.todos). On ("salvato", funzione ( ) api.router.navigate ("", trigger: true);) restituisce this.formView;  

Praticamente lo stesso. Tuttavia, questa volta dobbiamo fare qualcosa una volta inviato il modulo. E questo è in avanti l'utente alla home page. Come ho detto, ogni oggetto che estende le classi Backbone.js è in realtà un dispatcher di eventi. Ci sono metodi come sopra e grilletto che puoi usare.

Prima di continuare con il codice della vista, diamo un'occhiata al modello HTML:

 

Noi abbiamo un textarea e a pulsante. Il modello si aspetta a titolo parametro che dovrebbe essere una stringa vuota, se stiamo aggiungendo una nuova attività.

// views / form.js app.views.form = Backbone.View.extend (index: false, events: 'click button': 'save', initialize: function () this.render (); , render: function (index) var template, html = $ ("# tpl-form"). html (); if (typeof index == 'undefined') this.index = false; template = _.template ( html, title: ""); else this.index = parseInt (index); this.todoForEditing = this.model.at (this.index); template = _.template ($ ("# tpl-form ") .html (), title: this.todoForEditing.get (" title ")); this. $ el.html (template); this. $ el.find (" textarea "). focus (); this.delegateEvents (); return this;, save: function (e) e.preventDefault (); var title = this. $ el.find ("textarea"). val (); if (title == "" ) alert ("textarea vuota!"); return; if (this.index! == false) this.todoForEditing.set ("title", title); else this.model.add (title: titolo); this.trigger ("salvato");); 

La vista è solo 40 righe di codice, ma fa bene il suo lavoro. C'è solo un evento allegato e questo è il clic del pulsante Salva. Il metodo di rendering si comporta in modo diverso a seconda del passato indice parametro. Ad esempio, se stiamo modificando un impegno, passiamo l'indice e recuperiamo il modello esatto. In caso contrario, il modulo è vuoto e verrà creata una nuova attività. Ci sono diversi punti interessanti nel codice qui sopra. Innanzitutto, nel rendering abbiamo usato il .messa a fuoco() metodo per portare l'attenzione sul modulo una volta renderizzata la vista. Di nuovo il delegateEvents la funzione dovrebbe essere chiamata, poiché il modulo potrebbe essere staccato e collegato nuovamente. Il salvare il metodo inizia con e.preventDefault (). Ciò rimuove il comportamento predefinito del pulsante, che in alcuni casi potrebbe essere l'invio del modulo. E alla fine, una volta fatto tutto, abbiamo attivato il salvato evento che notifica al mondo esterno che il ToDo viene salvato nella raccolta.

Ci sono due metodi per il router che dobbiamo compilare.

// App.js newToDo: function () var view = ViewsFactory.form (); api.title ("Crea nuovo ToDo:"). changeContent (view. $ el); view.render (), editToDo: function (index) var view = ViewsFactory.form (); . Api.title ( "Modifica:") changeContent (vista $ el.); view.render (indice);  

La differenza tra loro è che passiamo in un indice, se il Modifica /: index percorso è abbinato. E ovviamente il titolo della pagina è cambiato di conseguenza.

Eliminazione di un record dalla raccolta

Per questa funzione, non abbiamo bisogno di una vista. L'intero lavoro può essere eseguito direttamente nel gestore del router.

delteToDo: function (index) api.todos.remove (api.todos.at (parseInt (index))); api.router.navigate ("", trigger: true);  

Conosciamo l'indice del ToDo che vogliamo eliminare. C'è un rimuovere metodo nella classe collection che accetta un oggetto modello. Alla fine, basta inoltrare l'utente alla pagina iniziale, che mostra l'elenco aggiornato.

Conclusione

Backbone.js ha tutto il necessario per creare un'applicazione a singola pagina completamente funzionale. Potremmo anche associarlo a un servizio di back-end di REST e il framework sincronizzerà i dati tra l'app e il database. L'approccio guidato dagli eventi incoraggia la programmazione modulare e una buona architettura. Uso personalmente Backbone.js per diversi progetti e funziona molto bene.