61

I can't seem to be able to capture the Window scroll event. On several sites I found code similar to this:

@HostListener("window:scroll", [])
onWindowScroll() {
  console.log("Scrolling!");
}

The snippets often come from version 2. This doesn't seem to work (anymore?) in Angular 4.2.2. If I replace "window:scroll" with "window:touchmove" for example, then then touchmove event is handled fine.

Does anyone know what I'm missing? Thank you very much!

6 Answers 6

124

Probably your document isn't scrolling, but a div inside it is. The scroll event only bubbles up to the window if it's called from document. Also if you capture the event from document and call something like stopPropagation, you will not receive the event in window.

If you want to capture all the scroll events inside your application, which will also be from tiny scrollable containers, you have to use the default addEventListener method with useCapture set to true.

This will fire the event when it goes down the DOM, instead of the bubble stage. Unfortunately, and quite frankly a big miss, angular does not provide an option to pass in the event listener options, so you have to use the addEventListener:

export class WindowScrollDirective {

    ngOnInit() {
        window.addEventListener('scroll', this.scroll, true); //third parameter
    }

    ngOnDestroy() {
        window.removeEventListener('scroll', this.scroll, true);
    }

    scroll = (event): void => {
      //handle your scroll here
      //notice the 'odd' function assignment to a class field
      //this is used to be able to remove the event listener
    };

}

Now this is not all there is to it, because all major browsers (except IE and Edge, obviously) have implemented the new addEventListener spec, which makes it possible to pass an object as third parameter.

With this object you can mark an event listener as passive. This is a recommend thing to do on an event which fires a lot of time, which can interfere with UI performance, like the scroll event. To implement this, you should first check if the current browser supports this feature. On the mozilla.org they've posted a method passiveSupported, with which you can check for browser support. You can only use this though, when you are sure you are not going to use event.preventDefault()

Before I show you how to do that, there is another performance feature you could think of. To prevent change detection from running (the DoCheck gets called every time something async happens within the zone. Like an event firing), you should run your event listener outside the zone, and only enter it when it's really necessary. Soo, let's combine all these things:

export class WindowScrollDirective {

    private eventOptions: boolean|{capture?: boolean, passive?: boolean};

    constructor(private ngZone: NgZone) {}

    ngOnInit() {            
        if (passiveSupported()) { //use the implementation on mozilla
            this.eventOptions = {
                capture: true,
                passive: true
            };
        } else {
            this.eventOptions = true;
        }
        this.ngZone.runOutsideAngular(() => {
            window.addEventListener('scroll', this.scroll, <any>this.eventOptions);
        });
    }

    ngOnDestroy() {
        window.removeEventListener('scroll', this.scroll, <any>this.eventOptions);
        //unfortunately the compiler doesn't know yet about this object, so cast to any
    }

    scroll = (): void => {
        if (somethingMajorHasHappenedTimeToTellAngular) {
           this.ngZone.run(() => {
               this.tellAngular();
           });
        }
    };   
}
Sign up to request clarification or add additional context in comments.

11 Comments

Thank you very much, this seems to work as expected! I think the problem is indeed that the document itself does not scroll in my situation.
@Robert I've updated my answer a bit more with additional info :)
Is the content inside the listener in the second code block correlated to intercepting the events? Furthermore, how do iche the direction of the scroll?
That's exactly what I said in my answer :)
@mkb it's not obsolete. It's very useful. It just doesn't support the use of the native event options yet.
|
40

If you happen to be using Angular Material, you can do this:

import { ScrollDispatchModule } from '@angular/cdk/scrolling';

In Ts:

import { ScrollDispatcher } from '@angular/cdk/scrolling';

  constructor(private scrollDispatcher: ScrollDispatcher) {    
    this.scrollDispatcher.scrolled().subscribe(x => console.log('I am scrolling'));
  }

And in Template:

<div cdkScrollable>
  <div *ngFor="let one of manyToScrollThru">
    {{one}}
  </div>
</div>

Reference: https://material.angular.io/cdk/scrolling/overview

9 Comments

Thank you very much, this seems to work as expected for me!
How do u get the scroll direction and offset in this case. The emitted event seems to be blank.
@SaurabhTiwari - See: measureScrollOffset in https://material.angular.io/cdk/scrolling/api. Could combine with Rxjs pairWise in an observable to see which direction you are scrolling. There might be a more direct way, but this is a solution.
I see what you are trying to suggest. But the measureScrollOffset method is not supported on scrollDispatcher. It is only supported on cdk-scrollable directive. Still, I might make something out of your suggestion. Thanks.
@SaurabhTiwari measureScrollOffset is what is available in the lambda value: subscribe(x => console.log(x.measureScrolloffset('top'))); as an example.
|
22

I am not allowed to comment yet. @PierreDuc your answer is spot on, except as @Robert said the document does not scroll. I modified your answer a little bit to use the event sent by the listener and then monitor the source element.

 ngOnInit() {
    window.addEventListener('scroll', this.scrollEvent, true);
  }

  ngOnDestroy() {
    window.removeEventListener('scroll', this.scrollEvent, true);
  }

  scrollEvent = (event: any): void => {
    const n = event.srcElement.scrollingElement.scrollTop;
  }

2 Comments

Thank you for including the event parameter. The accepted answer is kinda useless without it
Do you know how I can access the scope of the component from within the function?
6

In angular 8, implement this code, in my case it worked correctly to change the color of the navbar using scroll... your template:

<div class="level" (scroll)="scrolling($event)"  [ngClass]="{'level-trans': scroll}">
<!-- your template -->
</div>

your .ts

export class HomeNavbarComponent implements OnInit {

  scroll:boolean=false;
  constructor() { }

  ngOnInit() {
    window.addEventListener('scroll', this.scrolling, true)
  }
  scrolling=(s)=>{
    let sc = s.target.scrollingElement.scrollTop;
    console.log();
    if(sc >=100){this.scroll=true}
    else{this.scroll=false}
  }

your css

.level{
    width: 100%;
    height: 57px;
    box-shadow: 0 0 5px 0 rgba(0, 0,0,0.7);
    background: transparent;
    display: flex;
    position: fixed;
    top: 0;
    z-index: 5;
    transition: .8s all ease;
}
.level-trans{
    background: whitesmoke;
}

Comments

4

Just in case I was looking to capture the wheel action over an element that had no way to scroll since it didn't have a scroll bar ...

So, what I needed was this:

@HostListener('mousewheel', ['$event']) 
onMousewheel(event) {
     console.log(event)
}

Comments

1

an alternative with window in HostListener : use body

in core.d.ts:

The global target names that can be used to prefix an event name are document:, window: and body:

@HostListener('body:scroll', ['$event']) onScroll(event: any) {
    console.log(event);

  }

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.