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

4.5 KiB

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):

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:

class LeafletCircleElement extends Base {
  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 — which, for the internal bubbling events (leaflet-register, icon-changed, leaflet-line-sync, …), is itself an augmentation living in src/core/globals.ts (npm-only; see 07).

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.