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

95 lines
5.5 KiB
Markdown

# 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 `<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](./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.