Backbone.js on Vitae

Ryan Foster, Former Developer

Article Category: #Code

Posted on

I recently had the chance to work on Vitae, an online network for higher education professionals that features a variety of tools to help users manage career placement and advancement. Among those tools, profiles are probably the largest and most complex. They provide a wide range of flexibility and customization. Users can build profiles from more than a dozen unique sections like Education, Experience, Publications, and Grants. Within each section, they can create, edit, and arrange information about themselves. However they wish to be presented, a Vitae profile can accommodate.

But there's a catch: users are going to need to enter all that data. That can be a real pain.

We attacked the problem on multiple fronts with bulk editing and data importing from external sources. But there was no escape from beefing up the standard web forms. A bunch of page loads could cause too much friction for users to actually use the feature. Interactions had to be simple and clean. Potentially repetitive tasks had to be fast. Succeed, and users will cheer. Fail, and they will curse our names.

In which case, we were going to be writing a lot of JavaScript to power those pages. Code to push and pull data, to display data, and to orchestrate all the pieces. However, our problem domain wasn't really data persistence. Or data-view binding. Or event management. Sure, we needed all those things, but why let them distract us?

Enter Backbone.js! Backbone handled the generic problems so we could focus on making profile interactions fly.

Profile Editor

Let's look at some practices we used to leverage Backbone on Vitae profiles.

Bootstrapping data

This isn't a single-page app, so we weren't going to try to load data on demand once the Backbone kicked in. First, it's totally unnecessary because we have control over what gets rendered from the server. Second, any lag is going to be (painfully) felt by the user.

Don't: serve the page, load Backbone, call back to server for page data
Do: serve the page with embedded data, load Backbone, consume the embedded data into Backbone

We used this pattern extensively for profile editing. In a Rails ERB template:

 <section
 class="profile-section--presentations profile-section"
 data-section="<%= section.to_json %>"
 data-resources="<%= current_user.presentations.to_json %>"
>

And consume with JS in a Backbone view:

 // Load resources for a section
this.collection = new this.resourceCollection(this.$el.data('resources'));

Views all the way down

It's all about the views. Anything that can be a view, should be a view. It turns out to be a great way to attach behavior to a UI element, even very simple things. Here we see an experience section where the underlying views have been labeled.

Base prototypes

A profile can have a lot of sections, but each section behaves similarly. We made an effort to reduce duplication by introducing a base section prototype.

And section views weren't unique for having duplication. We extracted prototypes for edit modals, document modals, and more.

Error handling

We don't validate any data client side. There are certainly trade-offs, but we felt reducing duplication here was worth an increase in server load.

When a save does fail, we use this code in our base modal view:

 saveError: function(model, xhr, options) {
 var errorsArray = $.parseJSON(xhr.responseText).errors;

 this.$errors.html(
 this.errorsTemplate({ errorsArray: errorsArray })
 ).removeClass('visually-hidden');
}

which uses the shared error template:

 <ul>
 <% for (var field in errorsArray) { %>
 <% for(var m = 0, n = errorsArray[field].length; m < n; m++) { %>
 <li><%= field.capitalize() + ' ' + errorsArray[field][m] %></li>
 <% } %>
 <% } %>
</ul>

Organization

Profiles weren't the only feature on Vitae to have some rich client enchancements. As the lines of JavaScript started adding up beyond profiles, we took a good, hard look at how to organize the code.

Split concerns into applications

For most projects, it won't do to have one large Backbone application. It's much easier to reason about and modify code that has been split into separate concerns. For Vitae, the features were pretty orthogonal: profile editor, photo editor, messaging, dossier, etc. It was easy to put each in its own directory.

 app/assets/javascripts/[feature]

Application layout

Inside each app, we kept a Rails style layout:

  • collections
  • models
  • routers
  • templates
  • views

AMD with Almond

For organizing individual files, we used the AMD pattern with Almond. Almond provides the power of AMD without the full functionality of Require.js. It's a really nice trade-off since Rails asset pipeline is going to package and ship over all the JavaScript for us.

Rails

Rails and Backbone don't see eye to eye on every convention. One particular sticking point: by default Rails will send 204 No Content responses when receiveing a POST or PUT. It's really helpful for Backbone to actually get back the content that was created or updated. We used this handy responder to do just that:

 # We don't want 204 No Content responses from the server when we POST or 
# PUT json to the server. We want the resource data encoded as json.
#
# http://stackoverflow.com/a/10087240
#
module Responders::JsonResponder
 protected

 def api_behavior(error)
 if post?
 display resource, :status => :created
 elsif put?
 display resource, :status => :ok
 else
 super
 end
 end
end

Removing unnecessary flash messages

Rails makes it really easy to let the same controller share HTML and AJAX requests with respond_with. It's a big win for reducing duplication where the behavior would be identical. But what about the flash?

In the case of AJAX requests, we don't want to set a flash message. Doing so can lead to some interesting messages on the next page load. We could perform a check on each individual controller. Gross. We don't want that kind of technical debt. Instead, we just peel off any unnecessay flash messages as part of an after_filter on our application controller.

 class ApplicationController < ActionController::Base

 after_filter :clear_flash, :if => :xhr_request?

 private

 def clear_flash
 flash.discard
 end

 def xhr_request?
 request.xhr?
 end
end

Conclusion

Backbone turned out to be a good fit for Vitae. It could handle all the details we didn't need to be concerned with and then get out of the way. It freed our attention to tackle the real problems. And with a little tinkering, it paired nicely with our Rails backend.

Related Articles