I have devised this tutorial which, I hope, will help to bridge that gap and help you to understand why I think Backbone.js is excellent.
Backbone js - what and why?
Let's say you've been asked to make something that's going to use JavaScript. You're maybe a little nervous about the amount of time you have to do it in and you worry because you've seen work you did in the past turn into spaghetti when new requirements appear and when the look and feel of your project suddenly changes.
This is a common situation. Many of us start off with the best intentions when we start to write our code and become frustrated pretty quickly when we find the ideas we carefully laid out in the beginning don't scale well or just can't cope with the sheer variety of different requirements that appear in the lifetime of a project.
Enter Backbone. Backbone provides a framework onto which you can 'pin' your development work. It doesn't have a heavy footprint, weighing in at 3.9 kb and, if you follow its simple rules, you will find that your project gets larger and stays manageable.
Backbone uses MVC, which is a much lauded architectural pattern in software development. MVC stands for Model View Controller. This is not really the place to explain in depth how MVC works, but suffice it to say that MVC divides up what you are trying to do into Views (that which is exposed to the user), Models (managers of state) and Controllers (usually used to manage user interaction). More on MVC later when we actually start to make something.
Tutorial
I think the easiest way to understand the thinking behind Backbone is to actually develop a small web application. I will try to communicate the way Backbone likes to do things and what is expected from you as a developer.
Ok, for our example, we're going to use JavaScript to allow the user to enter whatever they want into a form field and see the results printed out in grey on the same page. We're going to call our application Story Maker. I want to keep the example really simple so that we get the thinking right for when you get asked to do the hard stuff.
Our example does something very simple: when the fields are editing in the top box (which I call 'user details'), the results appear in grey in the bottom box (which I call the 'story')
The markup for the above is nothing special. We simply add two
<div>
tags to the page: one to contain the <input>
boxes and another for the content to get injected into.Here is the complete markup:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Backbone.js for beginners</title>
<style type="text/css">
body{
font-family:Verdana;
margin:30px;
}
label,input{
display:block;
}
label{
font-size:13px;
padding:10px 0 10px 0;
}
.variable_content{
color:#999;
}
#content, #user_details, h1{
border:1px solid #ddd;
margin:20px;
padding:20px;
width:400px;
}
</style>
</head>
<body>
<h1>Story Maker</h1>
<div id="user_details">
<label for="name">Please choose a name</label>
<input type="text" id="name" name="name">
<label for="name">Please choose an animal</label>
<input type="text" id="animal" name="animal">
<label for="name">Please choose an action</label>
<input type="text" id="action" name="action">
</div>
<div id="content"></div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script type="text/javascript" src="https://raw.github.com/documentcloud/underscore/master/underscore.js"></script>
<script type="text/javascript" src="https://raw.github.com/documentcloud/backbone/master/backbone.js"></script>
<script type="text/javascript" src="https://raw.github.com/andyet/ICanHaz.js/master/ICanHaz.min.js"></script>
<script type="text/javascript" src="js/script.js"></script>
<script type="text/html" id="content_tmpl">
<h2><span class="variable_content">{{ name }}</span>'s special day</h2>
<p>One day, <i class="variable_content">{{ name }}</i> was walking down the road whistling a tune when he came upon a <i class="variable_content">{{ animal }}</i>.</p>
<p>"Hello <i class="variable_content">{{ animal }}</i> !" he exclaimed and bent down to <i class="variable_content">{{ action }}</i> it.</p>
</script>
</body>
</html>
The only thing you might notice is the strange script tag at the bottom of this markup. This is an 'icanhaz' template - a simple js templating solution. This contains the markup we want to inject into the 'content' div.
Find out more about Icanhaz here.
We are dealing with content (or data) here, so we want to be able to store it if we have to. In the MVC framework, the 'model' is responsible for handling data and informing views of changes in 'state' of that data. We want to be able to prepare for the possibility that what the user has entered may need to be stored somehow - be it in a database, some JS object or a cookie.
Remembering that Backbone expects us to define views and models in our project, we should think about how we are going to define our views. A view can be any interface that is available to the user - this means that a view could be some audio or visual indicator. In the case of this page, the view could be the whole page, or any small part of the page - you could make the view one input tag if you liked.
In our example, it's probably enough to define two Views:
DetailsEntryView
and StoryView
. These are going to be the the two grey boxes you see above. Often in MVC, the controller is something that the user interacts with - be it a button, a slider or whatever, but in Backbone, this kind of Controller functionality is baked into the View.Let's start by writing our DetailsEntryView:
var DetailsEntryView = Backbone.View.extend({
events: {
'keyup input[type=text]':'setDataInModel'
},
initialize: function() {
this.getDataFromModel();
},
getDataFromModel: function() {
_.each($(this.el).find('input'),function(el){
$(el).val(this.model.get($(el).attr('id')));
},this);
},
setDataInModel: function(e) {
var inpt = e.currentTarget;
this.model.set($(inpt).attr('id'),$(inpt).val());
}
}),
First, we define our Class:
DetailsEntryView
which extends Backbone.View
. Backbone provides this class with a method initialize
but we can override it with our custom method if we want (and we do in this case). When you make an instance of this class, you provide an id of the outermost container of your intended View.
This basically informs this Class that it's not going to be dealing with any elements outside of this area of the DOM.
The
events
object handles any user interaction with this View. It's very simple: rather than using jQuery to bind handlers to events, we use the built-in functionality that Backbone has to offer - you just provide the event, the element you want to target (this element MUST have the element you have specified as the container for this View as an ancestor) and the method you want to run. The method you specify takes the event (
e
) as an argument as it should, and we use that to get the currentTarget
.You obviously can bind your own handlers if you want for more complex stuff (I'm thinking hover here), but for the simple handling we usually have to do it's fine.
We've made a method that gets called when the instance of
DetailsEntryView
is initialized. getDataFromModel
basically populates the input boxes with default values on page load.The
setDataInModel
method we've made is going to set the data in the Model. This is an example of how the Controller part of MVC is integrated into the View in Backbone because, in some implementations of MVC, the View communicated to the Controller and the Controller decides what should be updated in the Model. Backbone lets the View decide what it should send to the Model.
So, when you type a character into any of the input boxes, the
setDataInModel
method gets called and the contents of the input boxes get sent to the Model. Let's make our Model next.
We want a Model to hold our form details. Let's call it
DetailsModel
. Here's the code:
DetailsModel = Backbone.Model.extend({});
That really is all of it. We don't need any extra functionality here for our purposes. When we make an instance of
DetailsModel
, we pass in a data Object that will store our form content like this:
var mdl = new DetailsModel({name:"Harry",animal:"cat",action:"stroke"});
The
name
, animal
and action
properties get put into an attributes object inside our Model. This object can be inspected and modified using the get
and set
methods that are already built in to Backbone's Model functionality.
Knowing what our Model is, we can make an instance of our
DetailsEntryView
and pass in the instance of our Model like this:
new DetailsEntryView({id:'user_details',model:mdl});
This makes sense of the reference to:
this.model
in DetailsEntryView
above. The Model we pass in is set as this.model internally by Backbone so we only have to write minimal code to set things up and Backbone takes care of the rest.
So that all takes care of the data transference and storage of our application. If you needed to, you could give the Model extra responsibility and allow it to store and load the data as you like. Backbone has methods to do that. I hope to make a new tutorial soon that shows all the possibilities that Backbone offers.
The only thing that's left is to output the data from the form back to the screen.
We've already decided that
StoryView
will handle this task. Here is the code:
StoryView = Backbone.View.extend({
initialize: function() {
this.model.bind("change", this.render, this);
this.render();
},
render:function(){
$('#content').empty().append(ich.content_tmpl(this.model.toJSON()));
}
}),
No
events
Object because this is a dumb View in that it never communicates data. (That would be easy to add if, say, there was a requirement to have the text clickable in the output) We do have to add a handler that listens for change in our Model (yes - Views can share Models in MVC) and updates the DOM with our new data. render
does that using the iCanHaz template described above. Notice how we are able to use the this.model.toJSON()
method of the model to get all the content.
Here is the complete JS file:
var DetailsEntryView = Backbone.View.extend({
events: {
'keyup input[type=text]':'setDataInModel'
},
initialize: function() {
this.getDataFromModel();
},
getDataFromModel: function() {
_.each($(this.el).find('input'),function(el){
$(el).val(this.model.get($(el).attr('id')));
},this);
},
setDataInModel: function(e) {
var inpt = e.currentTarget;
this.model.set($(inpt).attr('id'),$(inpt).val());
}
}),
StoryView = Backbone.View.extend({
initialize: function() {
this.model.bind("change", this.render, this);
this.render();
},
render:function(){
$('#content').empty().append(ich.content_tmpl(this.model.toJSON()));
}
}),
DetailsModel = Backbone.Model.extend({});
$(document).ready(function(){
var mdl = new DetailsModel({name:"Harry",animal:"cat",action:"stroke"});
new DetailsEntryView({el:$("#user_details"),model:mdl});
new StoryView({el:$("#content"),model:mdl});
});