Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retain scroll position on route change in AngularJS?

People also ask

How can I retain the scroll position of a scrollable area when pressing back button?

You can try $("div#element"). load(function() {}) and place the element outside the document ready handler. Also, if a user scroll down the page, then leave the page, and go back to the page again, it should be a new start for him so it should start at the top. But now it also remembers the previous position.

How to get scroll position in AngularJS?

on('scroll') and $scope. $apply to update a scope element on scroll. You'll receive more granularity on the scroll position because it's responding to the native scroll event, but you're going to be thrashing the angular scope by $applying that every time the handler fires.


I have a fiddle here that shows how to restore scroll position in the list view after a detail view; not encapsulated in a directive yet, working on that...

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

The fiddle also shows fetching the list once, outside of 'ListCtrl'.


Below is another version of keep-scroll-pos directive. This version

  • Remembers scroll position of each templateUrl of your $routeProvider definition.

  • Respects hash tags, e.g., #/home#section-2, will scroll to #section-2 not previous scroll position.

  • Is easy to use, as it is self-contained, and stores scroll positions internally.

Example of html use:

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

The code for keepScrollPos directive is below:

"use strict";

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

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

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

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

        scope.$on('$routeChangeStart', function() {
            // store scroll position for the current view
            if ($route.current) {
                scrollPosCache[$route.current.loadedTemplateUrl] = [ $window.pageXOffset, $window.pageYOffset ];
            }
        });

        scope.$on('$routeChangeSuccess', 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[$route.current.loadedTemplateUrl] || [ 0, 0 ];
                $timeout(function() {
                    $window.scrollTo(prevScrollPos[0], prevScrollPos[1]);
                }, 0);
            }
        });
    }
});

To disregard previously stored scroll position, and to force to scroll to the top, use pseudo hash tag: #top, e.g., href="#/home#top".

Alternatively, if you prefer to just always scroll to the top, use built-in ng-view autoscroll option:

<div ng-view autoscroll></div>

I have used the solution of @Joseph Oster in order to create a directive. I have also taken the liberty to update the answer to use:

  • $locationChangeStart
  • $locationChangeSuccess

as the other events are obsolete.

Fiddle is here: http://jsfiddle.net/empie/p5pn3rvL/

Directive source:

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

I haven't used it before, but angular has a $anchorScroll service. As to reloading the data, you could cache it using $cacheFactory, or store the data on a higher scope.


i created a directive that works on the window scroll ( it could updated to work on any element though )

html usage

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

where "service.scrollY" MUST be a variable within a service. Services retain their state and values, controllers are recreated every time they load and clear their values so you cant use them to store persistent data. the controller has a scope variable pointing to the service.

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

Based on the great answer from br2000, I updated the directive code to work with ui-router. For states with same name but different params I serialize the $state.params object to make up a unique key in the scrollPosCache object.

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