chickenninja565
chickenninja565

Reputation: 567

Angular 6 - Keep scroll position when the route changes

Previous Behavior:

Changing the route or navigation path would not affect the scroll position when navigating to another route. I.e the contents can change without the scroll position changing.

Current Behavior:

Changing the route will put you right back to the top of the page.

Action Done So Far:

Tested on current and a fresh new Angular 6 project

Is this a bug? change in feature? or is there a parameter I am missing.

Upvotes: 16

Views: 35974

Answers (6)

JWess
JWess

Reputation: 650

This answer is for those who came here because they want the scroll position not to change when a route navigation is used to update the query params like so:

this.router.navigate([], {
  relativeTo: this.activatedRoute,
  queryParams,
});

Enabling scrollPositionRestoration is recommended by Angular:

'enabled'- Restores the previous scroll position on backward navigation, else sets the position to the anchor if one is provided, or sets the scroll position to [0, 0] (forward navigation). This option will be the default in the future.

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

But when the route is changed, Angular considers a simple query param change to be a "forward navigation", even if the route itself stays the same. This workaround uses the pairwise function to compare the current and previous router events and see if the route actually changed, or only the route parameters (i.e. query params). NOTE: If this feature request gets implemented, this workaround will no longer be needed.

export class AppModule {
  constructor(
    private router: Router,
    private viewportScroller: ViewportScroller,
  ) {
    this.viewportScroller.setHistoryScrollRestoration('manual'); // Disable automatic scroll restoration to avoid race conditions
    this.handleScrollOnNavigation();
  }

  private handleScrollOnNavigation(): void {
    this.router.events.pipe(
      // import { Event } from '@angular/router'
      filter((e: Event): e is Scroll => e instanceof Scroll),
      pairwise(),
    ).subscribe((e: Scroll[]) => {
      const previous = e[0];
      const current = e[1];

      if (current.position) {
        // Backward navigation
        this.viewportScroller.scrollToPosition(current.position);
      } else if (current.anchor) {
        // Anchor navigation
        this.viewportScroller.scrollToAnchor(current.anchor);
      } else {
        // Check if routes match, or if it is only a query param change
        if (this.getBaseRoute(previous.routerEvent.urlAfterRedirects) !== this.getBaseRoute(current.routerEvent.urlAfterRedirects)) {
          // Routes don't match, this is actual forward navigation
          // Default behavior: scroll to top
          this.viewportScroller.scrollToPosition([0, 0]);
        }
      }
    });
  }

  private getBaseRoute(url: string): string {
    // return url without query params
    return url.split('?')[0];
  }
}

Upvotes: 2

Bojan
Bojan

Reputation: 21

If someone still have problems with that there is a solution.

  1. Make a service where you will store the position of an element
    @Injectable()
    export class AdsService {
     private _position = new BehaviorSubject<number>(0);

     constructor() {}

     get position$(): Observable<number> {
       return this._position.asObservable();
     }

     setPosition(position: number): void {
       this._position.next(position);
     }
   }
  1. Provide a service in a module ( ex. AppModule) and inject it into a constructor of component.
  • Subscribe to a position, check if not 0 and set "scrollTop" property of an element
  • Write a "savePositon" function which will store a position after you navigate to certain element
export class MyComponent implements OnInit, OnDestroy, AfterViewInit {
  @ViewChild('contentArea') private contentArea: ElementRef<HTMLDivElement>;

  constructor(private adsService: AdsService) {}

  ngAfterViewInit(): void {
    this.adsService.position$
      .pipe(
        filter((p) => p !== 0),
        take(1)
      )
      .subscribe((p) => {
        this.contentArea.nativeElement.scrollTop = p;
      });
  }

  ngOnDestroy(): void {}

  ngOnInit(): void {}

  savePosition = () => {
    // this.router.navigate([item.id]);
    const scrollTop = this.contentArea.nativeElement.scrollTop;
    this.adsService.setPosition(scrollTop);
  };
}
  1. My HTML snippet looks something like this
<div #contentArea class="overflow">
   <div
    *ngFor="let item of activeAds; trackBy: trackByFn"
    [routerLink]="item.id"
    (click)="savePosition()"
   >
    {{item.name}}
   </div>
</div>

Upvotes: 0

Jasir K
Jasir K

Reputation: 1

if scroll restoration doesnt work , Create a service that updates the current scroll Position on the change of that route and assign the scroll position to that service scroll position on ngOnit for the new route component and reset the service scroll.

Upvotes: 0

Jasir K
Jasir K

Reputation: 1

if it doesnt work with ScrollPositionRestoration:'enabled' configuration use the [scrollTop] property binding for the container and assign the value

Upvotes: 0

chickenninja565
chickenninja565

Reputation: 567

Seems like setting 'scrollPositionRestoration' to disabled fixes it

RouterModule.forRoot(
  appRoutes,
  { scrollPositionRestoration: 'disabled' } // <-- HERE
)

See https://angular.io/api/router/ExtraOptions

Upvotes: 26

Xinan
Xinan

Reputation: 3162

The scroll position won't change after the route is changed. This is always the default behaviour for Angular.

However, lots of devs are manually doing a window.scroll(0, 0) to overwrite this behaviour.

I would suggest you check if something in your code is doing this. Because it might be a newly installed 3rd party library or another developer's code commit.

Also, according to the following official article:

Angular v6.1 Now Available — TypeScript 2.9, Scroll Positioning, and more

There is a new option to keep the original scroll position by using

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

I believe this is not directly related to the question you are asking but just something good to know.

Upvotes: 7

Related Questions