pwshub.com

Using Asynchronous Content in Leaflet Popups

Today in my <Code><Br> stream (I'll share a link to the video at the bottom), I spent some time digging into Leaflet and worked on a demo that made use of the National Parks Service API. This is a fun API I've used many times in the past, especially at my last job at HERE. For the stream today, I wanted to build the following:

  • Create a map that loads a geojson file of NPS parks. The geojson file contains the code and name for each park.
  • On clicking one of the markers, use the NPS API to get more information about the park.

In general, I've found everything in Leaflet to be stupid easy, but this particular aspect turned out to be a bit more difficult, which of course made for a fun stream. I got it working, but I want folks to know I'm not 100% convinced that the solution shown here is the best. As always, if you've got a better idea, I'd love to hear more and you can leave me a comment below. Ok, let's get started.

The First Approach

Let me begin by showing the code that handled the geojson initially:

let geoReq = await fetch('./national-parks.geojson');
let geo = await geoReq.json();
map = L.map('map').setView([37.09024, -95.712891], 3);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
	maxZoom: 19,
	attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);	
L.geoJSON(geo).addTo(map);

Basically, load my geojson from the server, parse it, and after the Leaflet map is initialized, you can add it with the geoJSON method. I freaking love how simple that is.

The first change I made was to bind a popup to the layer:

L.geoJSON(geo).bindPopup(function (layer) {
	return layer.feature.properties.Name;
}).addTo(map);

In this case, the function I wrote there is called every time you click and will return the Name value from the properties. As I said above, only the code and name are available, but we're going to fix that with a quick API call.

From the NPS docs, you can retrieve park information with one quick call:

async function getParkData(code) {
	let r = await fetch(`https://developer.nps.gov/api/v1/parks?parkCode=${code}&api_key=KTGT4KgP0kgO8pO1U1rtHdHHrcubYua2CruhHzpy`);
	return (await r.json()).data[0];
}

The API returns an array of results in the data key and since I know I'll always be getting one, it's trivial to return just the park info. With that function written, I then turned to incorporating it into the popup.

I first tried something like this - and to be clear, I didn't check the docs so I fully didn't expect this to work:

L.geoJSON(geo).bindPopup(async function (layer) {
	let data = await getParkData(layer.feature.properties.Code);
	return layer.feature.properties.Name + `<p>${data.description}</p>`;
}).addTo(map);

On clicking, I got an error, so it was clear that Leaflet expected a synchronous response from the function.

The Second Approach

So, at this point, I actually did check the docs, and nothing really seemed helpful. I googled around and found this on Stackoverflow: Unable to successfully bind an async function to a Leaflet marker popup

The answer shared there shows returning a DOM element in the bindPopup function that gets updated later in code. Here's their solution:

marker.bindPopup(() => {
    const el = document.createElement('div');
    let html = `<h4>${r.title}</h4>`;
    const getData = async (url) => {
        const response = await fetch(url);
        if (response.ok) {
            const json = await response.json();
            html += `<p>${JSON.stringify(json)}</p>`;
            el.innerHTML = html;
        }
    };
    getData(`server/foo?id=${r.id}`);
    return el;
});

That seemed... like a possible solution. So I gave it a shot:

	L.geoJSON(geo).bindPopup(function (layer) {
		const el = document.createElement('div');
		let html = `<h4>${layer.feature.properties.Name}</h4>`
		getParkData(layer.feature.properties.Code).then(r => {
			console.log('got crap back', r);
			html += `<p>${r.description}</p>`;
			html += `<p><img src="${r.images[0].url}" width="250"></p>`;
			el.innerHTML = html;
		});
		return el;
	}, { minWidth: 500 }).addTo(map);
}

Remember that getParkData is the wrapper around the NPS API. I didn't use await on the call as I needed to return the DOM element as shown above and just update it later.

This... actually worked well. I was concerned however about race conditions. What happens if you click one marker and then quickly click another. As far as I can tell... it just works. Either Leaflet 'kills' the DOM element for the first marker so the later code does nothing, or it 'works' but as the popup is hidden, it doesn't impact the current popup.

As I said... I'm rather unsure about this, but it seems to work well. The code may be found here, and you try it yourself here: https://cfjedimaster.github.io/codebr/leaflet/nps.html

I think there's still improvements that could be made here. For one, caching the calls to the API in browser storage for example. The NPS API responds quickly, but I do know last week it was running a bit slower. Some quick sessionStorage caching would really help. Also, the popup shows up small and blank. I could possibly setup the park name initially and show some "loading..." type messages while it works.

That being said... any thoughts? I really think that there's possibly a better way and I'd love to see it if so!

Source: raymondcamden.com

Related stories
1 month ago - In this tutorial, you'll learn how to create and use asynchronous iterators and iterables in Python. You'll explore their syntax and structure and discover how they can be leveraged to handle asynchronous operations more efficiently.
1 month ago - Node.js is a powerful JavaScript runtime environment that lets you run JS code outside the browser. And a fundamental part of many Node.js applications involves reading and writing files – whether that's text, JSON, HTML, or other file...
1 month ago - Authentication is a very important aspect of software development. It is the process of verifying a user’s identity. Authentication ensures that only authorized individuals access specific resources or perform certain actions within a...
2 weeks ago - Explore the new Promise.withResolvers() mechanism to create a JavaScript Promise and cancel it before its completion. The post Mastering promise cancellation in JavaScript appeared first on LogRocket Blog.
1 month ago - Experimenting with subgroups, deprecate setting depth bias for lines and points, hide uncaptured error DevTools warning if preventDefault, WGSL interpolate sampling first and either, and more.
Other stories
10 minutes ago - Learn how to set up an Amazon API Gateway and secure the REST API with Okta Customer Identity Cloud (CIC) Adaptive MFA Risk Score
1 hour ago - This release candidate, a near-final look at Deno 2, includes the addition of Node's process global, better dependency management, and various API stabilizations, and more.
1 hour ago - Published: September 19, 2024 The CSS Working Group has combined the two CSS masonry proposals into one draft specification. The group hopes that...
1 hour ago - Stay organized with collections Save and categorize content based on your preferences. Published: September...
3 hours ago - DNS monitoring tool is a cloud-based scanner that constantly monitors DNS records and servers for anomalies. This tool aims to ensure that users are sent to genuine and intended website pages, instead of fakes or replicas. It alerts users...