Using Angular.js at Localytics
This is the story of how we rewrote our Backbone-powered web application in AngularJS, using nearly half the number of lines of code. (It is a love story.)
Our Time With Backbone
In the beginning, there was spaghetti code. Then out of the chaos came Backbone, and it was good. Our data lived in Models which lived in Collections which were observed by Views, and our application grew. But with growth came complexity, and as we began to nest our Views in deeper layers, evil Zombie Views plagued us, and we despaired.
Backbone promised a simplicity that eluded us in our real-world application:
"In a finished Backbone app, you don't have to write the glue code that looks into the DOM to find an element with a specific id, and update the HTML manually — when the model changes, the views simply update themselves." – Backbone.js docs
We found this statement misleading.
In Backbone, View.render
is a no-op, so the above would perhaps be better written: "...when the model changes, you are responsible for binding your view to the model’s change event and writing a render method that can be called each time this event fires, and make sure to unbind it when you’re done."
For us, this was easier said than done.
First, to keep our views manageable, we followed thoughtbot's example and wrote composite views made up of smaller views responsible for discrete parts of the page. Parent views had to keep track of child views and make sure to clean up after them.
The render
method's purpose became less clear with nested views. Should it render itself, then recreate and rerender all child views? Should it reuse existing child views and just tell them all to rerender? We ended up investing many lines of code and development time on composite view library classes to help manage these cases, not to mention the time spent hunting down zombie views that weren’t being cleaned up correctly.
Second, our app was heavy on user input. We never found a good way to keep our form inputs, models, and views in sync. We began by writing glue code that looked into the DOM to find an element with a specific id and grab the data from that element on keypress. That caused the model to change, so sure enough, our observant view rerendered, causing the input to lose focus.
Instead of simply rerendering the entire template, we wrote more glue code that looked into the DOM to find an element with a specific id and change the value of that element to reflect our model. Now we had double the amount of awful DOM manipulation code in our views, so we turned to a little two-way data binding library called Stickit. The plot thickens.
The tale of our journey down the path of Stickit is an epic in itself. In a nutshell, things got worse.
Stickit's philosophy is to “clean up your templates” (make them less declarative) by moving your data-binding logic to your view. So now we had a whole library whose purpose in life was to litter our views with the names of more DOM elements. Our zombie views grew fatter.
We were ready for a framework that offered more out of the box, and any moral objection we might have had to soiling our templates with non-standard HTML had been completely beaten out of us at this point…
The straw that broke the camel's back was Chosen, the jQuery plugin that turns ordinary select elements into pretty searchable dropdowns (an essential part of our UI). It doesn’t work with Stickit and it doesn’t work with Backbone views that haven’t yet been inserted into the DOM. But we persisted, writing tomes of code to coerce these three strangers into playing together. When it came time for a UI revamp of our app marketing platform, we realized we were going to have to rewrite enough of our existing code that we jumped at the chance to try something new.
Into Angular Land
We were ready for a framework that offered more out of the box, and any moral objection we might have had to soiling our templates with non-standard HTML had been completely beaten out of us at this point, so AngularJS's declarative two-way data-binding was starting to sound real good. When we found AngularUI, complete with datepicker and a Chosen lookalike ready to go, we were sold.
It was, to be sure, mind bending at first. Without our familiar friends, Model and Collection, we weren’t even sure how we were supposed to model our data, a question on which AngularJS is conspicuously unopinionated. We also weren’t sure how to organize our code, and the Angular documentation wasn’t very helpful here, as it's riddled with sample code which it then advises you not to follow in a real application. But a few days in, it was evident that we were writing so little code in Angular anyway that it didn’t really matter if our organization wasn’t perfect.
We finished the project in two weeks and in about half the number of lines of code, including HTML markup, as our old Backbone version:
Data-binding worked like magic. We wrote zero lines of DOM manipulation code, and spent zero minutes tracking down memory leaks or unpredictable event binding behavior.
As an unexpected bonus, Angular's module API and dependency injection system also turned out to be way cooler than we imagined. In Backbone we had struggled with a hand-rolled module loader to organize our code, and it took no small amount of discipline to keep passing all the variables needed into our views (which were nested several layers deep) instead of throwing our hands in the air and just using the module loader as a place to stick global variables. Now Angular takes care of all of this for us, managing our services and providing instant access to them wherever we require.
As it turned out, the select2 dropdown widget that comes with AngularUI didn’t quite meet our needs. The author acknowledges that the plugin is slow with large datasets, and we wanted something that would work with ngOptions. So we wrote our own directive for Chosen, open-sourced on Github. The directive works on any <select>
element, and plays well with ngModel
and ngOptions
:
<select
multiple chosen
ng-model="state"
ng-options="s for s in states">
</select>
Lessons Learned
To close, here are a few things we learned from the transition, and a few questions we’re still working on answering:
Directives are hard. Working with isolate scope and transclusion is tough, and Angular's documentation on the subject doesn’t make it easier. We found that before jumping into writing an ambitious directive, it's best to start by not writing a directive — just using normal templates and controllers — and then roll that code into a directive once you really figure out what your requirements are, or once you start repeating yourself.
It's also best to write smaller, less obtrusive directives that play well with others. For example, we went through a few iterations on the Chosen directive before realizing that it was best to keep it simple and define it as an attribute on a standard <select>
element, instead of creating a element that would render its own template. By keeping it lightweight and retaining access to the <select>
element outside of the directive, we can still add other directives on top of it, like ngModel
, ngOptions
, ngChange
, or any of the directives that work with validation.
Angular documentation needs some love. But we were able to figure it out anyway, and it turns out there isn’t as much to learn as we’d initially feared to get started. The developer's guides to directives and forms are now our two best friends.
Form validation is great but requires some thought. Our application helps customers create targeted in-app messaging campaigns through what's essentially multi-page form wizard, so dealing with form and model validation was tricky. Angular's built-in validation directives like ngRequired
helped us eliminate a huge chunk of tedious validation code that had previously lived on our Backbone models, but we still had some model-related business logic that couldn’t be defined declaratively, and which needed to be persisted if the user left our app and came back later without stepping through each page of the form again. Integrating model and form validation was tricky, and our implementation probably needs some rethinking. We were also surprised to find that Angular doesn’t seem to support required
validation on groups of radio buttons, and had to resort to adding a hidden input element with a required attribute to validate that a user had selected an option.
Declarative is good. It's funny, but we’ve seen interactive web apps with HTML and Javascript come full circle. We used to write things like <a href="#" onclick="doSomething()">
. Then we realized it was bad to couple presentation and behavior, so we made our Javascript unobtrusive, keeping our templates clean. But now we’re back at it again, writing <a href="" ng-click="doSomething()">
. Have we learned nothing?
My experience with Backbone has led me to conclude that the ethos of "keeping one's templates clean" is an antipattern that creates brittle connections between Javascript code and DOM structure. Web development is messy business, and the ugly code that brings HTML to life has to live somewhere. By abstracting it out of our templates, it just clogs up our views with hardcoded references to HTML elements whose names, classes, and structure might change—and which should be allowed to change without breaking behavior and needing a corresponding change in a separate view class.
Philosophy aside, Angular helped us get our job done with a lot less code, leaving us more time to do the things we love, like drink beer frolic in the sun write more code. Plus the t-shirts are pretty cool.