angularleafletserver-side-rendering

angular 17+ SSR and leaflet, ngx-leaflet, ngx-leaflet-draw


recently i updated my angular aopp to the newest version of 18 to make use of the new feature introduced in angular 17 native SSR (server side rendering)

one can add SSR quickly but after adding a bunch of errors came up. mainly it were errors caused by using window.xxx in my application code. this will obviously fail as it is running in a node environment for SSR. most of these errors i was able to fix but leaflet is causing me some problems.

especially ngx-leaflet and ngx-leaflet-draw. normal leaflet usage i am able to refactor into using an external service which loads the leaflet module but im not sure how to do this for ngx-leaflet-modules as i am not calling ngx-leaflet directly, but simply importing them in my module files like this.

// backoffice.module.ts
import { LeafletModule } from '@asymmetrik/ngx-leaflet';
import { LeafletDrawModule } from '@asymmetrik/ngx-leaflet-draw';


@NgModule({
  declarations: [
    ...
  ],
  imports: [
    LeafletModule
    LeafletDrawModule
  ]
})

i have the same code in two more module files: 'app.module.ts' and 'util.module.ts'

can i import the modules based on browser environment with the help of a service like described here?

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

I would like to only load these modules when used in a browser. how is it possible to lazyload a module/standalone comonent based on browser environment?

id be happy to be pointed in the right direction

// error message for reference
10:25:30 AM [vite] Error when evaluating SSR module /main.server.mjs:
|- ReferenceError: window is not defined
    at eval (/home/fiehra/dev/weplantaforest/ui2022/node_modules/leaflet/dist/leaflet-src.js:230:19)
    at eval (/home/fiehra/dev/weplantaforest/ui2022/node_modules/leaflet/dist/leaflet-src.js:7:66)
    at node_modules/leaflet/dist/leaflet-src.js (/home/fiehra/dev/weplantaforest/ui2022/node_modules/leaflet/dist/leaflet-src.js:10:1)
    at __require2 (/home/fiehra/dev/weplantaforest/ui2022/.angular/vite-root/ui2022/chunk-Q4AGBL6P.mjs:47:50)
    at eval (/home/fiehra/dev/weplantaforest/ui2022/node_modules/@asymmetrik/ngx-leaflet/fesm2022/asymmetrik-ngx-leaflet.mjs:3:49)
    at async instantiateModule (file:///home/fiehra/dev/weplantaforest/ui2022/node_modules/vite/dist/node/chunks/dep-cNe07EU9.js:55058:9)

10:25:30 AM [vite] Internal server error: window is not defined
      at eval (/home/fiehra/dev/weplantaforest/ui2022/node_modules/leaflet/dist/leaflet-src.js:230:19)
      at eval (/home/fiehra/dev/weplantaforest/ui2022/node_modules/leaflet/dist/leaflet-src.js:7:66)
      at node_modules/leaflet/dist/leaflet-src.js (/home/fiehra/dev/weplantaforest/ui2022/node_modules/leaflet/dist/leaflet-src.js:10:1)
      at __require2 (/home/fiehra/dev/weplantaforest/ui2022/.angular/vite-root/ui2022/chunk-Q4AGBL6P.mjs:47:50)
      at eval (/home/fiehra/dev/weplantaforest/ui2022/node_modules/@asymmetrik/ngx-leaflet/fesm2022/asymmetrik-ngx-leaflet.mjs:3:49)
      at async instantiateModule (file:///home/fiehra/dev/weplantaforest/ui2022/node_modules/vite/dist/node/chunks/dep-cNe07EU9.js:55058:9)

Solution

  • thanks to being pointed in the right direction i was able to find a working solution to make leaflet work with server side rendering in an angular 18+ app. angular introduced standalone components which i needed to make SSR work with leaflet. so i refactored my app from a module into a standalone approach before implementing SSR.

    in the next step I isolated the leaflet import into a helper file

    // leaflet.helper.ts
    import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
    
    @Injectable({
      providedIn: 'root',
    })
    export class LeafletHelper {
      public L = null;
    
      constructor(@Inject(PLATFORM_ID) private platformId: Object) {
        if (this.platformId === 'browser') {
          this.L = import('leaflet');
        }
      }
    
      async loadLeaflet() {
        this.L = await this.L;
        return this.L;
      }
    }
    

    then I injected the helper into my standalone leaflet map component and refactored the leafletmap component to use the vanilla JS implementation. this was necessary because the ngx-leaflet is not compatible with SSR.

    // leaflet-map.component.ts
    export class LeafletMapComponent {
      map: any;
      lat = 52.52437;
      lng = 13.41053;
    
      constructor(
        private leafletHelper: LeafletHelper,
        @Inject(PLATFORM_ID) private _platformId: Object
      ) {}
    
      ngOnInit() {
        // check if app is running in browser and then lazyload leaflet because of SSR
        if (this._platformId === 'browser') {
          this.leafletHelper.loadLeaflet().then((leafletLib) => {
            this.initMap(leafletLib);
          })
        }
      }
    
      initMap(lib: any): void {
        this.map = lib.map('map', {
          layers: [
            lib.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
              maxZoom: 21,
              maxNativeZoom: 19,
              minZoom: 3,
              attribution: '...',
              noWrap: true,
            }),
          ],
          zoom: 10,
          center: lib.latLng(this.lat, this.lng),
        });
      }
    }
    

    i also created a platformhelper to isolate browser functions like window...

    // platform.helper.ts
    import { DOCUMENT, isPlatformBrowser } from '@angular/common';
    import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
    
    @Injectable({
      providedIn: 'root',
    })
    export class PlatformHelper {
      isBrowser: boolean;
      localstorage: any;
    
      constructor(@Inject(PLATFORM_ID) private platformId: any) {
        this.isBrowser = isPlatformBrowser(this.platformId);
      }
    
      checkIfBrowser() {
        return this.isBrowser
      }
    
      scrollTop() {
        if (this.isBrowser) {
          window.scrollTo(0, 0);
        }
      }
    
    }
    

    Ultimately in the end i had to do a lot of refactoring to make this work. other browser functions were also not working and had to be isolated in a seperate helper/service file. in the end learning how to implement SSR in a smaller scope helped me integrate it into a more sophisticated app.

    source codes for reference:

    https://gitlab.com/fiehra/aeye/-/tree/main/frontend?ref_type=heads

    https://github.com/Dica-Developer/weplantaforest/tree/master/ui2022