Introduction
Leaflet is a lightweight, open-source JavaScript library for interactive maps. It provides an easy way to integrate maps into web applications. It offers powerful features such as markers, layers, and custom styling.
In this blog post, we will explore the use of the Leaflet library in a Lightning Web Component (LWC). We will show how to display interactive maps. Additionally, we’ll discuss the benefits of using Leaflet, the features it offers, and explain the code implementation in detail. We will use the dynamic element creation technique. This was discussed in our last blog Understanding the Difference: VisualForce vs Lightning Web Components DOM Behavior. This technique allows us to add a dynamic div and render the Leaflet map.
Benefits of Using Leaflet
- Lightweight and Fast: Leaflet’s small size ensures quick loading times and smooth performance, even on devices with limited resources.
- Customizable: Offers a wide range of plugins and APIs to customize the map’s appearance and behavior.
- Cross-Browser Compatibility: Works seamlessly across all modern browsers.
- Easy Integration: Simple API design makes it easy to integrate into any project, including Salesforce LWC.
- Rich Feature Set: Includes support for markers, popups, layers, and tile layers out of the box.
Key Features Implemented
- Interactive Map: Renders a fully functional map within an LWC.
- Dynamic Markers: Supports adding, removing, and managing map markers programmatically.
- Custom Events: Handles events like marker clicks and map readiness.
- Responsive Design: Adapts the map’s height dynamically using API properties.
Code Explanation
leafletMap.html
The HTML template defines a container for the map. The lwc:dom="manual" directive allows direct manipulation of the DOM, which is essential for integrating third-party libraries like Leaflet.
<template>
<div class="container" lwc:dom="manual"></div>
</template>
leafletMap.js
This JavaScript file contains the core logic for rendering and managing the Leaflet map.
Key Features
initializeLeaflet: Loads the Leaflet library usingloadScriptandloadStylefromlightning/platformResourceLoader. Initializes the map with default settings and a tile layer.setMarkers: Clears existing markers and adds new markers dynamically based on the provided data.addMarker: Adds a single marker to the map.removeMarker: Removes a specified marker.sizeMap: Adjusts the map’s view to fit all markers.fireMapReady: Dispatches a custom event when the map is ready.
initializeLeaflet() {
Promise.all([
loadScript(this, leaflet + "/leaflet.js"),
loadStyle(this, leaflet + "/leaflet.css"),
])
.then(() => {
const mapEl = this.template.querySelector("#map-root");
mapEl.style = "height: " + this.height + ";";
this.map = L.map(mapEl).setView([32.95574, -96.824257], 14);
L.tileLayer(
"https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}",
{ attribution: "Map data © OpenStreetMap contributors" }
).addTo(this.map);
this.fireMapReady();
})
.catch((error) => console.error("Error loading Leaflet resources:", error));
}
Example Use Case
recentContactsMap
This component leverages leafletMap to display recent contacts as markers on a map. It fetches contacts latitude and longitude data using an Apex method and updates the map dynamically.
Key Features
- Fetches contact data from Salesforce.
- Creates map markers with contact details.
- Handles marker click events for navigation or additional interactions.
getContacts() {
getRecentContacts()
.then((data) => {
this.contactMarkers = data.map(contact => ({
record: contact,
lat: contact.MailingLatitude,
lng: contact.MailingLongitude
}));
this.setMapMarkers();
})
.catch((error) => console.error("Error fetching contacts:", error));
}
setMapMarkers() {
if (this.map && this.contactMarkers) {
this.map.setMarkers(this.contactMarkers);
}
}
Demo –
Sample Code –
Download the latest leaflet zip file and upload in static resources.
| <!–leafletMap.html–> | |
| <template> | |
| <div class="container" lwc:dom = "manual"></div> | |
| </template> |
| /*leafletMap.js*/ | |
| // Import necessary LWC modules and decorators | |
| import { LightningElement, api } from "lwc"; | |
| // Import Leaflet library for map rendering and operations | |
| import leaflet from "@salesforce/resourceUrl/Leaflet"; | |
| // Import platformResourceLoader for loading external scripts and styles | |
| import { loadStyle, loadScript } from "lightning/platformResourceLoader"; | |
| /** | |
| * LeafletMap component provides a map interface using the Leaflet library. | |
| * Enables adding, removing, and managing map markers dynamically. | |
| */ | |
| export default class LeafletMap extends LightningElement { | |
| // Internal property to store the Leaflet map instance | |
| map; | |
| // Internal property to manage a group of markers | |
| markerGroup; | |
| // Tracks whether the plugin has been initialized | |
| pluginInitialized = false; | |
| scriptTagCreated = false; | |
| // Public property to dynamically set the map's height | |
| @api height; | |
| /** | |
| * Lifecycle hook initializes the Leaflet library when the component is added to the DOM. | |
| * Ensures Leaflet resources are loaded and the map is ready. | |
| */ | |
| connectedCallback() { | |
| if (!this.pluginInitialized) { | |
| this.initializeLeaflet(); | |
| this.pluginInitialized = true; | |
| } | |
| } | |
| /** | |
| * Lifecycle hook dynamically renders the map container if not already created. | |
| * This method ensures that the DOM structure required for the Leaflet map is in place. | |
| * @return {void} | |
| */ | |
| renderedCallback() { | |
| // Check if the script tag for the map container has already been created | |
| if (!this.scriptTagCreated) { | |
| try { | |
| // Create a new <div> element to act as the map container | |
| const mapHost = document.createElement("div"); | |
| // Set a unique ID for the map container for easy reference | |
| mapHost.id = "map-root"; | |
| // Append the map container to the designated container element in the component template | |
| // Ensure the template has an element with class "container" to hold the map | |
| this.template.querySelector(".container").appendChild(mapHost); | |
| } catch (error) { | |
| // Log an error if there's an issue during the creation or appending process | |
| console.error("Error creating map container:", error); | |
| } | |
| // Mark the script tag as created to prevent duplicate map container creation | |
| this.scriptTagCreated = true; | |
| } | |
| } | |
| /** | |
| * initializeLeaflet method loads the Leaflet library and initializes the map instance. | |
| * It ensures that required scripts and styles are loaded before creating the map. | |
| * @return {void} | |
| */ | |
| initializeLeaflet() { | |
| // Load the Leaflet JavaScript and CSS files using Lightning platform resource loader | |
| Promise.all([ | |
| loadScript(this, leaflet + "/leaflet.js"), // Load Leaflet JavaScript library | |
| loadStyle(this, leaflet + "/leaflet.css"), // Load Leaflet CSS for map styling | |
| ]) | |
| .then(() => { | |
| // Retrieve the DOM element where the map will be rendered | |
| const mapEl = this.template.querySelector("#map-root"); | |
| // Dynamically set the height of the map element based on the provided height property | |
| mapEl.style = "height: " + this.height + ";"; | |
| // Initialize the Leaflet map with the specified options | |
| // Set a default geographic view (latitude, longitude) and zoom level | |
| this.map = L.map(mapEl, { zoomControl: false }).setView( | |
| [32.95574, -96.824257], // Default location (latitude, longitude) | |
| 14 // Default zoom level | |
| ); | |
| // Add a tile layer to the map using an external URL | |
| // The tile layer defines the map's visual appearance and data source | |
| L.tileLayer( | |
| "https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}", // URL template for tiles | |
| { attribution: "Justin Lyon @ Slalom" } // Attribution text displayed on the map | |
| ).addTo(this.map); // Add the tile layer to the map instance | |
| // Dispatch a custom event to signal that the map is ready | |
| this.fireMapReady(); | |
| }) | |
| .catch((error) => { | |
| // Handle any errors encountered while loading Leaflet resources | |
| console.error("Error loading Leaflet resources:", error.message); | |
| }); | |
| } | |
| /** | |
| * setMarkers method clears existing markers and adds new markers to the map. | |
| * Resizes the map to fit all markers. | |
| * @param {Array} pins – Array of pin objects with latitude, longitude, and record details | |
| * @returns {Array} – Array of created markers | |
| */ | |
| @api | |
| setMarkers(pins) { | |
| // Clear existing markers or create a new marker group if not initialized | |
| this.resetMarkerGroup(); | |
| // Create new markers for each pin provided | |
| const newMarkers = pins.map(this.createMarker); | |
| // Add each created marker to the marker group | |
| newMarkers.forEach((marker) => marker.addTo(this.markerGroup)); | |
| // Adjust the map view to fit all markers after adding them | |
| this.sizeMap(); | |
| // Return the array of newly created markers for potential further use | |
| return newMarkers; | |
| } | |
| /** | |
| * addMarker method adds a single marker to the map. | |
| * @param {Object} pin – Pin object with latitude, longitude, and record details | |
| * @returns {Object} – The created marker | |
| */ | |
| @api | |
| addMarker(pin) { | |
| // Create a new marker from the provided pin object | |
| const newMarker = this.createMarker(pin); | |
| // Add the new marker to the marker group on the map | |
| newMarker.addTo(this.markerGroup); | |
| // Adjust the map view to accommodate the newly added marker | |
| this.sizeMap(); | |
| // Return the newly created marker for external use if needed | |
| return newMarker; | |
| } | |
| /** | |
| * removeMarker method removes a specific marker from the map. | |
| * @param {Object} marker – The marker to remove | |
| * @return {void} | |
| */ | |
| @api | |
| removeMarker(marker) { | |
| // Remove the specified marker from the marker group | |
| this.markerGroup.removeLayer(marker); | |
| // Resize the map view after removing the marker | |
| this.sizeMap(); | |
| } | |
| /** | |
| * sizeMap method adjusts the map view to fit all markers. | |
| * Expands the bounds slightly to provide padding around the markers. | |
| * @return {void} | |
| */ | |
| @api | |
| sizeMap() { | |
| this.map.fitBounds(this.markerGroup.getBounds().pad(0.1)); | |
| } | |
| /** | |
| * createMarker method creates a Leaflet marker from a pin object. | |
| * Each marker is associated with a specific record and can display a tooltip. | |
| * @param {Object} pin – Pin object with latitude, longitude, and record details | |
| * @returns {Object} – The created Leaflet marker | |
| */ | |
| createMarker(pin) { | |
| // Create a marker at the specified coordinates with associated record details | |
| const marker = L.marker([pin.lat, pin.lng], { record: pin.record }); | |
| // Bind a tooltip to the marker displaying the record name | |
| marker.bindTooltip(pin.record.Name); | |
| // Return the created marker for further use | |
| return marker; | |
| } | |
| /** | |
| * resetMarkerGroup method clears existing markers or initializes the marker group if not present. | |
| * Ensures the marker group is always ready to add new markers. | |
| * @return {void} | |
| */ | |
| resetMarkerGroup() { | |
| if (this.markerGroup) { | |
| // Clear all layers (markers) in the existing marker group | |
| this.markerGroup.clearLayers(); | |
| } else { | |
| // Create a new marker group to manage map markers | |
| this.markerGroup = new L.featureGroup(); | |
| // Add event listeners for marker group interactions | |
| this.markerGroup.on("click", (event) => this.clickMarker(this, event)); | |
| this.markerGroup.on("mouseover", (event) => { | |
| // Display tooltip when hovering over a marker | |
| event.target.openToolTip(); | |
| }); | |
| // Add the marker group to the map instance | |
| this.markerGroup.addTo(this.map); | |
| } | |
| } | |
| /** | |
| * fireMapReady method dispatches a custom event to notify that the map is ready for interaction. | |
| * Provides a hook for parent components to react to map readiness. | |
| * @return {void} | |
| */ | |
| fireMapReady() { | |
| // Create a "ready" custom event | |
| const ready = new CustomEvent("ready"); | |
| // Dispatch the event to notify parent components | |
| this.dispatchEvent(ready); | |
| } | |
| /** | |
| * clickMarker method handles marker click events and triggers a custom event with marker details. | |
| * Useful for external interactions or navigation based on clicked markers. | |
| * @param {Object} self – Reference to the component instance | |
| * @param {Object} layer – Layer object containing marker details | |
| * @return {void} | |
| */ | |
| clickMarker(self, { layer }) { | |
| self.fireMarker(layer); | |
| } | |
| /** | |
| * fireMarker method dispatches a custom event with the details of a clicked marker. | |
| * The event includes the record ID associated with the clicked marker. | |
| * @param {Object} layer – Layer object containing marker details | |
| * @return {void} | |
| */ | |
| fireMarker(layer) { | |
| const pinclick = new CustomEvent("pinclick", { | |
| // Pass the record ID in the event's detail | |
| detail: layer.options.record.Id, | |
| }); | |
| // Dispatch the event to notify parent components of the clicked marker | |
| this.dispatchEvent(pinclick); | |
| } | |
| } |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!–leafletMap.js-meta.xml–> | |
| <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> | |
| <apiVersion>63.0</apiVersion> | |
| <isExposed>true</isExposed> | |
| <targets> | |
| <target>lightning__AppPage</target> | |
| <target>lightning__RecordPage</target> | |
| <target>lightning__HomePage</target> | |
| <target>lightningCommunity__Page</target> | |
| <target>lightningCommunity__Default</target> | |
| </targets> | |
| </LightningComponentBundle> |
| <!–recentContactsMap.html–> | |
| <template> | |
| <c-leaflet-map | |
| class="leaflet-map" | |
| height="400px" | |
| onready={onMapInit} | |
| onpinclick={onMarkerClick} | |
| ></c-leaflet-map> | |
| </template> |
| /*recentContactsMap.js*/ | |
| // Import necessary LWC modules and Apex methods | |
| import { LightningElement } from 'lwc'; | |
| // Import the Apex method to fetch recent contacts | |
| import getRecentContacts from '@salesforce/apex/RecentContactsMapAuraService.getRecentContacts'; | |
| /** | |
| * RecentContactsMap component displays a map with recent contacts as markers. | |
| * Handles map initialization, marker interactions, and data fetching from Salesforce. | |
| */ | |
| export default class RecentContactsMap extends LightningElement { | |
| // Internal property to hold contact markers for the map | |
| contactMarkers; | |
| // Internal property to store the map instance | |
| map; | |
| // Internal property for error handling | |
| error; | |
| /** | |
| * onMapInit is a event handler for initializing the map component. | |
| * Sets up the map instance and fetches contact data to populate markers. | |
| * @returns {void} – No return value | |
| */ | |
| onMapInit() { | |
| // Query and store the map DOM element | |
| this.map = this.template.querySelector('.leaflet-map'); | |
| // Fetch contacts and update the map with markers | |
| this.getContacts(); | |
| } | |
| /** | |
| * onMarkerClick is a event handler for marker click events on the map. | |
| * Dispatches a custom event with marker details to the parent component. | |
| * @param {Event} event – The click event from the map marker | |
| * @returns {void} – No return value | |
| */ | |
| onMarkerClick(event) { | |
| // Dispatch a custom event with marker details | |
| const markerClick = new CustomEvent('markerclick', { | |
| detail: event.detail, // Pass marker details in the event payload | |
| }); | |
| this.dispatchEvent(markerClick); | |
| } | |
| /** | |
| * getContacts method fetches recent contacts using the Apex method. | |
| * Updates contact markers on success or logs an error on failure. | |
| * @returns {void} – No return value | |
| */ | |
| getContacts() { | |
| getRecentContacts() | |
| .then((data) => { | |
| console.log('Fetched contact data:', data); | |
| // Store the retrieved contact markers | |
| this.contactMarkers = data; | |
| // Update map with the retrieved markers | |
| this.setMapMarkers(); | |
| }) | |
| .catch((error) => { | |
| // Handle errors during contact data retrieval | |
| this.error = error; | |
| this.contactMarkers = null; | |
| console.error('Error fetching contacts:', error); | |
| }); | |
| } | |
| /** | |
| * createPins method creates map pin objects from contact data. | |
| * Each pin includes a reference to the contact record and its geographic coordinates. | |
| * @param {Array} contacts – List of contact records | |
| * @returns {Array} – Array of pin objects for the map | |
| */ | |
| createPins(contacts) { | |
| return contacts.map((contact) => ({ | |
| record: contact, | |
| lat: contact.MailingLatitude, | |
| lng: contact.MailingLongitude, | |
| })); | |
| } | |
| /** | |
| * setMapMarkers method updates the map with the current set of contact markers. | |
| * Ensures markers are only set when both map and markers are available. | |
| * @returns {void} – No return value | |
| */ | |
| setMapMarkers() { | |
| if (this.map && this.contactMarkers) { | |
| // Update the map with the current contact markers | |
| this.map.setMarkers(this.contactMarkers); | |
| } | |
| } | |
| } |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!–recentContactsMap.js-meta.xml–> | |
| <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> | |
| <apiVersion>63.0</apiVersion> | |
| <isExposed>true</isExposed> | |
| <targets> | |
| <target>lightning__AppPage</target> | |
| <target>lightning__RecordPage</target> | |
| <target>lightning__HomePage</target> | |
| <target>lightningCommunity__Page</target> | |
| <target>lightningCommunity__Default</target> | |
| <target>lightning__Tab</target> | |
| </targets> | |
| </LightningComponentBundle> |
Conclusion
Using the Leaflet library in LWC provides a powerful way to integrate interactive maps with dynamic data. Its lightweight nature and extensive feature set make it an excellent choice for Salesforce developers. By following the example provided, you can create maps tailored to your business needs. These maps enhance the user experience with visually engaging interfaces.
Reference –
- Special Thanks to justin lyon, for the reference code.
- Refer Leaflet Documentation.


Leave a comment