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:
Popup→obj.bindPopup(child),stopPropagation()Tooltip→obj.bindTooltip(child),stopPropagation()Layerand the parent hasaddLayer→obj.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.