Skip to content

Actions do not execute in left-to-right order if options differ #806

@zombiezen

Description

@zombiezen

As of 2024-12-31, the Stimulus reference for Actions says:

When an element has more than one action for the same event, Stimulus invokes the actions from left to right in the order that their descriptors appear.

This isn't entirely accurate. If you examine the dispatcher, a separate event listener is registered for each unique sequence of options (presumably to support the options to addEventListener):

const cacheKey = this.cacheKey(eventName, eventOptions)
let eventListener = eventListenerMap.get(cacheKey)
if (!eventListener) {
eventListener = this.createEventListener(eventTarget, eventName, eventOptions)
eventListenerMap.set(cacheKey, eventListener)
}

(where cacheKey is defined like so:)

private cacheKey(eventName: string, eventOptions: any): string {
const parts = [eventName]
Object.keys(eventOptions)
.sort()
.forEach((key) => {
parts.push(`${eventOptions[key] ? "" : "!"}${key}`)
})
return parts.join(":")
}

Only bindings within a single event listener will be ordered; everything else is undefined:

get bindings(): Binding[] {
return Array.from(this.unorderedBindings).sort((left, right) => {
const leftIndex = left.index,
rightIndex = right.index
return leftIndex < rightIndex ? -1 : leftIndex > rightIndex ? 1 : 0
})
}

This may be the expected for capture, once, and passive, but I would not have expected this for stop, prevent, self, or my custom options.

I suggest two possible resolutions:

  1. Rework the internals of dispatcher so that only capture, once, and passive are part of the cache key. IMO this matches what most users would expect of the library.
  2. Document this behavior without changing the code. I would find this unfortunate and reduces the utility of the action options, but is definitely less work and has less compatibility implications.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions