1

I would like to implement on my web page (ASP.NET Core SPA template (with Angular 2+ - generated with yeoman) Leaflet maps.

So I search for tutorial on leafletjs.com

Firstly tested this example (and it works as standalone):

<html>
<head>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css"
   integrity="sha512-07I2e+7D8p6he1SIM+1twR5TIrhUQn9+I6yjqD53JQjFiMf8EtC93ty0/5vJTZGF8aAocvHYNEDJajGdNx1IsQ=="
   crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js"
   integrity="sha512-A7vV8IFfih/D732iSSKi20u/ooOfj/AGehOKq0f4vLT1Zr2Y+RX7C+w8A1gaSasGtRUZpF/NZgzSAu4/Gc41Lg=="
   crossorigin=""></script>

   <style>
       #map { height: 180px; }
    </style>
</head>

<body>   
<div id="map"></div>


<script>
    var map = L.map('map').setView([50.0817444,19.9253805], 13);

L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

L.marker([50.0817444,19.9253805]).addTo(map)
    .bindPopup('A pretty CSS3 popup.<br> Easily customizable.')
    .openPopup();
</script>

</body>
</html>

Than I have tried to use it with Angular 2+ and create some simple component like LeafletMapComponent. I have done below steps:

  1. add dependencies to package.json
"dependencies": {
    ...
   "leaflet":"^1.0.3"
    ...
}
"devDependencies": {
    ...
    "@types/geojson":"^1.0.2",
    "@types/leaflet":"^1.0.60"
}
  1. add leaflet in webpack.config.vendor.js
entry: {
            vendor: [
                '@angular/common',
                '@angular/compiler',
                '@angular/core',
                '@angular/http',
                '@angular/platform-browser',
                '@angular/platform-browser-dynamic',
                '@angular/router',
                '@angular/platform-server',
                'angular2-universal',
                'angular2-universal-polyfills',
                'bootstrap',
                'bootstrap/dist/css/bootstrap.css',
                'es6-shim',
                'es6-promise',
                'event-source-polyfill',
                'jquery',
                'zone.js',
                'leaflet',
            ]
        },
        output: {
            publicPath: '/dist/',
            filename: '[name].js',
            library: '[name]_[hash]'
        },
  1. Then I have created new component with such simple code
import {Component, OnInit} from '@angular/core'; 
  import * as L from 'leaflet';

@Component({
    selector: 'leaflet-map',
    templateUrl: 'leaflet-map.component.html',
    styleUrls: ['leaflet-map.component.css', '../../../..//node_modules/leaflet/dist/leaflet.css'], }) export class
 LeafletMapComponent implements OnInit { 

    ngAfterViewInit() {
            L.map('leafletMap').setView([50.0817444,19.9253805], 13);
    }

    ngOnInit() {  

    }    
  }
  1. of course I also have #leafletMap div and style applied
 #leafletMap { 
     height: 400px; 
     width: 600px;
  }

  <div id="leafletMap"></div>
  1. I have run app like this:
  $ npm install
  $ rimraf dist && ./node_modules/.bin/webpack --config  
   webpack.config.vendor.js && ./node_modules/.bin/webpack --config
   webpack.config.js
  $ dot net run

But I am still getting an error like this:

fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[0] An unhandled exception has occurred: Call to Node module failed with error: Prerendering failed because of error : ReferenceError: window is not defined

Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143

1 Answers1

4

It seems like you are using Angular Universal (server side rendering) in your ASP.NET Core Angular starter. On server side there is no window object defined (that LeafletMap might use internally).

