9

Does anyone have any thoughts on the best way to implement a scrollspy in an angular app? I started out with Twitter Bootstrap's implementation but I am struggling to find a good place to initialize (and more importantly refresh after DOM changes) the plugin.

Basically my controller does an async request for data when it's loaded and can't fully render the DOM untill the request returns. Then when the DOM is fully rendered the scrollspy plugin needs to be refreshed. Right now I have my controller broadcast an event on its scope once the needed data has been received and I have a directive that picks up this event. I don't really like this solution for two reasons

  1. It just feels hacky.

  2. When I refresh the scrollspy plugin after receiving this event it is still too early since the DOM isn't updated untill later in the cycle. I tried evalAsync but in the end I had to use a timeout and just hope the DOM renders fast enough.

I had a look at the source for the Bootstrap plugin and it seems fairly straight forward to implement this from scratch. The problem I was having with that is that when I tried to make a directive for it I couldn't seem to subscribe to scroll events from the element I received in the link function.

Has anyone found a good solution to something like this, or does anyone have any suggestions? I'm in no way tied to using the Bootstrap implementation, as of right now the only dep I have on Bootstrap is the scrollspy-plugin I just added.

4

4 回答 4

20

Alexander Hill 发表了一篇文章,描述了 Bootstrap 的 ScrollSpy 的 AngularJS 实现:http: //alxhill.com/blog/articles/angular-scrollspy/

我已经将他的 CoffeeScript 代码翻译成 JavaScript,修复了一些错误,添加了一些安全检查和一个额外的功能。这是代码:

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

来自 Alexander 的博客文章的 HTML 片段,展示了如何实现该指令:

<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>
于 2014-02-24T18:55:49.657 回答
12

在 Angular 中使用 Scrollspy 存在一些挑战(这可能是 AngularUI 到 2013 年 8 月仍未包含 Scrollspy 的原因)。

每当您对 DOM 内容进行任何更改时,都必须刷新 scrollspy。

这可以通过 $watching 元素并为 scrollspy('refresh') 调用设置超时来完成。

如果您希望能够使用 nav 元素来导航可滚动区域,您还需要覆盖 nav 元素的默认行为。

这可以通过在锚元素上使用 preventDefault 来阻止导航并将 scrollTo 函数附加到 ng-click 来实现。

我在 Plunker 上抛出了一个工作示例:http ://plnkr.co/edit/R0a4nJi3tBUBsluBJUo2?p=preview

于 2013-08-06T12:46:55.857 回答
5

我在 github 上找到了一个项目,它可以添加 scrollspy、动画 scrollTo 和滚动事件。

https://github.com/oblador/angular-scroll

在这里您可以查看现场演示,这里是示例源

你可以使用 bower 来安装它

$ bower install angular-scroll
于 2015-01-14T05:07:23.077 回答
1

我正在使用原始 Bootstrap的滚动间谍。Nav 链接的 href 就像页面没有 Anuglar 的 ngRoute。然后使用选项为该页面设置 ngRoutereloadOnSearch: false对于滚动,我正在使用 Angular 内置 # 支持 - 如果 url 看起来像,http://host/#/path#anchor那么 ngRoute 将为指定的“/路径”加载页面并滚动到指定的“锚点”。

您看不到它,因为没有 reloadOnSearch:每次都加载错误页面,并且在滚动带有锚中指定的 id 的元素时尚未呈现。为了解决初始页面加载和渲染后初始滚动到锚点的问题,写了很多。

我的解决方案的最后一个元素是编写我自己的指令,该指令放置在每个导航链接上 - 它停止点击传播,防止默认操作,然后设置页面锚以对应 Angular 要求。例如,如果页面 URL 是http://host/#/path并且单击的链接具有 href="#some-id" 我的指令将创建新 URLhttp://host/#/path#some-id

于 2015-09-20T15:54:05.247 回答