web-dev-qa-db-fra.com

Angular 2 Faites défiler vers le haut sur le changement de route

Dans mon application Angular 2, lorsque je fais défiler une page et que je clique sur le lien en bas de page, la route est modifiée et la page suivante est affichée, mais le défilement ne fait pas défiler vers le haut. 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 manque de contenu. Étant donné que le contenu n'est visible que si l'utilisateur fait défiler vers le haut de la page.

Je peux faire défiler la fenêtre vers le haut de la page dans ngInit du composant, mais existe-t-il une meilleure solution capable de gérer automatiquement tous les itinéraires de mon application?

163
Naveed Ahmed

Vous pouvez enregistrer un écouteur de modification d'itinéraire sur votre composant principal et faire défiler vers le haut les modifications 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)
        });
    }
}
275
Guilherme Meireles

Angular 6.1 et versions ultérieures:

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

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

En outre, le blog indique "Il est prévu que cela deviendra la valeur par défaut dans une future version majeure", il est donc probable qu'à partir de Angular 7, vous n'ayez rien à faire dans votre code, et cela fonctionnera simplement. correctement sorti de la boîte.

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 affichée au même endroit que lorsque vous avez cliqué sur le lien, mais le fait de défiler vers le haut en arrivant à chaque page rompt manifestement cette attente. 

Le code ci-dessous développe la logique permettant de détecter ce type de navigation en vous abonnant à 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 revenez est suffisamment longue pour couvrir la totalité de la fenêtre, la position de défilement est automatiquement restaurée, mais comme @JordanNelson l'a correctement souligné, si la page est plus courte, vous devez conserver la position de défilement y d'origine et la restaurer. explicitement lorsque vous revenez à 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);
            }
        });
    }
}
153

À partir de Angular 6.1, vous pouvez maintenant éviter les tracas et passer extraOptions à votre RouterModule.forRoot() en tant que deuxième paramètre et vous pouvez spécifier scrollPositionRestoration: enabled pour indiquer à Angular de faire défiler vers le haut lorsque la route change.

Par défaut, vous trouverez ceci 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 angulaires

35
Abdul Rafay

Vous pouvez écrire ceci plus succinctement en tirant parti de la méthode observable filter:

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

Si vous rencontrez des problèmes pour faire défiler l'écran vers le haut lorsque vous utilisez le matériau angular Material 2 sidenav, cela vous aidera. La barre de défilement ne figure pas dans la barre de défilement de la fenêtre ou du corps du document; vous devez donc obtenir le conteneur de contenu 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, Angular CDK v6.x contient un package scrolling now qui pourrait vous aider à gérer le défilement.

25
mtpultz

Si vous avez un rendu côté serveur, veillez à ne pas exécuter le code à l'aide de 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 plate-forme actuelle sur laquelle l'application est rendue est un navigateur ou non. Nous lui donnons le platformId injecté.

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

if (typeof window != 'undefined')
16
Raptor

il suffit de le faire facilement avec l'action de clic

dans votre composant principal html, référencez #scrollContainer

<div class="main-container" #scrollContainer>
    <router-outlet (activate)="onActivate($event, scrollContainer)"></router-outlet>
</div>

en composant principal .ts

onActivate(e, scrollContainer) {
    scrollContainer.scrollTop = 0;
}
12
SAT

La meilleure réponse réside dans la discussion sur Angular GitHub ( Changer de route ne fait pas défiler vers le haut dans la nouvelle page ).

Vous voulez peut-être aller au sommet uniquement dans les modifications de routeur racine (pas dans les enfants, .__, car vous pouvez charger des itinéraires avec une charge différée dans p.ex. un jeu de tabulations)

app.component.html

<router-outlet (deactivate)="onDeactivate()"></router-outlet>

app.component.ts

onDeactivate() {
  document.body.scrollTop = 0;
  // Alternatively, you can scroll to top by using this other call:
  // window.scrollTo(0, 0)
}

Crédits complets à JoniJnm ( post original )

