60

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?

3
  • 5
    I had exactly the same issue today :) And I solved it exactly the same way (2nd version)! I also don't get why the ng-class is not updated but if you digest (Just add $scope.$apply() after you changed the boolChangeClass) it works. Commented Feb 14, 2013 at 17:16
  • I am actually just confused why we need to explicitly call to call the digestion because we don't use any third party library, everything is Angular. Mhh... Maybe someone will find out :) Commented Feb 15, 2013 at 13:05
  • Another option is to make a hook when some element (placed on top of the view) becomes visible. This will throw an event you will use to toggle the class. Commented Nov 20, 2014 at 10:46

6 Answers 6

86

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();
        });
    };
});
Sign up to request clarification or add additional context in comments.

4 Comments

This directive only fire one time for all. How can we have the possibility to have multiple element with they own "boolChangeClass" ?
I would very much advise AGAINST scope.$apply()! It results a full page reevaluation at each scroll event. In my case I change an image when the top menu gets fixed style (pretty much how google+ are doing). So I manually set it from the directive via javascript: logoelem.css('background-image', 'url(' + scope.imageLogo + ')');. When I did using scope.$apply() it recalculated every {{showMessage()}} style variable on the page. And I was using a table, so each scroll event resulted about 40 recalculations. It was laggy. Updating the needed element only spares many cpu cycle for me.
if state can be simplified to: scope.boolChangeClass = this.pageYOffset >= 100;
That's actually a very bad solution as @arcol already suggested. Scroll event handlers should execute as little code as possible and then use other means to catch those changes. Check John Resig's simplified scroll throttling that you could use in this case. And likely also use scope.$applyAsync() which will not start synchronous digest cycle but rather queue several of them to the same digest when it can actually be executed.
23

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>

3 Comments

This should be the selected answer. Works great! I love the reusable function and allowing us to define the class and offset.
Seems to me this is the best answer, I ended up adding an extra variable to target what element you want to apply the scrollClass to. Also changed the scroll event to bind to the element the directive is on instead of $window. Sweet directive, thank you!
Works great, and took 2 minutes to implement, thanks! Replacing a template's jquery.waypoints.js after SPA routing killed it.
16

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.

4 Comments

how can I get it working in my case. I am also facing issue in getting scroll event when user scrolls a div. I have a controller where I want to get event when user scrolls the list.
Here I watch for the scroll position of window object. I guess you have to watch for the scroll position of the specific element. i.e : document.getElementById('myDiv').scrollTop;
Problem is my code inside call back method " $window.onscroll = function()" is not getting called at all. When I do "$window.onscroll = function(){console.log("scrolling");}" then I don't see "scrolling" in logs.
You also have to target for the scroll on your specific element: document.getElementById('myDiv').onscroll = function ...
2

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 })
      }
   }
})

1 Comment

Thanks heaps for this! Worked for me, but not until I added an id attribute to the div.
1

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">

Comments

-4

Directives are not "inside the angular world" as they say. So you have to use apply to get back into it when changing stuff

1 Comment

To anyone coming to this question reading this answer and wondering; Don't mind it. Directives are of course well "inside the angular world" and don't need an $apply at the end. The reason why the apply is needed here is because the callback is bound to an event (in this case the scroll event). And browser events are generally outside of angular event loop

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.