Enkel sida ToDo-applikation med Backbone.js

Backbone.js är en JavaScript-ram för att bygga flexibla webbapplikationer. Det kommer med modeller, samlingar, åsikter, händelser, router och några andra bra funktioner. I den här artikeln kommer vi att utveckla en enkel ToDo-applikation som stödjer att lägga till, redigera och ta bort uppgifter. Vi ska också kunna markera en uppgift som Gjort och arkivera det. För att hålla denna postlängd rimlig kommer vi inte att inkludera någon kommunikation med en databas. Alla data sparas på klientsidan.

Inrätta

Här är filstrukturen som vi ska använda:

css └── styles.css js └─ - samlingar └── ToDos.js └── modeller └── ToDo.js └─ - leverantör └── backbone.js └── jquery-1.10.2.min.js └─ - underscore.js └─ - views └── App.js └──index.html 

Det finns få saker som är uppenbara, som /css/styles.css och /index.html. De innehåller CSS-format och HTML-markup. I samband med Backbone.js är modellen en plats där vi behåller våra data. Så, våra ToDos kommer helt enkelt att vara modeller. Och för att vi ska ha mer än en uppgift kommer vi att organisera dem i en samling. Affärslogiken distribueras mellan vyerna och huvudprogrammets fil, App.js. Backbone.js har bara ett hårt beroende - Underscore.js. Ramverket spelar också mycket bra med jQuery, så de båda går till Säljare katalogen. Allt vi behöver nu är bara lite HTML-märkning och vi är redo att gå.

   Mina TODOs    

Som ni ser kan vi inkludera alla externa JavaScript-filer mot botten, eftersom det är en bra övning att göra detta i slutet av koden. Vi förbereder också uppstartningen av applikationen. Det finns en behållare för innehållet, en meny och en titel. Huvudnavigering är ett statiskt element och vi kommer inte att ändra det. Vi ersätter innehållet i titeln och div under det.

Planerar applikationen

Det är alltid bra att ha en plan innan vi börjar jobba med någonting. Backbone.js har inte en super strikt arkitektur, som vi måste följa. Det är en av fördelarna med ramverket. Så, innan vi börjar med genomförandet av affärslogiken, låt oss prata om grunden.

Namespacing

En bra övning är att sätta din kod till sin egen räckvidd. Att registrera globala variabler eller funktioner är inte en bra idé. Det vi ska skapa är en modell, en samling, en router och några Backbone.js visningar. Alla dessa element borde leva i ett privat utrymme. App.js kommer att innehålla klassen som rymmer allt.

// App.js var app = (funktion ) var api = visningar: , modeller: , samlingar: , innehåll: null, router: null, todos: null, init: function this.content = $ ("# content");, changeContent: funktion (el) this.content.empty () .append (el); returnera detta;, titel: funktion (str) $ ("h1 ") .text (str); returnera det;; var ViewsFactory = ; var Router = Ryggraden.Router.extend (); api.router = ny router (); returnera api;) 

Ovanstående är en typisk implementering av det avslöjande modulmönstret. De api variabel är föremålet som returneras och representerar klassens offentliga metoder. De visningar, modeller och samlingar Egenskaper kommer att fungera som innehavare för de klasser som returneras av Backbone.js. De innehåll är ett jQuery-element som pekar på huvudanvändarens gränssnittsbehållare. Det finns två hjälpar metoder här. Den första uppdaterar den behållaren. Den andra anger sidans titel. Då definierade vi en modul som heter ViewsFactory. Det kommer att leverera våra åsikter och i slutet skapade vi routern.

Du kanske frågar, varför behöver vi en fabrik för synpunkterna? Tja, det finns några vanliga mönster när du arbetar med Backbone.js. En av dem är relaterad till skapandet och användningen av åsikterna.

var ViewClass = Backbone.View.extend (/ * logik här * /); var view = new ViewClass (); 

Det är bra att initialisera visningarna endast en gång och lämna dem levande. När uppgifterna ändras kallar vi vanligtvis metoder för vyn och uppdaterar innehållet i dess el objekt. Det andra mycket populära tillvägagångssättet är att återskapa hela visningen eller ersätta hela DOM-elementet. Men det är inte riktigt bra ur prestationssynpunkt. Så slutar vi normalt med en nytta klass som skapar en förekomst av vyn och returnerar den när vi behöver det.

Komponenter Definition

Vi har ett namnutrymme, så nu kan vi börja skapa komponenter. Så här ser huvudmenyn ut:

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

Vi skapade en egendom som heter meny som håller klassen av navigeringen. Senare kan vi lägga till en metod i fabriksmodulen som skapar en förekomst av den.

var ViewsFactory = meny: funktion () om (! this.menuView) this.menuView = nya api.views.menu (el: $ ("# menu"));  returnera this.menuView; ; 

Ovan är hur vi hanterar alla synpunkter, och det kommer att se till att vi bara får en och samma instans. Denna teknik fungerar bra, i de flesta fall.

Strömma

Ingångspunkten för appen är App.js och dess i det metod. Detta är vad vi kommer att ringa i onload handler av fönster objekt.

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

Därefter tar den definierade routern kontrollen. Baserat på webbadressen bestämmer den vilken hanterare som ska utföras. I Backbone.js har vi inte den vanliga modell-View-Controller-arkitekturen. Controller saknas och det mesta av logiken läggs in i vyerna. Så i stället kopplar vi modellerna direkt till metoder, inuti vyerna och får en omedelbar uppdatering av användargränssnittet, när data har ändrats.

Hantera data

Det viktigaste i vårt lilla projekt är data. Våra uppgifter är vad vi ska hantera, så låt oss börja därifrån. Här är vår modelldefinition.

// modeller / ToDo.js app.models.ToDo = Backbone.Model.extend (standardvärden: title: "ToDo", arkiverad: false, gjort: false); 

Bara tre fält. Den första innehåller texten i uppgiften och de andra två är flaggor som definierar status för posten.

Varje sak inuti ramen är faktiskt en händelsessändare. Och eftersom modellen ändras med setters, vet ramverket när data uppdateras och kan meddela resten av systemet för det. När du har bundet något till dessa meddelanden, kommer din ansökan att reagera på förändringarna i modellen. Detta är en väldigt kraftfull funktion i Backbone.js.

Som jag sa i början kommer vi att ha många register och vi kommer att organisera dem i en samling som heter Todos.

// samlingar / ToDos.js app.collections.ToDos = Backbone.Collection.extend (initiera: funktion () this.add (title: "Lär dig JavaScript-basics"); this.add (title: "Go till backbonejs.org "); this.add (title:" Utveckla en ryggradsapplikation ");, modell: app.models.ToDo up: funktion (index) if (index> 0) var tmp = this.models [index-1]; this.models [index-1] = this.models [index]; this.models [index] = tmp; this.trigger ("change");, ner: funktion index) om (index < 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);  ); 

De initialisera Metoden är insamlingspunktens ingångspunkt. I vårt fall lade vi till några uppgifter som standard. Naturligtvis i den verkliga världen kommer informationen från en databas eller någon annanstans. Men för att hålla dig fokuserad, kommer vi att göra det manuellt. Det andra som är typiskt för samlingar, ställer in modell fast egendom. Det berättar för klassen vilken typ av data som lagras. Resten av metoderna implementerar anpassad logik, relaterad till funktionerna i vår applikation. upp och ner funktioner ändrar ordern på ToDos. För att förenkla saker kommer vi att identifiera varje ToDo med bara ett index i samlingens array. Det betyder att om vi vill hämta en specifik post, bör vi peka på indexet. Så, beställningen byter bara elementen i en array. Som du kan gissa från koden ovan, this.models är den matris som vi pratar om. arkiv och byta status Ange egenskaper för det givna elementet. Vi lägger dessa metoder här, eftersom synpunkterna kommer att ha åtkomst till Todos insamling och inte direkt till uppgifterna.

Dessutom behöver vi inte skapa några modeller från app.models.ToDo klass, men vi behöver skapa en instans från app.collections.ToDos samling.

// App.js init: funktion () this.content = $ ("# content"); this.todos = nya api.collections.ToDos (); returnera detta;  

Visar vår första vy (huvudnavigering)

Det första som vi måste visa är huvudprogrammets navigering.

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

Det är bara nio rader av kod, men det händer massor av coola saker här. Den första är att ställa in en mall. Om du kommer ihåg, lade vi till Underscore.js till vår app? Vi ska använda sin templerande motor, eftersom det fungerar bra och det är enkelt att använda.

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

