Angular Universal (SSR), with Leaflet and ngx-leaflet

2.5k Views Asked by At

I am quite recent with angular (not to say noob) and I am strugling passing a standard Angular app to Angular Universal because of issues with Leaflet, I have found many exemples of projects working fine but there is no way that I manage to have it working the same way in my project. I managed to isolate the issue but i cannot solve it. I removed all references to leaflet in all the components and modules and just left the package.json as below :

  "name": "vyv-angular",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "dev:ssr": "ng run vyv-angular:serve-ssr",
    "serve:ssr": "node dist/vyv-angular/server/main.js",
    "build:ssr": "ng build --prod && ng run vyv-angular:server:production",
    "prerender": "ng run vyv-angular:prerender"
  },
  "private": true,
  "dependencies": {
    "@agm/core": "3.0.0-beta.0",
    "@angular/animations": "10.1.5",
    "@angular/cdk": "10.2.4",
    "@angular/common": "10.1.5",
    "@angular/compiler": "10.1.5",
    "@angular/core": "10.1.5",
    "@angular/fire": "6.0.3",
    "@angular/forms": "10.1.5",
    "@angular/localize": "10.1.5",
    "@angular/material": "10.2.4",
    "@angular/platform-browser": "^10.1.5",
    "@angular/platform-browser-dynamic": "10.1.5",
    "@angular/platform-server": "^10.1.5",
    "@angular/router": "10.1.5",
    "@asymmetrik/ngx-leaflet": "8.1.0",
    "@ng-bootstrap/ng-bootstrap": "7.0.0",
    "@ng-bootstrap/schematics": "2.0.0-alpha.1",
    "@ng-select/ng-select": "5.0.3",
    "@ngrx/store": "10.0.0",
    "@nguniversal/express-engine": "10.1.0",
    "@nguniversal/module-map-ngfactory-loader": "8.2.6",
    "@ngx-translate/core": "13.0.0",
    "@ngx-translate/http-loader": "6.0.0",
    "@swimlane/ngx-datatable": "18.0.0",
    "acorn": "8.0.1",
    "agm-direction": "0.8.7",
    "body-parser": "1.19.0",
    "bootstrap": "4.0.0",
    "classlist.js": "1.1.20150312",
    "core-js": "3.6.5",
    "cors": "2.8.5",
    "d3": "6.1.1",
    "datatables.net": "1.10.22",
    "date-fns": "2.16.1",
    "express": "4.17.1",
    "firebase": "7.23.0",
    "google-libphonenumber": "^3.2.13",
    "hopscotch": "0.3.1",
    "intl": "1.2.5",
    "intl-tel-input": "17.0.6",
    "leaflet": "1.7.1",
    "leaflet-ant-path": "1.3.0",
    "material-design-icons": "3.0.1",
    "md5-typescript": "1.0.5",
    "moment": "2.28.0",
    "ng-apexcharts": "1.5.1",
    "ng-click-outside": "7.0.0",
    "ngx-bootstrap": "^6.1.0",
    "ngx-custom-validators": "9.1.0",
    "ngx-device-detector": "2.0.0",
    "ngx-intl-tel-input": "^3.0.3",
    "ngx-perfect-scrollbar": "10.0.1",
    "ngx-quill": "12.0.1",
    "ngx-spinner": "10.0.1",
    "ngx-take-until-destroy": "5.4.0",
    "ngx-toastr": "13.0.0",
    "ngx-ui-switch": "10.0.2",
    "node-sass": "4.14.1",
    "nodemailer": "6.4.13",
    "nouislider": "14.6.2",
    "popper.js": "1.16.1",
    "postcss-rtl": "1.7.3",
    "prismjs": "1.21.0",
    "pug": "3.0.0",
    "quill": "1.3.7",
    "rxjs": "6.6.3",
    "rxjs-compat": "^6.6.3",
    "screenfull": "5.0.2",
    "tslib": "2.0.1",
    "web-animations-js": "2.3.2",
    "zone.js": "0.11.1"
  },
  "devDependencies": {
    "@angular-devkit/architect": "0.1001.6",
    "@angular-devkit/build-angular": "0.1001.6",
    "@angular/cli": "10.1.6",
    "@angular/compiler-cli": "10.1.5",
    "@nguniversal/builders": "10.1.0",
    "@types/chartist": "0.11.0",
    "@types/cors": "2.8.8",
    "@types/d3-shape": "1.3.2",
    "@types/express": "4.17.0",
    "@types/google-maps": "3.2.2",
    "@types/googlemaps": "3.39.12",
    "@types/jasmine": "3.5.0",
    "@types/jasminewd2": "2.0.3",
    "@types/jquery": "3.5.2",
    "@types/node": "14.11.8",
    "@types/nodemailer": "6.4.0",
    "codelyzer": "6.0.1",
    "firebase-admin": "9.2.0",
    "firebase-functions": "3.6.0",
    "firebase-functions-test": "0.2.2",
    "firebase-tools": "8.12.1",
    "fuzzy": "0.1.3",
    "inquirer": "6.2.2",
    "inquirer-autocomplete-prompt": "1.0.1",
    "jasmine-core": "3.6.0",
    "jasmine-spec-reporter": "4.2.1",
    "karma": "5.0.0",
    "karma-chrome-launcher": "3.1.0",
    "karma-coverage-istanbul-reporter": "2.1.0",
    "karma-jasmine": "3.0.1",
    "karma-jasmine-html-reporter": "1.4.2",
    "open": "7.3.0",
    "protractor": "7.0.0",
    "rxjs-compat": "6.6.3",
    "tslint": "6.1.0",
    "typescript": "4.0.3"
  }
}

