Skip to content

fix(blaze): only fire capturing event handlers for a matching target - #518

Open
drone1 wants to merge 1 commit into
meteor:fix/native-jquery-delegationfrom
drone1:fix/native-capture-exact-target-match
Open

drone1 wants to merge 1 commit into
meteor:fix/native-jquery-delegationfrom
drone1:fix/native-capture-exact-target-match

Conversation

@drone1

@drone1 drone1 commented Oct 10, 2026

Copy link
Copy Markdown

Fixes #517.

The native (no-jQuery) bindEventCapturer reused createWrapper, the bubbling delegation wrapper, which walks from event.target up to the closest ancestor matching the selector. Blaze uses capturing for non-bubbling events such as mouseenter/mouseleave, which the browser dispatches separately to every element the pointer crosses. Walking up therefore ran a 'mouseleave .card' handler, with currentTarget set to .card, when the pointer only left a child of .card (and 'mouseenter .card' when it entered one), so hover states built on these handlers would flicker or drop. Found while migrating a production app (Meteor 3.5.1, blaze@3.1.0-beta.0) off jQuery.

This PR gives the native branch its own createCaptureWrapper with the semantics of the jQuery branch ($(event.target).is($(elem).find(selector))):

  • The handler runs only when event.target itself is one of elem.querySelectorAll(scopedSelector). The :scope rooting from fix(blaze): match delegated event selectors within delegation root #513 is moved into a shared scopeSelector helper and reused.
  • currentTarget is set to the target, and the handler is called with this === elem, as in the jQuery branch.
  • Return values are ignored as in the jQuery branch. HandlerRec's capturing handler returns nothing anyway.
  • The wrapper is still stored on handler._meteorui_wrapper, so unbindEventCapturer is unchanged.

Notes:

  • I added a regression test next to the existing "capturing events" test in spacebars-tests/templating_tests.js. It dispatches non-bubbling mouseenter/mouseleave on a child of .card, on an unrelated sibling and on the delegation root, and expects none of them to run the .card handlers. Dispatching on .card itself must run both. I watched it fail before the fix (expected ["mouseenter card","mouseleave card"], actual [... ×4]).
  • I ran blaze and spacebars-tests locally with the CI method (meteor test-packages --driver-package test-in-console, driven headless in Chromium). Native: 348/348. jQuery (--extra-packages=jquery): 352/352. The new test passes on both backends, which confirms the two backends now agree.
  • Per-event querySelectorAll matches what $elem.find(selector) does per event on the jQuery path. It only runs for captured event types, which HandlerRec already filters.

🤖 Generated with Claude Code

The native (no-jQuery) bindEventCapturer reused the bubbling delegation
wrapper, which walks from event.target up to the closest ancestor matching
the selector. Capturing is what Blaze uses for non-bubbling events such as
mouseenter/mouseleave, which the browser dispatches to every element the
pointer crosses. Walking up therefore fired a 'mouseleave .card' handler,
with currentTarget set to .card, when the pointer merely left a child of
.card (and 'mouseenter .card' when it entered one), so hover states built
on these handlers flicker or drop.

Match the jQuery branch of bindEventCapturer instead: call the handler
only when event.target itself is one of $(elem).find(selector), still
evaluating the selector rooted at the delegation element.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Oct 10, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@dupontbertrand dupontbertrand left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @drone1, this matches the jQuery branch of bindEventCapturer exactly: the handler runs only when event.target itself is in $(elem).find(selector), with currentTarget = event.target and this === elem.

Verified locally on Meteor 3.5.3 with the full Blaze suite (test-in-console in headless Chromium; native mode uses a copy of test-in-console that doesn't pull in jquery, checked through Blaze._DOMBackend._hasJQuery):

native jQuery
release-3.1.0 (beta.0) 420/420 424/424
this PR 421/421 425/425
this PR's test against the beta's dombackend.js the new test fails

Same results on Meteor 3.6-rc.0.

One thing to carry over when the three PRs land: createCaptureWrapper sets currentTarget as an own property and never removes it, the leak #522 fixes in createWrapper. Whichever lands later should route it through #522's helper as well.

The CLA check is still pending on all three PRs.

@jankapunkt jankapunkt added this to the 3.1 milestone Oct 18, 2026
@jankapunkt

Copy link
Copy Markdown
Collaborator

@dupontbertrand the CLA check can only succeed, once @drone1 completes the CLA, linked in the comment

@drone1

drone1 commented Oct 18, 2026

Copy link
Copy Markdown
Author

@dupontbertrand the CLA check can only succeed, once @drone1 completes the CLA, linked in the comment

signed.

@jankapunkt
jankapunkt changed the base branch from release-3.1.0 to fix/native-jquery-delegation October 19, 2026 08:28

@jankapunkt jankapunkt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will merge this first as prposed, once the comments are converted to JSDoc function documenttion comments.

Please note I have changed the base from release-3.1.0 to fix/native-jquery-delegation which is a unaltered direct copy of release-3.1.0.

The reason is simple: all three PRs #518 #521 and #522 will be bundled into this branch while the branch itself will then finally merge into release-3.1.0 which will help us to keep a compact view of changed files of all three PRs. This is since the PRs have potential overlap and by retaining a narrow scope we might have it easier as human reviewers.

// the pointer crosses; walking up from the target to the closest match would
// fire a 'mouseleave .card' handler when the pointer merely leaves a child
// of .card.
const createCaptureWrapper = (elem, selector, handler) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please convert to a proper JSDoc comment, including parameter documentation

// element ($(elem).find(selector)): for 'div p', both the div and the p
// must live inside `elem`. A bare closest(selector) matches against the
// whole document, letting ancestors outside `elem` satisfy the selector.
const scopeSelector = (selector) => selector

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please convert to a proper JSDoc comment, including parameter documentation

This branch has not been deployed

No deployments
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.

4 participants