Bind class toggle to window scroll event

103,480

Solution 1

Why do you all suggest heavy scope operations? I don't see why this is not an "angular" solution:

.directive('changeClassOnScroll', function ($window) {
  return {
    restrict: 'A',
    scope: {
        offset: "@",
        scrollClass: "@"
    },
    link: function(scope, element) {
        angular.element($window).bind("scroll", function() {
            if (this.pageYOffset >= parseInt(scope.offset)) {
                element.addClass(scope.scrollClass);
            } else {
                element.removeClass(scope.scrollClass);
            }
        });
    }
  };
})

So you can use it like this:

<navbar change-class-on-scroll offset="500" scroll-class="you-have-scrolled-down"></navbar>

or

<div change-class-on-scroll offset="500" scroll-class="you-have-scrolled-down"></div>

Solution 2

Thanks to Flek for answering my question in his comment:

http://jsfiddle.net/eTTZj/30/

<div ng-app="myApp" scroll id="page" ng-class="{min:boolChangeClass}">

    <header></header>
    <section></section>

</div>

app = angular.module('myApp', []);
app.directive("scroll", function ($window) {
    return function(scope, element, attrs) {
        angular.element($window).bind("scroll", function() {
             if (this.pageYOffset >= 100) {
                 scope.boolChangeClass = true;
             } else {
                 scope.boolChangeClass = false;
             }
            scope.$apply();
        });
    };
});

Solution 3

This is my solution, it's not that tricky and allow you to use it for several markup throught a simple ng-class directive. Like so you can choose the class and the scrollPos for each case.

Your App.js :

angular.module('myApp',[])
    .controller('mainCtrl',function($window, $scope){
        $scope.scrollPos = 0;

        $window.onscroll = function(){
            $scope.scrollPos = document.body.scrollTop || document.documentElement.scrollTop || 0;
            $scope.$apply(); //or simply $scope.$digest();
        };
    });

Your index.html :

<html ng-app="myApp">
    <head></head>
    <body>
        <section ng-controller="mainCtrl">
            <p class="red" ng-class="{fix:scrollPos >= 100}">fix me when scroll is equals to 100</p>
            <p class="blue" ng-class="{fix:scrollPos >= 150}">fix me when scroll is equals to 150</p>
        </section>
    </body>
</html>

working JSFiddle here

EDIT :

As $apply() is actually calling $rootScope.$digest() you can directly use $scope.$digest() instead of $scope.$apply() for better performance depending on context.
Long story short : $apply() will always work but force the $digest on all scopes that may cause perfomance issue.

Solution 4

Maybe this can help :)

Controller

$scope.scrollevent = function($e){
   // Your code
}

Html

<div scroll scroll-event="scrollevent">//scrollable content</div>

Or

<body scroll scroll-event="scrollevent">//scrollable content</body>

Directive

.directive("scroll", function ($window) {
   return {
      scope: {
         scrollEvent: '&'
      },
      link : function(scope, element, attrs) {
        $("#"+attrs.id).scroll(function($e) { scope.scrollEvent != null ?  scope.scrollEvent()($e) : null })
      }
   }
})

Solution 5

What about performance?

  1. Always debounce events to reduce calculations
  2. Use scope.applyAsync to reduce overall digest cycles count
function debounce(func, wait) {
    var timeout;
    return function () {
        var context = this, args = arguments;
        var later = function () {
            timeout = null;
            func.apply(context, args);
        };

        if (!timeout) func.apply(context, args);
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

angular.module('app.layout')
  .directive('classScroll', function ($window) {    
    return {
        restrict: 'A',
        link: function (scope, element) {    
            function toggle() {
                angular.element(element)
                  .toggleClass('class-scroll--scrolled', 
                    window.pageYOffset > 0);
                scope.$applyAsync();
            }    
            angular.element($window)
              .on('scroll', debounce(toggle, 50));

            toggle();
        }
    };
});

3. If you don't need to trigger watchers/digests at all then use compile

.directive('classScroll', function ($window, utils) {
    return {
        restrict: 'A',
        compile: function (element, attributes) {
            function toggle() {
                angular.element(element)
                  .toggleClass(attributes.classScroll,
                    window.pageYOffset > 0);
            }

            angular.element($window)
              .on('scroll', utils.debounce(toggle, 50));
            toggle();
        }
    };
  });

And you can use it like <header class-scroll="header--scrolled">

Share:
103,480
StuR
Author by

StuR

Developer at Square Enix.

Updated on October 30, 2020

Comments

  • StuR
    StuR over 3 years

    When a user scrolls their browser window below a certain point, I am toggling the class of the #page div.

    What I have done so far works fine:

    http://jsfiddle.net/eTTZj/29/

    <div ng-app="myApp" scroll id="page">
    
        <header></header>
        <section></section>
    
    </div>
    
    app = angular.module('myApp', []);
    app.directive("scroll", function ($window) {
        return function(scope, element, attrs) {
            angular.element($window).bind("scroll", function() {
                 if (this.pageYOffset >= 100) {
                     element.addClass('min');
                     console.log('Scrolled below header.');
                 } else {
                     element.removeClass('min');
                     console.log('Header is in view.');
                 }
            });
        };
    });
    

    (when they scroll their window below the header, 100px, the class is toggled)

    Although, correct me if I'm wrong, I feel that this is not the correct way to be doing this with Angular.

    Instead, I presumed that the best method for doing this would be by using ng-class and storing a boolean value in the scope. Something like this:

    <div ng-app="myApp" scroll id="page" ng-class="{min: boolChangeClass}">
    
        <header></header>
        <section></section>
    
    </div>
    
    app = angular.module('myApp', []);
    app.directive("scroll", function ($window) {
        return function(scope, element, attrs) {
            angular.element($window).bind("scroll", function() {
                 if (this.pageYOffset >= 100) {
                     scope.boolChangeClass = true;
                     console.log('Scrolled below header.');
                 } else {
                     scope.boolChangeClass = false;
                     console.log('Header is in view.');
                 }
            });
        };
    });
    

    Although this is not dynamic, if I change the value of scope.boolChangeClass in the scroll callback, then the ng-class is not updating.

    So my question is: how is best to toggle the class of #page, using AngularJS, when the user scrolls below a certain point?