Juri Strumpflohner

RSS

Learning Angular: Gosh, my two-way binding doesn't seem to work properly!

Author profile pic
Juri Strumpflohner
Published
Angular data-binding seems magic, but obviously isn't. You have to give it a chance to do its work. As such, watch out and think about the difference "copy by value" and "copy by reference".

The issue

I have a sidebar with the application menu, which is wrapped into its own directive. Furthermore there is a button somewhere else on the application which - when clicked - should toggle the sidebar (hide/show it).

There are different implementation possibilities:

  1. Directly set the state of the sidebar by keeping a reference to it from the button
  2. Have a publisher/subscriber mechanism where the sidebar registers to an event, say “sidebar.toggle” and based on that hides/shows itself
  3. Establish a 2-way data binding with a common object that is known by the sidebar as well as by the button.

Option 1 is good for simple situations (like the menu of my blog here), but I don’t really like it in a more large-scale app. It quickly gets messy as you soon run into the situation where interaction with the menu is needed from multiple parts of the application. Keeping lots of “hard references” quickly complicates the codebase and makes it more fragile. Option 2 solves this by decoupling through events. This works nicely and is a cleaner solution.

There’s a 3rd option, namely to use some shared object which in Angular can be nicely represented by a service. Being a singleton it is an ideal candidate to hook on a 2-way-binding like:

  • sidebar directive binds to isVisible property defined on the service
  • button switches on the isVisible property on the service

That’s exactly what I did. The service is quite simple in that it exposes an object containing the current state.

.factory('service', function() {
    var service = {
      obj: {
        isVisible: true
      }
    };
    return service;
})

A corresponding directive reacts based on that state information:

.directive('container', function() {
    return {
      restrict: 'E',
      template: '<div ng-show="{{ vm.isVisible }}">Hi there!</div>',
      controller: function(service) {
        var vm = this;
        // directly bound
        vm.isVisible = service.obj.isVisible;
      },
      controllerAs: 'vm'
    };
});

Finally, a controller sets the property based on some user interaction (i.e. the click on the button).

.controller('MyCtrl', function(service) {
    var vm = this;
    // directly bound
    vm.isVisible = service.obj.isVisible;

    vm.toggle = function() {
      service.obj.isVisble = !service.obj.isVisble;
    };
})

The result:

As you can see it doesn’t really work as expected. What’s going on here? Even though there’s a lot of “magic” involved in how Angular realizes 2-way data binding, you have to give it a minimum chance keep track of what happens.

If you take a look at my code above, you can see that I directly bind the boolean property of the service to my $scope:

.controller('MyCtrl', function(service) {
  ...
  vm.isVisible = service.obj.isVisible;
  ...
}

Solution

The problem here is that the isVisible property on the $scope obviously won’t get updated as there is no connection to the one defined on the service object. Since booleans are a values types, simply the value is copied over. Hence, they don’t reference the same OBJECT.

Thus, what I have to do instead, is to bind an object instance onto the $scope.

.controller('MyCtrl', function(service, $scope) {
  ...

  vm.data = service.obj;

})

..and then obviously also update the binding in the HTML:

<div class="widget" ng-controller="MyCtrl as vm">
  <h3>MyCtrl</h3>
  ...
  {{ vm.data.isVisible }}
  ...
</div>

In this way, $scope and service point to the same object instance and hence the digest loop can watch the object to update the HTML accordingly:

Apparently this seems to be a common issue with Angular newbies as I encountered other devs on the IRC chat having similar problems. Response from the experts: “MOAR dots..” :)

...
[10:25:37] <denny009> hello all. I've a directive that inside do scope.$new() and it works well (create a json tree). The problem now is that I want communicate with the parent. The first time the communication works well but the second when the scope change I lost it
...
[10:28:21] <Grokling> denny009: MOAR dots..
...
[10:28:44] <denny009> Grokling: sorry?
[10:30:20] <denny009> Grokling: what mean "MOAR dots"
...
[10:31:14] <Grokling> denny009: Javascript passes objects by reverence, and primitives by value. You have a primitive 'jsonData' there, so you're copying the value, and it has no reference to any new value you might replace it with.
...
[10:35:02] <Grokling> denny009: You should always have a dot in your bindings (hence: MOAR dots). sendrequest(thing.request) would be an example.
```

Have a nice weekend!