Chris Ryan
Chris Ryan

Reputation: 233

Autoscroll in Angular 2

I'm experiencing an issue with Angular 2 where changing from one route to another does not automatically scroll to the top of the new view. I realize that Angular 1 allowed for an autoscroll property to be added to an HTML element, and others had come up with some simple javascript (such as window.scroll(0, 0)) to force views to scroll to the top when loaded.

However, I am not sure how to accomplish this with Angular 2. Does anyone know how to achieve this?

Upvotes: 23

Views: 16167

Answers (10)

RohanArihant
RohanArihant

Reputation: 2750

100% solution tested by me:

constructor(router:Router){
    this.router.events.subscribe(() => {
        window.scrollTo(0, 0);
    });
}

Upvotes: 3

dockleryxk
dockleryxk

Reputation: 407

I posted this in the issue thread, but I'll post it again here.

My team has been using what the angular team uses on this repo on angular.io. Just make a service and inject it like usual. Then, on ngAfterViewInit on each page you want this behavior, just call this.[scroll service variable name].scrollToTop(). Finally, you'll need to add this to the top of <body> in index.html: <div id="top-of-page"></div>

Service Code:

import { Injectable, Inject } from '@angular/core';
import { PlatformLocation } from '@angular/common';
import { DOCUMENT } from '@angular/platform-browser';
import {fromEvent} from 'rxjs/observable/fromEvent';

export const topMargin = 16;
/**
 * A service that scrolls document elements into view
 */
@Injectable()
export class ScrollService {

  private _topOffset: number | null;
  private _topOfPageElement: Element;

  // Offset from the top of the document to bottom of any static elements
  // at the top (e.g. toolbar) + some margin
  get topOffset() {
    if (!this._topOffset) {
      const toolbar = this.document.querySelector('md-toolbar.app-toolbar');
      this._topOffset = (toolbar && toolbar.clientHeight || 0) + topMargin;
    }
    return this._topOffset;
  }

  get topOfPageElement() {
    if (!this._topOfPageElement) {
      this._topOfPageElement = this.document.getElementById('top-of-page') || this.document.body;
    }
    return this._topOfPageElement;
  }

  constructor(
      @Inject(DOCUMENT) private document: any,
      private location: PlatformLocation) {
    // On resize, the toolbar might change height, so "invalidate" the top offset.
    fromEvent(window, 'resize').subscribe(() => this._topOffset = null);
  }

  /**
   * Scroll to the element with id extracted from the current location hash fragment.
   * Scroll to top if no hash.
   * Don't scroll if hash not found.
   */
  scroll() {
    const hash = this.getCurrentHash();
    const element: HTMLElement = hash
        ? this.document.getElementById(hash)
        : this.topOfPageElement;
    this.scrollToElement(element);
  }

  /**
   * Scroll to the element.
   * Don't scroll if no element.
   */
  scrollToElement(element: Element) {
    if (element) {
      element.scrollIntoView();

      if (window && window.scrollBy) {
        // Scroll as much as necessary to align the top of `element` at `topOffset`.
        // (Usually, `.top` will be 0, except for cases where the element cannot be scrolled all the
        //  way to the top, because the viewport is larger than the height of the content after the
        //  element.)
        window.scrollBy(0, element.getBoundingClientRect().top - this.topOffset);

        // If we are very close to the top (<20px), then scroll all the way up.
        // (This can happen if `element` is at the top of the page, but has a small top-margin.)
        if (window.pageYOffset < 20) {
          window.scrollBy(0, -window.pageYOffset);
        }
      }
    }
  }

  /** Scroll to the top of the document. */
  scrollToTop() {
    this.scrollToElement(this.topOfPageElement);
  }

  /**
   * Return the hash fragment from the `PlatformLocation`, minus the leading `#`.
   */
  private getCurrentHash() {
    return this.location.hash.replace(/^#/, '');
  }
}

Upvotes: 2

Kanchan
Kanchan

Reputation: 1619

Instead of writing code in each component, I added the following code in one place -

<router-outlet (activate)="onActivate($event)"></router-outlet>

