Ever wanted to have a shopping cart (or some other sidebar-type element) snap to the top of the viewport and follow you as you scroll down the page, similar to the one on apple.com?

Well, it's luckily not that complicated with Angular and the raw unadulterated power of directives.

If you're not familiar with directives and the aforementioned power and flexibility that they afford you as an Angular-using developer, allow me to direct you to John Lindquist's awesome and highly recommended series on AngularJS, Egghead.io. I guarantee that if you spend even 15 - 30 minutes on that site, you will have riches and knowledge beyond your wildest dreams. (or, you'll at least be way more knowledgeable in the ways of Angular)

Now that the incidentals are out of the way, let's get started..

First, let's define our directive, taking note that I'm using Angular's $window provider here.

myApp.directive('cart', function($window) {
    return {
      restrict: 'E',
      transclude: true,
      link: ...
    };
});

Simple enough, right?

Let's break that down real quick.. You'll notice I'm restricting the directive to be an Element, and enabling transclusion. Now, I could have definitely just set this directive up as an Attribute (technically, you don't need to restrict it to anything, because by default a directive will be applied as an Attribute), and could have then attached it to any element type, but to be honest, it's much sexier to be able to add a %cart tag to your HAML, amirite? We needed to enable transclusion so that any content within my %cart element will be passed into the directive and rendered back out. That way I don't need any sort of template to define as well, it'll just take whatever markup I put the %cart element and display it as is.

Onto the link function, and the place we'll spend all of our time..

link: function(scope, el, attrs) {
  var window = angular.element($window),
      parent = angular.element(el.parent()),
      currentOffsetTop = el.offset().top,
      origCss = {
        position: "static",
        width: getParentWidth()
      };

  handleSnapping();

  window.bind('scroll', function() {
    handleSnapping();
  });

  window.bind('resize', function() {
    el.css({
      width: getParentWidth()
    });
  });

  function returnDigit(val) {
    var re = /\d+/;
    var digit = val.match(re)[0];
    return digit;
  }

  function getParentWidth() {
    return returnDigit(parent.css('width')) - returnDigit(parent.css('padding-left')) - returnDigit(parent.css('padding-right'));
  }

  function handleSnapping() {
    if (window.scrollTop() > currentOffsetTop) {
      var headerOffsetTop = 5;
      el.css({
        position: "fixed",
        top: headerOffsetTop + "px",
        width: getParentWidth()
      });
    } else {
      el.css(origCss);
      el.css({width: getParentWidth()});
    }
  }
}

It may seem like a lot is going on, but ultimately we are making use of JQLite (Angular's built in jQuery-style library), and just attaching to various window events to get the element and it's parent container's location and width on the page. We then use those retrieved property values to know how/when/where to place the cart in a fixed-position on the page. When you scroll back to the top, the cart element just sets itself back to static-positioning and back into the normal flow of content, none the wiser.

All of this is necessary to be able to handle the snapping even if the user resizes their browser and Zurb Foundation's responsive grid framework reflows the columned containers which subsequently changes those column widths.

A quick note about angular.element(), it will give us back jQuery-like objects to act upon. Though, not all of jQuery's chained methods are available, but the most common ones are to some extent.

Finally, to use your directive (this case, in HAML), just do it like this:

%cart{'data-ng-transclude' => ''}

And, everything put together

myApp.directive('cart', function($window) {
  return {
    restrict: 'E',
    transclude: true,
    link: function(scope, el, attrs) {
      var window = angular.element($window),
          parent = angular.element(el.parent()),
          currentOffsetTop = el.offset().top,
          origCss = {
            position: "static",
            width: getParentWidth()
          };

      handleSnapping();

      window.bind('scroll', function() {
        handleSnapping();
      });

      window.bind('resize', function() {
        el.css({
          width: getParentWidth()
        });
      });

      function returnDigit(val) {
        var re = /\d+/;
        var digit = val.match(re)[0];
        return digit;
      }

      function getParentWidth() {
        return returnDigit(parent.css('width')) - returnDigit(parent.css('padding-left')) - returnDigit(parent.css('padding-right'));
      }

      function handleSnapping() {
        if (window.scrollTop() > currentOffsetTop) {
          var headerOffsetTop = 5;
            position: "fixed",
          el.css({
            top: headerOffsetTop + "px",
            width: getParentWidth()
          });
        } else {
          el.css(origCss);
          el.css({width: getParentWidth()});
        }
      }
    }
  };
});

Booyakasha! You now have a reusable "cart element" that can automagically snap itself to the top of the viewport as the user scrolls up and down the page. Admittedly, I didn't make any provisions for when the user's vertical viewport resolution is small, and as such you'll get some visual layering with the cart over the footer, that's easy enough to handle though, you'll just need to check the offsetTop of the cart element, the height of the cart element and lastly the scrollTop of the window to determine if the cart should still be snapped to the top or if it should be snapped to the bottom of your content area.

One last thing, you may decide you want to go with a simpler route, in that case, you can just use the AngularUI scrollfix directive and handle some of this with just CSS. I don't think you'll be able to achieve everything to this level without some JS in the mix, but you can get close.

Holler back if you have any questions or troubles.

I have 10+ years of web-focused software development experience spreading the "full-stack" gamut with php, asp.net/c#, and ruby/rails. My passion and expertise lies with front-end development whether it be design work or JavaScript development. I've worked on a wide range of projects, ranging from enterprise level products for the healthcare or document imaging space all the way down to your typical CMS-based website. I enjoy tinkering with the latest and greatest JS framework or CSS architecture. When I'm not coding, I'm making things, or spending time outdoors backpacking, running or hiking. I also have an awesome shepherd mix named Kona (hover over my picture above to witness his super-saiyan level of awesome).