Backbone is becoming wildly popular as a web application development framework. Along with this popularity comes countless extensions and plugins to enhance the power of the framework, and fill in holes that other felt needed filling. Let’s take a look at some of the best choices.
Developed by Derick Bailey, this extension is quite large and is my personal favorite. Rather than adding one or two features to Backbone, Derick decided to fill in all the biggest holes that he felt existed. Here’s what he says about it in the readme file of the GitHub project.
“Backbone.Marionette is a composite application library for Backbone.js that aims to simplify the construction of large scale JavaScript applications. It is a collection of common design and implementation patterns found in the applications that I (Derick Bailey) have been building with Backbone, and includes various pieces inspired by composite application architectures, such as Microsoft’s “Prism” framework.”
Let’s take a closer look at what Marionette provides us with:
- Application: This is a central object that everything in your application can communicate through. It offers a way to set up the main view(s) of your application quickly and easily, a central event hub that every module in your application can communicate through so they aren’t dependent on one another, and initializers for fine-grained control of how your application boots up.
- Modules: A means of encapsulating module code and namespacing those modules on the central application object.
- Views: New view classes to extend that offer native methods for cleaning up, so you don’t end up with memory leaks. It also contains rendering boilerplate; for simple views, simply specify the template and model, and it’ll handle the rest.
- Collection/Composite Views: This is where the “composite application library” bit comes into play. These two views allow you to easily create views that can handle the process of rendering all the views in a collection, or even a nested hierarchy of collections and models, with very little effort.
- Regions and Layouts: A region is a object that can handle all the work of rendering, unrendering, and closing views for a particular place in the DOM. A Layout is simply a normal view that also has regions built into it for handling subviews.
- AppRouter: A new type of router that can take a controller to handle the workload so that the router can just contain the configuration of the routes.
- Events: Extended from the Wreqr project, Marionette makes Backbone’s events even more powerful for creating large-scale event-based applications.
I’ve only scratched the surface of what Marionette can do. I definitely recommend heading over to GitHub and reading their documentation on the Wiki.
Additionally, Andrew Burgess covers Marionette in his Tuts+ Premium Advanced Backbone Patterns and Techniques course.
Backbone.Validation is designed to fill a small niche of a problem: namely, model validation. There are several validation extensions for Backbone, but this one seems to have garnered the most respect from the community.
Rather than actually having to write a validate method for your models, you can create a validation property for your models, which is an object that specifies each of the attributes that you wish to validate and a list of validation rules for each of those attributes. You can also specify error messages for each attribute and pass in custom functions for validating a single attribute. You can see an example below, which is modified from one of the examples on their website.
var SomeModel = Backbone.Model.extend({ validation: { name: { required: true }, 'address.street': { required: true }, 'address.zip': { length: 4 }, age: { range: [1, 80] }, email: { pattern: 'email', // supply your own error message msg: "Please enter a valid email address" }, // custom validation function for `someAttribute` someAttribute: function(value) { if(value !== 'somevalue') { return 'Error message'; } } } });There are countless built-in validators and patterns that you can check against, and there’s even a way to extend the list with your own global validators. This Backbone plugin doesn’t quite make validation fun, but it certainly eliminates any excuses for not adding in validation. Please visit their site for more examples and a deeper explanation for how to use this wonderful tool.
Backbone.LayoutManager is all about making Backbone’s Views better. Like Backbone.Marionette, it brings in cleanup code to prevent memory leaks, handles all of the boilerplate, and leaves you with just configuration and application-specific code. Unlike Marionette, LayoutManager focuses specifically on Views.
Because LayoutManager focuses solely on Views, it can do more for the views than Marionette does. For instance, LayoutManager is capable of doing asynchronous rendering, in the case that you want to dynamically load your templates from external files.
LayoutManager also can handle subviews, though in a very different way from Marionette. Either way, though, it is largely configuration, which makes things extremely simple and eliminates 90% of the work that you would have needed to do, if you were trying to implement this all on your own. Take a look below for a simple example for adding three subviews to a view:
Backbone.Layout.extend({ views: { "header": new HeaderView(), "section": new ContentView(), "footer": new FooterView() } });As usual, be sure to refer to the GitHub page and documentation to learn more.
Backbone.ModelBinder creates links between the data in your models and markup shown by your views. You can already do this by binding to the change event on your models and rendering the view again, but ModelBinder is more efficient and simpler to use.
Take a look at the code below:
var MyView = Backbone.View.extend({ template: _.template(myTemplate), initialize: function() { // Old Backbone.js way this.model.on('change', this.render, this); // or the new Backbone 0.9.10+ way this.listenTo(this.model, 'change', this.render); }, render: function() { this.$el.html(this.template(this.model.toJSON())); } });The problem with this approach is that any time a single attribute is changed, we need to re-render the entire view. Also, with every render, we need to convert the model to JSON. Finally, if want the binding to be bi-directional (when the model updates, so does the DOM and vice versa), then we need to add in even more logic to the view.
This example was using Underscore’s template function. Let’s assume the template that we passed into it looks like so:
<form action="..."> <label for="firstName">First Name</label> <input type="text" id="firstName" name="firstName" value="<%= firstName %>"> <label for="lastName">Last Name</label> <input type="text" id="lastName" name="lastName" value="<%= lastName %>"> </form>Using the tags <%= and %> make the template function replace those areas with the firstName and lastName properties that exist in the JSON that we sent in from the model. We’ll assume that the model has both of those attributes, too.
With ModelBinder, instead we can remove those templating tags and send in normal HTML. ModelBinder will see the value of the name attribute on the input tag, and will automatically assign the model’s value for that property to the value attribute of the tag. For example, the first input‘s name is set to “firstName”. When we use ModelBinder, it’ll see that and then set that input‘s value to the model’s firstName property.
Below, you’ll see how our previous example would look after switching to using ModelBinder. Also, realize that the binding is bi-directional, so if the input tags are updated, the model will be updated automatically for us.
HTML Template:
<form action="..."> <label for="firstName">First Name</label> <input type="text" id="firstName" name="firstName"> <label for="lastName">Last Name</label> <input type="text" id="lastName" name="lastName"> </form>JavaScript View:
var MyView = Backbone.View.extend({ template: myTemplate, initialize: function() { // No more binding in here }, render: function() { // Throw the HTML right in this.$el.html(this.template); // Use ModelBinder to bind the model and view modelBinder.bind(this.model, this.el); } });Now we have clean HTML templates, and we only need a single line of code to bind the view's HTML and the models together using modelBinder.bind. modelBinder.bind takes two required arguments and one optional argument. The first argument is the model with the data that will be bound to the DOM. The second is the DOM node that will be recursively traversed, searching for bindings to make. The final argument is a binding object that specifies advanced rules for how bindings should be done, if you don't like the default usage.
ModelBinder can be used on more than just input tags. It works on any element. If the element is some type of form input, such as input, select, or textarea, it'll update the values of those element, accordingly. If you bind to an element, such as a div or span, it will place the model's data between the opening and closing tags of that element (e.g. <span name="firstName">[data goes here]<span>).
There's plenty more power behind Backbone.ModelBinder than what I've demonstrated here, so be sure to read the documentation on the GitHub repository to learn all about it.
Conclusion
That wraps things up. I've only covered a handful of extensions and plugins, but these are what I consider to be of the most use.
The Backbone landscape is constantly changing. If you'd like to view a comprehensive list of the various Backbone extensions that are available, visit this wiki page, which the Backbone team regularly updates.
In my last article on the “How to build a large, single-page javascript application” series we used the JavascriptMVC framework to build Rebecca Murphey’s javascript’s community challenge application - srchr. In this article we’ll follow the same coding pattern to show how to do the same using Backbone.js as the base javascript MVC framework instead.
You can download all the project’s code from here and see a working demo by clicking this link. You can also clone this github repo and compare the different srchr implementations by jumping between “extjs”, “javascriptmvc” and “backbone” branches.
Following the same workflow as in the two previous articles let’s start by breaking down the application into a set of smaller backbone.js components and then work on each of the component event-driven interactions at a time.
![]()
Designing the Backone.js application
Let’s start by breaking down the application into a set of maneageable backbone.js views:
![]()
And this is the folder organization for our backbone application:
![]()
Since we’re going to use requirejs as the dependency management tool for this project I’ve decided to use the AMD enabled version of both the underscore and backbone libraries.
We’re also going to use the requirejs text plugin to load all the templates througout the application.
Scaffolding the Backbone.js application
Let’s get something working as quickly as possible by scaffolding the main layout of the application so we can have a base from which we can flesh out the application components and associated event-driven interactions later on. Here’s the necessary code to get things started:
index.htmlmain.js<!DOCTYPE HTML> <html> <head> <meta http-equiv='content-type' content='text/html; charset=utf-8'> <title>Index</title> <link rel="stylesheet" href="stylesheets/css/screen.css" type="text/css" media="screen, projection" charset="utf-8"> <!--[if IE]> <link href="/stylesheets/ie.css" media="screen, projection" rel="stylesheet" type="text/css" /> <![endif]--> </head> <body> <div id='srchr_application'></div> <script data-main='main' src='lib/require/require.js' type="text/javascript" charset="utf-8"></script> </body> </html>srchr/srchr.jsrequirejs.config({ baseUrl: '/', paths: { text: 'lib/require/plugins/text', jquery: 'lib/jquery-1.8.3', underscore: 'lib/underscore', backbone: 'lib/backbone' } }); requirejs(['jquery', 'underscore', 'backbone', 'srchr/srchr'], function($, _, Backbone, Srchr) { new Srchr({ el: $('#srchr_application') }); });srchr/templates/layout.tpdefine(['jquery', 'underscore', 'backbone', 'srchr/search_bar/search_bar', 'srchr/history/history', 'srchr/search_results/search_results', 'text!srchr/templates/layout.tp' ], function( $, _, Backbone, SearchBar, History, SearchResults, layoutTp ) { var Srchr = Backbone.View.extend({ initialize: function() { this.$el.html(_.template(layoutTp, {})); this.searchBar = new SearchBar({ el: this.$el.find('.search_bar') }); this.history = new History({ el: this.$el.find('.history') }); this.searchResults = new SearchResults({ el: this.$el.find('.search_results') }); } }); return Srchr; });srchr/search_bar.js<div class='search_bar'></div> <div class='container'> <div class='history'></div> <div class='search_results'></div> </div>srchr/search_bar/templates/search_bar.tpdefine(['jquery', 'underscore', 'backbone', 'text!srchr/search_bar/templates/search_bar.tp' ], function( $, _, Backbone, searchBarTp) { var SearchBar = Backbone.View.extend({ initialize: function() { this.$el.html(_.template(searchBarTp, {})); } }); return SearchBar; });srchr/history.js<h1 class='placeholder'>SEARCH_BAR PLACEHOLDER</h1>srchr/history/templates/history.tpdefine(['jquery', 'underscore', 'backbone', 'text!srchr/history/templates/history.tp' ], function( $, _, Backbone, historyTp) { var History = Backbone.View.extend({ initialize: function() { this.$el.html(_.template(historyTp, {})); } }); return History; });srchr/search_results.js<h1 class='placeholder'>HISTORY PLACEHOLDER</h1>srchr/search_results/templates/search_results.tpdefine(['jquery', 'underscore', 'backbone', 'text!srchr/search_results/templates/search_results.tp' ], function( $, _, Backbone, searchResultsTp) { var SearchResults = Backbone.View.extend({ initialize: function() { this.$el.html(_.template(searchResultsTp, {})); } }); return SearchResults; });srchr/stylesheets/sass/screen.scss<h1 class='placeholder'>SEARCH_RESULTS PLACEHOLDER</h1>srchr/stylesheets/sass/_srchr.scss@import "compass/reset"; @import "srchr"body { margin: 0; font-family: tahoma, arial, verdana, sans-serif; h1.placeholder { font-size: 18px; font-weight: bold; margin: 10px; } } #srchr_application { position: absolute; top: 0; right: 0; bottom: 0; left: 0; .search_bar { position: absolute; top:0; right: 0; left: 0; height: 60px; border: 1px solid #3ba3d0; } .container { position: absolute; top: 61px; right: 0; bottom: 0; left: 0; border-bottom: 1px solid #3ba3d0; .history { position: absolute; top: 0; bottom: 0; left: 0; width: 320px; border-right: 1px solid #3ba3d0; } .search_results { position: absolute; top: 0; right: 0; bottom: 0; left: 321px; } } }You can see how the scaffolded application looks in the following image:
![]()
Event-driven interaction between the SearchBar and History views
Following an event-driven API design pattern we will now implement the subset of the application consisting of the interaction between the SearchBar and History backbone views.
Every time the user hits the search button, SearchBar will be responseable for throwing a “search” event that will be caught by the main Srchr view that then commands History to add a new search entry to it’s search command list (stored in the browser’s localStorage).
The following image shows in detail the sequence of interactions described above:
![]()
The relevant changes to the codebase follow:
srchr/search_bar/search_bar.jssrchr/search_bar/templates/search_bar.tp// ... var SearchBar = Backbone.View.extend({ events: { 'click .search': 'search' }, initialize: function() { this.$el.html(_.template(searchBarTp, {})); }, search: function(ev) { var twitterChecked = $('#twitter').attr('checked'), answersChecked = $('#answers').attr('checked'), query = $('#query').val(); if(twitterChecked) { this.$el.trigger('search', { query: query, service: 'twitter' }); } else if(answersChecked) { this.$el.trigger('search', { query: query, service: 'answers' }); } else { //console.log('Invalid service'); } } });srchr/stylesheets/sass/_srchr.scss<div class='search_form'> <label for='query'>Query:</label> <input id='query' type='text' name='query' value=''> <button class='search'>Search</button> <label for="twitter">Twitter</label> <input id='twitter' type='radio' name='service' value='twitter' checked> <label for="answers">Answers</label> <input id='answers' type='radio' name='service' value='answers'> </div>srchr/srchr.js@mixin search_bar_background_gradient($startColor: #024a68, $endColor: #0772a1) { filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=$startColor, endColorstr=$endColor); /* for IE */ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, $startColor), color-stop(100%, $endColor)); background-image: -webkit-linear-gradient($startColor, $endColor); background-image: -moz-linear-gradient($startColor, $endColor); background-image: -o-linear-gradient($startColor, $endColor); background-image: linear-gradient($startColor, $endColor); } // ... #srchr_application { position: absolute; top: 0; right: 0; bottom: 0; left: 0; .search_bar { position: absolute; top:0; right: 0; left: 0; height: 60px; border: 1px solid #3ba3d0; @include search_bar_background_gradient; .search_form { margin: 20px 0 0 0; label { margin-left: 20px; color: #fff; } #query { width: 200px; padding: 2px; border: 1px solid #333; } button { cursor: pointer; } #twitter, #answers { margin-left: 10px; } } } // ... }srchr/models/search_list.jsdefine(['jquery', 'underscore', 'backbone', 'srchr/models/search_list', 'srchr/search_bar/search_bar', 'srchr/history/history', 'srchr/search_results/search_results', 'text!srchr/templates/layout.tp' ], function( $, _, Backbone, SearchList, SearchBar, History, SearchResults, layoutTp ) { var Srchr = Backbone.View.extend({ events: { 'search .search_bar': 'newSearch' }, initialize: function() { this.$el.html(_.template(layoutTp, {})); this.searchBar = new SearchBar({ el: this.$el.find('.search_bar') }); this.history = new History({ el: this.$el.find('.history'), searches: new SearchList() }); this.searchResults = new SearchResults({ el: this.$el.find('.search_results') }); }, newSearch: function(ev, search) { this.history.addSearch(search); } }); return Srchr; });srchr/history/history.jsdefine(['jquery', 'underscore', 'backbone', 'lib/backbone.localStorage'], function($, _, Backbone) { var SearchList = Backbone.Collection.extend({ localStorage: new Backbone.LocalStorage('searches') }); return SearchList; });srchr/history/templates/history.tpdefine(['jquery', 'underscore', 'backbone', 'text!srchr/history/templates/history.tp' ], function( $, _, Backbone, historyTp) { var History = Backbone.View.extend({ events: { 'click .remove': 'removeSearch', 'mouseleave .search': 'mouseleave', 'mouseenter .search': 'mouseenter' }, initialize: function() { this.options.searches.fetch(); this.$el.html(_.template(historyTp, { searches: this.options.searches.models })); this.options.searches.on('add', $.proxy(this.searchAdded, this)); this.options.searches.on('remove', $.proxy(this.searchRemoved, this)); }, addSearch: function(search) { search.id = +new Date(); this.options.searches.create(search); }, removeSearch: function(ev) { ev.stopPropagation(); this._getSearch(ev).destroy(); }, searchAdded: function(search, searches) { this.show(searches.models); }, searchRemoved: function(search, searches) { this.show(searches.models); }, show: function(searches) { this.$el.html(_.template(historyTp, { searches: searches })); }, mouseleave: function(ev) { this._getSearchEl(ev).removeClass('hover'); }, mouseenter: function(ev) { this._getSearchEl(ev).addClass('hover'); }, _getSearch: function(ev) { var searchEl = this._getSearchEl(ev), id = searchEl.attr('data-id'); return this.options.searches.get(id); }, _getSearchEl: function(ev) { var target = $(ev.target); return target.closest('.search'); } }); return History; });srchr/stylesheets/sass/_srchr.scss<% for(var i=0; i<searches.length; i++) { var s = searches[i]; %> <div class='search' data-id='<%= s.get('id') %>'> <span class='query'><%= s.get('query') %></span> (<span class='service'><%= s.get('service') %></span>) <span class='remove'>X</span> </div> <% } %>// ... .container { // ... .history { // ... .search { height: 25px; padding: 10px 0 0 20px; border-bottom: 1px solid #333; cursor: pointer; .query { font-weight: bold; } .remove { cursor: pointer; } &.hover { background-color: #ffc; } } } // ...You can see how the application looks so far in the image bellow:
![]()
Event-driven interaction between the SearchBar and SearchResults views
Continuing with the event-driven API pattern we will now work on the interaction between the SearchBar and SearchResults views.
Upon the user hiting the search button, the SearchBar view throws a “search” event, the main Srchr view listens to it, activates the correct tab and tells the relevant SearchResults view to execute the search for the query provided by the user.
A simplified image of the event-driven interaction is provided bellow:
![]()
The changes to the code follow:
srchr/srchr.jssrchr/templates/layout.tpdefine(['jquery', 'underscore', 'backbone', 'srchr/models/search_list', 'srchr/models/twitter', 'srchr/models/answers', 'srchr/search_bar/search_bar', 'srchr/history/history', 'srchr/tabs/tabs', 'srchr/search_results/search_results', 'text!srchr/templates/layout.tp', 'text!srchr/search_results/templates/twitter.tp', 'text!srchr/search_results/templates/answers.tp' ], function( $, _, Backbone, SearchList, Twitter, Answers, SearchBar, History, Tabs, SearchResults, layoutTp, twitterTp, answersTp ) { var Srchr = Backbone.View.extend({ events: { 'search .search_bar': 'newSearch' }, initialize: function() { this.$el.html(_.template(layoutTp, {})); this.searchBar = new SearchBar({ el: this.$el.find('.search_bar') }); this.history = new History({ el: this.$el.find('.history'), searches: new SearchList() }); this.tabs = new Tabs({ el: this.$el.find('.tabs_wrapper') }); this.twitterSearchResults = new SearchResults({ el: $('#twitter_panel'), model: new Twitter, template: twitterTp }); this.answersSearchResults = new SearchResults({ el: $('#answers_panel'), model: new Answers, template: answersTp }); }, newSearch: function(ev, search) { this.history.addSearch(search); this.performSearch(search); }, performSearch: function(search) { var service = search && search.service, tabId = '#'+service+'_panel', viewId = service + 'SearchResults', view = this[viewId]; if(view) { this.tabs.select(tabId); view.search(search); } } }); return Srchr; });srchr/search_results/search_results.js<div class='search_bar'></div> <div class='container'> <div class='history'></div> <div class='search_results'> <div class='tabs_wrapper'> <ul class='tabs'> <li class='tab current'><a href='#twitter_panel'>Twitter</a></li> <li class='tab'><a href='#answers_panel'>Answers</a></li> </ul> <div id='twitter_panel' class='panel'></div> <div id='answers_panel' class='panel'></div> </div> </div> </div>srchr/search_results/templates/twitter.tpdefine(['jquery', 'underscore', 'backbone' ], function( $, _, Backbone) { var SearchResults = Backbone.View.extend({ search: function(search) { this.options.model.search( search, $.proxy(this.resultsLoaded, this), $.proxy(this.errorLoadingResults, this) ); }, resultsLoaded: function(results) { this.$el.html( _.template(this.options.template, { results: results })); }, errorLoadingResults: function(model, xhr, options) { console.log('Error loading searches.'); } }); return SearchResults; });srchr/search_results/templates/answers.tp<% for(var i=0; i<results.length; i++) { var r = results[i]; %> <div class='search_result clearfix'> <div class='avatar'> <img src='<%= r.get('profile_image_url') %>'/> </div> <div class='info'> <div class='from_user'><%= r.get('from_user') %></div> <div class='text'><%= r.get('text') %></div> </div> </div> <% } %>srchr/models/twitter.js<% for(var i=0; i<results.length; i++) { var r = results[i]; %> <div class="search_result answer"> <div class="info"> <div class="text"><span class="question">Question:</span><%= r.get('Content') %></div> <div class="text"><span class="answer">Answer:</span><%= r.get('ChosenAnswer') %></div> </div> </div> <% } %>srchr/models/answers.jsdefine(['jquery', 'underscore', 'backbone' ], function( $, _, Backbone) { var Twitter = Backbone.Model.extend({ search: function(params, success, error) { var urlRoot = 'http://search.twitter.com/search.json'; this.fetch({ url: urlRoot + '?q=' + encodeURIComponent(params.query), type: 'get', dataType: 'jsonp', success: function(model, response, options) { var results = response && response.results || [], instances = []; for(var i=0;i<results.length;i++) { var r = results[i]; instances.push( new Twitter(r)); } success(instances); }, error: function() { error(arguments); } }); } }); return Twitter; });srchr/stylesheets/sass/_srchr.scssdefine(['jquery', 'underscore', 'backbone' ], function( $, _, Backbone) { var Answers = Backbone.Model.extend({ search: function(params, success, error) { var urlRoot = 'http://query.yahooapis.com/v1/public/yql'; var yql = 'select * from answers.search where query="' + params.query + '"'; yql = yql + ' and type="resolved"'; this.fetch({ url: urlRoot + '?q=' + encodeURIComponent(yql) + '&format=json', type: 'get', dataType: 'jsonp', success: function(model, response, options) { var instances = [], query = response && response.query, results = query && query.results, question = results && results.Question, len = question && question.length; if(question && len) { for(var i=0; i<len; i++) { var q = question[i]; instances.push( new Answers(q)); } } success(instances); }, error: function() { error(arguments); } }); } }); return Answers; });// ... @mixin rounded_corners($radius: 5px) { -moz-border-radius-topright: $radius; -moz-border-radius-topleft: $radius; -webkit-border-top-right-radius: $radius; -webkit-border-top-left-radius: $radius; border-top-right-radius: $radius; border-top-left-radius: $radius; } .clearfix:after{ clear: both; content: "."; display: block; height: 0; visibility: hidden; font-size: 0; } // ... .search_results { // ... .tabs_wrapper { position: absolute; top: 0; right: 0; bottom: 0; left: 0; .tabs { clear: both; height: 30px; margin: 0; padding: 0; border-bottom: 1px solid #333; .tab { float: left; list-style-type: none; margin: 8px 1px 0 0; padding: 5px 10px 0 10px; border-top: 1px solid #333; border-right: 1px solid #333; border-left: 1px solid #333; @include search_bar_background_gradient; @include rounded_corners; &.current { a { color: #333; } background-image: none; border-bottom: 1px solid #fff; } a { color: #fff; text-decoration: none; } } } .panel { position: absolute; top: 30px; right: 0; bottom: 0; left: 0; overflow: auto; .search_result { padding: 15px 0 0 5px; border-bottom: 1px solid #333; .avatar { float: left; margin: 0 5px 0 0; } .info { float: left; .text { max-width: 960px; padding: 2px; } } &.answer { .info { float: none; .text { .question, .answer { font-weight: bold; } } } } } } } } // ...And this is how the application looks after these changes:
![]()
Event-driven interaction between History and SearchResults views
We now move on to implement the final event-driven interaction between the History and SearchResults views. In this use case the user clicks the search button, the SearchBar view throws a “search” event, the Srchr view is listening for it, activates the correct tab panel and calls the search method on the relevant SearchResults view.
A diagram for this sequence of interactions is shown bellow:
![]()
Here are the changes to the code:
srchr/history/history.jssrchr/srchr.js// ... var History = Backbone.View.extend({ events: { 'click .remove': 'removeSearch', 'click': 'selectSearch', 'mouseleave .search': 'mouseleave', 'mouseenter .search': 'mouseenter' }, // ... selectSearch: function(ev) { this.$el.trigger('search', this._getSearch(ev).attributes); }, searchAdded: function(search, searches) { // ...var Srchr = Backbone.View.extend({ events: { 'search .search_bar': 'newSearch', 'search .history': 'historySearch' }, // ... historySearch: function(ev, search) { this.performSearch(search); }, performSearch: function(search) { // ...Final thoughts
In this article we’ve implemented Rebecca Murphey’s srchr application using Backbone.js javascript MVC framework following the same development flow as we did for the ExtJs and JavascriptMVC frameworks.
The full codebase can be downloaded from here and an online demo can be seen by clicking this link. A github repository with a branch for each of the different implementations can be cloned from this github page so you can easely compare the different versions.
In the next article we’ll tackle building the srchr application using AngularJS.
I hope this article was useful for you and thank you for reading my blog post.
Demo
Try them
Note: Backgrid.js and its author are not associated with Santa and santaclaus.com in any way.
Name (StringCell) Age (IntegerCell) Birthday (DateCell) Gender (SelectCell) Alive (BooleanCell) URL (UriCell) Email (EmailCell) Next Delivery Time (DateTimeCell) His Local Time (TimeCell) Change in Wallet (NumberCell) Configuring Cells
While many built-in cells provide reasonable defaults, you may choose to configure them to suit your own needs.
Cell types that you are most likely to configure are the NumberCell, DatetimeCell and SelectCell classes. Once configured, you may use them as the cell attribute values in column definitions.
var grid = new Backgrid.Grid({ columns: [{ name: "id", label: "ID", editable: false, // Dynamically defines a new cell type with new defaults. // ID is displayed as an integer without ','s. cell: Backgrid.IntegerCell.extend({ orderSeparator: '' }) }, { name: "lastaccessed", label: "Last Login Time", editable: false, cell: Backgrid.DatetimeCell.extend({ includeMilli: true }) }, { name: "gender", label: "Gender", cell: Backgrid.SelectCell.extend({ // It's possible to render an option group or use a // function to provide option values too. optionValues: [["Male", "m"], ["Female", "f"]] }) }], collection: col });Pro Tip
SelectCell treats all option values as strings by default, if you need to persist a different type of values into your model, you should extend SelectCell to provide your own formatter.
See the JSDoc for the various Cell classes for details on what you can configure using this method.
Custom Cell
If the built-in and extension cell classes are not enough for you, you may choose to create your own cell class and supply it to a column definition.
If your custom cell will still use a <input type="text" /> like the predefined ones for editing, you may choose to subclass Cell or one of the predefined classes and simply define a className and a formatter. In fact, most of the core cell classes are done this way.
// This is the verbatim Backgrid.StringCell definition var StringCell = Backgrid.StringCell = Cell.extend({ // Cell default class names are the lower-cased and dasherized // form of the the cell class names by convention. className: "string-cell", formatter: { fromRaw: function (rawData) { return rawData; }, toRaw: function (formattedData) { return formattedData; } } });If your cell class will render differently in display mode, you may simply override Cell#render() in your subclass.
Custom CellEditor
Advanced Usage
Some cell types, like the TextCell extension, may only make sense if the editor is rendered in a modal dialog or a form element in a different part of the page. In that case the default InputCellEditor, which renders a <input type="text" />, will not be suitable and a new CellEditor must be defined.
A custom cell editor should subclass CellEditor as it defines a number of required parameters in its initializer and clean up operations that are necessary for most cell editors. When a cell class enters edit mode, a new editor instance is constructed by given it the required parameters, and then a Backbone event edit is fired from the cell instance itself. A custom cell class can act on this event to do anything before the cell editor is rendered.
Once the cell has entered edit mode, a Backbone event editing is fired. A custom cell class can then act on it to do anything after the cell editor has been rendered, e.g. placing the focus inside the editor.
During editing, if an error is encountered (see the formatter protocol below), a cell editor should fire a Backbone event error so that listeners—usually a cell instance—can respond appropriately. When editing is done, a cell editor should fire a Backbone done event. A cell should be listening to this event so it can remove its editor and re-render itsef in display mode.
Truely Advanced Hacking
At the most basic level, Cells and CellEditors are simply Backbone.View classes that are guaranteed to be given a number of parameters when constructed by Row. You can use any Backbone.View as your Cell and CellEditor.
Backbone.ModelFactoryProvides a factory for generating model constructors which will never produce multiple instances of a model with the same unique identifier. It allows a developer to manage model sharing between views more easily by maintaining an internal cache of model instances based on the value of their idAttribute property.
In short, when you create a new instance of a model created with Backbone.ModelFactory and give it an id it will never create duplicate instances of the same model with a given id.
var user1 = new User({id: 1}); var user2 = new User({id: 1}); var user3 = new User({id: 2}); console.log(user1 === user2); // true console.log(user3 === user1); // falseInclusion
Backbone.ModelFactory supports three methods of inclusion.
Node:
var Backbone = require('backbone-model-factory');AMD/RequireJS:
require(['backbone-model-factory'], function (Backbone) { // Do stuff... });Browser Globals:
<script src="path/to/backbone.js"></script> <script src="path/to/backbone-model-factory.js"></script>Usage
Rather than extending Backbone.Model, constructors are created by a call to Backbone.ModelFactory. Instead of this:
var User = Backbone.Model.extend({ defaults: { firstName: 'John', lastName: 'Doe' } });...do this:
var User = Backbone.ModelFactory({ defaults: { firstName: 'John', lastName: 'Doe' } });Backbone.ModelFactory also supports inheritance, so model constructors can extend each other by providing a model constructor as the first argument:
var Admin = Backbone.ModelFactory(User, { defaults: { isAdmin: true } });Consequences of Extending Models
Models created with Backbone.ModelFactory will not share their unique-enforcement capabilities with models which they extend or which extend them. For example, using the User and Admin models above giving each the same ID would not result in the same object:
var user = new User({id: 1}); var admin = new Admin({id: 1}); console.log(user === admin); // falseThe ability to share instances between models in an inheritance chain is a feature which is actively being considered, but it leaves a lot of open questions.
Tests
Inclusion
There are 3 files in /test/inclusion and they account for the 3 supported methods of including this module. To execute these tests, simply open the HTML files in a browser or install the npm module backbone and run node node-module.js.
Unit
Mocha unit tests exist in test/test.js. To run, simply do mocha from the project root.
Inspriation
This is inspired by SoundCloud's approach detailed in Building the Next SoundCloud under "Sharing Models between Views."
Crashlytics Labs is proud to announce the release of Backbone.StateManager
Posted by Patrick Camacho on Oct 16, 2012 in Engineering, Featured, Technology![]()
Backbone.js is a highly extensible framework that provides an MV* paradigm for Javascript applications that we use here at Crashlytics. Its lightweight architecture provides us a great foundation for building modular thick web clients. Backbone’s adaptability allows for the creation of addons and plugins to help tackle many of the problems faced when building a large scale JS application.
However, one of the problems we ran into while using Backbone at Crashlytics was the difficulty of managing the state of our application and its child components in a modular, scalable fashion. Whether it is choosing how to render a view on the DOM or conditionalizing the functionality of a model, the persistent nature of a web application requires a way to manage state-related logic.
Enter Backbone.StateManager
Backbone.StateManager is a module for Backbone.js that adds the ability to manage and utilize states in any size JavaScript application. It can be used as a stand alone object or in conjunction with a target object. Built on top of Underscore.js and Backbone.js, StateManager allows for modular state definitions, sub/pub architecture support with Backbone.Events, transition events between states, and regular expression matching.
Continuing our commitment to open source software, we’re happy to announce that Backbone.StateManager is available today on Github.
Getting Started
The core functionality of StateManager is comprised of 3 simple methods: addState, triggerState, and removeState. Because StateManager extends Backbone.Events, it is also possible to subscribe to the object and listen as it sends out signals when moving between states.
Backbone.StateManager constructor takes two arguments, a state object and an options object, but neither is required. Passed in states will be automatically added and the options are set as an instance property.
1 var stateManager; 2 3 stateManager = new Backbone.StateManager(); 4 // or 5 stateManager = new Backbone.StateManager({ 6 foo: { 7 enter: function() { 8 return console.log('enter bar'); 9 }, 10 exit: function() { 11 return console.log('exit foo'); 12 } 13 }, 14 bar: { 15 enter: function() { 16 return console.log('enter bar'); 17 }, 18 exit: function() { 19 return console.log('exit bar'); 20 } 21 } 22 });State Definitions
A state is intended to be as modular as possible, so each state definition is expected to contain enter and exit methods that are used when entering or leaving that state. A state definition can also have a transitions property that contains several methods to be used when moving between specified states. There are 4 types of transitions that Backbone.StateManager will defaultly look for: onBeforeExitTo, onExitTo, onBeforeEnterFrom, and onEnterFrom. Each transition is a key value pair, where the value is a method and the key defines the transition type and the specified state (eg onEnterFrom:specifiedState).
1 { 2 enter: function() { 3 return console.log('enter'); 4 }, 5 exit: function() { 6 return console.log('exit'); 7 }, 8 transitions: { 9 'onBeforeExitTo:anotherState': function() {}, 10 'onExitTo:anotherState': function() {}, 11 'onBeforeEnterFrom:anotherState': function() {}, 12 'onEnterFrom:anotherState': function() {} 13 } 14 }Integration
A benefit of StateManager is that it provides an easy method to painlessly add itself to any object. Through the use of the addStateManager, which takes a target object parameter, it reads in any states defined on the target, binds state methods to the target, and creates a new Backbone.StateManager. It also sets a number of convenience methods on the target, including triggerState, getCurrentState, and a reference to the StateManager at target.stateManager.
1 var View; 2 3 View = Backbone.View.extend({ 4 states: { 5 foo: { 6 enter: function() { 7 return console.log('enter bar'); 8 }, 9 exit: function() { 10 return console.log('exit foo'); 11 }, 12 transitions: { 13 'onExitTo:bar': function() { 14 return 'just exited and bar is about to be entered'; 15 } 16 } 17 }, 18 bar: { 19 enter: function() { 20 return console.log('enter bar'); 21 }, 22 exit: function() { 23 return console.log('exit bar'); 24 } 25 } 26 }, 27 initialize: function() { 28 return Backbone.StateManager.addStateManager(this); 29 } 30 });Get It
Feedback Welcome
Our goal is to make Backbone.StateManager as useful as possible for you to make writing powerful Backbone.js apps painless. We’re just getting started, and the feedback you provide is critical in achieving this. Please report bugs and discuss features on our Github issues page https://github.com/crashlytics/backbone.statemanager/issues.
Join the Team
Interested in diving-deep into these and other workflow-enhancing challenges? We’re hiring! Give us a shout at jobs@crashlytics.com. You can stay up to date with all our progress on Twitter, Facebook, and Google+.
Backbone.MarionetteMake your Backbone.js apps dance with a composite application architecture!
About Marionette
Backbone.Marionette is a composite application library for Backbone.js that aims to simplify the construction of large scale JavaScript applications. It is a collection of common design and implementation patterns found in the applications that I (Derick Bailey) have been building with Backbone, and includes various pieces inspired by composite application architectures, such as Microsoft's "Prism" framework.
App Architecture On Backbone's Building Blocks
Backbone provides a great set of building blocks for our JavaScript applications. It gives us the core constructs that are needed to build small apps, organize jQuery DOM events, or create single page apps that support mobile devices and large scale enterprise needs. But Backbone is not a complete framework. It's a set of building blocks. It leaves much of the application design, architecture and scalability to the developer, including memory management, view management, and more.
Marionette brings an application architecture to Backbone, along with built in view management and memory management. It's designed to be a lightweight and flexible library of tools that sits on top of Backbone, providing a framework for building scalable application.
Like Backbone itself, you're not required to use all of Marionette just because you want to use some of it. You can pick and choose which features you want to use, when. This allows you to work with other Backbone frameworks and plugins very easily. It also means that you are not required to engage in an all-or-nothing migration to begin using Marionette.
Key Benefits
- Scale applications out with modular, event driven architecture
- Sensible defaults, such as using Underscore templates for view rendering
- Easy to modify to make it work with your applicaton's specific needs
- Reduce boilerplate for views, with specialized view types
- Build on a modular architecture with an Application and modules that attach to it
- Compose your application's visuals at runtime, with Region and Layout
- Nested views and layouts within visual regions
- Built-in memory management and zombie killing in views, regions and layouts
- Built-in event clean up with the EventBinder
- Event-driven architecture with the EventAggregator
- Flexible, "as-needed" architecture allowing you to pick and choose what you need
- And much, much more
Compatibility And Requirements
Backbone.Marionette currently works with the following libraries:
Marionette has not been tested against any other versions of these libraries. You may or may not have success if you use a version other than what it listed here.
While support for Zepto and Enderjs has been added, it is not officially tested against these libraries at this time.
Marionette makes use of jQuery's Deferred objects and, as such, will need supported methods in replacement libraries. Zepto users can use @Mumakil's Standalone-Deferred or @sudhirj's simply-deferred. Enderjs users, please let us know of how you solve any compatibility issues.
Source Code And Downloads
You can download the latest builds directly from the "lib" folder above.
For more information about the files in this folder, or to obtain an archive containing all Marionette dependencies (including Underscore, Backbone, etc), please see the downloads section on the website.Available Packages
Marionette is unofficially available from various package management systems, such as RubyGems, Node Package Manager, Nuget, etc. These packages are maintained by the community and are not part of the core Backbone.Marionette code.
Documentation
The primary documentation is split up in to multiple files, due to the size of the over-all documentation. You can find these files in the /docs folder, or use the links below to get straight to the documentation for each piece of Marionette.
Marionette's Pieces
These are the strings that you can pull to make your puppet dance:
- Marionette.Application: An application object that starts your app via initializers, and more
- Marionette.AppRouter: Reduce your routers to nothing more than configuration
- Marionette.Callbacks: Manage a collection of callback methods, and execute them as needed
- Marionette.CollectionView: A view that iterates over a collection, and renders individual ItemView instances for each model
- Marionette.Commands: An extension of Backbone.Wreqr.Commands, a simple command execution framework
- Marionette.CompositeView: A collection view and item view, for rendering leaf-branch/composite model hierarchies
- Marionette.Controller: A general purpose object for controlling modules, routers, view, and implementing a mediator pattern
- Marionette.EventAggregator: An extension of Backbone.Events, to be used as an event-driven or pub-sub tool
- Marionette.functions: A suite of helper functions and utilities for implementing common Marionette behavior in your objects
- Marionette.ItemView: A view that renders a single item
- Marionette.Layout: A view that renders a layout and creates region managers to manage areas within it
- Marionette.Module: Create modules and sub-modules within the application
- Marionette.Region: Manage visual regions of your application, including display and removal of content
- Marionette.Renderer: Render templates with or without data, in a consistent and common manner
- Marionette.RequestResponse: An extension of Backbone.Wreqr.RequestResponse, a simple request/response framework
- Marionette.TemplateCache: Cache templates that are stored in <script> blocks, for faster subsequent access
- Marionette.View: The base View type that other Marionette views extend from (not intended to be used directly)
The following have been extracted in to separate plugins:
Please note that this is documentation is rather dry - it's meant to be a reference for those that just need a reference. If you're looking for an introduction and/or examples on how to get started, please see the Wiki.
The Wiki: Sample Apps, Tutorials, And Much More
A wiki is an important aspect of a thriving community, as it provides a place for the community to contribute ideas, examples, answer frequently asked questions, and more. If you're looking for community-driven information, examples that go beyond the dry technical documentation, or want to contribute your own ideas and examples to the community, please see the wiki page.
Annotated Source Code
In addition to this readme, I've commented the source code quite heavily and run it through Docco as part of my build process. This produces a nicely formatted, annotated source code as documenation file.
You can read the annotated for all the detail of how Marionette works, and advice on which methods to override when.
Donations
Marionette needs your support, but not everyone can offer assitance with code, bug submissions, and answering questions. If you're using Marionette and you're finding that it is saving you as much time and effort as I believe it does, then please consider financial support for the project.
Donate via PayPal
Donate via GitTip
How To Contribute
If you would like to contribute to Marionette's source code, please read the guidelines for pull requests and contributions. Following these guidelines will help make your contributions easier to bring in to the next release.
Help Is Just A Click Away
#Marionette on FreeNode.net IRC
Join the #marionette channel on FreeNode.net to ask questions and get help.
Get announcements for new releases, share your projects and ideas that are using Marionette, and join in open-ended discussion that does not fit in to the Github issues list or StackOverflow Q&A.
For help with syntax, specific questions on how to implement a feature using Marionette, and other Q&A items, use StackOverflow.
Ask questions about using Marionette in specific scenarios, with specific features. For example, help with syntax, understanding how a feature works and how to override that feature to do what you need or how to organize the different view types to work best with your applications needs.
Questions on StackOverflow often turn in to blog posts and wiki entries.
Report issues with Marionette, submit pull requests to fix problems, or to create summarized and documented feature requests (preferably with pull requests that implement the feature).
Please don't ask questions or seek help in the issues list. There are other, better channels for seeking assistance, like StackOverflow and the Google Groups mailing list.
Lastly, I blog about Marionette on a regular basis, at my LosTechies.com blog.
Release Notes
For change logs and release notes, see the changelog file.
Legal Mumbo Jumbo (MIT License)
Copyright (c) 2012 Derick Bailey; Muted Solutions, LLC
Distributed under MIT license.
Last week we looked at Backbone.js’s internals, covering configuration, server support, events, and models. I actually really enjoy looking at projects this way, it’s one of the best ways to learn new programming techniques. So let’s continue dissecting Backbone by taking a look at Backbone.Collection.
Constructor
Backbone.Collection is a constructor function that accepts an array of models and an options object.
As an aside, notice that void 0 is used in this code. To understand why, recall that the void operator returns undefined. Since ECMAScript 5, the undefined property isn’t writable, so it’s safe to use it. However, in earlier versions it was writable, which meant malicious code could technically take advantage of this fact by assigning a value to the undefined property of the global object. The void operator expects an expression, so void 0 is considered the idiomatic way of safely obtaining undefined.
The constructor calls the reset method, which removes existing models and adds new ones. This is similar to instantiating a collection with no models, and then manually calling add on each one.
Inheritance and Mixins
The Collection class inherits from Backbone.Events. Events are used both publicly and internally. There’s a toJSON method that iterates over each model and calls the model’s toJSON method. This brings up an interesting point: collections use methods from Underscore.js, but Collection doesn’t inherit from Underscore. Why not? Well, certain methods are manually assigned to Collection.prototype, while others are rewritten in ways that make sense in Backbone. For example, the pluck method works on model attributes, and sort uses the boundComparator which has a slightly different API to Array.prototype.sort.
Adding and Removing Items
Collections are basically an array of models with events, wrapped with convenient Underscore-like iterator methods. The add method is always called however models are added, which means it’s a good place to do housekeeping like preventing invalid models and duplicates from being inserted into the collection. Models are also indexed by id, and all model events are bound to _onModelEvent. This method dynamically adds new models, removes deleted ones, and updates models with changes.
If the collection requires sorting, the add method will call sort once all models have been processed. And, if the silent option isn’t set, an add event will be triggered for each model that was successfully added.
It naturally follows that the remove method has a fair amount of work to do, given the complexity of add. The indexed ids must be deleted, and _removeReference is called to remove the model’s reference back to the collection.
Deleting items in JavaScript is interesting, because we actually have the delete keyword to do this for us. However, delete is only used for properties, so the authors have used the Array.prototype.splice technique to delete models from the array. The add and remove methods also update the length property, which allows the collection to behave in an Array-like manner, and helps support the mixed-in Underscore methods.
Now take a look at the simplicity of the where method. It basically loops over each model, comparing an attributes object. This is simple because the filter method is taken directly from Underscore.
Chainable API
Another bit of sugar is the support for Underscore’s chain method. This works by calling the original method with the current array of models and returning the result. In case you haven’t seen it before, the chainable API looks like this:
var collection = new Backbone.Collection([ { name: 'Tim', age: 5 }, { name: 'Ida', age: 26 }, { name: 'Rob', age: 55 } ]); collection.chain() .filter(function(item) { return item.get('age') > 10; }) .map(function(item) { return item.get('name'); }) .value(); // Will return ['Ida', 'Rob']Some of the Backbone-specific method will return this, which means they can be chained as well:
var collection = new Backbone.Collection(); collection .add({ name: 'John', age: 23 }) .add({ name: 'Harry', age: 33 }) .add({ name: 'Steve', age: 41 }); collection.pluck('name'); // ['John', 'Harry', 'Steve']Conclusion
I’ve been using Backbone for a while, and I’ve never really thought about how the Backbone.Collection methods can be chained. Sometimes it’s difficult to tell what’s possible though – once you’re in an Underscore chain you can’t use methods like pluck because Backbone’s models use the get method to access attributes, so you’ll end up with an array of undefined values.
Next week I’ll continue looking at Backbone by investing the formidable routing and history APIs.
During the past weekend, it feels like I’ve tried almost every resource to learn Backbone.js. I learn brand new topics best by listening to someone explain it, so I started out by going to a Meetup on Thursday night where there was a Backbone.js expert who was supposed to teach us Backbone.js. Let’s just say it’s great that that guy is not a teacher…
The problem is that Backbone.js does not have it’s own web server, so it has to be used in a combination with other web server frameworks. At the Meetup, the Backbone.js expert was teaching us using Brunch.io. So not only did I have to learn Backbone.js, but also how to work with Brunch.io, CoffeeScript, and a weird template language. Without much explanation on any of these, I walked away pretty disheartened and ready to try out other learning resources.Here are some reviews of the resources I’ve tried:
Peepcode
The Peepcode Backbone.js video walk-through was well worth the $12. I was able to follow along, understand the general Backbone.js concepts and actually build the app described in the video. The Peepcode tutorial uses Sinatra, which I’ve used before, and is in Javascript, which I know enough of, so I was really able to focus on the actual Backbone.js part of the project.
The part I didn’t like about the Peepcode tutorial was that the Backbone Model / View / Router / Collection wasn’t broken out into different files. All the Javascript stayed in the same file, which made it seem like Backbone.js is less structured than it really is.
Still, this is a really great video for an initial introduction to Backbone.
Rails + Backbone.js Railscast
While Sinatra is a nice light-weight web framework using Ruby, most of the time I’m building with Rails. That is why I was super excited to find the Rails + Backbone.js Railscast. I watched both parts, and they were amazing – again, well worth the money!
The nice thing about the backbone-on-rails gem is that it really structures your Backbone.js code into very neat folders (can you tell I’m an organization freak?). The Railscast was using CoffeeScript, which I haven’t used before, but was totally sold on by the end of it! Don’t think I can go back to plain Javascript to be honest. Bonus: Here is a great Railscast on CoffeeScript.
Anatomy of Backbone.js At CodeSchool
I’ve been going through the Anatomy of Backbone.js CodeSchool course exercises throughout the weekend. One thing that I found from this and my previous CodeSchool experiences, is that you really need to know the context for their courses. Once I understood the structure of Backbone.js from Peepcode, doing CodeSchool exercises was great for reinforcement and practice of the material.
Otherwise, you will be doing the exercises, but as soon as you finish the course, you will have no idea where to start on your own. Mostly, because they don’t even mention that you need a web server to run Backbone.js. They just jump right into the coding part of it.
The other negative part of the CodeSchool courses is that you can’t skip around levels. Some exercises are buggy or hard to understand, but you have to finish all of them before going on to the next level. I got stuck on an exercise because of a small technicality – I had to put my code into two documents, but didn’t notice the second tab – and that was it, there was nothing I could do to move on except tweet about the issue to them and wait a day for them to reply before moving on.
However, if you can understand the context first and get past some frustration, the exercises are great practice.
Building Your Own App
Of course it’s all fun to do Backbone.js exercises, but the ultimate way to learn to start using it in your own real-life applications! I played around Backbone.js (using Rails) yesterday, as I tried to build a Hacker News / Reddit for Backbone.js resources. Here is what I have so far. I’m probably not going to continue building that specific app, but I really enjoyed using Backbone.js enough to keep using it in my future projects
So to summarize, if you’re learning Backbone.js, make sure to seek out resources that not only give you an overview of Backbone.js, but also use it with the web framework you already use and are comfortable with.
What resources do you recommend for learning Backbone.js?
Topics will include MVC theory and how to build applications using Backbone's models, views, collections and routers. I'll also be taking you through advanced topics like modular development with Backbone.js and AMD (via RequireJS), solutions to common problems like nested views, how to solve the routing problems with Backbone and jQuery Mobile and a lot more.
Dependencies
Underscore.js
Backbone's only hard dependency. http://underscorejs.org
JSON2.js
Needed if you'd like to parse and serialize JSON in older browsers (read: "Internet Explorer") https://github.com/douglascrockford/JSON-js
jQuery
Recommended for DOM manipulation and Ajax. http://jquery.com
Zepto
Recommended as a jQuery alternative for mobile apps http://zeptojs.com
Model
Backbone Forms
Generate customisable forms from your model schema. Features include custom editors, nested forms, editable lists and validation.
backbone-deep-model
Improved support for models with nested attributes. Get and set attributes and listen to changes on them using a path, e.g. get('user.address.ln1').
Backbone-Nested
A plugin to make Backbone keep track of nested attributes.
Backbone.Memento
Create undo points to store / restore your model's state.
Get the code and read the documentation:
Backbone.actAs.Mementoable
Memento pattern realization for Backbone.js models with more flexible API than in Backbone.Memento
Backbone.actAs.Configurable
Simple helpers to configure your models from one configuration object.
Backbone.Articulation
Backbone.Validations
Backbone.Validation
Backbone.Validator
backbone-view-model
Backbone.ModelRef
- Lets you build your views while you wait for models to load.
- Notifies you when a model unloads so you can change your view state.
- Compatible with Knockback.js (https://github.com/kmalakoff/knockback)
Pretty much just a collection and model id reference. Simple, but useful.
workflow.js
Backbone Dotattr
Backbone.Mutators
Backbone.Chosen
Backbone fetch cache
Storage
Backbone.localStorage
backbone.couchdb.js
Backbone.ioBind
Backbone-websql
Backbone-slet
Backbone.SharePoint
Backbone.SAP
Backbone.Offline
Backbone.Rpc
Backbone.Safe
Relations
Backbone-relational
Backbone-associations
Backbone.Rel
Ligament
backbone-orm
Collection
backbone-pageable
A pageable, drop-in replacement for Backbone.Collection. Supports client-side and server-side pagination, single page and global sorting, and client-side bi-directional event handling. Inspired by Backbone.Paginator, but much better.
Backbone.actAs.Paginatable
Load your collections piece by piece. Very useful for large model sets.
Nesting.js
Simple helper function that makes it easy to work with nested Backbone collections
Backbone.CollectionSubset
Create subsets of collections using filter functions that automatically update allowing you to create a tree of collections that is always in sync.
Backbone Query
Provides NoSQL queries (like MongoDB) for backbone.js collections
Query Engine
Provides extensive querying, filtering, and searching functionality for backbone.js collections
- includes live interactive demo
- source-code documentation only
- runs on node.js and in the browser
- supports NoSQL queries (like MongoDB)
- supports filters (applying a filter function to a collection)
- supports search strings (useful for turning search input fields into useful queries)
- supports pills for search strings (e.g. author:ben priority:important)
- supports optional live collections (when a model is changed, added or removed, it can automatically be tested against the collections queries, filters, and search string, if it fails, remove it from the collection)
- supports parent and child collections (when a parent collection has a model removed, it is removed from the child collection too, when a parent collection has a model added or changed, it is retested against the child collection)
- https://github.com/bevry/query-engine
Backbone fetch cache
View
LayoutManager
Backbone.Stickit
Model-View binding in Backbone style.
Yabbe (Yet Another Backbone Binding Exension)
Synapse
Backbone.ModelBinder.js
Simple, flexible and powerful bidirectional view-model binding for backbone. A javascript only solution (no markup in html) that relies on jQuery delegates for binding. Similar to how backbone view event blocks work.
- Handles partial view binding, nested views, formatting, type conversion, arbitrary html attributes and most html element types.
- JS Weekly Article
- Git
Backbone Bindings
Bi-directional bindings between Backbone.View elements and Backbone.Model attributes.
Backbone.ModelBinding
Awesome model binding between models and form input elements, and more
- KnockoutJS-style data-bind attribute support
- Bind form fields to models and vice-versa
- Convention based
- Pluggable with your own conventions
- Extensible to bind to more than just form input elements
Get the code and read the documentation:
Knockback.js
Knockout bindings for Backbone.js models and collections.
Backbone on the Couch
A little plugin for use in leanback apps, where someone is sat on the couch using a remote control to use your app.
ProxyDollar
Plugin that proxies jQuery/Zepto/$ functions for Backbone Views.
Backbone.declarative
A small plugin that adds declarative model and collection event binding to Backbone Views.
outback.js
KnockoutJS-inspired declarative binding solution for Backbone Views.
- KnockoutJS-compatible binding handler API
- Explicit control over dependencies (e.g. cascading select boxes)
- Unobtrusive (i.e. code-based) and declarative (i.e. markup-based) configuration
- Multiple binding contexts for separating transient and persistent models.
- Tests, TodosMVC, and examples.
- https://github.com/politician/outback
Backbone.BindTo
Extension for automatic binding and unbinding of model events to views. It defines two special view attributes - bindToModel and bindToCollection. They are handled similar to events attribute, but instead of binding to dom events they bind to model/collection. Also this plugin takes care of cleaning events after the view is removed.
https://github.com/RStankov/backbone-bind-to
Backbone.PluginView
Make jQuery plugins from your Backbone Views.
Backbone.ViewKit
View management and transitions geared toward mobile applications.
Templates
Backrub
Helpers
Builder (DOM Builder for Backbone Views)
Widgets
Slickback (integration with SlickGrid)
Vienna IKS Editables
Backbone.Aura
Backbone.Aura is a decoupled, event-driven architecture on top of Backbone.js for developing widget-based applications
Routing
jQuery Mobile Routing
Before/After Filters
Query Parameters
Frameworks
Backbone Boilerplate
Backbone.CQRS
Capt by Ben Nolan
Chaplin
- https://github.com/chaplinjs/chaplin - An opinionated, full-featured architecture framework for creating complex single-page applications with a strong emphasis on memory management and class heirarchies. Includes a powerful CollectionView class, rails-style routing with proper params hash, and controller patterns.
Backbone.Marionette
Make your BackboneJS apps dance with a composite application structure!
Mixin.js
- Provides mixin classes for a local collections (not stored on a server) and for mixing Backbone.Events into any class (including a check if they are mixed-in).
- Provides a facility to use Backbone.Events trigger('destroy') to do memory cleanup instead of a destroy method (event-based instance life-cycles).
- https://github.com/kmalakoff/mixin
Backbone Module (Module loader for Backbone apps)
- Not a JS loader like require.js, but a way to structure your code in big Backbone app projects.
- Load JS without worrying about load sequence and dependencies between modules.
- Source: https://github.com/juggy/backbone-module
Singool.js
Backbone-Traversal
Other
Backbone.Analytics: Drop-In Google Analytics For Backbone's Router
Backbone-Callbacks: Node.js style callbacks for asynchronous methods
Backbone.jFeed: RSS/ATOM Feeds For Collections
Cleaning Up And Preventing Memory Leaks With Your Views
Thingy-Client
Backbone (adapted for MooTools)
Backbone-Factory
Backbone.StateMachine
Backbone-FSM
Backbone.Shortcuts
Brunch
- http://brunch.io - provides a powerful, rails-like directory structure and a powerful, extensible set of component generators (for working with Jade, handlebars, sass, less, stylus, etc.)
Backbone Tastypie
- Modifications to Backbone's Model an Collection so that they play nice with django-tastypie. Includes a way to easily paginate over a resource list and a way to access the associated meta information.
- https://github.com/amccloud/backbone-tastypie
Backbone Interface
- Errors are thrown when Backbone instance does not have all the required methods of the interface.
- Better organized code. Keep your interfaces separate, with comments - a well-documented reference point for the methods in your Models, Collections, Views, and Routers. (Not for production).
- https://github.com/luke-siedle/backbone-interface
Backbone.Service
Backbone.GoogleMaps
Preparation
Before starting this tutorial, you’ll need the following:
- alexyoung / dailyjs-backbone-tutorial at commit fcd653ec6
- The API key from part 2
- The “Client ID” key from part 2
- Update app/js/config.js with your keys (if you’ve checked out my source)
To check out the source, run the following commands (or use a suitable Git GUI tool):
git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git cd dailyjs-backbone-tutorial git reset --hard fcd653ec6Wireframe
The application we’re building has several main interface elements:
- A two column layout for displaying task lists and tasks
- Forms for adding and editing each item type
- Buttons for invoking the forms, deleting items, and clearing complete items
- Task state control (done checkbox)
The image below shows the basic layout.
In this tutorial we’ll start implementing the interface by using an unordered list to represent task lists.
List Items
Despite being relatively simple, implementing a navigable list of task lists involves several Backbone.js elements:
- HTML templates
- Backbone views: ListMenuView, ListMenuItemView
- Backbone collection: TaskLists
The ListMenuView contains the task list menu, and the ListMenuItemView is the navigation item for each task list itself. This can be modeled as a ul and a set of li elements.
Create a new directory called app/js/views/lists to store the task list-related Backbone.View classes, and another called app/js/templates/lists for the corresponding templates.
View: ListMenuView
This view resides in app/js/views/lists/menu.js:
define(['views/lists/menuitem'], function(ListMenuItemView) { var ListMenuView = Backbone.View.extend({ el: '.left-nav', tagName: 'ul', className: 'nav nav-list lists-nav', events: { }, initialize: function() { this.collection.on('add', this.render, this); }, render: function() { // TODO } }); return ListMenuView; });It loads views/lists/menuitem which we’ll create in a moment. Then it binds itself to the .left-nav element which was created by AppView and its corresponding template. The menu itself is an unordered list, and it uses some class names that will become more relevant once styles are added.
Notice that this view expects a collection. Collections can be passed to views during instantiation. For example, new ListMenuView({ collection: lists }) will pass the lists collection to an instance of this view.
The render method should look like this:
render: function() { var $el = $(this.el) , self = this; this.collection.each(function(list) { var item, sidebarItem; item = new ListMenuItemView({ model: list }); $el.append(item.render().el); }); return this; }The view’s element is used as the container for each ListMenuItemView which is passed a model by iterating over the collection.
View: ListMenuItemView
The app/js/views/lists/menuitem.js is similar to the previous view, but makes use of a template and Backbone’s declarative event binding feature.
define(['text!templates/lists/menuitem.html'], function(template) { var ListMenuItemView = Backbone.View.extend({ tagName: 'li', className: 'list-menu-item', template: _.template(template), events: { 'click': 'open' }, initialize: function() { this.model.on('change', this.render, this); this.model.on('destroy', this.remove, this); }, render: function() { var $el = $(this.el); $el.data('listId', this.model.get('id')); $el.html(this.template(this.model.toJSON())); return this; }, open: function() { var self = this; return false; } }); return ListMenuItemView; });The template is app/js/templates/lists/menuitem.html:
<a href="#" class="list-title" data-list-id=""></a>Notice that curly braces are used to insert values. This is provided by Underscore’s built-in template system.
The view’s open method is bound to click events, and I’ve also bound change and destroy model events to the view as well – these will come in handy later.
The template’s values are inserted by using the template method in render:
$el.html(this.template(this.model.toJSON()));The model’s raw JSON is passed to template so title and id will be resolved to the correct values.
Invoking ListMenuView
Open app/js/app.js and add ListMenuView to the list of define requirements:
define([ 'gapi' , 'views/app' , 'views/auth' , 'views/lists/menu' , 'collections/tasklists' ], function(ApiManager, AppView, AuthView, ListMenuView, TaskLists) {Last week I added a console.log to print out the name of each list. Remove that code and change it to render the ListMenuView:
connectGapi: function() { var self = this; this.apiManager = new ApiManager(this); this.apiManager.on('ready', function() { self.collections.lists.fetch({ data: { userId: '@me' }, success: function(res) { self.views.listMenu.render(); }}); }); }Go back up to the App constructor function to make it instantiate listMenu by passing the relevant collection:
var App = function() { this.views.app = new AppView(); this.views.app.render(); this.views.auth = new AuthView(this); this.views.auth.render(); this.collections.lists = new TaskLists(); this.views.listMenu = new ListMenuView({ collection: this.collections.lists }); this.connectGapi(); };Running It
Now if you run node server and visit http://localhost:8080/, you should see your task lists displayed in a simple unordered list.
Summary
The app is now communicating with Google, allowing users to sign in, and also displays the user’s task lists. It still doesn’t look too exciting because we haven’t yet applied any styles, but you should be able to adapt the code you’ve seen so far to work with other Google APIs and similar services.
This tutorial was commit 82fe08e on GitHub.
Preparation
Before starting this tutorial, you’ll need the following:
- alexyoung / dailyjs-backbone-tutorial at commit c1d5a2e7cc
- The API key from part 2
- The “Client ID” key from part 2
- Update app/js/config.js with your keys (if you’ve checked out my source)
To check out the source, run the following commands (or use a suitable Git GUI tool):
git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git cd dailyjs-backbone-tutorial git reset --hard c1d5a2e7ccGoogle’s Tasks API
To recap: the point of this tutorial series is to build a Backbone.js single page application that uses client-side JavaScript to communicate with Google’s authentication and to-do list APIs. Got that? Good!
Google provides access to our to-do lists through two APIs:
When loading Google’s JavaScript, the browser is bestowed with a global called gapi that provides access to various objects and methods. In the last part, I quietly included a call to gapi.client.load that loads the tasks API:
gapi.client.load('tasks', 'v1', function() { /* Loaded */ });This can be found in app/js/gapi.js. The remaining challenge before building the interface is to implement a new Backbone.sync method that uses gapi to communicate with the Tasks and Tasklists APIs.
Backbone.sync Structure
I’ve already talked about the overall structure of Backbone.sync in part 2. The pattern I’ll use in these tutorials is fairly generic, so you could use the same approach to communicate with something other than Google’s APIs.
The sync method itself takes three arguments, the first of which is the method (create, update, delete, and read). We need to map method to something Google’s API can understand.
This is what we’ve got so far:
Backbone.sync = function(method, model, options) { options || (options = {}); switch (method) { case 'create': break; case 'update': break; case 'delete': break; case 'read': break; } };Google’s Tasks API methods map to the Backbone method argument like this:
Google Tasks API Backbone.sync Method Description insert create Create a new task. update update Update an existing task. delete delete Delete a task. list read Get a list of tasks. Even though Google’s API doesn’t look like the Rails 3-based RESTful API that Backbone.js is designed for out of the box, it’s still very close.
Making Requests with gapi
The gapi object makes requests using this pattern:
- Call one of the gapi.client.tasks methods with the request content to get a request object
- Call request.execute with a callback to send the request
- The callback receives a response object, much like a standard Ajax request
Here’s what this looks like in reality:
var requestContent = {} , request , gapiResource; gapiResource = 'tasks'; requestContent['tasklist'] = tasklistId; // Assuming we have one requestContent['resource'] = model.toJSON(); // 'insert' is for creating new tasks request = gapi.client.tasks[gapiResource].insert(requestContent); // Send the request to the API request.execute(function(res) { // Handle the response });Looking at this, it’s clear that we need two models: Task and TaskList. There also need to be two corresponding collections: Tasks and TaskLists.
Backbone models and collections have URLs – these are used for making API requests. Similarly, Google’s APIs have URLs: tasks and tasklists, so by using the model URL Backbone.sync can determine which API resource is required for a given request.
Models
Create a new directory called app/js/models and add task.js:
define(function() { var Task = Backbone.Model.extend({ url: 'tasks' }); return Task; });You’ll also want to create a app/js/models/tasklist.js:
define(function() { var TaskList = Backbone.Model.extend({ url: 'tasklists' }); return TaskList; });Collections
Create another new directory called app/js/collections and add tasklists.js:
define(['models/tasklist'], function(TaskList) { var TaskLists = Backbone.Collection.extend({ model: TaskList , url: 'tasklists' }); return TaskLists; });We’re going to use the TaskList collection later on to load your task lists.
Making API Requests
Open up app/js/gapi.js and add a new line after line 36:
app.views.auth.$el.hide(); $('#signed-in-container').show(); self.trigger('ready'); // This oneThis 'ready' event will be used to signify that authentication was successful, and the Tasks API is ready for use. Next, add the following two lines to Backbone.sync, inside the read switch case:
case 'read': var request = gapi.client.tasks[model.url].list(options.data); Backbone.gapiRequest(request, method, model, options); break;This code creates a request, and then Backbone.gapiRequest will execute it and delegate the response.
Here’s the most basic Backbone.gapiRequest implementation:
Backbone.gapiRequest = function(request, method, model, options) { var result; request.execute(function(res) { if (res.error) { if (options.error) options.error(res); } else if (options.success) { result = res.items; options.success(result, true, request); } }); };All it does is run request.execute, which is provided by Google, and then maps the result to be compatible with Backbone’s API by running the success and error callbacks.
Just so you can see something is really happening, open app/js/app.js and make it load the TaskLists collection by changing the define invocation at the top:
define([ 'gapi' , 'views/app' , 'views/auth' , 'collections/tasklists' ], function(ApiManager, AppView, AuthView, TaskLists) {Now add this to the connectGapi method:
this.apiManager.on('ready', function() { self.collections.lists.fetch({ data: { userId: '@me' }, success: function(res) { _.each(res.models, function(model) { console.log(model.get('title')); }); }}); });That code uses Underscore’s each method to iterate over each “model” returned by Backbone.sync, which is called by the TaskList collection.
Run the server with npm start, and visit http://localhost:8080. If you run it in a browser that supports console, then you should see your task lists printed out.
If you’ve got this working then you’re not far off building a real world Backbone.js app that communicates with Google’s APIs. The same concepts can be applied to other Google JavaScript APIs.
Summary
The full source for this tutorial can be found in alexyoung / dailyjs-backbone-tutorial, commit fcd653ec6.
Backbone-relationalBackbone-relational provides one-to-one, one-to-many and many-to-one relations between models for Backbone. To use relations, extend Backbone.RelationalModel (instead of the regular Backbone.Model) and define a property relations, containing an array of option objects. Each relation must define (as a minimum) the type, key and relatedModel. Available relation types are Backbone.HasOne and Backbone.HasMany. Backbone-relational features:
- Bidirectional relations, which notify related models of changes through events.
- Control how relations are serialized using the includeInJSON option.
- Automatically convert nested objects in a model's attributes into Model instances using the createModels option.
- Lazily retrieve (a set of) related models through the fetchRelated(key<string>, [options<object>], update<bool>) method.
- Determine the type of HasMany collections with collectionType.
- Bind new events to a Backbone.RelationalModel for:
- addition to a HasMany relation (bind to add:<key>; arguments: (addedModel, relatedCollection)),
- removal from a HasMany relation (bind to remove:<key>; arguments: (removedModel, relatedCollection)),
- reset of a HasMany relation (bind to reset:<key>; arguments: (relatedCollection)),
- changes to the key itself on HasMany and HasOne relations (bind to update:<key>; arguments=(model, relatedModel/relatedCollection)).
Contents
Getting started
Resources to get you started with Backbone-relational:
Installation
Backbone-relational depends on backbone (and thus on underscore). Include Backbone-relational right after Backbone and Underscore:
<script type="text/javascript" src="./js/underscore.js"></script> <script type="text/javascript" src="./js/backbone.js"></script> <script type="text/javascript" src="./js/backbone-relational.js"></script>Backbone-relational has been tested with Backbone 0.9.9 (or newer) and Underscore 1.4.3 (or newer).
Backbone.Relation options
Each Backbone.RelationalModel can contain an array of relations. Each relation supports a number of options, of which relatedModel, key and type are mandatory. A relation could look like the following:
Zoo = Backbone.RelationalModel.extend({ relations: [{ type: Backbone.HasMany, key: 'animals', relatedModel: 'Animal', collectionType: 'AnimalCollection', reverseRelation: { key: 'livesIn', includeInJSON: 'id' // 'relatedModel' is automatically set to 'Zoo'; the 'relationType' to 'HasOne'. } }] }); Animal = Backbone.RelationalModel.extend({ urlRoot: '/animal/' }); AnimalCollection = Backbone.Collection.extend({ model: Animal, url: function( models ) { return '/animal/' + ( models ? 'set/' + _.pluck( models, 'id' ).join(';') + '/' : '' ); } });relatedModel
Value: a string (which can be resolved to an object type on the global scope), or a reference to a Backbone.RelationalModel type.
key
Value: a string. References an attribute name on relatedModel.
type
Value: a string, or a reference to a Backbone.Relation type
Example: Backbone.HasOne or 'HasMany'.
HasOne relations (Backbone.HasOne)
The key for a HasOne relation consists of a single Backbone.RelationalModel. The default reverseRelation.type for a HasOne relation is HasMany. This can be set to HasOne instead, to create a one-to-one relation.
HasMany relations (Backbone.HasMany)
The key for a HasMany relation consists of a Backbone.Collection, containing zero or more Backbone.RelationalModels. The default reverseRelation.type for a HasMany relation is HasOne; this is the only option here, since many-to-many is not supported directly.
Many-to-many relations
A many-to-many relation can be modeled using two Backbone.HasMany relations, with a link model in between:
Person = Backbone.RelationalModel.extend({ relations: [ { type: 'HasMany', key: 'jobs', relatedModel: 'Job', reverseRelation: { key: 'person' } } ] }); // A link object between 'Person' and 'Company', to achieve many-to-many relations. Job = Backbone.RelationalModel.extend({ defaults: { 'startDate': null, 'endDate': null } }) Company = Backbone.RelationalModel.extend({ relations: [ { type: 'HasMany', key: 'employees', relatedModel: 'Job', reverseRelation: { key: 'company' } } ] }); niceCompany = new Company( { name: 'niceCompany' } ); niceCompany.bind( 'add:employees', function( model, coll ) { // Will see a Job with attributes { person: paul, company: niceCompany } being added here }); paul.get( 'jobs' ).add( { company: niceCompany } );keySource
Value: a string. References an attribute on the data used to instantiate relatedModel.
Used to override key when determining what data to use when (de)serializing a relation, since the data backing your relations may use different naming conventions. For example, a Rails backend may provide the keys suffixed with _id or _ids. The behavior for keySource corresponds to the following rules:
- When a relation is instantiated, the contents of the keySource are used as it's initial data.
- The application uses the regular key attribute to interface with the relation and the models in it; the keySource is not available as an attribute for the model.
So you may be provided with data containing animal_ids, while you want to access this relation as zoo.get( 'animals' );.
NOTE: for backward compatibility reasons, setting keySource will set keyDestination as well. This means that when saving zoo, the animals attribute will be serialized back into the animal_ids key.
WARNING: when using a keySource, you should not use that attribute name for other purposes.
keyDestination
Value: a string. References an attribute to serialize relatedModel into.
Used to override key (and keySource) when determining what attribute to be written into when serializing a relation, since the server backing your relations may use different naming conventions. For example, a Rails backend may expect the keys to be suffixed with _attributes for nested attributes.
When calling toJSON on a model (either via Backbone.sync, or directly), the data in the key attribute is transformed and assigned to the keyDestination.
So you may want a relation to be serialized into the animals_attributes key, while you want to access this relation as zoo.get( 'animals' );.
WARNING: when using a keyDestination, you should not use that attribute name for other purposes.
collectionType
Value: a string (which can be resolved to an object type on the global scope), or a reference to a Backbone.Collection type.
Determine the type of collections used for a HasMany relation. If you define a url(models<Backbone.Model[]>) function on the specified collection, this enables fetchRelated to fetch all missing models in one request, instead of firing a separate request for each. See Backbone-tastypie for an example of a url function that can build a url for the collection (or a subset of models).
collectionKey
Value: a string or a boolean
Used to create a back reference from the Backbone.Collection used for a HasMany relation to the model on the other side of this relation. By default, the relation's key attribute will be used to create a reference to the RelationalModel instance from the generated collection. If you set collectionKey to a string, it will use that string as the reference to the RelationalModel, rather than the relation's key attribute. If you don't want this behavior at all, set collectionKey to false (or any falsy value) and this reference will not be created.
collectionOptions
Value: an options hash or a function that accepts an instance of a Backbone.RelationalModel and returns an option hash
Used to provide options for the initialization of the collection in the "Many"-end of a HasMany relation. Can be an options hash or a function that should take the instance in the "One"-end of the "HasMany" relation and return an options hash
includeInJSON
Value: a boolean, a string referencing one of the model's attributes, or an array of strings referencing model attributes. Default: true.
Determines how the contents of a relation will be serialized following a call to the toJSON method. If you specify a:
- Boolean: a value of true serializes the full set of attributes on the related model(s). Set to false to exclude the relation completely.
- String: include a single attribute from the related model(s). For example, 'name', or Backbone.Model.prototype.idAttribute to include ids.
- String[]: includes the specified attributes from the related model(s).
Only specifying true is cascading, meaning the relations of the model will get serialized as well!
createModels
Value: a boolean. Default: true.
Should models be created from nested objects, or not?
reverseRelation
If the relation should be bidirectional, specify the details for the reverse relation here. It's only mandatory to supply a key; relatedModel is automatically set. The default type for a reverseRelation is HasMany for a HasOne relation (which can be overridden to HasOne in order to create a one-to-one relation), and HasOne for a HasMany relation. In this case, you cannot create a reverseRelation with type HasMany as well; please see Many-to-many relations on how to model these type of relations.
Please note: if you define a relation (plus a reverseRelation) on a model, but never actually create an instance of that model, the model's constructor will never run, which means it's initializeRelations will never get called, and the reverseRelation will not be initialized either. In that case, you could either define the relation on the opposite model, or define two single relations. See issue 20 for a discussion.
autoFetch
Value: a boolean or an Object (see below). Default: false.
If this property is set to true, when a model is instantiated the related model is automatically fetched using fetchRelated. The value of the property can also be an object. In that case the related model is automatically fetched and the object is passed to fetchRelated as the options parameter.
var Shop = Backbone.RelationalModel.extend({ relations: [{ type: Backbone.HasMany, key: 'customers', relatedModel: 'Customer', autoFetch: true },{ type: Backbone.HasOne, key: 'address', relatedModel: 'Address', autoFetch: { success: function(model, response){ //... }, error: function(model, response){ //... } } } ]Backbone.RelationalModel
Backbone.RelationalModel introduces a couple of new methods, events and properties.
Methods
getRelations relationalModel.getRelations()
Returns the set of initialized relations on the model.
fetchRelated relationalModel.fetchRelated(key<string>, [options<object>], [update<boolean>])
Fetch models from the server that were referenced in the model's attributes, but have not been found/created yet. This can be used specifically for lazy-loading scenarios. Setting update to true guarantees that the model will be fetched from the server and any model that already exists in the store will be updated with the retrieved data. The options object specifies options to be passed to Backbone.sync.
By default, a separate request will be fired for each additional model that is to be fetched from the server. However, if your server/API supports it, you can fetch the set of models in one request by specifying a collectionType for the relation you call fetchRelated on. The collectionType should have an overridden url(models<Backbone.Model[]>) method that allows it to construct a url for an array of models. See the example at the top of Backbone.Relation options or Backbone-tastypie for an example.
Methods on the type itself
Several methods don't operate on model instances, but are defined on the type itself.
setup ModelType.setup()
Initialize the relations and submodels for the model type. See the Q and A for a possible scenario where it's useful to call this method manually.
build ModelType.build(attributes<object>, [options<object>])
Create an instance of a model, taking into account what submodels have been defined.
findOrCreate ModelType.findOrCreate(attributes<string|number|object>, [options<object>])
Search for a model instance in the Backbone.Relational.store.
- If attributes is a string or a number, findOrCreate will just query the store and return a model if found.
- If attributes is an object, the model will be updated with attributes if found. Otherwise, a new model is created with attributes (unless options.create is explicitly set to false).
Events
- add: triggered on addition to a HasMany relation.
Bind to add:<key>; arguments: (addedModel<Backbone.Model>, related<Backbone.Collection>).- remove: triggered on removal from a HasMany relation.
Bind to remove:<key>; arguments: (removedModel<Backbone.Model>, related<Backbone.Collection>).- update: triggered on changes to the key itself on HasMany and HasOne relations.
Bind to update:<key>; arguments: (model<Backbone.Model>, related<Backbone.Model|Backbone.Collection>).Properties
Properties can be defined along with the subclass prototype when extending Backbone.RelationalModel or a subclass thereof.
subModelTypes
Value: an object. Default: {}.
A mapping that defines what submodels exist for the model (the superModel) on which subModelTypes is defined. The keys are used to match the subModelTypeAttribute when deserializing, and the values determine what type of submodel should be created for a key. When building model instances from data, we need to determine what kind of object we're dealing with in order to create instances of the right subModel type. This is done by finding the model for which the key is equal to the value of the submodelTypeAttribute attribute on the passed in data.
Each subModel is considered to be a proper submodel of its superclass (the model type you're extending), with a shared id pool. This means that when looking for an object of the supermodel's type, objects of a submodel's type can be returned as well, as long as the id matches. In effect, any relations pointing to the supermodel will look for instances of it's submodels as well.
Example:
Mammal = Animal.extend({ subModelTypes: { 'primate': 'Primate', 'carnivore': 'Carnivore' } }); var Primate = Mammal.extend(); var Carnivore = Mammal.extend(); var MammalCollection = AnimalCollection.extend({ model: Mammal }); // Create a collection that contains a 'Primate' and a 'Carnivore'. var mammals = new MammalCollection([ { id: 3, species: 'chimp', type: 'primate' }, { id: 5, species: 'panther', type: 'carnivore' } ]);Suppose that we have an Mammal model and a Primate model extending Mammal. If we have a Primate object with id 3, this object will be returned when we have a relation pointing to a Mammal with id 3, as Primate is regarded a specific kind of Mammal; it's just a Mammal with possibly some primate-specific properties or methods.
Note that this means that there cannot be any overlap in ids between instances of Mammal and Primate, as the Primate with id 3 will be the Mammal with id 3.
subModelTypeAttribute
Value: a string. Default: "type".
The subModelTypeAttribute is a references an attribute on the data used to instantiate relatedModel. The attribute that will be checked to determine the type of model that should be built when a raw object of attributes is set as the related value, and if the relatedModel has one or more submodels.
See subModelTypes for more information.
Example
paul = new Person({ id: 'person-1', name: 'Paul', user: { id: 'user-1', login: 'dude', email: 'me@gmail.com' } }); // A User object is automatically created from the JSON; so 'login' returns 'dude'. paul.get('user').get('login'); ourHouse = new House({ id: 'house-1', location: 'in the middle of the street', occupants: ['person-1', 'person-2', 'person-5'] }); // 'ourHouse.occupants' is turned into a Backbone.Collection of Persons. // The first person in 'ourHouse.occupants' will point to 'paul'. ourHouse.get('occupants').at(0); // === paul // If a collection is created from a HasMany relation, it contains a reference // back to the originator of the relation ourHouse.get('occupants').livesIn; // === ourHouse // the relation from 'House.occupants' to 'Person' has been defined as a bi-directional HasMany relation, // with a reverse relation to 'Person.livesIn'. So, 'paul.livesIn' will automatically point back to 'ourHouse'. paul.get('livesIn'); // === ourHouse // You can control which relations get serialized to JSON (when saving), using the 'includeInJSON' // property on a Relation. Also, each object will only get serialized once to prevent loops. paul.get('user').toJSON(); /* result: { email: "me@gmail.com", id: "user-1", login: "dude", person: { id: "person-1", name: "Paul", livesIn: { id: "house-1", location: "in the middle of the street", occupants: ["person-1"] // just the id, since 'includeInJSON' references the 'idAttribute' }, user: "user-1" // not serialized because it is already in the JSON, so we won't create a loop } } */ // Load occupants 'person-2' and 'person-5', which don't exist yet, from the server ourHouse.fetchRelated( 'occupants' ); // Use the 'add' and 'remove' events to listen for additions/removals on HasMany relations (like 'House.occupants'). ourHouse.bind( 'add:occupants', function( model, coll ) { // create a View? console.debug( 'add %o', model ); }); ourHouse.bind( 'remove:occupants', function( model, coll ) { // destroy a View? console.debug( 'remove %o', model ); }); // Use the 'update' event to listen for changes on a HasOne relation (like 'Person.livesIn'). paul.bind( 'update:livesIn', function( model, attr ) { console.debug( 'update to %o', attr ); }); // Modifying either side of a bi-directional relation updates the other side automatically. // Make paul homeless; triggers 'remove:occupants' on ourHouse, and 'update:livesIn' on paul ourHouse.get('occupants').remove( paul.id ); paul.get('livesIn'); // yup; nothing. // Move back in; triggers 'add:occupants' on ourHouse, and 'update:livesIn' on paul paul.set( { 'livesIn': 'house-1' } );This is achieved using the following relations and models:
House = Backbone.RelationalModel.extend({ // The 'relations' property, on the House's prototype. Initialized separately for each instance of House. // Each relation must define (as a minimum) the 'type', 'key' and 'relatedModel'. Options are // 'includeInJSON', 'createModels' and 'reverseRelation', which takes the same options as the relation itself. relations: [ { type: Backbone.HasMany, // Use the type, or the string 'HasOne' or 'HasMany'. key: 'occupants', relatedModel: 'Person', includeInJSON: Backbone.Model.prototype.idAttribute, collectionType: 'PersonCollection', reverseRelation: { key: 'livesIn' } } ] }); Person = Backbone.RelationalModel.extend({ relations: [ { // Create a (recursive) one-to-one relationship type: Backbone.HasOne, key: 'user', relatedModel: 'User', reverseRelation: { type: Backbone.HasOne, key: 'person' } } ], initialize: function() { // do whatever you want :) } }); PersonCollection = Backbone.Collection.extend({ url: function( models ) { // Logic to create a url for the whole collection, or a set of models. // See the tests, or Backbone-tastypie, for an example. return '/person/' + ( models ? 'set/' + _.pluck( models, 'id' ).join(';') + '/' : '' ); } }); User = Backbone.RelationalModel.extend();Known problems and solutions
Q: (Reverse) relations or submodels don't seem to be initialized properly (and I'm using CoffeeScript!)
A: You're probably using the syntax class MyModel extends Backbone.RelationalModel instead of MyModel = Backbone.RelationalModel.extend. This has advantages in CoffeeScript, but it also means that Backbone.Model.extend will not get called. Instead, CoffeeScript generates piece of code that would normally achieve roughly the same. However, extend is also the method that Backbone-relational overrides to set up relations and other things as you're defining your Backbone.RelationalModel subclass.
For exactly this scenario where you're not using .extend, Backbone.RelationalModel has the .setup method, that you can call manually after defining your subclass CoffeeScript-style. For example:
class MyModel extends Backbone.RelationalModel relations: [ // etc ] MyModel.setup()See issue #91 for more information.
Q: After a fetch, I don't get add:<key> events for nested relations.
A: This is due to Backbone.Collection.reset silencing add events. Pass fetch( {add: true} ) to bypass this problem. You may want to override Backbone.Collection.fetch for this, and also trigger an event when the fetch has finished while you're at it. Example:
var _fetch = Backbone.Collection.prototype.fetch; Backbone.Collection.prototype.fetch = function( options ) { options || ( options = {} ); _.defaults( options, { add: true } ); // Remove old models this.reset(); // Call 'fetch', and trigger an event when done. var dit = this, request = _fetch.call( this, options ); request.done( function() { if ( !options.silent ) { dit.trigger( 'fetch', dit, options ); } }); return request; };Under the hood
Each Backbone.RelationalModel registers itself with Backbone.Store upon creation (and is removed from the Store when destroyed). When creating or updating an attribute that is a key in a relation, removed related objects are notified of their removal, and new related objects are looked up in the Store.
Backbone app skeletonJust clone this repo and use it as a starting point for your new Backbone.js based voyages.
Assumptions
Skeletons like these can work well only if there are certain assumptions/conventions.
- We assume that you use CoffeeScript for all your scripting.
- We assume that you use SASS for your styling.
- We assume that you use HAML for your base DOM.
- We assume that you use HAML(c) for your templates for Backbone views.
What gives
It provides:
- directory structure
- stubs for important foundation files
- application-wide events
- sophisticated and customizable init process
- compilation of CoffeeScript/SASS/HAML to native JS/CSS/HTML via Guard
- concatenation and compression of assets via Jammit
- self contained Ruby web server through Sinatra
The skeleton
Let's take a look at the actual structure. Inspired by Rails, I've created a skeleton Backbone application, for which the code was extracted from my latest production app.
So if we get down to business, this is the gist of it, or the skeleton of the skeleton:
. ├── Gemfile ├── Guardfile ├── config/ | └── assets.yml ├── public/ | ├── assets/ | ├── css/ | └── js/ | ├── _lib | └── templates ├── server └── src ├── coffeescript | ├── App.js.coffee | ├── Events.js.coffee | ├── init.js.coffee | ├── log.js.coffee | ├── ns.js.coffee | ├── Router.js.coffee | ├── models | | └── Model.js.coffee | └── views | └── ModelView.js.coffee ├── haml | └── index.html.haml ├── sass | └── application.css.sass └── templatesLet's examine all of these and learn about what they do and why they're important.
Gemfile
The Gemfile is simply instructions to the Ruby Bundler about which libraries we need. We're installing only Guard and its dependencies like HAML, SASS and CoffeeScript.
Guardfile
The Guardfile are instructions for Guard so it knows which files to watch for changes and what to do with them.
It's basically just using various Guard plugins that just need to know the input and output directories and an optional watch statement.
guard 'haml', :input => 'src/haml', :output => 'public/' do watch %r{^.+(\.html\.haml)\Z} end guard 'haml-coffee', :input => 'src/templates', :output => 'public/js/templates' do watch %r{^.+(\.js\.hamlc)\Z} end guard 'sass', :input => 'src/sass', :output => 'public/css' guard 'coffeescript', :input => 'src/coffeescript', :output => 'public/js' guard :jammit, :config_path => 'config/assets.yml' do watch %r{^public/js/(.*)\.js$} watch %r{^public/css/(.*)\.css$} endYou can learn a lot about the structure, by just examining these lines.
config/ and assets.yml
Config houses configuration and in the skeleton, the only configuration is for the Jammit library.
It lists which JavaScript and CSS files to concatenate together, in which order to do it and where to deposit the result.
public/
This is the "root" where you'll point your web server in production (in the included server, this is done already).
These files are ignored from Git, because they should always be reconstructed from source files in the src/ directory.
Oh, and Guard also compiles HTML files directly into public/.
public/assets/
Jammit puts in all the concatenated and compressed code. This is your actual end product.
public/css/
Guard compiles SASS into this directory.
public/js/
Guard compiles CoffeeScript into this directory.
public/js/_lib/
There is one special sub-directory here _lib. It's special because it's not ignored from Git. Inside you put whatever JavaScript libraries you'd like to use, such as jquery.cookie for cookie management.
public/js/templates/
Into templates/ Guard compiles your HAML Coffee templates, that you use in Backbone views.
server/
This irectory houses the Ruby/Sinatra web server. Consult the main app.rb file, where you can play with your server-side, mock up and API, etc.
src/
The heart and soul (i mean source) of your app.
src/coffeescript/
This is where all the JavaScript is concieved.
App.js.coffee
App is the main class. It holds critical initialization code and application wide events.
Backbone has an Events module, that can be plugged into your own classes.
# Use Backbone's events in your master class. events: _.extend(BBNS.Events, Backbone.Events)Events.js.coffee
Defines our own BBNS.Events class, that I use here to define s shorthand for the Events#trigger method.
init.js.coffee
This is the file that boots us up. It first makes an instance of the App class from App.js.coffee, then wires in the DOM load and other events.
log.js.coffee
A custom logging function. Nothing fancy here, just outputs the timestamp and ensures that things don't break even if window.console isn't defined.
It features also logging at different levels. For example, if you set debug to 6 in App.js.coffee, you will see all the app-wide events that are being triggered:
ns.js.coffee
Here we declare our root name space. All other files declare properties of this root name space.
In the skeleton app this is window.BBNS, which stands for Backbone Name Space. You should change this for your own needs.
Router.js.coffee
Defines the root Backbone router class. For my purposes I never needed more than one router until now. When I do, I'll put them under a separate routers/ directory like models and views.
src/models/
Define Backbone models here that will hold, manipulate, load and persist your data.
src/views/
Define Backbone views here and name them <model_or_functionality>View.js.coffee. Just append View to all files for consistency's sake.
haml/
Here we define the root file that the browser needs to load our app. index.html.haml from here becomes public/index.html when Guard is done with it.
It's unusual to need more than one file here, but depends on the complexity of the app.
sass/
Style your app here. I usually use just one main application.css.sass file and then use SASS @import statements to modularize code.
Why? If you try to compile partial stylesheets with Guard and you're using some helpers defined in the main file, Guard will complain.
templates/
Put in here your templates for use in Backbone Views and written in HAML Coffee. They get compiled into public/js/templates/.
Usage
Getting started with a new app using my skeleton is trivial. It uses Ruby in several critical places, so be sure you have a working installation of Ruby, preferably of the 1.9 kind.
Start by cloning my backbone-skeleton repo.
$ git clone https://github.com/mihar/backbone-skeleton.git my-new-backbone-appThen use the bundle command that comes with Ruby Bundler to install the necessary dependencies for guard. Guard will compile our HAML, SASS and CoffeeScript to their native counterparts.
$ cd my-new-backbone-app $ bundleOnce Bundler completes the installation, we can try starting Guard, to immediately start watching files for changes.
$ bundle exec guardWhile leaving guard running, go to another terminal and let's fire up a simple, bundled Ruby web server, that we'll use for development. The server will install all of it's dependencies by itself.
$ rake server [1/1] Isolating sinatra (>= 0). Fetching: rack-1.4.1.gem (100%) Fetching: rack-protection-1.2.0.gem (100%) Fetching: tilt-1.3.3.gem (100%) Fetching: sinatra-1.3.3.gem (100%) [2012-12-05 18:17:05] INFO WEBrick 1.3.1 [2012-12-05 18:17:05] INFO ruby 1.9.3 (2012-02-16) [x86_64-darwin11.3.0] [2012-12-05 18:17:05] INFO WEBrick::HTTPServer#start: pid=39675 port=9292Now our server is listening on http://localhost:9292, so go ahead, and open that.
If you see "Skeleton closet", everything is go.
Go check out the JavaScript console for more information.
Vertebrae front-end framework built with Backbone.js and RequireJS using AMDVertebrae provides AMD structure and additional objects for extending Backbone.js as an application framework.
Project / Goals
The project goals included... dynamic script loading, AMD module format, dependency management, build with mostly open source libraries, organize code in packages, optimize and build for one or many single page apps, host on fully cached server, e.g. no server-side scripting using only an API for data, and the funnest for me, use behaviour driven development for the project. There is a description on the project at : www.hautelooktech.com - Vertebrae post
The Problem:
Selected libraries for the framework (jQuery, Underscore.js, Backbone.js, RequireJS, Mustache) provide module loading, dependency management, application structure (for models, collections, views and routes), asynchronous interactions with API, various utilities and objects to manage asynchronous behaviors, e.g. (Promises) Deferreds, Callbacks. The remaining logic needed to complete the framework includes:
- An object (model) to manage state of the single-page application;
- A layout manager to present, arrange/transition and clear views, and
- Controllers which respond to routes, get/set application state, and hand off work to layout manager.
Our Solutions (implemented in Vertebrae):
Application State Manager -
The application manager stores data in memory and also persists data in browser storage to provide a resource for common data/metadata. Also provides data (state) to reconstruct the page views based on previous interactions (e.g. selected tab, applied filters). The application state manager provides a strategy for resources to retrieve state. Meant to act as a state machine.
Layout Manager -
The layout manager has one or many views as well as document (DOM) destinations for each (rendered) view. A page may transition between many views, so the layout manager keeps track of view states, e.g. rendered, not-rendered, displayed, not-displayed. You may use the layout manager to lazy load and render (detached) views that a site visitor is very likely to request, e.g. tab changes on a page. The transition between view states is managed by this object. An entire layout may be cleared so that view objects and their bindings are removed, preparing these objects for garbage collection (preventing memory leaks). The layout manager also communicates view state with controller(s).
Controller -
A controller object is called by a route handler function, and is responsible for getting relevant state (application models) to generate a page (layout), (also responsible for setting state when routes change). The controller passes dependent data (models/collections) and constructed view objects for a requested page to the layout manager. As a side-effect the use of controllers prevents the routes object from becoming bloated and tangled. A route should map to a controller which then kicks off the page view, keeping the route handling functions lean.
The Todos app is hosted both in dev mode and optimized on Heroku...
Many of the concepts in the frameworks are borrowed, e.g. the need to destroy views to prevent memory leaks, as pointed out by Derick Bailey - http://lostechies.com/derickbailey/ ; the Layout Manager by Tim Branyen http://tbranyen.github.com/backbone.layoutmanager/
Backbone.js is meant to be a tool in an application; the Backbone.js library does not provide the architecture needed to build a complete application, however does provide great interactions with an API and solid code structure for... Views (which act like controllers too), Models and Collections (the data layer), and finally* Routes*. We built the Vertebrae framework to meat the goals of our project, and decided to extract the code as a framework for others to use, learn, or whatever.
Build and optimize using:
r.js -o build.jsTo launch (Node.js) server
node app.jsBased on the setting in app.js the server runs from either the src or public directory at : http://localhost:4242/
docroot = path.join(application_root, "src"), // "src" for dev, or "public" for built versionThe build.js file is configured to build to the "public" directory.
So after you run r.js -o build.js to populate the "public" directory then you can use node app.js to view the site at : http://localhost:4242
Testing Framework using Jasmine, Sinon, Jasmine-jQuery
Views:
BaseView, CollectionView, SectionView, LayoutView (Manages Sections)
Base View
A view object to construct a standard view with common properties and utilties The base view extends Backbone.View adding methods for resolving deferreds, rendering, decorating data just in time for rendering, adding child views to form a composite of views under one view object, add a destroy method.
Collection View
Manages rendering many views with a collection
The CollectionView extends BaseView and is intended for rendering a collection. A item view is required for rendering withing each iteration over the models. See: http://liquidmedia.ca/blog/2011/02/lib-js-part-3/
Section View States
Mixin object to track view's state 'not-rendered', 'rendered', 'not-displayed', 'displayed'
A section view is the required view object for a layout view which expects views to track their own state. This view may be extended as need. to use in a layout, perhaps adding the Section prototype properties to another view.
Layout Manager View
Presents, arranges, transitions and clears views
The layout manager has one or many views as well as document (DOM) destinations for each (rendered) view. A page may transition between many views, so the layout manager keeps track of view states, e.g. 'not-rendered', 'rendered', 'not-displayed', 'displayed'.
The layout manager can be utilized to lazy load and render (detached) views. that a site visitor is very likely to request, e.g. tab changes on a page. The transition between view states is managed by this object. An entire layout may be cleared so that view objects and their bindings are removed, preparing these objects for garbage collection (preventing memory leaks). The layout manager also communicates view state with controller(s).
Models:
BaseModel, ApplicationState
Application state model
A model object to manage state within the single-page application Attributes: {String} name, {Object} data, {String} storage, {Date} expires
Collections:
BaseCollection, ApplicationStates
Application state collection
A collection object to reference various states in the application.
The application manager stores data in memory and also persists data in browser storage to provide a resource for common data/metadata. Also provides data (state) to reconstruct the page views based on previous interactions (e.g. selected tab, applied filters). The application state manager provides a strategy for resources to retrieve state.
Syncs:
syncFactory, application-state, storageFactory
Utils:
ajax-options, docCookies, debug, storage, shims, lib [checkType, duckTypeCheck, Channel (pub/sub), loadCss, formatCase, formatMoney]
Controller
A controller object should called withing a route handler function, and may be responsible for getting relevant state (application models) to generate a page (layout), (also responsible for setting state when routes change). The controller passes dependent data (models/collections) and constructed view objects for a requested page to the layout manager. As a side-effect the use of controllers prevents the routes object from becoming bloated and tangled. A route should map to a controller which then kicks off the page view, keeping the route handling functions lean.
Facade
Vendor libraries and specific methods used in the framework are required in the facade, and referenced from the facade module in the views, models, collections, lib and other objects in the framework.
AMD - Asynchronous Module Definition
Using RequireJS script loader and AMD there are a couple options for managing dependencies:
The the examples below the facade module has it's own dependencies for loading vendor libraries and maps them to an object. So vendor libraries Should only be required using the facade module which provides references to the libraries.
define(['facade','utils'], function (facade, utils) { var ModuleName, // References to objects nested in dependencies Backbone = facade.Backbone, $ = facade.$, _ = facade._, lib = utils.lib; ModuleName = DO SOMETHING HERE return ModuleName; }); define(['require','facade','utils'], function (require) { var ModuleName, // Dependencies facade = require('facade'), utils = require('utils'), // References to objects nested in dependencies Backbone = facade.Backbone, $ = facade.$, _ = facade._, lib = utils.lib; ModuleName = DO SOMETHING HERE return ModuleName; });References:
Code examples included in framework: application.js
The application.js has a few routes defined which handle a couple example code packages: products and hello. the chrome package has the Twitter bootstrap markup for rendering header component on the page.
App = Backbone.Router.extend({ routes: { '': 'defaultRoute', 'products': 'showProducts', 'hello': 'showHello', 'hello/': 'showHello', 'hello/:name': 'showHello' },The default route above links to the products route handler loading a list of products on the default page.
Hello World example using a the Layout Manager (View)
Routes in the hello package
/hello /hello/:namewithout the name parameter only one the 'about' view is rendered in the layout; with the parameter e.g. /hello/bill two views are rendered in the layout a 'welcome' and an 'about' view
Part 1: welcome section
Using a Layout view involves: adding a route, route handler function in App, new "hello" package with a template and view. The package controller file hello.js extends the Controller.prototype and is based on a (template) copy of the Controller.prototype in src/controller.js. The WelcomeSectionView prototype extends the SectionView prototype (class) and requries both name and destination properties when instantiated. The application.js method 'showHello' is mapped to the route '/hello/:name' and the showHello method instantiates a controller object
Files edited in the application:
src/application.js src/main.jsFiles added as a new package:
src/packages/hello.js [returns: HelloController] src/packages/hello/models/welcome.js src/packages/hello/templates/layout.html [HTML used by layout, has section element] src/packages/hello/templates/welcome.html src/packages/hello/views/welcome.js [returns: WelcomeSectionView, with article element] src/packages/hello/welcome.cssNew Route added in src/application.js
'hello/:name': 'showHello'New Package added in src/main.js
// ** Packages ** 'hello' : HL.prependBuild('/packages/hello'),Add dependency to application.js
define([ /*... ,*/ "hello" ], function ( /*... ,*/ HelloController ) { // BTW this is the AMD module format with "hello" file as dependecy });Add Method for new '/hello/:name' route handler
showHello: function (name) { controller = new HelloController({ "params": { "name": name }, "route": "/hello/" + name, "appStates" : this.states }); },The parameters hash is added as an option above for the controller object to deal with.
Code to add in your hello package for a welcome section:
src/packages/hello/templates/layout.html
src/packages/hello.js
src/packages/hello/views/welcome.js
src/packages/hello/templates/welcome.html
src/packages/hello/views/welcome.js
src/packages/hello/welcome.cssPart 2: Use JSON data for new about section
To get the 'about' section data a fixture (JSON file) was added in the test directory.
{ "_links": { "self": "/test/fixtures/hello/101", "shop": "http://www.hautelook.com/" }, "title": "About HauteLook", "content": "Welcome to HauteLook, where you will discover thousands of the top fashion and lifestyle brands at amazing savings. Each day at 8 AM Pacific, shop new sale events featuring the best names in women's and men's fashion and accessories, beauty, kids' apparel and toys, and home décor at up to 75% off. Membership is free and everyone is welcome!", "callToAction": "To start shopping, go to: <a href=\"www.hautelook.com\">www.hautelook.com</a>" }The above object is added as a server response in the Node.js app.js as well, so you can simulate using an api for data.
application.js updated with...
routes: { 'hello': 'showHello', 'hello/': 'showHello', 'hello/:name': 'showHello' },showHello: function (name) { controller = new HelloController({ "params": { "name": name }, "route": (name) ? "/hello/" + name : "/hello", "appStates" : this.states, "useFixtures" : true }); },Files added for a about section:
See the source code in the files below for how the new "About" section view is added to the layout (in addition to the simple hello name view created by the welcome view)...
src/packages/hello/templates/about.html
src/packages/hello/views/about.js
src/packages/hello/models/about.jsFiles changed to support the additional section:
Some files should have changed when working with the Hello World Part 2 example, and others need to be added for the about section...
src/packages/hello.js
src/packages/hello/templates/layout.html
src/packages/hello/templates/welcome.html
src/packages/hello/views/welcome.js
src/packages/hello/welcome.cssDocumentation:
Using docco annotated source code documentation is found in these directories:
/docs/ /models/docs/ /collections/docs/ /syncs/docs/ /utils/docs/ /views/docs/View docs at:
http://localhost:4242/docs/
http://localhost:4242/models/docs/
http://localhost:4242/collections/docs/
http://localhost:4242/syncs/docs/
http://localhost:4242/utils/docs/
http://localhost:4242/views/docs/Or, view docs on the demo site hosted on heroku at:
http://vertebrae-framework.herokuapp.com/docs/
http://vertebrae-framework.herokuapp.com/models/docs/
http://vertebrae-framework.herokuapp.com/collections/docs/
http://vertebrae-framework.herokuapp.com/syncs/docs/
http://vertebrae-framework.herokuapp.com/utils/docs/
http://vertebrae-framework.herokuapp.com/views/docs/To generate the docs:
cd src/ docco application.js facade.js controller.js cd models docco *.js cd ../collections docco *.js cd ../syncs docco *.js cd ../utils docco *.js cd ../views docco *.js cd ../ rm docs/docco.css collections/docs/docco.css models/docs/docco.css syncs/docs/docco.css utils/docs/docco.css views/docs/docco.cssReferences:
ThoraxAn opinionated, battle tested Backbone + Handlebars framework to build large scale web applications.
Standalone
Open the index.html file from the downloaded project in your browser.
Run npm start from the downloaded project.
Mobile
Run npm start from the downloaded project.
Rails
Run rails server from the downloaded project.
Download 2.0.0b5 Download 2.0.0b5 Download 2.0.0b5 Download 2.0.0b5 Thorax 2 is presently in beta, the stable source and documentation are still available.
Thorax can be used standalone in any JavaScript environment in addition the boilerplate projects provided above.
var view = new Thorax.View({ template: "Hello world!" }) view.render() $("body").append(view.el)Registry
Thorax creates a special hash for each type of class to store all subclasses in your application. The use of Thorax.Views and Thorax.templates is required to allow the view, template and other helper methods to operate, but the use of the others are optional and provided for consitency.
Class Registry Thorax.View Thorax.Views Thorax.Model Thorax.Models Thorax.Collection Thorax.Collections Thorax.Router Thorax.Routers templates Thorax.templates name klass.prototype.name
If a name property is passed to any Thorax classes' extend method the resulting class will be automatically set in the corresponding registry.
Thorax.View.extend({ name: "my-view" }); Thorax.Views["my-view"]templates Thorax.templates
A hash of templates, used by various Thorax helpers. If using the node or Rails boilerplate projects this hash will be automatically generated from the files in your templates directories. To manually add a template to the hash:
Thorax.templates['my-template-name'] = Handlebars.compile('template string');If a View has the same name as a template in the templates hash, it's `template' property will be automatically assigned.
Thorax.View
The base Thorax.View implementation is concerned only with Handlebars + Backbone integration. A variety of additional functionality is provided by the various included plugins. The boilerplate projects have a build of Thorax with all plugins included.
var view = new Thorax.View({ template: "{{key}}", key: "value" }); view.render(); $('body').append(view.el);children view.children
A hash of child view's indexed by cid. Child views may become attached to the parent with the view helper or may be automatically attached HelperView instanced created by helpers created with regsterViewHelper.
parent view.parent
If a view was embedded inside another with the view helper, or is a HelperView created by a helper creted with registerViewHelper the view will have a parent attribute.
template view.template
Assign a template to a view. This may be a string or a function which recieves a single context argument and returns a string. If the view has a name and a template of the same name is available the template will be auto-assigned.
new Thorax.View({ template: "{{key}}" });destroy view.destroy([options])
By default this will only call destroy on all child views. Other plugins override this method to implement custom cleanup behaviors. Your own behaviors can be added with the destroyed event. Pass children: false to this method to prevent the view's children from being destroyed.
render view.render([content])
Renders the view's template updating the view's el with the result, triggering the rendered event. content may be empty (render the template) or a function that will be called with the response from context or a string.
view.render() view.render('custom html')context view.context()
Used by render to determine what attributes are available in the view's template. The default context function returns this. If the model plugin is used, this + model.attributes will be the context.
Render a template with the view's context plus any optional extraContext parameters passed in.
ensureRendered view.ensureRendered()
Ensure that the view has been rendered at least once.
html view.html([content])
Get or set the innerHTML of the view, without triggering the rendered event.
View Helpers
super {{super}}
Embed the template from the parent view within the child template.
{{super}}template {{template name [options]}}
Embed a template inside of another, as a string. An associated view (if any) will not be initialized. By default the template will be called with the current scope but extra options may be passed which will be added to the context.
{{template "path/to/template" key="value"}}If a block is used, the template will have a variable named yield available that will contain the contents of the block.
{{#template "child"}} content in the block will be available in a variable named "yield" inside the template "child" {{/template}}This is useful when a child template will be called from multiple different parents.
view {{view name [options]}}
Embed one view in another. The first argument may be the name of a new view to initialize or a reference to a view that has already been initialized.
{{view "path/to/view" key="value"}} {{view viewInstance}}element {{element name [options]}}
Embed a DOM element in the view. This uses a placeholder technique to work, if the placeholder must be of a certain type in order to work (for instance a tbody inside of a table) specify a tag option.
{{element domElement tag="tbody"}}registerViewHelper Handlebars.registerViewHelper(name [,viewClass] ,callback)
Note that this differs from Handlebars.registerHelper. Registers a helper that will create and append a new HelperView instance, with it's template attribute set to the value of the captured block. callback will recieve any arguments passed to the helper followed by a HelperView instance. Named arguments to the helper will be present on options attribute of the HelperView instance.
A HelperView instance differs from a regular view instance in that it has a parent attribute which is always set to the declaring view, and a context which always returns the value of the parent's context method. The collection, empty and other built in block view helpers are created with registerViewHelper.
A helper that re-rendered a HelperView every time an event was triggered on the declaring view could be implemented as:
Handlebars.registerViewHelper('on', function(eventName, helperView) { //register a handler on the parent view, which will be automatically //unregistered when helperView is destroyed helperView.on(helperView.parent, eventName, function() { helperView.render(); }); });An example use of this would be to have a counter that would incriment each time a button was clicked. In Handlebars:
{{#on "incrimented"}}{{i}}{/on}} {{#button trigger="incrimented"}}Add{{/button}}And the corresponding view class:
new Thorax.View({ events: { incrimented: function() { ++this.i; } }, initialize: function() { this.i = 0; }, template: ... });Util
tag Thorax.Util.tag(name, htmlAttributes [,content] [,context])
Generate an HTML string. All built in HTML generation uses this method. If context is passed any Handlebars references inside of the htmlAttributes values will rendered with the context.
Thorax.Util.tag("div", { id: "div-{{number}}" }, "content of the div", { number: 3 });$
$.view $(event.target).view([options])
Get a reference to the nearest parent view. Pass helper: false to options to exclude HelperViews from the lookup. $.model and $.collection will also be available if you include the model and collection plugins.
$(event.target).view()Events
destroyed destroyed ()
Triggered when the destroy method is called.
rendered rendered ()
Triggered when the rendered method is called.
child child (instance)
Triggered every time a child view is inserted into the view with the view helper. Will not be triggered from view instances created by helper view helpers.
helper helper (name [,args...] ,helperView)
Triggered when a view helper (such as collection, empty, etc) create a new HelperView instance.
helper:name helper:name [,args...] ,helperView)
Triggered when a given view helper creates a new HelperView instance.
{{ view.on('helper:collection', function(collection, collectionView) { });HTML Attributes
Thorax and it's view helpers generate a number of custom HTML attributes that may be useful in debugging or generating CSS selectors to be used as arguments to $ or to create CSS. The *-cid attributes are generally used only internally. See $.model, $.collection and $.view to get a reference to objects directly from the DOM. The *-name attributes will only be present if the given objects have a name property.
Attribute Name Attached To data-view-cid Every view instances' el data-view-name Same as above, only present on named views data-collection-cid Element generated by the collection helper data-collection-name Same as above, only present when the bound collection is named data-collection-empty Set to "true" or "false" depending on wether the bound collection isEmpty data-model-cid A view's el if a model was bound to the view or each item element inside of elements generated by the collection helper data-model-name Same as above, only present if the model is named data-layout-cid The element generated by the layout helper or el inside of a LayoutView or ViewController instance data-view-helper Elements generated by various helpers including collection and empty from the collection plugin data-call-method Elements generated by the link and button helpers data-trigger-event Elements generated by the link and button helpers When creating CSS selectors it's recommended to use the generated attributes (especially data-view-name) rather than assigning custom IDs or class names for the sole purpose of styling.
[data-view-name="my-view-name"] { border: 1px solid #ccc; }Command Line
To use the command line utilities:
npm install -g thoraxbuild thorax build target [plugin] [plugin...]
Build a custom version of Thorax using a list of any of the given plugins:
- mixin
- event
- model
- collection
- helpers
- form
- view-controller
- loading
- mobile
Not specifying any plugins will build a version with all plugins except mobile. To build a version of Thorax with all plugins including the mobile plugin run:
thorax build ./thorax-mobile.jstemplates thorax templates ./templates ./templates.js
If using Thorax outside of the provided node or Rails downloads you can inline a directory of templates into a single file by running the thorax templates command.
npm install -g thorax thorax templates ./templates-dir ./templates.jsError Handling
onException Thorax.onException(name, error)
Bound DOM event handlers in Thorax are wrapped with a try / catch block, calling this function if an error is caught. This hook is provided primarily to allow for easier debugging in Android environments where it is difficult to determine the source of the error. The default error handler is simply:
function(name, error) { throw error; }Override this function with your own logging / debugging handler. name will be the event name where the error was thrown.
Copyright 2012 @WalmartLabs
Backbone.Syphon - serialize the forms in your Backbone.Views into a JSON object for use with Backbone's models.
Backbone.Syphon
Working with form elements in a Backbone view can become very tedious very quickly. You will either end up writing a lot of repetitive code to read values from the form, or end up using a key-value-observer or data-binding solution that automatically populates your model for you. While these are valid options and I highly recommend understanding how they work, there are times when these options are not the best choice for your application.
Backbone.Syphon aims to make it easy to serialize the form inputs of a Backbone.View in to a simple JSON object that contains all of the values from the form.
Source Code And Downloads
You can download the raw source code from the "src" folder above, or grab one of the builds from the "lib" folder.
To get the latest stable release, use these links which point to the 'master' branch's builds:
Standard Builds
Development: backbone.syphon.js
Production: backbone.syphon.min.js
AMD/RequireJS Builds
Development: backbone.syphon.js
Production: backbone.syphon.min.js
Documentation
This readme file contains basic usage examples.
Extensibility / API Documentation
If you need to modify the behaviors of Syphon, see the API document. It contains the documentation for the core APIs that Syphon exposes, with examples on how to change the behaviors of Syphon.
Annotated Source Code
Syphon has annotated source code using the Docco tool to turn comments in to documentation. This provides an in-depth look at what each section of is doing.
Basic Usage : Serialize
When the data from a form is needed, you can call the serialize method of Backbone.Syphon to retrieve an object literal that contains the data from your view's form.
Backbone.View.extend({ events: { "submit form": "formSubmitted" }, formSubmitted: function(e){ e.preventDefault(); var data = Backbone.Syphon.serialize(this); this.model.set(data); this.model.save(); }, render: function(){ // build the view's form, here } });Keys Retrieved By "name" Attribute
The default behavior for serializing fields is to use the field's "name" attribute as the key in the serialized object.
<form> <input name="a"> <select name="b"></select> <textarea name="c"></textarea> </form>Backbone.Syphon.serialize(view); // will produce => { a: "", b: "", c: "" }For information on how to change this behavior, see the Key Extractors section of the API Documentation.
Values Retrieved By jQuery .val() Call
The default behavior for serializing fields is to use jQuery's .val() to get the value of the input element.
<form> <input name="a" value="a-value"> <textarea name="b">b-value</textarea> </form>Backbone.Syphon.serialize(view); // will produce => { a: "a-value", b: "b-value", }For information on how to change this behavior, see the Input Readers section of the API Documentation.
Checkboxes
By default, a checkbox will return a boolean value signifying whether or not it is checked.
<form> <input type="checkbox" name="a"> <input type="checkbox" name="b" checked> </form>Backbone.Syphon.serialize(view); // will produce => { a: false, b: true }For information on how to change this behavior, see the Input Readers section of the API Documentation.
Radio Button Groups
Radio button groups (grouped by the input element "name" attribute) will produce a single value, from the selected radio button.
<form> <input type="radio" name="a" value="1"> <input type="radio" name="a" value="2" checked> <input type="radio" name="a" value="3"> <input type="radio" name="a" value="4"> </form>Backbone.Syphon.serialize(view); // will produce => { a: "2" }This behavior can be changed by registering a different set of Key Extractors, Input Readers, and Key Assignment Validators. See the full API Documentation. for more information on these.
Basic Usage : Deserialize
Syphon also allows you to deserialize an object's values back on to a form. It uses the same conventions and configuration as the serialization process, with the introduction of Input Writers to handle populating the form fields with the values. See the full API Documentation. for more information on Input Writers.
<form> <input type="text" name="a"> <input type="text" name="b"> </form>var data = { a: "foo", b: "bar" }; Backbone.Syphon.deserialize(this, data);This will populate the form input elements with the correct values from the data parameter.
Ignored Input Types
The following types of input are ignored, and not included in the resulting JavaScript object:
- <input type="submit"> buttons
- <input type="reset"> buttons
- standard <button> tags
- <fieldset> tags
If you need to get a value from the specific button that was clicked, you can either include it specifically (see below) or use a DOM event to listen for that element being manipulated (clicked, for example) and manually grab the data you need.
Ignoring Other Input Types
Syphon exposes the list of ignored input types as a raw array. You can push, pop, and manipulate this array as any other array, to specify which types of input fields you want to ignore.
This list is global to Syphon and there is no way to customize it for a specific call to serialize.
// ignore all <textarea> input elements Backbone.Syphon.ignoredTypes.push("textarea");Serializing Nested Attributes And Field Names
Syphon will parse nested attribute names and create a nested result object, using the Rails standard of name="foo[bar][baz]" by default.
<form> <input type="text" name="foo[bar]" value="a value"> <input type="text" name="foo[baz][quux]" value="another value"> </form>will produce
{ foo: { bar: "a value", baz: { quux: "another value" } } }Include / Exclude Specific Fields
You can include or exclude specific fields as needed. Inclusion is given priority and specifying fields to include will force Syphon to exclude all other fields. Including a field that is ignore by it's type will also force the field to be included.
Examples
Given this HTML:
<form> <input name="a" value="a-value"> <input name="b" value="b-value"> <input name="c" value="c-value"> <button name="d" value="d-value"> </form>The following will occur:
// include a, b only Backbone.Syphon.serialize(view, { include: ["a", "b"] }); // will produce => { a: "a-value", b: "b-value" }// include the normally excluded (button) "d" Backbone.Syphon.serialize(view, { include: ["a", "d"] }); // will produce => { a: "a-value", d: "d-value" }// exclude a Backbone.Syphon.serialize(view, { exclude: ["a"] }); // will produce => { b: "b-value", c: "c-value" }// include a and b, exclude b and c Backbone.Syphon.serialize(view, { include: ["a", "b"], exclude: ["b", "c"] }); // will produce => { a: "a-value", b: "b-value" }Include / Exclude Based On Key Extractors
The include / exclude process uses the registered Key Extractors to determine which fields to include / exclude.
This means if you are only using the default Key Extractor which uses the "name" attribute, all fields will be included or excluded based on the name of the field.
If you have registered other Key Extractors, they will be used when determining which fields to include / exclude.
<form> <input id="a"> <input type="radio" name="b"> <input id="c"> <input type="radio" name="d"> </form>// By default, use the "id" Backbone.Syphon.KeyExtractors.registerDefault(function($el){ return $el.prop("id"); }); // For radio buttons, use the "name" Backbone.Syphon.KeyExtractors.register("radio", function($el){ return $el.prop("name"); }); // Serialize the form Backbone.Syphon.serialize(view, { exclude: ["a", "b"] }); // This will produce => { c: "", d: "" }For more information on Key Extractors, see the full API Documentation.
Other Options
There are a few other options that can be specified when calling the Syphon.serialize method, which allow the behavior of Syphon to be altered for a single call instead of for all calls.
Key Extractors
Key extractors are used to generate the "key" in the {key: "value"} result. You can specify a KeyExtractorSet as part of the options:
extractors = new Backbone.Syphon.KeyExtractorSet(); // configure it ... Backbone.Syphone.serialize({ keyExtractors: extractors });For more information on Key Extractors, see the full API Documentation.
Input Readers
Input Readers are used to generate the "value" in the {key: "value"} result. You can specify a InputReadetSet as part of the options:
readers = new Backbone.Syphon.InputReaderSet(); // configure it ... Backbone.Syphone.serialize({ inputReaders: readers });For more information on Input Readers, see the full API Documentation.
Input Writers
Input Writers are used to set the value of form elements to the "value" in the {key: "value"} data / object. At this time, you cannot specify input writers in the deserialize method. That will come soon, hopefully.
For more information on Input Writers, see the full API Documentation.
Key Assignment Validators
Input Readers are used to validate the assignment of a key to a value, in the context of an element. You can specify a InputReadetSet as part of the options:
validators = new Backbone.Syphon.KeyAssignmentValidators(); // configure it ... Backbone.Syphone.serialize({ keyAssignmentValidators: validators });For more information on Key Assignment Validators, see the full API Documentation.
Current Limitations
There some known limitations in Backbone.Syphon, partially by design and partially implemented as default behaivors.
- You must have a <form> within your view's $el
- An input of type checkbox will return a boolean value. This can be overriden by replacing the Input Reader for checkboxes.
Building Backbone.Syphon
If you wish to build Backbone.Syphon on your system, you will need Ruby to run the Jasmine specs, and NodeJS to run the grunt build.
To Run The Jasmine Specs
Be sure you have Bundler installed in your Ruby Gems. Then run bundle install from the project folder
Once this is done, you can run rake jasmine to run the Jasmine server
Point your browser at http://localhost:8888 and you will see all of the specs for Backbone.Syphon
To Build The Packages
Be sure you have NodeJS and NPM installed on your system
Run npm install -g grunt to install the grunt build system
From the project folder, run grunt to produce a build
Screencasts
I've recorded several screencasts on how I built Syphon.
Release Notes
See the changelog.md file.
Legal Mumbo Jumbo (MIT License)
Copyright (c) 2012 Derick Bailey, Muted Solutions, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Prerequisites
You need a solid knowledge of JavaScript, familiarity with Backbone, Ruby, HAML, SASS and CoffeeScript to find this writeup useful.
Also, I’m developing on a Mac and have not tested this on other platforms. Although, I do not see any reason why it shouldn’t work.
1. Philosophy
Part of the success of Rails was the conventions and its predefined directory tree. While looking overwhelming and maybe annoying to a beginner at first, it soon becomes liberating. With experience things fall into place, and soon you feel feel like every tiny bit of code has it’s dedicated home.
Backbone, being the nimble, does not prescribe any particular code or directory structure. Until I read enough material and settled on this particular layout, I was feeling very confused and disoriented.
This skeleton app was extracted from a production app and then extensively annotated, to explain certain decisions and choices.
2. Backstory
When I first started to play with Backbone I was already heavily entrenched in the Ruby and Rails world. So naturally I thought, yeah, MVC, I know that. It turned out to be a bit farther from the truth than I wanted or cared to admit.
Disclaimer: I rarely developed pure client-side software, though I was using JavaScript extensively to make things faster and more responsive.
Thing is that MVC on the server-side is quite a bit different from MVC on the client. It has something to do with wiping the state clean each time you reload the page. Statelessness.
The client on the other hand is stateful and thus keeps all your bad practices in memory until they start to slow things down and eventually stop working.
This was the biggest client-side project for me so far, building out the Dubjoy editor for dubbing online video.
The hardest part of learning to develop MVC on the client and using Backbone was seeing the big picture. Seeing where all the little parts fit in and how this all works together in the grand scale.
So for the most part, my journey with Backbone consisted of finding out best practices for file and code organization, setting up the environment and directory structures.
Using backbone.js as a library was “easy”. (Not really, but this isn’t what this article’s about.)
One of the biggest mistakes I was making when starting out was trying to use Backbone constructs for everything.
Backbone is intentionally kept simple, because it’s supposed to be a complement to your own JavaScript. So just create your own App class, and populate it with the stuff and initialization your app really needs.
3. Tools
So a good workflow needs good tools. Here I’ll describe the tools that I found indispensable when developing in Backbone.
CoffeeScript, HAML and SASS
Because I resent cruft and redundancy, I’m a big fan of abstraction languages. Whenever I can, I opt for HAML, SASS and CoffeeScript.
The brevity they bring is paramount to me.
HAML Coffee
In Backbone, you usually need a template engine. Templates provide the markup for views. There’s a lot of solutions for this, but because I like to be consistent, the best choice was to use HAML.
Fortunately, there’s a library for this: haml-coffee, which enables you to use HAML intertwined with snippets of CoffeeScript.
Guard
To be able to use these languages seamlessly, you need some sort of a on-demand compiler. Turns out a Ruby gem called Guard does exactly this.
Guard is extremely flexible. It watches for file system changes and then doing something to files that changed.
Jammit
Jammit is an asset packaging library. It concatenates and compresses CSS and Javascript. It’s easy to use, but needs a configuration file, that defines which files to work on.
Sinatra with Isolate
Backbone apps are static files and you can run them directly off your hard drive. But to do proper paths and even maybe some API, we need a server.
Sinatra, a mini Ruby web framework, forms the base of the server. This enables some quick server-side magic as well as making an API for persistence.
To make this part as easy as possible to use, I packaged the server with Isolate, a small Ruby library for sand-boxing, which is like a mini-Bundler. When launching the server with rake server for the first time, it will check and auto-install it’s dependencies. It just works.
4. Using the skeleton
Getting started with a new app using my skeleton is trivial. It uses Ruby in several critical places, so be sure you have a working installation of Ruby, preferably of the 1.9 kind.
All of the files, directories and their meaning is described with more detail in the README file of the skeleton.
A working example of this app is available online. This way you can check if the console output is the same on your local setup and here.
Start by cloning my backbone-skeleton repo.
$ git clone https://github.com/mihar/backbone-skeleton.git my-new-backbone-appThen use the bundle command that comes with Ruby Bundler to install the necessary dependencies for guard. Guard will compile our HAML, SASS and CoffeeScript to their native counterparts.
$ cd my-new-backbone-app $ bundleOnce Bundler completes the installation, we can try starting Guard, to immediately start watching files for changes.
$ bundle exec guardWhile leaving guard running, go to another terminal and let’s fire up a simple, bundled Ruby web server, that we’ll use for development. The server will install all of it’s dependencies by itself.
$ rake server [1/1] Isolating sinatra (>= 0). Fetching: rack-1.4.1.gem (100%) Fetching: rack-protection-1.2.0.gem (100%) Fetching: tilt-1.3.3.gem (100%) Fetching: sinatra-1.3.3.gem (100%) [2012-12-05 18:17:05] INFO WEBrick 1.3.1 [2012-12-05 18:17:05] INFO ruby 1.9.3 (2012-02-16) [x86_64-darwin11.3.0] [2012-12-05 18:17:05] INFO WEBrick::HTTPServer#start: pid=39675 port=9292Now our server is listening on http://localhost:9292, so go ahead, and open that.
If you see “Skeleton closet”, everything is go.
Go check out the JavaScript console for more information.
5. Resources
They host all the libraries, including Backbone and underscore.
Peepcode has been my friend since my Ruby and Rails days. They produce high-quality screencasts on a variety of topics.
They have a series of 3 videos on Backbone, going from the basics to some pretty advanced stuff.
Be prepared to shell out $12 per video, though.
I’ve learned so much about the correct ways to do things on Derick’s blog. He’s a seasoned Backbone developer that has overcome many problems and written up on the progress. Wealth of resources.
Haven’t seen this one yet, but if I’m judging by his blog, this should be very worth the money. 4 videos, $12 a pop.
Documented patterns extracted from building many Backbone apps.
Article exploring similar problems of code/directory structure and organizations.
A much bigger project with a similar goal as mine.
Backbone.LayoutManagerCreated by Tim Branyen @tbranyen, with help from awesome contributors
Provides a logical structure for assembling layouts with Backbone Views. Designed to be adaptive and configurable for painless integration. Well tested, with over 140 assertions and 100% code coverage!
Tested with Underscore & Lo-Dash, Backbone and jQuery. You can swap out jQuery with a custom configuration or substitute Underscore with Lo-Dash.
Documentation
Refer to: http://tbranyen.github.com/backbone.layoutmanager/
Migration guide: https://github.com/tbranyen/backbone.layoutmanager/pull/184
Release notes
- Refactored _.extend to LayoutManager.augment to work with Lo-Dash and underscore.
- Normalized rendering order, when the parent has already rendered.
- Added better error handling for node.js build.
- Updated error message for node.js build.
- Added in Travis-CI and README updates.
Donate
I do my very best to ensure top quality and continued progress with LayoutManager. Developers using, but not contributing, may want to consider leaving a small donation to show their appreciation.
All funds collected will find their way to the mspca organization. Thanks! :)
Chaplin is an architecture for JavaScript applications using the Backbone.js library. Chaplin addresses Backbone's limitations by providing a lightweight and flexible structure that features well-profen design patterns and best practises.
Features
URL-Action Mapping
According to Backbone's Router component, the hash fragments (#page) or a portion of the static url can be used for routing. It's the same for BackboneMVC's routing strategy. In this documentation, we will assume you use the hash fragment method.
Like CakePHP, the hash fragments are divided by slashes into three parts: controller name, method and optional parameters.
URL_TO_ROUTE := CONTROLLER_NAME '/' METHOD_NAME (ADDITIONAL_PARAMETERS) ADDITIONAL_PARAMETERS := '/' LITERAL_VALUE (ADDITIONAL_PARAMETERS ) CONTROLLER_NAME := LITERAL_VALUE METHOD_NAME : LITERAL_VALUE LITERAL_VALUE: [^/]+Examples:
- controller1/method1
- my_controller/my-method/happy/birthday
The optional parameters can be 0 or many, which are also separated by slash(/). No matter how many parameters are in the url, they will all be tossed over to the method in the order of appearance.
BackboneMVC.Controller.extend({ name: 'my_controller', /* the only mandatory field */ 'my-method': function(how, when){ var phrase = how + ' ' + (when || 'unknown'); this._output(phrase); }, _output: function(string){ $('#area1').append($('<div>' + string + '</div>')); } })Try it in action:
Trigger my-method() with 2 parameters:
#my_controller/my-method/happy/birthdayTrigger my-method() with 1 parameter:
#my_controller/my-method/happyPrivate methods can't be triggered. This helps maintain your encapsulation.
Trigger a private method (this will fail): my_controller/_output/really?
Asynchronous Calling
The navigate() method of Backbone's Router component can trigger url routing programmatically. BackboneMVC further enhances this method, and the method now returns a JQuery Deferred object. You can use it to make sure the action method has finished. This is useful when making asynchronous calls.
See the following example:
var AsynchronousController = BackboneMVC.Controller.extend({ name: 'asynchronous', /* the only mandatory field */ 'method': function(){ var deferred = new $.Deferred(); var colors = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple']; var index = 0; var instance = this; (function op(){ if(index
See it in action:
Trigger the procedure1() method:
Click here to trigger procedure1()Event Hooks
Like CakePHP, before and after invoking an action method, beforeFilter and afterRender event handlers can also be set.
For example:
BackboneMVC.Controller.extend({ name: 'event_hooks', /* the only mandatory field */ beforeFilter: function(){ this._report('beforeFilter invoked'); }, method1: function(){ this._report('method1 invoked'); }, method2: function(){ var index = 0; var instance = this; var deferred = new $.Deferred(); (function op(){ if(index ++ < 5){ instance._report(index); setTimeout(op, 345); }else{ deferred.resolve(); } })(); return deferred; }, afterRender: function(){ this._report('afterRender invoked'); }, _report: function(text){ $('#area3').append('<div>' + text + '</div>'); } });Trigger mehtod1() #event_hooks/method1
If your action method return a Deferred object, then the afterRender event hook will be deferred in execution
Trigger method2() #event_hooks/method2
Rules for the call chain of beforeFilter, afterRender and the action method
- All of the beforeFilter, afterRender and the action method can opt to return a Deferred object. If either of them does, then its successor will wait on the predecessor's Deferred object. Unless the Deferred object is resolved, all the subsequent methods on the chain won't be executed.
- If a predecessor return a non-deferred value, and the value can be evaluated to true, the successor will run as it does normally, or otherwise the call chain will be interrupted.
- If a predecessor doesn't return a value, the successor will run as it does normally.
- If at any point, the call chain is interrupted, either by a rejected object or a false value, and the navigate() method is used to issue the call, the returned Deferred object from navigate() will be rejected.
See the following example:
var EventHooks1= BackboneMVC.Controller.extend({ name: 'event_hooks1', /* the only mandatory field */ beforeFilter: function(){ this._report('beforeFilter invoked'); //always successful (can also be achieved if return nothing) return true; }, method1: function(){ this._report('method1 invoked'); var deferred = new $.Deferred(); var colors = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple']; var index = 0; var instance = this; (function op(){ if(index < colors.length){ instance._changeColor(colors[index++]); setTimeout(op, 234); }else{ deferred.resolve(); } })(); return deferred; }, method2: function(){ this._changeColor('MidnightBlue'); this._report('method2 invoked'); this._report('afterRender won\'t be executed'); return false; // a false value, the subsequent method has no chance to be executed }, method3: function(){ this._changeColor('Indigo'); this._report('method3 invoked'); this._report('afterRender will be executed'); // return nothing has the same effect as returning true }, afterRender: function(){ // reject, so the call chain will eventually fail even though the // main action method is executed this._report('afterRender invoked'); return (new $.Deferred()).reject(); }, _report: function(text){ $('#area4').append('<div>' + text + '</div>'); }, _changeColor: function(color){ $('#area4').css('backgroundColor', color); } }); window['procedure2'] = function(){ var r = router.navigate('event_hooks1/method2', {trigger:true, replace: false}); r.done(function(){ (new EventHooks1())._report('procedure2 successful'); }).fail(function(){ (new EventHooks1())._report('procedure2 failed'); }); }; window['procedure3'] = function(){ var r = router.navigate('event_hooks1/method3', {trigger:true, replace: false}); r.done(function(){ (new EventHooks1())._report('procedure3 successful'); }).fail(function(){ (new EventHooks1())._report('procedure3 failed'); }); };Trigger method1() #event_hooks1/method1
Trigger procedure2() procedure2()
Trigger procedure3() procedure3()
When used with Deferred objects, the event hooks can be useful if you want to prepare templates for your actions to render views, or select the corresponding navigation entry for all the actions in the controller after their execution.
Session Checking
Similar to beforeFilter and afterRender methods, you can also define a checkSession method. Then all action methods with a prefix of 'user_' in their names, will be invoked only after the checkSession is called.
If checkSession returns false or a Deferred object, which will later be rejected, the action methods will not be invoked.
See example:
BackboneMVC.Controller.extend({ name: 'session_enabled', /* the only mandatory field */ beforeFilter: function(){ this._report('beforeFilter invoked'); return true; }, checkSession: function(){ this._report('checkSession invoked'); var deferred = new $.Deferred(); var instance = this; setTimeout(function(){ instance._report('session is valid!'); deferred.resolve(); }, 2000); return deferred; }, user_method1: function(){ this._report('method1 invoked'); }, user_method2: function(){ this._report('secure method2 invoked'); this._changeColor('green'); }, method2: function(){ this._report('normal method2 invoked'); this._changeColor('DimGray'); }, _report: function(text){ $('#area5').append('<div>' + text + '</div>'); }, _changeColor: function(color){ $('#area5').css('backgroundColor', color); } });Trigger user_method1() #session_enabled/user_method1
You can even omit the 'user_' prefix:
Trigger method1() #session_enabled/method1
However if the method without the 'user_' prefix is already defined, then the shortcut will not overwrite the existing one, see example:
Trigger user_method2() #session_enabled/user_method2
Trigger method2() #session_enabled/method2
As you can see, the checkSession is invoked after beforeFilter, but before the action method.
Controllers Are Singletons
Controllers are all singletons. So no matter when and where you instantiate a controller, the instance always keeps the same state.
var Singleton = BackboneMVC.Controller.extend({ name: 'singleton', /* the only mandatory field */ value: 0, method: function(){ this._report(this.value++); }, _report: function(text){ $('#area6').append('' + text + '
'); } }); window['procedure4'] = function(){ (new Singleton()).method(); };Trigger method() #singleton/method
Call procedure4() procedure4()
Controller Inheritance
Controller.extend() can be used to do class inheritance. So your controllers are able to share functionality from their parent controller.
See example:
var Parent = BackboneMVC.Controller.extend({ name: 'parent', /* the only mandatory field, even though the parent is not planned to be used, it will still need to be assigned a name. */ method: function(){ this._report('Parent method invoked'); this._changeColor('#141F2E'); }, _report: function(text){ $('#area7').append('<div>' + text + '</div>'); }, _changeColor: function(color){ $('#area7').css('backgroundColor', color); } }); var Child1 = Parent.extend({ name: 'child1', /* the only mandatory field */ method: function(){ this._report('Child1 method invoked'); this._changeColor('green'); } }); var Child2 = Parent.extend({ name: 'child2' /* the only mandatory field */ //this controller doesn't implement anything, so its parent's methods will be passed over. });Trigger Child1::method() #child1/method
Trigger Child2::method() #child2/method
Custom Routing Rules
When BackboneMVC's automatic routing doesn't suffice for your requirements, customized routing rules can be used to comply with backbone.js' convention. This can be achieved by further extending the BackboneMVC.Router class.
var MyExtendedRouter = BackboneMVC.Router.extend({ routes:{ '' : 'index', 'root/action/hardcoded_value': 'special_handling' }, index: function(){ // do customized logic, for example, launch a default controller's action router.navigate("root/index", {trigger:true, replace: true}); }, special_handling: function(){ (new RootController())._report( "I just thought of something more important to do."; ); (new RootController())._changeColor("purple"); } }); var RootController = BackboneMVC.Controller.extend({ name: "root", /* the only mandatory field */ index: function(){ this._report("'index' method invoked"); this._changeColor("blue"); }, action: function(param){ this._report("'action' method triggered with " + param); this._changeColor("red"); }, _report: function(text){ $("#area8").append("<div>' + text + '</div>"); }, _changeColor: function(color){ $("#area8").css("backgroundColor", color); } });Trigger routing for empty hash #
Some hash patterns that do not follow BackboneMVC's automatic routing can be dealt with this way, especially the empty hash which usually points to a default index page. The default controller's default action can be redirected to, as the example above shows, or any other code logic can be used.
If you define custom rules, they will always have higher priority than BackboneMVC's automatic routing. See examples:
Trigger special_handling() #root/action/hardcoded_value
Trigger RootController::method() #root/action/offer
A few weeks ago, I posted a three-part Backbone.js tutorial (part 1, part 2, part 3). Since then, I spent more time building a real-life application with Backbone. I ran into a number of interesting problems, spent time thinking about solutions, and decided to write them down in this post. These are not definitive answers, just my current thoughts… and as always, I value your input.
Based on this new experience, I revisited the Wine Cellar application used in my tutorial. You can find the improved version here (PHP back-end) or here (Java back-end).
Externalizing Templates
In the initial implementation of my wine cellar app, I “inlined” my templates in a succession of <script> tags like the one below inside index.html.
<script type="text/template" id="wine-list-item"> <li><a href='#wines/<%= id %>'><%= name %></a></li> </script>This approach works fine for a simple application with a limited number of templates. In a larger project, it makes templates hard to maintain. It also makes it difficult for multiple developers to work on different templates at the same time. Removing the script body and adding a src attribute to point to an external file didn’t seem to work to load HTML templates.
In this StackOverflow thread, Brian Genisio suggests loading external templates as string variables in a .js file. Using this approach, a template definition looks like this:app.templates.view = " \ <h3>something code</h3> \ ";That’s certainly a valid approach, but depending on your circumstances, it may still not be the ideal solution: long string concatenation is painful (especially for larger templates). In addition, if you define HTML in string variables, you will probably not be able to benefit from the HTML capabilities of your editor.
Other people load external templates using Require.js and the text plugin, which is definitely an approach worth considering for larger projects. In my case, I didn’t feel that the size of my project mandated it.
In the end, I decided to write my own simple template loader. It is implemented as follows:
tpl = { // Hash of preloaded templates for the app templates: {}, // Recursively pre-load all the templates for the app. // This implementation should be changed in a production environment: // All the template files should be concatenated in a single file. loadTemplates: function(names, callback) { var that = this; var loadTemplate = function(index) { var name = names[index]; console.log('Loading template: ' + name); $.get('tpl/' + name + '.html', function(data) { that.templates[name] = data; index++; if (index < names.length) { loadTemplate(index); } else { callback(); } }); } loadTemplate(0); }, // Get template by name from hash of preloaded templates get: function(name) { return this.templates[name]; } };I use it to preload all my templates before bootstrapping Backbone:
tpl.loadTemplates(['header', 'wine-details', 'wine-list-item'], function() { app = new AppRouter(); Backbone.history.start(); });Loading templates separately is great during development: you can modify and test individual templates without requiring a “build” or the concatenation of multiple template files. In production however, loading dozens of templates as separate HTTP requests can be a serious bottleneck. So when you are ready to move to production, it’s a good idea to somehow combine all your templates in a single file so that you can load them all with a single HTTP request.
Decouple Views from other DOM elements
In the initial version of my application, I didn’t pay much attention to the way I chose the “el” or “tagName” of my Views. I often assigned the View’s “el” to an existing element of the document.
“Before” code:
window.WineView = Backbone.View.extend({ el: $('#content'), render: function(eventName) { $(this.el).html(this.template(this.model.toJSON())); return this; } });… and here is the Router’s code that instantiated and displayed a WineView:
new WineView().render();After more careful consideration, I realized that this approach wasn’t very good. The View is not fully encapsulated: it “reaches out” to an attribute (element) of the containing document (‘#mainArea’), creating an undesirable dependency. A better approach is to make sure a View doesn’t know about its hosting document. It should not know about (or assume the presence) of other elements in the document. The knowledge of the View should be limited to the elements in its own template. That will make the view reusable in many different contexts. The corollary of this rule is that a View shouldn’t attach itself to a DOM element in its render() method. The render() method should be limited to populating its own “detached” el attribute. The code that invokes the View’s render() method can then decide what to do with the View’s HTML fragment: attach it, append it, etc. The View is therefore more reusable and more versatile.
“After” code:
window.WineView = Backbone.View.extend({ tagName: "div", // Not required since 'div' is the default if no el or tagName specified render: function(eventName) { $(this.el).html(this.template(this.model.toJSON())); return this; } });… and here is the Router’s code that instantiates and displays a WineView defined as above:
$('#content').html( new WineView().render().el );
Cleaning Up: Avoiding Memory Leaks and Ghost Views
Before getting rid of a view, you need to clean up, which mainly consists of removing the bindings that could prevent the View from being properly destroyed. A View that is not properly destroyed becomes a “Ghost View”: you don’t see it, you think it’s gone, but it still manifests itself in some unpleasant ways: its events are still firing and it still uses memory (memory leaks). Ghost Views often first manifest themselves in the form of events firing multiple times.
In my initial implementation, I provided views with a close() method that took care of cleaning up. There was nothing wrong with that approach except that you had to remember to call the close() method!
Derick Bailey has a great post on this topic and provides a more generalized approach: It consists of adding a generic close() method to the Backbone View prototype. I borrowed Derick’s approach and implemented my generic close() method as follows:
Backbone.View.prototype.close = function () { if (this.beforeClose) { this.beforeClose(); } this.remove(); this.unbind(); };The call to beforeClose() (if it exists in the view) is a hook to add some view-specific cleanup code if required.
In your Router, you then create new Views by invoking a custom showView() method that invokes close() on the currentView before replacing it with the new View.
showView: function(selector, view) { if (this.currentView) this.currentView.close(); $(selector).html(view.render().el); this.currentView = view; return view; }Note that this is only part of the solution. For example, if you have composite views (views containing other views), you still have to make sure you close the “child” views when the parent view is closed. A good approach here is for the parent view to keep track of its child views so that it can call their respective close() methods when its own close() method is invoked. The beforeClose() method (explained above) of the parent View is a good place to close the child Views.
Route pre-processing (aka Filters)
Backbone’s Router is great at routing requests to the important/stateful/bookmarkable locations in your application.
When defining the routes in my application, I sometimes find myself in a situation where a pre-condition has to be met before a route can be taken.
For example:
- Some routes may require the user to be authenticated.
- Some routes may require some data to be loaded.
It’s easy to verify the pre-condition in the method that implements a specific route. That’s what I did in the initial version of my application. But in a larger application, this approach will quickly start cluttering your route methods.
My instinct was to look for a “Filter” feature in the Backbone Router API, but it doesn’t currently exist out-of-the-box, and in this thread, Jeremy makes the case that the same result can be achieved using underscore’s _.wrap() method.
_.wrap() didn’t work for me because my pre-condition involved asynchronous processing and I can only pass control back to the routing method after the successful completion of my async code.
In the end, I implemented a simple/low-tech before() method that is somewhat similar to wrap() but supports async chaining.
var AppRouter = Backbone.Router.extend({ routes: { "wines/:id" : "wineDetails" }, wineDetails: function(id) { this.before(function() { var wine = app.wineList.get(id); app.showView( '#content', new WineView({model: wine}) ); }); }, showView: function(selector, view) { if (this.currentView) this.currentView.close(); $(selector).html(view.render().el); this.currentView = view; return view; }, before: function(callback) { if (this.wineList) { if (callback) callback(); } else { this.wineList = new WineCollection(); this.wineList.fetch({success: function() { $('#sidebar').html( new WineListView({model: app.wineList}).render().el ); if (callback) callback(); }}); } } });This solution is not as “decoupled” as a traditional filter implementation would be, but it allowed me to unclutter my code in a way that was suitable for my requirements. Jake Dempsey
has built a more traditional filter implementation here that you may want to take a look at.Download
The source code for this application is hosted on GitHub. The application uses RESTful services to access the data.
For a version of this application using a PHP back-end, use this project (use the ‘final’ folder for the version of the application discussed in this article.
For a version of this application using a Java back-end (using JAX-RS), use this project.
UPDATE: I posted a “Postface” to this series with some lessons learned and an improved version of the app. Make sure you read it here.
In Part 1 of this tutorial, we set up the basic infrastructure for the Wine Cellar application. In Part 2, we added the ability to create, update, and delete (CRUD) wines.
There are a few remaining issues in the application. They are all related to “deep linking”. The application needs to continuously keep its URL in sync with its current state. This allows you to grab the URL from the address bar at any point in time during the course of the application, and re-use or share it to go back to the exact same state.
In this last installment of the tutorial, we add support for deep linking in the Wine Cellar application.
Problem 1: Linking to a specific wine
The problem: Select a specific wine from the list. The URL looks like this: http://www.coenraets.org/backbone-cellar/part2/#wines/[id]. Now do one of these two things:
- Grab that URL from the address bar and try to access it from another browser window or tab
- Simply click your browser’s Refresh button
You get an empty screen with no data. If you look at a debug console (for example, in Chrome’s Developer Tools), you’ll get this message:
Let’s take a look at what’s going on here:
var AppRouter = Backbone.Router.extend({ routes:{ "":"list", "wines/:id":"wineDetails" }, initialize:function () { $('#header').html(new HeaderView().render().el); }, list:function () { this.wineList = new WineCollection(); this.wineListView = new WineListView({model:this.wineList}); this.wineList.fetch(); $('#sidebar').html(this.wineListView.render().el); }, wineDetails:function (id) { this.wine = this.wineList.get(id); if (app.wineView) app.wineView.close(); this.wineView = new WineView({model:this.wine}); $('#content').html(this.wineView.render().el); } });The problem is on line 20: we are assuming that a wine collection (this.wineList) already exists, and are trying to “get” a specific item from that list. That works well when we start the application with the default (“”) route. But this.wineList won’t exist if we start the application with the “wines/:id” route. There are different ways to address the issue:
We could modify the wineDetails function to fetch the requested item directly:
wineDetails: function(id) { this.wine = new Wine({id: id}); this.wineView = new WineView({model: this.wine}); this.wine.fetch(); }That takes care of loading the wine details in the form, but the wine list remains empty when we start the application with “wines/:id” route. We could add the following line of code to load the list if it doesn’t exist:
if (!this.wineList) this.list();But now, the wine model that is part of the collection and the wine model fetched separately are two different objects, which means that data binding and View synchronization will not work as expected.
Another approach is to check if the collection exists in the wineDetails function. If it does, we simply “get” the requested item and render it as we did before. If it doesn’t, we store the requested id in a variable, and then invoke the existing list() function to populate the list. We then modify the list function: When we get the list from the server (on success), we check if there was a requested id. If there was, we invoke the wineDetails function to render the corresponding item.
var AppRouter = Backbone.Router.extend({ routes:{ "":"list", "wines/new":"newWine", "wines/:id":"wineDetails" }, initialize:function () { $('#header').html(new HeaderView().render().el); }, list:function () { this.wineList = new WineCollection(); var self = this; this.wineList.fetch({ success:function () { self.wineListView = new WineListView({model:self.wineList}); $('#sidebar').html(self.wineListView.render().el); if (self.requestedId) self.wineDetails(self.requestedId); } }); }, wineDetails:function (id) { if (this.wineList) { this.wine = this.wineList.get(id); if (this.wineView) this.wineView.close(); this.wineView = new WineView({model:this.wine}); $('#content').html(this.wineView.render().el); } else { this.requestedId = id; this.list(); } }, newWine:function () { if (app.wineView) app.wineView.close(); app.wineView = new WineView({model:new Wine()}); $('#content').html(app.wineView.render().el); } });
Problem 2: Updating the URL after a wine is created
The problem: Add a new Wine, and click Save. The id that has been assigned to the newly created wine appears in the form field. However the URL is still:
http://localhost/backbone-cellar/part2/ when it should really be: http://localhost/backbone-cellar/part2/#wines/[id].You can easily fix that issue by using the router’s navigate function to change the URL. The second argument (false), indicates that we actually don’t want to “execute” that route: we just want to change the URL.
if (this.model.isNew()) { var self = this; app.wineList.create(this.model, { success: function() { app.navigate('wines/'+self.model.id, false); } }); } else { this.model.save(); }
Problem 3: Updating the URL while creating a new wine
The problem: Select a wine in the list. The URL looks like this: http://localhost/backbone-cellar/part2/#wines/[id]
Now, click the “Add Wine” button. You get an empty form to enter a new wine, but notice that the URL is still unchanged (http://localhost/backbone-cellar/part2/#wines/[id]). In other words, it doesn’t reflect the current state of the application.
In this case, we are going to add a new “route”:
routes: { "" : "list", "wines/new" : "newWine", "wines/:id" : "wineDetails" },The router’s newWine method is implemented as follows:
newWine: function() { console.log('MyRouter newWine'); if (app.wineView) app.wineView.close(); app.wineView = new WineView({model: new Wine()}); app.wineView.render(); }And we can change the HeaderView newWine() method as follows:
newWine: function(event) { app.navigate("wines/new", true); return false; }
Putting it all together
You can run the application (Part 3) here. The create/update/delete features are disabled in this online version. Use the link at the bottom of this post to download a fully enabled version.
Here is the final version of the code:
// Models window.Wine = Backbone.Model.extend({ urlRoot:"../api/wines", defaults:{ "id":null, "name":"", "grapes":"", "country":"USA", "region":"California", "year":"", "description":"", "picture":"" } }); window.WineCollection = Backbone.Collection.extend({ model:Wine, url:"../api/wines" }); // Views window.WineListView = Backbone.View.extend({ tagName:'ul', initialize:function () { this.model.bind("reset", this.render, this); var self = this; this.model.bind("add", function (wine) { $(self.el).append(new WineListItemView({model:wine}).render().el); }); }, render:function (eventName) { _.each(this.model.models, function (wine) { $(this.el).append(new WineListItemView({model:wine}).render().el); }, this); return this; } }); window.WineListItemView = Backbone.View.extend({ tagName:"li", template:_.template($('#tpl-wine-list-item').html()), initialize:function () { this.model.bind("change", this.render, this); this.model.bind("destroy", this.close, this); }, render:function (eventName) { $(this.el).html(this.template(this.model.toJSON())); return this; }, close:function () { $(this.el).unbind(); $(this.el).remove(); } }); window.WineView = Backbone.View.extend({ template:_.template($('#tpl-wine-details').html()), initialize:function () { this.model.bind("change", this.render, this); }, render:function (eventName) { $(this.el).html(this.template(this.model.toJSON())); return this; }, events:{ "change input":"change", "click .save":"saveWine", "click .delete":"deleteWine" }, change:function (event) { var target = event.target; console.log('changing ' + target.id + ' from: ' + target.defaultValue + ' to: ' + target.value); // You could change your model on the spot, like this: // var change = {}; // change[target.name] = target.value; // this.model.set(change); }, saveWine:function () { this.model.set({ name:$('#name').val(), grapes:$('#grapes').val(), country:$('#country').val(), region:$('#region').val(), year:$('#year').val(), description:$('#description').val() }); if (this.model.isNew()) { var self = this; app.wineList.create(this.model, { success:function () { app.navigate('wines/' + self.model.id, false); } }); } else { this.model.save(); } return false; }, deleteWine:function () { this.model.destroy({ success:function () { alert('Wine deleted successfully'); window.history.back(); } }); return false; }, close:function () { $(this.el).unbind(); $(this.el).empty(); } }); window.HeaderView = Backbone.View.extend({ template:_.template($('#tpl-header').html()), initialize:function () { this.render(); }, render:function (eventName) { $(this.el).html(this.template()); return this; }, events:{ "click .new":"newWine" }, newWine:function (event) { app.navigate("wines/new", true); return false; } }); // Router var AppRouter = Backbone.Router.extend({ routes:{ "":"list", "wines/new":"newWine", "wines/:id":"wineDetails" }, initialize:function () { $('#header').html(new HeaderView().render().el); }, list:function () { this.wineList = new WineCollection(); var self = this; this.wineList.fetch({ success:function () { self.wineListView = new WineListView({model:self.wineList}); $('#sidebar').html(self.wineListView.render().el); if (self.requestedId) self.wineDetails(self.requestedId); } }); }, wineDetails:function (id) { if (this.wineList) { this.wine = this.wineList.get(id); if (this.wineView) this.wineView.close(); this.wineView = new WineView({model:this.wine}); $('#content').html(this.wineView.render().el); } else { this.requestedId = id; this.list(); } }, newWine:function () { if (app.wineView) app.wineView.close(); app.wineView = new WineView({model:new Wine()}); $('#content').html(app.wineView.render().el); } }); var app = new AppRouter(); Backbone.history.start();
Download
The source code for this application is hosted on GitHub here (see part3). And here is a quick link to the download.
You will need the RESTful services to run this application. A PHP version (using the Slim framework) is available as part of the download.
UPDATE (1/11/2012): A version of this application with a Java back-end (using JAX-RS and Jersey) is also available on GitHub here. You can find more information on the Java version of this application here.
In Part 1 of this tutorial, we set up the basic infrastructure for the Wine Cellar application. The application so far is read-only: it allows you to retrieve a list of wines, and display the details of the wine you select.
In this second installment, we will add the ability to create, update, and delete (CRUD) wines.
RESTful Services
As mentioned in Part 1, Backbone.js provides a natural and elegant integration with RESTful services. If your back-end data is exposed through a pure RESTful API, retrieving (GET), creating (POST), updating (PUT), and deleting (DELETE) models is incredibly easy using the Backbone.js simple Model API.
This tutorial uses pure RESTful services. The services are implemented as follows:
HTTP Method URL Action GET /api/wines Retrieve all wines GET /api/wines/10 Retrieve wine with id == 10 POST /api/wines Add a new wine PUT /api/wines/10 Update wine with id == 10 DELETE /api/wines/10 Delete wine with id == 10
A PHP version of these services (using the Slim framework) is available as part of the download. A similar Java version of the API (using JAX-RS) is available as part of this post.Using Backbone.js with non-RESTful Services
If your persistence layer is not available through RESTful services, you can override Backbone.sync. From the documentation:
“Backbone.sync is the function that Backbone calls every time it attempts to read or save a model to the server. By default, it uses (jQuery/Zepto).ajax to make a RESTful JSON request. You can override it in order to use a different persistence strategy, such as WebSockets, XML transport, or Local Storage.”
Using non-RESTful services is not discussed in this tutorial. See the documentation for more information.
Part 2: Adding Create, Update, Delete
You can run the application (Part 2) here. The create/update/delete features are disabled in this online version. Use the link at the bottom of this post to download a fully enabled version.
Here is the code for the improved version of the applications. Key changes are discussed below.
// Models window.Wine = Backbone.Model.extend({ urlRoot:"../api/wines", defaults:{ "id":null, "name":"", "grapes":"", "country":"USA", "region":"California", "year":"", "description":"", "picture":"" } }); window.WineCollection = Backbone.Collection.extend({ model:Wine, url:"../api/wines" }); // Views window.WineListView = Backbone.View.extend({ tagName:'ul', initialize:function () { this.model.bind("reset", this.render, this); var self = this; this.model.bind("add", function (wine) { $(self.el).append(new WineListItemView({model:wine}).render().el); }); }, render:function (eventName) { _.each(this.model.models, function (wine) { $(this.el).append(new WineListItemView({model:wine}).render().el); }, this); return this; } }); window.WineListItemView = Backbone.View.extend({ tagName:"li", template:_.template($('#tpl-wine-list-item').html()), initialize:function () { this.model.bind("change", this.render, this); this.model.bind("destroy", this.close, this); }, render:function (eventName) { $(this.el).html(this.template(this.model.toJSON())); return this; }, close:function () { $(this.el).unbind(); $(this.el).remove(); } }); window.WineView = Backbone.View.extend({ template:_.template($('#tpl-wine-details').html()), initialize:function () { this.model.bind("change", this.render, this); }, render:function (eventName) { $(this.el).html(this.template(this.model.toJSON())); return this; }, events:{ "change input":"change", "click .save":"saveWine", "click .delete":"deleteWine" }, change:function (event) { var target = event.target; console.log('changing ' + target.id + ' from: ' + target.defaultValue + ' to: ' + target.value); // You could change your model on the spot, like this: // var change = {}; // change[target.name] = target.value; // this.model.set(change); }, saveWine:function () { this.model.set({ name:$('#name').val(), grapes:$('#grapes').val(), country:$('#country').val(), region:$('#region').val(), year:$('#year').val(), description:$('#description').val() }); if (this.model.isNew()) { app.wineList.create(this.model); } else { this.model.save(); } return false; }, deleteWine:function () { this.model.destroy({ success:function () { alert('Wine deleted successfully'); window.history.back(); } }); return false; }, close:function () { $(this.el).unbind(); $(this.el).empty(); } }); window.HeaderView = Backbone.View.extend({ template:_.template($('#tpl-header').html()), initialize:function () { this.render(); }, render:function (eventName) { $(this.el).html(this.template()); return this; }, events:{ "click .new":"newWine" }, newWine:function (event) { if (app.wineView) app.wineView.close(); app.wineView = new WineView({model:new Wine()}); $('#content').html(app.wineView.render().el); return false; } }); // Router var AppRouter = Backbone.Router.extend({ routes:{ "":"list", "wines/:id":"wineDetails" }, initialize:function () { $('#header').html(new HeaderView().render().el); }, list:function () { this.wineList = new WineCollection(); this.wineListView = new WineListView({model:this.wineList}); this.wineList.fetch(); $('#sidebar').html(this.wineListView.render().el); }, wineDetails:function (id) { this.wine = this.wineList.get(id); if (app.wineView) app.wineView.close(); this.wineView = new WineView({model:this.wine}); $('#content').html(this.wineView.render().el); } }); var app = new AppRouter(); Backbone.history.start();Wine
Two attributes were added to the Wine Model:
- urlRoot: RESTful service endpoint to retrieve or persist Model data. Note that this attribute is only needed when retrieving/persisting Models that are not part of a Collection. If the Model is part of a Collection, the url attribute defined in the Collection is enough for Backbone.js to know how to retrieve, update, or delete data using your RESTful API.
- defaults: Default values used when a new instance of the model is created. This attribute is optional. However, it was required in this application for the wine-details template to render an ‘empty’ wine model object (which happens when adding a new wine).
WineListView
When a new wine is added, you want it to automatically appear in the list. To make that happen, you bind the View to the add event of the WineListView model (which is the collection of wines). When that event is fired, a new instance of WineListItemView is created and added to the list.
WineListItemView
When a wine is changed, you want the corresponding WineListItemView to re-render automatically to reflect the change. To make that happen, you bind the View to the change event of its model, and execute the render function when the event is fired.
Similarly, when a wine is deleted, you want the list item to be removed automatically. To make that happen, you bind the view to the destroy event of its model and execute our custom close function when the event is fired. To avoid memory leaks and events firing multiple times, it is important to unbind the event listeners before removing the list item from the DOM.
Note that in either case we don’t have the overhead of re-rendering the entire list: we only re-render or remove the list item affected by the change.
WineView
In the spirit of encapsulation, the event handlers for the Save and Delete buttons are defined inside WineView, as opposed to defining them as free-hanging code blocks outside the “class” definitions. You use the Backbone.js Events syntax which uses jQuery delegate mechanism behind the scenes.
There are always different approaches to update the model based on user input in a form:
- “Real time” approach: you use the change handler to update the model as changes are made in the form. This is in essence bi-directional data binding: the model and the UI controls are always in sync. Using this approach, you can then choose between sending changes to the server in real time (implicit save), or wait until the user clicks a Save button (explicit save). The first option can be chatty and unpractical when there are cross-field validation rules. The second option may require you to undo model changes if the user navigates to another item without clicking Save.
- “Delayed” approach: You wait until the user clicks Save to update the model based on the new values in UI controls, and then send the changes to the server.
This discussion is not specific to Backbone.js and is therefore beyond the scope of this post. For simplicity, I used the delayed approach here. However I still wired the change event, and use it to log changes to the console. I found this very useful when debugging the application, and particularly to make sure I had cleaned up my bindings (see close function): I you see the change event firing multiple times, you probably didn’t clean up as appropriate.
HeaderView
Backbone.js Views are typically used to render domain models (as done in WineListView, WineListItemView, and Wine View). But they can also be used to create composite UI components. For example, in this application, we define a Header View (a toolbar) that could be made of different components and that encapsulates its own logic.
Download
The source code for this application is hosted on GitHub here (see part2). And here is a quick link to the download.
You will need the RESTful services to run this application. A PHP version (using the Slim framework) is available as part of the download.
UPDATE (1/11/2012): A version of this application with a Java back-end (using JAX-RS and Jersey) is also available on GitHub here. You can find more information on the Java version of this application here.
What’s Next?
The application so far doesn’t support deep-linking. For example, select a wine in the list, grab the URL in the address bar and paste it in another browser window: it doesn’t work. In Part 3, we will add complete support for deep linking.
One of the challenges when building nontrivial Web applications is that JavaScript’s non-directive nature can initially lead to a lack of structure in your code, or in other words, a lack of… backbone. JavaScript is often written as a litany of free-hanging and unrelated blocks of code, and it doesn’t take long before it becomes hard to make sense of the logic and organization of your own code.
Backbone.js is a lightweight framework that addresses this issue by adding structure to JavaScript-heavy Web applications.
Self-contained building blocksBackbone.js provides several classes (Model, Collection, View, Router) that you can extend to define the building blocks of your application. To build an app with Backbone.js, you first create the Models, Collections, and Views of your application. You then bring these components to life by defining a “Router” that provides the entry points of your application through a set of (deep-linkable) URLs.
With Backbone.js, your code is organized in self-contained entities (Models, Collections, Views): No more free-hanging and unrelated blocks of code.
Data Binding
With Backbone.js, you bind Views to Models so that when a Model’s data changes, all the Views bound to that Model automatically re-render. No more complex UI synchronization code.
Elegant REST Integration
Backbone.js also provides a natural / magical / elegant integration with RESTful services. If your back-end data is exposed through a pure RESTful API, retrieving (GET), creating (POST), updating (PUT), and deleting (DELETE) models is incredibly easy using the Backbone.js simple Model API.
Sample Application
In this three-part tutorial, you’ll create a Wine Cellar application. You can browse through a list of wines, as well as add, update, and delete wines.
- In Part 1 (this post), you define the basic infrastructure. You create a “read-only” version of the application: you’ll be able to retrieve a list of wine and get the details of each wine.
- In Part 2, you add the code to add, update and delete wines. You leverage Backbone’s powerful REST integration.
- In Part 3, you add complete support for history management and deep linking.
NOTE: I also blogged a non-Backbone version of the application here (Java back-end) and here (PHP back-end), which you can look at for comparison.Part 1: The Read-Only Wine Cellar Application
You can run the application (Part 1) here.
Here is the code:
// Models window.Wine = Backbone.Model.extend(); window.WineCollection = Backbone.Collection.extend({ model:Wine, url:"../api/wines" }); // Views window.WineListView = Backbone.View.extend({ tagName:'ul', initialize:function () { this.model.bind("reset", this.render, this); }, render:function (eventName) { _.each(this.model.models, function (wine) { $(this.el).append(new WineListItemView({model:wine}).render().el); }, this); return this; } }); window.WineListItemView = Backbone.View.extend({ tagName:"li", template:_.template($('#tpl-wine-list-item').html()), render:function (eventName) { $(this.el).html(this.template(this.model.toJSON())); return this; } }); window.WineView = Backbone.View.extend({ template:_.template($('#tpl-wine-details').html()), render:function (eventName) { $(this.el).html(this.template(this.model.toJSON())); return this; } }); // Router var AppRouter = Backbone.Router.extend({ routes:{ "":"list", "wines/:id":"wineDetails" }, list:function () { this.wineList = new WineCollection(); this.wineListView = new WineListView({model:this.wineList}); this.wineList.fetch(); $('#sidebar').html(this.wineListView.render().el); }, wineDetails:function (id) { this.wine = this.wineList.get(id); this.wineView = new WineView({model:this.wine}); $('#content').html(this.wineView.render().el); } }); var app = new AppRouter(); Backbone.history.start();Code Highlights:
- WineModel (line 2): Notice that we don’t need to explicitly define the attributes (name, country, year, etc). You could add validation, default values, etc. More on that in Part 2.
- WineCollection (lines 4 to 7): “model” indicates the nature of the collection. “url” provides the endpoint for the RESTFul API. This is all that’s needed to retrieve, create, update, and delete wines with Backbone’s simple Model API.
- WineListView (lines 10 to 25): The render() function iterates through the collection, instantiates a WineListItemView for each wine in the collection, and adds it to the wineList.
- WineListItemView (lines 27 to 38): The render() function merges the model data into the “wine-list-item” template (defined in index.html). By defining a separate View for list items, you will make it easy to update (re-render) a specific list item when the backing model changes without re-rendering the entire list. More on that in Part 2.
- WineView (lines 40 to 49): The view responsible for displaying the wine details in the Wine form. The render() function merges the model data (a specific wine) into the “wine-details” template retrieved from index.html.
- AppRouter (lines 52 to 71): Provides the entry points for the application through a set of (deep-linkable) URLs. Two routes are defined: The default route (“”) displays the list of wine. The “wines/:id” route displays the details of a specific wine in the wine form. Note that in Part 1, this route is not deep-linkable. You have to start the application with the default route and then select a specific wine. In Part 3, you will make sure you can deep-link to a specific wine.
Download
The source code for this application is hosted on GitHub here. And here is a quick link to the download.
You will need the RESTful services to run this application. A PHP version (using the Slim framework) is available as part of the download.
UPDATE (1/11/2012): A version of this application with a Java back-end (using JAX-RS and Jersey) is also available on GitHub here. You can find more information on the Java version of this application here.
Part 2 is available here.
In Part 1: Build Environment, I explained how to set up a simple Node server to host your Backbone.js app and test suite. Something that confused people was the way I used relative paths, which meant the tests could fail if you didn’t visit /test/ (/test won’t work). There was a reason for this: I developed the original version to run on Dropbox, so I wanted to use relative paths. It’s probably safer to use absolute paths, so I should have made this clearer.
In this part you’ll learn the following:
- How Backbone.sync works
- How to load Backbone.js and Underscore.js with RequireJS
- How to get started with Google’s APIs
The Backbone.sync Method
Network access in Backbone.js is nicely abstracted through a single method which has the following signature:
Backbone.sync = function(method, model, options) { };The method argument contains a string that can be one of the following values:
- create
- update
- delete
- read
Internally, Backbone.js maps these method names to HTTP verbs:
var methodMap = { 'create': 'POST', 'update': 'PUT', 'delete': 'DELETE', 'read': 'GET' };If you’re familiar with that particular flavour of RESTful API then this should all look familiar.
The second argument, model, is a Backbone.Model or Backbone.Collection – collections are used when reading multiple values.
The final argument, options, is an object that contains success and error callbacks. It’s ultimately handed off to the jQuery Ajax API.
To work with Google’s APIs we need to write our own Backbone.sync method. In general terms our implementation should be structured like this:
Backbone.sync = function(method, model, options) { switch (method) { case 'create': googleAPI.create(model, options); break; case 'update': googleAPI.update(model, options); break; case 'delete': googleAPI.destroy(model, options); break; case 'read': // The model value is a collection in this case googleAPI.list(model, options); break; default: // Something probably went wrong console.error('Unknown method:', method); break; } };The googleAPI object is fictitious, but this is basically how Backbone.sync is usually extended – a lightweight wrapper that maps the method names and models to another API. Using a lightweight wrapper means the underlying target API can be easily used outside of a Backbone.js.
In our case, Google actually provides a JavaScript API – there will be a gapi.client object available once the Google APIs have been loaded.
Setting Up A Google API Account
The main page for Google’s developer documentation is at developers.google.com, but what we’re interested in is the Google Tasks API which can be found under Application APIs.
Google’s Application APIs are designed to work well with both server-side scripting and client-side JavaScript. To work with the Google Tasks API you’ll need three things:
- A Google account (an existing one is fine)
- Google API Console access (you may have already enabled it if you work with Google’s services)
- An API key
To set up your account to work with the Google API Console, visit code.google.com/apis/console. Once you’ve enabled it, scroll down to Tasks API:
Then switch the button to on, and accept the terms (if you agree to them). Next, click API Access in the left-hand navigation bar, and look under Simple API Access for the API key. This “browser apps” key is what we need. Make a note of it for later.
OAuth 2.0 for Client-side Applications
Still in the API Access section of the console, click the button to create an OAuth 2.0 project. Enter “bTask” (or whatever you want) for the product name, then http://localhost:8080 for the URL. In the next dialog, make sure http:// is selected instead of https://, then enter localhost:8080 and click “Create client ID”.
You’ll now see a set of values under “Client ID for web applications”. The field that says “Client ID” is important – make a note of this one as well.
You should now have an API key and a Client ID. These will be used to load Google’s APIs and allow us to use an OAuth 2.0 service from within the browser – we won’t need to write our own server-side code to authenticate users.
Follow Along
If you want to check out the source from Part 1 so you can follow along, you can use Git to get the exact revision from last week:
git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git cd dailyjs-backbone-tutorial git reset --hard 2a8517eRequired Libraries
Before progressing, download the following libraries to app/js/lib/:
Open app/js/main.js and edit the shim property under requirejs.config to load Underscore.js and Backbone.js:
requirejs.config({ baseUrl: 'js', paths: { }, shim: { 'lib/underscore-min': { exports: '_' }, 'lib/backbone-min': { deps: ['lib/underscore-min'] , exports: 'Backbone' }, 'app': { deps: ['lib/underscore-min', 'lib/backbone-min'] } } }); require([ 'app' ], function(App) { window.bTask = new App(); });This looks weird, but remember we’re using RequireJS to load scripts as modules. RequireJS “shims” allow dependencies to be expressed for libraries that aren’t implemented using AMD.
Loading the Tasks API
The basic pattern for loading the Google Tasks API is:
- Load the Google API client library: https://apis.google.com/js/client.js
- Call gapi.client.load to load the “tasks” API
- Set the API key using gapi.client.setApiKey()
To implement this, you’ll need a place to put the necessary credentials. Create a file called app/js/config.js and add the API key and Client ID to it:
define([], function() { var config = {}; config.apiKey = 'your API key'; config.scopes = 'https://www.googleapis.com/auth/tasks https://www.googleapis.com/auth/userinfo.profile'; config.clientId = 'your client ID'; return config; });This file can be loaded by our custom Google Tasks API/Backbone.sync implementation.
Now create a new file called app/gapi.js:
define(['config'], function(config) { function ApiManager() { this.loadGapi(); } _.extend(ApiManager.prototype, Backbone.Events); ApiManager.prototype.init = function() { }; ApiManager.prototype.loadGapi = function() { }; Backbone.sync = function(method, model, options) { options || (options = {}); switch (method) { case 'create': break; case 'update': break; case 'delete': break; case 'read': break; } }; return ApiManager; });This skeleton module shows the overall layout of our Google Tasks API loader and Backbone.sync implementation. The ApiManager is a standard constructor, and I’ve used Underscore.js to inherit from Backbone.Events. This code will be asynchronous, so event handling will be useful later.
The loadGapi method loads Google’s JavaScript using RequireJS. Once the gapi global object has been found, it will do the rest of the configuration by calling the init method:
ApiManager.prototype.loadGapi = function() { var self = this; // Don't load gapi if it's already present if (typeof gapi !== 'undefined') { return this.init(); } require(['https://apis.google.com/js/client.js?onload=define'], function() { // Poll until gapi is ready function checkGAPI() { if (gapi && gapi.client) { self.init(); } else { setTimeout(checkGAPI, 100); } } checkGAPI(); }); });All the init method needs to do is load the Tasks API with gapi.client.load:
ApiManager.prototype.init = function() { var self = this; gapi.client.load('tasks', 'v1', function() { /* Loaded */ }); function handleClientLoad() { gapi.client.setApiKey(config.apiKey); window.setTimeout(checkAuth, 100); } function checkAuth() { gapi.auth.authorize({ client_id: config.clientId, scope: config.scopes, immediate: true }, handleAuthResult); } function handleAuthResult(authResult) { } handleClientLoad(); };The config variable was one of the dependencies for this file, and contains the credentials required by Google’s API.
Loading the API Manager
Now open app/js/app.js and make it depend on gapi, then create an instance of ApiManager:
define([ 'gapi' ], function(ApiManager) { var App = function() { this.connectGapi(); }; App.prototype = { connectGapi: function() { this.apiManager = new ApiManager(); } }; return App; });If you want to check this works by running the tests, you’ll need to change test/setup.js to flag gapi as a global:
var assert = chai.assert; mocha.setup({ ui: 'tdd' , globals: ['bTask', 'gapi', '___jsl'] });However, I don’t intend to load the API remotely during testing – this will effectively be mocked. I’ll come onto that in a later tutorial.
Results
If you run the app or tests and open a JavaScript console, a gapi global object should be available. Using Google’s APIs with RequireJS and Backbone.js seems like a lot of work, but most of this stuff is effectively just configuration, and once it’s done it should work solidly enough, allowing you to focus on the app design and development side of things.
Full Source Code
References
Backbone.Marionette is a composite application library for Backbone.js that aims to simplify the construction of large scale JavaScript applications. It is a collection of common design and implementation patterns found in the applications that we have been building with Backbone, and includes pieces inspired by composite application architectures, event-driven architectures, messaging architectures, and more.
# Backbone.ViewMaster Few **tested** opinions on how to handle deeply nested views in [Backbone.js][] focusing on encapsulation and reusability. This is implemented after writing several Backbone.js applications and by carefully picking the recurring patterns seen on them. Backbone.ViewMaster is a single View extended from Backbone.View. Views extended from it can be infinitely nested with each others using the four nesting methods [setView][], [appendView][], [prependView][] and [insertView][]. There is no separate concept of layouts or list views. It's just a humble Backbone View Class with versatile nesting capabilities. The main idea behind Backbone.ViewMaster is that views should be small and independent building blocks of application UI. Read the tutorial to see how it's encouraged. Created by [Esa-Matti Suuronen][esa] [@EsaMatti][]. # Download - [backbone.viewmaster.js][dev] **12K** with comments - [backbone.viewmaster.min.js][production] **2.7K** minified # API Docs [Here][api] # Tutorial In this tutorial we build the classic TODO app and go through the most important features of Backbone.ViewMaster while discussing the ideas behind them. ## Basics `Backbone.ViewMaster` is a View extended from `Backbone.View`. You start by extending your custom views from it. Lets define a layout for our app with some container elements for our nested views. ``` ``` ```javascript var TodoLayout = Backbone.ViewMaster.extend({ template: function(context){ return _.template($("#layout").html(), context); } }); ``` First thing you do for every ViewMaster view is set a [template][] function for it. It can be any function which takes a context object as the first argument and returns neither a HTML string or a DOM object. The context object is by default `this.model.toJSON()` or an empty object if your view does not have a model. You can customize this behavior by overriding the [context][] method. There is no need to define the [render][] method. ViewMaster already defines it and uses your template function to populate the `this.el` element. ## Nesting Now we create a nested view for the `addview-container`. ``` ``` ```javascript var AddTodoItem = Backbone.ViewMaster.extend({ template: function(context){ return _.template($("#addview").html(), context); }, events: { "click button": "addTodo" }, addTodo: function(e){ e.preventDefault(); this.collection.add(new Backbone.Model({ text: this.$("input").text() }); } }); ``` Nested views are also extended from `Backbone.ViewMaster`. Any ViewMaster view can be nested in any ViewMaster view and you can do as deep nesting as you want. Since we wanted this to be the view for the `addview-container` and because it's an element of `TodoLayout` it is the responsibility of `TodoLayout` to nest it. We do that in its constructor using the [setView][] method. ```javascript // TodoLayout constructor: function(){ Backbone.ViewMaster.prototype.constructor.apply(this, arguments); this.setView(".addview-container", new AddTodoItem({ collection: this.collection })); }, ``` Here it's important to notice that [setView][] and its friends, [appendView][], [prependView][], [insertView][] and [getViews][] should be considered in Java terms as protected methods. Which means you should use them only from within the view definition. This is because they all take a CSS selector as the first argument and because the CSS selectors are very implementation specific details of your view. If you need to set the view from the outside create a setter method for it to keep your views encapsulated and maintainable. ## Event management Event management is important part of view management in Backbone.js. Unfortunately the event callbacks start easily leaking memory if you don't remember to unbind them when you are done with the views. ViewMaster helps with this by introducing a [bindTo][] method which is bound to the view context. It will unbind all event callbacks automatically when you discard the view with [remove][]. ```javascript // TodoLayout constructor: function(){ ... snip ... this.bindTo(this.model, "change", this.render); }, ``` ## Rendering Now we are ready to create and render our nested view. We do that by calling the default render method. ```javascript var items = new Backbone.Collection(); var app = new Backbone.Model(); var layout = new TodoLayout({ model: app, collection: items }); layout.render(); $("body").append(layout.el); app.set("name", prompt("Your name")); ``` The render method takes care of rendering itself and the initial rendering of its child views. This means it will **render child views only once** unless `{ force: true }` is passed to the render method. This is because normally it should be the responsibility of the child view to know when it should render itself. The parent view only helps child views to get started. ### Child views We add new TodoItems to our layout whenever a todo model is added to the collection. When a new child view is added you need to call [render][] or [renderViews][] on the parent view to make them visible. ```javascript // TodoLayout constructor: function(){ ... snip ... this.bindTo(this.model, "change", this.render); this.bindTo(this.collection, "add", this.addItem); }, addItem: function(model){ this.appendView(".todo-container", new TodoItem({ model: model })); this.renderViews(); } ``` Here we use the [appendView][] method to append a view to `.todo-container` when a model is added to the collection. Every view container can contain multiple views. Just start adding more views to it if you need lists. The difference between [render][] and [renderViews][] is that the latter one renders only the new child views and adds them to the parent DOM tree leaving the parent untouched otherwise while the former renders the parent itself and the children. ## Removing views Any view, parent or child, can be discarded with the [remove][] method. It removes automatically all the Backbone and DOM event callbacks. If the view is a parent to other views it will call remove on them also. ``` ``` ```javascript var TodoItem = Backbone.ViewMaster.extend({ constructor: function(){ Backbone.ViewMaster.prototype.constructor.apply(this, arguments); this.bindTo(this.model, "change", this.render); }, template: function(context){ return _.template($("#item").html(), context); }, events: { "click .done": "done", "click .edit": "edit" }, done: function(){ this.model.destroy(); this.remove(); }, edit: function() { var newContent = prompt("Edit todo", this.$(".item").text()); if (newContent !== null) this.model.set("text", newContent); } }); ``` Views can be also removed by replacing them with [setView][]. ViewMaster automatically figures out which views was left out and calls [remove][] on them on the next [renderViews][] call. If you need to use the view and its children again some time later use the [detach][] method. It removes the view from in its parent view, but leaves the event callbacks untouched and children untouched. ## Event bubbling In order to keep views decoupled and resusable their implementation should not assume anything about their parents. When you need to communicate with the parent use events to send messages to them. Backbone.ViewMaster helps with this by implementing DOM like event bubbling: Event triggered in a child view is also seen on its parents all the way up to the view tree unless explicitly silenced. ```javascript // Bubbled event "refresh" to parents view.trigger("refresh"); // Do not bubble event "myevent" to parents view.trigger("myevent", { parent: false }); ``` ## Conclusion That's about it. Check out the full working todo app in the examples [directory][todo-example] or play with the live app [here][todo-live]. # FAQ ## How do I use jQuery plugins? Just override the render method, call the super method and then do your jQuery plugin stuff: ```javascript render: function(){ Backbone.ViewMaster.prototype.render.apply(this, arguments); this.$("element").jqueryPlugin(); } ``` ## Doesn't `Backbone.View#dispose()` unbind events already on remove? Some. It only works with events bound to `this.model` or `this.collection` and it also requires that the view is passed as the context object to the `on` method. To make sure that all events are always unbound on remove I recommend that you use always `view.bindTo(...)` with Backbone.ViewMaster. *DISCLAIMER: `dispose()` is a feature of unreleased Backbone.js version and it might change before actual release.* # License The MIT License. See LICENSE. [Backbone.js]: http://backbonejs.org/ [@EsaMatti]: https://twitter.com/EsaMatti [esa]: http://esa-matti.suuronen.org/ [dev]: http://epeli.github.com/backbone.viewmaster/lib/backbone.viewmaster.js [production]: http://epeli.github.com/backbone.viewmaster/lib/backbone.viewmaster.min.js [api]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html [setView]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_setView [appendView]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_appendView [prependView]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_prependView [insertView]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_insertView [bindTo]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_bindTo [template]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_template [render]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_render [renderViews]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_renderViews [context]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_context [rendered]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#property_rendered [getViews]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_getViews [detach]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_detach [remove]: http://epeli.github.com/backbone.viewmaster/classes/Backbone.ViewMaster.html#method_remove [todo-example]: https://github.com/epeli/backbone.viewmaster/tree/master/examples/todos [todo-live]: http://epeli.github.com/backbone.viewmaster/examples/todos
In Part 2: Google’s APIs, I laid the groundwork for talking to Google’s JavaScript APIs. Now you’re in a position to start talking to the todos API, but first a user account is required.
Preparation
Before starting this tutorial, you’ll need the following:
- alexyoung / dailyjs-backbone-tutorial at commit 9d09a66b1f
- The API key from part 2
- The “Client ID” key from part 2
- Update app/js/config.js with your keys (if you’ve checked out my source)
To check out the source, run the following commands (or use a suitable Git GUI tool):
git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git cd dailyjs-backbone-tutorial git reset --hard 9d09a66b1fGoogle’s OAuth 2.0 Client-side API
Open app/js/gapi.js and take a look at lines 11 to 25. There’s a method, provided by Google, called gapi.auth.authorize. This uses the “Client ID” and some scopes to attempt to authenticate. I’ve already set the scopes in app/js/config.js:
config.scopes = 'https://www.googleapis.com/auth/tasks https://www.googleapis.com/auth/userinfo.profile';This tells the authentication system that our application would like to access the user’s profile and Gmail tasks. Everything is almost ready to work, but two things are missing: an implementation for handleAuthResult and an interface.
Templates
RequireJS can load templates by using the text plugin. Download text.js from GitHub and save it to app/js/lib/text.js.
This is my preferred technique for handling templates. Although this application could easily fit into a monolithic index.html file, breaking up projects into smaller templates is more manageable in the long run, so it’s a good idea to get used to doing this.
Now open app/js/main.js and add the text plugin to the paths property of the RequireJS configuration:
paths: { text: 'lib/text' },Finally, add this to app/js/config.js:
_.templateSettings = { interpolate: /\{\{(.+?)\}\}/g };This tells Underscore’s templating system to use double curly braces for inserting values, otherwise known as interpolation.
The app needs some directories to store template-related things:
- app/js/views – This is for Backbone.js views
- app/js/templates – Plain HTML templates, to be loaded by the views
- app/css
The app/index.html file needs to load the CSS, so add a link tag to it:
<link rel="stylesheet" href="css/app.css">And create a suitable CSS file in app/css/app.css:
#sign-in-container, #signed-in-container { display: none }The application will start up by hiding both the sign-in button and the main content. The oauth API will be queried for existing user credentials – if the user has already logged in recently their details will be stored in a cookie, so the views need to be configured appropriately.
Templates
The templates aren’t particularly remarkable at this stage, just dump this into app/js/templates/app.html:
<div class="row-fluid"> <div class="span2 main-left-col" id="lists-panel"> <h1>bTask</h1> <div class="left-nav"></div> </div> <div class="main-right-col"> <small class="pull-right" id="profile-container"></small> <div> <div id="sign-in-container"></div> <div id="signed-in-container"> <p>You're signed in!</p> </div> </div> </div> </div>This template shows some things that we won’t be using yet, just ignore it for now and focus on sign-in-container and signed-in-container.
Next, paste the following into app/js/templates/auth.html:
<a href="#" id="authorize-button" class="btn btn-primary">Sign In with Google</a>The auth.html template will be inserted into sign-in-container. It’s very simple at the moment, I only really included it for an excuse to create extra Backbone.js views so you can see how it’s done.
Backbone Views
These templates need corresponding Backbone.js views to manage them. This part demonstrates how to load templates with RequireJS and render them. Create a file called app/js/views/app.js:
define([ 'text!templates/app.html' ], function(template) { var AppView = Backbone.View.extend({ id: 'main', tagName: 'div', className: 'container-fluid', el: 'body', template: _.template(template), events: { }, initialize: function() { }, render: function() { this.$el.html(this.template()); return this; } }); return AppView; });The AppView class doesn’t have any events yet, but it does bind to an element, body, and load a template: define(['text!templates/app.html']. The text! directive is provided by the RequireJS “text” plugin we added earlier. The template itself is just a string that contains the corresponding HTML. It’s rendered by binding it to the Backbone.View, and then calling jQuery’s html() method which replaces the HTML within an element: this.$el.html(this.template());.
The AuthView is a bit different. Create a file called app/js/views/auth.js:
define(['text!templates/auth.html'], function(template) { var AuthView = Backbone.View.extend({ el: '#sign-in-container', template: _.template(template), events: { 'click #authorize-button': 'auth' }, initialize: function(app) { this.app = app; }, render: function() { this.$el.html(this.template()); return this; }, auth: function() { this.app.apiManager.checkAuth(); return false; } }); return AuthView; });The app object is passed to initialize when AuthView is instantiated (with new AuthView(this) later on). The reason I’ve done this is to allow the view to call the required authentication code from ApiManager. This could also be handled with events, or many other ways – I just wanted to show that we can initialise views with values just like any other class.
App Core
The views need to be instantiated and rendered. Open app/js/app.js and change it to load the views using RequireJS:
define([ 'gapi' , 'views/app' , 'views/auth' ], function(ApiManager, AppView, AuthView) { var App = function() { this.views.app = new AppView(); this.views.app.render(); this.views.auth = new AuthView(this); this.views.auth.render(); this.connectGapi(); }The rest of the file can remain the same. Notice that the order these views are rendered is important – AuthView won’t work unless it has some of AppView’s tags available. A better way of modeling this might be to move AuthView inside AppView so the dependency is reflected. You can try this yourself if you want to experiment.
Authentication Implementation
The app/js/gapi.js file still doesn’t have the handleAuthResult function, so nothing will work yet. Here’s the code to handle authentication:
function handleAuthResult(authResult) { var authTimeout; if (authResult && !authResult.error) { // Schedule a check when the authentication token expires if (authResult.expires_in) { authTimeout = (authResult.expires_in - 5 * 60) * 1000; setTimeout(checkAuth, authTimeout); } app.views.auth.$el.hide(); $('#signed-in-container').show(); } else { if (authResult && authResult.error) { // TODO: Show error console.error('Unable to sign in:', authResult.error); } app.views.auth.$el.show(); } } this.checkAuth = function() { gapi.auth.authorize({ client_id: config.clientId, scope: config.scopes, immediate: false }, handleAuthResult); };The trick to a smooth sign-in flow is to determine when the user is already signed in. If so, then authentication should be handled transparently, otherwise the user should be prompted.
The handleAuthResult function is called by gapi.auth.authorize from the checkAuth function, which isn’t displayed here (it’s before handleAuthResult in the source file if you want to check it). The this.checkAuth method is different – this is a public method that calls gapi.auth.authorize with immediate set to false, while the other invocation calls it with true.
The immediate option is important because it determines whether a popup will be displayed or not. I’ve used it to check if the user is already signed in, otherwise it’s called again with immediate: false and will display a suitable popup so the user can see what permissions the app wants to use:
I designed it this way based on the Google APIs Client Library for JavaScript documentation:
“The standard authorize() method always shows a popup, which can be a little jarring if you are just trying to refresh an OAuth 2.0 token. Google’s OAuth 2.0 implementation also supports “immediate” mode, which refreshes a token without a popup. To use immediate mode, just add “immediate: true” to the login config as in the example above.”
I’ve also changed the ApiManager class to store a reference to App:
// Near the top of gapi.js var app; function ApiManager(_app) { app = _app; this.loadGapi(); }Summary
In this tutorial you’ve seen how to use Google’s APIs to sign into an app you’ve previously registered with the Google API Console (documented in part 2). It might seem like a lot of work to get RequireJS, Backbone.js, and Google OAuth working together, but think about what this has achieved: 100% client-side scripting that can authenticate with existing Google accounts.
If I’ve missed anything here, you can check out the full source from commit c1d5a2e7c.