366

In my Angular 2 app when I scroll down a page and click the link at the bottom of the page, it does change the route and takes me to the next page but it doesn't scroll to the top of the page. As a result, if the first page is lengthy and 2nd page has few contents, it gives an impression that the 2nd page lacks the contents. Since the contents are visible only if a user scrolls to the top of the page.

I can scroll the window to the top of the page in ngInit of the component but, is there any better solution that can automatically handle all routes in my app?

Nikita Fedyashev
  • 15,938
  • 11
  • 41
  • 69
Naveed Ahmed
  • 8,391
  • 11
  • 39
  • 75
  • 38
    Since Angular 6.1 we can use { scrollPositionRestoration: 'enabled' } on eagerly loaded modules or just in app.module and it will be applied to all routes. `RouterModule.forRoot(appRoutes, { scrollPositionRestoration: 'enabled' })` – Manwal Mar 16 '19 at 17:22
  • Muito obrigado sua solução funcionou perfeitamente para mim :) – Raphael Godoi Sep 02 '19 at 18:48
  • not one person mentioned focus? it's more important than ever before to properly support accessibility / screen readers and if you simply scroll to the top without considering focus then the next tab keypress can jump to the bottom of the screen. – Simon_Weaver Jan 09 '20 at 01:11
  • @Manwal you should put this as an answer as it is better than all the other solutions – MadMac Mar 03 '21 at 19:43
  • @MadMac Sure, I have added this as an answer also. – Manwal Mar 05 '21 at 07:36

27 Answers27

446

Angular 6.1 and later:

Angular 6.1 (released on 2018-07-25) added built-in support to handle this issue, through a feature called "Router Scroll Position Restoration". As described in the official Angular blog, you just need to enable this in the router configuration like this:

RouterModule.forRoot(routes, {scrollPositionRestoration: 'enabled'})

Furthermore, the blog states "It is expected that this will become the default in a future major release". So far this hasn't happened (as of Angular 11.0), but eventually you won't need to do anything at all in your code, and this will just work correctly out of the box.

You can see more details about this feature and how to customize this behavior in the official docs.

Angular 6.0 and earlier:

While @GuilhermeMeireles's excellent answer fixes the original problem, it introduces a new one, by breaking the normal behavior you expect when you navigate back or forward (with browser buttons or via Location in code). The expected behavior is that when you navigate back to the page, it should remain scrolled down to the same location it was when you clicked on the link, but scrolling to the top when arriving at every page obviously breaks this expectation.

The code below expands the logic to detect this kind of navigation by subscribing to Location's PopStateEvent sequence and skipping the scroll-to-top logic if the newly arrived-at page is a result of such an event.

If the page you navigate back from is long enough to cover the whole viewport, the scroll position is restored automatically, but as @JordanNelson correctly pointed out, if the page is shorter you need to keep track of the original y scroll position and restored it explicitly when you go back to the page. The updated version of the code covers this case too, by always explicitly restoring the scroll position.

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);
            }
        });
    }
}
Fernando Echeverria
  • 6,326
  • 4
  • 15
  • 12
  • does this go in app.component.ts or every component.ts which is part of my main `router-outlet`? – Moshe Jun 27 '17 at 22:53
  • 2
    This should go either in the app component directly, or in a single component used in it (and therefore shared by the whole app). For instance, I've included it in a top navigation bar component. You should not included in all your components. – Fernando Echeverria Jun 28 '17 at 02:21
  • I ended up putting this on my app.component. Question regarding "window," there are many articles and blogs recommending to use window as an Injectable service (window service), something with zone.js and "angular's consciousness"? Should I wrap `window` around a service? – Moshe Jun 28 '17 at 03:45
  • 3
    You can do that and it will make the code more widely compatible with other, non-browser, platforms. See https://stackoverflow.com/q/34177221/2858481 for implementation details. – Fernando Echeverria Jun 28 '17 at 13:26
  • 3
    If you click and hold the back/forward button in modern browsers, a menu appears that lets you navigate to locations other than your immediately previous/next one. This solution breaks if you do that. It's an edge case for most, but worth mentioning. – adamdport Nov 30 '17 at 17:28
  • This only works for back, it breaks on forward if you carefully test it – 1in9ui5t May 15 '18 at 23:36
  • Why not just use [`NavigationStart.navigationTrigger === 'imperative'`](https://angular.io/api/router/NavigationStart#navigationTrigger)? – Matt Thomas May 18 '18 at 17:46
  • 1
    [I just posted a more concise solution based on this one, which works with the popstate mechanisms provided by Angular and restores scroll levels across multiple consecutive navigation events](https://stackoverflow.com/a/51274898/3873526) – Simon Mathewson Jul 10 '18 at 22:50
  • For me, it's not able to subscribe to the location. My this.lastPoppedUrl is always undefined. Can anyone explain? – PiyaModi Nov 01 '18 at 00:28
  • 1
    Is there a way to enable "Router Scroll Position Restoration" for nested elements or it works only for `body`? – vulp Nov 18 '18 at 10:58
  • This doesn't seem to work with modules that use RouterModule.forChild(routes). The forChild function doesn't accept ExtraOptions. – Alan Smith Jun 04 '19 at 14:31
  • @AlanSmith that's true, but there should always be a call to RouterModuel.forRoot in the main app module, and that's where you should add this option. It's not necessary to add it also in other modules. – Fernando Echeverria Jun 04 '19 at 18:53
  • Doesn't work when you only add it to the root module, when the loaded page you need to scroll up on is defined in a child (lazy loaded) module with it's own routes. – Alan Smith Jun 06 '19 at 10:29
  • I added a window.setTimeout - I'm not sure why but for me when I press the browser back button the page does not automatically scroll to the correct position. I've posted my workaround here https://stackoverflow.com/questions/57214772/angular-8-restore-scroll-position-when-browser-back-button-is-selected-in-child. Let me know if there's other better ways to do it. Thanks! – iBlehhz Jul 29 '19 at 04:55
  • If you are using Angular v6.1 and above as mentioned in the answer **scrollPositionRestoration** and **anchorScrolling** options worked well for me. Explained well in the blog: [https://medium.com/lacolaco-blog/introduce-router-scroller-in-angular-v6-1-ef34278461e9] – Ravi Naidu Aug 20 '19 at 12:20
425

You can register a route change listener on your main component and scroll to top on route changes.

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)
        });
    }
}
Guilherme Meireles
  • 7,509
  • 2
  • 18
  • 15
  • Thank you so much @Guilherme does this approach has any performance implications? Since this subscription will last throughout app life. – Naveed Ahmed Sep 20 '16 at 19:33
  • It's just one subscription that is triggered only on route events. It should have very little or no performance impact. Just make sure to put it only in the main component of the application. If you decide to use somewhere else unsubscribe to the events when the component is destroyed to avoid leaks. – Guilherme Meireles Sep 20 '16 at 19:45
  • @Diego did you make any change to the answer? – Naveed Ahmed Sep 20 '16 at 20:52
  • @NaveedAhmed just remove the snippet, because it is code that not runs in a snippet, not the answer itself, and the editing goes through moderators please read Stack Overflow http://meta.stackexchange.com/questions/21788/how-does-editing-work – Diego Unanue Sep 20 '16 at 22:31
  • 13
    `window.scrollTo(0, 0)` is a more concise than `document.body.scrollTop = 0;`, and more readable IMO. – Mark E. Haase Dec 30 '16 at 03:38
  • 10
    Did anybody noticed, that even after implementing this, issue persists in safari browser of Iphone. any thoughts? – rgk Jan 05 '17 at 17:11
  • 1
    @mehaase Looks like your answer is the best one. window.body.scrollTop doesn't work for me on Firefox desktop. So thank you ! – KCarnaille Mar 02 '17 at 08:51
  • 4
    This worked for me, but it breaks the default "back" button behavior. Going back should remember the previous scroll position. – JackKalish Jun 13 '17 at 20:35
  • 7
    This worked!! Although I added `$("body").animate({ scrollTop: 0 }, 1000);` rather than `window.scrollTo(0, 0)` to animate smooth scrolling to top – Manubhargav Jul 05 '17 at 10:18
  • @GuilhermeMeireles what's about situation when route is changing on the same page? like with – Victor Bredihin Aug 09 '17 at 09:34
  • Why not: `if ( evt instanceof NavigationEnd ) { window.scrollTo(0, 0); }` – Henrique César Madeira Feb 08 '18 at 01:08
  • As mentioned by @JackKalish and others, this breaks the browser "back" button behavior. Not only that, it also breaks the forward button and when you hold either the back or forward button and select a specific history state, it also breaks. I've provided an answer below that seems to work and prevents any of those issues, while answering the original question. Hope it helps. – Sal_Vader_808 Mar 12 '18 at 12:42
  • 1
    [I just posted an improved solution based on this one, which works with the popstate mechanisms provided by Angular and restores scroll levels across multiple consecutive navigation events](https://stackoverflow.com/a/51274898/3873526) – Simon Mathewson Jul 10 '18 at 22:55
  • For your information the answer from @Fernando Echeverria is more useful and concise for Angular 6+ than this accepted answer – F3L1X79 Nov 07 '19 at 08:04
  • see answer below for angular 6.1+: RouterModule.forRoot(routes, {scrollPositionRestoration: 'enabled'}) – Ruben Dec 10 '19 at 22:23
  • instead of the if block, you might be able to use pipe and filter like this `this.router.events.pipe( filter(event => event instanceof NavigationEnd) ).subscribe((e: NavigationEnd ) => { window.scrollTo(0, 0); })` – Joey Gough Feb 23 '20 at 17:13
  • Rather use `ViewportScroller` as it's officially provided by angular instead of hardcoded `window.scrollTop`. For example `this.viewportScroller.scrollToPosition([0, 0]);` – dude Sep 09 '20 at 18:27
69

From Angular 6.1, you can now avoid the hassle and pass extraOptions to your RouterModule.forRoot() as a second parameter and can specify scrollPositionRestoration: enabled to tell Angular to scroll to top whenever the route changes.

By default you will find this in 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 { }

Angular Official Docs

Michael Czechowski
  • 2,840
  • 1
  • 20
  • 44
Abdul Rafay
  • 2,793
  • 2
  • 21
  • 40
  • 3
    Even though the answer above is more descriptive I like that this answer told me exactly where this needs to go – ryanovas May 31 '19 at 21:29
34

You can write this more succinctly by taking advantage of the observable filter method:

this.router.events.filter(event => event instanceof NavigationEnd).subscribe(() => {
      this.window.scrollTo(0, 0);
});

If you're having issues scrolling to the top when using the Angular Material 2 sidenav this will help. The window or document body won't have the scrollbar so you need to get the sidenav content container and scroll that element, otherwise try scrolling the window as a default.

this.router.events.filter(event => event instanceof NavigationEnd)
  .subscribe(() => {
      const contentContainer = document.querySelector('.mat-sidenav-content') || this.window;
      contentContainer.scrollTo(0, 0);
});

Also, the Angular CDK v6.x has a scrolling package now that might help with handling scrolling.

mtpultz
  • 13,197
  • 18
  • 96
  • 180
  • 2
    Great! For me that worked - `document.querySelector('.mat-sidenav-content .content-div').scrollTop = 0;` – Amir Tugi Jun 03 '17 at 16:53
  • Nice one fellas... at mtpultz & @AmirTugi. Dealing with this right now, and you nailed it for me, cheers! Probably will inevitably end up rolling my own side nav since Material 2's doesn't play nice when md-toolbar is position:fixed (at top). Unless you guys have ideas....???? – Tim Harker Jul 28 '17 at 13:56
  • Might have found my answer... https://stackoverflow.com/a/40396105/3389046 – Tim Harker Jul 28 '17 at 14:24
20

Angular lately introduced a new feature, inside angular routing module make changes like below

@NgModule({
  imports: [RouterModule.forRoot(routes,{
    scrollPositionRestoration: 'top'
  })],
Pran R.V
  • 612
  • 5
  • 15
18

If you have server side rendering, you should be careful not to run the code using windows on the server, where that variable doesn't exist. It would result in code breaking.

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 is a function used to check if the current platform where the app is rendered is a browser or not. We give it the injected platformId.

It it also possible to check for existence of variable windows, to be safe, like this:

if (typeof window != 'undefined')
Rohan Kumar
  • 38,998
  • 11
  • 69
  • 99
Raptor
  • 3,039
  • 2
  • 30
  • 30
  • 1
    Don't you need to inject `PLATFORM_ID` in the `constructor` and give this value as parameter in de `isPlatformBrowser` method? – Poul Kruijt Dec 18 '17 at 10:41
  • 1
    @PierreDuc Yes, the answer is wrong. `isPlatformBrowser` is a function and will always be truthy. I've edited it now. – Lazar Ljubenović Jan 14 '18 at 19:30
  • Thanks! It's correct now! Just verified the API: https://github.com/angular/angular/blob/bebedfed24d6fbfa492e97f071e1d1b41e411280/packages/common/src/platform_id.ts#L18 – Raptor Jan 15 '18 at 02:52
14

just do it easy with click action

in your main component html make reference #scrollContainer

<div class="main-container" #scrollContainer>
    <router-outlet (activate)="onActivate($event, scrollContainer)"></router-outlet>
</div>

in main component .ts

onActivate(e, scrollContainer) {
    scrollContainer.scrollTop = 0;
}
ScottMcGready
  • 1,556
  • 2
  • 21
  • 33
SAT
  • 139
  • 1
  • 3
  • The element to be scrolled might not be in the `scrollContainer` first node, you might need to dig a bit in the object, for me what it really worked was `scrollContainer .scrollable._elementRef.nativeElement.scrollTop = 0` – Byron Lopez Mar 16 '18 at 22:19
13

The best answer resides in the Angular GitHub discussion (Changing route doesn't scroll to top in the new page).

Maybe you want go to top only in root router changes (not in children, because you can load routes with lazy load in f.e. a tabset)

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)
}

Full credits to JoniJnm (original post)

zurfyx
  • 23,843
  • 15
  • 103
  • 130
9

You can add the AfterViewInit lifecycle hook to your component.

ngAfterViewInit() {
   window.scrollTo(0, 0);
}
stillatmylinux
  • 1,152
  • 10
  • 22
7

As of Angular 6.1, the router provides a configuration option called scrollPositionRestoration, this is designed to cater for this scenario.

imports: [
  RouterModule.forRoot(routes, {
    scrollPositionRestoration: 'enabled'
  }),
  ...
]
Marty A
  • 403
  • 1
  • 5
  • 9
6

In addition to the perfect answer provided by @Guilherme Meireles as shown below, you could tweak your implementation by adding smooth scroll as shown below

 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)
            });
        }
    }

