Angular 2 Desplácese hacia arriba en el Cambio de ruta


En mi aplicación Angular 2 cuando me desplazo hacia abajo en una página y hago clic en el enlace en la parte inferior de la página, cambia la ruta y me lleva a la siguiente página, pero no se desplaza a la parte superior de la página. Como resultado, si la primera página es larga y la segunda página tiene pocos contenidos, da la impresión de que la segunda página carece de contenidos. Dado que los contenidos son visibles solo si el usuario se desplaza a la parte superior de la página.

Puedo desplazar la ventana hasta la parte superior de la página en ngInit del componente pero, ¿hay alguna solución mejor que pueda manejar automáticamente todas las rutas en mi aplicación?

Author: Rohit Sharma, 2016-09-20

18 answers

Puede registrar un oyente de cambio de ruta en su componente principal y desplazarse hasta la parte superior en cambios de ruta.

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)
        });
    }
}
 235
Author: Guilherme Meireles,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-03 13:51:16

Angular 6.1 y posteriores :

Angular 6.1 (publicado el 25-07-2018) agregó soporte incorporado para manejar este problema, a través de una característica llamada "Restauración de posición de desplazamiento del enrutador". Como se describe en el blog oficial Angular , solo necesita habilitar esto en la configuración del enrutador de esta manera:

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

Además, el blog afirma que "Se espera que esto se convierta en el predeterminado en una futura versión principal", por lo que es probable que a partir de Angular 7, no necesite para hacer cualquier cosa en su código, y esto simplemente funcionará correctamente fuera de la caja.

Angular 6.0 y anteriores:

Mientras que la excelente respuesta de @GuilhermeMeireles soluciona el problema original, introduce uno nuevo, rompiendo el comportamiento normal que esperas cuando navegas hacia atrás o hacia adelante (con botones del navegador o a través de la Ubicación en el código). El comportamiento esperado es que cuando navegue de vuelta a la página, debe permanecer desplazada hacia abajo a la misma ubicación que estaba cuando al hacer clic en el enlace, pero desplazarse hasta la parte superior al llegar a cada página obviamente rompe esta expectativa.

El siguiente código expande la lógica para detectar este tipo de navegación suscribiéndose a la secuencia PopStateEvent de la ubicación y omitiendo la lógica de desplazamiento hacia arriba si la página recién llegada es el resultado de tal evento.

Si la página desde la que navega es lo suficientemente larga como para cubrir toda la vista, la posición de desplazamiento se restaura automáticamente, pero como @JordanNelson correctamente señalado, si la página es más corta, debe realizar un seguimiento de la posición original del desplazamiento y y restaurarla explícitamente cuando regrese a la página. La versión actualizada del código cubre este caso también, restaurando siempre explícitamente la posición de desplazamiento.

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);
            }
        });
    }
}
 75
Author: Fernando Echeverria,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-20 19:51:37

Puede escribir esto de manera más sucinta aprovechando el método observable filter:

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

Si tiene problemas para desplazarse hacia arriba al usar el sidenav Angular Material 2, esto le ayudará. La ventana o el cuerpo del documento no tendrá la barra de desplazamiento, por lo que debe obtener el contenedor de contenido sidenav y desplazarse por ese elemento, de lo contrario, intente desplazarse por la ventana como predeterminado.

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

También, el Angular CDK v6.x tiene un paquete de desplazamiento ahora que podría ayudar con manejo de desplazamiento.

 16
Author: mtpultz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-25 16:17:40

Si tiene renderizado del lado del servidor, debe tener cuidado de no ejecutar el código usando windows en el servidor, donde esa variable no existe. Daría lugar a la ruptura de código.

export class AppComponent implements {
  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 es una función que se utiliza para comprobar si la plataforma actual donde se representa la aplicación es un navegador o no. Le damos la inyección platformId.

También es posible comprobar la existencia de variable windows, para ser seguro, así:

if (typeof window != 'undefined')
 13
Author: Raptor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-14 19:33:37

Desde Angular 6.1, ahora puede evitar la molestia y pasar extraOptions a su RouterModule.forRoot() como segundo parámetro y puede especificar

scrollPositionRestoration: enabled

Para decirle a Angular que se desplace hacia arriba cada vez que cambie la ruta.

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      scrollPositionRestoration: 'enabled',
    })
  ],
  ...

Angular Official Docs

 11
Author: Abdul Rafay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-01 10:05:08

Simplemente hazlo fácil con click action

En tu componente principal html haz referencia # scrollContainer

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

En el componente principal .ts

onActivate(e, scrollContainer) {
    scrollContainer.scrollTop = 0;
}
 9
Author: SAT,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-13 04:26:52

La mejor respuesta reside en la discusión Angular de GitHub (Cambiar la ruta no se desplaza hacia arriba en la nueva página).

Tal vez quieras ir a la parte superior solo en los cambios del enrutador raíz (no en los niños, debido a que puede cargar rutas con carga perezosa en f. e. a tabset)

App.componente.html

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

App.componente.ts

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

Créditos completos para JoniJnm (post original )

 6
Author: zurfyx,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-25 19:31:50

Puede agregar el gancho de ciclo de vida AfterViewInit a su componente.

ngAfterViewInit() {
   window.scrollTo(0, 0);
}
 4
Author: stillatmylinux,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-02 20:05:43

Aquí hay una solución que se me ha ocurrido. Emparejé la estrategia de ubicación con los eventos del enrutador. Usando LocationStrategy para establecer un booleano para saber cuándo un usuario atraviesa actualmente el historial del navegador. De esta manera, no tengo que almacenar un montón de URL y datos de desplazamiento en y (que no funciona bien de todos modos, ya que cada dato se reemplaza en función de la URL). Esto también resuelve el caso de borde cuando un usuario decide mantener presionado el botón atrás o adelante en un navegador y retrocede o avanza varios páginas en lugar de solo una.

P.d. Solo he probado en la última versión de IE, Chrome, FireFox, Safari y Opera (a partir de este post).

Espero que esto ayude.

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;
      }
    });
  }
}
 3
Author: Sal_Vader_808,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-15 06:39:13

Esta solución se basa en la solución de @FernandoEcheverria y @GuilhermeMeireles, pero es más concisa y funciona con los mecanismos popstate que proporciona el Enrutador Angular. Esto permite almacenar y restaurar el nivel de desplazamiento de múltiples navegaciones consecutivas.

Almacenamos las posiciones de desplazamiento para cada estado de navegación en un mapa scrollLevels. Una vez que hay un evento popstate, el ID del estado que está a punto de ser restaurado es suministrado por el Enrutador Angular: event.restoredState.navigationId. Esto es entonces se usa para obtener el último nivel de desplazamiento de ese estado desde scrollLevels.

Si no hay un nivel de desplazamiento almacenado para la ruta, se desplazará hacia la parte superior como es de esperar.

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

    });
  }

}
 2
Author: Simon Mathewson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-10 23:16:36

Si necesita simplemente desplazarse por la página hasta la parte superior, puede hacer esto (no es la mejor solución, pero rápido)

document.getElementById('elementId').scrollTop = 0;
 1
Author: Aliaksei,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-28 17:15:15

Para iphone / ios safari puede envolver con un setTimeout

setTimeout(function(){
    window.scrollTo(0, 1);
}, 0);
 0
Author: tubbsy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-08 12:46:05

@ Fernando Echeverría ¡Órale! pero este código no funciona en enrutador hash o enrutador perezoso. porque no desencadenan cambios de ubicación. puede probar esto:

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
Author: Witt Bulter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-01 18:42:05

El uso de Router en sí causará problemas que no puede superar por completo para mantener una experiencia de navegador consistente. En mi opinión, el mejor método es simplemente usar un directive personalizado y dejar que esto restablezca el desplazamiento al hacer clic. Lo bueno de esto, es que si estás en el mismo url en el que haces clic, la página también se desplazará hacia arriba. Esto es consistente con los sitios web normales. El directive básico podría verse algo como esto:

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

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

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

Con lo siguiente uso:

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

Esto será suficiente para la mayoría de los casos de uso, pero puedo imaginar algunos problemas que pueden surge de esto:

  • No funciona en universal debido al uso de window
  • Pequeño impacto de velocidad en la detección de cambios, ya que se activa con cada clic
  • No hay forma de desactivar esta directiva

En realidad es bastante fácil superar estos problemas: {[16]]}

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

Esto toma en cuenta la mayoría de los casos de uso, con el mismo uso que el uno básico, con la ventaja de habilitarlo/deshabilitarlo:

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

Anuncios, no leas si no quieres que te anuncien

Otra mejora podría hacerse para comprobar si el navegador soporta o no eventos passive. Esto complicará un poco más el código, y es un poco oscuro si desea implementar todo esto en sus directivas/plantillas personalizadas. Por eso escribí un poco biblioteca que puede utilizar para abordar estos problemas. A tenga la misma funcionalidad que la anterior, y con el evento passive añadido, puede cambiar su directiva a esta, si utiliza la biblioteca ng-event-options. La lógica está dentro del oyente 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
Author: PierreDuc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-18 11:05:59

Hola chicos esto funciona para mí en angular 4. Solo tiene que hacer referencia al padre para desplazarse en router change '

Diseño.componente.pug

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

Diseño.componente.ts

 public routerActivate(event,outlet){
    outlet.scrollTop = 0;
 }`
 0
Author: Jonas Arguelles,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-05 09:49:39

Esto me funcionó mejor para todos los cambios de navegación, incluida la navegación hash

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
Author: Jorg Janke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-24 20:10:54

La idea principal detrás de este código es mantener todas las URL visitadas junto con los datos de scrollY respectivos en una matriz. Cada vez que un usuario abandona una página (NavigationStart) esta matriz se actualiza. Cada vez que un usuario entra en una nueva página (NavigationEnd), decidimos restaurar la posición Y o no dependiendo de cómo lleguemos a esta página. Si se utilizó una referencia en alguna página, nos desplazamos a 0. Si se utilizaron las funciones de retroceso/avance del navegador, nos desplazamos a Y guardado en nuestro array. Lo siento por mi inglés:)

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
Author: Vladimir Turygin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-17 09:58:55

A partir de Angular 6.1, el enrutador proporciona una opción de configuración llamada scrollPositionRestoration, esta está diseñada para atender este escenario.

imports: [
  RouterModule.forRoot(routes, {
    scrollPositionRestoration: 'enabled'
  }),
  ...
]
 0
Author: Marty A,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-11 23:32:32