AngularJS scoping best practices
Like every other javascript framework out there, AngularJS was designed with certain tradeoffs in mind. It’s easy to get a simple app up and running, and to create moderately complex applications without a deep understanding of the javascript language or any design patterns. You can bind plain javascript objects to templates, and most of the time, it just works. This initial ease of use (happily) insulates us from the complexity of the framework itself, but after repeated use, can expose baffling bugs for certain use cases. There are a few constructs unique to Angular that can easily lead to frustration. While best understood through experience, I’ll attempt to demystify the most common: Seemingly broken two-way bindings on $scope
attributes.
Play around with the example below. Notice how all the input fields are referencing the same primitive, amount
.
If you change the top field, the rest of the fields update accordingly. But if you change any of the other fields, none of the others update. Furthermore, if you change one of the bottom fields, then change the top field, the previous field doesn’t update anymore! WTF? Two-way binding is broken!? The horror!
Well, not exactly. Let’s dive into what’s going on here. First of all, it’s important to realize that ng-repeat
creates a new scope for every instance. So, when the scope is first initialized, the only attributes that exist are amount
, people
, and three child scopes, one for each instance of the people
array. Each child scope references amount
in its parent’s scope. If you change the value of the top field, amount
is updated, and the three child scopes pick this up and are notified appropriately.
The problem arises when you try and change Jimmy’s allowance. By design, the child scope won’t look through the scope hierarchy for an attribute called amount
. Rather, it creates amount
on itself and sets the value to be what you just entered. Now, you have an amount
attribute on both the parent and child’s scope. These are separate attributes, not in any way tied to one another. The only thing they share is a name. If amount
is changed on the parent scope, Jimmy’s scope’s amount
isn’t updated. If Jimmy’s amount
changes, nothing else is notified since there isn’t any other field referencing it.
So what do we do? You can always reference $parent
on any scope, right? Let’s just bind to that! Well, sure, that would work, but it’s generally poor style to reference $scope
‘s internal traversal properties such as $parent, $children, etc. For instance, if you were using the ng-repeat
section of HTML on several pages, it’d makes sense to pull it out into its own template, then use ng-include
to pull it in. Well, since ng-include
also creates its own scope, the $parent reference wouldn’t work anymore, and you’d have to change it to $parent.$parent.amount
! As your templates get more and more complicated, and you start abstracting code into directives and separate templates, such bindings become increasingly brittle.
So what if the child scope’s input
is bound to an attribute on a separate object? Then, when the user updates the input field, it won’t find that object in the current scope, and will have to use the one in the parent scope, right? Bingo. This, in fact, is the best practice as suggested by the AngularJS community.
Now, try editing the fiddle. Create a data
object on the main scope, and move amount
to that object. Then change the references in the template to data.amount
. Voila! Two-way binding as expected.
Comments are closed.