    onActivate(e) {
        window.scrollTo(0, 0);
    }

Upvotes: 1

Tyson
Tyson

Reputation: 413

I'm using material sidenav and I couldn't get any of the suggested answers to work for me. Here's my working solution:

import { Router, NavigationEnd } from '@angular/router';

...

constructor(
  private router: Router,
) {
  router.events.subscribe(event => {
    if (event instanceof NavigationEnd) {
      document.querySelector('.mat-sidenav-content').scrollTop = 0;
    }
  }
}

Upvotes: 1

G&#252;nter Z&#246;chbauer
G&#252;nter Z&#246;chbauer

Reputation: 657338

update

Currently there is no automatic way.

See also Angular 2 typescript error when using subscribe function on new router (rc 1)

See also https://github.com/angular/angular/issues/6595#issuecomment-244232725

class MyAppComponent {
  constructor(router: Router) {
    router.events.subscribe(s => {
      if (s instanceof NavigationEnd) {
        const tree = router.parseUrl(router.url);
        if (tree.fragment) {
          // you can use DomAdapter
          const element = document.querySelector("#" + tree.fragment);
          if (element) { element.scrollIntoView(element); }
        }
      }
    });
  }
}

update

In the new router V3-beta.2 you can pass a fragment with router links and router navigation

<a [routerLink]="..." fragment="top">

it should scroll to it but also adds #top to the URL (not tested myself yet)

Update

Original

There is an open issue covering this https://github.com/angular/angular/issues/6595

A workaround (mentioned in https://github.com/angular/angular/issues/6946)

Inject the router, subscribe to route changes and invoke the scroll to top:

>= RC.x

router.changes.subscribe() => {
  window.scrollTo(0, 0);
});

beta

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

Upvotes: 17

Thierry
Thierry

Reputation: 499

Has you can see here: https://angular.io/docs/ts/latest/api/router/index/Router-class.html#!#events-anchor, you have to use "router.events.subscribe" since Angular 2.0.0

So a good solution to automaticly scrool to the top of all page is to have a AppComponent like this:

import {Component} from '@angular/core';
import {Router, NavigationEnd} from "@angular/router";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
    constructor(private router: Router) {
        router.events.subscribe((val) => {
            if (val instanceof NavigationEnd){
                window.scrollTo(0,0);
            }
        });
    }
}

Upvotes: 13

Shane
Shane

Reputation: 91

I have used if(this.router.navigated) in the ngOnInit of each page to determine whether or not to use window.scrollTo(0, 0). This will cover most cases of routing to the page, while leaving the scroll position where it should be if you click the browser Back button.

if(this.router.navigated) {
  window.scrollTo(0, 0);
}

Upvotes: 4

Post Impatica
Post Impatica

Reputation: 16393

For Those of you who find window.scrollTo(0,0) not working (I'm guessing because of material design sidenav but totally guessing) use the method found here: Javascript / CSS window.scrollTo(0,0) not working

Upvotes: 1

the_critic
the_critic

Reputation: 12820

Newer RCs (>= RC.3) don't seem to expose a changes Observable, it probably has since been renamed to events or routerEvents.

Their utterly "fantastic" docs don't seem to provide any information on what's doing what, so I guess you're in for a little Russian Roulette there. Or flip a coin or something.

From this answer, it seems the events Observable returns events regarding navigation state:

router.events.subscribe(event:Event => {
    if(event is NavigationStart) {
    }
    // NavigationEnd
    // NavigationCancel
    // NavigationError
    // RoutesRecognized   
  }

Upvotes: 9

Federico Rodriguez
Federico Rodriguez

Reputation: 448

I had the same issue. Based on Gunter's answer I found that Angular's 2 RC.1 new router doesn't expose an Observable directly. Instead it has a changes property for that purpose. The workaround for RC.1 is:

this._router.changes.subscribe(() => {
    window.scrollTo(0, 0);
}); 

Upvotes: 4

Related Questions