📜 ⬆️ ⬇️

Magic AngularJS: never hang the binding on primitives

Magic AngularJS: never hang the binding on primitives


If you use AngularJS , most likely you have repeatedly encountered the rule “Do not hang binding on primitives”. In this post I will discuss in detail an example in which the use of primitives creates problems: creating a list of elements in which each of the elements is tied to a string.

Our example

Let's say you are working on an application with books, and each book has a list of tags. The naive way to allow the user to edit tags will be:
<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 field to add new tags and buttons to remove existing tags, but we will ignore this for the sake of simplicity.)

A demo of our example is available here . Try editing one of the input fields. It would seem that everything is fine. But it is not. If you take a closer look, you will see that the changes you have made are not synchronized with the book.tags array.
')
This happens because ng-repeat creates a child scope for each tag and in reality scopes can 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 two-way binding for the tag value. This means that when the first field changes, the ng-model simply changes the first child scope to {tag: 'something'}, and this is not reflected in the book object.

Now you have seen how primitives can play a bad joke with you. If we used objects for each tag instead of lines, then everything would work, since the tag in the child scopes would be the same instance as in book.tags, and changing its value (for example, tag.name) would just work , even without 2-way binding.

But suppose that we do not want to use objects here. What to do in this case?

Unsuccessful attempt

- I know! - you might think. - I will connect ng-repeat directly with the list of tags of a higher level! Let's try:
 <div ng-controller="bookCtrl"> <div ng-repeat="tag in book.tags"> <input type="text" ng-model="book.tags[$index]"> </div> </div> 

Thus, by associating ng-model directly with the desired elements in the list of tags and not referring to the child-scope, we made our code work. Almost. Now the values ​​inside the list will change when you enter text. But now something else is wrong. You can see for yourself . Do it, I'll wait.

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

This is to blame ng-repeat. For the sake of efficiency, ng-repeat keeps track of all the values ​​in the list and redraws specific elements that have been modified.

But primitives (numbers and strings, for example) are immutable in JavaScript. Therefore, if they need to be changed, the previous instance is thrown out and a new one is used. Thus, any change to the primitive causes ng-repeat to redraw it. In our case, this means that when we get rid of the old and add a new one, we lose focus on the way.

Decision

We need to find a way to make ng-repeat identify the elements in the list without depending on their primitive value. A good option would be to use the primitive index in the list. But how to teach ng-repeat track items in the list?

Fortunately, the track by operator appeared in Angular 1.2:
 <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 solves our problem, since ng-repeat now uses the index of the primitives in the list together of the primitives themselves. This means that ng-repeat no longer redraws the value of a tag each time you change its value, since its index remains the same. You can see for yourself .

In fact, track by is much more useful for improving application performance , but it’s also useful to know about the workaround described above. And, in my opinion, it will help to understand the magic of Angular a little better.

Note from the translator: on May 29, a mitap dedicated to AngularJS will be held in Moscow, where this topic will be discussed as well.

Source: https://habr.com/ru/post/223529/


All Articles