In this post we will build a concrete application step by step: a simple graphical editor (for the impatient, here is what we are going to build in about 100 lines of javascript). We will focus on basic aspects of models and views. Routing and communication with the server will be covered in the next part of this tutorial.
Before we start
If you want to test and hack the code snippets presented here you’ll need to create three files: editor.js, editor.css and editor.html. The first two files editor.js and editor.css are empty for now. Here is how your editor.html would look like:
<!doctype html> <html> <head> <link rel='stylesheet' type='text/css' href='editor.css'> <script src='http://cdnjs.cloudflare.com/ajax/libs/jquery/1.7/jquery.min.js'></script> <script src='http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.2.1/underscore-min.js'></script> <script src='http://cdnjs.cloudflare.com/ajax/libs/backbone.js/0.5.3/backbone-min.js'></script> <script src='editor.js'></script> </head> <body> <div id='page' style='width:2000px;height:2000px;'></div> </body> </html>
I’m using cdnjs here, an alternative free CDN for Javascript brought by cloudflare. It has many libs you won’t find at Google or Microsoft CDNs.
Some of the code samples are also hosted on jsfiddle for convenience. A link is provided in each section to the corresponding jsfiddle code snippet. You can view, test and fork them right there.
Now that we are set up, time to code!
Models
Let’s start by defining a model for a simple shape: the Shape class.
var Shape = Backbone.Model.extend({
defaults: { x:50, y:50, width:150, height:150, color:'black' },
setTopLeft: function(x,y) {
this.set({ x:x, y:y });
},
setDim: function(w,h) {
this.set({ width:w, height:h });
},
});
Our Shape class extends the Backbone.Model class. The extend method takes a hash as argument in order to configure the model. In our case, we have three properties: defaults, setTopLeft and setDim:
- defaults is a special property that backbone uses to define a set of default properties/values in the model. So by default here, a Shape instance will have the properties x, y, width, height and color defined and set to the provided default values. Notice that these model properties are encapsulated by backbone and instead of getting/setting them directly, we will use the get and set methods that Shape inherits from the backbone model. The encapsulation allows backbone to control modification of the model properties and fire change event on set method calls. This allows registering listeners for model changes.
- setTopLeft and setDim are two helper methods that use the backbone’s set method in order to, respectively, set the shape’s top left corner and dimension.
Now that our model class is defined, here is an example of how we would instantiate it and bind change events to its properties (jsfiddle):
var shape = new Shape();
shape.bind('change', function() { alert('changed!'); });
shape.bind('change:width', function() { alert('width changed! ' + shape.get('width')); });
shape.set({ width: 170 });
shape.setTopLeft(100, 100);
In line 3 we are registering for any property change in the model. In line 4 we are only listening only to property ‘width’ changes. By setting the shape’s width line 6, both callbacks will be invoked. On the other hand, setting the top left corner coordinates using ‘setTopLeft’ in line 7 will only trigger the ‘change’ event.
Binding page elements to model changes
In the last section, we were able to define models and listen to their change events. Now, we will try to do something useful with these events. Let’s first define a div element in our html page:
<div class='shape' />
and let’s tie our model changes to it as follows (we’ll be using jQuery for DOM manipulation):
shape.bind('change', function() {
$('.shape').css({ left: shape.get('x'),
top: shape.get('y'),
width: shape.get('width'),
height: shape.get('height'),
background: shape.get('color') });
});
That was easy! Now we can modify the model and observe the dom changing in reaction to model changes. Just open firebug or your favorite browser’s javascript console and try something like:
shape.setTopLeft(10, 10); shape.setDim(500, 500);
and you will see the page automatically updating itself and the shape automatically adapting to the new position/size. The interesting bit here is that we are no longer manipulating the DOM directly. A “piece of code” is listening to model changes and is updating the page on model changes automatically. This snippet is also on jsfiddle.
Time to use user’s input now in order to mutate the model and thus, indirectly, modify the page.
Basic user input handling
In this section, we will enable the user to drag the shape’s element around. In order to do that, we will listen to mouse events mousedown, mouseup and mousemove and update the model accordingly. Here is an example of how we can achieve this (jsfiddle):
var dragging = false;
$('.shape').mousedown(function (e) {
dragging = true;
shape.set({ color: 'gray' });
});
$('#page').mouseup(function () {
dragging = false;
shape.set({ color: 'black'});
});
$('#page').mousemove(function(e) {
if(dragging) {
shape.setTopLeft(e.pageX, e.pageY);
}
});
As a side note, notice that we are listening to mousemove and mouseup events on the parent page element and not on the shape’s div since we want the div to follow (resp. stop following) the mouse position even when it gets off (resp. is up outside of) the shape’s div borders.
What is worth considering here is the simplicity of the jQuery callbacks handling the user input. We are not modifying or even querying the DOM in this code. We are just listening to the user input — mouse events here– and translating it to model changes. The page magically updates itself through the model change listeners.
To sum up, we have separated the code handling user input and mutating the model, and the code updating the view in reaction to model change events. Looks like we implemented a model-view-controller here
.
This is a good step to separate concerns and reduce the callback spaghetti one ends up with when dealing with pages with more than a couple of jQuery callbacks. However, we can do better here as our controller and view code is still scattered across free anonymous functions here and there. We’ll work on this by defining proper view classes later.
Model Collections
Before going further, we have to introduce a special kind of model used in backbone: collections. Collections are simply a container that helps maintain an ordered list of model objects. In addition it comes with built in events for common collection operations like add and remove.
We will use a model collection in order to organize our previously defined Shape objects into a Document:
var Document = Backbone.Collection.extend({ model: Shape });
That’s it! We can now instantiate the Document class and listen to add and remove events as follows:
var document = new Document();
document.bind('add', function(model) { alert('added'); });
document.bind('remove', function(model) { alert('removed'); });
document.add(shape); // fires add event
document.remove(shape); // fires remove event
Views
A Backbone.js view is usually associated with a model (or a model collection). The view is responsible for two things:
- Rendering the model into a DOM element. It listens to model changes and updates the page accordingly.
- Handling the events of this DOM element and updating the model.
In theory, Backbone’s view is actually playing both MVC’s view and MVC’s controller roles as it is handling the user input (DOM events) and updating the model, and also listening to model events and updating the visual part. This doesn’t have a big impact in practice as you would have different methods for each operation kind.
Shape View
In this section we will go through the code of the view of our Shape model. The view manages an html element that represents the shape itself and also ‘control’ elements that decorate the shape and that will allow the user to drag, resize, delete and change the shape’s color.
var ShapeView = Backbone.View.extend({
initialize: function() {
this.model.bind('change', this.updateView, this);
},
render: function() {
$('#page').append(this.el);
$(this.el)
.html('<div class="shape"/>'
+ '<div class="control delete hide"/>'
+ '<div class="control change-color hide"/>'
+ '<div class="control resize hide"/>')
.css({ position: 'absolute', padding: '10px' });
this.updateView();
return this;
},
updateView: function() {
$(this.el).css({
left: this.model.get('x'),
top: this.model.get('y'),
width: this.model.get('width') - 10,
height: this.model.get('height') - 10 });
this.$('.shape').css({ background: this.model.get('color') });
},
events: {
'mousemove' : 'mousemove',
'mouseup' : 'mouseup',
'mouseenter .shape' : 'hoveringStart',
'mouseleave' : 'hoveringEnd',
'mousedown .shape' : 'draggingStart',
'mousedown .resize' : 'resizingStart',
'mousedown .change-color' : 'changeColor',
'mousedown .delete' : 'deleting',
},
hoveringStart: function () {
this.$('.control').removeClass('hide');
},
hoveringEnd: function () {
this.$('.control').addClass('hide');
},
draggingStart: function (e) {
this.dragging = true;
this.initialX = e.pageX - this.model.get('x');
this.initialY = e.pageY - this.model.get('y');
return false; // prevents default behavior
},
resizingStart: function() {
this.resizing = true;
return false; // prevents default behavior
},
changeColor: function() {
this.model.set({ color: prompt('Enter color value', this.model.get('color')) });
},
deleting: function() {
this.remove();
},
mouseup: function () {
this.dragging = this.resizing = false;
},
mousemove: function(e) {
if (this.dragging) {
this.model.setTopLeft(e.pageX - this.initialX, e.pageY - this.initialY);
} else if (this.resizing) {
this.model.setDim(e.pageX - this.model.get('x'), e.pageY - this.model.get('y'));
}
}
});
Don’t be afraid of the length of the code, it’s pretty simple. Here are the most important bits:
- intialize is a special function that is executed on view creation. This is where you usually wire your view to the model by listening to events. In our case, the view registers itself for change events.
- render is also a special function that is executed right after initializing the view. Here, the html element representing the view is initialized and “pushed” –i.e. added– to the DOM. The view’s html element is held into the inherited backbone property el. Line 6, we add el to the page, line 7 we set its html with the shape and controls elements, and finally, line 13 we update the view with the model properties.
- The events hash (line 24) is an important part in our view configuration. It maps events to handler methods. The format is { ‘event selector’ : ‘handler’ }. For example, the pair { ‘mousedown .shape’ : ‘draggingStart’ } means that a mousedown event on an element with class shape will trigger the method draggingStart. The events hash defines how the user input is handled and thus defines the ‘controller’ side of our backbone view.
We have a small technical problem here though. As we did in the previous section Basic user input handling, for a better user experience we should be listening to mousemove and mouseup events on the parent page element and not on the shape’s div itself. The current code works but resizing might a bit choppy if the user moves the mouse too fast. The work around is easy though. It is implemented in the jsfiddle snippet.
Document View
Now we need to add a bit more structure to the shape views by using the document model introduced earlier. The document has also a view that manages all the shape views as follows:
var DocumentView = Backbone.View.extend({
id: 'page',
views: {},
initialize: function() {
this.collection.bind('add', this.added, this);
this.collection.bind('remove', this.removed, this);
},
render: function() {
return this;
},
added: function(m) {
this.views[m.cid] = new ShapeView({
model: m,
id:'view_' + m.cid
}).render();
},
removed: function(m) {
this.views[m.cid].remove();
delete this.views[m.cid];
}
});
The id property indicates the identifier of the DOM element the view is tied to. Backbone will use this value to set the el property accordingly. Since we are using an existing element in the html page, there is nothing to do in the render method.
In the intialize method, the view registers itself for two built in model collection events: add and remove. On the add event, we create and render the view corresponding to the added shape model. We maintain a set of these views in the property views. When the remove event is fired, the shape removed from the document its view is fetched in the views set and removed from the page.
Here is the corresponding jsfiddle snippet.
Conclusion
The source of tutorial is available on github and on jsfiddle. You can also have a look at the online demo.
In this tutorial, we’ve been through some of the aspects of Backbone.js, mainly the MVC and event driven principles it puts in place. As I mentioned in the introduction, we didn’t dive into routing and server communication. Backbone.js has, for instance, built-in support for CRUD operations. I’ll try to cover some of these features in a future post.
Do you have questions or suggestions? Do not hesitate, please put a comment below!
Buckyball
January 12, 2012 at 6:12 pmI guess this saved me hours, u rock!
Waiting for part 2 now…
Haynes
January 12, 2012 at 11:43 pmI keep getting a this.set is not a function.
Even with minimal code
var Shape = Backbone.Model.extend({});
var shape = Shape();
If I remove
var shape = Shape();
it works fine.
Any idea?
Haynes
January 12, 2012 at 11:50 pmNever mind, stupid me forgot the new in:
var shape = new Shape()
ju
January 13, 2012 at 11:25 amThank you very much for this easy and useful explanation.
I’m also waiting to see part 2
Gabriel
January 18, 2012 at 8:15 pmFor the Shape View section I would suggest you show the jsfiddle code & not the code you have there currently along with an explanation about why you’re overriding the mouse event functions with the parent element’s functions. It’s not immediately obvious to people where they should be looking in the jsfiddle code to see the changes.
Dan
January 19, 2012 at 3:22 amThanks for posting this. It’s a great quick tutorial for those of us that just started looking at Backbone.
Andy
January 19, 2012 at 6:26 amThe link in this part:
The work around is easy though. It is implemented in the jsfiddle snippet.
Is wrong, it should be linking to the fourth snippet, not the third.
Thanks for the article though.
Pingback: IMP Links for Web Developers | TekRhythm.com
Chaker Nakhli
January 21, 2012 at 9:26 pm@Andy indeed, the link was wrong. Just corrected it, thank you for spotting this.
Kory
January 23, 2012 at 8:50 amHey, great tutorial! Let see some more
yachris
January 23, 2012 at 9:51 pmHello,
Great tutorial! I think you should talk about the ‘m.cid’ property (the ID automatically assigned to every object by Backbone) that you’re using in the last bit. That’s a very useful way to deal with many objects.
Joe
January 26, 2012 at 8:27 pmCould you explain this part more? I’m not sure what you mean by it, especially binding to #page vs the shape div.
“As a side note, notice that we are listening to mousemove and mouseup events on the parent page element and not on the shape’s div since we want the div to follow (resp. stop following) the mouse position even when it gets off (resp. is up outside of) the shape’s div borders.”
Pingback: Bookmarks for January 25th from 22:15 to 23:50 | gregs
Joe Zim
January 28, 2012 at 8:16 amWow, the more I see tutorials and articles about Backbone.js, the more I wish I had a good project to try it out on. I can’t wait to see the rest of the tutorial. I’ll definitely include a post or two (or more) about Backbone on my blog soon. Joe Zim’s JavaScript Blog
Pingback: 2012년 2월 3일 it 기술 동향 « jangnan
Pingback: Weekend Reading – Java Performance and Architecture, CSS Media, Fluid Design and More « Tales from a Trading Desk
Pingback: Backbone.js | BCSL SOLUTIONS BLOG
sreekanth
February 17, 2012 at 7:19 pmHIII AM NEW TO BACKBONE.JS….PLEASE HELP ME HOW TO GET AM MODEL BY FROM A COLLECTION USEING UNIQUE ATTRIBUTE LIKE ID….PLEASE REPLY…
Diego Gusava
March 20, 2012 at 11:49 amGreat tutorial!!!! Help me a lot!
Aaron Eikenberry
April 26, 2012 at 4:50 pmThanks for this. One question though: Why wouldn’t you put the button behaivors in the documentView as events? It seems like by just making the square and circle button actions jquery events like that, it’s going outside of what backbone aims for.
If I wanted to make those buttons apart of backbone’s events, how could I do it?
Tony
May 22, 2012 at 4:32 pmNice work, can’t wait to see the next one!
Joe
May 31, 2012 at 1:14 amThis is an excellent Backbone example. Love it. Thank You.
Mungo Jerry
June 15, 2012 at 3:27 pmHi, I’d like to see part deux cause there’s not many tutorials on doing the CRUD sync with a server.
Tony
July 7, 2012 at 2:46 pmJust wanted to say thanks for the most comprehensive tutorial I have seen on using Backbone!
Can’t wait for more dude m/
ekeren
August 3, 2012 at 8:32 pmExcellent tutorial, waiting to read your next one.
hoang tran
August 11, 2012 at 3:33 amthanks you
Rich
August 12, 2012 at 8:02 amWell-written, but I think it would have been better to use an easier-to-understand model for beginners to grasp
Senthil Kumar
September 9, 2012 at 1:16 pmFine. It is more useful.
Thanks
Senthil
Evan Hobbs
September 19, 2012 at 10:49 pmOne the clearest and most useful tutorials I’ve seen on backbone.js. I enjoyed it a lot.. Thanks!!
Mahmud
September 21, 2012 at 8:18 pmExcellent tutorial!!! Thank you so much.
Praveen
September 24, 2012 at 7:29 amSimple and very useful . Thanks a lot.
yogesh
October 26, 2012 at 12:58 pmits nice…
i want to ask something like How to attach key event(keyup, keydown) to dynamically created element in backbone ?
i attached click and all other but unable to attach key events.
yogesh
October 27, 2012 at 12:51 pmonly to the dynamically created div in p tag…
Paresh Patel
October 30, 2012 at 12:30 pmExcellent tutorial
ged
October 31, 2012 at 12:13 pmhttp://shapetodo.herokuapp.com/
inspired by this great tutorial & todomvc.com
very thanks
Mohit Jain
December 24, 2012 at 8:42 pmI am writing a series of blog posts for basics of backbone js. It will be great to have your feedback. http://www.codebeerstartups.com/a-complete-guide-for-learning-backbone-js/
Pingback: Простые вещи » Blog Archive » Backbone.js: самые первые материалы для быстрого старта