Skip to Content
Events

Events

<mapvx-website> reports what the visitor does through two DOM events dispatched on the element itself: userAction, which covers every interaction the component reports, and cardSelected, a legacy event for card clicks. Use userAction in new integrations.

<mapvx-website api-key="YOUR_API_KEY" institution-id="YOUR_INSTITUTION_ID"></mapvx-website> <script> const el = document.querySelector('mapvx-website') el.addEventListener('userAction', (event) => { const { type, data } = event.detail console.log(type, data) }) </script>

The userAction event

Every reportable interaction is dispatched as a userAction event. Its detail is a UserAction object, a union discriminated by type:

type FilterSelection = { filter: string; filters: string[] } type UserAction = | { type: 'select-filter'; data: FilterSelection } | { type: 'show-place'; data: { placeId: string; alias: string | undefined } } | { type: 'select-destination'; data: { destinationId: string; alias: string | undefined } } | { type: 'search'; data: { searchTerm: string } } | { type: 'return-to-home'; data: FilterSelection }

The package exports this type, plus one interface per member (SelectFilterAction, ShowPlaceAction, SelectDestinationAction, SearchAction, ReturnToHomeAction). They exist only as types, so import them with import type (see Types only). The declarations are listed in TypeScript types.

import type { UserAction } from '@mapvx/web-components-ssr'

placeId and destinationId are the place’s MapVX id (mapvxId). alias is the place’s client id (clientId): its alias in MapVX data, or its external id when it has no alias, and undefined when it has neither. The tenant and destination deep-link parameters accept either value.

Action types

typedataEmitted when
select-filter{ filter, filters }The visitor turns a category on or off.
show-place{ placeId, alias }A place profile is displayed, however it was opened.
select-destination{ destinationId, alias }The visitor clicks Pedestrian-friendly directions on a profile.
search{ searchTerm }A search runs with a new term.
return-to-home{ filter, filters }The component goes back to its home screen from a profile or from search.

filters is the whole category selection, which can hold several names or none. filter is the first of them, or 'All' when none are on; it predates multi-select and is kept so links that write it into ?tab= keep working. Prefer filters in new code.

categoriesChange

A separate event from userAction, dispatched whenever the set of categories the filter offers changes. Its detail is a CategoryFiltersData[]:

