Always scroll to top of the page with this Simple Angular Plugin

Milad Hi
4 min readFeb 14, 2017

In this post I’ll create a very simple Angular module that will give your single page app the ability to always scroll to top of the page.

First let’s understand what do expect from this plugin to do :

When I scroll to the bottom of anywhere in the page and then navigate to another one, I expect the page’s scroll position to be at the top in the new page.

When I scroll back to the previous page, I expect it to remember where I left off and scroll the page to that position.

Alright, let’s begin, shall we?

For us to be able to store the previous scroll position , we need to a storage to save the position, I prefer to use the native sessionStorage .

So I’ll create a service for that reason and name it SessionStorageService.

@Injectable()
export class SessionStorageService {
write ( key : string, value : any ) {
if ( value ) {
value = JSON.stringify( value );
}
sessionStorage.setItem( key, value );
}

read<T> ( key : string ) : T {
let value : string = sessionStorage.getItem( key );

if ( value && value != "undefined" && value != "null" ) {
return <T>JSON.parse( value );
}
}
}

Pretty simple , it has two methods , write and read .

Now we can start creating our ScrollStore .

@Injectable()
export class ScrollStore {
constructor ( private router : Router,
private storageService : SessionStorageService ) {
}
}

As you can see I’ve injected the router, because considering the usage of this plugin only would makes sense if you have a multi page application, so you should’ve already be familiar with the router and have imported it’s module into your app.

Now that I have the router, I’ll subscribe to it’s event and every time there is a page navigation, I call my function to scroll to top of the page , or retrieve the previous position:

subscribeToRouter () {
this.router.events.subscribe( event => {
if ( event instanceof NavigationStart ) {
this.saveScrollPos( this.currentUrl );
}
if ( event instanceof NavigationEnd ) {
this.retrieveScrollPos( event );
}
} );
}

As you can see, I’ve subscribed to router.events events and every navigation will give me the event which could be a NavigationStart or NavigationEnd event.

There are also couple of other ones which you can find here.

So now let’s update the consructor to call this function in the service :

@Injectable()
export class ScrollStore {
constructor ( private router : Router,
private storageService : SessionStorageService ) {
this.subscribeToRouter();
}

Alrighty , now let’s see what are those two methods :

When we start the navigation, we always want to save the current scroll position with the current url, so if we came right back to this page, we can retrieve it and scroll to the previous position.

private saveScrollPos ( url ) {
this.storageService.write( url, this.scrollTop );
}

Note that I’m passing currentUrl to saveScrollPos , let’s see what that could be :

currentUrl is simple a get method that always returns the current url which router will give us:

private get currentUrl () {
return this.router.url;
}

Now that we’ve saved the current scroll position agains current url, let’s work on the retrieve.

What we want is at the end of the navigation to the page, check if there is a saved scroll position against the new url and if the is one, scroll to that , otherwise scroll to zero ( top of the page ).

private retrieveScrollPos ( event : NavigationStart ) {
let retrievedScrollPos = this.storageService.read( event.url );
if ( retrievedScrollPos === undefined ) {
console.log( 'No saved position for ' + event.url + ' scroll to zero instead' );
this.scrollToZero();
} else {
console.log( 'Postion found for ' + event.url + ' scroll to' + retrievedScrollPos );
this.scrollTo( retrievedScrollPos as number );
}

}

What’s happing is , when retrieveScrollPos is called, I’m checking our storageService to see if there is a position saved against the url, if there is one, I say scrollTo(retreivedScrollPos) and if there isn’t means we should scroll to top, which I call scrollToZero .

Here are those two methods :

private scrollTo ( retrievedScrollPos : number ) {
this.scrollTop = retrievedScrollPos;
}
private scrollToZero () {
this.scrollTop = 0;
}

And here is the scrollTop which is has a getter and setter for document.body.scrollTop

private get scrollTop () {
return document.body.scrollTop;
}

private set scrollTop ( number : number ) {
document.body.scrollTop = number;
}

That’s it , and here is the service put together :

@Injectable()
export class ScrollStore {
constructor ( private router : Router, private storageService : SessionStorageService ) {
this.subscribeToRouter();
}

subscribeToRouter () {
this.router.events.subscribe( event => {
if ( event instanceof NavigationStart ) {
this.saveScrollPos( this.currentUrl );
}
if ( event instanceof NavigationEnd ) {
this.retrieveScrollPos( event );
}
} );
}

private scrollToZero () {
this.scrollTop = 0;
}

private get currentUrl () {
return this.router.url;
}

private get scrollTop () {
return document.body.scrollTop;
}

private set scrollTop ( number : number ) {
document.body.scrollTop = number;
}

private saveScrollPos ( url ) {
this.storageService.write( url, this.scrollTop );
}

private retrieveScrollPos ( event : NavigationStart ) {
let retrievedScrollPos = this.storageService.read( event.url );
if ( retrievedScrollPos === undefined ) {
console.log( 'No saved position for ' + event.url + ' scroll to zero instead' );
this.scrollToZero();
} else {
console.log( 'Postion found for ' + event.url + ' scroll to' + retrievedScrollPos );
this.scrollTo( retrievedScrollPos as number );
}

}

private scrollTo ( retrievedScrollPos : number ) {
this.scrollTop = retrievedScrollPos;
}
}

Last, but not least, we need to ship this service inside a module to make it pluggable:

@NgModule( {
providers : [
SessionStorageService,
ScrollStore
]
} )
export class ScrollStoreModule {
constructor ( private scrollStore : ScrollStore ) {

}
}

Injecting the ScrollStore inside theScrollStoreModule is the most important part here, that makes the service to be instantialised and start working.

Here is the github page, feel free to contribute.

--

--