What is the process for importing an untyped Leaflet plugin into an Angular application?

I am trying to incorporate the leaflet plugin leaflet-mapwithlabels into my angular project. However, the library does not provide an option for NPM installation. After following some guides, I attempted adding the JS file directly to the node-modules in its own folder and then importing it into my component like this:

import * as L from 'leaflet';
import {GeoJSON} from "leaflet";
import "node_modules/leaflet-mapwithlabels/dist/leaflet-mapwithlabels.js"

and including it in my angular.json file:

            "styles": [
              "@angular/material/prebuilt-themes/azure-blue.css",
              "./node_modules/leaflet/dist/leaflet.css",
              "./node_modules/leaflet-mapwithlabels/dist/leaflet-mapwithlabels.css",
              "src/styles.scss"
            ],
            "scripts": [
              "./node_modules/leaflet/dist/leaflet.js",
              "./node_modules/leaflet-mapwithlabels/dist/leaflet-mapwithlabels.js"
            ]

and referencing it here:

  private map!: L.MapWithLabels;

Unfortunately, this approach did not work as expected. Any guidance on how I can successfully include it would be greatly appreciated.

Here is some relevant information:

package.json:

  "devDependencies": {
    "@angular-devkit/build-angular": "^18.2.1",
    "@angular/cli": "^18.2.1",
    "@angular/compiler-cli": "^18.2.1",
    ...
  "dependencies": {
    ...
    "@bluehalo/ngx-leaflet": "^18.0.2",
    "@ngageoint/geopackage": "^4.2.6",
    "@stijn98s/leaflet.tilelayer.pouchdbcached": "^1.0.0",
    "@types/leaflet.fullscreen": "^3.0.2",
    "leaflet": "^1.9.4",
    "leaflet-geometryutil": "^0.10.3",
    "leaflet.fullscreen": "^3.0.2",

Update: Here is my current attempt, but I am still encountering errors:

I moved the JS file and added this configuration to my angular.json:

"scripts": [
  "node_modules/leaflet/dist/leaflet.js",
  "./node_modules/leaflet.fullscreen/Control.FullScreen.js",
  "./src/assets/scripts/leaflet-mapwithlabels.js"
]

I created a leaflet-extensions.d.ts file in my src/types directory:

import * as L from 'leaflet';
import * as geojson from "geojson";
import {Layer} from "leaflet";

declare module 'leaflet' {
  interface LayerOptions {
    // Specify label options here
  }
  // More definitions...
}

declare module "leaflet-map-with-labels" {
  export = L;
}

And my usage looks like this:

import { 
Injectable 
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import * as L from 'leaflet';
import { Geometry } from 'geojson';
import { CountryProperties } from "../model/interfaces";

@Injectable({
  providedIn: 'root'
})
export class MapService {
  constructor(private http: HttpClient) {}
  initMap(elementId: string): L.MapWithLabels {
    return L.mapWithLabels(elementId, {
      // Map initialization options
    })
  }
  loadGeoJSON(url: string): Observable<GeoJSON.FeatureCollection<Geometry, CountryProperties>> {
    return this.http.get<GeoJSON.FeatureCollection<Geometry, CountryProperties>>(url);
  }
}

However, I am still encountering the following error:

ERROR TypeError: L.mapWithLabels is not a function
at _MapService.initMap (map.service.ts:16:14)

If anyone can provide insights on what may have gone wrong, I would greatly appreciate the help.

Answer №1

After including the JavaScript file in the angular.json > "scripts" array, you need to add the necessary TypeScript definitions for the new classes, factories, and options provided by the library.

For instance, you can define something like this:

import * as geojson from 'geojson';
import { Layer } from 'leaflet';

declare module "leaflet" {
    export class MapWithLabels extends Map {
        constructor(element: string | HTMLElement, options?: MapWithLabelsOptions);
    }
    export interface MapWithLabelsOptions extends MapOptions {
        labelPane?: string;
    }
    export function mapWithLabels(element: string | HTMLElement, options?: MapWithLabelsOptions): MapWithLabels;

    type TypeOrFn<T, Ly extends Layer> = T | ((layer: Ly) => T)
    interface LabelFns<Ly extends Layer> {
        label?: TypeOrFn<string, Ly>;
        labelStyle?: TypeOrFn<object, Ly>;
        labelPriority?: TypeOrFn<number, Ly>;
    }

    export interface LayerOptions extends LabelFns<LayerWithFeature> {
        labelPos?: string;
        // etc.
    }

    class LayerWithFeature<P = any, G extends geojson.GeometryObject = geojson.GeometryObject> extends Layer {
        feature?: geojson.Feature<G, P>
    }
}

You can then utilize these additional classes, factories, and options using TypeScript:

import L from "leaflet";
import './assets/scripts/leaflet-mapwithlabels';

class MyComponent {
    private map!: L.MapWithLabels;
}

// Examples from the library documentation
const map = L.mapWithLabels('map_div', { labelPane: "tooltipPane" });

// Fetch geojson data and create a polygon layer with labels
fetch('hu_megyek.geojson').then(r => r.json()).then(d => {
    L.geoJSON(d, {
        label: l => l.feature?.properties.Nev,
        labelPos: 'cc',
        labelStyle: { color: 'darkblue', whiteSpace: 'normal', minWidth: '120px', textAlign: 'center' },
        labelPriority: l => l.feature?.properties.Nepesseg,
    }).addTo(map);
});

Playground Link

Please note: To ensure that the leaflet-mapwithlabels plugin extends the same global object L used in your application, import the Leaflet L directly instead of using "import * as L":

import L from 'leaflet'; // Import "L" directly, not as "* as L"

View live demo here: https://stackblitz.com/edit/stackblitz-starters-tyrpr2?file=src%2FMapService.service.ts

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The href attribute is not functioning correctly on Internet Explorer version 8

When dynamically generating HTML and appending the response to a DIV, here is the jQuery code: {% elif topic|length < 1 or topic.user_exhausted_attempts %} $('.questionHolder').hide(); var html = '<section class="span9">&a ...

Display a pop-up window using window.open and automatically print its contents using window.print upon loading

I am trying to open a new window with some HTML content and then automatically print it. Here is the code snippet I have: var windowObject = window.open('','windowObject','arguments...'); windowObject.document.write("<html ...

Error: JSDOM - The document variable has not been declared

After creating a basic webpage that displays a single message, I decided to experiment with JSDOM and encountered an error. Despite researching online examples and Stack Overflow questions, I have struggled to resolve even the most straightforward scenario ...

An analysis of Universal Angular.io and Prerender.io from the viewpoint of Googlebot

Currently, my website is set up with Angular 1.4.x and prerender.io, which delivers static cached pages to Googlebot. Googlebot visits each page twice - once by hitting the URL directly, and then again by appending ?_escaped_fragment_ to the URL to access ...

Dispatch an item containing a list within a list

Currently developing a web application utilizing Angular for user input of two arrays and a matrix, which is then sent to Java for calculations. Utilizing FormGroup for the form fields, the values sent to the server appear as follows: object { targetFuncti ...

What is the best way to upload images in Vue.js without using base64 formatting?

Is there a way to upload an image file without it being base64 encoded? I currently can only achieve base64 format when trying to upload an image. How can I modify the code to allow for regular file uploads? <script> submitBox = new Vue({ el: "#su ...

Is Axios phasing out support for simultaneous requests?

According to the current axios documentation, there is a section that is not very well formatted: Concurrency (Deprecated) It is advised to use Promise.all instead of the functions below. These are helper functions for managing concurrent requests. axio ...

Issues with relocating function during the NgOnInit lifecycle hook in an Angular 2 application

Currently, I am facing an issue with my Angular 2 app where the data sometimes lags in populating, causing a page component to load before the information is ready to display. When this happens, I can manually refresh the page for the data to appear correc ...

Utilizing Jquery Autocomplete with multiple arrays for data sourcing

I am facing a challenge where I want to display both doctors and their connections in an autocomplete search using jQuery. Although I have successfully displayed the doctors, I'm struggling to categorize and display data from both arrays separately un ...

For each individual category in the mongoose database, conduct a search

Seeking assistance with mongoose scope issue User.findById(req.user.id, (err, result) => { var cartProduct = []; result.cart.map(async (item) => { //item represents the following object: productId: "product-id", quan ...

JavaScript Thumbnail Slider Builder

I've been working hard on developing a custom JavaScript thumbnail slider that uses data-src. Everything seems to be in order, except the next and previous buttons are not functioning properly. Any assistance would be greatly appreciated. Here's ...

Validation of Date in Angular 5 (with specified minimum and maximum dates)

Struggling to find a simple solution for this issue, I have a date input like the following: <input [(ngModel)]="toolDate" type="text" class="tool_input tool_input__date"> To control the input and restrict it to only accept dates, I am using . In m ...

Managing the jQuery.noConflict function

Upon review, I noticed that the scripts I inherited start like this: var $j = jQuery.noConflict(); The purpose behind this code is not clear to me. While I understand the intent is to avoid conflicts, I am uncertain about the specific conflict it aims to ...

An object may be null when its type is A or undefined, but we are certain it is not undefined

Since the release of version 4.8.4, the TypeScript compiler has been flagging an issue with the following code: type A = {v: number} function get_the_first<T>(xs: T[]): T | undefined { if (xs.length > 1) return xs[0]; else ...

Using AngularJS and SpringMVC for uploading multiple files at once

Having recently delved into the world of angularJS, I've been attempting to upload a file using angular JS and Spring MVC. However, despite my efforts, I have not been able to find a solution and keep encountering exceptions in the JS Controller. Bel ...

I constantly encounter a TypeError message that returns as undefined

I'm currently facing an issue while trying to run my Node.js server locally. The error message I keep receiving is: /Users/rogerjorns/Desktop/templateassignment/node_modules/express/lib/router/index.js:458 throw new TypeError('Router.use() ...

Combining arrays using JavaScript

I'm struggling to enhance the following code - it looks a bit messy: Here is my data format: date d1 d2 d3 d4 d5 d6 110522 5 1 3 5 0 7 110523 9 2 4 6 5 9 110524 0 0 0 0 1 0 110525 0 0 3 0 4 0 ... I am importing data from a text file using d3.j ...

The canvas appears blank when using Firefox version 36

The application is made up of an interface (html) and workspace (canvas). We utilize three.js to create elements on the canvas. So far, it's been functioning perfectly on Chrome, Opera, and Firefox 35. You can see it in action here: However, in Firef ...

Is there a way to verify if all the values in an array of objects are identical?

In this scenario, my array consists of entries with identical address IDs but different phone types and numbers. I am in need of assistance with iterating through the array to extract the phone type and number when the address ID matches. I seem to encount ...

transfer item between a mother and offspring

In my project, I have a convention object that needs to be passed as an input to a child component. The convention ID is displayed correctly in the child's template, however, I encounter an issue where the convention appears as undefined when trying t ...