The basics of Leaflet controls in Angular

This approach is based upon the information published in asymmetrik/ngx-leaflet-tutorial-plugins

Background

When a Leaflet control is initialised - it usually does four things.

  1. Extends L.Control to create a new class, for instance L.Control.Loading(options) i.e. the constructor takes an option object.

  2. Create a factory function (should be L.control.loading) to create a new control, and

  3. Add the control, one way or another, to the map (for instance L.control.loading(options).addTo(map)

  4. Add listerners to the map to get data back - like clicks or location updates.

These are the tasks and objects that we will be dealing with.

Worked Examples

See the following for some worked examples and discusion.

Angular.io

In Angular.io we have to reproduce the above steps.

For the purposes of this project, we also want to do that in a way that is:

  1. Simple and clear,

  2. Idiomatic to Angular

  3. Provides the cleanest API for control in Angular.

In general, these are the steps that need to be performed for each control type with the design decisions that have been made in this project.

  1. Create Typings,

  2. Import the Code,

  3. Create a Directive,

  4. Add the Control to the Map

  5. Sort out the CSS

  6. Interact with the Control (see next page)

Angular Structure

There are many ways that the control could be included into the Angular App structure.

To meet the first three strictures above, we really need to include the control as a directive so that it is controllable by structural directives and it is idiomatic.

My preferred way to do this is as a component directive, either in the project or as an external library. The component is passed the options object and map object in the directive.

However, the controls in the @asymmetrik namespace use the approach of an attribute directive. I just think that this approach will get very complicated very fast as more controls are added to the map.

The rest of this page will assume a component with a component directive.

Create Typings

To use the control in Typescript in Angular CLI (which we do assume your are using) then the type definitions need to be included.

The general format is that the Control, Map and control definitions need to be expanded.

This usually takes the form of expanding the leaflet module with a new class for control in the Control namespace and new interfaces for control and for the options object. The later should ideally be in the leaflet namesapce, but some typings put it in the Control namespace. A typical example would be:

// Type definitions for leaflet-fullscreen 1.0.2
// Project: https://github.com/Leaflet/Leaflet.fullscreen
// Original Definitions by: Denis Carriere <https://github.com/DenisCarriere>
// Updated by : Paul Harwood <https://github.com/runette>

import {Control, ControlOptions} from 'leaflet';

declare module 'leaflet' {
    interface MapOptions {
        fullscreenControl?: boolean | FullscreenOptions;
    }

    interface FullscreenOptions extends ControlOptions {
        pseudoFullscreen?: boolean
        title?: {
            'false': string,
            'true': string
        }
    }

    interface Map {
        isfullscreen(): boolean;
        toggleFullscreen(): void;
    }

    namespace control {
        function fullscreen(options: FullscreenOptions): Control.Fullscreen;
        }
    
    namespace Control {
        export class Fullscreen extends Control{
            constructor (options: FullscreenOptions);
            options: FullscreenOptions;
        }
    }

}

In this case, the extension to the MapOptions definition allows you to configure the control in the map options and the extension to the Map definition is because the control adds methods to the map to return the fullscreen status.

The typings should be (in order of preference) :

  1. In the index.d.ts of the control npm module, in which case it is loaded automatically, or

  2. defined in the @types definition for the control, or

  3. Defined in application typings.d.ts.

Remember that all of the typings will be imported from 'leaflet' even though they are defined in your control!

This is because you have defined them by expanding the leaflet module and thst, in turn, is because controls are defined in Leaflet by expanding the base classes.

Compiling Correctly

The type definitions extend the module leaflet. In one component, there could be defnitions for this module in the @types/leaflet module and in a number of control modules. This is not an easy ask for the compiler.

Most of the time, it will only work if you tell the compiler where to look using compiler directives at the top of the component code. A typical example might be:

/// <reference types='leaflet-sidebar-v2' />
/// <reference types='leaflet.locatecontrol' />
import { Component, OnInit } from '@angular/core';
import {Layer, tileLayer, Map, Control, SidebarOptions} from 'leaflet';

...

Leaflet based apps created like this work for ng build and work with "aot": true but seem to fail badly for the defaultng build --prod configuration.

This seems to be around optimization and I think maybe down to tree-shaking. After some experimentation, I found that ng build --prod works reliably as long as “buildOptimizer”: false is in angular.json.

Import the Code

The Next step is t import the JS code into the TS component.

This is usually done from the npm module something like as follows:

import '../../../../node_modules/leaflet-loading/src/Control.Loading.js'

Exact location may vary.

Create a Directive

As discussed above, we assume that the control is being created as a component, either in the app or an external library.

in the @runette namespace, the component class names are of the form:

export class NgxLoadingControlComponent

and the directives are of the form:

<leaflet-control-loading 
 [map]="map"
 [options]="options"
></leaflet-control-loading>

( where "map" and "object" refer to a valid map object and a loading object valid for that control and, of course, the string "Loading" and "loading" should be replaced by the name of your control).

Obviously, the control component needs to include something like:

@Input map: Map;
@Input options: LoadingOptions

WIth an added complication about the map input ...

Adding the control to the map

This comes in two parts:

  1. Get the map object, and

  2. add the control to the map object.

For the control, the map object is passed to the component in the directive, so the first point does not come up. I would usually use the "onMapReady" method from ngx-leaflet as described in this article .

When a ma is passed to the component, we need to do the following as a minimum (we can, of course, do anything else as well).

  1. Create a new control object

  2. run .addTo(map) to add the control to the specified map.

I define get and set on the map property to allow this.

@Input() set map(map: Map){
    if (map) {
      this._map = map;
      this.loading = new Control.Loading(this.options);
      this.loading.addTo(map);
    }
  }
  get map(): Map {
    return this._map
  }

Sort out the CSS

Ngx-leaflet runs leaflet outside of Angular itself — which I understand to be the best way of avoiding the Leaflet events causing multiple change events in Angular.

However, probably because of this, Leaflet does not fit into the Angular.io CSS encapsulation system and all CSS references have to be at the global level.

This usually means that you add the Leaflet CSS in angular.json as follows

{
    ...
    "styles": [
        "./node_modules/leaflet/dist/leaflet.css",
        "styles.css"
    ],
    ...
}

or in styles.css:

@import "./node_modules/leaflet/dist/leaflet.css"

This is also true for the styles sheets for each control. The preferred solution for this project, since it promotes readability, is to put the leaflet.css in angular.json BEFORE the link to styles.css and to put links to the CSS stylesheets for the controls in styles.css.

In most cases, the stylessheet for the control will be part of the NPM package for the control. Therefore the link in stylse.css will be a reference to the relevant files, for instance:

@import "leaflet-loading/src/Control.Loading.css";

When wrapping a control for use in Angular, it is important to provide this detail in the documentation.

Note, the order of the invokation of the CSS stylessheet is important, since the controls will expect to overwrite the default CSS from leaflet. This is another reason for putting the leaflet CSS in angular.json and the control CSS in styles.css: as long as styles.css is last in angular.json there should not be a problem with this.

Example Usage

Last updated