arches.io Javascript Organization

23 Mar 2012

My friend recently asked me "Do you have any advice for encapsulating javascript or how to break an application into files/namespaces? Most of what I see online for javascript is hacky and not about designing for maintainability." Turns out I do!

First of all, you need jasmine and jasmine-jquery. I always thought TDD was a nice but impractical idea until I had an environment with fast tests. At Contour I had 130 jasmine tests (averaging maybe 3 assertions each) running in 500ms. Start TDDing with jasmine and you'll be hooked forever. Not that TDD isn't without its own bottlenecks and boondoggles, but over the last six months I've gotten to the point where it makes me really uncomfortable to write any code without tests. Now I sometimes run my JS specs half a dozen times in a row just because I can and it's fun =)

I've uploaded a couple examples of TDD'd javascript here and here.

My process for writing JS goes more or less like this:

  1. Write your web page
  2. Find the html you're planning to manipulate
  3. Copy it into your jasmine fixture and generalize it a bit
  4. Write a test, see it fail. I always keep jasmine open in my first tab so I can hit cmd-1 cmd-r to switch to it and run my tests.
  5. Write code to make it pass
  6. Repeat 4-5
  7. Integrate that JS back into the page

Jasmine itself is going to go a long way toward keeping your code organized, but a few more things will be useful:

Always namespace your code. The best way to make sure your namespace is available is this (copied from Crockford):

var Contour; if (!Contour) Contour = {};

For objects you plan to instantiate, write constructors like so:

Contour.AjaxPagination = function(paginationContainer, storiesContainer){};

Attach methods to the object's prototype instead of the object itself to save on memory:

Contour.AjaxPagination.prototype.currentPage = function(){};

Use closures to maintain scope inside event callbacks:

var me = this;
$(window).bind('popstate', function() {
    me.onPopstate();
});

Namespacing your code also encourages you to create namespaces for groups of functions that are logically related. Contour.URI for manipulating urls. Contour.FormField for showing suggestions in a text box. Etc. So, do that.

Now that all your js has names, you'll probably be dividing it up into individual files. Put those in a directory structure that makes sense. At Contour we have a three-teir organization, with folders for base, widgets, and scopes. We don't really end up using widgets that much, but the idea is that base has everything that's fundamental and site-wide. Email validation, etc. Widgets are for more specific display objects that we reuse. Scopes are for one-off stuff that's only on one or two pages. Eventually stuff gets pulled from scopes into widgets, and from widgets into base, as we use it more and more and refactor it to be more generally applicable.

Now that the vast majority of my JS is tested, I don't have to worry too much about the integration. I have a bunch of document.ready blocks where I instantiate my objects, but since I'm usually I'm just make them and passing a couple DOM elements I don't test those integrations (programatically. I test them manually in the browser).

One really important thing: try not to use jquery selectors inside your javascript, because they're fragile. You want to divorce your javascript from the dom as much as possible. If you change a class name, you don't want to have to edit your js as well. So, whatever DOM is going to be manipulated should be passed as arguments during initialization. Or worst case, pass in jquery selectors during initialization. Eg, if this is how I instantiate my javascript in my index.html.erb:

<% content_for :javascripts do %>
    $(document).ready(function(){
        new Contour.AjaxPagination($(".pagination_container"), $(".stories_container"), $(".filters_container"));
    });
<% end %>

...now when I need to rename the pagination container, the only place I'm referencing it is index.html.erb.

When I started moving contour's JS into jasmine, I had to rewrite pretty much all of it. It was sort of a hassle, but you really do write different code when you TDD, and it was a) actually kind of fun to TDD the code, so it didn't feel like a big deal to rewrite it all, and b) nice to have a big set of medium-sized projects ready to go, so I could learn jasmine all at once rather than just using it every few weeks when I had a new feature to write.