Im Blog-Artikel Einstieg in Leaflet wurde beschrieben, wie man mit dem Leaflet-API eine Landkarte in seine Seite einbinden kann. Darauf basierend wird in diesem Artikel gezeigt, wie man Custom Elements für eine Landkarte mit Markern erstellen kann.
Im Selfwiki wird das Grundgerüst für die Definition von Custom Elements vorgestellt. Da beim Entfernen des Elements nichts Weiteres passieren soll, und der Fall „Bewegen in ein anderes Dokument“ nicht vorgesehen ist, werden die Methoden disconnectedCallback und adoptedCallback weggelassen.
Das Custom Element für die Karte soll <osm-map> sein und die Eckkoordinaten der Karte sollen als Attribute übergeben werden. Die Definition sieht daher so aus:
<osm-map topleft="52.65, 13.2" bottomright="52.35, 13.6"></osm-map>
class osmMap extends HTMLElement {
…
}
customElements.define('osm-map', osmMap);
Im Getter observedAttributes wird festgelegt, dass auf die Änderung der Attribute topleft und bottomright reagiert werden soll:
static get observedAttributes() {
return ['topleft', 'bottomright'];
}
Im Konstruktor muss als erstes die Methode „super“ aufgerufen werden, die den Konstruktor der Elternklasse aufruft:
constructor() {
super();
Als nächstes wird dann das Shadow Dom angelegt:
const shadow = this.attachShadow({mode: closed});
Mode wurde auf „closed“ gesetzt, da von außen nicht per Javascript auf das Shadow Dom zugegriffen werden muss. Für die Landkarte benötigt das Leaflet-API ein DIV, das ins Shadow Dom eingehängt wird:
this.mapcanvas = document.createElement('div');
this.mapcanvas.className = "mapcanvas";
shadow.appendChild(this.mapcanvas);
Dann wird noch ein Stylesheet erstellt, in dem das Leaflet-CSS importiert wird. So ist dieses CSS Teil des Shadow Doms.
const style1 = document.createElement('style');
style1.textContent = `@import url('${LeafletBasePath}leaflet.css')`;
shadow.appendChild(style1);
Custom-Elemente sind Inline-Elemente. Um der Karte per css eine Größe geben zu können, erhält das osm-map-Element die Displayeigenschaft block, und das div für die Karte die Größe 100%.
const style2 = document.createElement('style');
style2.textContent = `
:host { display: block; }
.mapcanvas { width: 100%; height: 100%; }
`;
shadow.appendChild(style2);
}
Die Methode connectedCallback wird aufgerufen, wenn das Element ins DOM eingehängt wird. Leaflet orientiert sich beim Erstellen der Karte an den Maßen des Karten-Divs. Da Safari (Stand Dez. 2020) die Style-Regeln verzögert umsetzt, werden hier die kritischen Regeln noch einmal per Javascript gesetzt und dann die Methode makeMap aufgerufen, die die Karte erstellt:
connectedCallback() {
this.style.display = "block";
this.mapcanvas.style.height = "100%";
this.mapcanvas.style.width = "100%";
this.makeMap();
}
Die Methode attributeChangedCallback wird bei Attributänderung aufgerufen. Da diese Methode vor der Methode connectedCallback aufgerufen werden kann, muss geprüft werden, ob die Methode makeMap schon gelaufen ist und das Kartenobjekt angelegt hat:
attributeChangedCallback(name, oldValue, newValue) {
if(this.map) {
let topleft, bottomright;
topleft = this.getAttribute("topleft").split(",").map(Number);
bottomright = this.getAttribute("bottomright").split(",").map(Number);
const bounds = [ topleft, bottomright ];
this.map.fitBounds( bounds );
}
}
Die Methode makeMap liest die Attribute topleft und bottomright erstellt die Karte.
makeMap() {
let topleft, bottomright;
topleft = this.getAttribute("topleft").split(",").map(Number);
bottomright = this.getAttribute("bottomright").split(",").map(Number);
const bounds = [ topleft, bottomright ];
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: 'Map data © <a href="https://www.openstreetmap.org/" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> and contributors <a href="https://creativecommons.org/licenses/by-sa/2.0/" target="_blank" rel="noopener noreferrer">CC-BY-SA</a>'
});
this.map = L.map(this.mapcanvas, { layers: osm, tap: false } ) ;
L.control.scale({imperial:false}).addTo(this.map);
this.map.fitBounds( bounds );
this.map.on("resize", function(e){
this.map.fitBounds( bounds );
});
}
Auf der Karte sollen auch noch Orte mit einem Marker gekennzeichnet werden können. Das Custom Element sieht so aus:
<osm-marker latlon="52.363860434566206,13.489083283593702"
title="Flughafen BER"
popup="Endlich fertig.<br>Hat ja kaum noch einer dran geglaubt."></osm-marker>
Der Marker hat die Attribute latlon für die Koordinaten, title wird bei hover angezeigt und popup als Inhalt für ein Popup-Fenster, das sich bei Klick auf den Marker öffnet. Die letzten beiden sind optional.
Die Definition von osm-marker ist analog zu osm-map aufgebaut. Im Konstruktor wird nur der Basispfad für die Markergrafik angegeben. In connectedCallback wird geprüft, ob das Elternelement des Markers ein osm-map-Element ist und ob die Karte schon angelegt wurde. Wenn beides gegeben ist, wird der Marker erstellt. In attributeChangedCallback wird, wenn der Marker schon angelegt ist, auf das Setzen oder Ändern der jeweiligen Attribute reagiert.
Für die Erklärung zu den Methoden makeMap und makeMarker verweise ich auf Einstieg in Leaflet.
Da CSS, Script und Karten-Bilder von Fremdanbietern geladen werden, wird aus Datenschutzgründen gefragt, ob das OK ist. Daher wird das Leaflet-Script erts nach der Zustimmung mit einer Hilfsfunktion geladen, und die Definition der Elemente erfolgt im Callback der Hilfsfunktion. Das Leaflet-CSS wird dann erst in der Element-Definition importiert.
Das komplette Script sieht jetzt so aus (Live Beispiel):
const LeafletBasePath = "https://unpkg.com/leaflet@1.7.1/dist/";
loadScript(`${LeafletBasePath}leaflet.js`, function() {
class osmMap extends HTMLElement {
static get observedAttributes() {
return ['topleft', 'bottomright'];
}
constructor() {
super();
const shadow = this.attachShadow({mode: 'closed'});
this.mapcanvas = document.createElement('div');
this.mapcanvas.className = "mapcanvas";
shadow.appendChild(this.mapcanvas);
const style1 = document.createElement('style');
style1.textContent = `@import url('${LeafletBasePath}leaflet.css')`;
shadow.appendChild(style1);
const style2 = document.createElement('style');
style2.textContent = `
:host { display: block; }
.mapcanvas { width: 100%; height: 100%; }
`;
shadow.appendChild(style2);
}
connectedCallback() {
this.style.display = "block";
this.mapcanvas.style.height = "100%";
this.mapcanvas.style.width = "100%";
this.makeMap();
}
attributeChangedCallback(name, oldValue, newValue) {
if(this.map) {
let topleft, bottomright;
topleft = this.getAttribute("topleft").split(",").map(Number);
bottomright = this.getAttribute("bottomright").split(",").map(Number);
const bounds = [ topleft, bottomright ];
this.map.fitBounds( bounds );
}
}
makeMap() {
let topleft, bottomright;
topleft = this.getAttribute("topleft").split(",").map(Number);
bottomright = this.getAttribute("bottomright").split(",").map(Number);
const bounds = [ topleft, bottomright ];
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: 'Map data © <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a> and contributors <a href="https://creativecommons.org/licenses/by-sa/2.0/" target="_blank">CC-BY-SA</a>'
});
const map = L.map(this.mapcanvas, { layers: osm, tap: false } ) ;
this.map = map;
L.control.scale({imperial:false}).addTo(map);
map.fitBounds( bounds );
map.on("resize", function(e){
map.fitBounds( bounds );
});
}
}
customElements.define('osm-map', osmMap);
class osmMarker extends HTMLElement {
static get observedAttributes() {
return ['latlon', 'title', 'popup'];
}
constructor() {
super();
L.Icon.Default.prototype.options.imagePath = `${LeafletBasePath}images/`;
}
connectedCallback() {
if(!this.parentNode || this.parentNode.nodeName.toLowerCase() != "osm-map" ) {
console.error("CB: Kein osm-map-Element als Elternelement gefunden.");
return;
}
this.map = this.parentNode.map;
if(!this.map) {
console.error("CB: Kein Elternelement mit Karte gefunden.");
return;
}
this.makeMarker();
}
attributeChangedCallback(name, oldValue, newValue) {
if(this.marker) {
switch(name) {
case "latlon":
const latlon = newValue.split(",").map(Number);
this.marker.setLatLng(latlon);
break;
case "title":
this.marker.options.title = newValue;
break;
case "popup":
const title = this.hasAttribute("title")?this.getAttribute("title"):"";
if(newValue) this.marker.bindPopup("<h3>"+title+"</h3>"+newValue);
break;
}
}
}
makeMarker() {
if(this.map) {
let latlon, title="", popup=null;
if(this.hasAttribute("latlon")) latlon = this.getAttribute("latlon").split(",").map(Number);
else return;
if(this.hasAttribute("title")) title = this.getAttribute("title");
if(this.hasAttribute("popup")) popup = this.getAttribute("popup");
this.marker = L.marker(latlon,{title:title}).addTo(this.map);
if(popup) this.marker.bindPopup("<h3>"+title+"</h3>"+popup);
}
}
}
customElements.define('osm-marker', osmMarker);
});
Live Beispiel
Um zu testen, ob die Custom-Elemente auch dynamisch geladen und modifiziert werden können, gibt es in der Testseite noch einen Eventhandler für das load-Event, der diese Tests durchführt.