Using the Router
itself will cause issues which you cannot completely overcome to maintain consistent browser experience. In my opinion the best method is to just use a custom directive
and let this reset the scroll on click. The good thing about this, is that if you are on the same url
as that you click on, the page will scroll back to the top as well. This is consistent with normal websites. The basic directive
could look something like this:
import {Directive, HostListener} from '@angular/core';
@Directive({
selector: '[linkToTop]'
})
export class LinkToTopDirective {
@HostListener('click')
onClick(): void {
window.scrollTo(0, 0);
}
}
With the following usage:
<a routerLink="/" linkToTop></a>
This will be enough for most use-cases, but I can imagine a few issues which may
arise from this:
- Doesn't work on
universal
because of the usage of window
- Small speed impact on change detection, because it is triggered by every click
- No way to disable this directive
It is actually quite easy to overcome these issues:
@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)
);
}
}
}
This takes most use-cases into account, with the same usage as the basic one, with the advantage of enable/disabling it:
<a routerLink="/" linkToTop></a> <!-- always active -->
<a routerLink="/" [linkToTop]="isActive"> <!-- active when `isActive` is true -->
commercials, don't read if you don't want to be advertised
Another improvement could be made to check whether or not the browser supports passive
events. This will complicate the code a bit more, and is a bit obscure if you want to implement all these in your custom directives/templates. That's why I wrote a little library which you can use to address these problems. To have the same functionality as above, and with the added passive
event, you can change your directive to this, if you use the ng-event-options
library. The logic is inside the click.pnb
listener:
@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);
}
}
}
RouterModule.forRoot(appRoutes, { scrollPositionRestoration: 'enabled' })
– Killy