Tuesday, September 3, 2013

Things that suck in AngularJS

Every once in a while, I find that someone is interested in AngularJS, but not sure if they should stick to their jQuery comfort zone or make the plunge and learn the new thing. It's relatively easy to come across people singing praises to the paradigm shifts that Angular enforces, and there are plenty of tutorials out there showing how to get Angular to do cool stuff without much effort. There's also pretty good explanations out there for some of the more mysterious concepts (like $apply() and transclusions and animations).

The reason I'm writing this is not an attempt to rehash any of those. As it turns out, I've been knee deep doing some pretty neat stuff with AngularJS, RESTful web services, web sockets and other cool HTML5 techs at my job, and I think I've got enough experience to list out some very specific things that aren't that great about Angular. Don't get me wrong, it's a great tool for the stuff I'm doing, but I just feel like complaining a little :)

So, what is sucky in Angular anyways?

Documentation

Docs are a constant complaint in the community: despite being touted as a testable framework, the unit testing guide has been grossly incomplete for ages. Not only that, but most of the API documentation lacks code examples, and makes little to no effort pointing out arbitrary surprises (e.g. ng-selected takes a string instead of a boolean, .bootstrap()'s second parameter is an array, not a string, etc)

The page for directives is another (in)famous part of the docs that, despite relating to one of the most important aspects of the framework, is often derided as being overly dry, and spends more time talking about internals than how to actually create directives.

DOM integration and directives

Many common DOM actions are supported poorly, e.g setting focus on a specific element inside of a repeater, or setting the dimensions/position of an element based on the dimensions/position of another.

Directives - the blessed way of integrating to the DOM - have a lot of optional arguments that don't work well together, and others that break in subtle, hard-to-reason-about ways. For example, ngModelController.$render breaks once you add scope parameters. Transclusion also silently modifies the scope inheritance semantics if scope parameters are added.

Directives can also get stuck in infinite loops when two or more of them act on the same ng-model binding and there are change even handlers.

The problem is aggravated by compiled directives such as ng-repeat and the unstable-only ng-if, since those re-initialize child directives, and can cause event handlers to fire when things haven't really changed.

Another problem with directives nested in ng-repeats is that there's no easy way to control the lifecycle of a directive. Once a list changes (usually due to an ajax call), the redraw performance of a repeater can get unacceptably slow.

The fact that asynchrony is abstracted away in ng-include and the templateUrl argument in directives can also cause difficult to troubleshoot bugs.

I often find that I need to write large comments to explain some subtle side-effect that arises out of an $apply cycle when there are several things happening concurrently in a page.

Business Logic

In addition to the subtle errors that occasionally arise from asynchronous directives, I often run into problems with strictly business-logic ajax as well: for one thing, there's no equivalent to jQuery's $.when, which makes multi-request dependency management difficult (when you hit multiple web services to derive some conjoined information, for example).

It's even more difficult to reason about business logic when your app is big enough to contain nested controllers. You need to carefully manage when controllers are instantiated, what data is ready when, which variables hold promises and when they are resolved, and what is available where; all tied by the scope inheritance mechanism, which is unsettlingly reminiscent of global state.

Bidirectional bindings are two-edged: while they simplify some types of tasks, they make others surprising, e.g. some directives impose types on the variable bound to them, so you sometimes find yourself forced to cast booleans to strings for no seemingly good reason.

Also, I often find myself wrestling against bi-directionality conflicting w/ event-driven programming style, i.e. when calling http methods in response to user actions, as opposed to data changes. This closely relates to the infinite loop problem I mention above and it's a bit of a case of a "pure" system (in the side-effect sense) forgetting to account for the real-life need for non-idempotent actions.

Filter Caching

Repeater filters are not straightforward to cache: if I want to display the length of a filtered list, I have to either re-compute the filtered list (very expensive), or introduce assignment logic into the HTML view (untestable and ugly), or refactor the filter into the controller as part of the data initialization (not desirable for data required to compute derivative metadata).

3rd-Party integration

Calling Angular services, filters and controllers from outside of Angular is verbose and non-intuitive, making it a difficult sell for progressive adoption into a legacy codebase (read: most non-trivial codebases). More importantly, under those circumstances, it sometimes fails in inexplicable undebuggable ways (I'm looking at you, $http, and yes, I called $el.scope().$apply()).

Internal Complexity

I consider myself to be a reasonably good js developer, but the internals of AngularJS are overwhelmingly non-approachable. Even seemingly simple request changes like the ability to abort http requests require massive amounts of design discussion and implementation attempts because of all the levels of abstraction that need to be reconciled. My general feeling is that the API designers don't care much about orthogonality: several features are missing counterpart/complementary mechanisms (e.g. there's a handy `$timeout` but not a `$clearTimeout`, form.setPristine() was added in unstable, but it's not readily available in the controller and it doesn't have per-field equivalents, etc)

A lot of the complexity leaks from the abstraction prominently into developers' day-to-day: most Angular developers have seen the non-helpful "$apply() already in progress" error. Also, in my experience, bugs in AngularJS apps require a relatively intimate knowledge of several layers of abstraction simultaneously, and are much harder to debug than traditional jQuery ones.

Another semi-related note: The unstable branch is rightly named so: it introduces breaking API changes at every release, and new features often have integration problems when combined with other features. It does contain some incredibly useful features, but I'm not even sure how they plan on officially releasing this unstable stuff, since the effort required for developers to migrate from the current stable to the current unstable grows just that much more with every version increment.

Testability

There are no built-in mechanisms for testing directives.

Also, angular-mocks is incompatible with the ng-app directive (and .bootstrap(), for that matter), making in-page testing (using jasmine ConsoleReporter) impossible.

Integration testing can only be done when using ng-app, making multi-application pages untestable. This is a testability showstopper when your project involves a lot of legacy code that is ported gradually (like most legacy-laden projects are).

Parting Thoughts

This is a list of things that could be improved in AngularJS (1.2) from the point of view of someone whose full time job is pretty much to push techonology to the limit. It's not an attempt to promote Ember (which I haven't had the chance to look at) or Backbone/Knockout/jQuery/whatever, all of which have their own merits -- ok, Backbone sucks, but that's for another post :)

18 comments:

  1. As far as testing directives... that's a pretty broad topic. There are many, many different implementations and types of directives. Overly-complicated directives either shouldn't be directives, or they should have a controller, at which point you can easily test the controller. As for testing the bindings to the dom, you can either test your linking function directly to make sure certain things are set up, or you can use Protractor to run some e2e tests. I've found having Protractor for e2e tests against a mock data source and unit tests for all controllers, service, filters, directive controllers, etc, Have been more than adequate. Coverage is what to make of it. In the end, testing DOM interactions is never going to be easy no matter what library you're using.

    Also, I've promoted a policy on my team of not creating directives unless it's absolutely necessary, for a variety of reasons that is probably a blog post by itself.

    ReplyDelete
  2. One other thing to note:

    "for one thing, there's no equivalent to jQuery's $.when,"

    There is, actually, have a look at $q.all:

    http://docs.angularjs.org/api/ng.$q#methods_all

    ReplyDelete
  3. I recommend canjs . It's the newer and lighter "rewrite" of the JavascriptMVC which has been around since 2007.

    ReplyDelete
  4. Isn't $q.all() the same as $.when()

    ReplyDelete
  5. I believe Angular's $q.all() does what you're looking for with jQuery's $.when. At least I used it to accomplish what you were trying to do - execute several backend calls to assemble data into a single record.

    ReplyDelete
  6. Hey Leo, yes some very good stuff here! And we always love to get feature requests via blog posts. ;)

    A couple things:
    - Check the docs again. We've been hard at work! More improvements still coming.
    - We've got a much cleaner Directives API in the works. You can see it already in our Angular on Dart implementation. On the books for the Angular 2.0 timeframe on JavaScript.
    - There's another plan for AngularJS 2.0 to fix the issue around integration with non-Angular event model systems. Check Misko's slides from htmldevconf in the bit on Zones.

    Looks more or less valid and could be solved by anyone who'd like to submit PRs for them. I'd love to see you do some of them!

    ReplyDelete
  7. Speaking of..

    I've used Ember, and I've used Angular; and while admittedly the last time I used Ember was a 1.0 release candidate rather than the proper release, I'm afraid most of year concerns are just as true for that framework.

    If not more so; there's more magic.

    That being said, what's your favorite framework?

    ReplyDelete
  8. I wrote up a few helper tools for testing directives, specifically ngTest and ngExample check them out at https://github.com/andrewluetgers?tab=repositories

    ReplyDelete
  9. "there's no equivalent to jQuery's $.when"

    Isn't that what promises are for?

    ReplyDelete
  10. I'd agree with most of that except (unless I'm mistaken), there is an equivalent for resolving multiple promises in $q.all() (see: http://code.angularjs.org/1.0.8/docs/api/ng.$q).

    There is a function called $q.when() as well, but that is for resolving something that may or may not be a promise.

    ReplyDelete
  11. Can't wait to read why you think Backbone sucks?

    ReplyDelete
  12. Hey Leo!

    Nice post! With regards to the testability of directives, there actually is a way of doing this, Vojta put this https://github.com/vojtajina/ng-directive-testing

    ReplyDelete
    Replies
    1. (realizing fully well I'm late to this party), many of the core angular unit tests are actually testing core directives, so there is a very good example to follow right in the tree. Angular-ui/bootstrap also provides some fantastic examples for this.

      Delete
  13. You, Sir. Are Awesome. Thank you for this post. I myself been fighting uphills against ng fan boys for a while now. Happy to see I am not alone here.

    ReplyDelete
  14. You know this is probably the best "in the trenches" type of post I've read concerning AngularJS implementation. As a front end developer trying to get my mind around more sophisticated computer science concepts ("orthogonality" - great term!) it is extremely useful to know where the technology sucks, as a kind of road map for my own exploration.

    Anyway, great post, thanks for writing it! If you're interested I recently posted a write up concerning JS MVC framework basics (URL below) - which naturally THIS post would have been a great source for, but oh well!

    http://compassinhand.com/2013/10/26/understanding-js-mvc-frameworks/

    ReplyDelete
  15. "...there's no equivalent to jQuery's $.when...."

    What about $q? http://docs.angularjs.org/api/ng.$q

    ReplyDelete
  16. I agree with the documentation comment. It's possible that the documentation seems so terse because how to do everything just becomes so obvious once you know Angular. At least that's what I'm hoping -- and I think it's very likely to be true. But it does make things a bit tricky right at the start. (Although I am having fun.)

    ReplyDelete
  17. "here's a handy `$timeout` but not a `$clearTimeout`"

    Isn't there $timeout.cancel()?

    ReplyDelete