interface CategoryFiltersData { name: string placeData: string[] // mapvxId of every place in the category }

Use it to draw your own category control. The list is the resolved one, not the institution’s raw declaration: categories whose places this API key never returns are dropped, and the All entry is added. Reproducing those rules yourself would drift from them.

const map = document.querySelector('mapvx-website') map.addEventListener('categoriesChange', (event) => { renderCheckboxes(event.detail) // [{ name, placeData }, …] }) map.addEventListener('userAction', (event) => { if (event.detail.type === 'select-filter') { syncCheckboxes(event.detail.data.filters) } }) // Write the visitor's choice back in. onCheckboxToggle((names) => { map.setAttribute('selected-categories', names.join(',')) })

Pair it with selected-categories to drive the filter, and show-category-filters="false" to hide the built-in row.

These five types are the complete list. There are no events for choosing a city, switching between Grid View and Map View, changing page, picking a route origin or destination on the route screen, starting or stepping through a route, clicking Driving directions or a website link, or using the map controls. There is also no event for loading, readiness or errors.

select-filter

  • Emitted when the visitor turns a category on or off, including turning the last one off.
  • filters is the whole selection, in the order the categories were turned on. It is empty when none are on, which means no category filter.
  • filter is the first selected name, or All when none are on. It predates multi-select and is kept so hosts writing it into ?tab= keep working.
  • Not emitted when the selection is applied automatically, by the tab query parameter or by the selected-categories attribute.

show-place

Emitted every time the profile screen resolves the place to display:

  • a card click (grid, map-view list or search results);
  • a map marker click, in Map View (home screen or search results) or on a profile’s map;
  • a tenant deep link;
  • the Back button of the route screen, which returns to the profile of the place the route screen was opened for.

It can fire more than once for the same place, because the profile resolves the place again whenever the component’s place list changes, for example while data is still loading. If you count views, treat consecutive show-place events with the same placeId as one. When the place is not in the loaded list, the component requests it from the API first, so the event can arrive after a network round trip.

Opening a profile also smoothly scrolls the host page to the top of the first <mapvx-website> in the document (see The page scrolls when a place opens).

select-destination

Emitted when the visitor clicks Pedestrian-friendly directions on a profile, right before the route screen opens. destinationId and alias identify the profile’s place. The Driving directions link (it opens Google Maps in a new tab) and a destination deep link emit nothing.

Emitted by the search results view once the visitor has stopped typing for 500 ms, with the trimmed term, and only when that term is not empty and differs from the last term that view reported. It is not emitted on every keystroke. It also fires:

  • for a search deep link;
  • when the visitor comes back from a profile that was opened from search results: after the profile’s return-to-home, the component shows those results again and emits the remembered term again.

The clear (×) button of the search box emits return-to-home, not search. Deleting the text by hand emits nothing, and the search results stay open.

return-to-home

Emitted right before the component navigates to its home screen, from:

  • the Back button of a place profile;
  • the Back button of the search results view;
  • the clear (×) button of the search box;
  • any popstate event on window while a profile is open (see Browser Back).

filters is the category selection at that moment, and filter its first name or All when nothing is on. They still report a selection when the category row is hidden, for example with show-category-filters="false", even though the list is then not filtered by category. The route screen’s Back button does not emit it: it returns to the profile, which emits show-place.

citiesChange

Dispatched whenever the set of cities the view can be scoped to changes. Its detail is a CityChoice[]:

interface CityChoice { name: string // the city, labelled as the built-in picker labels it id: string // write this into `institution-id` to scope the view to it }

Use it to draw a city control of your own. Hiding the built-in picker with show-city-filter="false" takes away the control but keeps the scope, so this is what is left to move it:

const map = document.querySelector('mapvx-website') map.addEventListener('citiesChange', (event) => { renderCityOptions(event.detail) // [{ name, id }, …] }) // picking one map.setAttribute('institution-id', id)
  • The cities are the grouped ones the picker shows, not the raw options: an institution that sits inside a city is one entry of that city rather than a sibling of it.
  • A single entry means there is nothing to pick. The built-in picker hides itself in that case, and a control of your own can disable itself.
  • id is the option’s filterId, which identifies the city itself. institution-id accepts an institution’s own id too, so either resolves, but this one is unambiguous.

droppedPinChange

Dispatched whenever the visitor drops the origin pin or drags it somewhere else. Its detail is a LatLng:

interface LatLng { lat: number lng: number }

The pin is the one thing on the map the visitor moves and the host cannot see, so this is the only way to follow it. Writing the coordinate straight back into center keeps the map on the pin:

const map = document.querySelector('mapvx-website') map.addEventListener('droppedPinChange', (event) => { const { lat, lng } = event.detail map.setAttribute('center', `${lat},${lng}`) })
  • It fires on the first drop, on every later tap that moves the pin while the mode is armed, and on every drag, since all three leave the pin somewhere new.
  • It always carries a coordinate. Nothing takes the pin off the map once it is down, so there is no emptied state to guard against.
  • The pin also survives navigation, so the last coordinate reported stays the right one until the visitor moves it again.

zoomChange

Dispatched whenever the map settles on a zoom, whoever asked for it. Its detail is the zoom level, a number:

const map = document.querySelector('mapvx-website') map.addEventListener('zoomChange', (event) => { zoomReadout.textContent = event.detail.toFixed(2) })

It is the reporting half of zoom, and the only half the map can offer: it announces its zoom but never its centre, so a host can follow one and not the other.

  • It covers the zooms nothing asked for: a pinch, a double tap, the built-in zoom buttons, and the fit the component performs on the places it is listing.
  • Only the end of a gesture is reported. The frames in between are the animation rather than zooms the camera stopped at.
  • Writing what it carries straight back into zoom is safe. A zoom the map is already on is not treated as a move, so the attribute and the map cannot chase each other.

The cardSelected event (legacy)

cardSelected is the component’s older event. Its detail is a string: the MapVX id (mapvxId) of the clicked place.

el.addEventListener('cardSelected', (event) => { console.log('Card clicked:', event.detail) })

It is emitted only when a place card is clicked (grid, map-view list or search results), before the profile opens. Version 1.6.1 still emits it, but new code should use userAction, which covers every way a place can be opened. show-place is not a drop-in replacement:

TriggercardSelecteduserAction with show-place
Card clickYesYes, once the profile has resolved the place
Map marker clickNoYes
tenant deep linkNoYes
Back from the route screenNoYes
Repeats for the same openingNo, one event per clickCan repeat while place data changes
PayloadmapvxId string{ placeId, alias }

A card click emits both events: cardSelected first, then show-place once the profile has resolved the place. If you listen to both, do not count the click twice.

Listening to events

Both events are standard DOM CustomEvent objects:

PropertyValue
NamesuserAction and cardSelected, case-sensitive
TargetThe <mapvx-website> element
Payloadevent.detail
bubblesfalse
composedfalse
cancelablefalse

In practice:

  • Use the exact names. useraction, user-action and UserAction never fire.
  • Listen on the element. The events do not bubble, so a delegated listener on document or on a parent element is not called (except in the capture phase).
  • Events are notifications. preventDefault() has no effect; you cannot stop the component from navigating.
  • Handlers run synchronously, while the interaction is being handled. For return-to-home, select-destination and cardSelected that is before the component navigates, which is what makes URL sync work.
const el = document.querySelector('mapvx-website') el.addEventListener('userAction', (event) => { const action = event.detail switch (action.type) { case 'select-filter': case 'return-to-home': console.log('Category:', action.data.filter) break case 'show-place': console.log('Place:', action.data.placeId, action.data.alias) break case 'select-destination': console.log('Directions to:', action.data.destinationId, action.data.alias) break case 'search': console.log('Search:', action.data.searchTerm) break } })

In TypeScript, cast the event with (event as CustomEvent<UserAction>).detail. The package ships no typed event map for the element; see Element typing.

When to attach the listener

  • You can call addEventListener before the bundle has loaded or before the element is defined: it is a plain DOM listener on the same node. There is no need to poll customElements.get() or retry with setTimeout. If you need to wait for the element for other reasons, use customElements.whenDefined('mapvx-website') (see Waiting for the element).
  • Attach it as early as possible. Events are not queued or replayed, so a listener added later misses what already fired, such as the show-place of a tenant deep link or the search of a search deep link.
  • Events are dispatched only while the element is in the document. If you remove the element and create a new one, attach the listener to the new element.

Framework recipes

Load the bundle as described in Installation first. Then:

A ref plus addEventListener works in React 18 and 19. Remove the listener in the effect cleanup.

import { useEffect, useRef } from 'react' import type { UserAction } from '@mapvx/web-components-ssr' export function MapvxWebsite({ onAction }: { onAction: (action: UserAction) => void }) { const ref = useRef<HTMLElement>(null) useEffect(() => { const el = ref.current if (!el) return const handler = (event: Event) => onAction((event as CustomEvent<UserAction>).detail) el.addEventListener('userAction', handler) return () => el.removeEventListener('userAction', handler) }, [onAction]) return <mapvx-website ref={ref} api-key="YOUR_API_KEY" institution-id="YOUR_INSTITUTION_ID" /> }

TSX needs a JSX declaration for mapvx-website; see Element typing. In the Next.js App Router, put 'use client' at the top of this file, because it uses useEffect. React 19 also accepts event props that keep the event name’s case after on: onuserAction and oncardSelected. onUserAction does not fire, and React 18 does not attach event props on custom elements, which is why the ref version is shown here.

In any other framework, get a reference to the element and call addEventListener with the exact event name, as in Listening to events.

Keeping the host URL in sync

The component keeps its own navigation in memory and never writes to the host page’s URL or history. If you want shareable URLs, state that survives a reload, and a browser Back button that closes a profile, update the URL yourself from userAction events, using the parameter names the component reads on load (see Deep links).

const MAPVX_PARAMS = ['search', 'tenant', 'destination', 'tab'] function writeMapvxParams(params, mode) { const url = new URL(location.href) for (const name of MAPVX_PARAMS) url.searchParams.delete(name) for (const [name, value] of Object.entries(params)) { if (value) url.searchParams.set(name, value) } if (url.href === location.href) return if (mode === 'push') history.pushState(null, '', url) else history.replaceState(history.state, '', url) } document.querySelector('mapvx-website').addEventListener('userAction', (event) => { const { type, data } = event.detail switch (type) { case 'select-filter': case 'return-to-home': // Drops tenant, destination and search so the home screen does not reopen them writeMapvxParams({ tab: data.filter }, 'replace') break case 'search': writeMapvxParams({ search: data.searchTerm }, 'replace') break case 'show-place': { const params = new URLSearchParams(location.search) const current = params.get('tenant') ?? params.get('destination') const samePlace = current === data.placeId || (data.alias && current === data.alias) // New history entry only when a different place opens writeMapvxParams({ tenant: data.alias || data.placeId }, samePlace ? 'replace' : 'push') break } case 'select-destination': writeMapvxParams({ destination: data.alias || data.destinationId }, 'replace') break } })

Why it is written this way:

  • Update the URL synchronously in the listener. The component emits return-to-home before it navigates, and the home screen reads the query string when it is created. An update deferred to later (for example through an asynchronous router call) may arrive after the old parameters have already been read.
  • Remove tenant, destination and search on return-to-home. They are read again every time the home screen is created, so a leftover tenant sends the visitor straight back to that profile.
  • Replace on return-to-home. It is also emitted when the visitor presses the browser Back button on a profile; pushing at that moment would add an entry and discard the forward history.
  • Push only when a different place opens. show-place repeats for the same place, and it also fires for a deep link (whose id may be the mapvxId while the event carries the alias) and on the way back from the route screen. Replacing in those cases avoids duplicate entries.
  • Replace for search and select-filter. search fires for each new term, so pushing would create one entry per term. Keep tab current, because the component applies it again whenever the category list changes.
  • Keep the rest of the URL. Building the URL with URL and URLSearchParams preserves your own query parameters and hash, and encodes ids and search terms.

In a single-page app, call the History API directly as shown. A router’s navigation method may run asynchronously, and a navigation that remounts the page containing <mapvx-website> replaces the element with a new one, which does not keep the previous search text or selected category, and needs its listener attached again. Some routers also keep their own data in history.state; the example preserves it when replacing, but check how your router handles direct History API calls.

The component reads four query parameters from the host page’s location.search:

ParameterValueEffect
searchSearch termFills the search box and opens the search results.
tenantPlace mapvxId or aliasOpens that place’s profile.
destinationPlace mapvxId or aliasOpens the route screen with that place as the destination.
tabCategory namesTurns those categories on, several separated by commas.
<a href="/directory?tenant=YOUR_PLACE_ID">Open a place</a> <a href="/directory?search=coffee">Search for coffee</a> <a href="/directory?destination=YOUR_PLACE_ID">Get directions to a place</a> <a href="/directory?tab=Shopping">Show only Shopping</a> <a href="/directory?tab=Shopping,Hotels">Show Shopping and Hotels</a>
  • Priority. Only one of search, tenant and destination is used: search wins over tenant, and tenant wins over destination. tab is applied independently.
  • Ids. tenant and destination accept the place’s mapvxId or its client id (the alias of the events). For tenant, an id that is not in the loaded place list is requested from the API. The profile waits until places have loaded, so it stays empty if they never load (for example without a valid api-key).
  • destination depends on the institution. The route screen pre-fills the destination only when the place belongs to the currently selected institution. If institution-id is missing or matches no institution, a destination link opens the route screen with an empty destination. See institution-id.
  • tab must match exactly. Each value must equal a category name, including case. Unknown names are dropped rather than emptying the directory, and spacing around the commas is ignored. ?tab=All means no category filter, which is the same as leaving it out. The component applies it again whenever the category list is recomputed and whenever the list is shown again.
  • Query string only. The hash is not read, so hash-based routes such as #/directory?tenant=... do not work.
  • When they are read. search, tenant and destination are read every time the home screen is created: on first load, and each time the visitor returns home from a place profile.
  • Read, not watched. Changing the query string later does not navigate the component, and there is no method to open a place from script.
  • Events. A tenant link emits show-place and a search link emits search. destination and tab links emit nothing.

Rewrite deep links on return-to-home. If the page was opened with ?tenant=X (or ?destination=X) and the URL never changes, the component’s Back button returns to the home screen, which reads the same parameter and opens X again. Update the URL on return-to-home as shown in Keeping the host URL in sync.

These parameter names are effectively reserved. On any page that contains <mapvx-website>, search, tenant, destination and tab are read as deep links. If your page uses one of them for something else, rename your own parameter.

Browser Back

The component never changes the host page’s URL or history. While a place profile is open, it listens for popstate on window. Any popstate, whether it comes from the browser’s Back or Forward button, history.back(), history.go(), a host router moving through history, or a link or script that changes only the hash (#section), makes the component emit return-to-home and return to its home screen. The listener is removed when the profile closes.

What this means for your page:

  • Without URL sync, Back goes to the previous history entry. The component adds no entries of its own, so Back takes the visitor to whatever came before, usually the previous page. If Back should close the profile instead, push an entry when a profile opens, as in the URL sync example.
  • With the URL sync example, Back on a profile returns to your home URL and the component returns to its home screen. The return-to-home handler replaces the current entry, so the forward entry is kept.
  • Forward does not reopen the place. After Back, the component is on its home screen, where it does not listen for popstate: Forward changes only the URL. The place would open again on reload, because the URL then contains tenant.
  • Search results and the route screen do not listen. Browser Back on those screens changes only the host URL; the component stays where it is.
  • Host navigation counts too. If your own router traverses history while a profile is open, the component also emits return-to-home and goes home.
Last updated on