Let me be clear: there are virtually endless ways you can build a JavaScript application, between modules, loaders, sync and async, reliance on advanced features and whatnot.
I often feel like I'm just scratching the surface, no matter how many I build, nor how many I read the source of.
These are a set of patterns that I commonly repeat throughout when developing Backbone.js apps.
I hope they're helpful to you!
Backbone views, though what they should do is mostly clear (handle the presentation logic of any given part of the UI), when scaling applications up to a certain point, the lines start getting blurry.
You often end up with a mesh of relationships between models and views that goes both ways. And from there, it only goes downhill.
You may also have views that have no models associated, or views that talk to views. None of that stuff is too well documented. People find out how to deal with it by trial and error.
The thing to bear in mind though is: apply sensible software development, not The Rules Of Correct Backbone.js Usage™ by Whoever.
Let's start with a relatively quirky subject: templates.
The straight-in-the-body-script-tags approach:
It consists of putting your templates straight in the HTML where your JavaScript app will run on.
<script type="text/template" id="item-template">
<h1><%= name %></h1>
<p><%= description %></p>
</script>
<script type="text/template" id="another-template">
<p><%= foo %></p>
</script>
<script src="/js/jquery.js"></script>
<script src="/js/backbone.js"></script>
<script src="/js/app.js"></script>
The good: quick n' easy for small stuff, prototypes, or generally where you won't have that many templates.
The bad: any other situation, as you'll end up with tons of template markup embedded in your HTML.
Most web applications' HTML is rendered by templates (e.g.: Haml or ERB in Ruby, or Jade in node.js).
If that's the case with your application, you can (and probably should) use that to keep your templates on the server side, and then "export" them to the client by injecting JavaScript in the document. That is, as opposed to writing them straight in HTML like in the previous slide.
This will allow you to write your templates in a meta language, if that's your cup of tea.
People often ask me how to share templates between the server and the client. That's how you do it.
It goes like this: before loading the templates, make sure you have an object to store them.
window.App = { Templates : {} };
Then you put your templates in the App.Templates object assigned to properties
that read like a Unix file path, which makes it nice and readable.
But that can be done in two ways: you can load a JavaScript file which is actually
generated on the server, containing all the templates you need for your app in it,
and then source it from a <script> tag before
your application code...
// dependencies...
<script src="/templates.js"></script>
<script src="/app.js"></script>
Where the output of templates.js looks like:
App.Templates['items/new'] = _.template('<form>...</form>');
App.Templates['items/list'] = _.template('<ul>...</ul>');
...
... or you could write a helper function and call it in your application's template.
// For example, in ERB/Ruby, load all dependencies, and then...
<%= load_templates %>
<script src="/app.js"></script>
The code for both the helper and the /templates.js approach helper would look roughly like this (in pseudo code):
for each template in (path to templates on the server)
output_javascript("App.Templates['#{template path}'] = _.template('#{template contents}')")
So the object would look like App.Template[<path to template>] → <template object>.
Of course, should you be using a different templating engine than what's in underscore.js, modify it accordingly.
for each template in (path to templates on the server)
output_javascript("App.Templates['#{template path}'] = Mustache.compile('#{template contents}')")
The templates will be accessible from your JavaScripts like so:
App.Templates['items/list']
App.Templates['items/new']
App.Templates['home']
// And so on...
The good: Reads nicely. Good performance-wise, as you take advantage of page caching. You get to use Haml, Jade, and other goodies.
The bad: Not much really! From the perspective of performance, it's not too ideal since you need to load all templates before the app kicks in, though that's easily mitigated by bundling the templates along with the JavaScripts (say, by using Sprockets, or rack-pagespeed).
The asynchronous templates object approach:
This works much like the previous example. Except you'd fetch the templates using Ajax in runtime.
You'll need to make your templates available in an API call though, so the JavaScript app can get them from somewhere.
More or less like this:
App.Templates = {
fetch : function(callback) {
var self = this;
// Assuming /mytemplates.json returns a JSON array of
// template objects, { path : '/path', contents : 'contents' }
$.get('/mytemplates.json', function(templates) {
_.each(templates, function(template) {
self[template.path] = template.contents;
});
if (_.isFunction(callback)) callback();
});
}
};
Then somewhere else in the app, you need to call App.Templates.fetch()
and ensure that only when the templates are returned, the views (or to make things easier
the app) are loaded.
This is handy if you plan on showing something while the application loads. Soon as the application code is loaded, it could then replace a "please wait" sort of screen and boot up.
App.Templates.fetch(function() {
$('#splash').fadeOut(250);
// ... initialize models and views
});
The good: No littering whatsoever. You could potentially load templates selectively and on demand, though it'd take a bit more work.
The bad: You need to ensure the templates are available before your views need them, which could take quite a lot more work.
Now you know how to handle templates. But you need views to work with them.
But first, think of views as this: a miniature, mostly self-contained application which takes care of a small piece of the UI.
A view can be contained by another view, such as when you start breaking up one that's becoming too complex into smaller ones.
And remember: you can get away with breaking a lot of rules if you break them sensibly, keep things pretty and modular as opposed to tightly coupled.
Let's get our hands dirty...
Total: $14
Ain't gorgeous, but it'll do.
It has a "master" view, or let's call it ApplicationView.
This view cares about stuff such as sign in/out, and other changes
that have application-wide implications.
And then 3 "child" views that are more specific.
Let's look at what the code for each of those sections could be.
Straightforward, the only logic is checking whether the user is signed in or not.
var NavigationView = Backbone.View.extend({
el : $('nav'),
// ...
render : function() {
this.$el.html(App.Templates['nav'](App.currentUser));
}
})
Template code:
<ul>
<li>
<a role="home">Home</a>
</li>
<li>
<a role="items">Items</a>
</li>
<li>
<a role="settings">Settings</a>
</li>
<% if (isSignedIn()) { %>
<li>
<a role="sign-out">Sign Out</a>
</li>
<% } else { %>
<li>
<a role="sign-in">Sign In</a>
</li>
<% } %>
</ul>
Nothing too special about the cart either.
var CartView = Backbone.View.extend({
el : $('#cart'),
events : {
'click a.browse' : 'browse'
},
initialize : function() {
App.CartItems.on('add', 'add', this);
App.CartItems.on('reset', 'addAll', this);
App.CartItems.fetch();
}
add : function(item) {
var li = this.make('li', null, item.escape('name'));
this.$el.find('ul').append(li);
},
addAll : function() {
_.each(App.CartItems, this.add);
},
render : function() {
this.$el.find('ul').empty();
this.addAll();
}
browse : function() {
App.Router.navigate('shopping-cart', true);
}
});
Lets call this one WeeklyNewItemsView. Here's where things
will get interesting: I'll use complexity to gauge whether you ought to
have a "proper" view for individual entries in this list.
For a regular user (let's assume we have admins and users), all that this view does is it renders a list of items which the user can read from, as well as click an item and be taken to the item's page where he/she can see further details about it.
In this case, we do not need a template nor a view for each item.
We can get away with simply using make to create an
element, assigning a click event to it, and injecting it into the list.
var WeeklyNewItemsView = Backbone.View.extend({
// ...
add : function(item) {
var self = this,
li = $(this.make(li, null, item.escape('name')));
li.on('click', function() { self.browse(item.id); });
this.$el.find('ul').append(li);
return this;
},
// ...
});
"Oh but it might get more complex one day".
Software 101: The right measure of code is the least amount of code necessary to get the job done, without compromising on readability.
But say at some point we need to allow for inline item editing, should the user own a product, as an admin for the shop.
Now we have enough to warrant a view of its own.
We'll declare a new view called WeeklyNewItemView.
The view's template would look like this:
<p>
<%= name %>
<a role="edit">Edit</a>
</p>
<form>
<input type="text" value="<%= name %>" name="name" placeholder="Item's name">
<input type="text" value="<%= description %>" name="description" placeholder="Item's description">
<input type="text" value="<%= price %>" name="price" placeholder="Item's price">
</form>
... and the code would look like this:
var WeeklyNewItemView = Backbone.View.extend({
tagName : 'li',
events : {
'click' : 'browse',
'click a[role="edit"]' : 'toggleEdit'
},
render : function() {
this.$el.html(
App.Templates['items/weekly_new_item'](this.model.toJSON())
);
return this;
},
toggleEdit : function(event) {
event.stopPropagation(); // don't interfere with the root elt click
var p = this.$el.find('p'), form = this.$el.find('form');
if (p.is(':visible')) {
p.hide();
form.show();
} else {
form.hide();
p.show();
}
},
browse : function() {
App.Router.navigate('items/' + this.model.id, true);
}
});
And back to WeeklyNewItemsView:
var WeeklyNewItemsView = Backbone.View.extend({
// ...
add : function(item) {
var self = this, li, isAdmin = App.currentUser.isAdmin();
if (isAdmin) {
li = new WeeklyNewItemView({ model : item }).render().el;
} else {
li = $(this.make(li, null, item.escape('name')));
li.on('click', function() { self.browse(item.id); });
}
this.$el.find('ul').append(li);
return this;
},
// ...
});
Now I did mention earlier on that we have an ApplicationView.
Its role in here would be to keep track of the application's state.
In this example we have basically 2 discernible states: signed in, or not signed in.
The role that ApplicationView could play
here is tracking both sign-in and sign-out events,
and then having each view call render().
var ApplicationView = Backbone.View.extend({
initialize : function() {
App.currentUser.on('sign-in sign-out', this.render, this);
},
// ...
render : function() {
App.Views.Menu.render();
App.Views.Cart.render();
App.Views.WeeklyNewItems.render();
return this;
}
// ...
});
This is simple, but apparently not many people realise they can do it.
You can have a single view class for two states (e.g.: user signed-in / not signed in), and differentiate on the template level only.
var FooView = Backbone.View.extend({
events : {
'click' : 'browse',
'click menu a.edit' : 'toggleEdit',
'click menu a.delete' : 'delete'
},
render : function() {
if (App.currentUser.isAdmin()) {
this.$el.html(App.Templates['foo/admin']());
} else {
this.$el.html(App.Templates['foo/user']());
}
return this;
}
});
If App.Templates['foo/user'] has no <menu></menu>,
those last two events will simply not get attached to anything.
This is what I call a top-down approach: you write the most complex view only, and the less complex ones are made so by omitting some elements from the template.
Always instantiate a App.currentUser object in case the concept of
sign in exists in your app.
<script src="/js/app.js"></script>
<script>
// App object instantiated somewhere else
App.currentUser = new User;
</script>
Then write a few helpers in your user model:
var User = Backbone.Model.extend({
isSignedIn : function() {
return !this.isNew();
},
signIn: function(email, password, onFail, onSucceed) {
$.ajax({
url : '/sign-in',
method : 'POST',
dataType : 'json',
data : { email : email, password : password },
error : onFail,
success : onSucceed,
context : this
});
return this;
},
signOut : function() {
$.ajax({
url : '/sign-out',
method : 'POST'
}).done(function() {
this.clear();
this.trigger('signed-out');
});
}
});
Note that isNew() is defined by Backbone, and returns true when
a model's id attribute is not set, which it won't be should the model
be "empty", or without any attributes set.
The gist here is that App.currentUser will either be empty when the
user is not signed in, or it'll have the attributes of the user who's signed in.
Speaking of signing in and out, when the user arrives on a page and he/she is still carrying a valid session (i.e.: having previously signed in successfully and closed the browser), don't make the extra roundtrip of a request to the server to check for a valid session if you don't really need to.
In scenarios where the latency is too high, the experience is simply horrendous. Remember, fast is good. Users want fast.
For example, in Ruby/Rails and ERB land, on your application template:
<script src="/foo.js"></script>
<script>
<% if signed_in %>
App.currentUser = new User(<%= current_user.toJSON %>);
<% else %>
App.currentUser = new User;
<% end %>
Backbone.history.start();
</script>
Then in your router, when you're checking whether the user is signed in or not
before calling in the views, isSignedIn() returns true and the user
will just see things how they were left.
At times, you'll need to restrict parts of an application to users who are authenticated, or have more privileges to see certain things.
The purpose of doing this in a JavaScript application is not to make anything more secure, since you have to do your checks on the server-side anyway, but to avoid confusion (users seeing buttons or whole screens they shouldn't, etc).
On a related note, I never find myself doing validations and that sort of thing on the client, as I find it a pain in the ass to do the double work of checking on both the server and the client. You should though handle errors coming from the server (API) and be informative enough through error messages.
This is a common pattern I use to restrict sections of an application.
var AuthenticatedRouter = Backbone.Router.extend({
requireLogin : function(ifYes) {
if (App.currentUser.isSignedIn()) {
if (_isFunction(ifYes)) ifYes.call(this);
} else {
this.navigate('/?login-failed', true);
}
}
});
And then...
var SecretAreaRouter = AuthenticatedRouter.extend({
urls : {
'secret' : 'home'
},
home : function() {
this.requireLogin(function() {
new SecretAreaView.render();
});
}
});
It reads damn nicely in CoffeeScript:
class SecretAreaRouter extends AuthenticatedRouter
urls :
'secret' : 'home'
home : ->
@requireLogin ->
new SecretAreaView.render()
// do this and that
Assuming you're familiar with Backbone.js, you're probably not entirely new to this. There are ways to be effective with it though.
Namespacing consists of putting your application's classes/objects/functions (or models, views, routers) nested in an object to avoid littering the global namespace.
If not for any bullshit performance reason someone will give you at some stage, this helps keeping things organised and readable.
AppRouter = Backbone.Router.extend({
// ..
home : function() {
App.Views.Hello = new Lib.Views.Hello;
App.Views.Hello.render();
}
});
Note that there's 2 objects that I'm keeping in the global namespace: App and
Lib. The App object is where the actual application sits, with
models, views, routers and everything else in "ready to use" or instantiated form.
Lib is where I'm declaring my classes in. You are keeping your JavaScript classes'
code (whatever their purpose is) in separate files, right?
Say you have a view named MyView, which sits in javascripts/my_view.js.
Having previously declared Lib in the global window namespace, you could
add the class to it like so:
(function(views) {
views.MyView = Backbone.View.extend({
// ...
});
})(window.Lib.Views);
If you think this is too much to worry about, that's fine. Make sure though you don't skip
using an App namespace at least. You're otherwise welcome to put your
uninstantiated classes in the global namespace.
AppRouter = Backbone.Router.extend({
// ..
home : function() {
App.Views.Hello = new HelloView; // HelloView sitting in the window object
App.Views.Hello.render();
}
});
Seriously, no kittens are gonna die.
These suggestions, like nearly everything in software, scale well — to a point.
For example, in the previous online shop example, if you have a dozen sub-views, having
ApplicationView#render call render on each and every
one of them is code smell. Not just because, but because you'll have a single view
holding too many separate (and probably unrelated) pieces together.
If you're talking about a large enough app, you'll need to break things down into pieces, and probably have a few views playing the role of state machines / tracking what the smaller views are doing.
Don't be afraid to borrow patterns that you have been using on the back-end since longer than JS is around. This is simple modularisation we're talking about. It works.
Reach me on twitter on @julio_ody, or via email at julio@awesomebydesign.com!
Special thanks to the suggestions made by @toolmantim, @divya, and @mulpat. Truly helpful stuff guys!
/
#