On Github nisheed2440 / bbg-revealjs
By: Nisheed Jagadish
MVC is an architectural design pattern that encourages improved application organization through a separation of concerns. It enforces the isolation of business data (Models) from user interfaces (Views), with a third component (Controllers) traditionally managing logic, user-input, and coordination of Models and Views.
All the time!! No, Seriously.
Backbone is more of an MV* framework where MV* is actually Models Views Collection and Routers. Its different from frameworks like Angular as the binding of the data with the view is done within the View itself.
Here Collections are a set of Models which represents a grouping of similar data.
Routers help set chages in context of the pages via URL hash changes or window.history.pushstate changes.
//A simple person object var Person = function(config){ this.name = config.name; this.job = config.job; return this; } //Assuming every person works Person.prototype.work = function(){ return this.name + ' is currently working as a ' + this.job; }
//A simple person model var Person = Backbone.Model.extend({ defaults:{ name:'', job:'' }, /*Constructor function */ initialize:function(){ console.log('Model has been initialized'); this.work(); }, work:function(){ console.log(this.get('name') + ' is currently working as a ' + this.get('job')); } });
var Person = Backbone.Model.extend({}); var person = new Person(); person.set('name','Nisheed'); console.log(person.get('name')); person.set({ name:'Jagadish', job:'Web Developer' }); console.log(person.toJSON());
The Problem
var Person = Backbone.Model.extend({ defaults:{ name : '', age:0 } }); var person = new Person(); person.set({ age:-1 }); console.log(person.toJSON());
We know that every person should have a name and a postive age.
The Solution "model.validate" function
var Person = Backbone.Model.extend({ defaults:{ name:'', age:0 }, validate:function(attr){ if(!$.trim(attr.name)){ return 'Please specify a name!!!'; } if(attr.age <= 0){ return 'Please specify a non negative number'; } }, initialize:function(){ //Model is initialized } });
NOTES model.set method does not involke the validate method by default. In order to validate the model on set, pass the "validate" attribute "true" in the options
person.set({name:'Nisheed',age:-5},{validate:true});
var Person = Backbone.Model.extend({ defaults:{ name:'', age:0 }, validate:function(attr){ if(!$.trim(attr.name)){ return 'Please specify a name!!!'; } if(attr.age <= 0){ return 'Please specify a non negative number'; } }, initialize:function(){ //Model is initialized this.on('invalid',function(model,error){ console.log(error); }); } });
var PersonView = Backbone.View.extend({ tagName:'li', /*Constructor for the View*/ initialize:function(){ console.log('View has been initialized'); }, render:function(){ this.$el.html('A Person'); return this; } });
/*Constructor for the View*/ initialize:function(){ console.log('View has been initialized'); this.collection = new SampleCollection(); this.render(); } ...
render:function(){ //what the hell is $el!! var html = this.template(someJSONData); this.$el.html(html); return this; } ...
/*Model Person*/ var Person = Backbone.Model.extend({}); /*View Person*/ var PersonView = Backbone.View.extend({ tagName:'li', render:function(){ var innerHtml = this.model.get('name') + ' is currently working as ' + this.model.get('job'); this.$el.html(innerHtml); return this; } }); var person = new Person({name:'Nisheed',job:'Web Developer'}); var personView = new PersonView({model:person}); console.log(personView.render().el);
... render:function(){ /*inline non re usable template */ var innerHtml = this.model.get('name') + ' is currently working as ' + this.model.get('job'); this.$el.html(innerHtml); return this; } ...
/*Model Person*/ var Person = Backbone.Model.extend({}); /*View Person*/ var PersonView = Backbone.View.extend({ tagName:'li', template: _.template('<%= name %> is currently working as a <%= job %>'), render:function(){ var innerHtml = this.template(this.model.toJSON()); this.$el.html(innerHtml); return this; } }); var person = new Person({name:'Nisheed',job:'Web Developer'}); var personView = new PersonView({model:person}); console.log(personView.render().el);
Create a script tag in the html of type "text/template" and id "ext-template" and paste the following markup
<%= name %> is currently working as a <%= job %>
then the only change would be as follows
/*Model Person*/ var Person = Backbone.Model.extend({}); /*View Person*/ var PersonView = Backbone.View.extend({ tagName:'li', template: _.template($('#ext-template').html()), //<<<<<<<<<<<< render:function(){ var innerHtml = this.template(this.model.toJSON()); this.$el.html(innerHtml); return this; } }); var person = new Person({name:'Nisheed',job:'Web Developer'}); var personView = new PersonView({model:person}); console.log(personView.render().el);
Consider the following senario
var Person = Backbone.Model.extend({}); var PersonListItem = Backbone.View.extend({ tagName:'li', render:function(){ this.$el.append('Name:' + this.model.get('name') + '<br> Age:' + this.model.get('age')); return this; } }); /* In order to create many people we would have to instantiate the Person Model in the following way */ var personOne = new Person({name:'John',age:23}); var personTwo = new Person({name:'Doe',age:60}); .... var personN = new Person({name:'PersonN',age:20}); /* And in order to create views you would have to something like..*/ var personViewOne = new PersonView({model:personOne}); var personViewTwo = new PersonView({model:personTwo}); ... var personViewN = new PersonView({model:personN});
This is obviously not the way to go forward.
var Person = Backbone.Model.extend({ idAttribute:'age' }); var People = Backbone.Collection.extend({ model:Person, initialize:function(){ this.on('add',function(model){ console.log('Added model data to collection'); }); this.on('remove',function(model){ console.log('Deleted model from collection'); }); } }); var people = new People(); people.add([{name:'John',age:23},{name:'Doe',age:60}]); console.log(people.toJSON()); people.remove(people.get(23)); console.log(people.toJSON());
var Person = Backbone.Model.extend({}); var People = Backbone.Collection.extend({ model:Person, initialize:function(){ this.on('add',function(model){ console.log('Added model data to collection'); }); this.on('remove',function(model){ console.log('Deleted model from collection'); }); } }); var PersonView = Backbone.View.extend({ tagName:'li', render:function(){ this.$el.html('Name:' + this.model.get('name') + '<br> Age:' + this.model.get('age')); return this; } }); /*Collection View*/ var PeopleView = Backbone.View.extend({ tagName:'ul', render:function(){ this.collection.each(function(person){ this.addOnePerson(person); },this); return this; }, addOnePerson:function(person){ var personView = new PersonView({model:person}); this.$el.append(personView.render().el); return; } }); //Static data var peopleData = [{name:'John',age:23},{name:'Doe',age:60}]; var people = new People(); people.add(peopleData); var peopleView = new PeopleView({collection:people}); $(document.body).append(peopleView.render().el);
... template:_.template($('#ext-template').html()), ...
/*In the app.js global*/ (function(){ window.templateHelper = function(tplId){ var $tpl = $('#' + tplId); if($tpl.length){ return _.template($tpl.html()); } else { return _.template(''); } } })(); /*in the view we can then use*/ ... template: templateHelper('ext-template'), ...
Till now we have been polluting the global scope by using
var Person = Backbone.Model.extend({}); var People = Backbone.Collection.extend({ model:Person, }); var PeopleView = Backbone.View.extend({ tagName:'li' render:function(){} }); /* ALL THE ABOVE STATEMENTS WILL CREATE OBJECTS ON THE GLOBAL SCOPE*/
Create a global app namespace and then create Model,Views and Collections under it
/* In the app.js global*/ (function(){ window.bbg = {}; // bbg stands for backbone guide //We can then create objects under bbg as follows bbg.Person = Backbone.Model.extend({}); bbg.People = Backbone.Collection.extend({ model:bbg.Person, }); bbg.PeopleView = Backbone.View.extend({ tagName:'li' render:function(){} }); })();
Solution to simple namespacing complications - make it a tad bit modular
/* In the app.js global*/ (function(){ window.bbg = {}; // bbg stands for backbone guide bbg.Models = {}; bbg.Views = {}; bbg.Collections = {}; //We can then create objects under bbg as follows bbg.Models.Person = Backbone.Model.extend({}); bbg.Collections.People = Backbone.Collection.extend({ model:bbg.Models.Person }); bbg.Views.Person = Backbone.View.extend({ tagName:'li' render:function(){} }) })();
(function(){ window.bbg = {}; bbg.Models = {}; bbg.Views = {}; bbg.Collections = {}; //We can then create objects under bbg as follows bbg.Models.Person = Backbone.Model.extend({}); bbg.Collections.People = Backbone.Collection.extend({ model:bbg.Models.Person }); bbg.Views.Person = Backbone.View.extend({ tagName:'li', events:{ 'click .edit':function(e){ alert( this.model.get('name') + ' edit clicked !!!'); } }, template: _.template('<%= name %> is aged <%= age %> years <button class="edit">EDIT</button> <button class="delete">DELETE</button>'), render:function(){ this.$el.on('click','.delete',_.bind(function(e){ alert( this.model.get('name') + ' delete clicked !!!'); },this)); this.$el.append(this.template(this.model.toJSON())); return this; } }); bbg.Views.People = Backbone.View.extend({ tagName:'ul', render:function(){ this.collection.each(function(person){ var personView = new bbg.Views.Person({model:person}); this.$el.append(personView.render().el); },this); return this; } }); })(); var peopleData = [{name:'John',age:23},{name:'Doe',age:60}]; var people = new bbg.Collections.People(peopleData); var peopleView = new bbg.Views.People({collection:people}); $(document.body).append(peopleView.render().el);
(function(){ window.bbg = {}; bbg.Models = {}; bbg.Views = {}; bbg.Collections = {}; //We can then create objects under bbg as follows bbg.Models.Person = Backbone.Model.extend({ initialize:function(){ this.on('change',function(model){ console.log('Model updated'); }); } }); bbg.Collections.People = Backbone.Collection.extend({ model:bbg.Models.Person }); bbg.Views.Person = Backbone.View.extend({ tagName:'li', events:{ 'click .edit':function(e){ var newAge = prompt('Edit ' + this.model.get('name') + '\'s' + ' age',this.model.get('age')); this.model.set('age',newAge); } }, template: _.template('<%= name %> is aged <%= age %> years <button class="edit">EDIT</button> <button class="delete">DELETE</button>'), render:function(){ this.$el.on('click','.delete',_.bind(this.deleteOne,this)); this.$el.append(this.template(this.model.toJSON())); return this; }, deleteOne:function(){ Backbone.Events.trigger('model:delete',[this.model]); } }); bbg.Views.People = Backbone.View.extend({ tagName:'ul', initialize:function(){ this.collection.on('change', _.bind(this.render, this)); Backbone.Events.on('model:delete',_.bind(function(model){ this.collection.remove(model); this.render(); },this)); }, render:function(){ this.$el.empty(); this.collection.each(function(person){ var personView = new bbg.Views.Person({model:person}); this.$el.append(personView.render().el); },this); return this; } }); })(); var peopleData = [{name:'John',age:23},{name:'Doe',age:60}]; var people = new bbg.Collections.People(peopleData); var peopleView = new bbg.Views.People({collection:people}); $(document.body).append(peopleView.render().el);
(function(){ window.bbg = {}; bbg.Models = {}; bbg.Views = {}; bbg.Collections = {}; //We can then create objects under bbg as follows bbg.Models.Person = Backbone.Model.extend({ initialize:function(){ this.on('change',function(model){ console.log('Model updated'); }); } }); bbg.Collections.People = Backbone.Collection.extend({ model:bbg.Models.Person }); bbg.Views.Person = Backbone.View.extend({ tagName:'li', events:{ 'click .edit':function(e){ var newAge = prompt('Edit ' + this.model.get('name') + '\'s' + ' age',this.model.get('age')); this.model.set('age',newAge); } }, template: _.template('<%= name %> is aged <%= age %> years <button class="edit">EDIT</button> <button class="delete">DELETE</button>'), render:function(){ this.$el.on('click','.delete',_.bind(this.deleteOne,this)); this.$el.append(this.template(this.model.toJSON())); return this; }, deleteOne:function(){ Backbone.Events.trigger('model:delete',[this.model]); } }); bbg.Views.People = Backbone.View.extend({ tagName:'ul', events:{ 'click .add':'addOne' }, initialize:function(){ this.collection.on('change', _.bind(this.render, this)); this.collection.on('add',_.bind(this.render, this)); Backbone.Events.on('model:delete',_.bind(function(model){ this.collection.remove(model); this.render(); },this)); }, render:function(){ this.$el.empty(); this.collection.each(function(person){ var personView = new bbg.Views.Person({model:person}); this.$el.append(personView.render().el); },this); this.$el.append('<button class="add">ADD</button>') return this; }, addOne:function(){ var name = prompt('Enter name'); var age = prompt('Enter age'); if(name && age){ this.collection.add({name:name,age:age}); } } }); })(); var peopleData = [{name:'John',age:23},{name:'Doe',age:60}]; var people = new bbg.Collections.People(peopleData); var peopleView = new bbg.Views.People({collection:people}); $(document.body).append(peopleView.render().el);
In Backbone, routers provide a way for you to connect URLs (either hash fragments, or real) to parts of your application. Any piece of your application that you want to be bookmarkable, shareable, and back-button-able, needs a URL.
eg.
http://example.com/#about http://example.com/#search/seasonal-horns/page2
An application will usually have at least one route mapping a URL route to a function that determines what happens when a user reaches that route. This relationship is defined as follows:
'route' : 'mappedFunction'
var PeopleRouter = Backbone.Router.extend({ /* define the route and function maps for this router */ routes: { "about" : "showAbout", /* Sample usage: http://example.com/#about */ "people/:id" : "getPerson", /* This is an example of using a ":param" variable which allows us to match any of the components between two URL slashes */ /* Sample usage: http://example.com/#people/5 */ "search/:name" : "searchPeople", /* We can also define multiple routes that are bound to the same map function, in this case searchPeople(). Note below how we are optionally passing in a reference to a age number if one is supplied */ /* Sample usage: http://example.com/#search/nisheed */ "search/:name/:age" : "searchPeople", /* As we can see, URLs may contain as many ":param"s as we wish */ /* Sample usage: http://example.com/#search/nisheed/27 */ "people/:id/download/*documentPath" : "downloadDocument", /* This is an example of using a *splat. Splats are able to match any number of URL components and can be combined with ":param"s*/ /* Sample usage: http://example.com/#people/5/download/files/Resume.doc */ /* If you wish to use splats for anything beyond default routing, it is probably a good idea to leave them at the end of a URL otherwise you may need to apply regular expression parsing on your fragment */ "*other" : "defaultRoute", /* This is a default route that also uses a *splat. Consider the default route a wildcard for URLs that are either not matched or where the user has incorrectly typed in a route path manually */ /* Sample usage: http://example.com/# <anything> */ "optional(/:item)": "optionalItem", "named/optional/(y:z)": "namedOptionalItem" /* Router URLs also support optional parts via parentheses, without having to use a regex. */ }, showAbout: function(){ }, getPerson: function(id){ /* Note that the id matched in the above route will be passed to this function */ console.log("You are trying to reach person " + id); }, searchPeople: function(name, age){ var age = age || 1; console.log(name + ' with the age ' + age + ' passed'); }, downloadDocument: function(id, path){ console.log(id,path); }, defaultRoute: function(other){ console.log('Invalid. You attempted to reach:' + other); } }); /* Now that we have a router setup, we need to instantiate it */ var myPeopleRouter = new PeopleRouter();
Next, we need to initialize Backbone.history as it handles hashchange events in our application. This will automatically handle routes that have been defined and trigger callbacks when they’ve been accessed.
The Backbone.history.start() method will simply tell Backbone that it’s okay to begin monitoring all hashchange
If you would like to update the URL to reflect the application state at a particular point, you can use the router’s .navigate() method.
It is also possible for Router.navigate() to trigger the route along with updating the URL fragment by passing the trigger:true option
GET POST PUT AND DELETE
Browsers are limited in how many parallel requests they can make, so often it’s slow to load multiple files, as it can only do a certain number at a time. This number depends on the user’s settings and browser, but is usually around 4-8. When working on Backbone applications it’s good to split your app into multiple JS files, so it’s easy to hit that limit quickly. This can be negated by minifying your code into one file as part of a build process, but does not help with the next point.
Scripts are loaded synchronously. This means that the browser cannot continue page rendering while the script is loading, .
Loading the scripts asynchronously means the load process is non-blocking. The browser can continue to render the rest of the page as the scripts are being loaded, speeding up the initial load time.
We can load modules in more intelligently, having more control over when they are loaded and ensuring that modules which have dependencies are loaded in the right order.
// A module ID has been omitted here to make the module anonymous define(['foo', 'bar'], // module definition function // dependencies (foo and bar) are mapped to function parameters function ( foo, bar ) { // return a value that defines the module export // (i.e the functionality we want to expose for consumption) // create your module here var myModule = { doStuff:function(){ console.log('Yay! Stuff'); } } return myModule; });
Include the require.js library in the html using script tag.
pass the data-main attribute in the script tag as the main app file to be loaded.
In the main JS file that you load you can then configure require JS as follows
require.config({ // your configuration key/values here baseUrl: "app", // generally the same directory as the script used in a data-main attribute for the top level script paths: {}, // set up custom paths to libraries, or paths to RequireJS plugins shim: {}, // used for setting up all Shims (see below for more detail) });
require.config({ shim: { 'lib/underscore': { exports: '_' }, 'lib/backbone': { deps: ['lib/underscore', 'jquery'], exports: 'Backbone' } } });
require( 'lib/backbone', function( Backbone ) {...} );