488 votes

Angular 2 Défilement vers le haut en cas de changement de route

Dans mon application Angular 2, lorsque je fais défiler une page et que je clique sur le lien situé en bas de la page, l'itinéraire est modifié et je passe à la page suivante, mais je n'arrive pas à faire défiler le haut de la page. En conséquence, si la première page est longue et que la deuxième page a peu de contenu, cela donne l'impression que la deuxième page n'a pas de contenu. En effet, le contenu n'est visible que si l'utilisateur fait défiler la page jusqu'en haut.

Je peux faire défiler la fenêtre vers le haut de la page dans le ngInit du composant, mais y a-t-il une meilleure solution pour gérer automatiquement toutes les routes de mon application ?

52 votes

Depuis la version 6.1 d'Angular, nous pouvons utiliser { scrollPositionRestoration : 'enabled' } sur des modules chargés rapidement ou simplement dans app.module, et cette fonction sera appliquée à toutes les routes. RouterModule.forRoot(appRoutes, { scrollPositionRestoration: 'enabled' })

0 votes

Merci beaucoup, votre solution a parfaitement fonctionné pour moi :)

0 votes

Pas une seule personne n'a mentionné le focus ? il est plus important que jamais de prendre en charge correctement l'accessibilité / les lecteurs d'écran et si vous faites simplement défiler la page vers le haut sans tenir compte du focus, la prochaine pression sur la touche de tabulation peut sauter au bas de l'écran.

594voto

Angular 6.1 et plus :

Angular 6.1 (publié le 2018-07-25) a ajouté un support intégré pour gérer ce problème, à travers une fonctionnalité appelée "Restauration de la position du défilement de la roulette". Comme décrit dans le document officiel Blog Angular il suffit de l'activer dans la configuration du routeur comme ceci :

RouterModule.forRoot(routes, {scrollPositionRestoration: 'enabled'})

