30

I am having trouble getting a smooth scroll service to work in angular 2. Are there any services for smooth scrolling, or plain anchor scrolling, that might work until the angular 2 team gets the $anchorScroll angular2 equivalent working?

So far I have just tried:

Setting *ngFor loop incremental id on a parent div

[attr.id]="'point' + i"

Calling a scrollto on a button with the id passed

<button 
     type="button" 
     class="btn btn-lg btn-default " 
     (click)="smoothScroll('point'+i)">
           Scroll to point
</button>

And in the associated component I am trying to implement a plain js smooth scroll function

smoothScroll(eID) {
        var startY = currentYPosition();
        var stopY = elmYPosition(eID);
        var distance = stopY > startY ? stopY - startY : startY - stopY;
        if (distance < 100) {
            scrollTo(0, stopY); return;
        }
        var speed = Math.round(distance / 100);
        if (speed >= 20) speed = 20;
        var step = Math.round(distance / 25);
        var leapY = stopY > startY ? startY + step : startY - step;
        var timer = 0;
        if (stopY > startY) {
            for (var i = startY; i < stopY; i += step) {
                setTimeout(this.win.scrollTo(0, leapY), timer * speed);
                leapY += step; if (leapY > stopY) leapY = stopY; timer++;
            } return;
        }
        for (var i = startY; i > stopY; i -= step) {
            setTimeout(this.win.scrollTo(0,leapY), timer * speed);
            leapY -= step; if (leapY < stopY) leapY = stopY; timer++;
        }
    }
function currentYPosition() {
    // Firefox, Chrome, Opera, Safari
    if (self.pageYOffset) return self.pageYOffset;
    // Internet Explorer 6 - standards mode
    if (document.documentElement && document.documentElement.scrollTop)
        return document.documentElement.scrollTop;
    // Internet Explorer 6, 7 and 8
    if (document.body.scrollTop) return document.body.scrollTop;
    return 0;
}
function elmYPosition(eID) {
    var elm = document.getElementById(eID);
    var y = elm.offsetTop;
    var node = elm;
    while (node.offsetParent && node.offsetParent != document.body) {
        node = node.offsetParent;
        y += node.offsetTop;
    } return y;
}

I'm also trying to give access to the window for the this._win.scrollTo which is coming from a window provider service

import {Injectable, Provider} from 'angular2/core';
import {window} from 'angular2/src/facade/browser';
import {unimplemented} from 'angular2/src/facade/exceptions';

function _window(): Window {
  return window
}

export abstract class WINDOW {
  get nativeWindow(): Window {
    return unimplemented();
  }
}

class WindowRef_ extends WINDOW {
  constructor() {
    super();
  }
  get nativeWindow(): Window {
    return _window();
  }
}

export const WINDOW_PROVIDERS = [
  new Provider(WINDOW, { useClass: WindowRef_ }),
];

** EDIT ---------------------**

I changed the this.win.scrollTo to this.win.window.scrollTo and now I am getting an effect similar to angular1.x $anchorscroll where the scroll is a snappy just instead of a smooth transition, but the scroll is not smooth and I am getting the following exception error.

Exception error

UPDATE

I am no longer getting that error after finding out that angular2 is doing the setTimeout a bit differently, but the scroll is still instantaneous and not a smooth scroll.

I changed

  setTimeout(this.win.scrollTo(0, leapY), timer * speed);

to

 setTimeout(() => this.win.scrollTo(0, leapY), timer * speed);
Vel
  • 9,027
  • 6
  • 34
  • 66
Alex J
  • 3,085
  • 3
  • 19
  • 29

9 Answers9

42

there is a method in the window object called scrollTo(). If you set the behavior to 'smooth' the page will handle the smooth scroll. example (scroll to top of page):

 window.scrollTo({ left: 0, top: 0, behavior: 'smooth' });

And with fallback example:

    try 
    { 
     window.scrollTo({ left: 0, top: 0, behavior: 'smooth' });
     } catch (e) {
      window.scrollTo(0, 0);
      }
Mert
  • 1,333
  • 1
  • 12
  • 15
  • What does browser compatibility look like? – Randy Oct 25 '17 at 12:52
  • 3
    Vivaldi and IE wans't working during my tests. So I used a fallback. try { window.scrollTo({ left: 0, top: 0, behavior: 'smooth' }); } catch (e) { window.scrollTo(0, 0); } – Mert Oct 26 '17 at 05:56
