This is an exercise in building a basic Backbone.js blog site. Backbone is a JavaScript MVC, and creates a lightweight single page app with multiple views. Later in the week, we will add a backend database connection and AJAX data between. For now, we can start with a simple single-page app with no backend and all the backbone code in one JS file.
Objectives:
- Build a backbone app from scratch using the following components
- Model
- Collection
- View
- Router
- Model
- Incorporrate Handlebars templates into the views
- Make an app navigable onclick (vs <a> tags)
template.html has been included to aid in practice and in-class demonstrations
====== ##Models The concepts behind Backbone models and rails models are similar: they are objects that contain data specific to an entity, connected with some sort of backend.
Let's look at a basic model:
var Animal = Backbone.Model.extend({
defaults: {
type: 'animal',
ecosystem: '',
stripes: 0
},
initialize: function() {
alert("I am an animal");
this.on("change:type", function(model) {
var type = model.get("type");
alert("I am now a " + type);
});
}
});
var animal = new Animal();
It is common to define default attributes/parameters, and we can also define values when the objecr is created:
var animal = new Animal({type: 'giraffe', ecosystem: 'savanna'});
We don't set the "stripes" attribute since giraffes don't really have stripes, and that value defaults to 0 (per our defaults object).
We can define an initialize
method that is called whenever a new instance of a model is created (defining this method is not required). Since this method is called each time a new object is instantiated, it is common to declare any event listeners here (i.e. when data is changed).
In this method, we create a handler that is called any time the value of the "type" attribute is changed on the model. Running animal.set({type: "zebra"})
will launch an alert telling us our new type value. Conversely, running animal.set({stripes: 52})
will still set the value "stripes" to 52 on our model, however, the alert is not displayed because we aren't listening to changes on that attribute.
Something we also see in this example is the function model.get()
. This is how we retrieve particular values from a model. In our event listener, we are getting the "type" attribute. Outside this function we can say animal.get("type")
##Collections Backbone collections are basically an ordered set of models. Think of a collection as a grouping of related models. For example, we could have an Animals collection to hold each of our Animal models, or even call it Zoo. A collection is designed to have only one model type, just like a photo album only has photos (in code-speak, the Album collection has Photo models)
The following block defines a Zoo collection and adds animals to the zoo.
var Zoo = Backbone.Collection.extend({
model: Animal
});
var animal1 = new Animal({type: 'giraffe', ecosystem: 'savanna'});
var animal2 = new Animal({type: 'zebra', ecosystem: 'savanna', stripes: 52});
var animal3 = new Animal({type: 'giraffe', ecosystem: 'savanna'});
var myZoo = new Zoo([animal1, animal2, animal3]);
console.log(myZoo.models);
##Views Think of views as a visual representation of a model. Just like in rails, we retrieve models from a database, render them into ERB template files, and display them as HTML. In our class and examples, we generate HTML using handlebars with JSON data from our rails backend.
We can start by creating our main zoo view:
var ZooView = Backbone.View.extend({
el: $('#main'),
initialize: function() {
this.list = $('#animals');
},
render: function() {
this.$el.html($('#zoo-template').html());
this.collection.each(function(model) {
var template = Handlebars.compile($('#animal-template').html());
this.list.append(template(model.toJSON()));
}, this);
return this;
}
});
There are quite a few things defined here. The el:
property defines the main container that holds the view's html. In this case, there is an element such as <div id="zoo"></div>
somewhere in the DOM. This can later be referenced using this.$el
.
Next, we see an initialize
function. This is the same idea as we saw in the model, and this function is called every time a new view is instantiated. In this method, we cache a selector of the animals div (an element such as <div id="animals"></div>
exists somewhere in the DOM) for when we later append each individual animal to the view (remember jQuery selector caching?).
The final step in the process is the render
function. Here, we define how the view's actual display is rendered and appended to the DOM. We start by appending our initial HTML from our zoo-template block to our parent element, then we iterate through our collection of models, compile Handlebars templates for each individual animal, and append the output to the DOM.
Iterating through collections is easy using a method similar to underscore's _.each function. Notice the scope of "this" and explore why "this" is passed into the function. Next, our template is compiled as we explored during our JS templating lesson. Finally, we see this line:
this.list.append(template(model.toJSON()));
We could also break this out into a couple more lines to help us understand better:
var modelData = model.toJSON();
var html = template(modelData);
this.list.append(html);
model.toJSON()
returns a JS object with the model's attributes. This step is important because Handlebars templates expect a simple JS object. calling template(modelData) renders our compiled Handlebars template with our model data into HTML. this.list
refers to our cached $('#animals')
selector, and we're simply calling $.append on that element to add our HTML to the DOM.
When we create a new view, we can pass model and collection data into the constructor:
var zooView = new ZooView({collection: myZoo});
And then render it:
zooView.render();
####Events Backbone has an extensive and easy-to-use event system. Let's redefine our ZooView to have a click event:
var ZooView = Backbone.View.extend({
el: $('#main'),
events: {
"click h1": "headerClick"
},
initialize: function() {
this.list = $('#animals');
},
render: function() {
this.$el.html($('#zoo-template').html());
this.collection.each(function(model) {
var template = Handlebars.compile($('#animal-template').html());
this.list.append(template(model.toJSON()));
}, this);
return this;
},
headerClick: function(event) {
alert("You clicked the header!");
}
});
##Routers Typically, a backbone app has only one router (it can get very complex and difficult to maintain if you have more). It is basically a rails routes file coupled with a controller. A router defines the routes and expected URIs of your app, and applies event handlers when the URL changes.
var AppRouter = Backbone.Router.extend({
routes: {
"": "index",
"animals/:id": "viewAnimal",
"*actions": "defaultRoute"
}
});
var app_router = new AppRouter();
Backbone.history.start();
The routes object is a key:value representation of route:action, just like the rails routes.rb file matches a url to a particular controller#action. The *actions route matches to anything, an index page can be defined by an empty string '', and a variable element in a route can be denoted with a :attribute parameter (animals/:id responds to the urls like /animals/5).
Adding handlers can be done in 2 ways. One is after instantiation using the .on function, and the other is within the object definition.
app_router.on('route:index', function() {
alert("you found the home page");
});
app_router.on('route:viewAnimal', function(id) {
alert("viewing animal " + id);
});
app_router.on('route:defaultRoute', function(actions) {
alert(actions);
});
vs
var AppRouter = Backbone.Router.extend({
routes: {
"": "index",
"animals/:id": "viewAnimal",
"*actions": "defaultRoute"
},
index: function() {
alert("you found the home page");
var zooView = new ZooView({collection: myZoo});
zooView.render();
},
viewAnimal: function(id) {
alert("viewing animal " + id);
},
defaultRoute: function(actions) {
alert("default route!");
}
});
Notice how the variable parameters (:id and *actions) are passed into the event handler as a variable.