Skip to content

Instantly share code, notes, and snippets.

@iffa
Last active May 16, 2024 20:48
Show Gist options
  • Save iffa/9c820072135d25a6372d58075fe264dd to your computer and use it in GitHub Desktop.
Save iffa/9c820072135d25a6372d58075fe264dd to your computer and use it in GitHub Desktop.
Custom scroll position restoration logic for Angular 2+, that doesn't consider query parameter changes in route as forward navigation, thus preventing certain scenarios where you don't want query parameter changes to scroll-to-top as they would with 'scrollPositionRestoration: enabled'.
export class AppModule {
constructor(private router: Router, private viewportScroller: ViewportScroller) {
// Disable automatic scroll restoration to avoid race conditions
this.viewportScroller.setHistoryScrollRestoration('manual');
this.handleScrollOnNavigation();
}
/**
* When route is changed, Angular interprets a simple query params change as "forward navigation" too.
* Using the pairwise function allows us to have both the previous and current router events, which we can
* use to effectively compare the two navigation events and see if they actually change route, or only
* the route parameters (i.e. selections stored in query params).
*
* Related to: https://github.com/angular/angular/issues/26744
*/
private handleScrollOnNavigation(): void {
this.router.events.pipe(
// import { Event } from '@angular/router'
filter((e: Event): e is Scroll => e instanceof Scroll),
pairwise()
).subscribe((e: Scroll[]) => {
const previous = e[0];
const current = e[1];
if (current.position) {
// Backward navigation
this.viewportScroller.scrollToPosition(current.position);
} else if (current.anchor) {
// Anchor navigation
this.viewportScroller.scrollToAnchor(current.anchor);
} else {
// Check if routes match, or if it is only a query param change
if (this.getBaseRoute(previous.routerEvent.urlAfterRedirects) !== this.getBaseRoute(current.routerEvent.urlAfterRedirects)) {
// Routes don't match, this is actual forward navigation
// Default behavior: scroll to top
this.viewportScroller.scrollToPosition([0, 0]);
}
}
});
}
private getBaseRoute(url: string): string {
// return url without query params
return url.split('?')[0];
}
}
@FFKL
Copy link

FFKL commented May 31, 2021

Thanks!

@SiarheiBokuts
Copy link

thanks! but I have one question. is it ok to not unsubscribe from this subscription in case of just frontend app / in case of the app with server-side rendering?

@anbaran
Copy link

anbaran commented Dec 15, 2021

It is also working for me, thanks!
I have one more improvement. Add the following line before calling this.handleScrollOnNavigation();

this.viewportScroller.setHistoryScrollRestoration('manual');

This way automatic scrolling from browser will be turned off so there are no scrolling results race conditions. In my case on navigating back I first saw scroll to top even before route change and before this.viewportScroller.scrollToPosition(current.position); was called,

@iffa
Copy link
Author

iffa commented Dec 18, 2021

@anbaran Thanks, updated code to reflect this.

@kepek
Copy link

kepek commented Oct 27, 2022

👍

@Chewieez
Copy link

Chewieez commented May 16, 2024

This may have been obvious to others but it wasn't to me. I had to change our app's router settings for scrollPositionRestoration to:
{ scrollPositionRestoration: 'disabled' } to get this workaround to work.

It took me a bit to figure that out. So if you are also wondering why this doesn't seem to work for you, check and make sure that is disabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment