Backbone is a very powerful application development framework. However, it can be a little “close to the metal” in terms of how much work is needed to produce a working application with it. I see Backbone as a low level framework that could use some help in making it a bit easier and faster to use.
Keyhole has released an extension to help! The backbone.khs framework extension npm module (available by clicking the link) does its best to minimize the work necessary to get a Backbone application up and running.
The extension makes it easier to deal with:
- browser history
- root level non-Model Object implementation
- caching
- session support
- regions (which break pages up into more workable segments)
- a top-level Application object to manage the application
- modules to help with page and URL routing
- a Backbone View extension to seamlessly integrate Backbone Stickit and make Marionette templates easier
- a Collection View to enhance working with groups of items.
In this blog, I’ll describe these enhancements with some code examples. Look for a fully working example solution that replaces the (overly) simple plain JavaScript Whirlpool UI from my previous blog soon!
Browser History
The backbone.khs extension overrides the loadUrl()
function in Backbone’s History
object. This function loops through all available handlers, finding the route with the highest score, and uses that instead of simply picking the first handler that returns any positive result. The extension is called just like the normal Backbone.History.loadUrl
(fragment) call.
Root Level Object
The class
Object provides a non-Model top level class. This class supports Backbone’s “extend” functionality, and includes support for the optional initialize()
function when an object is created. All of the other classes in the framework extension extend from this class.
Caching
The backbone.khs extension provides a global Cache for keeping track of data. The cache can expire data at the end of a session, add, remove, and find data in the cache. Cache keys are simple strings. The following example places sampleObj
into the cache with the key “sample” that expires when the session ends, retrieves the data, then manually removes it from the cache.
Backbone.cache.put('sample', sampleObj, {expire: 'session:end'}); var sampleObj = Backbone.cache.get('sample'); Backbone.cache.remove('sample');
Session Support
The backbone.khs extension adds a Session
class that provides some basic structure around the session. It keeps track of whether or not the user is authenticated, the principle (key) of the user, and what roles the user has. It also provides helper functions for invalidating the session, finding out if a user has a role, adding roles to a user, and authenticating the user.
Regions
A region is a section of a page that is rendered. A region manager handles all the regions that are present on a page. This additional layer of hierarchy helps in situations like common nav bars and other areas where portions of a page show up repeatedly. The Application
class uses regions to tear down and rebuild pages during routing.
Application
Application is the top-level manager of the page. It should be created and invoked when the page is finished loading.
Backbone.$(document).ready(function (event) { window.myApp = new Application(event); });
This is all it takes to kick off your app. Application should set up caching, the initial rendering, and handle redirecting the user to the login page when there is no session.
Here’s an example Application.js. This is meant more for perusing than copy/pasting and trying to run, as it would require more code than just what you see here.
require('bootstrap'); var Backbone = require('backbone.khs'); // need to load overrides require('./backbone.iris'); var _ = require('underscore'); var UnauthenticatedModule = require('./UnauthenticatedModule'); var AuthenticatedModule = require('./AuthenticatedModule'); var Application = Backbone.Application.extend({ initialize: function () { var session = new Backbone.Session(); this.on("all", function (eventName) { Backbone.cache.trigger(eventName); }, this); this.addRegions({ body: '#body' }); window.location.hash = ''; Backbone.history.start({pushState: true, silent: true}); this.unauthenticatedModule = new UnauthenticatedModule({path: '', regionManager: this.regions.body}); this.authenticatedModule = new AuthenticatedModule({path: '', regionManager: this.regions.body}); session.comply('authenticated:success', this.authenticated, this); session.comply('authenticated:fail', this.notAuthenticated, this); session.checkAuthentication(); this.on('session:start', function () { console.log('session started event'); }); this.on('session:end', function () { Backbone.history.navigate('/login', {trigger: true}); console.log('session ended event'); }); }, command: function (name) { // make sure we pass all application event to the cache Backbone.cache.command.apply(Backbone.cache, arguments); return Backbone.Application.prototype.command.apply(this, arguments); }, notAuthenticated: function () { // make sure we have a clean session this.session.invalidate(); this.unauthenticatedModule.start(); this.authenticatedModule.stop(); Utility.forceNavigate('/login'); Backbone.history.loadUrl(); }, authenticated: function (session) { this.unauthenticatedModule.stop(); Backbone.cache.put('_session', session); // setup event for logout - only want this once. this.session.complyOnce('authenticated:invalidated', this.notAuthenticated, this); this.doAuthenticatedRedirect(session); }, doAuthenticatedRedirect: function (session) { this.authenticatedModule.start(); var redirect = this.redirect; if (Backbone.cache.has('session')) { var theSession = Backbone.cache.get('session'), url = theSession.get('url'); if (url.indexOf('login') > -1) { window.Iris.trigger('session:end'); } else { Backbone.history.loadUrl(theSession.get('url')); window.Iris.trigger('session:start', theSession); } } else if (redirect) { Backbone.history.loadUrl(redirect, {trigger: true})); } else { Backbone.history.loadUrl(); } } }); Backbone.$(document).ready(function (event) { window.myApp = new Application(event); });
Modules
Modules process routes and create Views
to display data. A super minimal module is shown below.
The important piece is the routes
object, which is processed when a URL is being loaded. The route below only has the default since it only displays one page. Routes are hierarchical, which each layer of a URL (a/b/c) being processed by a Module at that folder level in the file system. All possible routes must be accounted for.
Modules also have the opportunity to be called when a session starts, session ends, before a route is processed, and after a route is processed. This provides a lot of flexibility for verification, validation, and security.
'use strict'; var Backbone = require('backbone.khs'); var Region = require('./RegionView'); var EditView = require('./EditView'); module.exports = Backbone.Module.extend({ region: Region, routes: { '': 'show' }, show: function () { var model = new Backbone.Model(), view = new EditView({model: model}); this.region.regions.body.show(view); } });
Backbone View Extension
The extension to the Backbone View adds a Backbone.Radio
channel name and Backbone.Stickit
bindings to the Backbone View. It also provides calls for beforeRender()
and afterRender()
, beforeShow()
and afterShow()
, checking to see if the view is rendered, rendering templates, and building the data that the template will use.
CollectionView
A CollectionView
handles displaying sets of data, typically results from a query. Displaying this data can be as easy as what is shown below, but it can also handle very complex cases too. This example uses a table to render, but divs can be used just as easily.
ResultsView.js
'use strict'; var Backbone = require('backbone.khs'); var template = require('./results.ejs'); var ChildView = require('./ResultView.js'); module.exports = Backbone.CollectionView.extend({ tagName: 'table', template: template, childSelector: 'tbody', childView: ChildView });
results.ejs
<thead> <tr> <th>Foo</th> </tr> </thead> <tbody> </tbody>
ResultView.js
module.exports = Backbone.ItemView.extend({ events: {}, tagName: "tr", template: template, initialize: function() { this.model.on('change', this.render, this); }, _getTemplateData: function () { return { foo: "foo" }; }, remove: function() { this.model.off('change', this.render, this); Backbone.View.prototype.remove.call(this); } });
result.ejs
<td><%- foo %></td>
Conclusion
This has been a super quick overview of the capabilities that the backbone.khs framework extension brings to the table for enhancing development using the excellent Backbone framework. Again, a full working example is coming soon!