web-dev-qa-db-fra.com

Conserver la position du parchemin sur le changement de route dans AngularJS?

Exemple d'application: http://angular.github.com/angular-phonecat/step-11/app/#/phones

Si vous choisissez le dernier téléphone "Motorola charm", il vous montrera les détails du téléphone. Lorsque vous revenez dans votre navigateur, il recharge les données et le défilement est en haut. 

Quel est le meilleur moyen de faire automatiquement défiler jusqu’à l’endroit où il en restait lors de la navigation retour? .__ Et aussi, pourquoi angular recharge-t-il les données? 

J'ai le même échantillon "angular-phonecat" sur mon ordinateur et j'ai ajouté un défilement infini qui charge plus de données à mesure que vous faites défiler. Donc, je ne veux vraiment pas que l'utilisateur recharge à nouveau plus de 50 articles ou fasse défiler l'écran pendant 30 secondes.

47
user1012032

Je ne l'ai pas utilisé auparavant, mais angular a un service $ anchorScroll . En ce qui concerne le rechargement des données, vous pouvez le mettre en cache avec $ cacheFactory ou stocker les données sur une portée supérieure.

6
asgoth

J'ai un violon ici qui montre comment restaurer la position de défilement dans la vue liste après une vue de détail; pas encore encapsulé dans une directive, travaillant dessus ...

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

Le violon montre également qu'il faut aller chercher la liste une fois, en dehors de 'ListCtrl'.

31
Joseph Oster

Vous trouverez ci-dessous une autre version de la directive keep-scroll-pos. Cette version

  • Se souvient de la position de défilement de chaque templateUrl de votre définition $ routeProvider.

  • Respecte les balises de hachage, par exemple, #/home # section-2 , passera à # section-2 / pas la position de défilement précédente.

  • Il est facile à utiliser car il est autonome et stocke les positions de défilement en interne.

Exemple d'utilisation de HTML:

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

Le code de la directive keepScrollPos est ci-dessous:

 "use strict"; 

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

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

 // cache position de défilement du templateUrl 
 var. scrollPosCache de chaque route = {}; 

 // fonction de compilation 
 fonction de retour (portée, élément, attrs) {

 scope. $ on ('$ routeChangeStart', function () {
 // stocke la position de défilement de la vue actuelle 
 if ($ route.current) {
 scrollPosCache [$ route.current.loadedTemplateUrl] = [$ window.pageXOffset, $ window.pageYOffset]; 
} 
}). () {
 // si le hachage est spécifié explicitement, il remplace la position de défilement précédemment enregistrée .__ if ($ location.hash ()) {
 $ anchorScroll (); 

 // else obtenir la position de défilement précédente, sinon, défiler vers le haut de la page 
} else {
 var prevScrollPos = scrollPosCache [$ route.current.loadedTemplateUrl] || [0, 0]; 
 $ timeout (function () {
 $ window.scrollTo (prevScrollPos [0], prevScrollPos [1]); 
}, 0); 
 } 
 }); 
 } 
}); 

Pour ignorer la position de défilement précédemment stockée et pour forcer le défilement vers le haut, utilisez la pseudo balise de hachage: #top , par exemple, href = " #/home # top ".

Sinon, si vous préférez toujours faire défiler l'écran jusqu'en haut, utilisez la commande intégrée ng-view autoscroll :

<div ng-view autoscroll></div>
16
br2000

J'ai utilisé la solution de @ Joseph Oster afin de créer une directive .

  • $ locationChangeStart 
  • $ locationChangeSuccess

comme les autres événements sont obsolètes.

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

Source de la directive:

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

j'ai créé une directive qui fonctionne sur le défilement de la fenêtre (elle pourrait être mise à jour pour fonctionner sur n'importe quel élément)

utilisation du html

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

où "service.scrollY" DOIT être une variable dans un service. Les services conservent leur état et leurs valeurs, les contrôleurs sont recréés à chaque chargement et effacent leurs valeurs afin que vous ne puissiez pas les utiliser pour stocker des données persistantes. le contrôleur a une variable d’étendue pointant vers le 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());
        });
    }
});
5
Anton

Basé sur l'excellente réponse de br2000, j'ai mis à jour le code de la directive pour qu'il fonctionne avec ui-router. Pour les états ayant le même nom mais des paramètres différents, je sérialise l'objet $ state.params pour créer une clé unique dans l'objet 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);
        }
      });
    }
  })
