【问题标题】:Retain scroll position on route change in AngularJS?在AngularJS中保留路由更改的滚动位置?
【发布时间】:2012-12-15 23:06:56
【问题描述】:

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

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

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

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

【问题讨论】:

标签: javascript angularjs


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

    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”之外获取列表一次。

    【讨论】:

    • 正是我要找的那个。谢谢。
    • 您是否最终将其封装在指令中?
    • 我有类似的东西,但是发现页面滚动到顶部时有闪烁,然后我们将其滚动回底部。您找到避免这种情况的方法了吗?
    • @JPOD 请查看我对此代码指令的回答。 stackoverflow.com/questions/14107531/…
    • Angular 应用应该使用 $window 而不是 $(window) 因为 Angular 应用不应该使用 jQuery(除了内置的 lite 版本)。
    【解决方案3】:

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

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

    【讨论】:

    • 服务示例:mod.factory("CacheService", function($cacheFactory) { return { data:{ scrollY: 0 } }; });然后在控制器中: $scope.service = CacheService.data
    【解决方案4】:

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

        scope.$on("$routeChangeSuccess", function() {
            $timeout(function () {
                var scrollY = parseInt(scope.$eval(attrs.ngKeepScroll));
                $(window).scrollTop(scrollY ? scrollY : 0);
            }, 1000); // delay by 1 sec
        });
    

    【讨论】:

      【解决方案5】:

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

      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 中的一个函数中,并在成功请求后运行它。

      【讨论】:

        【解决方案6】:

        下面是 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 autoscroll 选项:

        <div ng-view autoscroll></div>
        

        【讨论】:

        • 这个解决方案最适合我。我使用 ui 路由器进行路由,所以我用 $state 替换了所有出现的 $route。否则,不需要其他更改。我也喜欢如何使用 $window 和 $anchorScroll 而不是在其他一些建议的答案中使用的 jquery 依赖 $(window)。
        【解决方案7】:

        我使用@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;
                    });
                }
            };
        })
        

        【讨论】:

        • 指令不错,但我想知道是否可以在ui路由器中使用?
        • 如果控制器/路由发生变化,此解决方案将不起作用
        • 它在小提琴上工作得很好......你希望看到什么,你在尝试什么?
        • @hariszaman 也许您遇到了这个非常好但简洁的解决方案的限制之一。试试this。尽管如果由于您使用 ui-router 而无法正常工作,您可能必须将对 $routeChangeX 的调用换成等价物
        • 如果 fiddle 没有显示好友列表或表格,可能是因为当 fiddle 是 HTTPS 时,您的浏览器阻止了通过 HTTP 对 angular-route JS 文件的调用。只需删除 angular-route.js 的“外部资源”并使用 HTTPS 前缀将其添加回来。
        【解决方案8】:

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

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

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

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

        欲了解更多信息,请参阅In AngularJS, how do I add a $watch on the URL hash?

        【讨论】:

          【解决方案9】:

          根据 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);
                  }
                });
              }
            })
          

          【讨论】:

          • 如果您只关心当前状态,您可能需要在此处删除 scrollPosCache 中的参数缓存。
          【解决方案10】:

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

          【讨论】:

            【解决方案11】:

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

            不仅如此,他们中的很多人都认为

            • 您只想记住一个滚动元素(也许您有窗格或其他类似应用的显示),
            • 您将body 作为滚动元素(例如,如果您使用angular snap 会怎样?),
            • 或者您的滚动元素没有被 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);
                        });
                    }
                };
            })
            

            【讨论】:

              【解决方案12】:

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

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

              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 中,在滚动到相应位置之前会出现黑屏 位置。

              【讨论】:

                【解决方案13】:

                @br2000 的出色解决方案。

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

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

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

                【讨论】:

                  【解决方案14】:

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

                  .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>
                  

                  【讨论】:

                    【解决方案15】:

                    对于那些接受 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);
                                            }
                                        });
                                    });
                                }
                            }
                        });
                    

                    【讨论】:

                      猜你喜欢
                      • 2014-07-07
                      • 2017-01-23
                      • 1970-01-01
                      • 2021-07-08
                      • 1970-01-01
                      • 2020-04-23
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多