Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Svelte with leaflet

Tags:

leaflet

svelte

I'm trying to find my way into Svelte combined with leaflet. Where I'm stuck is how to correctly split the leaflet components into files. For learning, I'm trying to build the official official leaflet quickstart with svelte.

This is how my app.svelte looks like:

<script>
  import L from 'leaflet';
  import { onMount } from "svelte";
  import { Circle } from "./components/Circle.svelte";

  let map;

  onMount(async () => {
    map = L.map("map");

    L.tileLayer("https://a.tile.openstreetmap.org/{z}/{x}/{y}.png ", {
      attribution:
        'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
      maxZoom: 18,
        tileSize: 512,
        zoomOffset: -1
    }).addTo(map);

    map.setView([51.505, -0.09], 13);
    Circle.addTo(map);

  });
</script>

<style>
    html,body {
        padding: 0;
        margin: 0;
    }
    html, body, #map {
        height: 100%;
        width: 100vw;
    }
</style>

<svelte:head>
    <link
    rel="stylesheet"
    href="https://unpkg.com/[email protected]/dist/leaflet.css"
    integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
    crossorigin="" />
</svelte:head>

<div id="map" />

and my circle component:

<script context="module">
    import L from 'leaflet';
    export let map_obj;

    export let Circle = L.circle([51.508, -0.11], {
        color: "red",
        fillColor: '#f03',
        fillOpacity: 0.5,
        radius: 500
    });
</script>

While this is working I do not think it's effective to consider every component and add it to the map with Circle.addTo(map);. How could I pass in the map object to the circle component or is there some better pattern to build the map with several components?

Note: I do know of svelte/leaflet but like to start from scratch for learning.

like image 848
toni Avatar asked Sep 05 '25 03:09

toni


1 Answers

This seemingly easy task is complicated due to the not-really-straightforward lifecycle of frameworks like Svelte, and the really-straightforward let-me-do-DOM-stuff architecture of Leaflet.

There are several approaches to this. I'll describe one, based on nesting Svelte components for Leaflet layers inside a Svelte component for a Leaflet map, and using setContext and getContext to handle the Leaflet L.Map instance around. (I'm borrowing this technique from https://github.com/beyonk-adventures/svelte-mapbox )

So a Svelte component for a L.Marker would look like:

<script>
    import L from 'leaflet';
    import { getContext } from "svelte";

    export let lat = 0;
    export let lng = 0;

    let map = getContext('leafletMapInstance');

    L.marker([lat, lng]).addTo(map);
</script>

Easy enough - get the L.Map instance from the Svelte context via getContext, instantiate the L.Marker, add it. This means that there must be a Svelte component for the map setting the context, which will need the components for the markers slotted in, i.e.

<script>
    import LeafletMap from './LeafletMap.svelte'
    import LeafletMarker from './LeafletMarker.svelte'
</script>

<LeafletMap>
    <LeafletMarker lat=40 lng=-3></LeafletMarker>
    <LeafletMarker lat=60 lng=10></LeafletMarker>
</LeafletMap>

...and then the Svelte component for the Leaflet map will create the L.Map instance, set it as the context, and be done, right? Not so fast. This is where things get weird.

Because of how Svelte lifecycle works, children components will get "rendered" before parent components, but the parent component needs a DOM element to create the L.Map instance (i.e. the map container). So this could get delayed until the onRender Svelte lifecycle callback, but that would happen after the slotted children get instantiated and their onRender lifecycle callbacks are called. So waiting for Svelte to instantiate a DOM element to contain the map and then instantiate the L.Map and then pass that instance to the context and then getting the context in the marker elements can be quite a nightmare.

So instead, an approach to this is to create a detached DOM element, instantiate a L.Map there, i.e. ...

let map = L.map(L.DomUtil.create('div')

...set it in the context, i.e. ...

import { setContext } from "svelte";
setContext('leafletMapInstance', map);

...this will allow Leaflet layers instantiated by the slotted components to be added to a detached (and thus invisible) map. And once all the lifecycle stuff lets the Svelte component for the L.Map have an actual DOM element attached to the DOM, attach the map container to it, i.e. have this in the HTML section of the Svelte component...

<div class='map' bind:this={mapContainer}>

...and once it's actually attached to the DOM, attach the map container to it and set its size, i.e. ...

let mapContainer;
onMount(function() {
    mapContainer.appendChild(map.getContainer());
    map.getContainer().style.width = '100%';
    map.getContainer().style.height = '100%';
    map.invalidateSize();
});

So the entire Svelte component for this Leaflet L.Map would look more or less like...

<script>
  import L from "leaflet";
  import { setContext, onMount } from "svelte";

  let mapContainer;
  let map = L.map(L.DomUtil.create("div"), {
    center: [0, 0],
    zoom: 0,
  });
  setContext("leafletMapInstance", map);
  console.log("map", map);

  L.tileLayer("https://a.tile.openstreetmap.org/{z}/{x}/{y}.png ", {
    attribution:
      'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
  }).addTo(map);

  onMount(() => {
    mapContainer.appendChild(map.getContainer());
    map.getContainer().style.width = "100%";
    map.getContainer().style.height = "100%";
    map.invalidateSize();
  });
</script>
<svelte:head>
  <link
    rel="stylesheet"
    href="https://unpkg.com/[email protected]/dist/leaflet.css"
    integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
    crossorigin=""
  />
</svelte:head>
<style>
  .map {
    height: 100vh;
    width: 100vw;
  }
</style>
<div class="map" bind:this="{mapContainer}">
  <slot></slot>
</div>

See a working example here.

As a side note, I'll say that one should think it twice before sandwiching Leaflet in another JS framework, and think twice about the architecture for this (slotted components seem the cleanest and most extensible, but maybe a big data structure and some imperative programming for the Leaflet bits would be simpler). Sometimes, making sense of the lifecycle implications of more than one framework working at once can be very confusing, and very time-consuming when bugs appear.

like image 184
IvanSanchez Avatar answered Sep 07 '25 21:09

IvanSanchez