ict.ken.be

 

Backbone.js Fundamentals - Notes

Related Posts

Categories: Javascript Notes

Backbone.js
Liam McLennan

I like the model inheritance and statics of backbone. It is also really nice that you can replace the server-side with a local storage implementation. I do prefer the model binding of knockout however. Just seems more clean then the makes.

1. Introduction
 
Backbone.js gives structure to web applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a REstful JSON interface.
 
It's not a framework and it is not MVC.
 
 
SPA 
Pros: Fast, Highly interactive, Scalable
Cons: Extra work for SEO, Difficult to test, Security issues
 
Server side pages -> Hybrid Ajax pages -> SPAs client side
 
Task board SPA - http://trello.com
 
Required Dependencies
  • Underscore.js
  • jQuery or zepto
  • Zepto lightweight jquery compatible api used for mobile devices. - http://zeptojs.com/  
var book = new Backbone.Model({title:'white tiger', author:'Aravid'});
book.get('title'); // "White tiger"
book.set('title', 'Funny tiger');
book.toJSON();
http://jsfiddle.net/ynkJE/ - preconfigured for backbone testing
(function() {
var Rectangle = Backbone.Model.extend({});
var RectangleView = Backbone.View.extend({
tagName: 'div',
className: 'rectangle',
events: {
'click': 'move'
}
render: function() {
this.setDimensions();
this.setPosition();
return this; //backbone convention
},
setDimensions: function() {
this.$el.css({
width: this.model.get('width') + 'px',
height: this.model.get('height') + 'px'
});
},
setPosition: function() {
var position = this.model.get('position');
this.$el.css({
left: position.x,
right: position.y
});
},
move: function() {
this.$el.css('left', this.$el.position().left + 10);
}
});
 
var myRectangle = new Rectangle({
width:100, height:60, position: { x:300, y:150 }
});
 
var myView = new RectangleView({ model: myRectangle });
$('div#canvas').append(myView.render().el);
 
//when creating an array
//_(models).each(function(model) {
//$('div#canvas').append(new RectangleView({model:model}).render().el);
//});
})(); 
2. Models
var Vehicle = Backbone.Model.extend(
{
defaults: {
'color': 'white',
'type': 'car'
}
initialize: function() {
console.log('this is a contructor');
}
},
{
summary: function() {return 'a static function';}
}
);
console.log(Vehicle.summary()):
var car = new Vehicle(); //will run constuctor
var SuperCar = Vehicle.extend({});
var superCar = new SuperCar();
console.log(superCar instanceof SuperCar); //true
console.log(superCar instanceof Vehicle); //true
console.log(superCar instanceof Backbone.Model); //true
 
ford.set({
'maximumSpeed': '99',
'color':'blue',
'description', '<script>injection</script>'
});
ford.escape('description');
ford.has('type'); //check if attribute defined 
Models raise events when their state changes, detect by listening to change event.
ford.on('change', function () {}); //listen to change of object
ford.on('change:color', function () {}); //listen to change of property
Custom model events are identified by string identifiers.
var volcano = _.extend({}, Backbone.Events);
volcano.on('disaster:eruption', function(options){
console.log('duck and cover - ' + options.plan);
}); //namespacing events
volcano.trigger('disaster:erruption', {plan:'run'});
volcano.off('disaster:erruption');
 
var ford = new Backbone.Model({});
console.log(ford.id) //undefined, because not saved to server yet
console.log(ford.cid) //c0, temp id
console.log(ford.isNew()); //true
Validation is called prior to set and save operations.
var Vehicle = Backbone.Model.extend(
{
validate: function (attrs) {
var validColors = ['white','red'];
var colorIsValid = function (attrs)
{
if (!attrs.color) return;
return _(validColors).include(attrs.color); 
}
if (!colorIsValid(attrs)) {
return "color must be one of: " + validColors.join(",");
}
}
});
var car = new Vehicle();
car.on('error', function (model,error){
console.log(error);
});
car.set({'color','green'}, {
error: function (error) {
alert(error);
}
});
var attrs = ford.toJSON(); //converts a model's attributes to a javascript object
console.log(JSON.stringify(attrs)); //string representation
3. Views
  • DOM - View - Model : glue between models and documents
var V = Backbone.View.extend({
tagName:'li',
id:'thing',
className:'active',
attributes: {
'data-value': 12345
}
});
var v = new V();
$('body').prepend(v.el);
var V = Backbone.View.extend({});
var v = new V({el: '#test'});
v.$el.css('background-color','Yellow');
 
var v = new Backbone.View({el:'body'});
v.el // <body></body> the dom element
v.$el // cached $(this.el) [<body></body>] the jquery wrapper of that element
this.$ // this.$('selector') = this.$el.find('selector') scoped to current view 
 