En outre, le blog indique que "cela devrait devenir la valeur par défaut dans une prochaine version majeure". Jusqu'à présent, cela n'a pas été le cas (à partir d'Angular 11.0), mais à terme, vous n'aurez plus besoin de faire quoi que ce soit dans votre code, et cela fonctionnera correctement dès la sortie de la boîte.

Vous pouvez obtenir plus de détails sur cette fonctionnalité et sur la manière de personnaliser ce comportement dans les documents officiels .

Angular 6.0 et versions antérieures :

Bien que l'excellente réponse de @GuilhermeMeireles corrige le problème initial, elle en introduit un nouveau, en brisant le comportement normal que vous attendez lorsque vous naviguez en arrière ou en avant (avec les boutons du navigateur ou via Location dans le code). Le comportement attendu est que lorsque vous revenez à la page, elle doit rester défilée vers le bas au même endroit que lorsque vous avez cliqué sur le lien, mais le défilement vers le haut en arrivant à chaque page brise évidemment cette attente.

Le code ci-dessous étend la logique pour détecter ce type de navigation en souscrivant à la séquence PopStateEvent de Location et en ignorant la logique de défilement vers le haut si la page nouvellement arrivée est le résultat d'un tel événement.

Si la page à partir de laquelle vous naviguez est suffisamment longue pour couvrir l'ensemble du champ d'affichage, la position de défilement est restaurée automatiquement, mais comme @JordanNelson l'a correctement souligné, si la page est plus courte, vous devez garder la trace de la position de défilement y d'origine et la restaurer explicitement lorsque vous retournez à la page. La version mise à jour du code couvre également ce cas, en restaurant toujours explicitement la position de défilement.

import { Component, OnInit } from '@angular/core';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { Location, PopStateEvent } from "@angular/common";

@Component({
    selector: 'my-app',
    template: '<ng-content></ng-content>',
})
export class MyAppComponent implements OnInit {

    private lastPoppedUrl: string;
    private yScrollStack: number[] = [];

    constructor(private router: Router, private location: Location) { }

    ngOnInit() {
        this.location.subscribe((ev:PopStateEvent) => {
            this.lastPoppedUrl = ev.url;
        });
        this.router.events.subscribe((ev:any) => {
            if (ev instanceof NavigationStart) {
                if (ev.url != this.lastPoppedUrl)
                    this.yScrollStack.push(window.scrollY);
            } else if (ev instanceof NavigationEnd) {
                if (ev.url == this.lastPoppedUrl) {
                    this.lastPoppedUrl = undefined;
                    window.scrollTo(0, this.yScrollStack.pop());
                } else
                    window.scrollTo(0, 0);
            }
        });
    }
}

0 votes

Cela doit-il être placé dans app.component.ts ou dans chaque component.ts qui fait partie de mon fichier principal ? router-outlet ?

2 votes

Il doit être placé soit dans le composant de l'application directement, soit dans un composant unique utilisé dans celle-ci (et donc partagé par l'ensemble de l'application). Par exemple, je l'ai inclus dans un composant de la barre de navigation supérieure. Vous ne devez pas l'inclure dans tous vos composants.

0 votes

J'ai fini par mettre cela dans mon app.component. Question concernant "window", il y a beaucoup d'articles et de blogs recommandant d'utiliser window comme un service injectable (window service), quelque chose avec zone.js et "angular's consciousness" ? Devrais-je envelopper window autour d'un service ?

438voto

Guilherme Meireles Points 4815

Vous pouvez enregistrer un écouteur de changement d'itinéraire sur votre composant principal et faire défiler vers le haut les changements d'itinéraire.

import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';

@Component({
    selector: 'my-app',
    template: '<ng-content></ng-content>',
})
export class MyAppComponent implements OnInit {
    constructor(private router: Router) { }

    ngOnInit() {
        this.router.events.subscribe((evt) => {
            if (!(evt instanceof NavigationEnd)) {
                return;
            }
            window.scrollTo(0, 0)
        });
    }
}

0 votes

Merci beaucoup @Guilherme. Cette approche a-t-elle des conséquences sur les performances ? Puisque cet abonnement durera pendant toute la durée de vie de l'application.

0 votes

Il s'agit simplement d'un abonnement qui n'est déclenché que sur les événements de l'itinéraire. L'impact sur les performances devrait être très faible, voire nul. Veillez simplement à le placer uniquement dans le composant principal de l'application. Si vous décidez de l'utiliser ailleurs, désabonnez-vous aux événements lorsque le composant est détruit pour éviter les fuites.

0 votes

@Diego avez-vous apporté des modifications à la réponse ?

86voto

Abdul Rafay Points 1310

À partir d'Angular 6.1, vous pouvez maintenant éviter les tracas et passer extraOptions à votre RouterModule.forRoot() comme second paramètre et peut spécifier scrollPositionRestoration: enabled pour dire à Angular de faire défiler vers le haut chaque fois que la route change.

Par défaut, vous le trouverez dans app-routing.module.ts :

const routes: Routes = [
  {
    path: '...'
    component: ...
  },
  ...
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      scrollPositionRestoration: 'enabled', // Add options right here
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Documents officiels d'Angular

6 votes

Même si la réponse ci-dessus est plus descriptive, j'aime que cette réponse me dise exactement où cela doit aller.

36voto

mtpultz Points 403

Vous pouvez écrire ceci de manière plus succincte en tirant parti de l'observable filter méthode :

this.router.events.filter(event => event instanceof NavigationEnd).subscribe(() => {
      this.window.scrollTo(0, 0);
});

Si vous rencontrez des problèmes de défilement vers le haut lors de l'utilisation du sidenav d'Angular Material 2, voici une solution. La fenêtre ou le corps du document n'aura pas la barre de défilement, vous devez donc obtenir l'attribut sidenav et faire défiler cet élément, sinon essayez de faire défiler la fenêtre par défaut.

this.router.events.filter(event => event instanceof NavigationEnd)
  .subscribe(() => {
      const contentContainer = document.querySelector('.mat-sidenav-content') || this.window;
      contentContainer.scrollTo(0, 0);
});

De plus, le CDK Angular v6.x dispose d'une fonction paquet défilant maintenant cela pourrait aider à gérer le défilement.

2 votes

Super ! Pour moi, ça a marché - document.querySelector('.mat-sidenav-content .content-div').scrollTop = 0;

0 votes

Bien joué les gars... à mtpultz & @AmirTugi. Je suis en train de m'occuper de ça en ce moment, et vous l'avez cloué pour moi, merci ! Je vais probablement finir par mettre en place mon propre système de navigation latérale, car Material 2 ne joue pas le jeu lorsque md-toolbar est position:fixed (en haut). A moins que vous n'ayez des idées.... ????

0 votes

J'ai peut-être trouvé ma réponse... stackoverflow.com/a/40396105/3389046

18voto

Raptor Points 1996

Si vous avez un rendu côté serveur, vous devez faire attention à ne pas exécuter le code en utilisant windows sur le serveur, où cette variable n'existe pas. Cela entraînerait une rupture de code.

export class AppComponent implements OnInit {
  routerSubscription: Subscription;

  constructor(private router: Router,
              @Inject(PLATFORM_ID) private platformId: any) {}

  ngOnInit() {
    if (isPlatformBrowser(this.platformId)) {
      this.routerSubscription = this.router.events
        .filter(event => event instanceof NavigationEnd)
        .subscribe(event => {
          window.scrollTo(0, 0);
        });
    }
  }

  ngOnDestroy() {
    this.routerSubscription.unsubscribe();
  }
}

isPlatformBrowser est une fonction utilisée pour vérifier si la plateforme actuelle où l'application est rendue est un navigateur ou non. Nous lui donnons le nom injecté platformId .

Il est également possible de vérifier l'existence de la variable windows pour être sûr, comme ça :

if (typeof window != 'undefined')

1 votes

Vous n'avez pas besoin d'injecter PLATFORM_ID dans le constructor et donner cette valeur en tant que paramètre dans de isPlatformBrowser méthode ?

1 votes

@PierreDuc Oui, la réponse est fausse. isPlatformBrowser est une fonction et sera toujours véridique. Je l'ai édité maintenant.

0 votes

Merci. C'est correct maintenant ! Je viens de vérifier l'API : github.com/angular/angular/blob/

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X