$watch deep Data Structures in AngularJs

$watch a collection can be quite a challenge, this can be done in several ways ($watch, $watchCollection, $watchGroup).
our challenge was to find a solution for watching a tree structure that contains nodes with cyclic references.

The Challenge

Let’s say each tree node has parent and children references attached to it (to make traversing the tree as simple as possible). Each node will also contain a value property that links to the actual data. Thus, each node might look similar to this:

$scope.node = {
  value: { /* data */ },
  parent: { /* another node */ },
  children: [ /* list of more nodes */ ]
};

How would you watch this data structure from your controller, so that changes to the node value will call a function?

Possible solution?

the first thought was, just watch it:

$scope.$watch('node', function () {
  // do something
});

this code doesn’t yield any error and it looks simple
Unfortunately the watcher will not get triggered when a nested property from within the value object is changed.
This is because $watch will only shallow check the referenced value by default.

Another possible solution?

What if we just add a true as the third parameter in the $watch call to deep-watch the value…

$scope.$watch('node', function () {
  // do something
}, true);

That would work (in theory)  it throw an exception:
RangeError: Maximum call stack size exceeded
When deep-watching an object, angular will follow all references to other objects. Because of the parent and children attributes, this results in an infinite loop. The result an exception.

The solution!

This is the way to only watch the value property of each node

$scope.$watch(watchNode, function () {
  // do something
}, true);

function watchNode() {
  return $scope.node.value;
}

According to the documentation, instead of passing an expression, you can also pass a function to $watch. This function is called in each $digest cycle and it’s return value will be compared to the previous one.
Done! This is how to deep-watch a circular data structure in angular.

Now, what about $watch multiple properties on each node?

In the given example, there’s only one property per node that needs to be watched. Here’s how you’d deal with multiple properties:

$scope.complexNode = {
  even: { /* data */ },
  more: { /* data */ },
  properties: { /* data */ },
  parent: { /* another node */ },
  children: [ /* list of more nodes */ ]
};

$scope.$watch(watchComplexNode, function () {
  // do something
}, true);

function watchComplexNode() {
  return without($scope.complexNode, ['parent', 'children']);
}

function without(obj, keys) {
  return Object.keys(obj).filter(function (key) {
    return keys.indexOf(key) === -1;
  }).reduce(function (result, key) {
    result[key] = obj[key];
    return result;
  }, {});
}

Now we want to $watch a list of nodes:

we going to use a map function:

$scope.$watch(watchNodeList, function () {
  // do something
}, true);

function watchNodeList() {
  return $scope.nodeList.map(nodeValue);
}

function nodeValue (node) {
  return node.value;
}

Performance notes:

These $watch functions will be executed at least once per $digest cycle, so be sure to not do any heavy computation.

 

Related Posts: