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/03-component-tree.md

5.5 KiB

03 — Component tree & registration

Components never read each other's properties and never query the DOM for their relatives. They join the tree by dispatching DOM events that bubble, and a parent claims a child by handling the event. This replaces the imperative parent/child wiring Leaflet normally needs.

The leaflet-register protocol

src/core/register.ts. On connect, unless attach: 'none', a component calls:

registerWithParent(el, leafletObject);
// dispatches CustomEvent('leaflet-register', {
//   detail: { leafletObject, element: el }, bubbles: true, composed: true })

The event bubbles up the DOM. The nearest ancestor component that is in attach: 'children' mode has a leaflet-register listener; its #onChildRegister handler (with-props.ts) inspects detail.leafletObject by instanceof and:

  • Popupobj.bindPopup(child), stopPropagation()
  • Tooltipobj.bindTooltip(child), stopPropagation()
  • Layer and the parent has addLayerobj.addLayer(child), stopPropagation()
  • otherwise: let it keep bubbling

Discrimination is by instanceof, not duck typing — which is why the child-registration tests need real Popup/Tooltip/Layer instances.

If nothing claims it, the event reaches <leaflet-map>, whose #onRegister calls e.detail.leafletObject.addTo(this.map) and stops it. The map is the terminus; nothing can register before it exists, which is why it's exported first (see 06).

composed: true lets the event cross the map's shadow boundary.

Un-registration

When a child disconnects, the mixin's #destroyObject() calls obj.remove(), which detaches it from whatever it was added to. When a parent disconnects, #releaseChildren() walks the entries it adopted and calls unbindPopup / unbindTooltip / removeLayer for each.

recreateLeafletObject() re-runs registerWithParent — that call otherwise happens only once, from connectedCallback — so a recreated layer (e.g. a WMS layer that picked up a child CRS) still gets re-added to its parent.

attach modes

Passed as WithProps(PROPS, { attach }). Default 'children'.

Mode Registers with parent Adopts registering descendants Used by
'children' yes yes (layers, popups, tooltips) every layer type, groups
'self' yes no leaflet-popup, leaflet-tooltip, controls
'none' no no leaflet-map (the root), icons (not tree members)

The other announcement events

The leaflet-register protocol only carries Layer subclasses. Three other relationships use the same bubble-and-claim shape with their own event types, so a plain custom element (no base class) can participate just by firing the right event:

Event Fired by Claimed by Purpose
icon-changed ({ icon: Icon | null }) leaflet-icon, leaflet-div-icon, on connect / disconnect leaflet-marker marker.setIcon(icon ?? new Icon.Default())
leaflet-line-sync ({ element, latlng }) leaflet-line, on connect and every lat/lng change leaflet-polygon, leaflet-polyline feed the VertexTracker
leaflet-line-remove ({ element }) leaflet-line, on disconnect (from its cached parent — see 05) same drop the vertex
leaflet-crs-changed ({ crs: CRS | null }) any element nested in a CRS-accepting component, on connect leaflet-tile-layer-wms supply a CRS Leaflet doesn't ship by name; null reverts to the component's default

null in a payload consistently means "revert to the host component's own default", mirroring icon-changed.

One more internal event rides the same bubble: leaflet-add-layer / leaflet-remove-layer ({ layer }), dispatched only by leaflet-control-layers for an active entry and handled only by leaflet-map (map.addLayer / removeLayer). See 05.

Why events, not lookups

  • No load-order coupling between a parent and the DOM query it would otherwise run (a child may upgrade before or after its parent).
  • A child at any nesting depth works without the parent knowing the shape of the subtree.
  • Third-party elements can nest into the tree without importing anything — just dispatch the documented event.

The one cost is the load-order constraint in 06: a one-shot connect-time announcement is lost if the listening parent isn't defined yet.