Angular Universal: ReferenceError: window is not defined

Thabo Ambrose
Motf Creations
Published in
5 min readJul 29, 2020

--

This error can be caused by a reference to the Window object if you are rendering your application from a server like Node.js.

Image source: https://learnwithmanoj.com/angular-5-universal/

I will share how I have solved this issue, in my case this was caused by HammerJS which does not support server-rendered applications. This error can also be caused by your code that references the Window object or any third-party module that references the Window object without having a defensive check against an undefined value of Window.

Angular core: v10.0.5

I have incurred many errors when resolving this issue, during the processes, I will quickly list some of the issues a had and tentative solutions to them.

  • No such file or directory, google/firestore/v1/firestore.proto’
    Solutions:
    In angular.json file, I have added the following:
...
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/PROJECT_NAME/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json",
"stylePreprocessorOptions": {
"includePaths": ["src/assets/styles"]
},
"externalDependencies": ["@firebase/firestore"]
}
}
...

The externalDependencies option allows you to exclude the listed dependencies in the array from the main bundle and instead, the generated bundle relies on these dependencies to be available during runtime.
I understand the above statement but do not understand why I got the error for @firebase/firestore.

Some suggestions were to exclude the following packages by listing them under externalDependencies.
"grpc",
"@grpc/proto-loader"

But what is gRPC?

gRPC is an open source remote procedure call system initially developed at Google in 2015.

In gRPC, a client application can directly call a method on a server application on a different machine as if it were a local object, making it easier for you to create distributed applications and services. As in many RPC systems, gRPC is based around the idea of defining a service, specifying the methods that can be called remotely with their parameters and return types. On the server side, the server implements this interface and runs a gRPC server to handle client calls. On the client side, the client has a stub (referred to as just a client in some languages) that provides the same methods as the server.
By default, gRPC uses Protocol Buffers
SOURCE: https://grpc.io/docs/what-is-grpc/introduction/

What are Protocol Buffers?

Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data — think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
SOURCE: https://developers.google.com/protocol-buffers

And what's the thing with gRPC and Firebase/Firestore?

I do not know, I am only guessing that the relationship between the two involves the mechanics of the Google cloud services like the Firebase cloud functions, the Database rules .etc.

Handle the Window object in my Angular code

First I have the service that wraps the global Window object.

import { Injectable } from '@angular/core';function _window(): any {
return window;
}
@Injectable()
export class WindowRef {
get nativeWindow(): any {
return _window();
}
}

I have provided this to the app’s main module AppModule so that it is in the upper-most injector so that it is available in the whole application.

I have used this service by injecting it in the components that use the Window object like below, I have omitted some code for brevity.

import { PLATFORM_ID } from "@angular/core";import { isPlatformBrowser } from '@angular/common';
import { WindowRef } from './services/windowRef.service';
...constructor(
@Inject(PLATFORM_ID) private platformId: any,
private windowRef: WindowRef
){}
...if(isPlatformBrowser(this.platformId)) { // Use the window reference: this.windowRef Eg: this.windowRef.nativeWindow.location.pathname}

Let’s go through the important parts for the above code:

PLATFORM_ID is an Opaque Token (Now InjectionToken) of type String object, this token helps the DI to get the current platform (browser/server).
isPlatformBrowser a common function that returns whether a platform id represents a browser platform.

Cool.. with the above code snippet, you can generalize the defensive mechanism for the existence of the Window object as you like, if you are abusing the window Object in your application, just make sure you do the check if it is a browser or server. There is a function to check if the platform is a server — isPlatformServer .

Fixing Window issue for HammerJS

This fix was straight forward, I have quickly gone through the docs of HammerJS and noticed that the check if the Window exists is not there and the usage of Window happens in the exported module, which caused the Window to be undefined in my SSR even when I used isPlatformBrowser before I can use HammerJS.

NOTE: Using isPlatformBrowser against HammerJS did not work because the error happened on the import level/line of HammerJS.

Solution

Use @egjs/hammerjs, this version of HammerJS is server friendly and is tree-shakable. So I have installed this module and did not have to change anything because it uses the same interface as the old HammerJS and also comes with the type definitions;

So go ahead and import this new module and remove the old import;

Fix ReferenceError: getComputedStyle is not defined for GSAP

I am using a nice great animation engine in my application; But for SSR, I got the above error.

Solution

Use domino a server-side DOM implementation based on Mozilla’s dom.js.

With domino, I am not sure why it solved my issue with getComputedStyle (A Window global) being undefined but not my other issues with Window being undefined. So I am just going to drop the code that a have on my server.ts file to address issues with browser/server globals.
The below code is at the top of the server.ts file

(global as any).WebSocket = require('ws');
(global as any).XMLHttpRequest = require('xhr2');
const domino = require('domino');
const fs = require('fs');
const path = require('path');
...const distFolder = join(process.cwd(), 'dist/motif/browser');
const template = fs.readFileSync(path.join(distFolder, 'index.html')).toString();
const win = domino.createWindow(template.toString());
global['window'] = win;
global['document'] = win.document;
global['self'] = win
global['IDBIndex'] = win.IDBIndex
global['document'] = win.document
global['navigator'] = win.navigator
global['getComputedStyle'] = win.getComputedStyle;
...import 'zone.js/dist/zone-node';
import { AppServerModule } from './src/main.server';

Note the import of AppServerModule after my play there, there seems to be importance with the order of imports, so note that.

The SSR scripts in the package.json

"dev:ssr": "ng run motif:serve-ssr",
"serve:ssr": "node dist/motif/server/main.js",
"build:ssr": "ng build --prod && ng run motif:server:production",
"prerender": "ng run motif:prerender"

After all the hassle, when running the below script, the build completes with a few warnings inherent to my project.

npm run build:ssr && npm run serve:ssr

By the way, all the Angular Universal boiler plating was achieved with the below command

ng add @nguniversal/express-engine

Refer to Angular Universal.

And that’s it! if I have missed something I will update this article. Ciao

UPDATE:

The isPlatformBrowser offered by Angular seems to be not helping out with the window Object availability checks.

There is an open issue regarding this on Github.

Hopefully, this will be resolved by the Angular team because having to change the source of third party libraries that uses the Window object without a check is just a pain on the fingers.

I will keep this writing in sync.

UPDATE: I have solved my issue a while back and forgot to update this article.

If you are having this issue and the above solutions did not help.

Please see this Github thread for some more solutions which one of them was the one I have posted that solved the issue in my case. I hope this gives more hope.

--

--