Vad du har i slutet, är en funktion som accepterar ett objekt som håller din information i nyckelvärdespar och templateString är HTML markup. Ok, så det accepterar en HTML-sträng, men vad är det $ ( "# TPL-meny"). Html () gör det där? När vi utvecklar ett litet enkelsidigt program sätter vi normalt mallarna direkt på sidan så här:

// index.html  

Och eftersom det är en skriptikett visas den inte för användaren. Från en annan synpunkt är det en giltig DOM-nod, så vi kan få innehållet med jQuery. Så det korta stycket ovan tar bara innehållet i den här skripttaggen.

De göra Metoden är verkligen viktig i Backbone.js. Det är den funktion som visar data. Normalt binder du händelserna som drivs av modellerna direkt till den metoden. Men för huvudmenyn behöver vi inget sådant beteende.

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

detta. $ el är ett objekt som skapas av ramen och varje vy har det som standard (det finns en $ framför el eftersom vi har inkluderat jQuery). Och som standard är det tomt

. Naturligtvis kan du ändra det genom att använda taggnamn fast egendom. Men det som är viktigare här är att vi inte direkt tilldelar ett värde till det objektet. Vi ändrar inte det, vi ändrar bara innehållet. Det finns en stor skillnad mellan linjen ovan och den här följande:

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

Poängen är att om du vill se ändringarna i webbläsaren ska du ringa återställningsmetoden före, för att lägga till vyn i DOM. Annars kommer endast den tomma diven att fästas. Det finns också ett annat scenario där du har kapslade visningar. Och eftersom du ändrar fastigheten direkt, är inte moderkomponenten uppdaterad. De bundna händelserna kan också brytas och du måste fästa lyssnarna igen. Så, du borde bara ändra innehållet i detta. $ el och inte egenskapens värde.

Vyn är nu klar och vi måste initiera den. Låt oss lägga till det i vår fabriksmodul:

// App.js var ViewsFactory = meny: funktion () om (! This.menuView) this.menuView = nya api.views.menu (el: $ ("# menu"));  returnera this.menuView; ; 

I slutet ringer du bara på meny metod i bootstrapping-området:

// App.js init: funktion () this.content = $ ("# content"); this.todos = nya api.collections.ToDos (); ViewsFactory.menu (); returnera detta;  

Observera att medan vi skapar en ny instans från navigerings klassen överför vi ett redan existerande DOM-element $ ( "# Menyn"). Så, den detta. $ el egendom i utsikten pekar faktiskt på $ ( "# Menyn").

Lägga till rutter

Backbone.js stöder tryck tillstånd operationer. Med andra ord kan du manipulera den aktuella webbläsarens URL och resa mellan sidor. Vi klarar dock till exempel de goda gamla hash-typadresserna / # Redigera / 3.

// App.js var Router = Ryggrad.Router.extend (rutter: "arkiv": "arkiv", "nytt": "newToDo", "edit /: index": "editToDo", "delete / ":" delteToDo "," ":" list ", lista: funktion (arkiv) , arkiv: funktion () , newToDo: funktion () , editToDo: funktion (index) , delteToDo: funktion (index) ); 

Ovan är vår router. Det finns fem rutter definierade i ett hashobjekt. Nyckeln är vad du skriver i webbläsarens adressfält och värdet är den funktion som kommer att ringas. Observera att det finns :index på två av rutterna. Det är den syntax som du behöver använda om du vill stödja dynamiska webbadresser. I vårt fall, om du skriver # Redigera / 3 de editToDo kommer att utföras med parameter index = 3. Den sista raden innehåller en tom sträng, vilket innebär att den hanterar vår hemsidas startsida.

Visar en lista över alla uppgifter

Hittills har vi byggt huvudbilden för vårt projekt. Den hämtar data från samlingen och skriver ut den på skärmen. Vi kan använda samma vy för två saker - visa alla aktiva ToDos och visa de som är arkiverade.

Innan vi fortsätter med listvisningsimplementering, låt oss se hur det faktiskt initieras.

// i App.js visningar fabrikslista: funktion () om (! this.listView) this.listView = new api.views.list (model: api.todos);  returnera this.listView;  

Observera att vi passerar i samlingen. Det är viktigt eftersom vi senare kommer att använda den här modellen för att komma åt lagrade data. Fabriken returnerar vår listvy, men routern är killen som måste lägga till den på sidan.

// i App.js router lista: funktion (arkiv) var view = ViewsFactory.list (); api.title (arkiv? "Arkiv:": "Din ToDos:") .changeContent (visa. $ el); view.setMode (arkiv? "arkiv": null) .render ();  

