angular: scrollPositionRestoration has several problems

I’m submitting a…


[ ] Regression (a behavior that used to work and stopped working in a new release)
[x] Bug report  
[ ] Performance issue
[x] Feature request
[x] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
[ ] Other... Please describe:

Current behavior

I started experimenting with the new scrollPositionRestoration feature in the RouterModule extra options. I expected scroll restoration to work by default when setting the property to ‘enabled’, but it doesn’t. And the documentation has issues, too.

Expected behavior

The documentation says:

‘enabled’–set the scroll position to the stored position. This option will be the default in the future.

So I naïvely thought that setting the flag to ‘enabled’ would be sufficient to restore the scroll position. But it isn’t.

Indeed, the scroll event is fired, and the scroll position is restored, before the ngAfterViewInit hook of the activated component has been called. So the view of the component is not ready yet when the router tries to restore the scroll position (i.e. there is no way to scroll to the end of a long list, because the list isn’t there yet).

And even if it was restored after the view is ready, that would only work if the activated component used a resolved guard to load the data.

So, the documentation should, IMHO, at least indicate that restoring the scroll position always requires to

  • explicitly intercept the Scroll event, and scroll imperatively after a delay. This can be done in a single place, but I don’t see how to do that in a reliable way, since there is no way to know if the delay is sufficient for the data to have been loaded (but it would at least work if resolve guards are used consistently), or
  • explicitly intercept the Scroll event in each routed component, and imperatively scroll when the data has been loaded and the view has been rendered. This is not a trivial task.

I read the remaining of the documentation, which has examples about doing this kind of stuff (although it doesn’t really say that they’re required). But those examples are all incorrect.

Here’s the first example:

    class AppModule {
     constructor(router: Router, viewportScroller: ViewportScroller, store: Store<AppState>) {
       router.events.pipe(filter(e => e instanceof Scroll), switchMap(e => {
         return store.pipe(first(), timeout(200), map(() => e));
       }).subscribe(e => {
         if (e.position) {
           viewportScroller.scrollToPosition(e.position);
         } else if (e.anchor) {
           viewportScroller.scrollToAnchor(e.anchor);
         } else {
           viewportScroller.scrollToPosition([0, 0]);
         }
       });
     }
    }

This example uses a Store service, which is not part of Angular (I guess it’s part of ngrx). So that makes it hard to understand and adapt for those who don’t use ngrx.

Besides, it doesn’t compile, because a closing parenthesis is missing, and because e is of type Event, and not of type Scroll, and thus has no position property.

The second example is the following:

    class ListComponent {
      list: any[];
      constructor(router: Router, viewportScroller: ViewportScroller, fetcher: ListFetcher) {
        const scrollEvents = router.events.filter(e => e instanceof Scroll);
        listFetcher.fetch().pipe(withLatestFrom(scrollEvents)).subscribe(([list, e]) => {
          this.list = list;
          if (e.position) {
            viewportScroller.scrollToPosition(e.position);
          } else {
            viewportScroller.scrollToPosition([0, 0]);
          }
        });
      }
    }

It doesn’t compile because it still uses an old, non-pipeable operator, and because, once again, e is of type Event, not Scroll.

But even after fixing the compilation errors, it doesn’t work because the view hasn’t been updated with the new list yet when viewportScroller.scrollToPosition(e.position); is called.

So the code would have to be changed to the following in order to compile and work as expected

    class ListComponent {
      list: any[];
      constructor(router: Router, viewportScroller: ViewportScroller, fetcher: ListFetcher) {
        const scrollEvents = router.events.filter(e => e instanceof Scroll);
        listFetcher.fetch().pipe(withLatestFrom(scrollEvents)).subscribe(([list, e]) => {
          this.races = list;
          const scrollEvent = e as Scroll;
          of(scrollEvent).pipe(delay(1)).subscribe(s => {
            if (s.position) {
              viewportScroller.scrollToPosition(s.position);
            } else {
              viewportScroller.scrollToPosition([0, 0]);
            }
          });
        });
      }
    }

I think that none of these solutions is really simple enough, though. Here are two ideas that could maybe make things easier:

  • only fire the Scroll event and try to restore the position after the ngAfterViewInit hook has been called. This should at least make things work when a resolve guard is used to load the list. Or when the list is available immediately.
  • for the other cases, allow to inject a service that the component could call when the list has been loaded. It would be up to this service to get the last scroll position or anchor, to wait until the view has been rendered, and then to restore the scroll position. It would ignore all but the first call after the component has been activated.

Minimal reproduction of the problem with instructions

Here’s a repo illustrating the various issues and solutions presented above: https://github.com/jnizet/scrollbug. It’s a standard angular-cli project. I can’t run it in stackblitz unfortunately (probably because Stackblitz doesn’t support the beta release of Angular).

What is the motivation / use case for changing the behavior?

First, the documentation should be fixed and made clearer

  1. it should not use ngrx
  2. it should contain examples that compile, and run as expected
  3. it should make it clear than simply setting the flag to ‘enabled’ is not sufficient to enable scroll restoration

Second, it should be way easier to make that feature work. See ideas above.

Environment


Angular version: 6.1.0-beta.1


Browser:
- [x] Chrome (desktop) version 67.0.3396.87
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [x] Firefox version 60.0.2 
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
 

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 285
  • Comments: 114 (28 by maintainers)

Commits related to this issue

Most upvoted comments

I have resolved this issue by implementing custom scroll restoration behavior.

The reason of this behavior is that page didn’t already render and scrollToPosition no have effect.

There are not a good hack with timeout but it works.

export class AppModule {
  constructor(router: Router, viewportScroller: ViewportScroller) {
    router.events.pipe(
      filter((e): e is Scroll => e instanceof Scroll)
    ).subscribe(e => {
      if (e.position) {
        // backward navigation
        setTimeout(() => {viewportScroller.scrollToPosition(e.position); }, 0);
      } else if (e.anchor) {
        // anchor navigation
        setTimeout(() => {viewportScroller.scrollToAnchor(e.anchor); }, 0);
      } else {
        // forward navigation
        setTimeout(() => {viewportScroller.scrollToPosition([0, 0]); }, 0);
      }
    });
  }
}

Just leaving this here for all future visitors, as I did some debugging to determine why this wasn’t working. For anyone who sees this in the future. If you have the following code in your stylesheet…

html,body {height: 100%}

This expected functionality will appear to be a bug for you.

Removing that style has a decent potential to fix this issue and make this PR do what was intended. Granted, removing that style may make a bunch of other things break, but that’s a different problem.

Ninja edit: It also appears that this seems to intermittently work with https://github.com/angular/material2

Working Version (My own personal anecdote): I have a working app with material (which uses mat-sidenav) and all window.scroll variants work as expected.

Non-working version (as reported by @crisbeto ): https://github.com/angular/material2/issues/11552

Sorry

@mhevery @jasonaden please reopen.

This issue is not about a missing scroll position restoration. It’s precisely about the new scroll position restoration feature that was introduced in 6.1.0-beta.1.

And it thus can’t be closed by #20030: this issue is precisely about several problems in the code and documentation introduced by #20030.

Thanks for the feedback so far on this issue. We’re going to be making some minor improvements to the current implementation, but also will be adding some new documentation in the next few weeks to help with some of the scenarios you guys have been having trouble with. Thanks for the patience on this one.

When using with rxjs, all i needed to do, was adding a delay of zero. All is working smoothly, except IE. Any chance an scroll polyfill needs to be added?

app.component.ts

constructor(
	private cd: ChangeDetectorRef,
	private readonly router: Router,
	public swUpdate: SwUpdate,
	private viewportScroller: ViewportScroller,
) {
       this.router.events
		.pipe(
			filter((e: any): e is Scroll => e instanceof Scroll),
			delay(0),
		)
		.subscribe(e => {
			if (e.position) {
				viewportScroller.scrollToPosition(e.position);
			} else if (e.anchor) {
				viewportScroller.scrollToAnchor(e.anchor);
			} else {
				viewportScroller.scrollToPosition([0, 0]);
			}
		});
}

app-routing.module.ts

RouterModule.forRoot(appRoutes, {
	enableTracing: false,
	onSameUrlNavigation: 'reload',
	scrollPositionRestoration: 'enabled',
}),

Just chiming in here, my initial expectation of this feature was the following:

  1. The “scrollPosition”, specifically navigation to the point previously visited on prior pages, was going to be handled internally by angular with “sane” defaults that are simple to configure (‘enabled’|‘disabled’|‘top’ make perfect sense as outlined by @vsavkin).

To test this idea I tried the following as a simple use-case:

With scrollPositionRestoration set to enabled, I navigate halfway down “Page A” and click a link to “Page B”. I expect to be at the top of “Page B”, not at the height of the point I was at on “Page A” (this is what I currently experience). Next, if I scroll to another point on “Page B” and navigate back to “Page A”, I should be at the halfway point I was at on “Page A” (Not at the other point I scrolled to on “Page B”, what I currently experience).

This simple use-case doesn’t seem to be resolved by the aforementioned #20030. Is that PR supposed to solve this use case?

But, just like @jnizet after upgrading a simple project to 6.1.0-rc1 it appears that these options don’t actually do anything (maybe I’m missing something)?

I’m curious about the following:

  1. Are we expected to implement this scroll functionality on our own with the help of the ViewportScroller?
  2. What do the flags actually do out-of-the-box?

Sorry this is a mess. Here are multiple issues in one. You don’t even care to split those and work them out one by one.

Then I look into your code and see things… It really looks like code by Java developer very simple but without any further knowledge of DOM APIs and the needs of web developers.

@damienwebdev Thanks for that note on the body height. That very well could help quite a number of people

On the other things discussed in this issue, I’ve talked with @vsavkin about it and we’re going to solve this in a couple ways. First is some documentation to make sure your use cases are discussed and solutions given for some of the common patterns. That documentation will be forthcoming.

Another is to fix an issue that was introduced some time ago where it’s possible to have a NavigationEnd event fire before change detection. We need to change this so NavigationEnd happens after CD to make sure the page is actually rendered before trying to scroll.

Thanks for the detailed issue report! Great to get feedback on this new feature and hopefully get these things addressed quickly.

It’s not quite clear, is there any way to set the scroll policy for certain routes?

Would appreciate it if you discuss other frameworks in private messages and keep the discussion ontopic. Thank you.

Hate to say it but I left for VueJS over this. This was the straw that broke the camel’s back. So many basic functions in Angular that’s been open issues for a year+

scrollPositionRestoration has another issue. If you load data using a ‘load more’-type component that updates the query parameters, the default behaviour of scrolling to the top of the page breaks the default ui (additional data is rendered below the current and the scroll position should not change). That’s a very common ui that should be accounted for.

There could be an additional argument that specifies the behaviour when a query parameter is updated, rather than just treating it the same as a full route change.

I don’t want to sound too critical of the feature, it seems to work great on static pages, but the current solution seems like an oversimplification the second you start loading content dynamically.

@kolombet I was able to get a custom ViewportScroller implementation working and it solved my problem. In your app module, use a factory just like Angular does internally with their BrowserViewportScroller implementation:

providers: [
  {
    provide: ViewportScroller,
    useFactory: () => new CustomViewportScroller('content-scroller', ɵɵinject(DOCUMENT), window, ɵɵinject(ErrorHandler))
  },

In this simple implementation, I’m passing a string with an ID of my scrolling element (a div in this case). You would obviously want that to be in a config file or something. My custom constructor looks like this:

constructor(private scrollElementID: string, private document: Document, private window: any, private errorHandler: ErrorHandler) { }

After that, I just modified getScrollPosition and scrollToPosition to be whatever I want. Something like this might work:

/**
 * Retrieves the current scroll position.
 * @returns The position in screen coordinates.
 */
getScrollPosition(): [number, number] {
    const scrollEl = this.document.querySelector(`#${this.scrollElementID}`);
    if (this.supportScrollRestoration() && scrollEl) {
        return [scrollEl.scrollLeft, scrollEl.scrollTop];
    } else {
        return [0, 0];
    }
}

/**
 * Sets the scroll position.
 * @param position The new position in screen coordinates.
 */
scrollToPosition(position: [number, number]): void {
    const scrollEl = this.document.querySelector(`#${this.scrollElementID}`);
    if (this.supportScrollRestoration() && scrollEl) {
        // Total hack but waiting for for content/images to load to give us a 
        // better chance of hitting our scroll target. It also gives the UI a bit
        // of movement to show users that we scrolled them after page load. In a
        // real implementation of ViewportScroller, we should get rid of this but
        // it suits my current needs.
        setTimeout(() => {
            scrollEl.scrollTo(position[0], position[1]);    
        }, 200);
    }
}

Anyway, hopefully this helps anybody who wants to go with a custom ViewportScroller implementation. Here’s a working StackBlitz demonstrating all of this.

And final word of caution, as with my previous comment about tightly coupling yourself to the DOM, you need to make sure you’re executing on a browser.

Agreed. I will continue to lobby the team for it. Hopefully this will get into a v9 minor or v10. 🤞🏼

Angular 7.1.2 | Material 7.1.1 (using ‘mat-sidenav’). Having the same issue. However,

  • Lukasz196 css solution doesn’t seem to apply.

‘@HostListener(‘window:scroll’)’ functionality doesn’t seem to work as well with this layout type.

@damienwebdev - Can you supply a stackblitz showing your method to how it works for you?

I have a working app with material (which uses mat-sidenav) and all window.scroll variants work as expected.

Side Note: I just started a huge project three months ago using Angular for the first time and I am very disappointed that a simple scrolling function(s) after 7 version releases is still broke. We just started mocking with large data sets and realized that the UI cannot scroll to the top when the user switches views. Imagine scrolling on a mobile device dozens of items down then needing to go back a view that has just as many and having to manually thumb-it back to the top? We can’t even add a simple “scroll-to-top” fab because the ‘window:scroll’ seems to break with Material2 as well (I think the two are related). This is a very basic, 101 function for UX.

Should we (angular/angular) be communicating/working together with the crew at angular/material2 with this bug/issue? I may be wrong, but I am thinking that this is a “right-hand, left-hand” situation(?).

@lnaie Your comments from 3 days ago are inappropriate and violate the Code of Conduct. I suggest you review that, and try to communicate with professionalism and respect.

On Chrome, Using min-height doesn’t break auto-scrolling and will force body t be at least screen height. https://stackoverflow.com/questions/3740722/min-height-does-not-work-with-body

html {
    min-height: 100%;
    display: flex;
}
body {
    min-height: 100%;
    flex: 1;
}

I just stumbled upon this issue. In my case my scroll container unfortunately is not the window therefore the scrollPositionRestoration ‘default’ does not work. I wrote a quick workaround and just wanted to leave this here in hope it might help someone:

export class AppRoutingModule {

  scrollTopPositions: { [url: string]: number } = {};

  constructor(router: Router) {
    router.events.pipe(
      filter((e: RouterEvent) => e instanceof NavigationStart || e instanceof NavigationEnd)
    ).subscribe({
      next: (e: RouterEvent) => {
        const scrollContainer = document.getElementById('scrollableContent');
        if (e instanceof NavigationStart) {
          this.scrollTopPositions[router.url] = scrollContainer.scrollTop;
        } else if (e instanceof NavigationEnd) {
          const newUrl = router.url;
          setTimeout(() => {
            scrollContainer.scrollTop = this.scrollTopPositions[newUrl];
          }, 0);
        }
      }
    });
  }
}

@aFarkas you didn’t hurt my feelings. I was just trying to help you avoid getting banned.

If you feel like you can break this down into a separate issue with a clear reproduction, please open a new issue and reference this one.

If you feel like you have a solution to that issue, please open a PR.

surprised by the ignorance towards this issue and its triage

It seems like there is a bit of a language barrier here. I understand that it is frustrating when an issue that is important to you is not fixed quickly. However, the team has been focused on other priorities like shipping Ivy, improving template type checking, runtime performance, etc. Hopefully some of the feature areas that have gotten less attention lately (forms, router) will get some attention post-version 9 (after Ivy ships).

is this ever gonna be fixed?

Just to confirm what is posted above. Simple examples work like they should, but as soon as you do some kind of loading of data with a resolver, the timing will be off and the NavigationEnd-event (and therefor Scroll-event) will fire before the actual page is shown. This results in no scrolling at all.

An update on the timeline would be appreciated 😃 (or a workaround of course)

One additional note I would like to mention here: In some frameworks like IONIC 3 or other applications, the scrolling element is by default not the document body.

(I did workaround this by setting all parent elements until the body to position: static / relative (they were position: absolute before), which introduced other problems, e.g. with modal dialogs, backdrops or scroll bar issues with fixed elements on different OS with different settings…)

Feature request:

  • A possibility to register another scrolling element.
  • Handle multiple elements for scrolling. (router outlets + custom elements)

Potential problems

  • Multiple / nested router outlets
    • Possibility to set and get the scroll position for each one (Note: The default scrolling element could be the parent element of the router outlet element, but we would maybe need to register another one.)
  • Async between different router outlets and their scrolling position until the components are finished with loading and building the desired content (perceived performance strikes here again).
    • Note: Yes one could also resolve first the data. But how to handle perceived performance here? Maybe setting a component into a specific position while resolving the data?

@michaelurban - thank you for highlighting this issue.

As you can see from the many comments and suggestions, this is not a trivial feature to implement - indeed there are a number of approaches discussed above.

This issue was discussed in a feature triage at the start of September and while we agree it is a useful feature to add, it is a significant effort and one that needs designing carefully. As such, it has been marked as needing a project proposal. What this means is that, one the proposal is complete, it will go into our prioritisation process and eventually get added to the backlog of work for the team to do, based upon its priority compared to other work.

We can update here as this proceeds.

In the meantime, you may well be able to unblock yourself by applying one of the workarounds mentioned above.

For those who use Angular Material or Ionic …

Here is how I solved the scroll problem. For Ionic, it will be necessary to adapt the code, it is the same principle.

In app.component.html add #sidenavContent reference

<mat-sidenav-container>
  ...
  <mat-sidenav-content #sidenavContent>
    ...
  </mat-sidenav-content>
</mat-sidenav-container>

In app.component.ts

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
  @ViewChild('sidenavContent', {read: MatSidenavContent}) sidenavContentScrollable?: MatSidenavContent;

  subscriptions$ = new Subject<any>();

  constructor(
    @Inject(DOCUMENT) private document: Document,
    private router: Router
  ) {
  }

  ngOnInit(): void {
    this.router.events
      .pipe(
        filter((event: Event): event is Scroll => event instanceof Scroll),
        takeUntil(this.subscriptions$)
      )
      .subscribe((event: Scroll) => {
        if (!this.sidenavContentScrollable) {
          return;
        }

        const nativeElement = this.sidenavContentScrollable.getElementRef().nativeElement;

        if (event.position) {
          // backward navigation
          nativeElement.scroll(event.position[0], event.position[1]);
        } else if (event.anchor) {
          // anchor navigation
          const offsetTop = this.document.getElementById(event.anchor)?.offsetTop || 0;

          nativeElement.scroll(0, offsetTop);
        } else {
          // forward navigation
          nativeElement.scroll(0, 0);
          
          // An alternative the scrolling box scrolls in a smooth fashion, see browser compatibility before
          // https://developer.mozilla.org/en-US/docs/Web/API/Element/scroll#browser_compatibility

          // this.sidenavContentScrollable.scrollTo({left: 0, top: 0, behavior: 'smooth'});
        }
      });
  }

  ngOnDestroy(): void {
    this.subscriptions$.next();
    this.subscriptions$.complete();
  }
}

A general approach used here by most is to delay the call to viewportScroller.scrollToPosition(). This is not a very viable solution. If you are rendering data from a backend then it may take several seconds to fetch the data and fully populate the page. I think what we need is a simple API in the ActivatedRoute service to restore the scroll position. We should be able to call this API at any point to restore the scroll position.

Any news on the topic?

@mhevery (Edit: Sorry nevermind!)

I know one could preload all the data with Resolve from ‘@angular/router’. This would be fine and we could provide for each component an equal named class which could resolve all the needed data. Then the scroll position restoration could work just as expected. Usually this would also mean to show an overall spinner on the page, so the user would see there is something going on.

Another also very common practice is to use perceived performance. This is the way we decided to interact with preloading data. In our application we have a lot of times a list, which would show some grey boxes unless the data arrives and the list is shown.

As the component already is constructed and all the events are fired this would mean we would need to have the possibility to provide some asynchronous way to tell the data has been loaded for the scroll restoration service.

Do you see the possibility to provide a:

1. CustomScrollPositionRestorationStrategy or 2. another Lifecycle Hook

interface AfterContentReady {
    ngAfterContentReady(): Promise<boolean>;
}

I am also open for other ways. Maybe one would have some suggestions?

EDIT: Okay, one way to go would be to store the value and call the scrollToPosition method of the ViewportScroller yourself. E.g. Create an abstract class, from which you inherit and call a resolve method, every time you know your data is loaded and call in this abstracts class method, which implements this call this.viewportScroller.scrollToPosition(this.scrollPosition); - you get the idea.

Excerpt from: https://github.com/Ninja-Squad/ninja-squad.github.com/pull/268/files https://blog.ninja-squad.com/2018/07/26/what-is-new-angular-6.1/

export class PendingRacesComponent {
  scrollPosition: [number, number];
  races: Array<RaceModel>;

  constructor(route: ActivatedRoute, private router: Router, private viewportScroller: ViewportScroller) {
    this.races = route.snapshot.data['races'];
    this.router.events.pipe(
      filter(e => e instanceof Scroll)
    ).subscribe(e => {
      if ((e as Scroll).position) {
        this.scrollPosition = (e as Scroll).position;
      } else {
        this.scrollPosition = [0, 0];
      }
    });
  }

  ngAfterViewInit() {
    this.viewportScroller.scrollToPosition(this.scrollPosition);
  }

}

Why this solution is hard to resolve like this? It’s almost 2 years

@aFarkas I’m not really sure who you are responding to. However, please note that the Code of Conduct prohibits personal attacks and insults.

I wonder too - especially when the core is literally half the weight (and caries more out of the box!).

I too struggled with this issue but I managed to fix it with a polyfill from Ben Nadel!

Links to all important sources and explanations are in his blog post about the polyfill: https://www.bennadel.com/blog/3534-restoring-and-resetting-the-scroll-position-using-the-navigationstart-event-in-angular-7-0-4.htm

You can check out the demo over here: http://bennadel.github.io/JavaScript-Demos/demos/router-retain-scroll-polyfill-angular7/with-polyfill/#/app

For example: Go to “Section B”, wait for the content to load and scroll a bit > go to “Section C” > press back button > profit!

It works like a charm! The only thing that might be handy is making this an NPM package as I’ve copied the code into my project.

P.s.: I’m using Angular Material and even with the mat-sidenav component I managed to get this to work as expected 👍

As an addition, I want to report that when using the following code

        this.router.navigate([], {
            relativeTo: this.route,
            queryParams: queryParams,
            replaceUrl: true
        });

The routerevents, including scrolling, will happen as well. This is not expected behaviour, if you ask me. When replacing only the url (for example by changing the queryParams, as in the example above), you don’t want the page to suddenly scroll to the top of the page as well.

I tried to use skipLocationChange as well, without success.

Whats the status on the issue? Hash anyone found a perfect solution/ when will this issue be fixed?

Oh yes, I know you can do that but it boils down to UX - Do I want the user have to download more and create more over head on the browser?

I’d like to add in combination with Ionic 4 the ViewportScroller’s Scroll event position property is always [0, 0] See minimal repro: https://github.com/coonmoo/IonicScrollPosBug

It seems that af8afee addresses some change detection portion of this issue, though I am not if that anyhow resolves the issue for those people that don’t have html/body their scrollable element. It would be nice if we could pass either a specific selector for scrollable container ourselves or some kind of custom strategy for resolving the component dynamically ourselves.

When using with rxjs, all i needed to do, was adding a delay of zero. All is working smoothly, except IE. Any chance an scroll polyfill needs to be added?

app.component.ts

constructor(
	private cd: ChangeDetectorRef,
	private readonly router: Router,
	public swUpdate: SwUpdate,
	private viewportScroller: ViewportScroller,
) {
       this.router.events
		.pipe(
			filter((e: any): e is Scroll => e instanceof Scroll),
			delay(0),
		)
		.subscribe(e => {
			if (e.position) {
				viewportScroller.scrollToPosition(e.position);
			} else if (e.anchor) {
				viewportScroller.scrollToAnchor(e.anchor);
			} else {
				viewportScroller.scrollToPosition([0, 0]);
			}
		});
}

app-routing.module.ts

RouterModule.forRoot(appRoutes, {
	enableTracing: false,
	onSameUrlNavigation: 'reload',
	scrollPositionRestoration: 'enabled',
}),

@rickvandermey thanks your solution worked for me

I have the same problems. None of the solutions above worked. I have no height: 100% on my html or body. I use Angular 8 + Resolver. So the routing just ends when the data is loaded and the view is ready. But nevertheless, it doesn’t work! On every route and every back button it’s scrolling to top.

When I remove the option completely, then the scrolling to the top doesn’t work neither.

Using mat-dialog with angular material and having issues?

You must wait for the dialog to close before redirecting - or as others have said you can have issues related to height: 100% (or similar).

  this.dialogRef.afterClosed().subscribe(() => {
      this.router.navigate(redirect);
  }); 
  this.dialogRef.close();

Note that afterClosed() emits only once and doesn’t need unsubscribing from.

@lnaie That is intended. This feature will work when use browser’s navigating back. Redirecting to ../ is not navigating back, it’s going up one level.

Hi everybody,

concerning this issue, you could have a look at NgxScrollPositionRestoration. A library for scroll position restoration in Angular.

The library supports scroll position restoration on:

  • Any scrollable element (for example: .mat-sidenav-content in case you are using Angular Material Sidenav)
  • Lazy loading content
  • Named router-outlets (multiple router-outlets)
  • Child routes (nested router-outlets)
  • Backward and forward navigation

View the demo

Compatibility

Angular version Compatible with (tested on)
Angular 6 Yes
Angular 7 Yes
Angular 8 Yes
Angular 9 Yes
Angular 10 Yes
Angular 11 Yes
Angular 12 Yes
Angular 13 Yes

Another documented Angular feature that has been broken out of the box for 2+ years. This is my fourth this quarter.

A nice option would be to allow to set a custom reference for the scroll-behavior of the RouterModule to attach to. So that the call signature would be like

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

with a sidenav for example:

<mat-sidenav-container>
  ...
  <mat-sidenav-content #scrollMe>
    ...
  </mat-sidenav-content>
</mat-sidenav-container>

Disabling

scroll-behavior: smooth;

worked for me in FF.

@iank- not at this time. An update on this was just posted 12 days ago. If there are more updates to provide, they will be posted here. It’s not beneficial to ask periodically for an update as that doesn’t provide value to the 52 other subscribers to this issue.

If you have a critical use case that you need to have addressed and want to raise the priority of issues like this, you can reach out to devrel@angular.io.

made a simple stackblitz to demonstrate the bug (you might need to open result in a separate window!) https://stackblitz.com/edit/angular-jjsm86 Steps to reproduce:

  1. Scroll until you see the link Page.
  2. click on it.
  3. go back.

To ‘fix’ it, uncomment the line with a bunch of breaks in page.component.html file and try again.

P.S. I use breaks as oppose to height property, so make sure there’s enough of them for scroll to appear.

Just leaving this here for all future visitors… as I did some debugging to determine why this wasn’t working… For anyone who sees this in the future. If you have the following code in your stylesheet…

html,body {height: 100%}

This will be a bug for you.

Removing that fixes that and makes this PR do what was intended. Granted, removing that style may make a bunch of other things break, but that’s a different problem.

Ninja edit: It also appears that this seems to intermittently work with https://github.com/angular/material2

Working Version (My own personal anecdote): I have a working app with material (which uses mat-sidenav) and all window.scroll variants work as expected.

Non-working version (as reported by @crisbeto ): angular/components#11552

I actually need html, body height=“100%” in order to use cdk-virtual-scroll-viewport with height=“100%”. I used the solution provided by you but didn’t achive the desired result. The scrollTo functions still won’t work.

This makes scrollPositionRestoration incompatible with cdk-virtual-scroll…

Any advice? thanks.

Edit: Solved it with @ouijan solution

@StefanRein, thanks for your example, it worked for me. But I don’t like to put any code inside the constructor, except injections. And if we move it to the ngOnInit, it won’t work.

So, inspired by your solution, I desided to move subscription to the root App component and listen router event there. And in child component we only need to trigger scrollToPosition from the root. It is more reusable and no code inside constructor 😃

export class AppComponent implements OnInit, OnDestroy {

    private scrollPosition: [number, number] = [0, 0];
    private scrollPositionSubscription: Subscription = new Subscription();

    constructor(
        private router: Router,
        private viewportScroller: ViewportScroller
    ) {}

    ngOnInit(): void {
        this.scrollPositionSubscription = this.router.events.pipe(
            filter((e: Event) => e instanceof Scroll),
            tap((e: Event) => {
                this.scrollPosition = (e as Scroll).position ? (e as Scroll).position : [0, 0];
            })
        ).subscribe();
    }

    ngOnDestroy(): void {
        this.scrollPositionSubscription.unsubscribe();
    }

    restoreScrollPosition(): void {
        this.viewportScroller.scrollToPosition(this.scrollPosition);
    }
}

…and child component…

export class ChildComponent implements AfterViewInit {
    constructor(
        @Inject(forwardRef(() => AppComponent)) private appComponent: AppComponent
    ) {}

    ngAfterViewInit(): void {
        this.appComponent.restoreScrollPosition();
    }
}

Until angular addresses the issue, this solution worked best for me. You might need to tweak for your use case. I borrowed some of the code from this blog post https://blog.angularindepth.com/reactive-scroll-position-restoration-with-rxjs-792577f842c

import { Event, Routes, RouterModule, Route, Router, NavigationStart, NavigationEnd } from '@angular/router';

interface ScrollPositionRestore {
    event: Event;
    positions: { [K: number]: number };
    trigger: 'imperative' | 'popstate' | 'hashchange';
    idToRestore: number;
}

@NgModule(...)
export class AppRoutingModule {

    constructor(
        public router: Router,
    ) {
        let lastScrollTop = 0;

        fromEvent(window, 'scroll')
            .pipe(
                debounceTime(50),
                tap(() => {
                    lastScrollTop = document.querySelector('html').scrollTop;
                }),
            )
            .subscribe();

        this.router.events
            .pipe(
                filter(
                    event =>
                        event instanceof NavigationStart || event instanceof NavigationEnd,
                ),
                scan<Event, ScrollPositionRestore>((acc, event) => {
                    return {
                        event,
                        positions: {
                            ...acc.positions,
                            ...(event instanceof NavigationStart
                                ? {
                                    [event.id]: lastScrollTop,
                                } : {}),
                        },
                        trigger:
                            event instanceof NavigationStart
                                ? event.navigationTrigger
                                : acc.trigger,
                        idToRestore:
                            (event instanceof NavigationStart &&
                                event.restoredState &&
                                event.restoredState.navigationId + 1) ||
                            acc.idToRestore,
                    };
                }),
                filter(({ event, trigger }) => event instanceof NavigationEnd && !!trigger),
                observeOn(asyncScheduler),
            )
            .subscribe(({ trigger, positions, idToRestore }) => {
                if (trigger === 'imperative') {
                    document.querySelector('html').scrollTop = 0;
                }

                if (trigger === 'popstate') {
                    document.querySelector('html').scrollTop = positions[idToRestore];
                }
            });
    }
}

For my project in Angular 6.1.9 work solution described in link bellow (for jQuery but this is only css). https://stackoverflow.com/a/18573599/4490997

html {
  height: 100%;
  overflow: auto;
} 
body {
  height: 100%;
}

then

{scrollPositionRestoration: 'top'}

works

@goat67 it’s a known issue with how this feature works with md-sidenav-container and/or other containers like Ionic uses. This is mentioned above and in https://github.com/angular/material2/issues/4280.

what’s the status? I just upgraded from angular 6 to 6.1 (took me 3 hours 😄) to implement this feature but now it turns out it’s not working at all (at least for me) I just want the page to start at the top without writing tons of code, so I use scrollPositionRestoration: 'top', ?

Edit: it’s working now

explicitly intercept the Scroll event in each routed component, and imperatively scroll when the data has been loaded and the view has been rendered. This is not a trivial task.

This is a must after watching Jason’s video about ng6.1 features where he says : […] be aware on when and where you’re rendering out your position.

Video: https://youtu.be/jNsbB8V9u0A?t=6m10s

@jasonaden Could you tell, if following use case will be covered:

We are using perceived performance and intentionally are loading the data every time on component initialization, instead of caching, thus the app could be used with multiple devices at the same time. Will there be a solution to tell the scroll restoration when the data of my component did load, so the view could be updated, elements could be created and the service finally would be able to scroll to the correct scroll position?

Thank you in forward.

@DeanPDX Yes, as I previously stated, in this case the documentation is incomplete/oversimplified rather than incorrect.

The feature also needs some work aside from the documentation but, the core issue is: Sometimes in Angular things are not as simple or as solid the documentation would have you believe. If a feature has several caveats, special cases, or, has not worked for years (as in #30477), that should be spelled out clearly in the docs.

I would be very happy to review and help merge updates to the docs that highlight these known issues, if you would like to send in some PRs.

@petebacondarwin That is an impressively byzantine process. And I would consider it an impressive process if I had not seen several issues go through it, receive a “P<number>” status (or the equivalent priority under the old system), only to sit unresolved for months to years.

This is my core problem:

A few of the issues that I’ve been tracking for a long time are related to documented features. Most of those features come with sample code in the documentation that a reasonable person should expect to work. But the sample code doesn’t work. Instead of the documentation highlighting the fact the the feature is broken and linking to the GH issue, hours are lost trying to debug example code that will never work, that is known not to work, and, often, that has not worked for years

Yes, each individual issue can be updated as work proceeds (assuming the issue isn’t automatically closed due to inactivity after being open for an extended period of time) but the documentation remains unchanged during that time. How many issues can each developer be expected to track? How many developers are expected to track them? How long will these developers be required to track them? It adds up.

Keeping broken or shallow sample code in the documentation is deeply disrespectful to the developer community.

In this case, the short and swish documentation for scrollPositionRestoration https://angular.io/api/router/ExtraOptions#scrollPositionRestoration does not hint at the deep conversations taking place in this thread. The flag is presented plainly and it’s indicated that enabled will be the default behavior in the future. The user should not be surprised that the ice cube they confidently ran their ship into turns out to be an iceberg.

Not updating the documentation to reflect the current state of the project is deeply disingenuous. If doc-bug impedance mismatches were rare or if the underlying issues were resolved quickly that would be one thing. However, I come across an issue like this about once a month and, upon investigation, every one of my issues has been a known bug for, again, months or years.

Now with that out off the way, thank you for responding. It is a critical UX feature. I hope the issue moves through the prioritization process and is cleared from the backlog quickly.

How hard would it be to provide some sort of config option to set your scrolling element? Because I’m running in to this same problem and traced my router and found that the events are firing as they should, but due to the Angular Material config I’m using, body isn’t my scrolling element for my app (it’s my content div, with body/html being height 100%).

One other easy option I can see would be to allow use of custom implementations of ViewportScroller. I’m looking in to that now and will update with stackblitz if I can easily get it working.

@LorisBachert I appreciate that example. I’m not sure if you are concerned about this, but, that code isn’t 100% safe due to the fact that it creates a tight coupling between your app and rendering layers. Via the docs:

Relying on direct DOM access creates tight coupling between your application and rendering layers which will make it impossible to separate the two and deploy your application into a web worker.

If you wanted to double-check that you’re running in a browser, I’ve implemented that using a method like this:

import { Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

export class ExampleClass {
  /**
   * Indicates whether the current platform is a web browser and thus safe for DOM-specific code.
   */
  private isRunningInBrowser: boolean = null;

  constructor(@Inject(PLATFORM_ID) platformId: string) {
    this.isRunningInBrowser = isPlatformBrowser(platformId);
  }

  domSpecificFunction() {
    // Make sure we're running in a browser
    if (!this.isRunningInBrowser) {
      return;
    }
  }
}

Anyway, it probably doesn’t matter in your environment but I figured I’d mention it.

Same problem when using Angular 9 and route resolver. NavigationEnd is emitted before initialization of the view, and scroll is restored on incorrect position. It’s a serious bug after all this time with no update on this issue. But I think that it will be good to have another route event after initialization of the view because now I’m using asyncScheduler of rxjs module and it has negative effects. Here is simple example for scroll restoration service:

import { Injectable } from "@angular/core";
import { Event, NavigationEnd, NavigationStart, Router } from '@angular/router';
import { ViewportScroller } from '@angular/common';
import { filter, observeOn, scan } from 'rxjs/operators';
import { asyncScheduler } from 'rxjs';

@Injectable({
  providedIn: 'root'
})

export class RouterScrollProvider {

  constructor(
    private router: Router,
    private viewportScroller: ViewportScroller
  ) { }

  setRouterListener() {
    this.router.events
      .pipe(
        filter(
          event => event instanceof NavigationStart || event instanceof NavigationEnd,
        ),
        scan<Event, ScrollPositionRestore>((scroll, event) => {
          const extras = this.router.getCurrentNavigation().extras;
          return {
            update: extras.state ? extras.state.update : true,
            event,
            positions: {
              ...scroll.positions,
              ...(event instanceof NavigationStart
                ? {
                  [event.id]: this.viewportScroller.getScrollPosition(),
                }
                : {}),
            },
            trigger:
              event instanceof NavigationStart
                ? event.navigationTrigger
                : scroll.trigger,
            idToRestore:
              (event instanceof NavigationStart &&
                event.restoredState &&
                event.restoredState.navigationId + 1) ||
              scroll.idToRestore,
          }
        }),
        filter(
          ({ event, trigger, update }) => event instanceof NavigationEnd && !!trigger && update,
        ),
        observeOn(asyncScheduler),
      )
      .subscribe(({ trigger, positions, idToRestore }) => {

        if (trigger === 'imperative') {
          this.viewportScroller.scrollToPosition([0, 0]);
        }

        if (trigger === 'popstate') {
          const position = positions[idToRestore] || [0, 0];
          this.viewportScroller.scrollToPosition(position);
        }

      });
  }

}

interface ScrollPositionRestore {
  update: boolean,
  event: Event;
  positions: { [K: number]: [number, number] };
  trigger: 'imperative' | 'popstate' | 'hashchange';
  idToRestore: number;
}

And using a query param change on current route, restoration is terrible, here is an example on the code above if you change only query params on same route without change scroll position:

   this.Router.navigate([], {
      relativeTo: this.ActiveRoute,
      replaceUrl: true,
      state: {
       update: false
      },
      queryParams: query,
    });

I’ve tried using scrollPositionRestoration and indeed face the troubles mentioned in this issue. I’ve documented my findings and current solution here, just in case anyone is also looking for a solution: https://medium.com/@dSebastien/handling-scrolling-on-angular-router-transitions-e7652e57d964

It’s far from perfect, but it’s an improvement for apps that have fixed elements.

Another issue I would like to point out is that scroll restoration does not work well with router animations. If you navigate to a new route, and thus trigger a new animation, both components are displayed at the same time (which is fine, because you want a smooth transition). However, the ‘scroll to top’ behaviour for non-back navigation kicks in right at the start of the animation, meaning that the previous component is scrolling to the top. This makes the animations look less smooth. If we have a sane and sound way to control when scrolling should happen, this issue could potentially also be solved.

On Chrome, Using min-height doesn’t break auto-scrolling and will force body t be at least screen height. https://stackoverflow.com/questions/3740722/min-height-does-not-work-with-body

html {
    min-height: 100%;
    display: flex;
}
body {
    min-height: 100%;
    flex: 1;
}

Thank you very much @ouijan . After having searched several hours this was the fix. The body in my project had height:100%. After changing to min-height everything works well. One or two beers for you.

It’s possible disable anchorScrolling for some routes? (For example for auth callback rout, which use #access_token=… Thanks.

with scroll position sort of works in some browsers. works with the Location.back() only, in Chrome & IE, but it does not in FF v62, Safari for macos/ios – maybe you should fix that, btw 😃. and telling it in the docs would avoid confusion.