user3111277
user3111277

Reputation: 2297

Using ScrollSpy in AngularJS

I'm new to AngularJS. I'm using AngularJS 1.2.5 and Bootstrap 3.0. I'm trying to include ScrollSpy in my app. However, I'm having some challenges. I'm trying to incorporate the code found here. Currently, my code looks like this:

index.html

<div class="row" scroll-spy>
  <div class="col-md-3 sidebar">
    <ul style="position:fixed;" class="nav nav-pills nav-stacked">
      <li class="active" spy="overview"><a href="#overview">Overview</a></li>
      <li spy="main"><a href="#main">Main Content</a></li>
      <li spy="summary"><a href="#summary">Summary</a></li>
      <li spy="links"><a href="#links">Other Links</a></li>
    </ul>
  </div>

  <div class="col-md-9 content">
    <h3 id="overview">Overview</h3>
    Lorem Ipsum Text goes here...

    <h3 id="main">Main Body</h3>
    Lorem Ipsum Text goes here...

    <h3 id="summary">Summary</h3>
    Lorem Ipsum text goes here...

    <h3 id="links">Other Links</h3>
  </div>
</div>

index.html.js

angular.module('td.controls.scrollSpy', [])
  .directive('spy', function ($location) {
    return {
      restrict: 'A',
      require: '^scrollSpy',
      link: function (scope, elem, attrs, scrollSpy) {
        var _ref;
        if ((_ref = attrs.spyClass) == null) {
          attrs.spyClass = 'current';
        }
        elem.click(function () {
          return scope.$apply(function () {
            return $location.hash(attrs.spy);
          });
        });
        return scrollSpy.addSpy({
          id: attrs.spy,
          'in': function () {
            return elem.addClass(attrs.spyClass);
          },
          out: function () {
            return elem.removeClass(attrs.spyClass);
          }
        });
      }
    };
  })
  .directive('scrollSpy', function ($location) {
    return {
      restrict: 'A',
      controller: function ($scope) {
        $scope.spies = [];
        return this.addSpy = function (spyObj) {
          return $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));
            } else {
              _results.push(void 0);
            }
          }
          return _results;
        });
        return $($window).scroll(function () {
          var highlightSpy, pos, spy, _i, _len, _ref;
          highlightSpy = null;
          _ref = scope.spies;
          for (_i = 0, _len = _ref.length; _i < _len; _i++) {
            spy = _ref[_i];
            spy.out();
            spyElems[spy.id] = spyElems[spy.id].length === 0 ? elem.find('#' + spy.id) : spyElems[spy.id];

            if (spyElems[spy.id].length !== 0) {
              if ((pos = spyElems[spy.id].offset().top) - $window.scrollY <= 0) {
                spy.pos = pos;
                if (highlightSpy == null) {
                  highlightSpy = spy;
                }
                if (highlightSpy.pos < spy.pos) {
                  highlightSpy = spy;
                }
              }
            }
          }
          return highlightSpy != null ? highlightSpy['in']() : void 0;
        });
      }
    };
  })
;

When I run this in the browser I get several errors. When I initially run it, I see the following errors in my browser console:

TypeError: Object function (spyObj) { return $scope.spies.push(spyObj); } has no method 'addSpy'
ReferenceError: $window is not defined

I can't figure out a) Why I'm getting these errors or b) how to get this basic example to work. I really like this approach to using scrollspy with AngularJS. It's the cleanest implementation I've seen. For that reason, I'd love to figure out how to get this working.

Upvotes: 1

Views: 1402

Answers (2)

haiflive
haiflive

Reputation: 1551

if you wont that it working with ng-include change the follow condition

      if (spyElems[spy.id].offset() === undefined) {
        continue;
      }

on this

if (spyElems[spy.id].offset() === undefined) {
    // try to refind it
    spyElems[spy.id] = elem.find('#' + spy.id);
    if(spyElems[spy.id].offset() === undefined)
        continue;
}

Upvotes: 0

Nicholas Pappas
Nicholas Pappas

Reputation: 10624

I recently ran across Alexander's solution as well and went through the process of translating it.

To answer your direct question: You need to import $window into your scrollSpy directive.

.directive('scrollSpy', function ($location, $window) {

Below is the complete translation I did of Alexander's code:

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');
        }
      });
    }
  };
});

The above code also supports highlight the last spy element in the menu if the browser is scrolled to the bottom, which the original code did not.

Upvotes: 2

Related Questions