javascript - node - ¿Retener la posición de desplazamiento en el cambio de ruta en AngularJS?
descargar angular (15)
Ejemplo de aplicación: http://angular.github.com/angular-phonecat/step-11/app/#/phones
Si elige el último teléfono "Motorola charm" le mostrará los detalles del teléfono. Cuando navegas hacia atrás en tu navegador, vuelve a cargar los datos y el desplazamiento está en la parte superior.
¿Cuál es la mejor manera de desplazarse automáticamente hacia donde se dejó cuando se navegó de nuevo? Y también, ¿por qué angular recarga los datos?
Tengo la misma muestra de "angular-phonecat" en mi computadora y he agregado un desplazamiento infinito que carga más datos a medida que se desplaza. Así que realmente no quiero que el usuario vuelva a cargar más de 50 elementos o que se desplace hacia abajo durante 30 segundos.
A continuación se muestra otra versión de la directiva keep-scroll-pos. Esta versión
Recuerda la posición de desplazamiento de cada templateUrl de la definición de $ routeProvider.
Respeta las etiquetas hash, por ejemplo, # / home # section-2 , se desplazará a # section-2 no a la posición de desplazamiento anterior.
Es fácil de usar, ya que es autónomo y almacena las posiciones de desplazamiento internamente.
Ejemplo de uso de html:
<div ng-view keep-scroll-pos></div>
El código para la directiva keepScrollPos está abajo:
"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); } }); } });
Para ignorar la posición de desplazamiento previamente almacenada y forzar para desplazarse hacia la parte superior, use la etiqueta pseudohash : #top , por ejemplo, href = " # / home # top ".
Alternativamente, si prefiere desplazarse siempre hacia la parte superior, use la opción de desplazamiento automático ng-view incorporada :
<div ng-view autoscroll></div>
A diferencia de las otras respuestas, quería recordar algo más que solo desplazamientos, es decir, value
campo de input
s.
No solo eso, sino que muchos de ellos asumieron.
- solo quería recordar un elemento de desplazamiento (tal vez tenga paneles o alguna otra pantalla similar a una aplicación),
- tiene el
body
como elemento de desplazamiento (p. ej., ¿qué sucede si utiliza un ajuste angular ?), - o su elemento de desplazamiento no se reemplaza por angular (es decir, está fuera de la
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>
He escrito una directiva de alcance compartido [desafortunadamente mucho más larga] que se ocupa de estos problemas.
.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);
});
}
};
})
Basándome en la gran respuesta de br2000, actualicé el código de la directiva para trabajar con ui-router. Para estados con el mismo nombre pero diferentes parámetros, serializo el objeto $ state.params para crear una clave única en el objeto 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);
}
});
}
})
Creé una directiva que funciona en el desplazamiento de la ventana (aunque podría actualizarse para trabajar en cualquier elemento)
uso de html
<div ng-keep-scroll="service.scrollY">
<!-- list of scrolling things here -->
</div>
donde "service.scrollY" DEBE ser una variable dentro de un servicio. Los servicios conservan su estado y valores, los controladores se recrean cada vez que se cargan y borran sus valores, por lo que no puede usarlos para almacenar datos persistentes. El controlador tiene una variable de alcance que apunta al servicio.
directiva 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());
});
}
});
Debe restablecer la posición de desplazamiento en cada cambio de ruta. Usa esto en tu AppController principal:
$scope.$on("$routeChangeSuccess", function () {
$anchorScroll();
});
O si estás usando ui-route:
$scope.$on("$stateChangeSuccess", function () {
$anchorScroll();
});
Para obtener más información, consulte En AngularJS, ¿cómo agrego un $ watch en el hash de URL?
Estoy usando una solución personalizada en mi proyecto.
Paso 1: Obtenga la posición del clic en la lista y guárdelo en el almacenamiento local.
var position = document.body.scrollTop;
localStorage.setItem("scrollPosition",position);
Paso 2: En la vista de detalles, establezca una variable global backFromDetailView en true.
backFromDetailView = true;
Paso 3: Al regresar de la página de vista detallada a la lista. Todo el contenido se vuelve a cargar desde el servidor hasta la posición desplazada.
Para este propósito, enlace una función en el html usando la siguiente línea:
Y el controlador contiene la función:
$scope.goto = function (){
if(backFromDetailView){
window.scrollTo(0, localStorage.getItem("scrollPosition"));
}
}
Algunas desventajas de esta técnica:
Todo el contenido, incluido el contenido adicional, se vuelve a cargar.
En iOS, aparece una pantalla en negro antes de desplazarse a la posición adecuada.
Gran solución por @ br2000.
Sin embargo, desafortunadamente, mi página a la que me estaba desplazando hacia atrás seguía cargando datos desde el backend a una larga lista cuando la directiva intentó restaurar la posición.
Así que obviamente no se pudo restaurar la posición de desplazamiento. Lo resolví usando $interval
lugar de $timeout
y le di 20 repeticiones con 300ms timeout
. Guardé la promesa devuelta de $interval
y luego verifiqué dentro de la función $interval
si la posición actual es ahora la misma que la posición almacenada y, en caso afirmativo, llamo un método de alcance que cancela $interval - $interval.cancel(promise).
Además, inicialmente mi pageYOffset
y pageXOffset
siempre fueron 0, porque overflow-x: hidden
se aplicó a la div
raíz en el DOM
. Lo resolví envolviendo el div
raíz dentro de otro div
en el que luego coloqué esta directiva.
He encontrado otra forma sencilla de resolver este problema:
var scrollValue = $(window).scrollTop();
$rootScope.$on("$routeChangeStart", function() {
scrollValue = $(window).scrollTop();
});
$rootScope.$on(''$routeChangeSuccess'', function(newRoute, oldRoute) {
setTimeout(function() { $(window).scrollTop(scrollValue); }, 0);
});
Solo póngalo en .run ().
De esta manera, establecer el valor de tiempo de espera en 0 todavía funciona, pero se ejecuta después de que se haya procesado la página (sin la función de tiempo de espera se ejecuta antes de que se represente el contenido (es decir, la carga de la plantilla o los datos), haciendo que la función sea inútil.
Si obtiene datos de alguna API, puede ajustar el tiempo de espera en una función en $ rootScope y ejecutarlo después de una solicitud exitosa.
He utilizado la solución de @Joseph Oster para crear una directiva. También me he tomado la libertad de actualizar la respuesta para usar:
- $ locationChangeStart
- $ locationChangeSuccess
Como los demás eventos son obsoletos.
Fiddle está aquí: http://jsfiddle.net/empie/p5pn3rvL/
Fuente de la directiva:
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;
});
}
};
})
Hice una versión que funciona con cualquier elemento desbordado, no solo con el cuerpo del documento:
.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);
});
}
}
})
Utilízalo como:
<div keep-scroll-pos="some-identifier"> ... </div>
No lo he usado antes, pero angular tiene un servicio $anchorScroll . Para recargar los datos, puede almacenar en caché usando $cacheFactory , o almacenar los datos en un ámbito superior.
Para aquellos de ustedes que fueron con la respuesta de emp, pero estaban usando ui-router angular> = versión 1.0.0 (1.0.3 actual), ver su directiva reescrita, usando las nuevas transiciones de ui-routers.
HTML
<div ui-view keep-scroll-pos></div>
Directiva angular
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);
}
});
});
}
}
});
Si su página requiere la obtención de datos para mostrar, es posible que tenga que usar $ routeChangeSuccess y retrasar la llamada de la función de desplazamiento.
scope.$on("$routeChangeSuccess", function() {
$timeout(function () {
var scrollY = parseInt(scope.$eval(attrs.ngKeepScroll));
$(window).scrollTop(scrollY ? scrollY : 0);
}, 1000); // delay by 1 sec
});
Tengo un violín aquí que muestra cómo restaurar la posición de desplazamiento en la vista de lista después de una vista detallada; Aún no encapsulado en una directiva, trabajando en eso ...
$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);
});
El violín también muestra cómo recuperar la lista una vez, fuera de ''ListCtrl''.
esto puede resolver su problema, me funciona $httpProvider.defaults.cache = true;