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
type | data | Se 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.
filterses 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.filteres el primer nombre seleccionado, oAllsi 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
tabo por el atributoselected-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.
search
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-homedel 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
popstateenwindowmientras 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.
ides elfilterIdde la opción, que identifica a la ciudad en sí.institution-idtambié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
zoomes 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:
| Disparador | cardSelected | userAction con show-place |
|---|---|---|
| Clic en una tarjeta | Sí | Sí, una vez que el perfil resolvió el lugar |
| Clic en un marcador del mapa | No | Sí |
Enlace profundo con tenant | No | Sí |
| Back desde la pantalla de ruta | No | Sí |
| Se repite para la misma apertura | No, un evento por clic | Puede repetirse mientras cambian los datos de lugares |
| Payload | Cadena 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:
| Propiedad | Valor |
|---|---|
| Nombres | userAction y cardSelected, distinguen mayúsculas y minúsculas |
| Destino | El elemento <mapvx-website> |
| Payload | event.detail |
bubbles | false |
composed | false |
cancelable | false |
En la práctica:
- Usa los nombres exactos.
useraction,user-actionyUserActionnunca se disparan. - Escucha en el elemento. Los eventos no se propagan hacia los elementos padre (no hacen bubbling), así que un listener delegado en
documento 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-destinationycardSelectedeso 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
addEventListenerantes 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 consultarcustomElements.get()en bucle ni reintentar consetTimeout. Si necesitas esperar al elemento por otros motivos, usacustomElements.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-placede un enlace profundo contenanto elsearchde un enlace profundo consearch. - 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:
React
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-homeantes 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,destinationysearchenreturn-to-home. Se vuelven a leer cada vez que se crea la pantalla de inicio, así que untenantque 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-placese repite para el mismo lugar, y además se dispara con un enlace profundo (cuyo id puede ser elmapvxIdmientras el evento trae el alias) y al volver de la pantalla de ruta. Reemplazar en esos casos evita entradas duplicadas. - Reemplaza la entrada en
searchyselect-filter.searchse dispara con cada término nuevo, así que agregar entradas crearía una por término. Manténtabactualizado, 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
URLyURLSearchParamsconserva 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.
Enlaces profundos
El componente lee cuatro parámetros de query string del location.search de la página anfitriona:
| Parámetro | Valor | Efecto |
|---|---|---|
search | Término de búsqueda | Completa el cuadro de búsqueda y abre los resultados. |
tenant | mapvxId o alias del lugar | Abre el perfil de ese lugar. |
destination | mapvxId o alias del lugar | Abre la pantalla de ruta con ese lugar como destino. |
tab | Nombres de categorías | Prende 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,tenantydestination:searchtiene prioridad sobretenant, ytenantsobredestination.tabse aplica de forma independiente. - Ids.
tenantydestinationaceptan elmapvxIddel lugar o su id de cliente (elaliasde los eventos). En el caso detenant, 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 unapi-keyválido). destinationdepende 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 faltainstitution-ido no coincide con ninguna institución, un enlace condestinationabre la pantalla de ruta con el destino vacío. Consulta institution-id.tabdebe 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=Allsignifica 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,tenantydestinationse 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
tenantemiteshow-placey uno consearchemitesearch. Los enlaces condestinationytabno 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-homereemplaza 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 contienetenant. - 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-homey vuelve al inicio.