17

Alright, after scratching my head a little bit, here is a solution that seems to be working ok.

Same as before, I declared my conditional id and a button with the scrollTo function call when clicked.

Now, there are only two files in the solution is a service that will help return the document window and the template's component. Nothing was changed in the window service from the state above but I will include it again for the sake of a good answer.

window.service.ts : shout out to https://gist.github.com/lokanx/cc022ee0b8999cd3b7f5 for helping with this piece

import {Injectable, Provider} from 'angular2/core';
import {window} from 'angular2/src/facade/browser';
import {unimplemented} from 'angular2/src/facade/exceptions';

function _window(): Window {
  return window
}

export abstract class WINDOW {
  get nativeWindow(): Window {
    return unimplemented();
  }
}

class WindowRef_ extends WINDOW {
  constructor() {
    super();
  }
  get nativeWindow(): Window {
    return _window();
  }
}

export const WINDOW_PROVIDERS = [
  new Provider(WINDOW, { useClass: WindowRef_ }),
];

app.component.ts

import { bootstrap } from 'angular2/platform/browser';
import { Component } from 'angular2/core';
import {WINDOW, WINDOW_PROVIDERS} from './window.service';

@Component({
  selector: 'my-app',
  templateUrl: 'app.tpl.html',
  providers: [WINDOW_PROVIDERS]
})

class AppComponent {
    win: Window;
    private offSet: number;
    constructor(
        private _win: WINDOW) { 
        this.win = _win.nativeWindow;
    }
    title = 'Ultra Racing';
    things = new Array(200);

    scrollTo(yPoint: number, duration: number) {
        setTimeout(() => {
            this.win.window.scrollTo(0, yPoint)
        }, duration);
        return;
    }
    smoothScroll(eID) {
        var startY = currentYPosition();
        var stopY = elmYPosition(eID);
        var distance = stopY > startY ? stopY - startY : startY - stopY;
        if (distance < 100) {
            this.win.window.scrollTo(0, stopY); return;
        }
        var speed = Math.round(distance / 100);
        if (speed >= 20) speed = 20;
        var step = Math.round(distance / 100);
        var leapY = stopY > startY ? startY + step : startY - step;
        var timer = 0;
        if (stopY > startY) {
            for (var i = startY; i < stopY; i += step) {
                this.scrollTo(leapY, timer * speed);
                leapY += step; if (leapY > stopY) leapY = stopY; timer++;
            } return;
        }
        for (var i = startY; i > stopY; i -= step) {
            this.scrollTo(leapY, timer * speed);
            leapY -= step; if (leapY < stopY) leapY = stopY; timer++;
        }
    }
}
function currentYPosition() {
    // Firefox, Chrome, Opera, Safari
    if (self.pageYOffset) return self.pageYOffset;
    // Internet Explorer 6 - standards mode
    if (document.documentElement && document.documentElement.scrollTop)
        return document.documentElement.scrollTop;
    // Internet Explorer 6, 7 and 8
    if (document.body.scrollTop) return document.body.scrollTop;
    return 0;
}
function elmYPosition(eID) {
    var elm = document.getElementById(eID);
    var y = elm.offsetTop;
    var node = elm;
    while (node.offsetParent && node.offsetParent != document.body) {
        node = node.offsetParent;
        y += node.offsetTop;
    } return y;
}

bootstrap(AppComponent)

I created a plunk to show this example working: Plunk Example

Alex J
  • 3,085
  • 3
  • 19
  • 29
13

The easier way to achieve this is by using this polyfill: http://iamdustan.com/smoothscroll/

  1. Install it as: npm install smoothscroll-polyfill
  2. Import it in your polyfill.ts file as: require('smoothscroll-polyfill').polyfill();
  3. Now you can use behavior option of scrollIntoView as:

    (document.querySelector('#'+ anchor)).scrollIntoView({ behavior: 'smooth' });

