Waiting for a library require to finish executing

The first time I load this page I get an error in the “map” cell:
https://beta.observablehq.com/@simonw/san-francisco-trees-from-datasette

“TypeError: L.markerClusterGroup is not a function”

If I then click on that cell and re-execute it it works fine. This is because the “LMC = require(‘leaflet.markercluster’)” line in an earlier cell has not yet executed. How do I make sure that require has loaded before the map cell gets executed, avoiding the error on first page load?

Unrelated question concerning the above: what would be the best way of setting it up to allow a user to type a search term in an input box and then have the JSON fetch re-execute and redraw the map? I’m basically trying to recreate https://sf-tree-search.now.sh/ in an Observable notebook, as a learning exercise.

I think you can do something like:

L = {
  let L = await require('leaflet@1.2.0')
  await require('leaflet.markercluster')
  return L
}

Not sure if it’s the most idiomatic way though.

1 Like

Sure thing! So for the Leaflet.markerCluster issue - Leaflet plugins have various ways of attaching themselves to Leaflet, and markerCluster does a technique which is essentially ‘expect the L object to be available’. Unfortunately that’s not super self-explanatory, but nonetheless: doable! Here’s how I’d do it:

L = {
  const L = window.L = await require('leaflet@1.2.0');
  await require('leaflet.markercluster');
  return L;
}

So, you require Leaflet and add it to the window where markerCluster expects it, and then include markerCluster, and return the modified Leaflet. This way you can be certain that Leaflet is loaded before markerCluster and that all cells only get the markerCluster-infused Leaflet object.

Edit: @litenjacob has it! Awesome :slight_smile:

1 Like

That worked perfectly, thanks!

1 Like

Oh, and your other question! @mbostock just wrote an excellent notebook More deliberate inputs.

Tried my hand at making that map searchable - here’s the result - there’s definitely room to clean it up a bit, I’ve been considering different approaches to the way Leaflet/Mapbox GL both insist on using offsetWidth & offsetHeight to size themselves, which cuts against the grain of how notebooks work.

The gist there -

  • The map cell returns the map container, but i pin the map object to it
  • Another cell manages the cluster group and is updated by the input -> url string -> refetch flow. That cell uses the this value to refer to its old value, so it can run clearLayers and keep using the same markerGroup instance.

Aha, I see - the magic is this HTML form:

searchForm = html`<form>
  <input value="olive" name=text placeholder="Type here, then click submit.">
  <button type=submit>Submit</button>
</form>`

Combined with this magic to detect when it gets submitted:

searchQuery = Generators.observe(notify => {
  function submitted(event) { notify(searchForm.text.value); event.preventDefault(); }
  searchForm.addEventListener("submit", submitted);
  notify(searchForm.text.value);
  return () => searchForm.removeEventListener("submit", submitted);
})

Would be nice if there was a more obvious way of achieving this, the above feels a bit obscure.

What’s the purpose of this last line?

I decided I didn’t need a submit button, so I switched to the pattern Mike described in the first sentence of that article instead - which gives an autocomplete-style effect which actually works very nicely: https://beta.observablehq.com/@simonw/san-francisco-trees-from-datasette

The function returned to Generators.observe removes the submit event listener when the cell is discarded. You can omit this line if you don’t care about leaking the event listener when the cell is re-evaluated, but it’s a good habit, especially since cells can be re-evaluated many times with reactivity.