Skip to content

Instantly share code, notes, and snippets.

@nwpappas
Created February 24, 2014 19:07
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save nwpappas/9194765 to your computer and use it in GitHub Desktop.
Save nwpappas/9194765 to your computer and use it in GitHub Desktop.
An AngularJS directive implementation of Twitter Bootstrap's ScrollSpy, ported and updated from https://gist.github.com/alxhill/6886760.
app.directive('scrollSpy', function ($window) {
return {
restrict: 'A',
controller: function ($scope) {
$scope.spies = [];
this.addSpy = function (spyObj) {
$scope.spies.push(spyObj);
};
},
link: function (scope, elem, attrs) {
var spyElems;
spyElems = [];
scope.$watch('spies', function (spies) {
var spy, _i, _len, _results;
_results = [];
for (_i = 0, _len = spies.length; _i < _len; _i++) {
spy = spies[_i];
if (spyElems[spy.id] == null) {
_results.push(spyElems[spy.id] = elem.find('#' + spy.id));
}
}
return _results;
});
$($window).scroll(function () {
var highlightSpy, pos, spy, _i, _len, _ref;
highlightSpy = null;
_ref = scope.spies;
// cycle through `spy` elements to find which to highlight
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
spy = _ref[_i];
spy.out();
// catch case where a `spy` does not have an associated `id` anchor
if (spyElems[spy.id].offset() === undefined) {
continue;
}
if ((pos = spyElems[spy.id].offset().top) - $window.scrollY <= 0) {
// the window has been scrolled past the top of a spy element
spy.pos = pos;
if (highlightSpy == null) {
highlightSpy = spy;
}
if (highlightSpy.pos < spy.pos) {
highlightSpy = spy;
}
}
}
// select the last `spy` if the scrollbar is at the bottom of the page
if ($(window).scrollTop() + $(window).height() >= $(document).height()) {
spy.pos = pos;
highlightSpy = spy;
}
return highlightSpy != null ? highlightSpy["in"]() : void 0;
});
}
};
});
app.directive('spy', function ($location, $anchorScroll) {
return {
restrict: "A",
require: "^scrollSpy",
link: function(scope, elem, attrs, affix) {
elem.click(function () {
$location.hash(attrs.spy);
$anchorScroll();
});
affix.addSpy({
id: attrs.spy,
in: function() {
elem.addClass('active');
},
out: function() {
elem.removeClass('active');
}
});
}
};
});
@rbaggett
Copy link

rbaggett commented Aug 7, 2014

I am using this directive, and it works great in Chrome. However, It is not working in IE 11. The active class is not correctly assigned, and I am not able to navigate to the first anchor. Debugged it quite a bit, but not seeing anything to indicate why it is failing.

@kevinlbatchelor
Copy link

one reason it doesn't work in ie is $window.scrollY. Use pageYOffset instead.

@manishdipankar1711
Copy link

Hi
I got this error trying to implement your code.

Error: [$compile:ctreq] Controller 'scrollSpy', required by directive 'spy', can't be found!

AngularJS docs says that The ^ prefix means that this directive searches for the controller on its parents (without the ^ prefix, the directive would look for the controller on just its own element).
In this case we have controller in scrollSpy directive, still it gives this error. Help please.

@morfin
Copy link

morfin commented Feb 11, 2015

Does not work:
Error: [$compile:ctreq] Controller 'scrollSpy', required by directive 'spy', can't be found!

@yorch
Copy link

yorch commented May 13, 2015

You are missing the scroll-spy in your markup, it should look like (notice the first line):

<div class="row" scroll-spy>
  <div class="col-md-3 sidebar">
    <ul>
      <li spy="overview">Overview</li>
      <li spy="main">Main Content</li>
      <li spy="summary">Summary</li>
      <li spy="links">Other Links</li>
    </ul>
  </div>
  <div class="col-md-9 content">
    <h3 id="overview">Overview</h3>
    <!-- overview goes here -->
    <h3 id="main">Main Body</h3>
    <!-- main content goes here -->
    <h3 id="summary">Summary</h3>
    <!-- summary goes here -->
    <h3 id="links">Other Links</h3>
    <!-- other links go here -->
  </div>
</div>

@monomial
Copy link

I have found a couple of things that may help people trying to get this code to work for them.

  1. Modify the "click" event handler on the "spy" directive to call scope.$apply().

For example:

elem.click(function () {
   scope.$apply(function () {
      $location.hash(attrs.spy);
    });
});

The call to $apply is in Alxhill's CoffeScript code, so must have accidentally gotten removed when converting to JavaScript.

  1. If you're using Angular routing (ngRoute / ngView), make sure to set "reloadOnSearch" to false so that the view doesn't reload every time $location.hash is called.

@stevie-p
Copy link

I have implemented this angular scrollspy in my single page application - which also uses ng-route and dynamically populates the sidenav based on the h1-h6 elements within #main.

See my code on Plnkr: http://plnkr.co/edit/OXyMEACVW5RDx09UG8LT?p=preview
Because my code creates some spys without id's I've included a condition in the spy directive:

if (attrs.spy != "") {
  affix.addSpy({``
    id: attrs.spy,
    in: function() {
      elem.addClass('active');
    },
    out: function() {
      elem.removeClass('active');
    }
  });
}

Edit: Does anyone have a working version with nested sidenavs which keeps the .active on parent elements with .active children?

@alokkumardubey
Copy link

in mozilla if i m clicking on a specfic li it assign the active class in the upper li how to resolve this

@PatrickWatzlawik
Copy link

Big thanks mate!

@lkende
Copy link

lkende commented Jul 14, 2016

For this to work with dynamically generated content, i.e., ng-repeat creating the DOM for both the navigation links and the header tags with matching ids, you must add a $timeout or an $evalAsync around the code that actually calls elem.find('#' + spy.id) else those elements don't exist yet and will not work as expected.

@pedrodsa
Copy link

@ikend do you fixe the problem? If yes, can you send the solution?
As far as I understand, the $scope.spies.push(spyObj) in the this.addSpy function doesn't triggers the $watch('spies'... (watcher that adds the jquery element on the spyElems list). I'm not a master on angularjs and I cannot guess why this watcher doesn't trigger on a watched variable changes...

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment