Skip to content
Create a gist now

Instantly share code, notes, and snippets.

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

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

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

@manishdipankar1711

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

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

@yorch

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

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.

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.