Solvedangular Keep scroll position when navigating back

I'm submitting a ... (check one with "x")

[ ] bug report => search github for a similar issue or PR before submitting
[x] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior
When navigating forward and then back, the scroll position is lost.

Expected/desired behavior
When navigating on an extensive list of items, the user scrolls down the list and click on some item. The app activates the next route/component which shows the product details. If the user decides do navigate back, the page does not scroll to the previous point.

Reproduction of the problem

Live example: http://plnkr.co/edit/vUROPirs5tjklyBw6t4H?p=preview
step 1 - first, adjust browser zoom until heroes from the hero list disappear
step 2 - scroll down to the end of the page
step 3 - click on "RubberMan"
step 4 - click on the back button either on the browser or the component
step 5 - the page is not scrolled to the same position of stop 2

What is the expected behavior?
The browser should scroll to the previous position like it happens in any non-single page application.

What is the motivation / use case for changing the behavior?
It should be considered a bug that can reduce the level of the user experience.

Please tell us about your environment:

  • Angular version: 2.0.0-rc.4
  • Browser: [all]
  • Language: [TypeScript 1.8 | ES5]
42 Answers

Ôťö´ŞĆAccepted Answer

The problem is harder than you may think.
It is relatively easy to just scroll top on any route change, but to scroll to previous scroll position is hard. You need to store the scroll position for each route you leave (preferably in session storage to make Duplicate tab browser feature work as expected) and restore it when the route is active again, but only when it active through navigating back, and not when user just clicked Heroes list again (how to determine if it is the first or second case?). Also the route you navigating to can have some dynamically loadable content, and the scroll position should be restored only after all this content is loaded again (or else it will be wrong scroll position).
On non-SPA applications the browser takes care of all this cases. It even understands ajax-loadable content and changes scroll position only after this content loaded again when navigating back/forward.
Maybe we can somehow use this browser feature to implement this, I don't know. But if it should be implemented fully on JS - it is hard problem I think.

But I definitely +100500 to this feature. It is very user unfriendly to not restore the scroll position on Back/Forward and in some cases it even can be the only reason to not use Angular or some other SPA framework at all.

Other Answers:

This works for me: app.component.ts
I use the pairwise() operator to catch previous router event:

export class AppComponent implements OnInit, OnDestroy {

  private _routeScrollPositions: {[url: string] : number}[] = [];
  private _subscriptions: Subscription[] = [];

  constructor(private router: Router) {}

  ngOnInit() {
    this._subscriptions.push(
      // save or restore scroll position on route change
      this.router.events.pairwise().subscribe(([prevRouteEvent, currRouteEvent]) => {
        if (prevRouteEvent instanceof NavigationEnd && currRouteEvent instanceof NavigationStart) {
          this._routeScrollPositions[prevRouteEvent.url] = window.pageYOffset;
        }
        if (currRouteEvent instanceof NavigationEnd) {
          window.scrollTo(0, this._routeScrollPositions[currRouteEvent.url] || 0);
        }
      })
    );
  }

  ngOnDestroy() {
    this._subscriptions.forEach(subscription => subscription.unsubscribe());
  }
}

Anyone know if utilizing RouteReuseStrategy is the official, intended way to handle this and make route scrolling behave however you want?

Like this example: http://stackoverflow.com/questions/41280471/how-to-implement-routereusestrategy-shoulddetach-for-specific-routes-in-angular/41515648#41515648

Though I personally believe "navigating forward scrolls to top; back restores scroll position" is normal and expected web behavior which should be the default, right now I'm just trying to figure out how the Angular team expects this to be handled.

In the spirit of this thread, here's my workaround (add to the root component):

What it does:

  • scroll to top when using router navigation
  • remember scroll positions for each route, keyed by url
  • restore scroll position during browser history navigation
  • avoid scroll jumping by restoring the scroll position directly instead of using setInterval

Limitations:

  • scroll will still jump on IE/Edge because they do not support history.scrollRestoration = 'manual';
  • probably won't work if multiple instances of the same route can be active
    private _isPopState = false;
    private _routeScrollPositions: { [url: string]: number } = {};
    private _deferredRestore = false;

    constructor(@Inject(PLATFORM_ID) private platformId: Object,
                private router: Router,
                private locStrat: LocationStrategy) {
    }

    ngOnInit(): void {
        if (isPlatformBrowser(this.platformId)) {
            // prevent nguniversal problems
            this.addScrollTopListeners();
        }
    }

    /**
     * Save the current ``window.pageYOffset`` in {@link _routeScrollPositions}, keyed by the given url.
     *
     * @param url scroll route
     */
    private saveScroll(url) {
        this._routeScrollPositions[url] = window.pageYOffset;
    }

    /**
     * Attempt to restore the scroll position for the given url. The scroll position is restored only if the body clientHeight is big
     * enough to accomodate it. {@link _deferredRestore} is reset to false on success or if there is no saved scroll posiiton for the url.
     *
     * @param url key in {@link _routeScrollPositions}
     * @returns {boolean} true if the scroll position was changed
     */
    private restoreScroll(url) {
        const savedScroll = this._routeScrollPositions[url];
        if (savedScroll === undefined) {
            // no saved scroll position for this url :(
            this._deferredRestore = false;
            return false;
        }

        const documentHeight = document.body.clientHeight, windowHeight = window.innerHeight;

        if (savedScroll + windowHeight <= documentHeight) {
            // document is already tall enough to scroll directly
            window.scrollTo(0, savedScroll);
            this._deferredRestore = false;
            return true;
        }

        return false;
    }

    private addScrollTopListeners() {
        // force scroll position at top of page when route changes through routerLink navigation
        //  (and not when it changes through browser back/forward)
        //  https://github.com/angular/angular/issues/10929#issuecomment-372265497
        // remember and restore scroll positions when navigating using back/forward
        //  https://github.com/angular/angular/issues/10929#issuecomment-274264962

        if ('scrollRestoration' in history) {
            // disable automatic scroll restoration by browsers, since it's doing a bad job
            // https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration
            history.scrollRestoration = 'manual';
        }

        this.router.events.subscribe((event) => {
            if (event instanceof NavigationStart) {
                // the position should not be saved at NavigationStart during popstate navigation because it will already be mangled
                if (!this._isPopState) {
                    // save scroll position of urls on router navigation
                    // at NavigationStart, router.url is still the url of the current route (not the target of the navigation)
                    this.saveScroll(this.router.url);
                    console.log(this.router.url);
                }
            }

            if (event instanceof NavigationEnd) {
                if (this._isPopState) {
                    // popstate navigation, try to restore saved scroll position immediately
                    // immediate restoration might be possible if the source view is taller than the target view
                    if (!this.restoreScroll(event.url)) {
                        // document is too short, and restoring the saved scroll position would not work;
                        // defer the restoration to the next tick, when document should be reflowed and reach its target height
                        setTimeout(() => {
                            if (this._deferredRestore) {
                                this.restoreScroll(event.url);
                                this._deferredRestore = false;
                            }
                        });

                        // using _deferredRestore, the route is marked for restoring its scroll position at a later time
                        // attempts are made to restore the scroll position in ngAfterContentChecked; it's most likely that this will
                        //  happen in the current tick, but setTimeout is added just in case
                        this._deferredRestore = true;
                    }
                } else {
                    // scroll to top on regular router navigation
                    window.scrollTo(0, 0);
                }

                // end of navigation event, remove _isPopState flag
                this._isPopState = false;
            }
        });

        this.locStrat.onPopState(() => {
            // during a navigation event we must know if it was triggered by popstate navigation or regular router navigation
            this._isPopState = true;

            // on browser back/forward navigation, popstate is fired before any Navigation or other router events
            // router.url is still the current route and the position must be saved here because it can be
            // mangled by browser behavior before reaching NavigationStart
            this.saveScroll(this.router.url);
        });
    }

    ngAfterContentChecked() {
        if (this._deferredRestore) {
            // ngAfterContentChecked is used to try and restore the scroll position in the
            // same tick as the first document reflow which makes it possible (i.e. before setTimeout)
            // this could prevent scroll flashes/jankiness
            this.restoreScroll(this.router.url);
        }
    }

#20030 ­čĹŹ

Related Issues:

608
angular Angular5.x lazyLoad problem, undefined is not a function
For others that find this issue via Google as i did: I had the same problem when trying to lazy load...
348
angular Cyclic dependency error with HttpInterceptor
I resolved simply not setting authService in constructor but getting in the intercept function. ...
277
angular Uncaught Error: Can't resolve all parameters for ...
You are missing an @Injectable() annotation on your ApiService Support requests like these should li...
266
angular Force reload/refresh current route with RouteReuseStrategy
Hi If you really need to trick the Router into reloading the component on each routerLink click ...
260
angular Misleading error message "Cannot find a differ supporting object '[object Object]'"
I just ran into the same issue I'm not sure if the recommended solution will work for my case ...
224
angular update 2 to 4 has problem [ts] Property 'map' does not exist on type 'Observable<Response>'.
I met the same problem with the angular cli 6.0.0 and rxjs 6.1.0 And I solved the problem by replaci...
170
angular Angular2 AoT Compiler Errors
pls try /cc @chuckjaz When I try to compile my project with ngc it throws the below error: Error: Er...
152
angular HttpClient.delete() cannot handle a body in its request
It would be great to have body param in .delete() We just migrated our project to HttpClient and for...
140
angular Http - Observable completed function not firing
Third callback haven't been called when error occures ES6 promises hasn't method finally only then a...
133
angular Using multiple components in different modules causing "Type X is part of the declarations of 2 modules" error
as @brandonroberts saids create a shared module like this: then use the SharedModule like this.. ...
112
angular Unsupported platform for fsevents@1.0.14: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
@DzmitryShylovich did you edit package.json only? if npm-shrinkwrap.json is still there please remov...
98
angular Angular v5 ngc compiler: Error encountered in metadata generated for exported symbol 'Subscription'
Got this problem I'm submitting a.. Current behavior Building an Angulary library using 5.0.0-beta.4...
98
angular 404 on route refresh in angular 4
Got it working Just adding .htaccess in root I'm submitting a.. Current behavior I created a new com...
97
angular routerLinkActive not updating when routerLink changed
I have a hack that seems to work After looking at the source code it looks like this.update() is als...
97
angular No provider for HttpClient!
If you are using angular v5 version import HttpClientModule in your app.module.ts after HttpModule T...
81
angular ╔ÁDomAnimationEngine and ╔ÁNoopAnimationEngine module missing in animations 4.2.1
@dubedoy I installed @angular/animations@4.1.3 and it worked again. I'm submitting a .. ...
80
angular Error: Runtime compiler is not loaded in angular6 --prod mode
Do not and i repeat do not import your feature modules in app module and also when addressing featur...
79
angular appending header in HttpHeaders from '@angular/common/http' doesn't work
@trotyl I didn't understand your comment I'm submitting a.. ...
77
angular Can't bind to 'formGroup' since it isn't a known property of 'form'
did you import ReactiveFormsModule? I'm submitting a .. ...
73
angular AOT Compiler requires public properties, while non-AOT allows private properties
@aluanhaddad you have a big misunderstanding in here There is no subset of Typescript in here No one...
73
angular [Bug] angular/elements: Failed to construct 'HTMLElement': Please use the 'new' operator
Hi I have solved this issue by changing the target:es5 in the tsconfig.json to target:es2015 these i...
70
angular Cannot run angular 2+ from file:/// - looks like 'base href="/"' is the issue
Thanks @Markovy @audrummer15 I got it working completely in a fairly complex angular 2 app with mult...
69
angular HttpClient fails to parse an empty 200 response in IE11
For my error I was able to fix the problem by setting the responseType: to 'text' in the options ...
66
angular Function calls are not supported in decorators when fullTemplateTypeCheck is not specified and @dynamic has no effect
Regarding Ward's repro: @wardbell The build will succeed / fail depending on the combination of angu...
62
angular error TS2451: Cannot redeclare block-scoped variable 'ngDevMode'
had to add this line in the main tsconfig I'm submitting a.. ...
62
angular Problem with ngFor
Wouldn't [(ngModel)]=testItems[i] do the trick? I think that the error is saying that you can assign...
61
angular Issue with importing Observable from rxjs/Rx (import-blacklisted)
You shouldn't import from 'rxjs' or 'rxjs/Rx' since either import will import the whole of rxjs whic...
53
angular [RC5]: Minified bundle breaks
@robertoforlani Hopefully someone will have time to write a comprehensive explanation soon In the me...
53
angular router-outlet is appending rather than replacing when using BrowserAnimationsModule
Trying this solved the problem for me: this.zone.run(() => { this.router.navigate(['/main']); }); Re...
52
angular Lazy loaded module in named outlet throws error
We have this Where proxy route component is simply [x] bug report [ ] feature request [ ] support re...
52
angular IVY Error NG6002: Appears in the NgModule.imports of AppModule, but could not be resolved to an NgModule class
Not sure this will provide anyone relief or assist with figuring out what the root cause is but clea...
51
angular How to run angular 2 application on apache hosting server
Sorry to rock the boat I hope this doesn't attract more questions I'm only going to comment once :) ...
51
angular Support adding rel=canonical link tags using an included service
Eventually there will be some DocumentService part of Core that will handle both Meta/Link elements ...
50
angular Concept of Angular (ngZone + ChangeDetection) better than concept React, Vue (Virtual DOM)?
Concept of Angular (ngZone + ChangeDetection) better than concept React Vue (Virtual DOM)? If you ca...
50
angular I'd like to be able to use ngModel without specifying a name
Thank you all for the great feedback - very helpful! Here's how we are thinking about it: In the cas...
49
angular Model values not trimming automatically in angular 2
@laskoviymishka White space it already something If you are a programmer and think globally - yes ...
46
angular HttpClient - HttpErrorResponse not json but blob
I created this interceptor as a temporary solution until this one is fixed: I'm submitting a.. ...
45
angular Router's ActivatedRoute data returns empty {} if module is lazy
data is available only with this hell-like construction: And this is if you have children: ...
44
angular Misleading errormessage when using HostBinding with @animation trigger but no defined animations
The error message is not fine The error message says you're importing BrowserAnimationsModule incorr...
42
angular Memory leak when FormControlName created/destroyed few times
This issue has been around for nearly 3 years now (I usually don't like to start a message this way ...
41
angular Remove System.import() usage in favor of import()
I use a parser rule in my webpack configuration to disable the warning: https://webpack.js.org/confi...
41
angular Async event subscriber not updating UI after async call
Hi! The issue is that the async call result is outside ngZone thus not triggering the UI update You ...
40
angular Using router.navigate to navigate to another component does not invoke the onInit method
I have the same issue Angular is running in a Cordova app for iOS I tried the router-version 4.1.3 (...
37
angular Router: AoT compilation fails when using a function with loadChildren
Calling functions or calling new is not supported in metadata when using AoT This includes things li...
35
angular Provide a mock service using TestBed
I was having this issue as well however I noticed that my @component metadata still had the provider...
35
angular Angular2 download excel file from Web API, file is corrupt
@healkar01 I had the same issue and I resolved using native angular2 http request in this way: Backe...
35
angular 4.0.0-rc.6 [platform-server] - Cannot find module '@angular/animations/browser'. & other errors
(Just incase others find it) Make sure @angular/animations is installed as a dependency and the erro...
35
angular HttpClient mapping to typescript types not working
I agree with all the previous comments I find the syntax misleading widget.service.zip widget.servic...