Skip to Content
EmbedsSyncing with events

Syncing embeds with events

Put a map beside a trail list and, by default, they ignore each other. Turn on selection events and they become one interface: tap a trail in the list and the map flies to it; tap it on the map and the list scrolls to that row and highlights it.

Nothing needs wiring between them. Each element broadcasts on window, and every other element on the page hears it.

How it works

Each embed lives in its own shadow DOM, so ordinary DOM events would be trapped inside it. Instead the elements dispatch a CustomEvent on window, with bubbles and composed set so it crosses the shadow boundary. Any element, any script on the page, can listen.

window.dispatchEvent(new CustomEvent('ullr-trail-selected', { detail: { trailId: 'a1b2c3d4-…', senderId: 'ullr-trail-list-1730500000000-k3f9x2p1', }, bubbles: true, composed: true, }));

Every element instance generates its own senderId when it is created, and ignores any event carrying its own id. Because an element broadcasts on window, it also hears its own broadcast; the guard is what stops it from reacting to the click it just handled — re-centring the camera on the trail you tapped, or collapsing a multi-selection you were building.

The events

Three events, one per kind of thing you can select. The same name is used in both directions: an element emits ullr-trail-selected and listens for ullr-trail-selected.

ullr-trail-selecteddetail: { trailId: string, senderId: string }
Emitted and heard by <ullr-map>, <ullr-trail-list> and <ullr-widget>.
ullr-lift-selecteddetail: { liftId: string, senderId: string }
Emitted and heard by <ullr-map>, <ullr-lift-list> and <ullr-widget>.
ullr-feature-selecteddetail: { featureId: string, senderId: string }
Emitted and heard by <ullr-map> and <ullr-widget>. No list element renders terrain park features.

The ids are the same ones the Public API returns, so a selection you receive can be looked up directly.

Turning it on

Events are off by default. Each one is gated by its own attribute, and the attribute controls both directions at once — you cannot emit without also listening.

enableEventsTrailSelectionbooleanOptional
Emit and listen for ullr-trail-selected.
Default: false
enableEventsLiftSelectionbooleanOptional
Emit and listen for ullr-lift-selected.
Default: false
enableEventsFeatureSelectionbooleanOptional
Emit and listen for ullr-feature-selected.
Default: false

Set the attribute on every element that should take part. A map with enableEventsTrailSelection="true" next to a trail list without it is a one-way street — and not the direction you would guess, since the list is then deaf to the map and silent when tapped.

Attribute names are camelCase, not kebab-case. enableEventsTrailSelection="true" works; enable-events-trail-selection="true" is silently ignored and you get no events and no error. This applies to every Ullr embed attribute, but it bites hardest here, where the failure looks like “events are broken”.

Example: a map and a trail list

The common case. Tap a row, the map flies to that trail; tap the trail on the map, the list scrolls to it.

<script type="module" src="https://widget.ullr.ski/web-components/ullr-map.js"></script> <script type="module" src="https://widget.ullr.ski/web-components/ullr-trail-list.js"></script> <ullr-map apiKey="1a2b3c4d5e6f.yourPublishableKey" areaId="your-area-id" sport="SNOW" showTrails="true" showLifts="true" enableEventsTrailSelection="true" enableCooperativeGestures="true" style="display:block;position:relative;height:500px" ></ullr-map> <ullr-trail-list apiKey="1a2b3c4d5e6f.yourPublishableKey" areaId="your-area-id" sport="SNOW" visibleColumns="status,name,surface,length" enableEventsTrailSelection="true" style="display:block;height:400px" ></ullr-trail-list>

That is the whole integration — one attribute on each element. No script.

Example: a map with both lists

Same idea, with trails and lifts kept in sync independently. A trail selection never disturbs the lift list, because they travel on different event names.

<script type="module" src="https://widget.ullr.ski/web-components/ullr-map.js"></script> <script type="module" src="https://widget.ullr.ski/web-components/ullr-trail-list.js"></script> <script type="module" src="https://widget.ullr.ski/web-components/ullr-lift-list.js"></script> <ullr-map apiKey="1a2b3c4d5e6f.yourPublishableKey" areaId="your-area-id" showTrails="true" showLifts="true" enableEventsTrailSelection="true" enableEventsLiftSelection="true" style="display:block;position:relative;height:500px" ></ullr-map> <ullr-trail-list apiKey="1a2b3c4d5e6f.yourPublishableKey" areaId="your-area-id" enableEventsTrailSelection="true" style="display:block;height:350px" ></ullr-trail-list> <ullr-lift-list apiKey="1a2b3c4d5e6f.yourPublishableKey" areaId="your-area-id" enableEventsLiftSelection="true" style="display:block;height:350px" ></ullr-lift-list>

Driving selection from your own code

The elements do not care where an event came from. Dispatch one yourself and every listening embed responds — useful for your own search box, a deep link, or a “show me this trail” button elsewhere on the page.

Use a senderId that is not any element’s id. Any string of your own will do.

function selectTrail(trailId) { window.dispatchEvent(new CustomEvent('ullr-trail-selected', { detail: { trailId, senderId: 'my-site' }, bubbles: true, composed: true, })); } // e.g. open the page at /conditions#trail=<id> and select it on load const trailId = new URLSearchParams(location.hash.slice(1)).get('trail'); if (trailId) { selectTrail(trailId); }

Listening works the same way, which is the easy path for analytics:

window.addEventListener('ullr-trail-selected', (event) => { const { trailId, senderId } = event.detail; analytics.track('Trail viewed', { trailId, source: senderId }); });

Dispatch after the elements have loaded and fetched their data. An event for a trail the map has not loaded yet is dropped — the map keeps the selection but has no geometry to fly to. Waiting for window.load, or a short delay, is usually enough.

The conditions widget’s older event names

The conditions widget takes part in everything above. It also shipped first, with a vocabulary of its own, which is still supported so pages written against it keep working:

  • It emits ullr-select-trail alongside the standard ullr-trail-selected (and likewise for feature and lift). The payload is the bare id string.
  • It accepts ullr-external-select-trail as a way to drive selection, and translates it into a standard event, so dispatching one still moves every other embed on the page.

Use the standard events for anything new. The old names are widget-specific — the map and the list elements have never spoken them.

A page rarely needs the widget and the smaller embeds, since the widget already contains a map and its own lists. The combination works if you want it — a widget beside a trail list, say — but check you are not paying for the same data twice.

Notes and limits

  • Selection received through an event is always single-select, even on a map with enableMultiSelect="true". An incoming event replaces the selection rather than adding to it.
  • Attributes are read once, when the element is inserted into the page. Setting enableEventsTrailSelection later with JavaScript has no effect — render the element with the attribute already on it.
  • Nothing is scoped to an area. Every listening element on the page reacts, including a second embed pointing at a different areaId. If you show two areas on one page, leave events off or give each its own page.
  • Unknown ids are harmless. An id an element does not recognise is stored as the selection but changes nothing visible.
Last updated on