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!

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript