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.
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 now 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.
Window Service
Just like with the document object, it is best practice in Angular to not reference the DOM directly, including the Window
object.
Here is a simple service for injecting the Window
object into our component:
import { isPlatformBrowser } from "@angular/common";
import { ClassProvider, FactoryProvider, InjectionToken, PLATFORM_ID } from '@angular/core';
/* Create a new injection token for injecting the window into a component. */
export const WINDOW = new InjectionToken('WindowToken');
/* Define abstract class for obtaining reference to the global window object. */
export abstract class WindowRef {
get nativeWindow(): Window | Object {
throw new Error('Not implemented.');
}
}
/* Define class that implements the abstract class and returns the native window object. */
export class BrowserWindowRef extends WindowRef {
constructor() {
super();
}
get nativeWindow(): Window | Object {
return window;
}
}
/* Create an factory function that returns the native window object. */
export function windowFactory(browserWindowRef: BrowserWindowRef, platformId: Object): Window | Object {
if (isPlatformBrowser(platformId)) {
return browserWindowRef.nativeWindow;
}
return new Object();
}
/* Create a injectable provider for the WindowRef token that uses the BrowserWindowRef class. */
export const browserWindowProvider: ClassProvider = {
provide: WindowRef,
useClass: BrowserWindowRef
};
/* Create an injectable provider that uses the windowFactory function for returning the native window object. */
export const windowProvider: FactoryProvider = {
provide: WINDOW,
useFactory: windowFactory,
deps: [ WindowRef, PLATFORM_ID ]
};
/* Create an array of providers. */
export const WINDOW_PROVIDERS = [
browserWindowProvider,
windowProvider
];
Note, you must provider the WINDOW_PROVIDERS
in the app.module.ts decorator’s providers
array:
import { WINDOW_PROVIDERS } from './window.service';
@NgModule({
imports: [BrowserModule],
declarations: [App],
bootstrap: [App],
providers: [WINDOW_PROVIDERS],
})
export class AppModule {}
Then, using this service, we can inject the browser’s native Window
object into our component, for example:
import { Inject } from "@angular/core";
import { DOCUMENT } from "@angular/platform-browser";
import { WINDOW } from "./window.service";
export class LayoutNavComponent implements OnInit {
constructor(
@Inject(DOCUMENT) private document: Document,
@Inject(WINDOW) private window: Window
) { }
}
Component
Here is what my component looks like:
import { Component, HostListener, Inject, OnInit } from "@angular/core";
import { DOCUMENT } from '@angular/platform-browser';
import { WINDOW } from "./window.service";
@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
@Inject(WINDOW) private window
) { }
ngOnInit() { }
@HostListener("window:scroll", [])
onWindowScroll() {
let number = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;
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>
Stackblitz
Here is an example of using the @HostListener
decorator using Angular 6: