While learning AngularJS I have often banged my head against the wall
when being faced with values defined on my controller scopes that just
won’t change along with the things they are representing. This post is
about a common error that beginners are prone to make in such a situation,
a small dive into the angular $digest
loop, a short explanation of assignment of values in javascript
and, finally, a way to confidently have values updated in your controllers.
So let’s say you are a mad scientist that is trying to create a living being that will commit acts of cruelty and murder driven by its loneliness and despair. Let’s also say you have set up your whole machinery and what-not, leaving you waiting for a thunderstorm that will provide enough energy through lightning to finally ‘shock’ your creature to life.
In order to be alerted when the monster becomes alive, you build a
monster service
:
1 2 3 4 5 6 7 8 9 |
|
As soon as something calls the Monster
Service with the right weather,
it will become alive.
Since you care about the status of the abomination you created, you try to check for the monsters’s aliveness and display your emotions accordingly in a controller:
1 2 3 4 |
|
The accompanying view:
1 2 3 4 5 6 7 8 9 10 11 |
|
Now since Angular.js has all this two-way binding magic you spend your time waiting with a girl you just met at Starbucks, confident of being informed once the monster becomes alive.
Unfortunately, the result is this: Once the weather changes to a thunderstorm, your monster becomes alive and you don’t notice while chatting about how great the latest Beirut album is and how no one really gets it. Your monster is free to roam and wreak havoc on mankind as we know it.
So why is that?
Angular.js two-way data binding (aka $digest
loop) roughly works as follows:
- When referencing
alive
in the view, angular creates a “watch” for the$scope.alive
property. - Upon certain events (user input, network interaction, …) in the angular domain, all watches will be evaluated and will be compared to their previous values.
- If a value has changed (i.e. the previous value is not equal to the current value), machinery is set in motion to propagate the changes in the value to the watching parties – e.g. the view in this case, causing an update of the UI.
Now, that may be fine, but: Given that the monster has become alive, why
won’t the change be propagated up to the MadScientistCtrl
?
It turns out that the operation
1
|
|
does not make $scope.alive
reference the Monster.alive
property, but
merely ends up copying it.
Thus, although the Monster.alive
value changes, the copy in $scope.alive
does not and the angular machinery can not pick up the change.
Primitives, Objects, References and Immutability
I will go into some theoretical detail to debug the simple assignment instruction to $scope.alive
.
The error shown above is a result of misunderstanding how JavaScript
assignment works – we need to understand what primitives, objects and
assignments are to understand Variable Assignment:
What is a Primitive?
A primitive is a value of type string
, number
, boolean
, null
or
undefined
. Primitives are immutable – i.e. its value cannot be
changed by invoking functions on it.
What is an Object?
The Mozilla JavaScript Glossary defines an object as an “unordered collection of properties”, among other things. An object is mutable – you can change the values its properties reference, add new properties or delete some.
What is a Variable?
A variable is a reference to value that is either an object or a primitive. The same variable can reference different values in its lifecycle.
Variable Assignment
Every assignment of a value to a variable makes the variable reference the value.
1 2 |
|
An assignment of variable b
to variable a
makes variable
b
reference the same value that variable a
references.
1 2 3 |
|
Assigning a value y
to a variable that references a value x
does
not change the value x
– it merely makes the variable reference a
different value without losing the original one. A reference to the old
value x
will still refer to the original, unchanged value.
1 2 3 4 5 6 |
|
Back to the monster
During this little excursion the monster has been running rampant and you consider it a lost case. Naturally you have already created the next creature to bring darkness to the world and are, again, in the position of waiting for the weather to produce a thunderstorm.
Again, you spend your time talking to a girl that you met at the vegan farmers market, explaining why you consider the renaissance 80’s sneakers to be an ironic display of non-mainstream fashion sensibility. And again, you want to be informed of the change in your monster’s aliveness. But this time, you change your routine:
1 2 3 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
Check the result here.
Obviously the experiment worked and you have been informed about the
change. But Why? Let’s consider the angular $digest
loop again and see
how this small change in the code made it work:
- When referencing
monster.alive
in the view, angular creates a “watch” for the$scope.monster.alive
property. - Upon clicking the button, angular saves old values of all watches
(i.e.
$scope.monster.alive = false
). - It then executes the functionality induced by clicking the button (e.g. changing the weather and then bringing the creature to life).
- At the end of this process, the old watch values are compared to new watch values – which is where our change comes into play:
When we assigned $scope.monster = Monster;
, we created a reference to
the Monster service (an object!) on the $scope
. Thus when comparing the old and new
values of $scope.monster.alive
, angular looks up the reference and
sees the new value in Monster.alive
. The difference in the value makes
angular re-render the UI, finally showing you as excited as you ought
to be for not only once, but twice creating a creature that is an insult
to all living things.
Same deal, using $watch
Of course other techniques for binding service values to controllers exist if you feel that directly referencing an object in your view is too much coupling, or that it is still necessary to bind to a primitive value somehow,
This can be achieved by adding a “manual” watch, which will update your scopes variables upon change of the watched value:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
The accompanying view:
1 2 3 4 5 6 7 8 9 10 11 |
|
Further Reading
- Mastering Web Application Development with
AngularJS: Chapter 11 is a fantastic summary of the workings of the
$digest
loop and related concepts. - (Update 4/11/13): Make Your Own AngularJS, Part 1: Scopes And
Digest by Tero Parviainen shows how to build your own
$digest
cycle and is a really insightful read. Highly recommended.
Images from “Young Frankenstein” and “Frankenstein” are believed to be in public domain.