An Unconventional Review of AngularJS

AngularJS is everything I expect from a framework. That’s not a good thing.

In November, December, and January, I reviewed AngularJS for Let’s Code JavaScript’s “front-end frameworks” series. All together, I spent forty hours researching, coding, and problem-solving. As usual, my goal was to explore and critique AngularJS by creating a real application.

Angular is probably the most popular front-end framework currently available. It’s produced by a team at Google, which gives it instant credibility, and it’s in high demand by employers. It’s so popular, it has its own acronym. It’s part of the “MEAN” stack: MongoDB, Express, AngularJS, Node.JS. A who’s-who of cutting-edge technology.

Angular describes itself as a toolkit for enhancing HTML. It lets you extend HTML with new vocabulary—in the form of “directives”—that turn a static HTML document into a dynamic template. Directives can appear as attributes or tags (or even comments or classes, but that’s unusual) and they turn a static HTML page into something that lives and breathes, seemingly without added JavaScript.

The best example of this is Angular’s famous two-way binding. Your HTML template can include variables, as with most templating languages, but in Angular’s case, your page automatically updates whenever the variables change.

For example, the application I produced for the review has a spreadsheet-like table that changes whenever certain configuration fields change. Here’s the code that renders a row of that table. Notice that there’s no event handling or change monitoring… just a template that describes the cells in the row. Angular automatically ensures that the cells update whenever their values change.

// Copyright (c) 2014-2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file.
(function() {
  "use strict";

  var StockMarketCell = require("./stock_market_cell.js");

  var stockMarketRow = module.exports = angular.module("stockMarketRow", [StockMarketCell.name]);

  stockMarketRow.directive("stockMarketRow", function() {
    return {
      restrict: "A",
      transclude: false,
      scope: {
        value: "="
      },
      template:
        '<tr>' +
          '<td stock-market-cell value="value.year()"></td>' +
          '<td stock-market-cell value="value.startingBalance()"></td>' +
          '<td stock-market-cell value="value.startingCostBasis()"></td>' +
          '<td stock-market-cell value="value.totalSellOrders().flipSign()"></td>' +
          '<td stock-market-cell value="value.capitalGainsTaxIncurred().flipSign()"></td>' +
          '<td stock-market-cell value="value.growth()"></td>' +
          '<td stock-market-cell value="value.endingBalance()"></td>' +
        '</tr>',
      replace: true
    };
  });
})();

Magic.

With examples like this, it’s easy to see why Angular is popular. It makes hard problems seem trivial. But will it stand the test of time?

An Unconventional Review

Too many frameworks fall into an all-too-common trap: they make it easy to get started quickly, which is great, and then make it very hard to maintain and extend your code over time. That part’s not so great.

So when I review a framework, I don’t look at the common criteria of performance, popularity, or size. (It’s good to know these things, but you can easily find that information elsewhere.) No, I want to know the answer to a simpler and more vital question:

Over the 5-10+ years I’ll be supporting my product, will this code cause me more trouble than it’s worth?

Most frameworks are designed to save you time when you initially create a product. But that time is trivial in comparison to the cost of maintaining your application for years. Before I can recommend a framework, I need to know that it will stand the test of time. Will it grow and change along with me? Or will I be shackled by a barely-maintainable legacy application in three years?

I look at five common pitfalls.

  1. Lock-In. When I decide to upgrade to a new version, or switch to a different framework, how hard will it be?

  2. Opinionated Architecture. Can I do things in the way that best fits the needs of my app, or do I have to conform to the framework’s pre-canned approach?

  3. Accidental Complexity. Do I spend my time working on my application, or do I waste it on figuring out how to make the framework do what I need?

  4. Testability. Can I test my code using small, fast unit tests, using standard off-the-shelf tools, without excessive mocking?

  5. Server-Side Rendering. Will users have to wait for JavaScript to execute before they see anything useful? Will I have to jump through ridiculous hoops to get search engines to index my site?

I rated Angular in each category with a ☺ (yay!), ☹ (boo!), or ⚇ (it’s a toss-up).

1. Lock-In: ☹ (Boo!)

There’s no question: Angular locks you in. You define your UI with Angular-specific directives, in Angular-specific HTML templates, using Angular-specific jargon and code. There’s no way to abstract it. It will all have to be rewritten when you switch to a different tool.

This isn’t unusual. It’s so usual, in fact, that this level of lock-in normally warrants a “meh” toss-up face. Angular works hard for its frown.

First, Angular wants to own all your client-side code. Writing your app the Angular way means writing validation logic using Angular-specific validators, putting business logic in Angular-specific services, and connecting to the back-end via Angular’s built-in services.

Second, the Angular team has shown that maintenance costs aren’t a priority for them. Angular 1.3 dropped support for IE 8. Angular 2 is a major rewrite of the framework that eliminates several core concepts in the current version. It’s likely to require a rewrite of your app.

This bears repeating: Your entire front-end is locked in, and even staying current will likely require a rewrite. Rewrites are a terrible idea; you’ll spend buckets of money and time just reproducing what you already have. A framework that has a rewrite built into its roadmap is unacceptable, and that’s what AngularJS appears to have.

2. Opinionated Architecture: ⚇ (It’s a toss-up.)

Angular wants you to build your application in a particular way, but it’s not very explicit about it. Call it “passive-aggressive architecture.”

Opinionated architecture is one of those “short-term good, long-term bad” deals. In the short term, an opinionated framework can help you get started quickly by showing you how to structure your application. In the long-term, though, an overly-opinionated framework will limit your options. As your needs grow, the opinions of the framework become a straight-jacket requiring increasingly complex contortions to overcome.

Angular’s passive-aggressive architecture provides the worst of both worlds. It makes assumptions about your application design, but it doesn’t guide you towards those assumptions. I’m not sure I fully understand it even now, but this is what I’ve gleaned so far:

Fundamentally, Angular assumes you use stateless “service” objects for logic and dumb data-structure objects (objects without methods) for state. Services are effectively global variables; most functions can use any service by referencing its name in a particular way. Data structure objects are stored in the “$scope” associated with templates and directives. The data structure objects are manipulated by “controllers” (glue code associated with templates and directives) and services.

I’m not a big fan of this architecture. By separating state and business logic, Angular breaks encapsulation and splits apart tightly coupled concepts. Rather than putting logic alongside the data it operates on, Angular wants you to spread the logic around your application. It risks the “shotgun surgery” code smell: any change requires making lots of little edits.

Angular’s tutorial application demonstrates the problem. The application displays a list of smart phones, and “phone” objects are a core concept. Ideally, a change to the internal structure of the phone objects wouldn’t affect anything else. But they’re just dumb data objects, so a change would require edits throughout the application: the phone-list template, the phone-detail template, and both controllers for those templates.

I prefer rich domain objects that encapsulate state and business logic. That allows me to make changes without breaking things. For my sample app, I used a rich domain layer that relied on immutable value objects. Angular’s passive-aggressive architecture didn’t support that approach—there were times that I had to contort my code to work around Angular’s assumptions—but it wasn’t impossible, either. It could have been worse, and that’s the best I can say about it.

3. Accidental Complexity: ☹ (Boo!)

Angular is known for having a steep learning curve and poor documentation. I think these are symptoms of a bigger problem. It’s not the documentation that’s at fault; it’s Angular. It’s just poorly designed. Here are a few of the flaws I discovered:

  • Leaky abstractions. To use Angular for a non-trivial project, you have to understand, at a deep level, how it works under the covers. You’ll need to understand scopes and how they relate to prototypal inheritance; the digest loop; $watch, $watchCollection, and $apply; and much more.

  • Magic strings as a workaround for poor cohesion. You’ll often have code that’s closely related but spread among different files. They’re connected by using the same string in both places.

  • Obscure sigils everywhere. Angular has multiple tiny languages that you’ll embed into various strings in your application. Be prepared to understand the difference between "=", "&", "=*", and "@"; "E", "A", and "EA"; the "|" operator; and more.

  • Subtle incompatible differences. Problems can be solved in multiple ways, each with small but vital incompatibilities. For example, the way you define a controller will determine the syntax you use in your template and how variables are stored on Angular’s $scope.

  • Bias toward silent failure. It’s easy to do something wrong, have your app not work, and get no indication of why. Did you use "E" where you meant to use "A"? Your application just stopped working.

When I built the sample application for the first time in my React review, it took me 28¾ hours. Doing the same thing with Angular took me 39½ hours, despite having done it once before and being able to reuse some of the React code. That’s more than ten extra hours. The extra time can be laid firmly at the feet of Angular’s excessive complexity.

4. Testability: ⚇ (It’s a toss-up.)

Angular makes a big deal about testing. One of its major features, dependency injection, is specifically intended to make testing easier.

Given this focus, I was surprised how poor Angular’s testing story is. It emphasizes testing logic in controllers and services, but it has poor to non-existant support for testing UI behavior. There’s no support for simulating browser events and it’s flat-out impossible to unit test HTML templates. Custom directives can be tested, but it’s ugly to test a directive that contains another.

Angular focuses on allowing you to unit test business logic. But it only needs to do that because its architecture encourages putting business logic in the UI (specifically, in controllers and services). A better architecture would put business logic in objects that are independent of the UI, rendering the whole thing moot.

A lot of Angular feels like this. Band-aids over self-inflicted wounds.

Once you take out the business logic, as my sample app did, you’re left with testing how Angular renders HTML in reaction to events, and Angular didn’t support me in that. The Angular team recommends using their purpose-built end-to-end testing framework, Protractor, instead.

End-to-end tests are slow and brittle. They should be a kept to a minimum, not relied upon as the centerpiece of your testing strategy. Fortunately, by putting my application UI in custom directives, it was possible for me to unit test Angular, if not pretty, so Angular barely slides by with a “meh” face. If you look close, you can see a single tear sliding down.

5. Server-Side Rendering: ☹ (Boo!)

AngularJS is not meant to run on the server. This isn’t a surprise, nor is it unusual, but it’s something to be aware of.

Summary: Avoid.

Working with Angular was a real slog. Every step exposed a new quirk or challenge to figure out, and by the end of my review, I was well and truly sick of it. If I had done things the Angular way, rather than sticking with my own design, I might have found it easier going, but my purpose was to understand Angular’s long-term maintainability prospects, not get done as quickly as possible.

And those prospects are poor. Angular is a complex framework that’s grown awkwardly. It’s popular, but not good, and I suspect it will quickly fade as better options rise in prominence. With Angular 2 on the horizon, embracing Angular today means you’re likely to need to rewrite in a couple of years. Although the next version may fix its flaws, Angular as it exists today is a poor choice. Avoid it.

If you liked this essay, you’ll probably like:

comments powered by Disqus