Angular sticky-navigation directive (fix a control to the bottom of the page when scrolling past it)

This directive is particularly useful when you want to avoid duplicate controls on long pages. The idea is simple: when we scroll past an element (could be a collection of buttons), the element appears at the bottom of the page, fixed. When we scroll back up, we move it back to the original position.

Here's how it can end up (the blue controls pop up at the bottom):
Angular sticky directive demo

Grab it from github, see a quick demo here or add it as a dependency to your app using bower install ng-sticky --save.

...and here's how I made it work:

myModule.directive("stickyNav", function stickyNav($window){  
  function stickyNavLink(scope, element){
    var w = angular.element($window),
        size = element[0].clientHeight,
        top = 0;

    function toggleStickyNav(){
      if(!element.hasClass('controls-fixed') && $window.pageYOffset > top + size){
        element.addClass('controls-fixed');
      } else if(element.hasClass('controls-fixed') && $window.pageYOffset <= top + size){
        element.removeClass('controls-fixed');
      }
    }

    scope.$watch(function(){
      return element[0].getBoundingClientRect().top + $window.pageYOffset;
    }, function(newValue, oldValue){
      if(newValue !== oldValue && !element.hasClass('controls-fixed')){
        top = newValue;
      }
    });
  }

  return {
    scope: {},
    restrict: 'A',
    link: stickyNavLink
  };
});

The only HTML you need to add is this:

<section sticky-nav>  
  <button>Do something!</button>
</section>  

This directive toggles the class 'controls-fixed' on the directive element.
Now the only thing we have to do is put together some CSS to actually make the element fixed when the class is applied. Here's a way it can be done:

.controls-fixed{
  background-color:rgba(0, 0, 0, 0.1);
  bottom:0;
  left:0;
  padding:10px 15px;
  position:fixed;
  top:auto;
  width:100%;
}

While we're adding CSS, you should keep in mind that a static or relative-positioned element can seem 'jumpy', so you might want to set position absolute by default. It's also a good idea to add a bottom-padding that is at least equal to the 'sticky' element to the page (this way the content will be visible even when users scroll to the very bottom).

However, this won't cut it. The problem is that I am setting a targetScrollTop on directive initilaization. This is an issue because on viewport resize, the top doesn't update. If you are not concerned about people resizing their browser windows, you should be. Even if not many do it on desktops, people do it very often on portable devices when switching between landscape and portrait mode. To fix this, we need to watch for the window resize event. Here's the improved directive code:

myModule.directive("stickyNav", function stickyNav($window){  
  function stickyNavLink(scope, element){
    var w = angular.element($window),
        size = element[0].clientHeight,
        top = 0;

    function toggleStickyNav(){
      if(!element.hasClass('controls-fixed') && $window.pageYOffset > top + size){
        element.addClass('controls-fixed');
      } else if(element.hasClass('controls-fixed') && $window.pageYOffset <= top + size){
        element.removeClass('controls-fixed');
      }
    }

    scope.$watch(function(){
      return element[0].getBoundingClientRect().top + $window.pageYOffset;
    }, function(newValue, oldValue){
      if(newValue !== oldValue && !element.hasClass('controls-fixed')){
        top = newValue;
      }
    });

    w.bind('resize', function stickyNavResize(){
      element.removeClass('controls-fixed');
      top = element[0].getBoundingClientRect().top + $window.pageYOffset;
      toggleStickyNav();
    });
    w.bind('scroll', toggleStickyNav);
  }

  return {
    scope: {},
    restrict: 'A',
    link: stickyNavLink
  };
});

That's it!