Skip to content

Click Handling and Event Propagation

Overview

The Creative-Engine uses a layered click handling system where events bubble up through nested Vue components. Two things happen on click: navigation (opening clickthrough URLs) and analytics (tracking user interaction). The .stop modifier and stopPropagation() are used strategically to prevent clicks on interactive elements from triggering parent handlers — most importantly the background clickthrough.

Understanding this system is critical because removing or adding .stop in the wrong place can cause:

  • Clickthrough URLs opening when users interact with video controls, forms, or buttons
  • Double analytics events
  • Lost first-interaction tracking for expandable creatives

Component Hierarchy

Creative.vue                         @click.self → creativeContainerClicked()
  └── CreativeBody.vue               @click.self → handleBgClick()
       ├── CreativeConversationBlock
       │    └── MessageHolder         @click.stop → onMessageWrapperClick()
       │         └── Flow components (Text, Choice, Link, Slider...)

       ├── CreativeVideoBlock         @click → $emit('bgClick')
       │    ├── <video>
       │    └── CreativeVideoControls @click.stop (section wrapper)
       │         ├── togglePlay()
       │         └── toggleMute()

       ├── CreativeSliderBlock
       ├── CreativeFormBlock          @click.stop (form-block div)

       ├── VisualElements (via VisualElementMixin)
       │    ├── CreativeButtonBlock   @click → handleClick() (no propagation stop)
       │    ├── CreativeTextBlock     @click → handleClick() (no propagation stop)
       │    ├── CreativeGraphicBlock  @click → handleClick() (no propagation stop)
       │    └── CreativeHtmlBlock     @click → handleClick() (no propagation stop)

       ├── CloseButton               @click.stop → handleButtonClick()
       └── RemoveButton              @click.stop → handleButtonClick()

CreativeHeader (expandable only, outside CreativeBody)
  ├── Tagline                        @click.stop → toggleExpand()
  └── ExpandableIcon

Click Event Flow

User clicks something inside the creative

Is it an interactive element? (button, video control, form input, tagline)
         ↓ YES                              ↓ NO
Component handles it                   Event bubbles up
(.stop prevents bubbling)                    ↓
         ↓                             CreativeBody.vue handleBgClick()
Component-specific action                    ↓
(play video, submit form,             Has backgroundClickthroughUrl?
 open element clickthrough)            ↓ YES              ↓ NO
                                  Opens the URL           Nothing
                                  Sends analytics

Key Files & Their Roles

FileRoleStops Propagation
Creative.vueRoot container. Tracks first interaction, clicks counter, goal timerNo — uses @click.self
CreativeBody.vueHandles background clickthrough URL navigationNo — uses @click.self
MessageHolder.vueWraps conversation messages, emits bgClick only for wrapper clicksYes — @click.stop
CreativeVideoControls.vuePlay/pause and mute/unmute buttonsYes — @click.stop on <section>
CreativeVideoBlock.vueVideo container, emits bgClick for background clicksNo
CreativeFormBlock.vueForm wrapper, prevents form clicks from reaching backgroundYes — @click.stop on inner div
CreativeButtonBlock.vueClickthrough buttonNo — @click without .stop
VisualElementMixin.tsShared click handler for visual elements with clickthrough URLsNo — handleClick() does not call stopPropagation()
Tagline.vueExpandable creative header, triggers expand/collapseYes — @click.stop
CloseButton.vueClose button for bannersYes — @click.stop
RemoveButton.vueRemove/close button for expandablesYes — @click.stop
ToggleExpand.tsExpand/collapse logic for expandable creativesN/A (utility function)

Background Clickthrough

The background clickthrough is the most important thing to protect against accidental triggers. It opens a URL when the user clicks the creative background.

javascript
// CreativeBody.vue — handleBgClick()
handleBgClick() {
  const { backgroundClickthrough, backgroundClickthroughUrl }
    = DataStore.creativeSettings.creativeDelivery

  // Flow URL takes precedence over delivery config
  const bgClickUrl = DataStore.currentBgClickUrl.value || clickthroughUrl
  if (!bgClickUrl) return

  handleLinkOpens(getLinkUrl(bgClickUrl))     // Navigate
  Analytics.sendCount(COUNTABLE_METRICS.backgroundClick)
  Analytics.sendEvent({ clickedLink: true, linkUrl: bgClickUrl })
}

Why .stop matters: Without .stop on interactive elements, clicking a video play button or form input would bubble up to handleBgClick() and open the clickthrough URL. This is especially dangerous with CreativeVideoBlock which emits bgClick on every click inside it — so any child click that doesn't stop propagation will trigger navigation.

Analytics Events on Click

There are several analytics mechanisms that fire on click:

MechanismWhat it doesWhen it fires
sendFirstEvent()Sets creativeClickedOnce, starts goal timer, sends secondsToActiveOnce per creative lifetime, on first user interaction
sendEvent({})Sends a general analytics eventOn various interactions
sendCount(metricId)Sends a countable metric (-1 = header click, -2 = bg click)On clickthrough navigation
clicks counterIncrements Analytics.clicks, included in Bunny payloadOn every click (tracked in Creative.vue)
numUserActionsIncrements user action counter, included as chatbotActionsOn expand, form submit, other interactions

First Event Flow

sendFirstEvent() is the gatekeeper for first-interaction tracking:

javascript
// analytics.ts
sendFirstEvent(expandable = false) {
  if (DataStore.creativeClickedOnce.value) return   // Only fires once
  DataStore.creativeClickedOnce.value = true

  if (expandable) this.numUserActions++             // Expandables count as action

  Analytics.startGoalTimer()                         // Start tracking goal time
  this.sendEvent({ secondsToActive: ... })           // Time from load to first click
  this.postMessage({ creativeClickedOnce: true })    // Notify parent frame
}

Important for expandable creatives: The expandable = true parameter is passed from ToggleExpand.ts and Tagline.vue because for expandables, the first interaction is clicking the tagline — not the creative background. Since Creative.vue uses @click.self, the container handler won't fire from tagline clicks, so sendFirstEvent must be called explicitly from the expand path.

Visual Element Clickthrough

Visual elements (buttons, text blocks, graphics, HTML blocks) use a shared mixin for click handling:

javascript
// VisualElementMixin.ts
handleClick() {
  if (!this.clickthroughUrl) return    // No URL = no action, click bubbles
  this.trackClick()
}

trackClick() {
  handleLinkOpens(getLinkUrl(this.clickthroughUrl))
  Analytics.sendCount(COUNTABLE_METRICS.headerClick)   // -1
  Analytics.sendEvent({ clickedLink: true, linkUrl: this.clickthroughUrl })
}

Note: handleClick() does NOT call stopPropagation(). Clicks always bubble up regardless of clickthrough URL. Visual elements without a URL produce no analytics at all -- see analytics/visual-element-tracking-gap.md for the full tracking gap analysis.

Expandable Creatives

Expandable creatives have a different click flow because interaction starts at the header (tagline), not the creative body.

User clicks Tagline

Tagline.vue toggleExpand() — @click.stop

Analytics.sendFirstEvent(true)     ← expandable flag

$emit('toggle-expand')

ToggleExpand.ts executes

DataStore.creativeExpanded = true
SafeFrame.triggerExpand()
eventEmitter.emitStartFlow()       ← conversation starts

On collapse:

User clicks Tagline again (or CloseButton)

ToggleExpand.ts

DataStore.creativeExpanded = false
Analytics.clearGoalTimer()
Analytics.sendEvent({ secondsTotalActive: ... })

URL Building

All clickthrough URLs go through getLinkUrl() before navigation:

javascript
// linkHelper.ts — getLinkUrl()
// 1. Append link params (UTM etc)
// 2. Append GDPR params
// 3. Prepend click macro (DSP tracking) — must be last

Navigation priority:

  1. MRAID (window.mraid.open) — mobile in-app
  2. VPAID (AdClickThru event) — Yahoo/proprietary players
  3. Standard (window.open) — regular browser

Rules of Thumb

  1. Any interactive element must stop propagation — otherwise clicks trigger handleBgClick() and can open the clickthrough URL
  2. .stop goes on the outermost clickable wrapper — e.g. CreativeVideoControls stops at the <section> level, not individual buttons
  3. Visual elements do NOT stop propagation -- VisualElementMixin.handleClick() never calls stopPropagation(). Clicks always bubble. Without a clickthrough URL, the handler returns immediately with no side effects.
  4. First interaction is handled by @click.capture on Creative.vue -- fires on any trusted click in the container. Expandables additionally call from Tagline/ToggleExpand.
  5. @click.self is not a substitute for .stop — it only prevents the handler from firing on bubbled events, but intermediate components (like CreativeVideoBlock) may re-emit the event explicitly

SPA Considerations

In SPA mode, multiple creatives load on the same page without refresh. The Analytics and Visibility objects are singletons that persist between creatives. State like numUserActions, flowSequenceNumber, clicks, and visibility tracking (bucketIndex, lastBucketSent) must be reset in teardown() to avoid stale data leaking into the next creative.

Internal documentation