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/04-events.md

88 lines
4.3 KiB
Markdown

# 04 — Events
## Forwarding: every Leaflet event → `leaflet:<type>`
`#forwardEvents()` in `src/core/with-props.ts`. Leaflet has no wildcard
listener, so the mixin wraps the instance's own `fire()` method (safe — it's
an object we created and hold alone):
```ts
target.fire = (type, data, propagate) => {
const result = originalFire.call(obj, type, data, propagate);
const sourceTarget = data?.sourceTarget ?? obj;
const detail = { ...data, type, target: obj, sourceTarget };
this.dispatchEvent(new CustomEvent(`leaflet:${type}`, { detail }));
return result;
};
```
Properties of this:
- **Generic** — no per-component or per-event-type registration. Any event
Leaflet fires on the object is re-emitted.
- **`detail` mirrors Leaflet's own `Evented#fire` merge** — original event
data plus `type` / `target` / `sourceTarget` — so a `leaflet:click`
listener's `event.detail` is exactly what a real `map.on('click', …)`
handler would receive. This also keeps Leaflet's typed event interfaces
(`LeafletMouseEvent`, etc.) honest as the `detail` type.
- **Dispatched _after_ Leaflet's own handlers** run, so any attributes the
two-way sync updated in response to the same event are already current when
the DOM event fires.
- **Not bubbling.** Leaflet already propagates layer events up to the map
internally, so a bubbling DOM event would make `<leaflet-map>` see each one
twice.
## Typing: `event-types.ts`
`src/core/event-types.ts` holds small reusable **event-name → payload-type**
fragments, mirroring how `shared-props.ts` shares prop fragments:
```
MouseEvents click/dblclick/mousedown/… → LeafletMouseEvent
MoveEvents movestart/move/moveend → LeafletEvent
DragEvents dragstart/drag → LeafletEvent, dragend → DragEndEvent
PopupBindEvents popupopen/popupclose → PopupEvent
TileEvents loading/load/tileload/tileerror/…
LayerGroupEvents layeradd/layerremove → LayerEvent
```
composed into one type per family:
| Family type | = | Applied by |
| ----------------------- | ---------------------------------------------------------------------- | ------------------------------------ |
| `BaseLayerEvents` | `LayerAddRemoveEvents & PopupBindEvents & TooltipBindEvents` | (building block) |
| `PathEvents` | `BaseLayerEvents & MouseEvents` | circle, polygon, polyline, rectangle |
| `MarkerEvents` | `BaseLayerEvents & MouseEvents & MoveEvents & DragEvents` | marker |
| `TileLayerEvents` | `BaseLayerEvents & TileEvents` | tile-layer, tile-layer-wms |
| `DivOverlayLayerEvents` | `LayerAddRemoveEvents & MouseEvents & DivOverlayEvents` | popup, tooltip |
| `GroupEvents` | `LayerAddRemoveEvents & LayerGroupEvents` | layer-group, feature-group, geojson |
| `MapEvents` | move + mouse + popup/tooltip + zoom + resize + location + keyboard + … | map |
A component narrows its listener types with a `declare` field — the same
zero-runtime idiom as `declare readonly leafletObject?: Marker`:
```ts
class LeafletCircleElement extends WithProps(PROPS) {
declare addEventListener: LeafletAddEventListener<PathEvents>;
declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
}
```
Components that fire nothing meaningful (controls, icons, `leaflet-line`)
skip this and keep the default `HTMLElementEventMap` typing.
### Why no string fallback overload
`LeafletAddEventListener` / `LeafletRemoveEventListener` (defined in
`with-props.ts`) deliberately have **no** generic `(type: string, …)`
overload, unlike the real DOM API. A fallback would silently accept any
misspelled `leaflet:*` name, which defeats the purpose of typing this. The
cost: a genuinely dynamic (non-literal) event-name string needs a cast.
### Keep `detail` honest
If you change what a component fires, keep `#forwardEvents`'s merge shape in
mind — don't type an event's `detail` against a Leaflet interface it wouldn't
actually match at runtime.