3
Radu

J'ai créé une version qui fonctionne avec n'importe quel élément débordé, pas seulement le corps du document:

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

      });

    }
  }

})

Utilisez-le comme:

<div keep-scroll-pos="some-identifier"> ... </div>
1
nice ass

Si votre page nécessite l'extraction de données à afficher, vous devrez peut-être utiliser $ routeChangeSuccess et retarder l'appel de fonction de défilement.

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

cela peut résoudre votre problème, cela fonctionne pour moi $httpProvider.defaults.cache = true; 

0
Thilak Raj

Pour ceux d'entre vous qui sont allés avec la réponse emp, mais utilisaient un routeur angulaire angulaire> = version 1.0.0 (version 1.0.3 actuelle), veuillez voir sa directive réécrite, en utilisant les nouvelles transitions des routeurs ui. 

HTML

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

Directive angulaire

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);
                        }
                    });
                });
            }
        }
    });
0
Lee Brindley

J'utilise une solution personnalisée dans mon projet.

Étape 1: Obtenez la position du clic sur la liste et enregistrez-la dans la mémoire de stockage locale.

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

Étape 2: dans la vue détaillée, définissez la variable globale backFromDetailView sur true.

backFromDetailView = true;

Étape 3: En revenant de la page de vue détaillée vers la liste. Tout le contenu est rechargé à partir du serveur à la position défilée. 

Pour cela, liez une fonction dans le code HTML à l'aide de la ligne suivante:

Et le contrôleur contient la fonction:

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

Quelques inconvénients de cette technique:

  1. Tout le contenu, y compris le contenu supplémentaire, est rechargé à nouveau. 

  2. Sous iOS, un écran noir apparaît avant le défilement vers la position appropriée

0
Akash Pal

J'ai trouvé un autre moyen simple de résoudre ce problème:

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

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

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

Il suffit de le mettre dans .run ().

De cette façon, définir la valeur de délai d’attente sur 0 fonctionne toujours, mais s’exécute après le rendu de la page (sans la fonction de délai d’expiration, elle est exécutée avant le contenu (modèle ou chargement de données), ce qui rend la fonction inutile.

Si vous récupérez des données à partir d'une API, vous pouvez envelopper le délai dans une fonction de $ rootScope et l'exécuter après une requête réussie.

0
Grzegorz Judas

Vous devez réinitialiser la position de défilement à chaque changement d’itinéraire . Utilisez ceci dans votre AppController principal:

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

Ou si vous utilisez ui-route:

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

Pour plus d'informations, voir Dans AngularJS, comment puis-je ajouter une surveillance $ sur le hachage de l'URL?

0
JumpLink

Contrairement aux autres réponses, je voulais retenir plus que des rouleaux, à savoir input field values.

Non seulement cela, mais beaucoup d'entre eux ont supposé

  • vous vouliez seulement vous souvenir d'un élément de défilement (vous avez peut-être des panneaux ou un autre affichage semblable à une application),
  • vous avez body comme élément de défilement (par exemple, si vous utilisez capture angulaire ?),
  • ou votre élément de défilement n’est pas remplacé par angular (c’est-à-dire qu’il se trouve en dehors du 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>

J'ai écrit une directive [malheureusement beaucoup plus longue] de portée partagée qui prend en charge ces problèmes.

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

Excellente solution de @ br2000. 

Cependant, malheureusement, la page sur laquelle je revenais chargeait toujours les données du backend vers une longue liste lorsque la directive tentait de restaurer la position. 

Donc, évidemment, la position de défilement n'a pas été restaurée. Je l'ai résolu en utilisant $interval au lieu de $timeout et lui ai donné 20 répétitions avec 300ms timeout. J'ai stocké la promesse retournée de $interval et ensuite vérifié dans la fonction $interval si la position actuelle est maintenant identique à la position stockée et si oui, j'appelle une méthode de portée qui annule la fonction $interval - $interval.cancel(promise)..

De plus, mes pageYOffset et pageXOffset étaient toujours à 0, car overflow-x: hidden a été appliqué à la racine div dans la DOM. Je l'ai résolu en enveloppant la racine div dans une autre div sur laquelle j'ai ensuite placé cette directive.

0
Tadej Krevh