Where did my $window go? No problem, use this provider to inject the window into your Angular component.
Why?
Because we don’t want to reference the global Window
object directly in our Angular component since our application may be used in Angular Universal, and it’s considered best practice to avoid directly referencing the global objects.
While you may have no plans to use Angular Universal, this enables us to cleanly inject the browser’s native Window
object into a component.
How is this different?
While there are plenty of solutions out there already that propose methods for indirectly accessing the Window
object, I’ve found that many of them don’t actually perform any conditional verification that our Angular application is executing in the context of a browser, where we will have the Window
object.
Here’s an example of a solution that I felt was inadequate:
import { Injectable } from '@angular/core';
function getWindow (): any {
return window;
}
@Injectable()
export class WindowRefService {
get nativeWindow (): any {
return getWindow();
}
}
A quick look at this indicates that we are not checking if the application is executing in the context of a browser.
Further, we may want to support a context for tests, where we might want to mock some properties or methods on the Window
object.
Proposed Solution
Here is a proposed solution for creating an extensible method for injecting the Window
into our Angular components.
I’ve put this into a src/app/core/services/window.service.ts file in my application:
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. */
const browserWindowProvider: ClassProvider = {
provide: WindowRef,
useClass: BrowserWindowRef
};
/* Create an injectable provider that uses the windowFactory function for returning the native window object. */
const windowProvider: FactoryProvider = {
provide: WINDOW,
useFactory: windowFactory,
deps: [ WindowRef, PLATFORM_ID ]
};
/* Create an array of providers. */
export const WINDOW_PROVIDERS = [
browserWindowProvider,
windowProvider
];
A few things to note:
- I have an
InjectionToken
that is declared asWINDOW
that is exported. We will use this to inject theWindow
object into our components. - I define an abstract class
WindowRef
that has an accessor for thenativeWindow
property. - I then define the
BrowserWindowRef
class that extends the abstract class, implementing thenativeWindow
property to return the globally availablewindow
object. - The
windowFactory()
function determines if the application is executing within the context of the browser using theisPlatformBrowser()
function. Right now, if the application is not executing within the context of a browser, then I simply return a newObject
. This part is extensible in that you could return a mock object in another context. The function expects an instance of theBrowserWindowRef
and theplatformId
object. These dependencies are specified in thewindowProvider
- The
browserWindowProvider
is aClassProvider
that provides an instance of theBrowserWindowRef
using theWindowRef
injection token. - The
windowProvider
is aFactoryProvider
that uses thewindowFactory
to return theWindow
object (or an emtpy object) when the theWINDOW
injection token is used. - Finally, we declare an array of providers called
WINDOW_PROVIDERS
.
To use this, we need to add the WINDOW_PROVIDERS
to our app.module.ts module decorator’s providers
array:
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [WINDOW_PROVIDERS],
bootstrap: [AppComponent],
})
export class AppModule {}
You may also have a CoreModule
(perhaps with a different name) that is a module that contains all of the services, interceptors, models, etc. that is available to your application.
If that is the case, just import the WINDOW_PROVIDERS
in that module, which is then imported into your AppModule
.
Then, we can use the WINDOW
injection token to inject the window
into our component:
import { WINDOW } from "../core/services/window.service";
export class IndexComponent {
constructor(@Inject(WINDOW) private window: Window) {
console.log(window);
}
}
Here is an example showing the window.navigator.userAgent
string: