stsc3000's blog

A Tale of Frankenstein and Binding to Service Values in Angular.js

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:

Monster Service
1
2
3
4
5
6
7
8
9
  angular.module("app.services").service("Monster", [function(){

    this.alive = false;

    this.weatherChange = function(weather) {
      this.alive = (weather.status == "thunderstorm")
    };

  }]);

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:

MadScientist Controller
1
2
3
4
  angular.module("app.services")
    .controller("MadScientistCtrl", ["$scope", "Monster", function($scope, Monster) {
      $scope.alive = Monster.alive;
    }]);

The accompanying view:

MadScientist View
1
2
3
4
5
6
7
8
9
10
11
  <div ng-controller="MadScientistCtrl" class="column">

    <div ng-show="alive">
      <img ng-src="img/frankenstein_excited.jpg">
    </div>

    <div ng-hide="alive">
      <img ng-src="img/frankenstein_calm.jpg">
    </div>

  </div>

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
$scope.alive = Monster.alive;

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
  var a = "string-primitive";
  a == "string-primitive"; // true

An assignment of variable b to variable a makes variable b reference the same value that variable a references.

1
2
3
  var a = "string-primitive";
  var b = a;
  b == a; // true

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
  var a = "x";
  var b = a;
  b == "x"; // true
  b = "y";
  b == "y"; // true
  a == "x"; // true

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:

MadScientist Controller
1
2
3
  angular.module("app.services").controller("MadScientistCtrl", ["$scope", "Monster", function($scope, Monster) {
    $scope.monster = Monster;
  }]);
MadScientist View
1
2
3
4
5
6
7
8
9
10
11
  <div ng-controller="MadScientistCtrl" class="column">

    <div ng-show="monster.alive">
      <img ng-src="img/frankenstein_excited.jpg">
    </div>

    <div ng-hide="monster.alive">
      <img ng-src="img/frankenstein_calm.jpg">
    </div>

  </div>

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:

MadScientist Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  angular.module("app.services")
    .controller("MadScientistCtrl", ["$scope", "Monster", function($scope, Monster) {

      $scope.alive = Monster.alive;

      $scope.$watch(
        function(){ return Monster.alive },

        function(newVal) {
          $scope.alive = newVal;
        }
      )

    }]);

The accompanying view:

MadScientist View
1
2
3
4
5
6
7
8
9
10
11
  <div ng-controller="MadScientistCtrl" class="column">

    <div ng-show="alive">
      <img ng-src="img/frankenstein_excited.jpg">
    </div>

    <div ng-hide="alive">
      <img ng-src="img/frankenstein_calm.jpg">
    </div>

  </div>

Further Reading

Images from “Young Frankenstein” and “Frankenstein” are believed to be in public domain.