10
zurfyx

Vous pouvez ajouter le hook AfterViewInit lifecycle à votre composant.

ngAfterViewInit() {
   window.scrollTo(0, 0);
}
6
stillatmylinux

Voici une solution que j'ai proposée. J'ai associé la LocationStrategy aux événements Router. Utilisation de LocationStrategy pour définir un booléen afin de savoir quand un utilisateur parcourt actuellement l'historique du navigateur. De cette façon, je n'ai pas besoin de stocker un tas de données d'URL et de y-scroll (ce qui ne fonctionne pas bien, car chaque donnée est remplacée en fonction de l'URL). Cela résout également le cas Edge lorsqu'un utilisateur décide de maintenir le bouton Précédent/Suivant d'un navigateur et avance ou recule dans plusieurs pages au lieu d'une seule.

P.S. Je n'ai testé que sur la dernière version d'IE, Chrome, FireFox, Safari et Opera (à compter de cet article).

J'espère que cela t'aides.

export class AppComponent implements OnInit {
  isPopState = false;

  constructor(private router: Router, private locStrat: LocationStrategy) { }

  ngOnInit(): void {
    this.locStrat.onPopState(() => {
      this.isPopState = true;
    });

    this.router.events.subscribe(event => {
      // Scroll to top if accessing a page, not via browser history stack
      if (event instanceof NavigationEnd && !this.isPopState) {
        window.scrollTo(0, 0);
        this.isPopState = false;
      }

      // Ensures that isPopState is reset
      if (event instanceof NavigationEnd) {
        this.isPopState = false;
      }
    });
  }
}
4
Sal_Vader_808

Depuis Angular 6.1, le routeur fournit une option configuration appelée scrollPositionRestoration, conçue pour répondre à ce scénario.

imports: [
  RouterModule.forRoot(routes, {
    scrollPositionRestoration: 'enabled'
  }),
  ...
]
3
Marty A

Si vous avez simplement besoin de faire défiler la page en haut, vous pouvez le faire (pas la meilleure solution, mais rapide)

document.getElementById('elementId').scrollTop = 0;
3
Aliaksei

Cette solution est basée sur les solutions de @ FernandoEcheverria et @ GuilhermeMeireles, mais elle est plus concise et fonctionne avec les mécanismes popstate fournis par Angular Router. Cela permet de stocker et de restaurer le niveau de défilement de plusieurs navigations consécutives.

Nous stockons les positions de défilement pour chaque état de navigation dans une carte scrollLevels. Une fois qu'il y a un événement popstate, l'ID de l'état sur le point d'être restauré est fourni par le routeur angulaire: event.restoredState.navigationId. Ceci est ensuite utilisé pour obtenir le dernier niveau de défilement de cet état à partir de scrollLevels.

S'il n'y a pas de niveau de défilement enregistré pour l'itinéraire, le défilement se déroulera comme prévu.

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

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

  constructor(private router: Router) { }

  ngOnInit() {
    const scrollLevels: { [navigationId: number]: number } = {};
    let lastId = 0;
    let restoredId: number;

    this.router.events.subscribe((event: Event) => {

      if (event instanceof NavigationStart) {
        scrollLevels[lastId] = window.scrollY;
        lastId = event.id;
        restoredId = event.restoredState ? event.restoredState.navigationId : undefined;
      }

      if (event instanceof NavigationEnd) {
        if (restoredId) {
          // Optional: Wrap a timeout around the next line to wait for
          // the component to finish loading
          window.scrollTo(0, scrollLevels[restoredId] || 0);
        } else {
          window.scrollTo(0, 0);
        }
      }

    });
  }

}
3
Simon Mathewson

Angular a récemment introduit une nouvelle fonctionnalité, à l'intérieur du module de routage angular

