user3267755
user3267755

Reputation: 1080

Angular Directive Firing Multiple Times

I am newer to AngularJS and having an issue that I hope someone can point me in the right direction to figuring out. I have created a directive called sizeWatcher which is placed as an attribute into the HTML which essentially just gets the height of the element it's placed on and echos that height into a scope variable named style that I set onto another element through the use of ng-style="style".

I'm finding that whenever I open my accordion, the $watch fires on the directive but it's firing multiple times. I have a console.log in my $watch and am seeing 3 log entries, the first 2 are the same (guessing this happens on click before the accordion opens, and then the accordion opens and the 3rd log entry is the final height after the accordion is opened). The main issue is that the style variable is only getting set to the smaller heights before the accordion is expanded even though the log is registering the greater height as the last time the directive is hit -- How can I ignore the first $watch event firings and only act accordingly on the last and final run-through of the directive? Any insight into this would be greatly appreciated. Relevant code attached below:


TEMPLATE:

 <div class="content-wrap" id="height-block" ng-style="style">
     <!-- Other HTML etc... -->
     <uib-accordion size-watcher close-others="oneAtATime">
          <!-- Accordion Directive HTML.. -->
     </uib-accordion>
 </div>

JavaScript:

.directive("sizeWatcher", function () { //add size-watcher attribute to element on the page to have it echo its' height to the {{style}} scope
    function link(scope, element, attrs) {
        scope.$watch(function () { //watch element for changes
            var height = element[0].offsetHeight;
            console.log(height);
            if (height > 150) {
                scope.style = {
                    height: height + 'px'
                };
            }
        });
    }
    return {
        restrict: "AE", //attribute & element declarations
        link: link
    };
})

Upvotes: 1

Views: 1812

Answers (3)

madhur
madhur

Reputation: 1001

This is happening because you are not using $watch correct way,

  • The first parameter to $watch is a variable which you want to watch(this can be a callback).
  • The second parameter to $watch is a callback which performs the desired action on change

So in your case it would be something like this

scope.$watch(
   function () {
       return element[0].offsetHeight;
   },
   function () { //watch element for changes
      var height = element[0].offsetHeight;
      console.log(height);
      if (height > 150) {
          scope.style = {
              height: height + 'px'
          };
      }
   }
)

Please notice the first function, so whenever the value it is returning changes, the second callback will execute

Hope this helps you

Upvotes: 0

H W
H W

Reputation: 2596

Obviously an answer to this needs a link to the Angular-Documentation for $watch ;)

it states the following:

After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization.

which probably explains your first call.

I'm guessing the second call happens because the accordion is rerendered after initialization (with a title/ or label or anything) which triggers the $digest and thus the $watch expression on the height.

Finally the third call happens when you open the accordion and the height actually changes.

To fix this you can compare the newValue and oldValue of the watched expression like Maxim Shoustin said in his answer. Here is an example (again taken from the Angular-docs)

scope.$watch(
  // This function returns the value being watched. It is called for each turn of the $digest loop
  function() { return food; },
  // This is the change listener, called when the value returned from the above function changes
  function(newValue, oldValue) {
    if ( newValue !== oldValue ) {
      // Only increment the counter if the value changed
      scope.foodCounter = scope.foodCounter + 1;
    }
  }
);

However if you actually want to change the style of the element you might want to take a look into ng-class instead of manually registering any watchers!

Upvotes: 0

Maxim Shoustin
Maxim Shoustin

Reputation: 77910

How can I ignore the first $watch event firings and only act accordingly on the last and final run-through of the directive?

You can ignore watcher when new or old values are undefined and not equal to each other:

$scope.$watch(function () {
    return element.height(); // or something else
},
function (newVal, oldVal) {

   if (newVal !== undefined && oldVal !== undefined && newVal !== oldVal) {
         // your stuff   
         if (newVal > 150) {
            scope.style = {
                height: newVal + 'px'
            };
        }    
   }                
});

Anyways you can play with if statement regards to your needs


FYI, to improve performance $watch returns cancel callback so you can stop watcher whenever you want:

var cancelWatch = $scope.$watch(function () {
    return element.height();
},
function (newVal, oldVal) {        
    if (<some condition>) {            
        cancelWatch();            
    }        
});

Upvotes: 3

Related Questions