Search code examples
angularjsangularjs-directiveangularjs-components

How to Send Variable to Parent Controller from Directive without Isolate Scope


How to watch variable from directive in controller AngularJS?

I have a component with controller and directive

In my directive I'm listening for scroll and returning true if element is in view or false if it is not.

I want to be able to accesss my boolChangeClass variable in the controller so that I can call my function only if it is true.

My question is how can I access boolChangeClass in controller?

    'use strict';
    
    angular.module('myModule')
      .component('myComponent', {
        template: `
          <div class="component" inview ng-class="{min: boolChangeClass}">
          </div>
        `,
    
        controller: function ($scope) {
          let $ctrl = this;

          let func = () => {console.log('my function')}

      $scope.$watch('boolChangeClass', func);
    }
  })
  .directive('inview', ($window) => {
    return function(scope, element, attrs) {
        angular.element($window).bind("scroll", function() {
          let rect = element[0].getBoundingClientRect();
          scope.boolChangeClass =  rect.bottom > 0 &&
              rect.right > 0 &&
              rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
              rect.top < (window.innerHeight || document.documentElement.clientHeight)
          scope.$apply();
        });
    };
  })

Solution

  • tl;dr;

    To send variable to parent controller from a directive that uses scope:false:

    <div on-scroll="$ctrl.fn($event)"></div>
    
    scope.$apply( () => {
        scope.$eval(attrs.onScroll, {$event: event});
    });
    

    Answer

    Instead of setting the class with ng-class, have the directive set the class:

    <div class="component" inview-class="min"  ̶n̶g̶-̶c̶l̶a̶s̶s̶=̶"̶{̶m̶i̶n̶:̶ ̶b̶o̶o̶l̶C̶h̶a̶n̶g̶e̶C̶l̶a̶s̶s̶}̶" >
    </div> 
    
    app.directive('inviewClass', ($window) => {
      return function(scope, elem, attrs) {
        angular.element($window).on("scroll", function(event) {
          let rect = elem[0].getBoundingClientRect();
          let boolChangeClass =  rect.bottom > 0 &&
              rect.right > 0 &&
              rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
              rect.top < (window.innerHeight || document.documentElement.clientHeight);
          let className = attrs.inviewClass;
          boolChangeClass ? elem.addClass(className) : elem.removeClass(className);
        });
      };
    })
    

    This avoids involving the AngularJS digest cycle, modifying $scope, and adding a watcher.


    Update

    And what should I do in my controller? Check if class exists and then call he method?

    Use an attribute to specify the function to be called on a scroll event:

    <div class="component" inview-class="min" on-scroll="$ctrl.fn($event)" >
    </div> 
    
    app.directive('inviewClass', ($window) => {
      return function(scope, elem, attrs) {
        angular.element($window).on("scroll", function(event) {
          let rect = elem[0].getBoundingClientRect();
          let boolChangeClass =  rect.bottom > 0 &&
              rect.right > 0 &&
              rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
              rect.top < (window.innerHeight || document.documentElement.clientHeight);
          let className = attrs.inviewClass;
          boolChangeClass ? elem.addClass(className) : elem.removeClass(className);
          //
          event.inview = {};
          event.inview[className] = boolChangeClass;
          scope.$apply( () => {
              scope.$eval(attrs.onScroll, {$event: event});
          });
        });
      };
    })
    

    On each scroll event, the directive will evaluate the on-scroll attribute as an AngularJS expression using $event as a local.


    Update #2

    But what if I want to call method in controller on scroll but only once when my variable is true? I tried to use one-time binding but it doesn't really work.

    It would be wiser to have the controller filter the events:

    <div class="component" inview-class="min" on-scroll="$ctrl.fn($event)" >
    </div> 
    
    var minOnce;
    this.fn = (event) => {
        if (minOnce || !event.inview.min) return;
        //ELSE
        minOnce = true;
        //
        // ...
    };
    

    This way the directive is more versatile and testing is easier.