Map Multiplier: Showcasing Multiple Locations with Popups on a Single Map using Leaflet.js Library (Part 6) in Your Angular-15 Ionic-7 App
Welcome back to the sixth part of our series on building a mobile app with Angular 15 and Ionic 7. In this tutorial, we will learn how to display multiple locations on a single map with popups using the leaflet-js library. Leaflet-js is a lightweight and easy-to-use JavaScript library for creating interactive maps. We will use it to show the locations of all the course modules that we displayed in Part 2 and provide some information about them in the popups. This will help us to enhance the user experience and functionality of our app. Let's get started!
Once individual modules had their own maps displayed, I went on to the main modules page and started working on a map feature that would display all the module locations on a single map. For this, I’ve created a floating action button on the My Modules page i.e. tab1.page.html
like this:
<ion-header [translucent]="false">
...
</ion-header>
<ion-content [fullscreen]="true" *ngIf="newModules">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large"> My Modules</ion-title>
</ion-toolbar>
</ion-header>
...
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button (click)="openMapFull()">
<ion-icon name="map"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>
I have referred to ion-fab: Floating Action Button for Android and iOS Ionic Apps (ionicframework.com) to build the FAB which has a click event binding that calls the openMapFull()
method. Before writing the logic for this method, I created a new modal page:
ionic generate page pages/map-full
Then I shift the page’s route path from app.route.ts
to tabs.route.ts
and import it in the app.component.ts
file.
Next, I import the map-full page in the parent component that is tab1.page.ts
and write the logic for the openMapFull()
method in this way:
...
import { MapFullPage } from '../map-full/map-full.page';
@Component({
selector: 'app-tab1',
templateUrl: 'tab1.page.html',
styleUrls: ['tab1.page.scss'],
standalone: true,
imports: [IonicModule, CommonModule, FormsModule, RouterLink, MapFullPage],
})
export class Tab1Page implements OnInit {
// create 2 variables to store results emitted from observable
modules: any = [];
newModules: any = [];
...
// inject module service into the constructor
constructor(
private moduleService: ModuleServiceService,
private modalCtrl: ModalController
) {}
// call the getModuleData() method when the page loads
ngOnInit() {
// call the getModuleData() method
this.getModuleData();
}
...
// create a method to open modal to view all modules on a map
async openMapFull() {
// create a modal
const modal = await this.modalCtrl.create({
component: MapFullPage,
componentProps: {
// pass the modules array to the modal
modules: this.newModules,
},
breakpoints: [0, 0.75, 1], // set the breakpoints
initialBreakpoint: 0.75, // set the initial breakpoint
});
// present the modal
return await modal.present();
}
...
}
The above method creates a modal to display all modules on a map. It uses the async/await feature to handle asynchronous operations. It starts by defining an async method called openMapFull()
. It creates a modal using the create()
method from the modalCtrl
object. The modal is specified to display the MapFullPage
component with the modules array passed as a property to the component using the componentProps
object. Additionally, it sets breakpoints for the modal using the breakpoints
array which specifies the percentage of the screen width at which the modal will switch between different display modes. The initialBreakpoint
property sets the initial breakpoint value for the modal. Finally, the modal.present()
method is called to present the modal to the user. The await
keyword is used to wait for the modal to be presented before continuing with the code execution.
I have referred to the following Leaflet documentation to build this feature: Quick Start Guide - Leaflet - a JavaScript library for interactive maps (leafletjs.com).
Next, I begin to write the logic for displaying the full map with all module locations in map-full.page.ts
file in the following way:
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { Input } from '@angular/core';
import * as L from 'leaflet';
@Component({
selector: 'app-map-full',
templateUrl: './map-full.page.html',
styleUrls: ['./map-full.page.scss'],
standalone: true,
imports: [IonicModule, CommonModule, FormsModule],
})
export class MapFullPage implements OnInit {
@Input() modules!: Array<any>;
map!: L.Map;
constructor() {}
ngOnInit() {}
ionViewDidEnter() {
// create a spinner to show while the map is loading
const spinner = document.createElement('ion-spinner');
spinner.name = 'crescent';
spinner.style.cssText = 'position: absolute; top: 10%; left: 50%;';
document.getElementById('map')?.appendChild(spinner);
setTimeout(() => {
// timeout to wait for the modal to load and for the div id called map to be created in the html otherwise the the error appears that 'el' property is undefined
//get the first module in the array and use it to set the map view
this.map = L.map('map').setView(
[this.modules[0].lat, this.modules[0].long],
14
);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution:
'Map data © <a href="<https://www.openstreetmap.org/>">OpenStreetMap</a> contributors',
}).addTo(this.map);
// create a blue icon for the markers
const blueIcon = L.icon({
iconUrl: 'assets/images/marker-icon-2x.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowUrl: 'assets/images/marker-shadow.png',
shadowSize: [41, 41],
shadowAnchor: [12, 41],
});
// add a marker to the map for each module in the array
this.modules.forEach((module) => {
L.marker([module.lat, module.long], { icon: blueIcon })
.addTo(this.map)
.bindPopup(
`${module.moduleName} | ${module.location} | Room No. ${module.room}`
)
.openPopup();
});
// zoom map to fit all markers
this.map.fitBounds(this.map.getBounds());
}, 500);
}
}
In the above code, the component class has an @Input()
decorator that specifies an array of modules. The map
variable is declared using the L.Map
type from the Leaflet library which is installed using npm install leaflet
previously. The ionViewDidEnter()
method is executed when the component is viewed. It first creates a spinner element to show while the map is loading. The setTimeout()
method first dismisses the spinner, and then used to wait for the modal to load and for the div
element with an id
attribute of map
to be created in the HTML before the map is rendered. After the timeout, the Leaflet library is used to create a map instance. The map's view is set to the first module's latitude and longitude values. A tile layer is added to the map using the OpenStreetMap's URL template. A blue icon is created for the markers using the L.icon()
method. The forEach()
method is used to add a marker to the map for each module in the array. A popup is bound to each marker that shows the module's name, location, and room number. Finally, the map is zoomed to fit all markers using the fitBounds()
method.
Next, I will use this logic on template page map-full.page.ts
from the official docs in the following way:
<ion-content [fullscreen]="true">
<div class="map-container">
<div class="map-frame">
<div class="map" id="map"></div>
</div>
</div>
</ion-content>
Along with the template, I have added a bit of styling to fill it into the modal in map-full.page.scss
file in the following way:
.map-container {
height: 100%;
width: 100%;
}
.map-frame {
height: 100%;
}
#map {
height: 100%;
margin-top: 20px;
}
And finally, the user can now view all the module locations on a single map as demonstrated in Figure 19 and 20:
Figure 19: My Modules page with Map FAB |
Figure 20: Map Modal with all module locations |
Honestly, I had to do a little digging to figure out how to display multiple datasets on the same component but it was possible with loops. There is still a bug with the complete map where it doesn’t list down all the module names located in the same location but I guess that’s the issue with the pop-up method that doesn’t accept a list of multiple values. As it is visible on the modal page, multiple module names on the same location are not visible.
You have reached the end of this tutorial part on how to create an Angular-15 Ionic-7 app that displays multiple locations on a single map with popups using the leaflet-js library. I hope you enjoyed learning how to use these technologies and how to integrate them in a simple and elegant way.
You have learned how to set up your project, how to create a map component, how to add markers and popups, and how to use geolocation and geocoding services. You have also seen some of the benefits and challenges of using hybrid frameworks like Ionic and web mapping libraries like leaflet-js.
You should now be able to create your own map-based apps with Angular and Ionic, and explore more features and functionalities of these tools.
Thank you for following along and happy coding!