Nick
Nick

Reputation: 175

ERROR TypeError: a.markerClusterGroup is not a function

I am currently using asymmetrik's ngx-leaflet and ngx-leaflet-markercluster with the following versions:

"@asymmetrik/ngx-leaflet": "^5.0.2",
"@asymmetrik/ngx-leaflet-markercluster": "^2.1.1"

During development everything is fine. However, when I build the angular project for production (ng serve --prod), I am not able to see the cluster markers and I get the following error in Chrome's console:

ERROR TypeError: a.markerClusterGroup is not a function
    at t.ngOnInit (main-es2015.94505399b2d83c23de95.js:1)
    at main-es2015.94505399b2d83c23de95.js:1
    at main-es2015.94505399b2d83c23de95.js:1
    at Kb (main-es2015.94505399b2d83c23de95.js:1)
    at xw (main-es2015.94505399b2d83c23de95.js:1)
    at Object.updateDirectives (14-es2015.a3c30690716486447af1.js:1)
    at Object.updateDirectives (main-es2015.94505399b2d83c23de95.js:1)
    at Xb (main-es2015.94505399b2d83c23de95.js:1)
    at rw (main-es2015.94505399b2d83c23de95.js:1)
    at nw (main-es2015.94505399b2d83c23de95.js:1)

I am importing 'leaflet' and 'leaflet.markercluster' in my component. Below is the map.component.ts code:

import { Component, Input, SimpleChanges } from '@angular/core';
import { tileLayer, latLng, control, marker, divIcon, LatLngBounds, MarkerClusterGroup, MarkerClusterGroupOptions } from 'leaflet';

import { NgElement, WithProperties } from '@angular/elements';

import * as L from 'leaflet';
import 'leaflet.markercluster';
...

@Component({
    selector: 'map',
    styleUrls: ['./map.component.scss'],
    templateUrl: './map.component.html'
})
export class MapComponent {

    layers = [];
    markerClusterGroup: L.MarkerClusterGroup;
    markerClusterData: any[] = [];
    maxBounds: L.LatLngBounds;
    map: L.Map;

    options = {
        layers: [
            tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18 }),
        ],
        zoom: 2,
        zoomControl: false,
        maxZoom: 18
    };

    markerClusterOptions: L.MarkerClusterGroupOptions = {
        showCoverageOnHover: true,
        animate: true,
        chunkedLoading: true,
        zoomToBoundsOnClick: true,
        spiderfyOnMaxZoom: true,
        chunkDelay: 500,
        animateAddingMarkers: true,
        chunkInterval: 7,
        iconCreateFunction: function (cluster) {
            var markers = cluster.getAllChildMarkers();
            var icon = divIcon({
                iconSize: [35, 35],
                iconAnchor: [10, 10],
                popupAnchor: [10, 0],
                shadowSize: [0, 0],
                html: '<div>' + cluster.getChildCount() + '</div>'
            });

            if (markers.find(x => x.options.alt == enum.A)) {
                icon.options.className = 'cluster cluster-critical';
            } else if (markers.find(x => x.options.alt == enum.B)) {
                icon.options.className = 'cluster cluster-warning';
            } else {
                icon.options.className = 'cluster cluster-normal';
            }

            return icon;
        }
    };

    ngOnChanges(changes: SimpleChanges): void {
        this.fixMap();
    }

    mapReady(map: L.Map) {
        this.map = map;
        map.addControl(control.zoom({ position: 'bottomright' }));
        this.fixMap();
    }

    fixMap() {
        if (this.map) {
            this.setBounds();
            this.map.fitBounds(this.maxBounds);
            this.initMarkers();
            setTimeout(() => {
                this.map.invalidateSize();
            }, 0);
        }
    }

    markerClusterReady(group: L.MarkerClusterGroup) {
        this.markerClusterGroup = group;
        this.markerClusterGroup.addLayers(this.markerClusterData);
    }

    initMarkers() {
        this.markerClusterData = [];
        this.objectList.forEach(x => {
            var location = latLng(x.location.latitude, x.location.longitude);
            var system = marker(location, {
                icon: divIcon({
                    iconSize: [30, 30],
                    iconAnchor: [10, 10],
                    popupAnchor: [10, 0],
                    shadowSize: [0, 0],
                    className: this.getMarkerStyle(x),
                }),
                title: x.serialNumber,
                alt: x.objectStatusSummary.status
            });

            system.bindPopup(fl => this.createPopupComponent(x), {
                className: 'popup',
                closeButton: false
            }).on('click', (data) => {
                this.focusOnMarker(x);
            });

            this.markerClusterData.push(system);
        });
    }

    focusOnMarker(object: Object) {
        var bounds = new L.LatLngBounds([[object.objectLocation.latitude, object.objectLocation.longitude]]);
        this.map.fitBounds(bounds, {
            animate: true,
            duration: 1,
            easeLinearity: 1,
            maxZoom: this.map.getZoom(),
            noMoveStart: true
        });
    }

    setBounds() {
        var latLngArray: any[] = [];
        if (this.objectList && this.objectList.length > 0) {
            this.objectList.forEach(x => {
                latLngArray.push({
                    lat: x.objectLocation.latitude,
                    lng: x.objectLocation.longitude,
                });
            });
        }
        else {
            latLngArray.push({
                lat: 0,
                lng: 0
            });
        }
        this.maxBounds = new L.LatLngBounds(latLngArray);
    }

    getMarkerStyle(object: object) {
        return 'marker marker-' + this.businessHelper.getClassByStatus(<objectHealthStatusEnum>object.objectStatusSummary.status) + ' marker-' + object.serialNumber;
    }

    resetZoom() {
        this.map.fitBounds(this.maxBounds, {
            animate: true,
            duration: 1,
            easeLinearity: 1,
            noMoveStart: true
        });
    }

    public createPopupComponent(object) {
        const popupEl: NgElement & WithProperties<MapPopupComponent> = document.createElement('popup-element') as any;
        popupEl.addEventListener('closed', () => document.body.removeChild(popupEl));
        popupEl.object = object;
        document.body.appendChild(popupEl);
        return popupEl;
    }
}

