Skip to Content
Eventos

Eventos

<mapvx-website> informa lo que hace el visitante mediante dos eventos DOM que se despachan sobre el propio elemento: userAction, que cubre todas las interacciones que el componente reporta, y cardSelected, un evento heredado para los clics en tarjetas. Usa userAction en las integraciones nuevas.

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

El evento userAction

Cada interacción reportable se despacha como un evento userAction. Su detail es un objeto UserAction, una unión discriminada por 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 }

El paquete exporta este tipo y una interfaz por cada miembro (SelectFilterAction, ShowPlaceAction, SelectDestinationAction, SearchAction, ReturnToHomeAction). Solo existen como tipos, así que impórtalos con import type (consulta Solo tipos). Las declaraciones están en Tipos TypeScript.

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

placeId y destinationId son el id MapVX del lugar (mapvxId). alias es el id de cliente del lugar (clientId): su alias en los datos de MapVX o, si no tiene alias, su id externo, y undefined cuando no tiene ninguno de los dos. Los parámetros de enlace profundo tenant y destination aceptan cualquiera de los dos valores.

Tipos de acción

typedataSe emite cuando
select-filter{ filter, filters }El visitante prende o apaga una categoría.
show-place{ placeId, alias }Se muestra el perfil de un lugar, sin importar cómo se abrió.
select-destination{ destinationId, alias }El visitante hace clic en Pedestrian-friendly directions en un perfil.
search{ searchTerm }Se ejecuta una búsqueda con un término nuevo.
return-to-home{ filter, filters }El componente vuelve a su pantalla de inicio desde un perfil o desde la búsqueda.

filters es la selección completa de categorías, que puede tener varias o ninguna. filter es la primera de ellas, o 'All' cuando no hay ninguna prendida; es anterior a la selección múltiple y se mantiene para que los enlaces que lo escriben en ?tab= sigan funcionando. En código nuevo, preferí filters.

categoriesChange

Un evento aparte de userAction, que se emite cada vez que cambia el conjunto de categorías que ofrece el filtro. Su detail es un CategoryFiltersData[]:

interface CategoryFiltersData { name: string placeData: string[] // mapvxId de cada lugar de la categoría }

Sirve para dibujar tu propio control de categorías. La lista es la ya resuelta, no la declaración cruda de la institución: las categorías cuyos lugares esta API key nunca devuelve se descartan, y se agrega la entrada All. Reproducir esas reglas por tu cuenta se desincronizaría de las nuestras.

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) } }) // Devolver la elección del visitante al componente. onCheckboxToggle((names) => { map.setAttribute('selected-categories', names.join(',')) })

Combinalo con selected-categories para manejar el filtro, y con show-category-filters="false" para esconder la fila interna.

Estos cinco tipos son la lista completa. No hay eventos para elegir una ciudad, cambiar entre Grid View y Map View, cambiar de página, elegir el origen o el destino en la pantalla de ruta, iniciar o avanzar paso a paso una ruta, hacer clic en Driving directions o en un enlace a un sitio web, ni para usar los controles del mapa. Tampoco hay eventos de carga, de disponibilidad ni de errores.

select-filter

  • Se emite cuando el visitante prende o apaga una categoría, incluida la última que apaga.
  • filters es la selección completa, en el orden en que se prendieron. Está vacía cuando no hay ninguna, lo que significa sin filtro de categoría.
  • filter es el primer nombre seleccionado, o All si no hay ninguno. Es anterior a la selección múltiple y se mantiene para que los hosts que lo escriben en ?tab= sigan funcionando.
  • No se emite cuando la selección se aplica automáticamente, por el parámetro de query string tab o por el atributo selected-categories.

show-place

Se emite cada vez que la pantalla de perfil resuelve el lugar que debe mostrar:

  • un clic en una tarjeta (cuadrícula, lista de la vista de mapa o resultados de búsqueda);
  • un clic en un marcador del mapa, en Map View (pantalla de inicio o resultados de búsqueda) o en el mapa de un perfil;
  • un enlace profundo con tenant;
  • el botón Back de la pantalla de ruta, que vuelve al perfil del lugar para el que se abrió esa pantalla.