var RefreshingView = Backbone.View.extend({
initialize:function() {
this.model.on('change', function() { this.render(); }, this);
},
render: function() {
this.$el.html(this.model.get('text'));
}
});
var m = new Backbone.Model({text:new Date().toString()});
var v = new RefreshingView({model:m, el:'body'});
v.render();
setInterval(function() { m.set({text:new Date().toString());},1000);
 
//<h3 class="not-imortant">first version</h3>
var el = new Backbone.View().make('h3', { class: 'not-important' } , 'first version'); 
 
v.remove() //$el.remove();  to remove view from DOM
 
//this.$('.clickable').click(handleClick);
var FormView = Backbone.View.extend({
events: {
'click .clickable': 'handleClick'
},
handleClick: function() {}
});
Guidelines
  • Do not attach to existing elements
  • Do not access DOM elements the view does not own
  • Pass el to the constructor of self-updating view
 
4. Templating
 
Client-side Templating
 
Underscore.js
<% ... %> //exececute arbitrary code 
<%= ... %> //evaluate an expression and render the result inline
<%- ... %> //evaluate an expression and render the html escaped result inline
<script id="latlon-template" type="text/html">
<%=lat%> <%=lon%>
<% ([1,2,3]).each(function(number) {%>
<p><%=number%></p>
<% }); %>
</script>
var V = Backbone.View.Extend({
render: function() {
var data = { lat:-27, lon=123 };
var template = $('#latlon-template').html();
this.$el.html(_.template(template,data));
return this;
}
}):
var v = new V({el:'body'});
v.render();
 
var compiled = _.template(source);
 Handlebars is templating engine based on mustache. (no code philosophy)
{{lat}} {{lon}}
{{#each numbers}}
<p>{{this}}</p>
{{/each}}
var compiled = Handlebars.compile(source);
var rendered = compiled({lat:-27}); 
Server-side Templating
handlebars <input> -f <output> //pre-compile file based templates
var rendered = Handlebars.templates.list(data);
 
5. Routing 
  • Don't use routing like mvc controllers!
var DocumentRouter = Backbone.Router.extend({
routes: {
'contents' : 'contents'
'view/:title': 'viewDocument'
},
contents: function(){
$('body').html(new ContentsView({collection:documents}).render().el);
}
viewDocument: function (title) {
var selectedDocument = _(documents).find(function (document) {
return document.get('title') == title;
}
$('body').empty().append(new DocumentView({model: selectedDocument}).render().el);
}
});
var router = new DocumentRouter();
Backbone.history.start();
router.navigate('contents', {trigger:true});
eventAggregator.on('document:selected', function(document){
var urlPath = 'view/' + document.get('title');
router.navigate(urlPath, {trigger:true});
}
window.history.pushState(...); //html5 history api, else backbone will use hash fragments
http://localhost/search/cats vs http://localhost/#search/cats
 
SEO
  1. Render content on the server
  2. #! Urls 
 
Backbone.history.start({pushState:false});
router.navigate('!search/cats', {trigger:true});
// generated url -> /#!search/cats
// google converts to -> /_escaped_fragment_=search/cats which will need server rendered content 
https://developers.google.com/webmasters/ajax-crawling/docs/specification
 
6. Collections 
  • Container for multiple models of the same type
  • Retrieve models from the server
  • Create models and save them to the server
  • Group models by some attribute
  • Collection is an array-like object (can't use square brackets)
var c = new Backbone.Collection([
{name:'thing'},
{name:'other'}
]);
console.log(c.length); //2
console.log(c.at(0)); // thing
 
var Vehicles = Backbone.Collection.extend({
model:vehicle,
comparator: function(vehicle) {
return vehicle.get('sequence');
}
comparator: function(vehicle1, vehicle2) {
return vehicle1.get('sequence') < vehicle2.get('sequence') ? -1 : 1;
}
});
var vehicles = new Vehicles(...);
vehicles.add({name:'Fred', age:6}); //will convert to backbone model
vehicles.remove(vechicles.at(1));
vehicles.on('add', function (model, col, options) { ... options.index ... };
vehicles.add({name:'Fred2', age:6}, {at:2, silent:true}); 
var v1 = vehicles.get(1);
var v2 = vehicles.getByCid('c0');
 
//proxy from underscore.js
collection.forEach(function (item { print(item); });
collection.forEach(print);
collection.map(function(item) { return transform(item); });
collection.reduce(function(memo,item) { return memo + item.get('age'); }, 0 );
var dave = collection.find(function(model) { return model.get('name') == 'Dave'; });
collection.on('remove', function(model, collection) {});
collection.on('change:name', function(model, options) {});
7. Connecting to a Server
  • CORS - Cross-origin resource sharing 
  • Origin is application layer protocol + domain name + port number
  • Cors is an alternative to jsonp (jsonp only has GET verb)
  • Cors supports all verbs but needs modern browser (IE10)
var MyModel = Backbone.Model.extend({
url: 'http://tra.la/mymodel'
});
myModelInstance.save({}, {
success: function() {}, //response will give id
error: function() {}
});
https://github.com/liammclennan/backbone-server (with node.js)
people.create({name:"Tom", age:50}); //http post to insert a model
people.fetch() //get array of models that must have an id
var person = new Person({id:0});
people.add(person); //will get url from the collection
person.fetch({ success: function() {...} });
person.set('age',18);
person.save();
person.destroy();
Backbone.sync
  • A function that interfaces between backbone and the server
  • Implements crud behavior and can be overridden globally, per collection, or per model
 
Backbone.localStorage
  • Sets backbone.sync to local browser storage
 
8. Testing with Jasmine (instead of Mocha or qUnit)
  • Test model specifications
  • Test views (rendered elements, raised events)
  • Testing routes (difficult, so keep routing easy)
 
beforeEach(function(){
rectangle = new app.Rectangle();
});
 
describe('some context', function(){
it('should show some observable behavior', function() {
//assert expectations here
expect(rectangle.area()).toBe(28);
expect(mySetFunction).toThrow(); // will give error
expect(rectangleView.el.tagName).toBe('DIV');
expect(rectangleView.$el.hasClass('rectangle')).toBe(true);
});
});
Testing without a browser
 
  • For QUnit you can use http://phantomjs.org/ 
  • (you can also use this to take screenshots)
  • phantomjs.exe run-jasmine-fixed.js SpecRunner.html //0 success 1 failure