Backbone.js is a JavaScript framework for building flexible web applications. It comes with Models, Collections, Views, Events, Router and a few other great features. In this article we will develop a simple ToDo application which supports adding, editing, and removing tasks. We should also be able to mark a task as done and archive it. In order to keep this post's length reasonable, we will not include any communication with a database. All the data will be kept on the client-side.
There are few things which are obvious, like
As you can see, we are including all the external JavaScript files towards the bottom, as it's a good practice to do this at the end of the body tag. We are also preparing the bootstrapping of the application. There is container for the content, a menu and a title. The main navigation is a static element and we are not going to change it. We will replace the content of the title and the
Above is a typical implementation of the revealing module pattern. The
You may ask, why do we need a factory for the views? Well, there are some common patterns while working with Backbone.js. One of them is related to the creation and usage of the views.
It's good to initialize the views only once and leave them alive. Once the data is changed, we normally call methods of the view and update the content of its
We created a property called
Above is how we will handle all of the views, and it will ensure that we get only one and of the same instance. This technique works well, in most cases.
After that, the defined router takes control. Based on the URL, it decides which handler to execute. In Backbone.js, we don't have the usual Model-View-Controller architecture. The Controller is missing and most of the logic is put into the views. So instead, we wire the models directly to methods, inside the views and get an instant update of the user interface, once the data has changed.
Just three fields. The first one contains the text of the task and the other two are flags which define the status of the record.
Every thing inside the framework is actually an event dispatcher. And because the model is changed with setters, the framework knows when the data is updated and can notify the rest of the system for that. Once you bind something to these notifications, your application will react on the changes in the model. This is a really powerful feature in Backbone.js.
As I said in the beginning, we will have many records and we will organize them into a collection called
The
Additionally, we don't need to create any models from the
It's only nine lines of code, but lots of cool things are happening here. The first one is setting a template. If you remember, we added Underscore.js to our app? We are going to use its templating engine, because it works good and it is simple enough to use.
What you have at the end, is a function which accepts an object holding your information in key-value pairs and the
And because it's a script tag, it is not shown to the user. From another point of view, it is a valid DOM node so we could get its content with jQuery. So, the short snippet above just takes the content of that script tag.
The
The point is, that if you want to see the changes in the browser you should call the render method before, to append the view to the DOM. Otherwise only the empty div will be attached. There is also another scenario where you have nested views. And because you are changing the property directly, the parent component is not updated. The bound events may also be broken and you need to attach the listeners again. So, you really should only change the content of
The view is now ready and we need to initialize it. Let's add it to our factory module:
At the end simply call the
Notice that while we are creating a new instance from the navigation's class, we are passing an already existing DOM element
Above is our router. There are five routes defined in a hash object. The key is what you will type in the browser's address bar and the value is the function which will be called. Notice that there is
Before to continue with the list view implementation, let's see how it is actually initialized.
Notice that we are passing in the collection. That's important because we will later use
For now, the method
The
It's interesting what is happening inside the
And here is the implementation of the
We are looping through all the models in the collection and generating an HTML string, which is later inserted into the view's DOM element. There are few checks which distinguish the ToDos from archived to active. The task is marked as done with the help of a checkbox. So, to indicate this we need to pass a
Notice that there is a CSS class defined called
It's time to add some events to the view.
In Backbone.js the event's definition is a just a hash. You firstly type the name of the event and then a selector. The values of the properties are actually methods of the view.
Here we are using
As we mentioned above, we will use the same view for the
Above is the same route handler as before, but this time with
Pretty much the same. However, this time we need to do something once the form is submitted. And that's forward the user to the home page. As I said, every object which extends Backbone.js classes, is actually an event dispatcher. There are methods like
Before we continue with the view code, let's take a look at the HTML template:
We have a
The view is just 40 lines of code, but it does its job well. There is only one event attached and this is the clicking of the save button. The render method acts differently depending of the passed
There are two methods for the router which we have to fill in.
The difference between them is that we pass in an index, if the
We know the index of the ToDo which we want to delete. There is a
Setup
Here is the file structure which we'll use:01 02 03 04 05 06 07 08 09 10 11 12 13 14 | 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 |
/css/styles.css
and /index.html
. They contain the CSS styles and the HTML markup. In the context of Backbone.js, the model is a place where we keep our data. So, our ToDos will simply be models. And because we will have more than one task, we will organize them into a collection. The business logic is distributed between the views and the main application's file, App.js
. Backbone.js has only one hard dependency - Underscore.js. The framework also plays very well with jQuery, so they both go to the vendor
directory. All we need now is just a little HTML markup and we are ready to go.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <! doctype html> < html > < head > < title >My TODOs</ title > < link rel = "stylesheet" type = "text/css" href = "css/styles.css" /> </ head > < body > < div class = "container" > < div id = "menu" class = "menu cf" ></ div > < h1 ></ h1 > < div id = "content" ></ div > </ div > < script src = "js/vendor/jquery-1.10.2.min.js" ></ script > < script src = "js/vendor/underscore.js" ></ script > < script src = "js/vendor/backbone.js" ></ script > < script src = "js/App.js" ></ script > < script src = "js/models/ToDo.js" ></ script > < script src = "js/collections/ToDos.js" ></ script > < script > window.onload = function() { // bootstrap } </ script > </ body > </ html > |
div
below it.Planning the Application
It's always good to have a plan before we start working on something. Backbone.js doesn't have a super strict architecture, which we have to follow. That's one of the benefits of the framework. So, before we start with the implementation of the business logic, let's talk about the basis.Namespacing
A good practice is to put your code into its own scope. Registering global variables or functions is not a good idea. What we will create is one model, one collection, a router and few Backbone.js views. All these elements should live in a private space.App.js
will contain the class which holds everything.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // App.js var app = ( function () { var api = { views: {}, models: {}, collections: {}, content: 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; })(); |
api
variable is the object which is returned and represents the public methods of the class. The views
, models
and collections
properties will act as holders for the classes returned by Backbone.js. The content
is a jQuery element pointing to the main user's interface container. There are two helper methods here. The first one updates that container. The second one sets the page's title. Then we defined a module called ViewsFactory
. It will deliver our views and at the end, we created the router.You may ask, why do we need a factory for the views? Well, there are some common patterns while working with Backbone.js. One of them is related to the creation and usage of the views.
1 2 | var ViewClass = Backbone.View.extend({ /* logic here */ }); var view = new ViewClass(); |
el
object. The other very popular approach, is to recreate the whole view or replace the whole DOM element. However, that's not really good from a performance point of view. So, we normally end up with a utility class which creates one instance of the view and returns it when we need it.Components Definition
We have a namespace, so now we can start creating components. Here is how the main menu looks:1 2 3 4 5 | // views/menu.js app.views.menu = Backbone.View.extend({ initialize: function () {}, render: function () {} }); |
menu
which holds the class of the navigation. Later, we may add a method in the factory module which creates an instance of it.01 02 03 04 05 06 07 08 09 10 | var ViewsFactory = { menu: function () { if (! this .menuView) { this .menuView = new api.views.menu({ el: $( "#menu" ) }); } return this .menuView; } }; |
Flow
The entry point of the app isApp.js
and its init
method. This is what we will call in the onload
handler of the window
object.1 2 3 | window.onload = function () { app.init(); } |
Managing the Data
The most important thing in our small project is the data. Our tasks are what we should manage, so let's start from there. Here is our model definition.1 2 3 4 5 6 7 8 | // models/ToDo.js app.models.ToDo = Backbone.Model.extend({ defaults: { title: "ToDo" , archived: false , done: false } }); |
Every thing inside the framework is actually an event dispatcher. And because the model is changed with setters, the framework knows when the data is updated and can notify the rest of the system for that. Once you bind something to these notifications, your application will react on the changes in the model. This is a really powerful feature in Backbone.js.
As I said in the beginning, we will have many records and we will organize them into a collection called
ToDos
.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // collections/ToDos.js app.collections.ToDos = Backbone.Collection.extend({ initialize: function (){ this .add({ title: "Learn JavaScript basics" }); this .add({ title: "Go to backbonejs.org" }); this .add({ title: "Develop a Backbone application" }); }, model: 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 (index) { if (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); } }); |
initialize
method is the entry point of the collection. In our case, we added a few tasks by default. Of course in the real world, the information will come from a database or somewhere else. But to keep you focused, we will do that manually. The other thing which is typical for collections, is setting the model
property. It tells the class what kind of data is being stored. The rest of the methods implement custom logic, related to the features in our application. up
and down
functions change the order of the ToDos. To simplify things, we will identify every ToDo with just an index in the collection's array. This means that if we want to fetch one specific record, we should point to its index. So, the ordering is just switching the elements in an array. As you may guess from the code above, this.models
is the array which we are talking about. archive
and changeStatus
set properties of the given element. We put these methods here, because the views will have access to the ToDos
collection and not to the tasks directly.Additionally, we don't need to create any models from the
app.models.ToDo
class, but we do need to create an instance from the app.collections.ToDos
collection.1 2 3 4 5 6 | // App.js init: function () { this .content = $( "#content" ); this .todos = new api.collections.ToDos(); return this ; } |
Showing Our First View (Main Navigation)
The first thing which we have to show, is the main application's navigation.01 02 03 04 05 06 07 08 09 10 | // 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({})); } }); |
1 | _.template(templateString, [data], [settings]) |
templateString
is HTML markup. Ok, so it accepts an HTML string, but what is $("#tpl-menu").html()
doing there? When we are developing a small single page application, we normally put the templates directly into the page like this:1 2 3 4 5 6 7 8 | // index.html < script type = "text/template" id = "tpl-menu" > < ul > < li >< a href = "#" >List</ a ></ li > < li >< a href = "#archive" >Archive</ a ></ li > < li class = "right" >< a href = "#new" >+</ a ></ li > </ ul > </ script > |
The
render
method is really important in Backbone.js. That's the function which displays the data. Normally you bind the events fired by the models directly to that method. However, for the main menu, we don't need such behavior.1 | this .$el.html( this .template({})); |
this.$el
is an object created by the framework and every view has it by default (there is a $
infront of el
because we have jQuery included). And by default, it is an empty <div></div>
. Of course you may change that by using the tagName
property. But what is more important here, is that we are not assigning a value to that object directly. We are not changing it, we are changing only its content. There is a big difference between the line above and this next one:1 | this .$el = $( this .template({})); |
this.$el
and not the property's value.The view is now ready and we need to initialize it. Let's add it to our factory module:
01 02 03 04 05 06 07 08 09 10 11 | // App.js var ViewsFactory = { menu: function () { if (! this .menuView) { this .menuView = new api.views.menu({ el: $( "#menu" ) }); } return this .menuView; } }; |
menu
method in the bootstrapping area:1 2 3 4 5 6 7 | // App.js init: function () { this .content = $( "#content" ); this .todos = new api.collections.ToDos(); ViewsFactory.menu(); return this ; } |
$("#menu")
. So, the this.$el
property inside the view is actually pointing to $("#menu")
.Adding Routes
Backbone.js supports the push state operations. In other words, you may manipulate the current browser's URL and travel between pages. However, we'll stick with the good old hash type URLs, for example/#edit/3
.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | // App.js var Router = Backbone.Router.extend({ routes: { "archive" : "archive" , "new" : "newToDo" , "edit/:index" : "editToDo" , "delete/:index" : "delteToDo" , "" : "list" }, list: function (archive) {}, archive: function () {}, newToDo: function () {}, editToDo: function (index) {}, delteToDo: function (index) {} }); |
:index
on two of the routes. That's the syntax which you need to use if you want to support dynamic URLs. In our case, if you type #edit/3
the editToDo
will be executed with parameter index=3
. The last row contains an empty string which means that it handles the home page of our application.Showing a List of All the Tasks
So far what we've built is the main view for our project. It will retrieve the data from the collection and print it out on the screen. We could use the same view for two things - displaying all the active ToDos and showing those which are archived.Before to continue with the list view implementation, let's see how it is actually initialized.
1 2 3 4 5 6 7 8 9 | // in App.js views factory list: function () { if (! this .listView) { this .listView = new api.views.list({ model: api.todos }); } return this .listView; } |
this.model
to access the stored data. The factory returns our list view, but the router is the guy who has to add it to the page.1 2 3 4 5 6 7 8 | // in App.js's router list: function (archive) { var view = ViewsFactory.list(); api .title(archive ? "Archive:" : "Your ToDos:" ) .changeContent(view.$el); view.setMode(archive ? "archive" : null ).render(); } |
list
in the router is called without any parameters. So the view is not in archive
mode, it will show only the active ToDos.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | // views/list.js app.views.list = Backbone.View.extend({ mode: null , events: {}, initialize: function () { var handler = _.bind( this .render, this ); 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; return this ; } }); |
mode
property will be used during the rendering. If its value is mode="archive"
then only the archived ToDos will be shown. The events
is an object which we will fill right away. That's the place where we place the DOM events mapping. The rest of the methods are responses of the user interaction and they are directly linked to the needed features. For example, priorityUp
and priorityDown
changes the ordering of the ToDos. archive
moves the item to the archive area. changeStatus
simply marks the ToDo as done.It's interesting what is happening inside the
initialize
method. Earlier we said that normally you will bind the changes in the model (the collection in our case) to the render
method of the view. You may type this.model.bind('change', this.render)
. But very soon you will notice that the this
keyword, in the render
method will not point to the view itself. That's because the scope is changed. As a workaround, we are creating a handler with an already defined scope. That's what Underscore's bind
function is used for. And here is the implementation of the
render
method.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | // views/list.js render: function () {) var html = '<ul class="list">' , self = this ; 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 === "archive" ? "unarchive" : "archive" , done: todo.get( "done" ) ? "yes" : "no" , doneChecked: todo.get( "done" ) ? 'checked=="checked"' : "" }); } }); html += '</ul>' ; this .$el.html(html); this .delegateEvents(); return this ; } |
checked=="checked"
attribute to that element. You may notice that we are using this.delegateEvents()
. In our case this is necessary, because we are detaching and attaching the view from the DOM. Yes, we are not replacing the main element, but the events' handlers are removed. That's why we have to tell Backbone.js to attach them again. The template used in the code above is:01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | // index.html < script type = "text/template" id = "tpl-list-item" > < li class="cf done-<%= done %>" data-index="<%= index %>"> < h2 > < input type = "checkbox" data-status <%= doneChecked %> /> < a href = "javascript:void(0);" data-up>↑</ a > < a href = "javascript:void(0);" data-down>↓</ a > <%= title %> </ h2 > < div class = "options" > < a href="#edit/<%= index %>">edit</ a > < a href = "javascript:void(0);" data-archive><%= archiveLink %></ a > < a href="#delete/<%= index %>">delete</ a > </ div > </ li > </ script > |
done-yes
, which paints the ToDo with a green background. Besides that, there are a bunch of links which we will use to implement the needed functionality. They all have data attributes. The main node of the element, li
, has data-index
. The value of this attribute is showing the index of the task in the collection. Notice that the special expressions wrapped in <%= ... %>
are sent to the template
function. That's the data which is injected into the template.It's time to add some events to the view.
1 2 3 4 5 6 7 | // views/list.js events: { 'click a[data-up]' : 'priorityUp' , 'click a[data-down]' : 'priorityDown' , 'click a[data-archive]' : 'archive' , 'click input[data-status]' : 'changeStatus' } |
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | // views/list.js priorityUp: function (e) { var index = parseInt(e.target.parentNode.parentNode.getAttribute( "data-index" )); this .model.up(index); }, priorityDown: function (e) { var index = parseInt(e.target.parentNode.parentNode.getAttribute( "data-index" )); this .model.down(index); }, 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); } |
e.target
coming in to the handler. It points to the DOM element which triggered the event. We are getting the index of the clicked ToDo and updating the model in the collection. With these four functions we finished our class and now the data is shown to the page. As we mentioned above, we will use the same view for the
Archive
page.01 02 03 04 05 06 07 08 09 10 | list: function (archive) { var view = ViewsFactory.list(); api .title(archive ? "Archive:" : "Your ToDos:" ) .changeContent(view.$el); view.setMode(archive ? "archive" : null ).render(); }, archive: function () { this .list( true ); } |
true
as a parameter.Adding & Editing ToDos
Following the primer of the list view, we could create another one which shows a form for adding and editing tasks. Here is how this new class is created:01 02 03 04 05 06 07 08 09 10 11 | // App.js / views factory form: function () { if (! this .formView) { this .formView = new api.views.form({ model: api.todos }).on( "saved" , function () { api.router.navigate( "" , {trigger: true }); }) } return this .formView; } |
on
and trigger
which you can use.Before we continue with the view code, let's take a look at the HTML template:
1 2 3 4 5 6 | < script type = "text/template" id = "tpl-form" > < form > < textarea ><%= title %></ textarea > < button >save</ button > </ form > </ script > |
textarea
and a button
. The template expects a title
parameter which should be an empty string, if we are adding a new task.01 02 03 04 05 06 07 08 09 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 | // 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( "Empty textarea!" ); return ; } if ( this .index !== false ) { this .todoForEditing.set( "title" , title); } else { this .model.add({ title: title }); } this .trigger( "saved" ); } }); |
index
parameter. For example, if we are editing a ToDo, we pass the index and fetch the exact model. If not, then the form is empty and a new task will be created. There are several interesting points in the code above. First, in the rendering we used the .focus()
method to bring the focus to the form once the view is rendered. Again the delegateEvents
function should be called, because the form could be detached and attached again. The save
method starts with e.preventDefault()
. This removes the default behavior of the button, which in some cases may be submitting the form. And at the end, once everything is done we triggered the saved
event notifying the outside world that the ToDo is saved into the collection.There are two methods for the router which we have to fill in.
01 02 03 04 05 06 07 08 09 10 11 | // App.js newToDo: function () { var view = ViewsFactory.form(); api.title( "Create new ToDo:" ).changeContent(view.$el); view.render() }, editToDo: function (index) { var view = ViewsFactory.form(); api.title( "Edit:" ).changeContent(view.$el); view.render(index); } |
edit/:index
route is matched. And of course the title of the page is changed accordingly.Deleting a Record From the Collection
For this feature, we don't need a view. The entire job can be done directly in the router's handler.1 2 3 4 | delteToDo: function (index) { api.todos.remove(api.todos.at(parseInt(index))); api.router.navigate( "" , {trigger: true }); } |
remove
method in the collection class which accepts a model object. At the end, just forward the user to the home page, which shows the updated list.
No comments:
Post a Comment