Exemple d’un composant affichant une carte avec un marqueur

Cet exemple présente un composant permettant d’afficher une carte avec éventuellement un marqueur et une info-bulle.

Description du composant

Le composant affiche une carte en utilisant la librairie Leaflet, un marqueur avec une info-bulle peut également y être ajouté.

Le composant est une fonction nommée MapContainer prenant 2 paramètres, le premier contient 4 propriétés :

  1. provider : un objet requis décrivant le fournisseur des fonds de cartes,
  2. center : un object ou un tableau contenant la latitude et la longitude de centrage de la carte à sa création, la valeur par défaut centre la carte sur la France.
  3. zoom : un entier indiquant le zoom de la carte à sa création, la valeur par défaut est 5.
  4. scrollWheelZoom : un booléen pour activer le zoom avec la molette de la sourie, la valeur par défaut est false.

Le deuxième paramètre est optionnel, c’est un objet qui représente un marqueur à afficher éventuellement sur la carte, il a 3 propriétés :

  1. center : un objet ou un tableau (Leaflet support les deux notations) contenant la position (latitude et longitude) du marqueur,
  2. zoom : un entier indiquant le zoom de la carte à utiliser avec le marqueur,
  3. content : le contenu de l’info-bulle du marqueur qui peut être un texte ou un élément HTML.

Si le contenu de l’info-bulle est vide (""), null ou undefined, l’info-bulle n’est pas affichée.

Code du composant

Le code du composant MapContainer est représenté ici.

async function MapContainer({provider, center = [47.366056, 1.762689], zoom = 5, scrollWheelZoom = false}, marker) {
const {div, p} = Room.elements();

let L = null;
let mapRef = null;
let resizerRef = null;
let markerRef = null;

const showMarker = () => {
if (mapRef && marker && marker.center) {
const fly = markerRef !== null;
markerRef && markerRef.remove();
markerRef = L.marker(marker.center).addTo(mapRef);
marker.content && markerRef.bindPopup(marker.content, {maxWidth: 240}).openPopup();
mapRef[fly ? "flyTo" : "setView"](marker.center, marker.zoom || zoom);
}
};

const onMount = element => {
// Création de la carte avec Leaflet
mapRef = L.map(element, {center, zoom, scrollWheelZoom});
provider && L.tileLayer(provider.url, provider.options).addTo(mapRef);
// Création d'un observeur en cas de redimensionnement de l'élément
if ("ResizeObserver" in window) {
resizerRef = new ResizeObserver(() => mapRef.invalidateSize());
resizerRef.observe(element);
}
// Création d'un effet pour créer le marqueur sur la carte
Room.createEffect(showMarker);
};

const onUnmount = () => {
markerRef && markerRef.remove();
resizerRef && resizerRef.disconnect();
mapRef && mapRef.remove();
mapRef = null;
resizerRef = null;
markerRef = null;
};

try {
L = await import("Leaflet");
return div({class: "mapContainer", onMount, onUnmount});
} catch {
return p("Problème de chargement du module.");
}
}

La fonction MapContainer() commence par tenter de charger dynamiquement le module Leaflet qui a été défini dans carte d’importation de la page.

Il est bien sûr possible d’utiliser l’importation standard des modules ECMAScript avec l’instruction import, mais avec l’importation dynamique, ceci permet de ne charger le module que quand le composant est utilisé. C’est une bonne pratique évoquée dans un article de GreenerSoft sur le chargement paresseux.

Si le module Leaflet n’est pas importé correctement, la fonction MapContainer() retourne un élément <p> avec un texte signalant le problème.

Si le module Leaflet est importé correctement, la fonction MapContainer() retourne un élément <div> de classe mapContainer qui va contenir la carte.

La classe mapContainer est là pour permettre de définir au niveau CSS, une largeur et une hauteur à l’élément contenant la carte, sans cela la carte ne s’affiche pas.

Les évènements spéciaux de Room mount et unmount sont attribués à l’élément <div> qui va contenir la carte et donc déclencher la fonction onMount() du composant quand il est monté dans le DOM et la fonction onUnmount() quand il est supprimé du DOM de la page.

La fonction onMount() :

  • utilise les fonctions du module Leaflet (variable L) pour créer la carte (variable mapObject) avec les données passées en paramètre du composant (provider, center, zoom et scrollWheelZoom),
  • fabrique un observateur de redimensionnement (API Resize Observer) pour le cas où les dimensions de l’élément changent,
  • créé un effet avec la fonction createEffect() de Room en utilisant la fonction showMarker().

Inversement, la fonction onUnmount() s’occupe de libérer la mémoire en supprimant les éléments créés par la fonction createMap() (mapRef et resizerRef) ou par l’effet (markerRef).

On voit ici qu’un effet peut très bien être créé après le montage dans le DOM de la page, ce n’est pas un problème pour Room.

Il est à noter qu’il était possible d’appeler directement la fonction showMarker() sans l’utiliser comme un effet, mais l’idée est ici que, si la propriété marker est une donnée observable, alors le marqueur va pouvoir être mis à jour si la donnée change. Cette possibilité va être utilisée dans l’exemple de géolocalisation d’une adresse IP.

Utilisation du composant

Pour utiliser le composant MapContainer, nous supposons que nous avons un élément dans la page HTML avec un identifiant mapContainer et nous utilisons de 2 fonctions utilitaires.

La première fonction utilitaire est la fonction importCSS() qui permet de charger dynamiquement un fichier CSS. La CSS de Leaflet étant requise pour que les cartes s’affichent correctement, nous allons utiliser cette fonction pour le faire.

Le code de la fonction importCSS() est le suivant :

function importCSS(url) {
if (!Array.from(document.querySelectorAll("link[rel='stylesheet']")).find(l => l.getAttribute("href") == url)) {
document.head.append(Room.elements().link({
href: url,
rel: "stylesheet"
}));
}
}

Il était bien sûr aussi possible de simplement avoir un élément <link> directement dans l’élément <head> de la page, mais on évite ainsi le chargement de la CSS si le composant n’est pas utilisé.

La deuxième fonction utilitaire est la fonction openStreetMapProvider() qui retourne un objet qui va être donné comme propriété provider au composant MapContainer. Cet objet va permettre au module Leaflet d’utiliser les fonds de carte d’OpenStreetMap.

Le code de la fonction openStreetMapProvider() est le suivant :

function openStreetMapProvider() {
return {
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
options: {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}
};
}

Avec ces deux fonctions utilitaires, le code pour utiliser le composant MapContainer peut être par exemple :

// Importation de la CSS de Leaflet via un CDN
importCSS("https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.css");
// Création d'un marqueur
const marker = {
center: {lat: 48.856578, lng: 2.351828},
zoom: 11,
content: Room.elements().a({
href: "https://fr.wikipedia.org/wiki/Paris",
target: "_blank",
rel: "noopener"
}, "Paris")
}
// Récupération d'un élément où mettre le composant
const mapContainer = document.getElementById("mapContainer");
if (mapContainer) {
// Récupération du provider
const provider = openStreetMapProvider();
// Ajout du composant dans l'élément
mapContainer.append(await MapContainer({provider}, marker));
}

Ce qui donne le résultat suivant :

Normalement, la carte est centrée sur Paris avec un marqueur et une info-bulle contenant un lien vers la page Wikipédia de Paris.

Dernière mise à jour :