navigationend - router events angular 6
Angular 2 Desplácese hacia arriba en Cambio de ruta (21)
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 página siguiente, pero no se desplaza hacia 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 contenido. Dado que los contenidos son visibles solo si el usuario se desplaza hacia la parte superior de la página.
Puedo desplazar la ventana a 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?
@Fernando Echeverria genial! pero este código no funciona en enrutadores hash o enrutadores perezosos. porque no desencadenan cambios de ubicación. puede intentar esto:
import {Directive, HostListener} from ''@angular/core'';
@Directive({
selector: ''[linkToTop]''
})
export class LinkToTopDirective {
@HostListener(''click'')
onClick(): void {
window.scrollTo(0, 0);
}
}
A partir de Angular 6.1, el enrutador proporciona una
opción de configuración
llamada
scrollPositionRestoration
, que está diseñada para satisfacer este escenario.
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;
}
});
}
}
Además de la respuesta perfecta proporcionada por como se muestra a continuación, puede modificar su implementación agregando un desplazamiento suave como se muestra a continuación
setTimeout(function(){
window.scrollTo(0, 1);
}, 0);
luego agregue el fragmento a continuación
.wrapper(#outlet="")
router-outlet((activate)=''routerActivate($event,outlet)'')
a tus styles.css
Angular introdujo recientemente una nueva característica, dentro del módulo de enrutamiento angular realiza cambios como a continuación
public routerActivate(event,outlet){
outlet.scrollTop = 0;
}`
Aquí hay una solución que se me ocurrió. Emparejé la estrategia de ubicación con los eventos del enrutador. Usando LocationStrategy para establecer un valor booleano para saber cuándo un usuario atraviesa actualmente el historial del navegador. De esta forma, no tengo que almacenar un montón de URL y datos de desplazamiento en y (que de todos modos no funciona bien, ya que cada información se reemplaza en función de la URL). Esto también resuelve el caso límite cuando un usuario decide mantener presionado el botón de retroceso o avance en un navegador y retrocede o reenvía varias páginas en lugar de solo una.
PD: Solo he probado en la última versión de IE, Chrome, Firefox, Safari y Opera (a partir de esta publicación).
Espero que esto ayude.
document.getElementById(''elementId'').scrollTop = 0;
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 indicarle a Angular que se desplace hacia arriba cada vez que cambie la ruta.
Por defecto, encontrará esto en
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 { }
El uso del
Router
sí causará problemas que no puede superar por completo para mantener una experiencia de navegador coherente.
En mi opinión, el mejor método es usar una
directive
personalizada y dejar que esto restablezca el desplazamiento al hacer clic.
Lo bueno de esto es que si está en la misma
url
que hace clic, la página también se desplazará hacia arriba.
Esto es consistente con los sitios web normales.
La
directive
básica podría verse así:
<a routerLink="/" linkToTop></a>
Con el siguiente uso:
@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 será suficiente para la mayoría de los casos de uso, pero puedo imaginar algunos problemas que pueden surgir de esto:
-
No funciona en
universal
debido al uso de lawindow
- Impacto de poca velocidad en la detección de cambios, ya que se activa por cada clic
- No hay forma de deshabilitar esta directiva
En realidad, es bastante fácil superar estos problemas:
<a routerLink="/" linkToTop></a> <!-- always active -->
<a routerLink="/" [linkToTop]="isActive"> <!-- active when `isActive` is true -->
Esto tiene en cuenta la mayoría de los casos de uso, con el mismo uso que el básico, con la ventaja de habilitarlo / deshabilitarlo:
@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);
}
}
}
comerciales, no leas si no quieres que te anuncien
Se podría hacer otra mejora para verificar si el navegador admite o no eventos
passive
.
Esto complicará el código un poco más y es un poco oscuro si desea implementar todo esto en sus directivas / plantillas personalizadas.
Es por eso que escribí una pequeña
library
que puede usar para abordar estos problemas.
Para tener la misma funcionalidad que anteriormente, y con el evento
passive
agregado, puede cambiar su directiva a esto, si usa la biblioteca
ng-event-options
.
La lógica está dentro del oyente
click.pnb
:
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);
}
});
}
Esta solución se basa en la solución de @ FernandoEcheverria y @ GuilhermeMeireles, pero es más concisa y funciona con los mecanismos de 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 enrutador angular proporciona el ID del estado que está por restaurarse:
event.restoredState.navigationId
.
Esto se usa para obtener el último nivel de desplazamiento de ese estado de
scrollLevels
.
Si no hay un nivel de desplazamiento almacenado para la ruta, se desplazará a la parte superior como era 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);
}
}
});
}
}
Esto funcionó para mí mejor para todos los cambios de navegación, incluida la navegación hash
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();
}
}
Hola chicos, esto funciona para mí en angular 4. Solo tienes que hacer referencia al padre para desplazarte en el cambio de enrutador`
layout.component.pug
html {
scroll-behavior: smooth;
}
layout.component.ts
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)
}
})
}
La idea principal detrás de este código es mantener todas las URL visitadas junto con los datos de desplazamiento respectivos en una matriz. Cada vez que un usuario abandona una página (NavigationStart), esta matriz se actualiza. Cada vez que un usuario ingresa a una nueva página (NavigationEnd), decidimos restaurar la posición Y o no, según 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 nuestra matriz. Lo siento por mi ingles :)
this.router.events.subscribe((evt) => {
if (evt instanceof NavigationEnd) {
document.body.scrollTop = 0;
}
});
La mejor respuesta reside en la discusión de Angular GitHub ( Cambiar la ruta no se desplaza hacia arriba en la nueva página ).
Tal vez quiera ir al principio solo en los cambios del enrutador raíz (no en los hijos, porque puede cargar rutas con carga diferida en un conjunto de pestañas)
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éditos completos a JoniJnm ( publicación original )
Puede agregar el AfterViewInit ciclo de vida AfterViewInit a su componente.
ngAfterViewInit() {
window.scrollTo(0, 0);
}
Puede escribir esto de manera más sucinta aprovechando el método de
filter
observable:
this.router.events.filter(event => event instanceof NavigationEnd).subscribe(() => {
this.window.scrollTo(0, 0);
});
Si tiene problemas para desplazarse hacia la parte superior al usar el material angular de Sidenav 2, esto será útil.
La ventana o el cuerpo del documento no tendrán la barra de desplazamiento, por lo que debe obtener el
sidenav
contenido
sidenav
y desplazar ese elemento; de lo contrario, intente desplazar la ventana por defecto.
this.router.events.filter(event => event instanceof NavigationEnd)
.subscribe(() => {
const contentContainer = document.querySelector(''.mat-sidenav-content'') || this.window;
contentContainer.scrollTo(0, 0);
});
Además, Angular CDK v6.x tiene un paquete de desplazamiento ahora que podría ayudar con el manejo del desplazamiento.
Puede registrar una escucha de cambio de ruta en su componente principal y desplazarse hacia arriba en los 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)
});
}
}
Si necesita simplemente desplazarse por la página hacia arriba, puede hacerlo (no es la mejor solución, pero es rápido)
imports: [
RouterModule.forRoot(routes, {
scrollPositionRestoration: ''enabled''
}),
...
]
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.
Resultaría en la ruptura del código.
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
es una función que se utiliza para verificar si la plataforma actual donde se representa la aplicación es un navegador o no.
Le damos la
platformId
inyectada.
También es posible verificar la existencia de
windows
variables, para estar seguro, así:
if (typeof window != ''undefined'')
para iphone / ios safari puedes envolver con un setTimeout
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)
});
}
}
solo hazlo fácil con la acción de hacer clic
en su componente principal html haga 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;
}
window.scrollTo()
no funciona para mí en Angular 5, por lo que he usado
document.body.scrollTop
como,
this.router.events.subscribe((evt) => { if (evt instanceof NavigationEnd) { document.body.scrollTop = 0; } });
Angular 6.1 y posterior :
Angular 6.1 (lanzado el 25-07-2018) agregó soporte incorporado para manejar este problema, a través de una función llamada "Restauración de la posición de desplazamiento del enrutador". Como se describe en el blog oficial de Angular , solo necesita habilitar esto en la configuración del enrutador de esta manera:
RouterModule.forRoot(routes, {scrollPositionRestoration: ''enabled''})
Además, el blog dice "Se espera que esto se convierta en el predeterminado en una versión principal futura". Hasta ahora, esto no ha sucedido (a partir de Angular 8.2), pero eventualmente no necesitará hacer nada en su código, y esto simplemente funcionará correctamente de fábrica.
Puede ver más detalles sobre esta función y cómo personalizar este comportamiento en los documentos oficiales .
Angular 6.0 y anterior :
Si bien la excelente respuesta de @ GuilhermeMeireles soluciona el problema original, presenta una nueva, al romper el comportamiento normal que espera cuando navega hacia atrás o hacia adelante (con los botones del navegador o mediante la Ubicación en el código). El comportamiento esperado es que cuando navega de regreso a la página, debe permanecer desplazado hacia abajo a la misma ubicación que cuando hizo clic en el enlace, pero desplazarse hacia 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 Location 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 ventana gráfica, la posición de desplazamiento se restaura automáticamente, pero como @JordanNelson señaló correctamente, si la página es más corta, debe realizar un seguimiento de la posición de desplazamiento original y restaurarla. explícitamente cuando vuelves a la página. La versión actualizada del código también cubre este caso, siempre restaurando 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);
}
});
}
}