ng-conf
Posted on June 3, 2021
Jim Armstrong | ng-conf | Oct 2019
This article is oriented to beginning Angular developers wishing to integrate Leaflet into their applications. The integration of third-party dependencies into Angular can be anywhere from trivial to problematic. While Leaflet tends more towards the trivial side, maps that fluidly adjust with browser size can present some setup problems relative to the Angular lifecycle.
Before starting the deconstruction, point your friendly, neighborhood browser to the following Github.
theAlgorithmist/Angular8-Leaflet](https://github.com/theAlgorithmist/Angular8-Leaflet) on github.com
Now, you can follow along or fork the project and use it as a base for your own applications.
Background
This tutorial presumes a small amount of prior Leaflet knowledge, but no more than is expressed in the Leaflet Quickstart Guide.
ArcGis is used for tiling in the application covered in this article (the esri-leaflet package is installed from npm).
Angular Setup
Very little needs to be done after the ubiquitous npm install. Necessary Leaflet styles are made available through the angular.json file,
"styles": [
"src/styles.scss",
"./node_modules/leaflet/dist/leaflet.css"
],
I’ve had mixed results with @types/leaflet, so this application does not exploit typings for sake of simplicity. Leaflet and ESRI tiles are imported as shown below,
import * as esri from 'esri-leaflet';
import * as L from 'leaflet';
A Leaflet Map, for example, is temporarily typed as any; feel free to firm up typings as an exercise after you have deconstructed the tutorial.
Coordinates of a map’s center are important for map initialization. Many demos hardcode this information. As an alternative, this tutorial illustrates how to inject map-center coordinates using Angular’s Injection Tokens.
A coordinate is a two-dimensional point on a map, represented by latitude and longitude. An InjectionToken for a coordinate may be created as illustrated in the /src/app/tokens.ts file,
import { InjectionToken } from '@angular/core';
export const INIT_COORDS = new InjectionToken<{lat: number, long: number}>('INIT_COORDS');
INIT_COORDS is used in the /src/app/app.module.ts file to perform the actual (lat/long) provision (or definition),
providers: [
{ provide: INIT_COORDS, useValue: {lat: 32.9756, long: -96.89} }
],
This moves the definition of the map center to the highest level in the Angular application (where it is easily seen and almost self-documenting).
In order to inject this coordinate into the application, import INIT_COORDS from the tokens file and inject from a class constructor. This is illustrated in the /src/app/map.component.ts file (the primary map component in this tutorial),
constructor( @Inject(INIT_COORDS) protected _initCoords: {lat: number, long: number} )
Map Component
The application covered in this article displays a fixed-height map with a width that extends across the entire browser window. The map redraws as the window resizes. Some markers are also rendered on top of the map tiles.
The map is integrated into the application through the main app component’s template, as shown below
/src/app/app.component.html
<app-map [markers]="markers"></app-map>
and the map component’s source code is found in /src/app/maps/map.component.ts.
The map is rendered into a container (DIV) that is currently in the map component’s template,
/src/app/maps/map.component.html
<div>
<div id="map-container">
<div #primaryMap [style.height.px]="currentHeight" [style.width.px]="currentWidth"></div>
<map-control class="map-control"></map-control>
<div class="mouse-coords">
<span id="mouse-coords-label"></span>
<span [innerHTML]="mcText"></span>
</div>
</div>
</div>
If the width and height of the containing DIV are fixed, then map tiles can be rendered at an early point in the component lifecycle. Two approaches are possible for passing variable width and height, outside-in and inside-out. The outside-in method defines Angular Inputs for width and height. The parent component is responsible for communicating width and height to the map component. The inside-out approach defines width and height internal to the map component and ‘passes’ that information to its template. Most applications use the outside-in approach. This tutorial employs the inside-out method for purposes of illustration. Modify the code to change the passing of width and height as an exercise.
Variable width and height introduce some subtle issues relative to map rendering. It is tempting to perform everything related to Leaflet setup in the on-init handler, and this typically works with fixed width and height. Since map width varies with browser size in this tutorial, it is better to separate the map initialization process between the on-init and after-view-init lifecycle handlers. This is illustrated in the following code segments from /src/app/maps/map.component.ts.
First, the identification of a containing DIV for map initialization is offloaded to a ViewChild,
@ViewChild('primaryMap', {static: true}) protected mapDivRef: ElementRef;
protected mapDiv: HTMLDivElement;
Most of the map setup is performed in ngOnInit,
public ngOnInit(): void
{
// Reference to DIV containing map is used in Leaflet initialization
this.mapDiv = this.mapDivRef.nativeElement;
this.__initializeMap();
this.__renderMap();
this.__showMarkers();
}
Invalidating the map size is deferred to ngAfterViewInit, when the full width and height necessary for the map tiles to work is available. The map variable refers to the Leaflet Map created in the ngOnInit handler.
public ngAfterViewInit(): void
{
this.map.invalidateSize();
this.__initMapHandlers();
}
Note that the update map size handler recomputes the class currentWidth and currentHeight variables, which are bound to the DIV styles that control the width and height of the map DIV. Leaflet uses that width/height information to re-tile and re-render the map.
Since map width/height definition and resizing the map with window changes are handled internal to the map component, a simple window-resize handler is added.
@HostListener('window:resize', ['$event'])
protected __onResize(event: any): void
{
this.__updateMapSize();
this.map.invalidateSize();
}
This could also be handled by an outside service, run outside Angular, and debounced. If there is sufficient interest, I will provide a tutorial that illustrates the technique.
Map Controls
Leaflet does allow custom controls(this will be a link), but you may also use Angular components as map controls. Create the Angular component and use CSS for positioning. I’ve used this in several projects for highly customized zoom controls. See the /src/app/maps/map-control folder for a skeleton that may be used as a starting point.
Refer to /src/app/maps/map.component.scss for relevant styling.
Beware the z-index
If tiling or some other aspect of map display does not appear to work, it may be an issue with z-index. In the past, I’ve had to re-map z-indices simply from changing tile providers or even migrating from Angular 7 to Angular 8!
Map Markers
Markers with known coordinates may be added to a map. Define the coordinates and then create a Leaflet Icon for each one. Marker coordinates for this tutorial are defined in the main app component,
/src/app/app.component.ts
public markers: {lat: number, long: number}[]; // Map markers (relevance depends on map center)
constructor()
{
// some map markers
this.markers = [
{ lat: 32.9756, long: -96.89 },
{ lat: 33.1543, long: -96.8352 },
{ lat: 32.93 , long: -96.8195 },
{ lat: 32.8998, long: -97.0403 },
{ lat: 33.0737, long: -96.3697 },
{ lat: 33.1014, long: -96.6744 }
];
}
and are passed as an Angular Input to the Map Component,
/src/app/app.component.html
<app-map [markers]="markers"></app-map>
Markers are rendered in the map component,
/src/app/maps/map.component.ts
protected __showMarkers(): void
{
if (this.markers !== undefined && this.markers != null && this.markers.length > 0)
{
// Add markers
const icon = L.icon({
iconUrl: MapIconOptions.mapIcon,
iconSize: MapIconOptions.iconSize,
iconAnchor: MapIconOptions.iconAnchor,
shadowUrl: MapIconOptions.mapShadowIcon,
shadowSize: MapIconOptions.shadowSize,
shadowAnchor: MapIconOptions.shadowAnchor,
});
const n: number = this.markers.length;
let i: number;
let m: L.marker;
let x: number;
let y: number;
for (i = 0; i < n; ++i) {
x = this.markers[i].lat;
y = this.markers[i].long;
if (x !== undefined && !isNaN(x) && y !== undefined && !isNaN(y))
{
// okay to add the icon
m = L.marker([x, y], {icon: icon}).addTo(this.map);
}
else
{
// implement your own error handling
console.log('MARKER ERROR, Marker number: ', (i+1), 'x: ', x, ' y: ', y);
}
}
}
}
Interactivity
A couple handlers for basic map interactivity have been added for you, namely mouse-click and mouse-move. The latter updates the current map position (lat/long) based on the mouse position.
/src/app/maps/map.component.ts
protected __onMapMouseMove(evt: any): void
{
const lat: string = evt.latlng.lat.toFixed(3);
const long: string = evt.latlng.lng.toFixed(3);
this.mcText = `Latitude: ${lat} Longitude: ${long}`;
}
The string reflecting the current position is displayed in the map component’s template,
/src/app/maps/map.component.html
<div class="mouse-coords">
<span id="mouse-coords-label"></span>
<span [innerHTML]="mcText"></span>
</div>
The Result
Build the application and you should observe something similar to this screenshot,
Angular version 8 and Leaflet Map
Move the mouse over the map to observe updates of the mouse position in map coordinates.
I hope this gets you started on working with Angular and Leaflet. These two applications work very well together and there is near-limitless potential for highly interactive, engaging maps.
Good luck with your Angular efforts!
For more Angular goodness, be sure to check out the latest episode of The Angular Show podcast.
ng-conf: Join us for the Reliable Web Summit
Come learn from community members and leaders the best ways to build reliable web applications, write quality code, choose scalable architectures, and create effective automated tests. Powered by ng-conf, join us for the Reliable Web Summit this August 26th & 27th, 2021.
https://reliablewebsummit.com/
Posted on June 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.