För nu, metoden lista i routern kallas utan några parametrar. Så utsikten är inte in arkiv läge, kommer det bara att visa de aktiva ToDos.

// views / list.js app.views.list = Backbone.View.extend (läge: null, händelser: , initiera: funktion () var handler = _.bind (this.render, this); this .model.bind ('change', handler); this.model.bind ('add', handler); this.model.bind ('remove', handler);, render: function () , priorityUp: funktion (e) , prioritetDown: funktion (e) , arkiv: funktion (e) , changeStatus: function (e) , setMode: funktion (mode) this.mode = mode; ); 

De läge egendom kommer att användas under rendering. Om dess värde är mode = "arkiv" då visas bara de arkiverade ToDos. De evenemang är ett objekt som vi kommer att fylla genast. Det är den plats där vi placerar DOM-händelserna kartläggning. Resten av metoderna är svar på användarens interaktion och de är direkt kopplade till de funktioner som behövs. Till exempel, priorityUp och priorityDown ändrar beställningen av ToDos. arkiv flyttar objektet till arkivområdet. byta status Markerar bara ToDo som gjort.

Det är intressant vad som händer inom initialisera metod. Tidigare sa vi att du normalt binder förändringarna i modellen (samlingen i vårt fall) till göra Metod för utsikten. Du kan skriva this.model.bind ('change', this.render). Men mycket snart kommer du märka att detta sökord, i göra Metoden kommer inte att peka på utsikten själv. Det beror på att räckvidden ändras. Som en lösning skapar vi en hanterare med redan definierat räckvidd. Det är vad Underscore är binda funktionen används för.

Och här är genomförandet av göra metod.

// visningar / list.js gör: funktion () ) var html = '
    ", själv = det här; this.model.each (funktion (todo, index) if (self.mode === "arkiv"? todo.get ("archived") === true: todo.get ("archived") === false ) varmall = _.template ($ ("# tpl-list-item"). html ()); html + = mall (title: todo.get ("title"), index: index, archiveLink: .mode === "arkiv"? "unarchive": "arkiv", gjort: todo.get ("done") "ja": "nej", doneChecked: todo.get ("done")? 'checked = = "kontrollerat" ': "");); html + = '
'; . Denna $ el.html (html); this.delegateEvents (); returnera detta;

Vi slingrar igenom alla modeller i samlingen och skapar en HTML-sträng som senare läggs in i visningens DOM-element. Det finns få kontroller som skiljer ToDos från arkiverad till aktiv. Uppgiften är markerad som Gjort med hjälp av en kryssruta. Så för att ange detta måste vi skicka en kontrollerade == "markerad" attribut till det elementet. Du märker kanske att vi använder this.delegateEvents (). I vårt fall är det nödvändigt eftersom vi tar bort och bifogar utsikten från DOM. Ja, vi ersätter inte huvudelementet, men händelsernas hanterare tas bort. Det är därför vi måste berätta Backbone.js att bifoga dem igen. Mallen som används i koden ovan är:

// index.html  

Observera att det finns en CSS-klass definierad kallad gjort-ja, som målar ToDo med en grön bakgrund. Dessutom finns det en massa länkar som vi ska använda för att genomföra den nödvändiga funktionaliteten. De har alla dataattribut. Huvudkoden hos elementet, li, har data index. Värdet på det här attributet visar indexet för uppgiften i samlingen. Observera att de speciella uttryck som inslagits i <%=… %> skickas till mall fungera. Det är de data som injiceras i mallen.

Det är dags att lägga till några händelser i vyn.

// visningar / list.js händelser: 'klicka på [data-up]': 'priorityUp', 'klicka på [data-down]': 'priorityDown', 'klicka på [data arkiv]': 'arkiv ',' klick in [data status] ':' changeStatus ' 

I Backbone.js är händelsens definition en bara en hash. Du skriver först namnet på händelsen och sedan en väljare. Värdena för egenskaperna är faktiskt visningsmetoder.

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

Här använder vi e.target kommer in till handlaren. Det pekar på DOM-elementet som utlöste händelsen. Vi får indexet för den klickade ToDo och uppdaterar modellen i samlingen. Med dessa fyra funktioner slutade vi vår klass och nu visas data till sidan.

Som vi nämnde ovan kommer vi att använda samma syn för arkiv sida.

lista: funktion (arkiv) var view = ViewsFactory.list (); api.title (arkiv? "Arkiv:": "Din ToDos:") .changeContent (visa. $ el); view.setMode (arkiv? "arkiv": null) .render (); , arkiv: funktion () this.list (true);  

Ovan är samma rutthanterare som tidigare, men den här gången med Sann som en parameter.

Lägga till och redigera ToDos

Efter primeren i listvyen kunde vi skapa en annan som visar ett formulär för att lägga till och redigera uppgifter. Så här är den här nya klassen skapad:

// App.js / views fabriksform: funktion () if (! This.formView) this.formView = new api.views.form (model: api.todos). På ("sparade" funktion ) api.router.navigate ("", trigger: true);) returnera this.formView;  

Typ samma sak. Men den här gången måste vi göra något när formuläret lämnas in. Och så vidarebefordrar användaren till hemsidan. Som jag sa är varje objekt som sträcker sig Backbone.js klasser faktiskt en händelsessändare. Det finns metoder som och avtryckare som du kan använda.

Innan vi fortsätter med visningskoden, låt oss ta en titt på HTML-mallen:

 

Vi har en textarea och a knapp. Mallen förväntar sig a titel parameter som borde vara en tom sträng om vi lägger till en ny uppgift.

// visningar / form.js app.views.form = Backbone.View.extend (index: false, events: 'klickknapp': 'spara', initiera: funktion () this.render (); , render: funktion (index) var mall, html = $ ("# tpl-form"). html (); om (typof index == 'undefined') this.index = false; template = _.template html, title: ""); annat this.index = parseInt (index); this.todoForEditing = this.model.at (this.index); template = _.template ($ ("# tpl-form ") .html (), title: this.todoForEditing.get (" title ")); detta. $ el.html (mall); detta. $ el.find (" textarea "). this.delegateEvents (), returnera detta;, spara: funktion (e) e.preventDefault (); var title = this. $ el.find ("textarea") .val (); om (title == "" ) alert ("Empty textarea!"; return; om (this.index! == false) this.todoForEditing.set ("title", titel); else this.model.add (title: titel); this.trigger ("sparade");); 

Vyn är bara 40 linjer kod, men det gör sitt jobb bra. Det finns bara en händelse kopplad och det här är att klicka på Spara-knappen. Återställningsmetoden fungerar annorlunda beroende på det godkända index parameter. Om vi ​​till exempel redigerar en ToDo passerar vi indexet och hämtar den exakta modellen. Om inte, är formuläret tomt och en ny uppgift skapas. Det finns flera intressanta punkter i koden ovan. Först, i utförandet använde vi .fokus() metod för att fokusera på formuläret när vyn är gjord. Återigen delegateEvents funktionen bör kallas, eftersom formuläret kan lösas och fästas igen. De spara Metoden börjar med e.preventDefault (). Detta tar bort standardbeteendet för knappen, som i vissa fall kan skicka formuläret. Och i slutet, när allt är klart, utlöste vi sparade händelse som meddelar omvärlden att ToDo är sparad i samlingen.

Det finns två metoder för routern som vi måste fylla i.

// App.js newToDo: funktion () var view = ViewsFactory.form (); api.title ("Skapa ny ToDo:"). changeContent (visa. $ el); view.render (), editToDo: funktion (index) var view = ViewsFactory.form (); . Api.title ( "Edit") changeContent (se $ el.); view.render (index);  

Skillnaden mellan dem är att vi passerar i ett index, om redigera /: index rutt är matchad. Och naturligtvis ändras sidans titel i enlighet med detta.

Radering av en post från samlingen

För den här funktionen behöver vi inte en vy. Hela jobbet kan göras direkt i routerns hanterare.

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

Vi vet indexet för ToDo som vi vill ta bort. Det finns en ta bort metod i samlingsklassen som accepterar ett modellobjekt. I slutet, bara vidarebefordra användaren till hemsidan, som visar den uppdaterade listan.

Slutsats

Backbone.js har allt du behöver för att bygga en fullt fungerande applikation för enstaka sidor. Vi kan till och med binda den till en REST back-end-tjänst och ramverket kommer att synkronisera data mellan din app och databasen. Den händelsedrivna inställningen uppmuntrar modulär programmering, tillsammans med en bra arkitektur. Jag använder personligen Backbone.js för flera projekt och det fungerar väldigt bra.