Backbone.js by Example - Part 1
Join the DZone community and get the full member experience.
Join For FreeBackbone.js is a Javascript MVC (model-view-controller) library that provides complete support for building clean and easy to maintain client side code. What I like in Backbone.js is its light weight (~4.5kb) and that it doesn’t get in my way by imposing how my application should be architected or organised. Backbone.js is not even restricted to single page interfaces or to Javascript heavy applications. It lays a good foundation that helps writing well structured, easy to maintain and easy to extend client side code.
I started learning Backbone.js a few weeks ago and I fell in love with it. 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 we’ll 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!
Source: http://www.sinbadsoft.com/blog/backbone-js-by-example-part-1/
Published at DZone with permission of Chaker Nakhli. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments