Skip to main content

Events

React to what visitors do in your experience — clicks, hovers, state changes, and video playback. Both a Component and a ComponentSet expose the same method:

on(type: SdkEventType, handler: (event: ComponentEvent) => void): Unsubscribe
const off = experience.findByLocator('cta').on('component.click', (event) => {
console.log('clicked', event.target.cmlLocator)
})

off() // stop listening

on returns an unsubscribe function. Call it to detach; calling it more than once is harmless.

Event types

TypeFires when
'component.click'A component is clicked.
'component.hover'A component is hovered.
'component.state-changed'A component's state changes.
'video.play'A video starts playing.
'video.seek'A video is seeked.
'video.progress'A video's playback position advances.

What your handler receives

interface ComponentEvent {
type: SdkEventType // the event name you subscribed to
target: Component // the component that fired it
originalEvent: Event // the underlying DOM event
}

target is the useful one, and it's a full Component — so it tells you what fired and lets you act on it in the same breath:

experience.findByLocator('faq-question').on('component.click', (event) => {
event.target.states.toggle('open') // act on whatever was clicked
console.log(event.target.cmlLocator, event.target.componentType)
})

originalEvent is the raw DOM event, for anything the component surface doesn't carry — pointer coordinates, modifier keys, or a video event's payload:

experience.findByTag('cml-video').on('video.progress', (event) => {
console.log(event.originalEvent) // the underlying event, with its detail
})

Events bubble

A listener fires for its own component and everything inside it. A listener on a leaf hears only itself; a listener on a group or section hears its whole subtree.

That makes one listener on a container the best way to handle many children — including children that come and go:

const quiz = experience.findByLocator('quiz').only()

quiz.on('component.click', (event) => {
if (event.target.cmlLocator === 'answer') {
checkAnswer(event.target)
}
})

You can also subscribe on the experience itself to hear everything:

experience.on('component.click', (event) => {
analytics.track('click', { component: event.target.cmlLocator })
})

Subscribing to many components at once

ComponentSet.on(...) attaches to every member and returns one function that detaches them all:

const off = experience.findByLocator('cta').on('component.click', handler)

// later
off() // detaches from every member at once

The members are captured when you call on. Components that appear afterwards aren't covered — either subscribe on a container and rely on bubbling, or run the lookup and subscribe again.

Cleaning up

You don't need to unsubscribe before leaving a page; the listeners go away with it. Do keep the unsubscribe function when a listener is only meant to be live for part of the visit:

const off = experience
.findByLocator('modal-close')
.on('component.click', () => {
closeModal()
off() // one-shot
})

On an embedded experience that swaps pages in place, re-attach your listeners after each swap — see Delivery modes.