# 04 — Events ## Forwarding: every Leaflet event → `leaflet:` `#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 `` 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 LeafletCircle extends WithProps(PROPS) { declare addEventListener: LeafletAddEventListener; declare removeEventListener: LeafletRemoveEventListener; } ``` 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.