and then

ng run dev:ssr

runs fine and the app works with server side rendering (except the maping part of course as it was removed).

Then I start to re-introduce the ngx-leaflet module step by step. The first step is to simply add the module in the app.module.ts

import {LeafletModule} from '@asymmetrik/ngx-leaflet';

and in the imports seccion :

 imports: [
LeafletModule,

just adding this, without any usage in any components ( i leave my maps components empty) gives me the following error :

    > ng run vyv-angular:serve-ssr

****************************************************************************************
This is a simple server for use in testing or debugging Angular applications locally.
It hasn't been reviewed for security issues.

DON'T USE IT FOR PRODUCTION!
****************************************************************************************
Hash: c84349b0ffb562a756f2
Time: 40944ms
Built at: 2020-10-11 9:17:28 ├F10: AM┤
      Asset      Size  Chunks                          Chunk Names
    main.js  13.3 MiB    main  [emitted]        [big]  main
main.js.map  13.2 MiB    main  [emitted] [dev]         main
Entrypoint main [big] = main.js main.js.map
chunk {main} main.js, main.js.map (main) 11.4 MiB [entry] [rendered]

chunk {main} main.js, main.js.map (main) 1.36 MB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 147 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.15 kB [entry] [rendered]
chunk {styles} styles.css, styles.css.map (styles) 1.36 MB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 8.77 MB [initial] [rendered]
Date: 2020-10-11T14:17:45.082Z - Hash: bde98637bf23c20bf894 - Time: 50331ms

Compiled successfully.
F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:65133
  var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer;
                  ^

ReferenceError: window is not defined
    at F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:65133:19
    at F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:64914:11
    at Object.4R65 (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:64916:2)
    at __webpack_require__ (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:20:30)
    at F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:116999:129
    at Object.RlJC (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:117001:2)
    at __webpack_require__ (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:20:30)
    at Module.ZAI4 (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:134326:82)
    at __webpack_require__ (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:20:30)
    at Module.24aS (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:54859:69)

A server error has occurred.
node exited with 1 code.
connect ECONNREFUSED 127.0.0.1:58155

I identified that the "var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer;" line is inside the leaflet-src.js, under node-modules/ so it is the leaflet javascript reference to the window that fails, this is normal because running on a server, but why is this error appearing if i don't even trigger any usage of the script ? I managed to clone repositories of working exemples but the package.json , app.module.ts, and other components are identical to mine. So the issue is somewhere else but i can't findout where. Did I simply migrate my standard Angular app to Angular Universal in a wrong way ? I followed the standard guidelines for this running :

ng add @nguniversal/express-engine

and all went fine.

I then I also ran :

ng add @angular/fire

in order to be able to deploy as a function in firebase but for the moment the issue is not the deploying part, the SSR server doesn't start with the ngx-leaflet module imported in the app.module.ts.

Should I add any other files to help here ? The ng add @nguniversal/express-engine created new files for the server and ts configs but i did not modify them...

Any idea or comment to put me on the right track are welcomed !

EDIT :

The only diference between the working exemples I cloned and mine is that the import of the ngx-module is done in a sub-module only loaded via the router. I tried in those projects to add the import in the main app.modules.ts and the same error occurs !! So the the issue seems only happens when the import of this module is at the top level. I will try to implement my maps components in a separate module and will keep this post updated with a working solution if I find one...

3

There are 3 best solutions below

2
On BEST ANSWER

Well after a few days of suffuring I decided to drop the ngx-leaflet module. I managed to have the leaflet maps working with the original javascript library based on the idea of Sergey from this issue : How to use @angular/universal with Leaflet?. This meant some modifications to the component as the ease of integration of leaflet within angular provided by ngx-leaflet is lost but this is worth the pain.

The solution is as Sergey explained is to wrap the access to the leaflet script inside a service, but then on my project the "require" javascript method used in the service was not recognized, and this even if the "@types/node" was correctly installed. The solution to this next issue is the modify the tsconfig.app.json and add the following : "types": [ "node" ] in the compiler options, that resulted as follows in my case:

  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": [ "node" ],
    "typeRoots": [ "../node_modules/@types" ]
  },

because it was already present in the tsconfig.server.json but this wasn't enough.

in your coponents then (also as Sergei explained), be careful not to use or import any Leflet methods, "@types/leaflet" are fine though. All reference to the "L" leaflet script has to be done through the service , embeded in a test that tests the presence of the browser :

  if (isPlatformBrowser(this.platformId)) {
     this.leafletService.L.
  }
3
On

The error message is clear, it's because of your usage of the window object. You can identify the browser and execute the code only when inside the browser as follow:

import { isPlatformBrowser } from '@angular/common';
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';

@Component({
  selector: 'app-yourcomponent',
  templateUrl: './app.yourcomponent.html',
})
export class YourComponent implements OnInit {
  constructor(
    @Inject(PLATFORM_ID) private _platformId
  ) {}

  ngOnInit() {
    if (isPlatformBrowser(this._platformId)) {
      // Whatever you want to run inside the browser
      window.scrollTo(0, 0);
    }
  }
}
0
On

You have to wrap leaflet import into the service. Working example can be downloaded from the github here: https://github.com/NickToony/universal-starter-leaflet-example

import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable()
export class MapService {

  public L = null;

  constructor(@Inject(PLATFORM_ID) private platformId: Object) {
    if (isPlatformBrowser(platformId)) {
      this.L = require('leaflet');
    }
  }

}