48

示例应用程序: http: //angular.github.com/angular-phonecat/step-11/app/#/phones

如果您选择最后一部手机“摩托罗拉魅力”,它将向您显示手机的详细信息。当您在浏览器上导航时,它会重新加载数据并且滚动位于顶部。

导航返回时自动滚动到左侧的最佳方法是什么?而且,为什么 Angular 会重新加载数据?

我的计算机上有相同的“angular-phonecat”示例,并且我添加了一个无限滚动,它会在您滚动时加载更多数据。所以我真的不希望用户再次重新加载 50 多个项目或向下滚动 30 秒。

4

15 回答 15

31

我在这里有一个小提琴,它显示了如何在详细视图之后恢复列表视图中的滚动位置;尚未封装在指令中,正在处理...

http://jsfiddle.net/BkXyQ/6/

$scope.scrollPos = {}; // scroll position of each view

$(window).on('scroll', function() {
    if ($scope.okSaveScroll) { // false between $routeChangeStart and $routeChangeSuccess
        $scope.scrollPos[$location.path()] = $(window).scrollTop();
        //console.log($scope.scrollPos);
    }
});

$scope.scrollClear = function(path) {
    $scope.scrollPos[path] = 0;
}

$scope.$on('$routeChangeStart', function() {
    $scope.okSaveScroll = false;
});

$scope.$on('$routeChangeSuccess', function() {
    $timeout(function() { // wait for DOM, then restore scroll position
        $(window).scrollTop($scope.scrollPos[$location.path()] ? $scope.scrollPos[$location.path()] : 0);
        $scope.okSaveScroll = true;
    }, 0);
});

小提琴还显示了在“ListCtrl”之外获取列表一次。

于 2013-04-24T17:29:17.643 回答
17

下面是 keep-scroll-pos 指令的另一个版本。这个版本

  • 记住$routeProvider 定义的每个templateUrl的滚动位置。

  • 尊重哈希标签,例如 #/home# section-2,将滚动到#section-2而不是之前的滚动位置。

  • 易于使用,因为它是独立的,并在内部存储滚动位置。

html使用示例:

<div ng-view keep-scroll-pos></div>

keepScrollPos 指令的代码如下:

“使用严格”;

angular.module("myApp.directives", [])

.directive("keepScrollPos", function($route, $window, $timeout, $location, $anchorScroll) {

    // 缓存每条路由的templateUrl的滚动位置
    var scrollPosCache = {};

    // 编译函数
    返回函数(范围,元素,属性){

        scope.$on('$routeChangeStart', function() {
            // 存储当前视图的滚动位置
            如果($route.current){
                scrollPosCache[$route.current.loadedTemplateUrl] = [ $window.pageXOffset, $window.pageYOffset ];
            }
        });

        scope.$on('$routeChangeSuccess', function() {
            // 如果明确指定了哈希值,它会优先于之前存储的滚动位置
            if ($location.hash()) {
                $anchorScroll();

            // 否则获取上一个滚动位置;如果没有,滚动到页面顶部
            } 别的 {
                var prevScrollPos = scrollPosCache[$route.current.loadedTemplateUrl] || [0, 0];
                $超时(函数(){
                    $window.scrollTo(prevScrollPos[0], prevScrollPos[1]);
                }, 0);
            }
        });
    }
});

要忽略先前存储的滚动位置,并强制滚动到顶部,请使用伪哈希标签:#top,例如 href=" #/home#top "。

或者,如果您更喜欢始终滚动到顶部,请使用内置的 ng-view自动滚动选项:

<div ng-view autoscroll></div>
于 2014-08-01T05:37:21.560 回答
9

我使用@Joseph Oster 的解决方案来创建指令。我还冒昧地更新了使用的答案:

  • $locationChangeStart
  • $locationChangeSuccess

因为其他事件已过时。

小提琴在这里:http: //jsfiddle.net/empie/p5pn3rvL/

指令来源:

angular.module('myapp', ['ngRoute'])
    .directive('autoScroll', function ($document, $timeout, $location) {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            scope.okSaveScroll = true;

            scope.scrollPos = {};

            $document.bind('scroll', function () {
                if (scope.okSaveScroll) {
                    scope.scrollPos[$location.path()] = $(window).scrollTop();
                }
            });

            scope.scrollClear = function (path) {
                scope.scrollPos[path] = 0;
            };

            scope.$on('$locationChangeSuccess', function (route) {
                $timeout(function () {
                    $(window).scrollTop(scope.scrollPos[$location.path()] ? scope.scrollPos[$location.path()] : 0);
                    scope.okSaveScroll = true;
                }, 0);
            });

            scope.$on('$locationChangeStart', function (event) {
                scope.okSaveScroll = false;
            });
        }
    };
})
于 2014-10-07T20:40:20.957 回答
6

我以前没用过,但是 angular 有一个$anchorScroll服务。至于重新加载数据,您可以使用$cacheFactory对其进行缓存,或者将数据存储在更高的范围内。

于 2013-01-01T11:45:27.710 回答
5

我创建了一个适用于窗口滚动的指令(它可以更新为适用于任何元素)

html用法

<div ng-keep-scroll="service.scrollY">
<!-- list of scrolling things here -->
</div>

其中“service.scrollY”必须是服务中的变量。服务保留它们的状态和值,每次加载和清除它们的值时都会重新创建控制器,因此您不能使用它们来存储持久数据。控制器有一个指向服务的范围变量。

指令js

app.directive('ngKeepScroll', function ($timeout) {
    return function (scope, element, attrs) {

        //load scroll position after everything has rendered
        $timeout(function () {
            var scrollY = parseInt(scope.$eval(attrs.ngKeepScroll));
            $(window).scrollTop(scrollY ? scrollY : 0);
        }, 0);

        //save scroll position on change
        scope.$on("$routeChangeStart", function () {
            scope.$eval(attrs.ngKeepScroll + " = " + $(window).scrollTop());
        });
    }
});
于 2013-05-12T00:48:57.397 回答
3

基于 br2000 的出色回答,我更新了指令代码以使用 ui-router。对于名称相同但参数不同的状态,我序列化 $state.params 对象以构成对象中的唯一键scrollPosCache

.directive("keepScrollPos", function($state, $window, $timeout, $location, $anchorScroll) {

    // cache scroll position of each route's templateUrl
    var scrollPosCache = {};

    // compile function
    return function(scope, element, attrs) {

      scope.$on('$stateChangeStart', function() {
        // store scroll position for the current view
        if ($state.current.name) {
          scrollPosCache[$state.current.name + JSON.stringify($state.params)] = [ $window.pageXOffset, $window.pageYOffset ];
        }
      });

      scope.$on('$stateChangeSuccess', function() {
        // if hash is specified explicitly, it trumps previously stored scroll position
        if ($location.hash()) {
          $anchorScroll();

          // else get previous scroll position; if none, scroll to the top of the page
        } else {
          var prevScrollPos = scrollPosCache[$state.current.name + JSON.stringify($state.params)] || [ 0, 0 ];
          $timeout(function() {
            $window.scrollTo(prevScrollPos[0], prevScrollPos[1]);
          }, 0);
        }
      });
    }
  })
于 2015-03-13T08:29:51.333 回答
1

如果您的页面需要获取数据以显示,您可能必须使用 $routeChangeSuccess 并延迟滚动函数调用。

    scope.$on("$routeChangeSuccess", function() {
        $timeout(function () {
            var scrollY = parseInt(scope.$eval(attrs.ngKeepScroll));
            $(window).scrollTop(scrollY ? scrollY : 0);
        }, 1000); // delay by 1 sec
    });
于 2013-09-25T13:20:03.110 回答
1

我制作了一个适用于任何溢出元素的版本,而不仅仅是文档正文:

.directive("keepScrollPos", function($route, $timeout, $location, $anchorScroll) {

  // cache scroll position of each route's templateUrl
  var cache = {};

  return {
    restrict : 'A',
    link: function($scope, elements, attrs){

      $scope.$on('$routeChangeStart', function() {

        // store scroll position for the current view
        if($route.current)
          cache[$route.current.loadedTemplateUrl + ':' + attrs.keepScrollPos] = [elements[0].scrollLeft, elements[0].scrollTop];              

      });

      $scope.$on('$routeChangeSuccess', function(){
        // if hash is specified explicitly, it trumps previously stored scroll position
        if($location.hash()){
          $anchorScroll();
          return;
        }

        // else get previous scroll position and apply it if it exists
        var pos = cache[$route.current.loadedTemplateUrl + ':' + attrs.keepScrollPos];
        if(!pos)
          return;

        $timeout(function(){                  
          elements[0].scrollLeft = pos[0];
          elements[0].scrollTop = pos[1];            
        }, 0);

      });

    }
  }

})

像这样使用它:

<div keep-scroll-pos="some-identifier"> ... </div>
于 2017-04-30T14:18:04.703 回答
0

我找到了另一种解决此问题的简单方法:

var scrollValue = $(window).scrollTop();

$rootScope.$on("$routeChangeStart", function() {
    scrollValue = $(window).scrollTop();
});

$rootScope.$on('$routeChangeSuccess', function(newRoute, oldRoute) {
    setTimeout(function() { $(window).scrollTop(scrollValue); }, 0);
});

只需将其放入 .run() 中即可。

这样,将超时值设置为 0 仍然有效,但在页面渲染后运行(没有超时功能它在内容(即模板或数据加载)渲染之前运行,使该功能无用。

如果您从某些 API 获取数据,您可以将超时包装在 $rootScope 中的函数中,并在成功请求后运行它。

于 2014-04-28T10:18:10.603 回答
0

您需要在每次路线更改时重置滚动位置。在你的主 AppController 中使用它:

  $scope.$on("$routeChangeSuccess", function () {
    $anchorScroll();
  });

或者,如果您使用的是 ui-route:

  $scope.$on("$stateChangeSuccess", function () {
    $anchorScroll();
  });

有关更多信息,请参阅在 AngularJS 中,如何在 URL 哈希上添加 $watch?

于 2014-10-13T14:51:57.497 回答
0

这可能会解决您的问题,它对我有用 $httpProvider.defaults.cache = true;

于 2015-11-17T12:29:23.410 回答
0

与其他答案不同,我想记住的不仅仅是卷轴,即inputfield values。

不仅如此,很多人还以为

  • 您只想记住一个滚动元素(也许您有窗格或其他类似应用程序的显示),
  • 你有body你的滚动元素(例如,如果你使用角度捕捉怎么办?),
  • 或者您的滚动元素不会被 angular 替换(即它在 之外ng-view)。
<body> <!-- doesn't scroll -->
    <div snap-drawers>..</div>

    <div snap-content="" history="scrollTop"> <!-- the scrolling div-->
        <header>...</header>

        <div ng-view>
            <input name="email" history="val"> <!-- tag with value we want remembered -->

            <div history="scrollLeft" history-watch="scroll" id="evenHorizontalScroll"><!--
                custom value we want remembered.
                NB it must have an id to be identified after angular
                removes it from the DOM between views,
                and since it is not a recognised default we can tell my
                directive the jquery event function what to watch
            --></div>
        </div>
    </div>
</body>

我已经编写了一个 [n 不幸的是更长的] 共享范围指令来处理这些问题。

.directive('history', function($compile, $rootScope, $location) {
    return {
        restrict : 'A',
        replace : false,
        scope : false,

        controller : function($scope, $timeout) {
            //holds all the visited views
            var states = new Object();
            //the current view
            var state = null;
            //how many names have been generated where the element itself was used
            var generated = 0;

            //logs events if allowed
            function debug(from) {
                //comment this to watch it working
                //return;

                console.log('StateHistory: ' + from);
                if (from == 'went')
                    console.log(state);
            }

            //applies the remembered state
            function apply() {
                var element;
                //for each item remembered in the state
                for (var query in state) {
                    //use the element directly, otherwise search for it
                    (state[query].element || $(query))
                        //use the appropriate function
                        [state[query].property](
                            //and set the value
                            state[query].value
                        )
                    ;
                    debug('applying:' + query + ':' + state[query].value);
                }

                //start recording what the user does from this point onward
                $scope.ignore = false;
            }

            //generates a reference we can use as a map key
            $scope.generateRef = function() {
                return '' + (++generated);
            };

            //views changed
            $scope.went = function() {
                debug('went');

                //set the current state
                state = states[$location.path()];

                //if we dont remember the state of the page for this view
                if (!state)
                    //get recording!
                    state = states[$location.path()] = new Object();

                //apply the state after other directives
                //(like anchorScroll + autoscroll) have done their thing
                $timeout(apply);
            };

            //one of the elements we're watching has changed
            $scope.changed = function(name, element, property, useObject) {
                //if we're not meant to be watching right now
                //i.e. if the user isnt the one changing it
                if ($scope.ignore) {
                    debug('ignored');
                    return;
                }

                //if we havent recorded anything for this here yet
                if (!state[name]) {
                    //start recording
                    state[name] = {property:property};

                    //and remember to leave behind a reference if the name isn't
                    //good enough (was generated)
                    if (useObject)
                        state[name].element = element;
                }

                //use the requested function to pull the value
                state[name].value = element[property]();

                debug('changed:' + name + ':' + state[name].value);
            };

            //initial view
            $scope.went();

            //subsequent views
            $rootScope.$on('$routeChangeSuccess', $scope.went);
            $rootScope.$on('$routeChangeError', $scope.went);

            $rootScope.$on('$routeChangeStart', function() {
                debug('ignoring');
                $scope.ignore = true;
            });
        },

        link: function (scope, element, attrs) {
            //jquery event function name
            var watch = attrs.historyWatch;
            //if not set, use these defaults
            if (!watch) {
                switch (attrs.history) {
                case 'val':
                    watch = 'change';
                    break;
                case 'scrollTop':
                    watch = 'scroll';
                    break;
                default:
                    watch = attrs.history;
                }
            }

            //the css selector to re-find the element on view change
            var query = null;
            //the reference to the state remembered
            var name;

            //try using the id
            if (attrs.id)
                name = query = '#' + attrs.id;
            //try using the form name
            else if (attrs.name)
                name = query = '[name=' + attrs.name + ']';
            //otherwise we'll need to just reference the element directly
            //NB should only be used for elements not swapped out by angular on view change,
            //ie nothing within the view. Eg the view itself, to remember scrolling?
            else
                name = scope.generateRef();

            //jquery value function name
            var property = attrs.history;

            //watch this element from here on out
            element.on(watch, function() {
                scope.changed(name, element, property, !query);
            });
        }
    };
})
于 2016-02-12T01:32:20.147 回答
0

我在我的项目中使用自定义解决方案。

第一步:获取列表中点击的位置,并保存在本地存储中。

var position = document.body.scrollTop;
localStorage.setItem("scrollPosition",position);

第 2 步:在详细视图中将全局变量 backFromDetailView 设置为 true。

backFromDetailView = true;

第 3 步:从详细视图页面返回到列表时。所有内容再次从服务器重新加载到滚动位置。

为此,使用以下行在 html 中绑定一个函数:

控制器包含以下功能:

$scope.goto = function (){
    if(backFromDetailView){
         window.scrollTo(0, localStorage.getItem("scrollPosition"));
     }
}

这种技术的一些缺点:

  1. 包括附加内容在内的所有内容都会重新加载。

  2. 在 iOS 中,在滚动到适当位置之前会出现黑屏。

于 2016-06-25T14:25:21.940 回答
0

@br2000 的绝佳解决方案。

然而不幸的是,当指令试图恢复位置时,我滚动回的页面仍在将数据从后端加载到长列表中。

所以很明显它无法恢复滚动位置。我通过使用$interval而不是解决$timeout了它,并用 . 重复了 20 次300ms timeout。我存储了从返回的承诺$interval,然后在$interval函数内部检查当前位置现在是否与存储的位置相同,如果是,我调用一个范围方法来取消$interval - $interval.cancel(promise).

此外,最初 mypageYOffsetpageXOffset始终为 0,因为overflow-x: hidden应用于div. DOM我通过将根包裹div在另一个里面来解决它,div然后我在上面放置了这个指令。

于 2016-12-14T14:03:21.197 回答
0

对于那些接受 emp 的回答但使用 angular ui-router >= version 1.0.0(当前为 1.0.3)的人,请参阅使用 ui-router 新转换重写的指令。

HTML

<div ui-view keep-scroll-pos></div>

角度指令

angular.module("app")
    .directive("keepScrollPos", function($transitions, $state, $window, $timeout, $location, $anchorScroll) {

        // cache scroll position of each state's templateUrl
        var scrollPosCache = {};

        return {
            link: function(scope, element, attrs) {


                $transitions.onStart({ }, function( trans ) {

                    // store scroll position for the current view
                    if (trans.from().name) {
                        scrollPosCache[trans.from().templateUrl] = [ $window.pageXOffset, $window.pageYOffset ];
                    }

                    trans.promise.finally(function () {


                        // if hash is specified explicitly, it trumps previously stored scroll position
                        if ($location.hash()) {
                            $anchorScroll();

                        // else get previous scroll position; if none, scroll to the top of the page
                        } else {
                            var prevScrollPos = scrollPosCache[trans.to().templateUrl] || [ 0, 0 ];
                            $timeout(function() {
                                $window.scrollTo(prevScrollPos[0], prevScrollPos[1]);
                            }, 200);
                        }
                    });
                });
            }
        }
    });
于 2018-03-07T12:21:21.027 回答