then add the snippet below

 html {
      scroll-behavior: smooth;
    }

to your styles.css

Ifesinachi Bryan
  • 1,566
  • 12
  • 18
4

If you need simply scroll page to top, you can do this (not the best solution, but fast)

document.getElementById('elementId').scrollTop = 0;
Aliaksei
  • 485
  • 4
  • 10
4

Here's a solution that I've come up with. I paired up the LocationStrategy with the Router events. Using the LocationStrategy to set a boolean to know when a user's currently traversing through the browser history. This way, I don't have to store a bunch of URL and y-scroll data (which doesn't work well anyway, since each data is replaced based on URL). This also solves the edge case when a user decides to hold the back or forward button on a browser and goes back or forward multiple pages rather than just one.

P.S. I've only tested on the latest version of IE, Chrome, FireFox, Safari, and Opera (as of this post).

Hope this helps.

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;
      }
    });
  }
}
Sal_Vader_808
  • 453
  • 6
  • 10
4

This solution is based on @FernandoEcheverria's and @GuilhermeMeireles's solution, but it is more concise and works with the popstate mechanisms that the Angular Router provides. This allows for storing and restoring the scroll level of multiple consecutive navigations.

We store the scroll positions for each navigation state in a map scrollLevels. Once there is a popstate event, the ID of the state that is about to be restored is supplied by the Angular Router: event.restoredState.navigationId. This is then used to get the last scroll level of that state from scrollLevels.

If there is no stored scroll level for the route, it will scroll to the top as you would expect.

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);
        }
      }

    });
  }

}
Simon Mathewson
  • 693
  • 3
  • 18
  • Awesome. I had to make a slightly custom version to scroll a div rather than window, but it worked. One key difference was `scrollTop` vs `scrollY`. – BBaysinger Jul 18 '18 at 03:35
2

Since Angular 6.1 we can use following on eagerly loaded modules or just in app.module and it will be applied to all routes

{ scrollPositionRestoration: 'enabled' } 

Full syntax:

RouterModule.forRoot(appRoutes, { scrollPositionRestoration: 'enabled' })

More details in doc: https://angular.io/api/router/ExtraOptions#scrollPositionRestoration

Manwal
  • 22,117
  • 10
  • 57
  • 89
1

for iphone/ios safari you can wrap with a setTimeout

setTimeout(function(){
    window.scrollTo(0, 1);
}, 0);
tubbsy
  • 61
  • 8
  • in my case it also required the page wrapping element css to be set to; `height: 100vh + 1px;` – tubbsy Mar 10 '17 at 17:52
1

Hi guys this works for me in angular 4. You just have to reference the parent to scroll on router change`

layout.component.pug

.wrapper(#outlet="")
    router-outlet((activate)='routerActivate($event,outlet)')

layout.component.ts

 public routerActivate(event,outlet){
    outlet.scrollTop = 0;
 }`
Community
  • 1
  • 1
1

If you are loading different components with the same route then you can use ViewportScroller to achieve the same thing.

import { ViewportScroller } from '@angular/common';

constructor(private viewportScroller: ViewportScroller) {}

this.viewportScroller.scrollToPosition([0, 0]);
sandy
  • 278
  • 2
  • 8
1

You can also use scrollOffset in Route.ts. Ref. Router ExtraOptions

@NgModule({
  imports: [
    SomeModule.forRoot(
      SomeRouting,
      {
        scrollPositionRestoration: 'enabled',
        scrollOffset:[0,0]
      })],
  exports: [RouterModule]
})
Arvind Singh
  • 642
  • 1
  • 10
  • 27
0

@Fernando Echeverria great! but this code not work in hash router or lazy router. because they do not trigger location changes. can try this:

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)
    }
  })
}
0

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);
      }        
    }
}
Poul Kruijt
  • 58,329
  • 11
  • 115
  • 120
0

This worked for me best for all navigation changes including hash navigation

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);
    }
  });
}
Jorg Janke
  • 837
  • 6
  • 9
0

The main idea behind this code is to keep all visited urls along with respective scrollY data in an array. Every time a user abandons a page (NavigationStart) this array is updated. Every time a user enters a new page (NavigationEnd), we decide to restore Y position or don't depending on how do we get to this page. If a refernce on some page was used we scroll to 0. If browser back/forward features were used we scroll to Y saved in our array. Sorry for my English :)

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();
  }
}
0

window.scrollTo() doesn't work for me in Angular 5, so I have used document.body.scrollTop like,

this.router.events.subscribe((evt) => {
   if (evt instanceof NavigationEnd) {
      document.body.scrollTop = 0;
   }
});
Rohan Kumar
  • 38,998
  • 11
  • 69
  • 99
0

window scroll top
Both window.pageYOffset and document.documentElement.scrollTop returns the same result in all the cases. window.pageYOffset is not supported below IE 9.

app.component.ts

import { Component, HostListener, ElementRef } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  isShow: boolean;
  topPosToStartShowing = 100;

  @HostListener('window:scroll')
  checkScroll() {

    const scrollPosition = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;

    console.log('[scroll]', scrollPosition);

    if (scrollPosition >= this.topPosToStartShowing) {
      this.isShow = true;
    } else {
      this.isShow = false;
    }
  }

  gotoTop() {
    window.scroll({ 
      top: 0, 
      left: 10, 
      behavior: 'smooth' 
    });
  }
}

app.component.html

<style>
  p {
  font-family: Lato;
}

button {
  position: fixed;
  bottom: 5px;
  right: 5px;
  font-size: 20px;
  text-align: center;
  border-radius: 5px;
  outline: none;
}
  </style>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>
<p>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus, repudiandae quia. Veniam amet fuga, eveniet velit ipsa repudiandae nemo? Sit dolorem itaque laudantium dignissimos, rerum maiores nihil ad voluptates nostrum.
</p>

<button *ngIf="isShow" (click)="gotoTop()"></button>
Yogesh Waghmare
  • 496
  • 3
  • 6
0

For everyone who is looking for a solution and read up to this post. The

imports: [
  RouterModule.forRoot(routes, {
    scrollPositionRestoration: 'enabled'
  }),
  ...
]

doesn't answer the question of the topic. If we look into Angular source code then we could read there interesting lines:

enter image description here

So this stuff will only work on back navigation. One of the solutions could be something like:

constructor(router: Router) {

    router.events
        .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
        .subscribe(() => {
            this.document.querySelector('#top').scrollIntoView();
        });
}

This will look on each navigation to the div with that id and scroll to it;

Another way of doing that is to use the same logic but with help of decorator or directive which will allow you to select where and when to scroll top;

0
lastRoutePath?: string;

ngOnInit(): void {
  void this.router.events.forEach((event) => {
    if (event instanceof ActivationEnd) {
      if (this.lastRoutePath !== event.snapshot.routeConfig?.path) {
        window.scrollTo(0, 0);
      }
      this.lastRoutePath = event.snapshot.routeConfig?.path;
    }
  });
}

it won't scroll to the top if you stay on the same page, but only change the slug / id or whatever