Fix: Actions with options may run out of order #684
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Problem
I was exploring implementing a new action option -
:stopImmediate
- and in working on that, I ran across what I believe is a bug in action ordering when options are provided.Consider the following, assuming there is a controller called
a
:Then let's say I modify the element with JS dynamically to have a second action like this:
When I run these actions, I would expect them to run in left-to-right order, as specified in the docs. However, that doesn't happen - the actions run in
secondAction, firstAction
order. Technically, thestop
actually happens in between thesecondAction
andfirstAction
, since it runs before the action it modifies.Cause
A single event handler is attached on an element for each event type + action option(s) combination. This makes sense for options which are passed to the native DOM handlers - we need these to be separate event handlers under the hood, as they are constructed differently.
For both the "standard" Stimulus set (currently
:stop
,:prevent
,:self
) and the user-defined option set added viaregisterActionOption()
, the underlying event handler could be the same as in actions without these options.Proposed Solution
I modified the
Dispatcher#cacheKey
method to only create separate keys (and therefore, separate event handlers) when using the 4 action options which are passed to the nativeaddEventListener
. This lets us run more (but, as mentioned below, not all) actions in the correct sequence.Caveats
This solution does not fix the issue if I replace the
:stop
in my example above with:once
- any option handled natively by the browser could still run in an unexpected order. I still believe this is valuable because it patches several (most?) usages (and opens the way for a:stopImmediate
action option that stops further processing as expected, without having to callevent.stopImmediatePropagation()
in the action).The more general case is much harder to fix. We could maintain a single event listener per event type (ignoring action options), and implement the
:once
ourselves - but that doesn't help with:passive
(though I suppose it'd be unusual to attach a:passive
handler alongside something that's not:passive
for the same event, that would seem to defeat the point).Maybe we could track which bindings have already executed for a given event, but that's its own can of worms.
Or, we could move towards a separation of native browser event attributes and Stimulus- or user-implemented actions. Perhaps putting browser-driven modifiers next to the event type itself could help since they apply at a deeper level - ex.
keydown:once->a#firstAction
)Then, the section after the
->
could support a larger set of options / filters. Something likekeydown:once->beforeOption:a#firstAction:stop
. (I personally find it unintuitive thata#firstAction:stop
means thatstop
runs beforefirstAction
)Anyway - that's all a much larger discussion that may be out of scope here. I do think this patch is worthwhile for making more of the action + option combos run in the expected order today.