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 |
|
(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 |
|
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 |
|
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.