Skip to content

Add support for custom options and cancelable $dispatch#4608

Merged
calebporzio merged 2 commits into
alpinejs:mainfrom
nicolagianelli:dispatch-cancelable-and-options
Feb 9, 2026
Merged

Add support for custom options and cancelable $dispatch#4608
calebporzio merged 2 commits into
alpinejs:mainfrom
nicolagianelli:dispatch-cancelable-and-options

Conversation

@nicolagianelli

@nicolagianelli nicolagianelli commented Apr 23, 2025

Copy link
Copy Markdown
Contributor

Proposal: Extend $dispatch functionality

Hello everyone,
While working with AlpineJS to build a UI library for Laravel, I noticed some limitations in the magic function $dispatch. Specifically:

  1. It doesn't allow extended options.
  2. It doesn’t support proper cancellation via preventDefault.

These limitations can lead to issues when building modular, event-driven components. Below, I explain the motivation and propose a minimal enhancement.

Problem Overview

  1. Lack of Extendable Options
    Currently, it's not possible to customize event behavior (e.g., disabling bubbles). This becomes problematic in cases like:

A modal and a dropdown both dispatching an event named closed.
When used together, the parent modal might unintentionally respond to the dropdown's event, causing conflicts.

Yes, we could rename events (e.g., modal:closed vs. dropdown:closed), but this doesn't solve issues with nested modals or more complex component structures.

  1. Non-Cancelable in Practice
    Although dispatched events are marked as cancelable: true, the function does not return the created CustomEvent.
    As a result, it's not possible to check whether preventDefault() was called.

This prevents useful patterns like:

const event = $dispatch('close');
if (event) {
    // Proceed to close the modal
}

This kind of control is useful for building modular components.

Proposed Solution

The required change is minimal:

  1. Add support for a third parameter in $dispatch for passing native CustomEvent options (like bubbles, etc.).
  2. Return the CustomEvent instance from $dispatch so developers can inspect and control event behavior.

Benefits

  • Prevents event conflicts across nested or reused components.
  • Enables advanced workflows like conditional UI actions based on event handling.
  • Maintains backward compatibility while giving developers more flexibility and control.

@calebporzio

Copy link
Copy Markdown
Collaborator

PR Review: #4608 — Add support for custom options and cancelable $dispatch

Type: Feature
Verdict: Needs discussion

What's happening (plain English)

This PR makes two changes to $dispatch, both in packages/alpinejs/src/utils/dispatch.js:

Change 1 — Return the result. Currently $dispatch fires the event and returns nothing (undefined). This PR adds return so it returns what el.dispatchEvent() returns: true if no handler called preventDefault(), false if one did. This enables a pattern like:

// Before: impossible — $dispatch returns undefined (always falsy)
// After: works — returns false only if a handler called preventDefault()
if ($dispatch('close')) {
    open = false
}

Change 2 — Options parameter. Adds a third parameter to $dispatch that gets spread into the CustomEvent constructor, letting you override defaults like bubbles:

// Event won't bubble to parent elements
$dispatch('update', detail, { bubbles: false })

The implementation is just two lines changed in the source:

// Before
export function dispatch(el, name, detail = {}) {
    el.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true, cancelable: true }))
}

// After
export function dispatch(el, name, detail = {}, options = {}) {
    return el.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true, cancelable: true, ...options }))
}

Other approaches considered

  1. Return the event object instead of the boolean — Could do let e = new CustomEvent(...); el.dispatchEvent(e); return e so users get the full event (check e.defaultPrevented, read modified detail, etc.). More powerful but the boolean is cleaner for the if ($dispatch(...)) pattern and matches what dispatchEvent() natively returns.

  2. Split into two separate PRs — The return value and the options parameter are independent features. The return value is zero-risk and universally useful. The options parameter adds API surface. Could merge the return value alone and debate the options separately.

  3. Use a single options object — Instead of $dispatch(name, detail, options), combine into $dispatch(name, { detail, bubbles }). This would be a breaking change and less ergonomic for the common case where you just pass detail.

Changes Made

No changes made. The code is clean and follows Alpine conventions.

Test Results

All 4 tests pass:

  • $dispatch dispatches events properly ✓ (pre-existing)
  • $dispatch with bubbles: false shouldn't bubble (parent listener)
  • $dispatch with bubbles: false shouldn't bubble (same element listener)
  • $dispatch cancelable via preventDefault()

Regression verified: Reverted dispatch.js to main, rebuilt, ran tests — the bubbles-parent and cancelable tests both fail without the fix. CI also passes.

Code Review

  • packages/alpinejs/src/utils/dispatch.js:2-11 — The implementation is as minimal as it can be. Two lines changed. The ...options spread at the end correctly overrides the defaults.
  • packages/alpinejs/src/magics/$dispatch.js:4 — The existing .bind(dispatch, el) correctly passes through all additional arguments, so the third options param reaches the dispatch function as expected.
  • Potential footgun: If a user passes {detail: something} in the options object, it would override the detail positional parameter. This is unlikely in practice and is consistent with how spread overrides work, but worth noting.
  • Docs are included and well-written.
  • 5 thumbs-up reactions on the PR show community demand.

Security

No security concerns identified. The options parameter only affects CustomEvent constructor options (bubbles, composed, cancelable) — no new attack surface.

Verdict

This is two features in one PR. Here's my take on each:

The return addition (returning dispatchEvent result): Merge this. It's zero-risk, backward-compatible (previously returned undefined, now returns a boolean), and enables useful conditional event patterns. This is the kind of "why wasn't it always like this?" improvement.

The options parameter: This is the one that needs your call, Caleb. It adds public API surface to $dispatch — a third parameter that most users will never need. The use case (preventing bubbling in nested same-name component events) is real, but the question is whether Alpine should solve it at the $dispatch level or if users should use unique event names / .self modifier / other existing patterns.

If you want to merge just the return value and skip the options parameter for now, I can strip it out. If you're comfortable with both, the implementation is clean and ready to go.


Reviewed by Claude

@calebporzio
calebporzio merged commit ce82dee into alpinejs:main Feb 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants