Brian Love
Angular + TypeScript Developer in Denver, CO

Angular 2 window scroll event using @HostListener

Reading time ~2 minutes

Angular 2 is awesome, but it also a major departure from Angular 1. One of these changes is the $window service is not in Angular 2. I was playing around with Materialize and wanted to toggle if the navbar was fixed vs. relative based on the offset of the body. My old-school event-driven approach kicked in, and I wanted to inject the window object into my component so I could detect when the window is scrolled.

I was able to create a service that would inject the browser’s native window object, but I also want to keep my code from being browser-dependent so that I can easily run tests against it. So, I ditched the idea of injecting the window object after learning about the @HostListener decorator in Angular 2.

The @HostListener Decorator

I couldn’t find too much information about the @HostListener decorator in the docs, only the interface specification in the API. But, what I was able to learn via other blogs and questions on stack overflow is that the HostListener enables us to listen for events on the host, and to specify the values that are passed as arguments to the decorated function or class.

In this example I want to listen for the window’s scroll event. Here is the simple markup for this:

import { HostListener} from "@angular/core";

@HostListener("window:scroll", [])
onWindowScroll() {
 //we'll do some stuff here when the window is scrolled
}

My primary concern at this point is, have I created a dependency on the browser’s window object? I don’t know the answer to that. If and when I do find out, I’ll be sure to post an update. But, this was a lot easier than injecting the window from a service.

The onWindowScroll() method in my component’s class will not be triggered when the window is scrolled.

The next thing I wanted to do is to determine the body’s offset. This way I could fade-in a fixed navbar when the user had scrolled into the page, say 100 pixels.

Inject Document object

In order to determine the body’s scrollTop value we need to inject the Document object. To do this, Angular 2 has provided a DOCUMENT dependency injection (DI) token to get the application’s rendering context, and when rendering in the browser, this is the browser’s document object.

import { Inject } from "@angular/core";
import { DOCUMENT } from "@angular/platform-browser";

export class LayoutNavComponent implements OnInit {
  constructor(@Inject(DOCUMENT) private document: Document) { }
}

First, I import the Inject decorator as well as the DOCUMENT DI token. Then, in my component’s constructor function I can inject the Document object. Now that I have the document, I can use this to easily determine the scrollTop value in my onWindowScrolled() method.

Here is what my component looks like:

import { Component, HostListener, Inject, OnInit } from "@angular/core";
import { DOCUMENT } from '@angular/platform-browser';

@Component({
  selector: "app-layout-nav",
  templateUrl: "./layout-nav.component.html",
  styleUrls: ["./layout-nav.component.scss"]
})
export class LayoutNavComponent implements OnInit {

  public navIsFixed: boolean = false;

  constructor(@Inject(DOCUMENT) private document: Document) { }

  ngOnInit() { }

  @HostListener("window:scroll", [])
  onWindowScroll() {
    let number = this.document.body.scrollTop;
    if (number > 100) {
      this.navIsFixed = true;
    } else if (this.navIsFixed && number < 10) {
      this.navIsFixed = false;
    }
  }
}

Using the navIsFixed boolean value I can easily update a class named fixed that I am applying to an element that wraps my <nav> element:

<div class="nav-container" [class.fixed]="navIsFixed">
  <nav role="navigation">
    ...
  </nav>
</div>

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).