Skip to content

Instantly share code, notes, and snippets.

@rosslavery
Created May 25, 2017 16:03
Show Gist options
  • Save rosslavery/2a85c6e177d14b69dcae99e09ec02d52 to your computer and use it in GitHub Desktop.
Save rosslavery/2a85c6e177d14b69dcae99e09ec02d52 to your computer and use it in GitHub Desktop.
Angular service to save / restore scroll position of arbitrary elements on route change
import { AfterViewInit, Directive, ElementRef, NgZone, OnDestroy } from '@angular/core';
import { NavigationEnd, NavigationStart, Router } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { ScrollTrackerService } from './scroll-tracker.service';
@Directive({
selector: '[scrollTracker]'
})
export class ScrollTrackerDirective implements AfterViewInit, OnDestroy {
private element: HTMLElement;
private enterSubscription: Subscription;
private exitSubscription: Subscription;
private intervalDuration: number = 250;
private intervalId;
private maxScrollAttempts: number = 5;
private curScrollAttempts: number = 0;
constructor(
private elementRef: ElementRef,
private router: Router,
private scrollTrackerService: ScrollTrackerService,
private zone: NgZone) { }
ngAfterViewInit() {
this.element = this.elementRef.nativeElement;
/**
* Listen for when the component's route is exited.
* Store the current scroll position in the service.
*/
this.exitSubscription = this.router.events
.pairwise()
.filter(([prevRouteEvent, currRouteEvent]) => (prevRouteEvent instanceof NavigationEnd && currRouteEvent instanceof NavigationStart))
.do(() => this.clearScrollChecker())
.map(([prevRouteEvent]) => this.scrollTrackerService.getUrlForEvent(prevRouteEvent))
.subscribe(url => {
this.scrollTrackerService.saveScroll(url, {
elementId: this.element.id || null,
position: this.element.scrollTop
});
});
/**
* Listen for when the component's route is re-entered.
* Get the stored scroll position from the service, and prepare a scroll attempt.
*/
this.enterSubscription = this.router.events
.filter(event => event instanceof NavigationEnd)
.map(event => this.scrollTrackerService.getUrlForEvent(event))
.map(url => this.scrollTrackerService.getScroll(url))
.filter(scrollPosition => scrollPosition && scrollPosition.elementId === this.element.id)
.subscribe(scrollPosition => this.prepareScroll(scrollPosition.position));
}
prepareScroll(position: number) {
this.zone.runOutsideAngular(() => {
this.clearScrollChecker();
this.intervalId = setInterval(() => {
this.attemptScroll(position);
}, this.intervalDuration);
});
}
clearScrollChecker() {
this.curScrollAttempts = 0;
if (!this.intervalId) { return; }
this.zone.runOutsideAngular(() => {
clearInterval(this.intervalId);
});
}
attemptScroll(position: number) {
/**
* If you've tried the maximum number of times, and the element does have
* a scrollHeight, then at least scroll the element to the bottom.
*/
if (this.curScrollAttempts === this.maxScrollAttempts) {
if (this.element.scrollHeight > 0) {
this.element.scrollTop = this.element.scrollHeight;
}
return this.clearScrollChecker();
}
/**
* If the element is at least as tall as the desired scroll position,
* scroll to the desired position.
*/
if (this.element.scrollHeight >= position) {
this.element.scrollTop = position;
return this.clearScrollChecker();
}
this.curScrollAttempts++;
}
ngOnDestroy() {
this.clearScrollChecker();
this.exitSubscription && this.exitSubscription.unsubscribe();
this.enterSubscription && this.enterSubscription.unsubscribe();
}
}
import { Injectable } from '@angular/core';
import { NavigationEnd, NavigationStart } from '@angular/router';
export interface RouteScrollPositions {
[url: string]: RouteScrollPosition;
}
export interface RouteScrollPosition {
position: number;
elementId: string;
}
@Injectable()
export class ScrollTrackerService {
private routeScrollPositions: RouteScrollPositions = {};
constructor() { }
saveScroll(url: string, scrollPosition: RouteScrollPosition) {
this.routeScrollPositions[url] = scrollPosition;
}
getScroll(url: string): RouteScrollPosition {
return this.routeScrollPositions[url];
}
getUrlForEvent(event): string {
if (event instanceof NavigationStart) {
return event.url.split(';', 1)[0];
}
if (event instanceof NavigationEnd) {
return (event.urlAfterRedirects || event.url).split(';', 1)[0];
}
}
}
@jaibatrik
Copy link

Thanks for the helpful code. Could you please explain how to use the classes?

@K-Vishwak
Copy link

K-Vishwak commented Jan 31, 2020

this.router.events.filter -- showing error saying that 'Property filter doesn't exist on type 'Observable'

Tried a workaround like using

  1. pipe
  2. Observable.from

but no fetch. Please help.

my rxjs: 6.3.3
Angular: 7

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