codelord.net

Code, Angular, iOS and more by Aviv Ben-Yosef

Understanding Angular's Magic: Don't Bind to Primitives

| Comments

If you do Angular, chances are you’ve seen the rule “Don’t bind to primitives” quite a few times. In this post I’ll dig into one such example where using primitives causes problems: a list of <input> elements, each bound to a string.

Our example

Let’s say you’re working on an app with books, and each book has a list of tags. A naive way of letting the user edit them would be:

1
2
3
4
5
<div ng-controller="bookCtrl">
    <div ng-repeat="tag in book.tags">
        <input type="text" ng-model="tag">
    </div>
</div>

(You will probably want to add another input for adding new tags and buttons for removing existing ones, but we’re ignoring that for simplicity’s sake.)

A live demo of the example is available here. Go ahead and edit one of the inputs. It might seem like everything is fine. But it’s not. Taking a closer look would show that the changes you make aren’t being synced back to the book.tags array.

That is because ng-repeat creates a child scope for each tag, and so the scopes in play might look like this:

bookCtrl scope = { tags: [ 'foo', 'bar', 'baz' ] }

ng-repeat child scopes: { tag: 'foo' }, { tag: 'bar' }, { tag: 'baz' }

In these child scopes, ng-repeat does not create a 2-way binding for the tag value. That means that when you change the first input, ng-model simply changes the first child scope to be { tag: 'something' } and none of that is reflected up to the book object.

You can see here where primitives bite you. Had we used objects for each tag instead of a plain string, everything would have worked since the tag in the child scopes would be the same instance as in book.tags and so changing a value inside it (e.g. tag.name) would just work, even without 2-way binding.

But, let’s say we don’t want to have objects here. What can you do?

A failed attempt

“I know!” you might be thinking, “I’ll make ng-repeat wire directly to the parent’s tags list!” Let’s try that:

1
2
3
4
5
<div ng-controller="bookCtrl">
    <div ng-repeat="tag in book.tags">
        <input type="text" ng-model="book.tags[$index]">
    </div>
</div>

This way, by binding the ng-model directly to the right element in the tags list and not using some child-scope reference, it will work. Well, kinda. It will change the values inside the list as you type. But now something else is going wrong. Here, have a look yourself. Do it, I’ll wait.

As you can see, whenever you type a character, the input loses focus. WTF?

The blame for this is on ng-repeat. To be performant, ng-repeat keeps track of all the values in the list and re-renders the specific elements that change.

But primitive values (e.g. numbers, strings) are immutable in JavaScript. So whenever a change is made to them it means we are actually throwing away the previous instance and using a different one. And so any change in value to a primitive tells ng-repeat it has to be re-rendered. In our case that means removing the old <input> and adding a new one, losing focus along the way.

The solution

What we need to do is find a way for ng-repeat to to identify the elements in the list without depending on their primitive value. A good choice would be their index in the list. But how do we tell ng-repeat how to keep track of items?

Lucky for us Angular 1.2 introduced the track by clause:

1
2
3
4
5
<div ng-controller="bookCtrl">
    <div ng-repeat="tag in book.tags track by $index">
        <input type="text" ng-model="book.tags[$index]">
    </div>
</div>

This does the trick since ng-repeat now uses the index of the primitive in the list instead of the actual primitive, which means it no longer re-renders whenever you change the value of a tag, since its index remains the same. See for yourself here.

track by is actually way more useful for improving performance in real apps, but this workaround is nice to know as well. And I find that it helps in understanding the magic in Angular a bit better.

“Maintaining AngularJS feels like Cobol 🤷…”

You want to do AngularJS the right way.
Yet every blog post you see makes it look like your codebase is obsolete. Components? Lifecycle hooks? Controllers are dead?

It would be great to work on a modern codebase again, but who has weeks for a rewrite?
Well, you can get your app back in shape, without pushing back all your deadlines! Imagine, upgrading smoothly along your regular tasks, no longer deep in legacy.

Subscribe and get my free email course with steps for upgrading your AngularJS app to the latest 1.6 safely and without a rewrite.

Get the modernization email course!

Comments