Reputation:
Sample app: http://angular.github.com/angular-phonecat/step-11/app/#/phones
If you choose the last phone "Motorola charm" it will show you the details of the phone. When you navigate back with on your browser it reloads the data and scrolling is at the top.
What is the best way to automatically scroll to where is was left when navigatin back? And also, why does angular reloads the data?
I have the same "angular-phonecat" sample on my computer and I have added an infinite scroll which loads more data as you scroll. So I really dont want the user to reload 50+ items again or scrolling down for 30 seconds.
Upvotes: 48
Views: 52936
Reputation: 6512
For those of you that went with emp's answer, but were using angular ui-router >= version 1.0.0 (current 1.0.3), please see his directive rewritten, using ui-routers new transitions.
HTML
<div ui-view keep-scroll-pos></div>
Angular Directive
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);
}
});
});
}
}
});
Upvotes: 0
Reputation: 16719
I made a version that works with any overflowed element, not just the document body:
.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);
});
}
}
})
Use it like:
<div keep-scroll-pos="some-identifier"> ... </div>
Upvotes: 1
Reputation: 442
Great solution by @br2000.
However unfortunately my page that I was scrolling back to, was still loading data from backend to a long list when the directive tried to restore the position.
So obviously it failed to restore the scroll position. I solved it by using $interval
instead of $timeout
and gave it 20 repetitions with 300ms timeout
. I stored the promise returned from $interval
and then checked inside the $interval
function if current position is now the same as stored position and if yes, I call a scope method that cancels the $interval - $interval.cancel(promise).
Additionally, initially my pageYOffset
and pageXOffset
were always 0, because overflow-x: hidden
was applied to the root div
in the DOM
. I solved it by wrapping the root div
inside another div
on which I then placed this directive.
Upvotes: 0
Reputation: 126
I am using a custom solution in my project.
Step 1: Obtain the position of the click on the list and save it in local storage.
var position = document.body.scrollTop;
localStorage.setItem("scrollPosition",position);
Step 2:In the detail view set a global variable backFromDetailView to true.
backFromDetailView = true;
Step 3:On returning from the detail view page to the list. All the content is reloaded from server again up to the scrolled position.
For this purpose bind a function in the html using the following line:
And the controller contains the function:
$scope.goto = function (){
if(backFromDetailView){
window.scrollTo(0, localStorage.getItem("scrollPosition"));
}
}
Some disadvantages of this technique:
All the content including additional content is reloaded again.
In iOS, a black screen appear before scrolling to the appropriate position.
Upvotes: 0
Reputation: 13023
Unlike the other answers I wanted to remember more than just scrolls, namely input
field value
s.
Not only that, but a lot of them assumed
body
as your scrolling element (e.g. what if you're using angular snap?),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>
I've written a[n unfortunately much longer] shared-scope directive that takes care of these problems.
.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);
});
}
};
})
Upvotes: 0
Reputation: 920
this may solve your problem, it works for me
$httpProvider.defaults.cache = true;
Upvotes: 0
Reputation: 133
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);
}
});
}
})
Upvotes: 3
Reputation: 507
You need to reset the scroll position on each route change. Use this in your main AppController:
$scope.$on("$routeChangeSuccess", function () {
$anchorScroll();
});
Or if you are using ui-route:
$scope.$on("$stateChangeSuccess", function () {
$anchorScroll();
});
For more infomation see In AngularJS, how do I add a $watch on the URL hash?
Upvotes: 0
Reputation: 5065
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:
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;
});
}
};
})
Upvotes: 9
Reputation: 969
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>
Upvotes: 17
Reputation: 469
I've found another simple way to solve this issue:
var scrollValue = $(window).scrollTop();
$rootScope.$on("$routeChangeStart", function() {
scrollValue = $(window).scrollTop();
});
$rootScope.$on('$routeChangeSuccess', function(newRoute, oldRoute) {
setTimeout(function() { $(window).scrollTop(scrollValue); }, 0);
});
Just put it in .run().
This way, setting timeout value to 0 it still works, but runs after the page is rendered (without timeout function it runs before the content (i.e. template or data loading) is rendered, making the function useless.
If you fetch data from some API, you can wrap the timeout in a function in $rootScope and run it after successful request.
Upvotes: 0
Reputation: 341
If your page requires fetching of data to display, you may have to use $routeChangeSuccess and delay the scrolling function call.
scope.$on("$routeChangeSuccess", function() {
$timeout(function () {
var scrollY = parseInt(scope.$eval(attrs.ngKeepScroll));
$(window).scrollTop(scrollY ? scrollY : 0);
}, 1000); // delay by 1 sec
});
Upvotes: 1
Reputation: 7719
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());
});
}
});
Upvotes: 5
Reputation: 5545
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...
$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'.
Upvotes: 31
Reputation: 35829
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.
Upvotes: 6