This is the map.component.html code:

<div class="map-wrapper">
    <div 
        leaflet 
        [leafletOptions]="options" 
        (leafletMapReady)="mapReady($event)"

        [leafletMarkerCluster]="markerClusterData"
        [leafletMarkerClusterOptions]="markerClusterOptions"
        (leafletMarkerClusterReady)="markerClusterReady($event)"
    ></div>
</div>

You can find the main code here.

Any ideas?

Upvotes: 3

Views: 2796

Answers (4)

genter_morgan
genter_morgan

Reputation: 61

I've come across this issue too. I solved this problem by next steps:

  1. Add leaflet and leaflet.markercluster scripts in a “scripts” array (angular.json), in the current order:
"scripts": [
  "node_modules/leaflet/dist/leaflet.js",
  "node_modules/leaflet.markercluster/dist/leaflet.markercluster.js"
    ];
  1. These scripts will add “L” object as a field of the global “window” object. Now you can get Leaflet extended by “markercluster” plugin as: window.L anywhere;

  2. Define a private field (“leaflet” in my case) in component/directive/services classes, where you use Leaflet:


import 'leaflet.markercluster'; // you need import this locally anyway for "markercluster" methods ts-declaration

@Component({})
export class AppComponent() {
  private leaflet = window.L; // here is our leaflet with the clusters

  initMap() {
    const map = this.leaflet.map('map').setView([0, 0], 10);
    const markers = this.leaflet.markerClusterGroup(); // now this method is defined!
    map.addLayer(markers);
  }
}

Works for me on Angular 18.2.0.

Upvotes: 0

Jovan Nikolic
Jovan Nikolic

Reputation: 21

Hello I had same issue in Angular project. I'm using NPMs.

npm i leaflet @types/leaflet
npm i leaflet.markercluster @types/leaflet.markercluster
npm i leaflet.fullscreen @types/leaflet.fullscreen

Solution: Import leaflet and its plugins in your app.module.ts file:

import 'leaflet';
import 'leaflet.markercluster';
import 'leaflet.fullscreen'

And also import these like this in your component where you use your map:

import * as L from 'leaflet';
import 'leaflet.markercluster';
import 'leaflet.fullscreen';

It is because of Lazy loading modules. Project will work on localhost, but build version won't. So you have to import like I said above.

Upvotes: 2

user3740359
user3740359

Reputation: 405

I have had the same problem and found a solution. The problem is that there are different leaflets imported as 'L' from the definitions. Make sure to import leaflet first, then the other packages, e.g.:

import * as L from 'leaflet';
import { Map, MapOptions, MarkerClusterGroup, MarkerClusterGroupOptions } from 'leaflet';
import 'leaflet.markercluster';

another guess would be that the timing in production is different than on develop. your initMarkers gets called via ngOnChanges but that might be to early. The map or the cluster might not be initialized yet. Try to call initMarkers within markerClusterReady or mapReady

Upvotes: 2

Jay Z
Jay Z

Reputation: 17

Don't import leaflet through html, install the package and import the modules you need instead

Upvotes: 0

Related Questions