You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
leaflet-components/docs/05-special-cases.md

7.9 KiB

05 — Per-component special cases

Where a component does something the WithProps mixin can't express from a prop table alone.

leaflet-map — shadow DOM, CSS, resize

src/elements/leaflet-map.ts.

  • Shadow root built in connectedCallback (not via createLeafletObject): a <div> at 100% × 100% for Leaflet to render into, a <style> that gives :host a block display and a default 400px height, and a <link> to Leaflet's stylesheet.
  • css-url / css-integrity / css-crossorigin are not Leaflet options — they describe that <link>. Their set just calls applyCss(), which re-links the stylesheet and re-derives Icon.Default.imagePath from the same URL directory. applyCss() reads the attributes directly (not the properties) because absent and empty mean different things here. Defaults point at unpkg.com/leaflet@1.9.4 with a matching SRI hash.
  • lat / lng / zoom are positional but not constructor args in the usual sense — the map is positioned by setView() right after construction, and they're written back on moveend / zoomend.
  • ResizeObserver on the host calls map.invalidateSize(). jsdom lacks ResizeObserver, so the test setup stubs it (see 07).
  • As tree root it also listens for leaflet-add-layer / leaflet-remove-layer (used by group internals) alongside leaflet-register.
  • fit-to-markers / fit-padding / fit-max-zoom are not Leaflet options. With fit-to-markers present the map ignores lat/lng/zoom and instead fitBounds()es a box around every layer it can locate — anything with a getLatLng() (markers, circles) or getBounds() (rectangles, image/video overlays); tile layers have neither and are skipped, as are open popups/tooltips (a map layer with a getLatLng()). The only reframe trigger is a component registering — initial page load and any child added later. Panning, zooming, opening a popup and removing a marker all leave the view exactly where it is. #onRegister calls #scheduleFit(), which coalesces the burst of registrations during page load onto one microtask, so it's a single fitBounds call, not one per marker. fit-padding (default 20) is the pixel gutter left around the bounds; fit-max-zoom (default none) caps the zoom, which matters when a single marker would otherwise snap to max zoom. setView() still runs once at construction so the map has a valid view before the first frame.
  • Panning / zooming always writes the live centre and zoom back to the lat / lng / zoom attributes (moveend / zoomend, the standard event: write-back in the prop table) — including the view fitBounds() itself lands on. That write-back is one-way here: it never re-triggers a fit.

leaflet-polygon / leaflet-polyline — vertices from children

src/elements/leaflet-polygon.ts, leaflet-polyline.ts, src/core/vertex-tracker.ts.

Vertices come from <leaflet-line> children, event-driven, never a lookup:

  • <leaflet-line> is a plain HTMLElement (no mixin). On connect and on every lat/lng change it fires leaflet-line-sync on itself, carrying its own [lat, lng]. On disconnect it fires leaflet-line-remove.
  • The polygon/polyline feeds those events into a shared VertexTracker, which keeps an ordered coordinate list. A newly-registered vertex is inserted at its actual document position via compareDocumentPosition — registration order is not assumed to match DOM order.
  • After every sync/remove the component calls obj.setLatLngs(tracker.coords()).

The leaflet-line-remove wrinkle: disconnectedCallback fires after the node is already detached from its parent, so a bubbling dispatch from the node has nowhere to go. <leaflet-line> caches parentNode in connectedCallback and dispatches the remove event from that cached reference instead (emitLineRemove(from, el) in register.ts).

leaflet-popup / leaflet-tooltip — content is markup

src/elements/leaflet-popup.ts, leaflet-tooltip.ts.

  • Content is this.innerHTML, passed as the content option, not an attribute.
  • Both run a MutationObserver (childList + characterData + subtree) and call setContent(this.innerHTML) on any change.
  • A popup/tooltip only carries a position of its own when it's not bound to a parent layer — createLeafletObject sets setLatLng only if both lat and lng attributes are present.
  • attach: 'self' — they register with a parent but adopt no children.

leaflet-layer-group / leaflet-feature-group — passthrough

const PROPS: Record<never, never> = {} — an empty prop table (new LayerGroup([]) / new FeatureGroup([])). Record<never, never>, not Record<string, never>: the latter would put a never string-index signature on the generated instance type that the subclass's own members can't satisfy. No options of their own, but they still get the full lifecycle, child registration (attach: 'children'), and leaflet: event forwarding. A descendant layer's leaflet-register is claimed by the mixin's own #onChildRegister (Layer + parent has addLayerobj.addLayer), stopping there instead of bubbling on to the map. The group itself then registers with its parent, so a group can nest in a group.

leaflet-control-layers — dual role

src/elements/leaflet-control-layers.ts. attach: 'self' (it's a control — registers with the map, no children through the standard path), but it also attaches its own leaflet-register listener to intercept its child layer entries (<… name="OSM" type="base" active>): addBaseLayer when type="base", else addOverlay, keyed by the child's name attribute. A child marked active also gets a leaflet-add-layer event dispatched upward, which <leaflet-map> handles with map.addLayer(layer) so that layer is shown initially. (The map listens for leaflet-add-layer / leaflet-remove-layer for exactly this; leaflet-control-layers is their only dispatcher.)

leaflet-geojson — style nesting

src/elements/leaflet-geojson.ts. Extends pathProps, but GeoJSON takes style options nested under a style key so they apply to each generated feature: new GeoJSON(data, { style: options }). data is positional + json; its set does clearLayers() then addData(value).

leaflet-tile-layer-wms — extensible CRS

src/elements/leaflet-tile-layer-wms.ts.

  • The crs attribute only covers the four CRSes Leaflet ships by name (EPSG3857, EPSG4326, EPSG3395, Simple) via a NAMED_CRS lookup.
  • A CRS Leaflet doesn't ship comes from a nested child element that fires leaflet-crs-changed with a CRS instance — no base class, no registry, just the event shape (see 03). It takes priority over the attribute. crs: null reverts to the attribute's named lookup.
  • crs is constructor-only, so picking up a child CRS means recreateLeafletObject() — a full rebuild of the layer.
  • WMS request params (layers, styles, format, transparent, version) have no individual setters; each prop's set merges through obj.setParams({ [key]: value }).

leaflet-icon / leaflet-div-icon — recreate on change

Icons are created with WithProps(PROPS, { recreate: true }): Leaflet gives no way to mutate an Icon in place, so any attribute change throws the icon away and rebuilds it. They're attach: 'none' — not tree members — and announce themselves to a parent leaflet-marker via icon-changed.

leaflet-marker — live <img> attributes

title and alt end up on the <img> Leaflet renders, so their set reaches obj.getElement() and assigns the DOM property directly. draggable's set toggles obj.dragging.enable()/disable(). Note that getElement() only exists after the marker is added to a real map — tests touching it append through a <leaflet-map>.