Learning AngularJS Magic: Expressions

This post is the first in what we hope to be an ongoing series written by our Engineering team about what we're learning at Pathgather. If you're not a web developer who is familiar with AngularJS, you'll probably find this post a bit... technical. Hopefully you'll learn something new!

AngularJS is a pretty magical thing. It brings a lot of helpful magic to the table: declarative templates, automatic data-binding, etc. But sometimes, the magic can surprise you... Here's my cautionary tale.

Pulling Back The Curtain

A couple of weeks ago, I was experimenting with some layout logic using familiar directives like ng-repeat and ng-click, and hit an unexpected snag. While my particular example was nested deep inside our application, here's a very simple application that reproduces the gist of what I ran into.

Can you guess what the issue is?

  1. Take a look at the code below
  2. Before you check out the preview, try to figure out what the application is doing
  3. Click the 'Preview' button to see the application running
  4. The 'Add Points' button doesn't work... why not?

The problem here is that our button doesn't add points like it's supposed to, but why? Every time you click it, the points property should be incremented by one. The ng-click attribute couldn't be any simpler: ++points, which is valid Javascript, so what gives?

The explanation is simple, but the implications aren't immediately clear: ++points is valid Javascript, but it's not a valid Angular expression. Let's learn some more about the magic.

Angular Expressions vs. Javascript

First things first: what's an Angular expression? If you are writing Angular applications, you're writing a ton of Angular expressions. You might not realize it at first, but a bunch of the Angular magic is implemented by taking strings from your templates and interpreting them as Angular expressions. Here are a couple of examples:

ng-click="foo()"      ---->    "foo()"
ng-show="foo != bar"  ---->    "foo != bar"
Hello, {{ name }}!    ---->    "name"

New Angular developers will see cases like these and assume that these expressions are Javascript, but they are wrong! These may look a lot like Javascript and act a lot like Javascript, but Angular expressions are definitely not Javascript. In fact, Angular expressions are their own language, which is why our toy example above doesn't work as you might expect. In most cases, they are pretty similar though: you use '.' to access properties, '[]' to index arrays, '=' for assignment, etc. In fact, as long as you are behaving well, you'll likely never notice the distinction, but here are a few interesting differences:

  1. no support for any loop constructs (while, for, etc.)
  2. no support for any conditionals, except the ternary operator: ?
  3. support only a subset of Javascript operators (*, /, %, etc.)
  4. added support for appending a filter chain via pipe operator: |
  5. added support for declaring a one-time binding via :: prefix

For more details on Angular expression syntax and some of the design philosophy, you can check out the Angular Expressions page from the Developer Guide.

Angular's $parse Service

OK, back to our example. Since ++points isn't a valid Angular expression, what happens? The helpful error message in the console points us in the right direction:

Error: [$parse:syntax] Syntax Error: Token '+' not a primary expression at column 2 of the
expression [++points] starting at [+points]

From this message, we see that our expression used some unsupported syntax (in this case, the pre-increment operator ++). The error message is thrown by the very magical core Angular service, $parse. Let's investigate some more.

Since Angular expressions are their own language, we need a way to evaluate them using our specialized grammar. That's where $parse comes in. $parse turns an expression like this:

"data"

...into an executable Javascript function like this:

function (s, l) {
  if(s == null) return undefined;
  s=((l&&l.hasOwnProperty("data"))?l:s).data;
  return s;
}

How does it work? Therein lies the magic of $parse: it implements a language-within-a-language that essentially transpiles to regular Javascript. Whenever you call $parse, Angular does what you'd expect a compiler to do: it tokenizes the expression (via a Lexer that implements the Angular grammar), and then parses those tokens into executable statements (via a Parser). The statements are wrapped up into a new Function object and returned, and afterwards you can evaluate the expression by invoking the Function and passing the scope and locals as arguments. That's how it works! If you thumb through the source code a bit, you'll see the implementation of the Angular expression "language" tucked inside of parse.js:

function $parse(exp, interceptorFn) {
  ...
  var lexer = new Lexer($parseOptions);
  var parser = new Parser(lexer, $filter, $parseOptions);
  parsedExpression = parser.parse(exp);
  ...
}

As you might expect, it's a bit tricky to implement a compiler in regular Javascript code: there's a lot of loops, deeply nested functions, conditionals, and string manipulation going on under the hood. You should definitely skim through the Angular source code some time to see for yourself what's going on, but be forewarned: it's as crazy as it sounds. For example, here's a snippet of how the Lexer identifies some basic tokens like strings, numbers, etc.:

while (this.index < this.text.length) {
  this.ch = this.text.charAt(this.index);
  if (this.is('"\'')) {
    this.readString(this.ch);
  } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) {
    this.readNumber();
  } else if (this.isIdent(this.ch)) {
    this.readIdent();
  } else if (this.is('(){}[].,;:?')) {
    ...
  }
}

Yikes, that's a lot of conditionals... Somehow, I doubt that's how gcc tokenizes source files. Joking aside, I understand the motivation behind doing this: expressions are necessary to support declarative templates, and Angular's implementation is more secure than the alternative (Javascript eval()). However, that doesn't mean it's not a bit scary to know that this is happening every time you write an ng-show directive to hide some social buttons.

The good news is that the performance of this code isn't really much of a concern: calls to $parse are relatively infrequent, and there are some pretty reasonable optimizations in place to make it speedy (e.g. an expression cache), so in general, I wouldn't worry about it. But still, now that you know what's in there, anytime you find yourself using $parse directly just be careful about it. For example:

  1. try to avoid calling it from inside something that runs frequently (like a watcher)
  2. make your expressions simple; when possible, try putting the data you need directly on the scope instead of through references (i.e. "data" instead of "someObject.getHandle(4).getChild().container.data")
  3. etc.

How To Use $parse Directly

Most of the time, $parse is going to be called for you automatically, or it'll be part of a core directive you are using. I think that's for the best, but if you do have a scenario where you have to use it, it's easy to do. Just inject the $parse service like any other and use $parse(expr) to get the parsed function, which can be invoked to get the expression value by passing in a scope object for context:

$scope.data = "foo";
var parsedExprFn = $parse("data");
console.log(parsedExprFn($scope)); // "foo"

Some $parse Demos

Now that you understand the basics of how Angular magically transforms string expressions into executable functions, here are a couple demos to play around with.

1. Using $parse to generate Javascript functions

2. Basic $parse performance comparison

This test generates 10000 string expressions and evaluates the performance of Javascript eval() against Angular's $eval (which calls $parse). Notice how much faster $eval is the second time around thanks to the expression cache which skips the lexing & parsing!

Fixing Our Bug

Finally, let's fix that original demo, so we can start giving ourselves points for all this Angular learning we're doing. Since the only problem was using an unsupported operator, we can fix it by changing:

<a class="button" href="#" ng-click="++points">Add points</a>

to

<a class="button" href="#" ng-click="points = points + 1">Add points</a>

The result:

Keep Learning

Now that we've gone through it, it doesn't seem as magical anymore, does it? That's why open source projects like Angular are so great: you can learn a ton by just reading through the source code, helping out in the IRC channel, and replying to Github issues and Stack Overflow questions. So get out there, learn something new, and help out while you're doing it!

If you love Angular, open source, and learning, you'd probably love working with us: we're work with Angular and contribute to open source projects everyday while we're helping companies learn, and we'd love to hear from you. Check out our jobs page or e-mail us for more info!

Mobile Analytics