Puede dispararse más de una vez para el mismo lugar, porque el perfil vuelve a resolver el lugar cada vez que cambia la lista de lugares del componente, por ejemplo mientras los datos todavía se están cargando. Si cuentas visitas, trata como uno solo los eventos show-place consecutivos con el mismo placeId. Cuando el lugar no está en la lista cargada, el componente primero lo pide a la API, así que el evento puede llegar después de una petición de red.

Abrir un perfil también desplaza la página anfitriona de forma suave hasta el borde superior del primer <mapvx-website> del documento (consulta La página se desplaza al abrir un lugar).

select-destination

Se emite cuando el visitante hace clic en Pedestrian-friendly directions en un perfil, justo antes de que se abra la pantalla de ruta. destinationId y alias identifican el lugar del perfil. El enlace Driving directions (abre Google Maps en una pestaña nueva) y un enlace profundo con destination no emiten nada.

Lo emite la vista de resultados de búsqueda cuando el visitante lleva 500 ms sin escribir, con el término sin espacios al inicio ni al final, y solo si ese término no está vacío y es distinto del último que informó esa vista. No se emite con cada tecla. También se dispara:

  • con un enlace profundo con search;
  • cuando el visitante vuelve de un perfil que abrió desde los resultados de búsqueda: después del return-to-home del perfil, el componente muestra de nuevo esos resultados y vuelve a emitir el término recordado.

El botón para borrar (×) del cuadro de búsqueda emite return-to-home, no search. Borrar el texto a mano no emite nada, y los resultados de búsqueda siguen abiertos.

return-to-home

Se emite justo antes de que el componente navegue a su pantalla de inicio, desde:

  • el botón Back del perfil de un lugar;
  • el botón Back de la vista de resultados de búsqueda;
  • el botón para borrar (×) del cuadro de búsqueda;
  • cualquier evento popstate en window mientras hay un perfil abierto (consulta Botón Atrás del navegador).

filters es la selección de categorías en ese momento, y filter su primer nombre o All si no hay ninguna prendida. Siguen informando una selección cuando la fila de categorías está oculta, por ejemplo con show-category-filters="false", aunque en ese caso la lista no se filtra por categoría. El botón Back de la pantalla de ruta no lo emite: vuelve al perfil, que emite show-place.

citiesChange

Se dispara cada vez que cambia el conjunto de ciudades a las que se puede acotar la vista. Su detail es un CityChoice[]:

interface CityChoice { name: string // la ciudad, con la misma etiqueta que usa el selector interno id: string // escribí esto en `institution-id` para acotar la vista a ella }

Sirve para dibujar un control de ciudades propio. Ocultar el selector interno con show-city-filter="false" quita el control pero mantiene el alcance, así que esto es lo que queda para moverlo:

const map = document.querySelector('mapvx-website') map.addEventListener('citiesChange', (event) => { renderCityOptions(event.detail) // [{ name, id }, …] }) // al elegir una map.setAttribute('institution-id', id)
  • Las ciudades son las agrupadas que muestra el selector, no las opciones crudas: una institución que vive dentro de una ciudad es una entrada de esa ciudad y no una hermana suya.
  • Una sola entrada significa que no hay nada para elegir. El selector interno se esconde solo en ese caso, y un control propio puede deshabilitarse.
  • id es el filterId de la opción, que identifica a la ciudad en sí. institution-id también acepta el id propio de una institución, así que cualquiera resuelve, pero este es el que no deja lugar a dudas.

droppedPinChange

Se dispara cada vez que el visitante deja el pin de origen o lo arrastra a otro lado. Su detail es un LatLng:

interface LatLng { lat: number lng: number }

El pin es lo único del mapa que el visitante mueve y el host no puede ver, así que esta es la única forma de seguirlo. Devolver la coordenada directo a center deja el mapa sobre el pin:

const map = document.querySelector('mapvx-website') map.addEventListener('droppedPinChange', (event) => { const { lat, lng } = event.detail map.setAttribute('center', `${lat},${lng}`) })
  • Se dispara en la primera colocación, en cada toque posterior que mueve el pin mientras el modo está armado, y en cada arrastre: las tres dejan el pin en un lugar nuevo.
  • Siempre trae una coordenada. Nada saca el pin del mapa una vez puesto, así que no hay un estado vacío del que cuidarse.
  • El pin además sobrevive a la navegación, así que la última coordenada informada sigue siendo la buena hasta que el visitante lo mueva otra vez.

zoomChange

Se dispara cada vez que el mapa queda en un zoom, lo haya pedido quien lo haya pedido. Su detail es el nivel de zoom, un número:

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

Es la mitad que informa de zoom, y la única que el mapa puede ofrecer: anuncia su zoom pero nunca su centro, así que el host puede seguir uno y no el otro.

  • Cubre los zooms que nadie pidió: un pellizco, un doble toque, los botones de zoom internos, y el encuadre que el componente hace sobre los lugares que está listando.
  • Solo se informa el final del gesto. Los fotogramas del medio son la animación, no zooms en los que la cámara se haya detenido.
  • Devolver lo que trae directo a zoom es seguro. Un zoom en el que el mapa ya está no se toma como un movimiento, así que el atributo y el mapa no se persiguen.

El evento cardSelected (heredado)

cardSelected es el evento más antiguo del componente. Su detail es una cadena: el id MapVX (mapvxId) del lugar en el que se hizo clic.

el.addEventListener('cardSelected', (event) => { console.log('Clic en tarjeta:', event.detail) })

Se emite solo cuando se hace clic en la tarjeta de un lugar (cuadrícula, lista de la vista de mapa o resultados de búsqueda), antes de que se abra el perfil. La versión 1.6.1 todavía lo emite, pero el código nuevo debería usar userAction, que cubre todas las formas de abrir un lugar. show-place no es un reemplazo directo:

DisparadorcardSelecteduserAction con show-place
Clic en una tarjetaSíSí, una vez que el perfil resolvió el lugar
Clic en un marcador del mapaNoSí
Enlace profundo con tenantNoSí
Back desde la pantalla de rutaNoSí
Se repite para la misma aperturaNo, un evento por clicPuede repetirse mientras cambian los datos de lugares
PayloadCadena con el mapvxId{ placeId, alias }

Un clic en una tarjeta emite los dos eventos: primero cardSelected y luego show-place, cuando el perfil resolvió el lugar. Si escuchas ambos, no cuentes el clic dos veces.

Escuchar eventos

Ambos eventos son objetos CustomEvent estándar del DOM:

PropiedadValor
NombresuserAction y cardSelected, distinguen mayúsculas y minúsculas
DestinoEl elemento <mapvx-website>
Payloadevent.detail
bubblesfalse
composedfalse
cancelablefalse

En la práctica:

  • Usa los nombres exactos. useraction, user-action y UserAction nunca se disparan.
  • Escucha en el elemento. Los eventos no se propagan hacia los elementos padre (no hacen bubbling), así que un listener delegado en document o en un elemento padre no se llama (salvo en la fase de captura).
  • Los eventos son notificaciones. preventDefault() no tiene efecto; no puedes impedir que el componente navegue.
  • Los handlers se ejecutan de forma síncrona, mientras se procesa la interacción. Para return-to-home, select-destination y cardSelected eso ocurre antes de que el componente navegue, y es lo que permite sincronizar la URL.
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('Categoría:', action.data.filter) break case 'show-place': console.log('Lugar:', action.data.placeId, action.data.alias) break case 'select-destination': console.log('Indicaciones hacia:', action.data.destinationId, action.data.alias) break case 'search': console.log('Búsqueda:', action.data.searchTerm) break } })

En TypeScript, convierte el evento con (event as CustomEvent<UserAction>).detail. El paquete no incluye un mapa de eventos tipado para el elemento; consulta Tipado del elemento.

Cuándo registrar el listener

  • Puedes llamar a addEventListener antes de que cargue el bundle o antes de que se defina el elemento: es un listener DOM común sobre el mismo nodo. No hace falta consultar customElements.get() en bucle ni reintentar con setTimeout. Si necesitas esperar al elemento por otros motivos, usa customElements.whenDefined('mapvx-website') (consulta Esperar al elemento).
  • Regístralo lo antes posible. Los eventos no se encolan ni se vuelven a emitir, así que un listener agregado más tarde pierde lo que ya se disparó, como el show-place de un enlace profundo con tenant o el search de un enlace profundo con search.
  • Los eventos se despachan solo mientras el elemento está en el documento. Si quitas el elemento y creas uno nuevo, registra el listener en el elemento nuevo.

Recetas por framework

Primero carga el bundle como se describe en Instalación. Después:

Una ref más addEventListener funciona en React 18 y 19. Quita el listener en la función de limpieza del efecto.

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 necesita una declaración JSX para mapvx-website; consulta Tipado del elemento. En el App Router de Next.js, pon 'use client' al inicio de este archivo, porque usa useEffect. React 19 también acepta props de evento que conservan las mayúsculas y minúsculas del nombre del evento después de on: onuserAction y oncardSelected. onUserAction no se dispara, y React 18 no registra props de evento en custom elements; por eso aquí se muestra la versión con ref.

En cualquier otro framework, obtén una referencia al elemento y llama a addEventListener con el nombre exacto del evento, como en Escuchar eventos.

Sincronizar la URL de la página

El componente guarda su propia navegación en memoria y nunca escribe en la URL ni en el historial de la página anfitriona. Si quieres URLs que se puedan compartir, un estado que sobreviva a una recarga y un botón Atrás del navegador que cierre el perfil, actualiza tú la URL a partir de los eventos userAction, con los nombres de parámetro que el componente lee al cargar (consulta Enlaces profundos).

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': // Quita tenant, destination y search para que la pantalla de inicio no los vuelva a abrir 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) // Nueva entrada en el historial solo cuando se abre un lugar distinto writeMapvxParams({ tenant: data.alias || data.placeId }, samePlace ? 'replace' : 'push') break } case 'select-destination': writeMapvxParams({ destination: data.alias || data.destinationId }, 'replace') break } })

Por qué está escrito así:

  • Actualiza la URL de forma síncrona en el listener. El componente emite return-to-home antes de navegar, y la pantalla de inicio lee la query string cuando se crea. Una actualización diferida (por ejemplo, mediante una llamada asíncrona al router) puede llegar cuando los parámetros anteriores ya se leyeron.
  • Quita tenant, destination y search en return-to-home. Se vuelven a leer cada vez que se crea la pantalla de inicio, así que un tenant que quedó en la URL lleva al visitante directo de vuelta a ese perfil.
  • Reemplaza la entrada en return-to-home. También se emite cuando el visitante pulsa el botón Atrás del navegador en un perfil; agregar una entrada en ese momento descartaría el historial hacia adelante.
  • Agrega una entrada solo cuando se abre un lugar distinto. show-place se repite para el mismo lugar, y además se dispara con un enlace profundo (cuyo id puede ser el mapvxId mientras el evento trae el alias) y al volver de la pantalla de ruta. Reemplazar en esos casos evita entradas duplicadas.
  • Reemplaza la entrada en search y select-filter. search se dispara con cada término nuevo, así que agregar entradas crearía una por término. Mantén tab actualizado, porque el componente lo vuelve a aplicar cada vez que cambia la lista de categorías.
  • Conserva el resto de la URL. Construir la URL con URL y URLSearchParams conserva tus propios parámetros y el hash, y codifica los ids y los términos de búsqueda.

En una aplicación de una sola página (SPA), llama directamente a la API History como en el ejemplo. El método de navegación de un router puede ejecutarse de forma asíncrona, y una navegación que vuelve a montar la página que contiene <mapvx-website> reemplaza el elemento por uno nuevo, que no conserva el texto de búsqueda ni la categoría seleccionada anteriores y necesita que vuelvas a registrar el listener. Algunos routers además guardan sus propios datos en history.state; el ejemplo los conserva al reemplazar, pero revisa cómo maneja tu router las llamadas directas a la API History.

El componente lee cuatro parámetros de query string del location.search de la página anfitriona:

ParámetroValorEfecto
searchTérmino de búsquedaCompleta el cuadro de búsqueda y abre los resultados.
tenantmapvxId o alias del lugarAbre el perfil de ese lugar.
destinationmapvxId o alias del lugarAbre la pantalla de ruta con ese lugar como destino.
tabNombres de categoríasPrende esas categorías, varias separadas por comas.
<a href="/directory?tenant=YOUR_PLACE_ID">Abrir un lugar</a> <a href="/directory?search=coffee">Buscar café</a> <a href="/directory?destination=YOUR_PLACE_ID">Cómo llegar a un lugar</a> <a href="/directory?tab=Shopping">Mostrar solo Shopping</a> <a href="/directory?tab=Shopping,Hotels">Mostrar Shopping y Hotels</a>
  • Prioridad. Solo se usa uno de search, tenant y destination: search tiene prioridad sobre tenant, y tenant sobre destination. tab se aplica de forma independiente.
  • Ids. tenant y destination aceptan el mapvxId del lugar o su id de cliente (el alias de los eventos). En el caso de tenant, un id que no está en la lista de lugares cargada se pide a la API. El perfil espera a que se carguen los lugares, así que queda vacío si nunca se cargan (por ejemplo, sin un api-key válido).
  • destination depende de la institución. La pantalla de ruta completa el destino solo cuando el lugar pertenece a la institución seleccionada en ese momento. Si falta institution-id o no coincide con ninguna institución, un enlace con destination abre la pantalla de ruta con el destino vacío. Consulta institution-id.
  • tab debe coincidir exactamente. Cada valor debe ser igual al nombre de una categoría, incluidas mayúsculas y minúsculas. Los nombres desconocidos se descartan en vez de vaciar el directorio, y los espacios alrededor de las comas se ignoran. ?tab=All significa sin filtro de categoría, lo mismo que omitirlo. El componente lo vuelve a aplicar cada vez que se recalcula la lista de categorías y cada vez que la lista se muestra de nuevo.
  • Solo la query string. El hash no se lee, así que las rutas basadas en hash como #/directory?tenant=... no funcionan.
  • Cuándo se leen. search, tenant y destination se leen cada vez que se crea la pantalla de inicio: en la primera carga y cada vez que el visitante vuelve al inicio desde el perfil de un lugar.
  • Se leen, no se observan. Cambiar la query string después no hace navegar al componente, y no hay ningún método para abrir un lugar desde un script.
  • Eventos. Un enlace con tenant emite show-place y uno con search emite search. Los enlaces con destination y tab no emiten nada.

Reescribe los enlaces profundos en return-to-home. Si la página se abrió con ?tenant=X (o ?destination=X) y la URL nunca cambia, el botón Back del componente vuelve a la pantalla de inicio, que lee el mismo parámetro y abre X otra vez. Actualiza la URL en return-to-home como se muestra en Sincronizar la URL de la página.

Estos nombres de parámetro quedan reservados en la práctica. En cualquier página que contenga <mapvx-website>, search, tenant, destination y tab se leen como enlaces profundos. Si tu página usa alguno de ellos para otra cosa, cambia el nombre de tu parámetro.

Botón Atrás del navegador

El componente nunca cambia la URL ni el historial de la página anfitriona. Mientras hay un perfil de lugar abierto, escucha popstate en window. Cualquier popstate, ya venga del botón Atrás o Adelante del navegador, de history.back(), de history.go(), de un router del sitio que recorre el historial o de un enlace o script que solo cambia el hash (#seccion), hace que el componente emita return-to-home y vuelva a su pantalla de inicio. El listener se quita cuando se cierra el perfil.

Qué significa esto para tu página:

  • Sin sincronizar la URL, Atrás va a la entrada anterior del historial. El componente no agrega entradas propias, así que Atrás lleva al visitante a lo que haya antes, normalmente la página anterior. Si Atrás debe cerrar el perfil, agrega una entrada al historial cuando se abre un perfil, como en el ejemplo de sincronización de la URL.
  • Con el ejemplo de sincronización de la URL, Atrás en un perfil vuelve a tu URL de inicio y el componente vuelve a su pantalla de inicio. El handler de return-to-home reemplaza la entrada actual, así que se conserva la entrada hacia adelante.
  • Adelante no vuelve a abrir el lugar. Después de Atrás, el componente está en su pantalla de inicio, donde no escucha popstate: Adelante solo cambia la URL. El lugar se volvería a abrir al recargar, porque la URL entonces contiene tenant.
  • Los resultados de búsqueda y la pantalla de ruta no escuchan. En esas pantallas, el botón Atrás del navegador solo cambia la URL de la página anfitriona; el componente se queda donde está.
  • La navegación del sitio también cuenta. Si tu propio router recorre el historial mientras hay un perfil abierto, el componente también emite return-to-home y vuelve al inicio.
Última actualización: