6

In my custom directive, I'm adding elements to the DOM based on the number of objects in my datasource array. I need to watch a specific property in each object. As I add these elements to the DOM, I want to set up a $watch on the checked property of each object in the toppings array, but it's not working, and I don't know why. I set up a breakpoint inside the function that should be invoked when the property changes from true to false or false to true, but that function is never invoked. Is the reason obvious? I'm just learning Angular, so I could easily be making a stupid error.

$scope.bits = 66;  (i.e. onions and olives)


$scope.toppings = [
    { topping: 1, bits: 2, name: 'onions' },
    { topping: 2, bits: 4, name: 'mushrooms' },
    { topping: 3, bits: 8, name: 'peppers' },
    { topping: 4, bits: 16, name: 'anchovies' },
    { topping: 5, bits: 32, name: 'artichokes' },
    { topping: 6, bits: 64, name: 'olives' },
    { topping: 7, bits: 128, name: 'sausage' },
    { topping: 8, bits: 256, name: 'pepperoni' }
     ]

Each object in the model gets a new checked property which will be true or false.

NOTE: the object array will at most contain a dozen or so items. Performance is not a concern.

link: function link(scope, iElement, iAttrs, controller, transcludeFn) {


  <snip>

 // At this point  scope.model refers to $scope.toppings. Confirmed.

  angular.forEach(scope.model, function (value, key) {

     // bitwise: set checked to true|false based on scope.bits and topping.bits

     scope.model[key].checked = ((value.bits & scope.bits) > 0);  


     scope.$watch(scope.model[key].checked, function () {
         var totlBits = 0;
         for (var i = 0; i < scope.model.length; i++) {
            if (scope.model[i].checked) totlBits += scope.model[i].bits;
         }
          scope.bits = totlBits;
      });

   });

     <snip>

4 Answers 4

4

Array of Objects:

$scope.toppings = [
   { topping: 1, bits: 2, name: 'onions' },
   { topping: 2, bits: 4, name: 'mushrooms' },
   { topping: 3, bits: 8, name: 'peppers', checked:undefined /*may be*/ }
];

Watch using AngularJs $WatchCollection:

Instead of monitoring objects array above, that can change for any property in the object, we will create an array of properties of the elements for which we are watching the collection (.checked).

We filter the array's elements to only monitor those elements which have .checked defined and map that to an array for angular watchCollection.

When a change fires, I will compare the old and new arrays of (.checked) to get exact changed element using lodash difference method.

 $scope.$watchCollection(

                // Watch Function
                () => (
                   $scope
                     .toppings
                     .filter(tp => tp.checked !== undefined)
                     .map(tp => tp.checked)
                 ),

                // Listener
                (nv, ov) => {
                    // nothing changed
                    if(nv == ov || nv == "undefined") return;

                    // Use lodash library to get the changed obj
                    let changedTop = _.difference(nv,ov)[0];
                
                    // Here you go..
                    console.log("changed Topping", changedTop);
            })
Sign up to request clarification or add additional context in comments.

3 Comments

This is one of the better answers in my opinion, and it works much better for arrays, however for the sake of the question author, and anyone else looking for help on this, you should add more detail to your answer to explain that you're using type script, and explain your reasoning for filtering and mapping the array. Otherwise a great solution.
This is not TypeScript but Javascript ES6 :-). Edited for the reason of applying filter.
I think this works only in the very speciifc case (that happens to be the OP's, as they mentioned in a remark) that the watched property has no more than two states.
2

You use MAP to collect all of the property values you need + convert them into a small string representation (in this case 1 and 0) and then join them together into a string that can be observed.

A typescript example:

        $scope.$watch(
            () => this.someArray.map(x => x.selected ? "1" : "0").join(""),
            (newValue, oldValue, scope) => this.onSelectionChanged(this.getSelectedItems()));

1 Comment

I suspect this to be a tiny bit more performant than first doing a filter and then a map as in the other answer by @ahmadalibaloch
1

The watchExpression parameter to $scope.$watch should either be a string or a function. I've not experimented extensively with this (I try and avoid explicit watches where possible) but I think it does also work when you watch 'simple' scope properties as object references, but not so well with more complex references.

I think if you supply the reference as a string, e.g. 'model[' + key + '].checked' then you may have some success (I only say this because I've done something similar with $watchCollection previously).

Alternatively you should be able to supply a function, e.g.

$scope.$watch(function() { return scope.model[key].checked; }, function() { ... });

Hope this helps!

3 Comments

The downside with this approach is that you are creating one watcher per key on the object. The $watchCollection is more performatic.
@William Lepinski: Could you explain why a watchCollection on the array, which has to watch every property on every item, would perform better than a watch on only the property that I need to be watched? I'm not disagreeing, I'd just like to know how that is done. How is a deep watch implemented?
@Tim watchCollection does shallow watch i.e it does not watch each property of an array It just watches if something pushed/popped/overwritten from/to array(ng-repeat uses watchCollection )..
0

Use $watchCollection instead.

From docs:

$watchCollection(obj, listener); Shallow watches the properties of an object and fires whenever any of the properties change (for arrays, this implies watching the array items; for object maps, this implies watching the properties). If a change is detected, the listener callback is fired.

The obj collection is observed via standard $watch operation and is examined on every call to $digest() to see if any items have been added, removed, or moved. The listener is called whenever anything within the obj has changed. Examples include adding, removing, and moving items belonging to an object or array.

2 Comments

but it watches all properties of a collection. not just a single property. In this case OP wants to watch only checked property.
Well you can pluck your array grabbing just the checked property. Assign this newly created array to the scope and use $watchCollection to watch over the array of checked properties.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.