Paul Ionescu
  • 131
  • 1
  • 4
  • Should we use require or should we use import 'smoothscroll-polyfill'; – Naveed Ahmed Apr 03 '17 at 01:01
  • You should use require, I tried with import and it didn't worked. Also, dont forget to call .polyfill(): require('smoothscroll-polyfill').polyfill(); in your polyfill.ts file – Paul Ionescu Apr 03 '17 at 19:31
  • In my case require is throwing error, it doesnt recognize require when I try to compile – Naveed Ahmed Apr 03 '17 at 20:55
  • 1
    For typescript, just do: `import 'smoothscroll-polyfill';` – Steve Brush May 04 '17 at 20:57
  • did it all, and still jumps! smooth option ins't working. – André Dos Santos May 19 '17 at 01:11
  • try with a setTimeout, setTimeout(() => { (document.querySelector('#'+ anchor)).scrollIntoView({ behavior: 'smooth' }); }, 0); – Paul Ionescu May 20 '17 at 06:23
  • Like @AndréDosSantos, mine also jumps. Any further suggestions to get this to work would be appreciated. – Kevin LeStarge Jun 07 '17 at 16:31
  • It works when I do `require('smoothscroll-polyfill').polyfill();` in polyfills.ts using JIT compilation. However, the AOT build doesn't work with the require statements (as of angular 4 I believe). But `import 'smoothscroll-polyfill';` doesn't seem to be working... any suggestions? – Kevin LeStarge Jun 07 '17 at 17:26
  • 2
    I got it to work by adding this to the polyfills.ts: ```import smoothscroll from 'smoothscroll-polyfill/dist/smoothscroll'; smoothscroll.polyfill();``` – Kevin LeStarge Jun 07 '17 at 17:58
  • I solved this by not giving any height to my div, and allowing all scroll to happen through the document body. – André Dos Santos Jun 08 '17 at 14:38
4

example:

function goToElement(elemId){
 let element = window.getElementById(elemId);
 element.scrollIntoView({behavior: "smooth"});
}
Laxa Tif
  • 41
  • 1
  • 2
    This worked for me in Angular 6+. With a few modifications: `ngOnInit() { this.route.fragment.subscribe(fragment => { if (fragment) { document.querySelector('#' + fragment).scrollIntoView({ behavior: 'smooth' }) } }) }` – Gus Jul 20 '18 at 22:41
2

If you want a very simple anchor jump that works after routing and within routed views, you can also use ng2-simple-page-scroll.

<a simplePageScroll href="#myanchor">Go there</a>

Or right after routing:

<a simplePageScroll [routerLink]="['Home']" href="#myanchor">Go there</a>

It does a simple instant jump, but it works.

Benny Bottema
  • 11,111
  • 10
  • 71
  • 96
  • Any chance of you adding a demo the ng2-simple-page-scroll site? – Michael JDI May 21 '16 at 15:48
  • @MichaelJDI, yep, I'll work on it. Hopefully somewhere tomorrow I'll finish it. In the meantime, checkout https://github.com/bbottema/simple-java-mail/tree/master/src/main/webapp. It features a working info site for a java library, but it's Angular2 rc1 including ng2-simple-page-scroll. – Benny Bottema May 21 '16 at 19:49
  • @BennyBottema Does this work with router-deprecated or only with the new router? Do I have to use specific LocationStrategy? – koninos Jun 07 '16 at 16:50
  • @Konst It works only with the new router, else you won't be able to compile it, I think. I hadn't thought of a specific hash strategy, but I've been using HashLocationStrategy. – Benny Bottema Jun 07 '16 at 17:39
  • @BennyBottema can we use simplePageScroll without the `` tag? Can we trigger the scroll programmatically? – BeniaminoBaggins Sep 01 '16 at 21:53
  • I am making this the new accepted answer because my original solution isn't working even after converting everything to rc.5 methods even though I was looking for a smooth scroll solution. – Alex J Sep 15 '16 at 15:17
  • @Benny, this is cool, but when I try to do this, I get a console error: EXCEPTION: Uncaught (in promise): Error: Cannot match any routes. URL Segment:... – Manoj Amalraj Mar 23 '17 at 17:50
2

For anyone still on the search for a smooth scroll @alex-j 's answer works great for me in Angular 2.0 - but I had to change the Window service to this :-

import { Injectable } from '@angular/core';

function _window() : any {
    // return the global native browser window object
    return window;
}

@Injectable()
export class WindowRef {
    get nativeWindow() : any {
        return _window();
    }
}

All props to this blog http://juristr.com/blog/2016/09/ng2-get-window-ref/ - now I have a smooth scroll service I can call from anywhere :)

Joe Keene
  • 2,175
  • 21
  • 27
  • You solved me the problem I had with other unrelated issue with that piece of code, thanks! :D – Dunos Jan 03 '17 at 11:28
1

i use this code .

        var dis = distance  ;
        var interval = setInterval(() => {
            this.document.body.scrollTop = dis;
             dis=dis-5 ;
             if (dis<10){
                 clearInterval(interval);
             }
        }, 5);
ahmad haeri
  • 435
  • 4
  • 5
0

Thanks to the accepted answer I was able to implement a smooth "scroll to top". Scrolling to the top is actually even easier than scrolling to a particular target element since we are scrolling to the 0-position always. Here is the code:

scrollTo(yPoint: number, duration: number) {
    setTimeout(() => {
        window.scrollTo(0, yPoint)
    }, duration);
    return;
}

smoothScrollToTop() {
    let startY = this.currentYPosition();
    let stopY = 0; // window top
    let distance = stopY > startY ? stopY - startY : startY - stopY;
    if (distance < 100) {
        window.scrollTo(0, stopY);
        return;
    }
    let speed = Math.round(distance / 100);
    let step = speed;
    speed = Math.max(9, speed); //min 9 otherwise it won't look smooth
    let leapY = stopY > startY ? startY + step : startY - step;
    let timer = 0;
    if (stopY > startY) {
        for (let i = startY; i < stopY; i += step) {
            // since setTimeout is asynchronous, the for-loop will will fire all scrolls
            // nearly simoultaniously. Therefore, we need to multiply the speed with
            // a counter which lets the scrolls start with a growing offset which lets the
            // setTimeout wait for a growing time till it scrolls there
            // that way, we prevent the window to scroll instantly to the target Yposition
            this.scrollTo(leapY, timer * speed);
            leapY += step; if (leapY > stopY) leapY = stopY; timer++;
        }
        return;
    } else {
        for (let i = startY; i > stopY; i -= step) {
            this.scrollTo(leapY, timer * speed);
            leapY -= step; if (leapY < stopY) leapY = stopY; timer++;
        }
    }
}

currentYPosition() {
    // Firefox, Chrome, Opera, Safari
    if (self.pageYOffset) return self.pageYOffset;
    // Internet Explorer 6 - standards mode
    if (document.documentElement && document.documentElement.scrollTop)
        return document.documentElement.scrollTop;
    // Internet Explorer 6, 7 and 8
    if (document.body.scrollTop) return document.body.scrollTop;
    return 0;
}

If you want, you can let your "Scroll-To-Top" button appear dynamically when user scrolls:

@HostListener('window:scroll', ['$event'])
onWindowScroll(event) {
    this.onScrollFadeInOutScrollToTopButton();
}

shouldShowScrollToTop: boolean = false;

onScrollFadeInOutScrollToTopButton() {
    this.shouldShowScrollToTop = (window.pageYOffset >= window.screen.height/2);
}

And the HTML for the Scroll-to-top Button:

<div class="back-to-top">
<button *ngIf="shouldShowScrollToTop" [@fadeInOutTrigger]="animateButtonEntryState" class="mat-primary" md-fab (click)="smoothScrollToTop()">^</button>

As you can see, that button also has an animation trigger. You can think about using an icon for the button and ideally, your button should have a position:fixed; style.

Hans
  • 1,162
  • 11
  • 18
-3

There is another approach, which sould be considered: using jQuery.

Maybe it is not so elegant as the native solutions, but very easy and works perfectly.

In your index.html you have to add this to the end of the body:

<script
src="https://code.jquery.com/jquery-3.1.1.min.js"
integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
crossorigin="anonymous"></script>

<script>
  $(document).on("click", "a[href*='#']:not([href='#'])", function() {
    if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
      var target = $(this.hash);
      target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
      if (target.length) {
          $('html, body').animate({
          scrollTop: target.offset().top - 100
          }, 1000);
          return false;
      }
    }
  });

</script>

And now you can use the simple <a href("#section")> navigation like this:

<a href="#section2">Link</a>

It also works with routing:

<a class="btn" role="button" routerLink="/contact" fragment="contact_form">Contact us!</a>
Ábó Szilágyi
  • 155
  • 1
  • 5
  • 6
    No way I;d add the whole jQuery library to the project just for the scrollTo functionality. Strongly object ;) There is no need for jQuery in angular world ;) – DS_web_developer Feb 17 '17 at 16:27