Global objects in Angular
In JavaScript we often use entities, such as window or navigator. Some of these objects have been there forever. But you might have seen DOCUMENT token used in Angular. Let's discuss why it exists and what we can learn from it to make our apps cleaner and more flexible.

In JavaScript we often use entities, such as window
, navigator
, requestAnimationFrame
or location
. Some of these objects have been there forever, some are parts of the ever-growing Web APIs feature set. You might have seen Location
class or DOCUMENT
token used in Angular. Let's discuss why they exist and what we can learn from them to make our apps cleaner and more flexible.
DOCUMENT
DOCUMENT
is a built-in Angular token. Here's how you can use it. Instead of this:
constructor(@Inject(ElementRef) private readonly elementRef: ElementRef) {}
get isFocused(): boolean {
return document.activeElement === this.elementRef.nativeElement;
}
You can write this:
constructor(
@Inject(ElementRef) private readonly elementRef: ElementRef,
@Inject(DOCUMENT) private readonly documentRef: Document,
) {}
get isFocused(): boolean {
return this.documentRef.activeElement === this.elementRef.nativeElement;
}
In the first code snippet we accessed global variable directly. In the second example we injected it as a dependency to constructor. I'm not going to throw fancy acronyms at you or say that the first approach breaks some programming principles or patterns. To be honest, I'm not that fluent in those. Instead, I will show you exactly why token approach is better. And we will start by taking a look at where this token comes from. There's nothing special about its declaration in the @angular/common
package:
export const DOCUMENT = new InjectionToken<Document>('DocumentToken');
Inside @angular/platform-browser
, however, we see it getting actual value (code snippet simplified):
{provide: DOCUMENT, useValue: document}
When you add BrowserModule
to your app.browser.module.ts
you register a bunch of different implementations for built-in tokens, such as RendererFactory2
, Sanitizer
, EventManager
and our DOCUMENT
. Why is it like that? Because Angular is multiplatform framework. Or rather platform agnostic. It works with abstractions and utilizes dependency injection mechanism heavily to be able to work in browser, on server or mobile platforms. To figure it out, let's take a sneak peak at ServerModule
— another bundled platform (code snippet simplified):
{provide: DOCUMENT, useFactory: _document, deps: [Injector]},
// ...
function _document(injector: Injector) {
const config = injector.get(INITIAL_CONFIG);
const window = domino.createWindow(config.document, config.url);
return window.document;
}
We see that it uses domino to create an imitation of a document, based on some config it once again got from the DI. This is what you get when you, for example, use Angular Universal for server side rendering. We already see the first and most important benefit. Using DOCUMENT
token would work in SSR environment whereas accessing global document
will not.
Other global entities
So Angular team took care of document
for us which is nice. But what if we want to check our browser with userAgent
string? To do so we would typically access navigator.userAgent
. What this actually means is that we first reach for global object, window
in browser environment, and then get its navigator
property. So let's start by implementing WINDOW
token. It is pretty easy to do with a factory that you can add to token declaration:
export const WINDOW = new InjectionToken<Window>(
'An abstraction over global window object',
{
factory: () => inject(DOCUMENT).defaultView!
},
);
This is enough to start using WINDOW
token similar to DOCUMENT
. Now we can use similar approach to create NAVIGATOR
:
export const NAVIGATOR = new InjectionToken<Navigator>(
'An abstraction over window.navigator object',
{
factory: () => inject(WINDOW).navigator,
},
);
We will take it one step further and create a token for USER_AGENT
the same way. Why? We'll see later!
Sometimes making a simple token is not enough. Angular's Location
class is basically a wrapper over native location
that improves our dev experience. Since we are used to RxJS streams let's replace requestAnimationFrame
with an Observable
implementation:
export const ANIMATION_FRAME = new InjectionToken<
Observable<DOMHighResTimeStamp>
>(
'Shared Observable based on `window.requestAnimationFrame`',
{
factory: () => {
const performanceRef = inject(PERFORMANCE);
return interval(0, animationFrameScheduler).pipe(
map(() => performanceRef.now()),
share(),
);
},
},
);
We skipped PERFORMANCE
token creation because it follows the same pattern. Now we have a single shared requestAnimationFrame
based stream of timestamps which we can use across our app. After we replaced everything with tokens our components no longer rely on magically available items and get everything they depend on from dependency injection, which is neat.
Server Side Rendering
Now say in our app we want to do window.matchMedia('(prefers-color-scheme: dark)')
. While there is certainly something in our WINDOW
token on the server side, it definitely does not provide full API that Window
object has. If we try the above method in SSR we would probably get undefined is not a function
error. One thing we can do is wrap such cases with isPlatformBrowser
checks but that's boring. Advantage of DI is we can override stuff. So instead of handling these situation as special cases we can provide WINDOW
in our app.server.module.ts
with a type-safe mock object that will protect us from non-existent properties.
This showcases another important advantage this approach has: token values can be replaced. This makes it very easy to test components that rely on browser API, especially if you test in Jest where native API is not available otherwise. But mocks are dull. Sometimes we can actually provide something meaningful. In SSR environment we have request object which contains user agent data. That's why we separated it into its own token — because we can actually obtain it separately sometimes. Here's how we can turn request into provider:
function provideUserAgent(req: Request): ValueProvider {
return {
provide: USER_AGENT,
useValue: req.headers['user-agent'],
};
}
And use it in our server.ts
when we are setting up Angular Universal:
server.get('*', (req, res) => {
res.render(indexHtml, {
req,
providers: [
{provide: APP_BASE_HREF, useValue: req.baseUrl},
provideUserAgent(req),
],
});
});
Node.js also has its own implementation of Performance
which we can use on the server side:
{
provide: PERFORMANCE,
useFactory: performanceFactory,
}
// ...
export function performanceFactory(): Performance {
return require('perf_hooks').performance;
}
In case of requestAnimationFrame
we will not need Performance
though. We probably do not want our Observable
chain to run on the server so we can provide EMPTY
:
{
provide: ANIMATION_FRAME,
useValue: EMPTY,
}
We can follow this pattern to "tokenize" all global objects that we use across our app and provide replacements for various platforms we might run it on.
Wrapping up
With this approach your code becomes well abstracted. Even if you do not run it on the server now, you might want to do it later and you will be ready. Besides, it is much easier to test code when things it relies upon can be substituted. We have created a tiny library of common tokens that we use:
If you need something that is currently missing, feel free to create an issue! There is also a sidekick package with SSR versions of those tokens:
You can explore this Rick and Morty themed project by Igor Katsuba using SSR to see all this in action. If you are interested in Angular Universal in particular, read his article about issues he faced and how to overcome them.
Thanks to this pattern our Angular components library Taiga UI was able to run on both Angular Universal and Ionic setups with no extra fuss. I hope it will be beneficial to you as well.