# 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: ```ts 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()` - `Layer` **and** the parent has `addLayer` → `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 ``, 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](./06-load-order.md)). `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](./05-special-cases.md)) | 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](./05-special-cases.md). ## 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](./06-load-order.md): a one-shot connect-time announcement is lost if the listening parent isn't defined yet.