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 }<ullr-map>, <ullr-trail-list> and <ullr-widget>.ullr-lift-selecteddetail: { liftId: string, senderId: string }<ullr-map>, <ullr-lift-list> and <ullr-widget>.ullr-feature-selecteddetail: { featureId: string, senderId: string }<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.
enableEventsTrailSelectionbooleanOptionalullr-trail-selected.falseenableEventsLiftSelectionbooleanOptionalullr-lift-selected.falseenableEventsFeatureSelectionbooleanOptionalullr-feature-selected.falseSet 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-trailalongside the standardullr-trail-selected(and likewise forfeatureandlift). The payload is the bare id string. - It accepts
ullr-external-select-trailas 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
enableEventsTrailSelectionlater 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.