@NgModule({
  imports: [RouterModule.forRoot(routes,{
    scrollPositionRestoration: 'top'
  })],
1
Pran R.V

Salut les gars ça marche pour moi en angle 4. Il vous suffit de référencer le parent pour faire défiler le changement de routeur 

layout.component.pug

.wrapper(#outlet="")
    router-outlet((activate)='routerActivate($event,outlet)')

layout.component.ts

 public routerActivate(event,outlet){
    outlet.scrollTop = 0;
 }`
1
Jonas Arguelles

pour iphone/ios safari vous pouvez envelopper avec un setTimeout

setTimeout(function(){
    window.scrollTo(0, 1);
}, 0);
0
tubbsy

L'utilisation de la variable Router elle-même entraînera des problèmes que vous ne pourrez pas complètement résoudre pour conserver une expérience de navigation cohérente. À mon avis, la meilleure méthode consiste simplement à utiliser une variable directive personnalisée et à laisser cette option réinitialiser le défilement au clic. La bonne chose à ce sujet est que si vous êtes sur la même url que celle sur laquelle vous cliquez, la page reviendra également au début. Ceci est compatible avec les sites Web normaux. La directive de base pourrait ressembler à ceci:

import {Directive, HostListener} from '@angular/core';

@Directive({
    selector: '[linkToTop]'
})
export class LinkToTopDirective {

    @HostListener('click')
    onClick(): void {
        window.scrollTo(0, 0);
    }
}

Avec l'usage suivant:

<a routerLink="/" linkToTop></a>

Ce sera suffisant pour la plupart des cas d'utilisation, mais je peux imaginer quelques problèmes qui pourraient en découler:

  • Ne fonctionne pas sur universal en raison de l'utilisation de window
  • Impact à petite vitesse sur la détection des modifications, car il est déclenché à chaque clic
  • Aucun moyen de désactiver cette directive

Il est en fait assez facile de surmonter ces problèmes:

@Directive({
  selector: '[linkToTop]'
})
export class LinkToTopDirective implements OnInit, OnDestroy {

  @Input()
  set linkToTop(active: string | boolean) {
    this.active = typeof active === 'string' ? active.length === 0 : active;
  }

  private active: boolean = true;

  private onClick: EventListener = (event: MouseEvent) => {
    if (this.active) {
      window.scrollTo(0, 0);
    }
  };

  constructor(@Inject(PLATFORM_ID) private readonly platformId: Object,
              private readonly elementRef: ElementRef,
              private readonly ngZone: NgZone
  ) {}

  ngOnDestroy(): void {
    if (isPlatformBrowser(this.platformId)) {
      this.elementRef.nativeElement.removeEventListener('click', this.onClick, false);
    }
  }

  ngOnInit(): void {
    if (isPlatformBrowser(this.platformId)) {
      this.ngZone.runOutsideAngular(() => 
        this.elementRef.nativeElement.addEventListener('click', this.onClick, false)
      );
    }
  }
}

Cela prend en compte la plupart des cas d'utilisation, avec le même usage que celui de base, avec l'avantage de l'activer/de le désactiver:

<a routerLink="/" linkToTop></a> <!-- always active -->
<a routerLink="/" [linkToTop]="isActive"> <!-- active when `isActive` is true -->

commercials, ne lisez pas si vous ne voulez pas être annoncé

Une autre amélioration pourrait être apportée pour vérifier si le navigateur prend en charge les événements passive. Cela compliquera un peu plus le code et est un peu obscur si vous souhaitez implémenter tout cela dans vos directives/modèles personnalisés. C'est pourquoi j'ai écrit un peu library que vous pouvez utiliser pour résoudre ces problèmes. Pour avoir les mêmes fonctionnalités que ci-dessus, et avec l'événement passive ajouté, vous pouvez changer votre directive en ceci, si vous utilisez la bibliothèque ng-event-options. La logique est à l'intérieur de l'écouteur click.pnb:

@Directive({
    selector: '[linkToTop]'
})
export class LinkToTopDirective {

    @Input()
    set linkToTop(active: string|boolean) {
        this.active = typeof active === 'string' ? active.length === 0 : active;
    }

    private active: boolean = true;

    @HostListener('click.pnb')
    onClick(): void {
      if (this.active) {
        window.scrollTo(0, 0);
      }        
    }
}
0
PierreDuc

En plus de la réponse parfaite fournie par @Guilherme Meireles comme indiqué ci-dessous, vous pouvez peaufiner votre implémentation en ajoutant un défilement lisse comme indiqué ci-dessous

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

puis ajoutez l'extrait ci-dessous

 html {
      scroll-behavior: smooth;
    }

à votre styles.css

0
Ifesinachi Bryan

L'idée principale derrière ce code est de conserver toutes les URL visitées avec les données de défilement respectives dans un tableau. Chaque fois qu'un utilisateur abandonne une page (NavigationStart), ce tableau est mis à jour. Chaque fois qu'un utilisateur entre une nouvelle page (NavigationEnd), nous décidons de restaurer la position Y ou non, en fonction de la manière dont nous accédons à cette page. Si une référence a été utilisée sur une page, nous passons à 0. Si des fonctions de navigateur précédent/précédent ont été utilisées, nous passons à Y enregistré dans notre tableau. Désolé pour mon anglais :)

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Location, PopStateEvent } from '@angular/common';
import { Router, Route, RouterLink, NavigationStart, NavigationEnd, 
    RouterEvent } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';

@Component({
  selector: 'my-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {

  private _subscription: Subscription;
  private _scrollHistory: { url: string, y: number }[] = [];
  private _useHistory = false;

  constructor(
    private _router: Router,
    private _location: Location) {
  }

  public ngOnInit() {

    this._subscription = this._router.events.subscribe((event: any) => 
    {
      if (event instanceof NavigationStart) {
        const currentUrl = (this._location.path() !== '') 
           this._location.path() : '/';
        const item = this._scrollHistory.find(x => x.url === currentUrl);
        if (item) {
          item.y = window.scrollY;
        } else {
          this._scrollHistory.Push({ url: currentUrl, y: window.scrollY });
        }
        return;
      }
      if (event instanceof NavigationEnd) {
        if (this._useHistory) {
          this._useHistory = false;
          window.scrollTo(0, this._scrollHistory.find(x => x.url === 
          event.url).y);
        } else {
          window.scrollTo(0, 0);
        }
      }
    });

    this._subscription.add(this._location.subscribe((event: PopStateEvent) 
      => { this._useHistory = true;
    }));
  }

  public ngOnDestroy(): void {
    this._subscription.unsubscribe();
  }
}
0
Vladimir Turygin

window.scrollTo() ne fonctionne pas pour moi dans Angular 5, j'ai donc utilisé document.body.scrollTop comme,

this.router.events.subscribe((evt) => {
   if (evt instanceof NavigationEnd) {
      document.body.scrollTop = 0;
   }
});
0
Rohan Kumar

Cela fonctionnait le mieux pour moi pour tous les changements de navigation, y compris la navigation par hachage 

constructor(private route: ActivatedRoute) {}

ngOnInit() {
  this._sub = this.route.fragment.subscribe((hash: string) => {
    if (hash) {
      const cmp = document.getElementById(hash);
      if (cmp) {
        cmp.scrollIntoView();
      }
    } else {
      window.scrollTo(0, 0);
    }
  });
}
0
Jorg Janke

@ Fernando Echeverria Génial! mais ce code ne fonctionne pas dans un routeur hash ou un routeur paresseux. car ils ne déclenchent pas de changement d'emplacement. .__ peut essayer ceci:

private lastRouteUrl: string[] = []
  

ngOnInit(): void {
  this.router.events.subscribe((ev) => {
    const len = this.lastRouteUrl.length
    if (ev instanceof NavigationEnd) {
      this.lastRouteUrl.Push(ev.url)
      if (len > 1 && ev.url === this.lastRouteUrl[len - 2]) {
        return
      }
      window.scrollTo(0, 0)
    }
  })
}

0
Witt Bulter