There are two options:

  1. Disable server side rendering in your project by removing asp-prerender-module attribute from the element in Views/Home/Index.cshtml.
  2. Limit the use of LeafletMap to the client side part by implementing a dedicated execution branch in your code (see https://github.com/angular/universal#universal-gotchas)

EDIT

According to option 2 you must note that there is no panacea on that when loading third party libraries like jquery or LeafletMap. Just the import statement itself (if not removed on code optimization) can cause strange side effects on server side. A "possible" practice (since there is no "best" practice) can be to implement a service that conditionally loads those libraries.

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

@Injectable()
export class LegacyService {
    private _L : any;
    private _jquery : any;

    public constructor(@Inject(PLATFORM_ID) private _platformId: Object) {
        this._init();
    }

    public getJquery() {
        return this._safeGet(() => this._jquery);
    }

    public getL() {
        return this._safeGet(() => this._L);
    }

    private _init() {
        if(isPlatformBrowser(this._platformId)){
            this._requireLegacyResources();     
        }
    }

    private _requireLegacyResources() {
        this._jquery = require('jquery');
        this._L = require('leaflet');
    }

    private _safeGet(getCallcack : () => any) {
        if(isPlatformServer(this._platformId)){
            throw new Error ('invalid access to legacy component on server');
        }

        return getCallcack();
    }
}

On component implementation you should also implement conditional branches that only use the service on client side:

ngAfterViewInit() {
    if(isPlatformBrowser(this.platformId)){
      this._legacyService.getL().map('leafletMap').setView([50.0817444,19.9253805], 13);
    }
  }
Walter Licht
  • 121
  • 4
  • First solution works but the second doesn't. I think there is still the problem with import * as L from 'leaflet'; How to make it conditional based on current platform i.e. browser/server? – Michał Ziobro May 05 '17 at 09:57
  • I have such error so this PLATFORM_ID, isPlatformBrowser, isPlatformServer aren't found! ERROR in [at-loader] ./ClientApp/app/components/leaflet-map/leaflet-map.component.ts:7:29 TS2305: Module '"/Users/michzio/Developer/Angular2_ASP.NET/damkorki/damkorki_app/node_modules/@angular/common/ index"' has no exported member 'isPlatformServer'. – Michał Ziobro May 05 '17 at 10:04
  • Sry, I wasn't reading your post careful enough. isPlatformServer was introduced with Angular 4. Either you update your angular version (recommended on new project) or you follow the instructions for Angular 2 (deprecated repo) see https://github.com/angular/universal/tree/2.x#universal-gotchas – Walter Licht May 05 '17 at 10:51
  • I am trying to update it to angular 4 :) and check how this will work – Michał Ziobro May 05 '17 at 15:56
  • Ok I have upgraded Angular from v2 to v4.1 using this sample https://github.com/angular/universal. Now I can import this isPlatformServer or isPlatformBrowser functions but they seems to not work ? When I wrap my Leaflet code into if(isPratformBrowser(this._platformId)) { } the pre rendering error with no window object. Should I include my LeafletComponent somehow specifically to not being prerendered or build app in some special way (maybe something should be done in web pack configuration)? – Michał Ziobro May 05 '17 at 18:13
  • I edited my answer according to your further issues. Since server side rendering is something that comes with several tradeoffs (like you can see), I would propose that you should always ask for justification when it comes to Angular Universal. See links below: - [https://github.com/angular/universal-starter/issues/137](https://github.com/angular/universal-starter/issues/137) - [https://github.com/angular/universal/issues/490](https://github.com/angular/universal/issues/490) - [https://github.com/angular/universal/issues/512](https://github.com/angular/universal/issues/512) – Walter Licht May 06 '17 at 17:44
  • I don't understand this server prerendering process. It seems like all the code is getting prerendered on the server and when something is checked against isPlatformBrowser() then it is not rendered, and when application is opened in the browser this code isn't rendered anymore. I also similary don't have any console.logs() printed in Javascript Console in Developer Tools of web browser. I am getting this console.log(), and isPlatformBrowser() code blocks executed only when application is rebuilded by HMR (webpack) on some file changes in the project. This seems very odd! – Michał Ziobro May 06 '17 at 20:42
  • Ok it works consol.log(), events handling, router, and even map with prerendering on the server. I have some bugs in client bootstraping file. When I have fix this code, your solution start to works correctly. – Michał Ziobro May 06 '17 at 22:18