MapLibre events come back as ordinary Hologram actions.
Handler syntax
Event props accept the same shapes Hologram's own longhand event bindings do:
# Bare action name, dispatched to the page.
on_click={:parcel_clicked}
# Longhand, when the handler lives on a component rather than the page.
on_click={%{action: :parcel_clicked, target: "sidebar"}}
# With static params merged into the payload.
on_click={%{action: :parcel_clicked, target: "sidebar", params: %{layer: "parcels"}}}A keyword list works wherever a map does. An unset handler attaches no MapLibre listener at all, so an event prop can be driven from state:
on_click={if @selectable do :parcel_clicked end}Targets
target is a component cid, "page", or "layout". It defaults to "page".
That default differs from Hologram's own, which is the closest enclosing
stateful component. HoloMap's children are deliberately stateless and have
no cid, so there is nothing to fall back to, and silently targeting the
map container would send the action to HoloMap.Map rather than to your
handler.
If your handler lives on a stateful component, name it:
<HoloMap.Layer.Fill id="parcels" source="parcels"
on_click={%{action: :picked, target: "sidebar"}} />Params
Whatever you supply in params is merged under the runtime payload, so the
data MapLibre reports always wins over a static value with the same key. Keys
arrive in your handler as atoms, as they do for any Hologram action.
Payloads
Map events
on_load, on_click, on_dbl_click, on_context_menu, on_mouse_move,
on_move_start, on_move_end, on_zoom_end, on_rotate_end, on_pitch_end,
on_idle, on_error.
| Key | Type | Notes |
|---|---|---|
center | [lng, lat] | Camera centre after the event |
zoom | number | |
bearing | number | |
pitch | number | |
bounds | [[w, s], [e, n]] | The visible extent |
lng_lat | [lng, lat] | Pointer events only |
point | [x, y] | Pointer events only, in pixels |
def action(:map_moved, params, component) do
put_state(component, :bounds, params.bounds)
endLayer events
on_click, on_dbl_click, on_context_menu, on_mouse_enter,
on_mouse_leave, on_mouse_move, on_mouse_down, on_mouse_up,
on_touch_start, on_touch_end.
| Key | Type | Notes |
|---|---|---|
layer | string | The layer's id |
lng_lat | [lng, lat] | |
point | [x, y] | In pixels |
features | list | The features under the pointer |
Each feature carries id, source, source_layer, properties and
geometry. MapLibre's own feature objects hold back-references to their source
and tile which cannot cross into Elixir, so only the actionable parts are
carried over.
Every key is an atom, all the way down
Hologram converts JavaScript object keys to atoms when an action's params cross the boundary, and it does so recursively. A feature therefore arrives like this:
%{
id: 0,
source: "cities",
source_layer: nil,
properties: %{name: "Santo Domingo", population: 2_908_000},
geometry: %{type: "Point", coordinates: [-69.93, 18.48]}
}Your own GeoJSON property names are part of that: a property written as
"parcel_id" in the data is matched as %{parcel_id: id}. Matching with
string keys silently fails. The clause simply does not match, and a case
with a catch-all falls through without an error.
def action(:parcel_clicked, params, component) do
case params.features do
[%{properties: %{parcel_id: id}} | _rest] -> put_state(component, :selected, id)
[] -> put_state(component, :selected, nil)
end
endfeatures is a list because layers overlap; it is empty for a click on empty
space when the event is bound to the map rather than a layer. Match on it rather
than assuming a head.
Popup events
on_open, on_close. Neither carries a payload.
on_close fires for every close, including the ones MapLibre performs without
asking: the close button, a click on the map (close_on_click is on by
default) and a camera move. Handle it by clearing whatever state opened the
popup. Otherwise the component still believes the popup is open, and clicking
the same feature again re-renders nothing.
def action(:selection_cleared, _params, component) do
put_state(component, :selected, nil)
endMarker events
on_click, on_drag_start, on_drag, on_drag_end.
| Key | Type | Notes |
|---|---|---|
lng_lat | [lng, lat] | For drag events, where the marker has been moved to |
def action(:pin_moved, params, component) do
[lng, lat] = params.lng_lat
put_state(component, :coords, {lng, lat})
endon_drag fires on every pointer move, and writing the position back into state
from it is safe: the value going back is the one MapLibre just reported, so the
setLngLat the reconciler performs is a no-op and the marker stays locked to
the cursor. Prefer on_drag_end when only the final position matters, since there is
no reason to re-render on every frame for a value nothing reads until the drag
ends.
Hover styling
The usual pattern needs feature ids, which GeoJSON features often lack. Set
generate_id on the source, or point promote_id at a property that is already
unique:
<HoloMap.Source.GeoJSON id="parcels" data={@parcels} generate_id />
<HoloMap.Layer.Fill
id="parcels-fill"
source="parcels"
paint={%{fill_opacity: ["case", ["boolean", ["feature-state", "hover"], false], 0.8, 0.35]}}
on_mouse_move={:parcel_hover}
on_mouse_leave={:parcel_unhover}
/>def action(:parcel_hover, params, component) do
case params.features do
[%{id: id} | _rest] ->
clear_hover(component)
HoloMap.API.set_feature_state("explorer", %{source: "parcels", id: id}, %{hover: true})
put_state(component, :hovered, id)
[] ->
component
end
endWhat is not reported back
A camera move the reconciler itself made, because you changed center or
zoom from state, does not fire on_move_end. Only a move the user or the
imperative API caused does.
Without that, writing the camera from state would echo straight back into an action: a feedback loop at best, and at worst an action arriving on a page that has already changed. If you need to know when your own camera assignment settled, you already do: it is the state change that caused it.
Ordering and timing
Hologram.dispatchAction is asynchronous. An event dispatched at the moment a
navigation begins can execute after the new page has mounted, which is why
HoloMap refuses to dispatch from a map whose container has gone stale. Your
handlers do not need to defend against this, but it is worth knowing why an
action can appear to arrive late.