Customizable Maps with Leaflet

Leaflet is an awesome JavaScript library for creating maps. Today, we’ll see how easy it is to create a map with customizable markers. There’s a ton of ways you can customize a marker in Leaflet (custom marker images, styling, etc). For this example, I chose to add the ability to change the text of the marker’s popup.

Here’s a working example if you’d rather see it in action than read about it 🙂

Heads up that I chose to use jQuery for code simplicity and ease of access to those newer to JavaScript. I encourage you to use a JavaScript framework such as Vue/React/etc so you don’t have to do any DOM manipulation or event handling on your own.

Now on with the code walkthrough.

The Code

First, Leaflet requires an element with some height to display the map inside of:

<div id="map">
</div>
#map { height: 400px; }

I wanted a way to present the data you’ve added to the map so there’s also a table for it:

<table id="data-table">
  <thead>
    <tr>
      <th>Latitude</th>
      <th>Longitude</th>
      <th>Note</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>

Creating the actual map is dead simple.

// Create the Leaflet Map object
let map = L.map('map').setView([51.5074, -0.1278], 13);

// Add the OpenStreetMap tile set and associated attribution info
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map)

There are a couple helper functions needed for actions we do over and over. First, we want a function that inspects the map to update the data table:

/*
  Update the data table with the location and text of each marker
*/
function updateDataTable (map) {
	let dataTableBody = $('#data-table tbody')
  dataTableBody.html('<tr></tr>')
  map.eachLayer(layer => {
    let popup = layer.getPopup()
    if (popup) {
      let text = $(popup.getContent()).find('.popup-span').text()
      let latLng = layer.getLatLng()
      dataTableBody.append(
        $('<tr></tr>').append(
          $('<td></td>').text(latLng.lat),
          $('<td></td>').text(latLng.lng),
          $('<td></td>').text(text),
         )
      )
    }
  })
}

We also need a function to construct the contents of all these popups we’ll be making. The popups have three components: the customizable text, an edit button to change that text, and a trash button to remove the marker from the map:

/*
  Create and return the contents of our custom popups including
 	some default text and buttons for editing the text and removing
  the marker.
*/
function createPopupContents(marker) {
  let popupContents = $('<div></div>')
  popupContents.append(
    $('<span></span>').addClass('popup-span').text('Default marker')
  )
  
  let trashButton = $('<button><i class="fas fa-trash" /></button>')
    .on('click', event => {
  	  marker.remove()
      updateDataTable(map)
    })
  
 	let editButton = $('<button><i class="fas fa-edit" /></button>')
    .on('click', event => {
      let newContent = prompt('Enter the note for this marker')
      popupContents.find('.popup-span').text(newContent)
      updateDataTable(map)
    })
    
  popupContents.append(editButton, trashButton)
  return popupContents[0]
}

To enable adding a marker when clicking on the map, we add a click handler to Leaflet.

This part is a little more complex than it should be. One of the built-in ways to close a Popup in Leaflet is by clicking outside of it, which makes sense. However, since there’s no way to easily differentiate between clicking to add our marker and clicking to close a popup, I decided to create my own way. Luckily, Leaflet provides a preclick event that can be used to check if any popups are open when clicking. I then use the boolean I set in that check to decide whether to actually create a new marker in the click handler. A little messy but it works.

// Keep track if any popups are open (used below to prevent
// creating a new marker when a user clicks on the map just
// to close a popup)
let popupOpen = null
map.on('preclick', mapEvent => {
	popupOpen = false
  map.eachLayer(layer => {
  	if (layer.getPopup() && layer.getPopup().isOpen()) {
    	popupOpen = true
    }
  })
})

// When a user clicks on the map, add a marker
map.on('click', mapEvent => {
  if (!popupOpen) {
    let marker = L.marker(mapEvent.latlng)
    marker.addTo(map).bindPopup(createPopupContents(marker));
    updateDataTable(map)
  }
})

The only thing left to do is add a marker to the map so there’s something to see before interacting with the map. We also want to initialize the data table with that default marker info:

// Create a default marker when the map first loads
let defaultMarker = L.marker([51.5074, -0.14])
defaultMarker.addTo(map).bindPopup(createPopupContents(defaultMarker));

// Set the initial state of the data table
updateDataTable(map)
Leaflet Map with Customizable Marker Text

Don’t forget to check out the Fiddle to see the map action.

Next Steps

There’s plenty you can add from here. A couple suggestions that as a good next step:

  • Enable dragging on the marker (be sure to update the data table after dragging)
  • Try getting the user’s location to use as the default map position

And if you want to learn more about Leaflet and maps in general, check out some of my other posts on the topic: