Bind class toggle to window scroll event

AngularjsAngularjs Directive

Angularjs Problem Overview


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?

Angularjs Solutions


Solution 1 - Angularjs

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 2 - Angularjs

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 3 - Angularjs

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

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

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

Solution 6 - Angularjs

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionStuRView Question on Stackoverflow
Solution 1 - AngularjsStuRView Answer on Stackoverflow
Solution 2 - Angularjsuser1415066View Answer on Stackoverflow
Solution 3 - AngularjsFreezystemView Answer on Stackoverflow
Solution 4 - AngularjsKelkView Answer on Stackoverflow
Solution 5 - AngularjsgrigsonView Answer on Stackoverflow
Solution 6 - AngularjsGoffView Answer on Stackoverflow