Michał Ziobro
Michał Ziobro

Reputation: 11752

Angular 2+ and implementing Leaflet maps (Open Street Map)

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/[email protected]/dist/leaflet.css"
   integrity="sha512-07I2e+7D8p6he1SIM+1twR5TIrhUQn9+I6yjqD53JQjFiMf8EtC93ty0/5vJTZGF8aAocvHYNEDJajGdNx1IsQ=="
   crossorigin=""/>
    <script src="https://unpkg.com/[email protected]/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

Upvotes: 1

Views: 6144

Answers (1)

Walter Licht
Walter Licht

Reputation: 121

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);
    }
  }

Upvotes: 4

Related Questions