Skip to content

fix(blaze): restore the native event's currentTarget after a native-backend handler runs - #522

Open
drone1 wants to merge 2 commits into
meteor:fix/native-jquery-delegationfrom
drone1:fix/native-event-restore-currenttarget
Open

drone1 wants to merge 2 commits into
meteor:fix/native-jquery-delegationfrom
drone1:fix/native-event-restore-currenttarget

Conversation

@drone1

@drone1 drone1 commented Oct 10, 2026

Copy link
Copy Markdown

Fixes #520

The native (no-jQuery) backend shows handlers the matched element by defining an own currentTarget property on the native event (createWrapper), and never removed it. Every later listener of the same dispatch, e.g. a document-level addEventListener, then read the element the Blaze handler had matched instead of its own currentTarget. The event also kept reporting that element after dispatch, where the native getter returns null. In jQuery mode handlers receive a jQuery.Event and the native event is never modified.

This PR adds a small withCurrentTarget(event, currentTarget, callback) helper. It defines the own property, runs the callback, and deletes the property again in a finally, so the native Event.prototype.currentTarget getter is visible again even if the handler throws. createWrapper now calls the handler through it. On release-3.1.0 that covers both delegated and capture-mode handlers, since the native bindEventCapturer also uses createWrapper.

Notes:

  • I added two regression tests at the end of spacebars-tests/template_tests.js:

    • A 'click .card' handler plus a document listener. The handler must see .card, the document listener must see document, and currentTarget must be null after the dispatch.
    • A capture-mode 'mouseenter .card'. currentTarget must be null after the dispatch.

    Both fail on the native backend before the fix, both pass after, and both pass on the jQuery backend.

  • I verified in Chromium that delete on the configurable own property restores the prototype getter: inside a later listener it returns that listener's element, and after dispatch it returns null.

  • I ran blaze and spacebars-tests locally with the CI method (meteor test-packages --driver-package test-in-console, driven headless in Chromium). Before: native 347/349. After: native 349/349, jQuery (--extra-packages=jquery) 353/353.

  • Interaction with fix(blaze): only fire capturing event handlers for a matching target #518 and the nested-matches PR (fix(blaze): run native delegated handlers for every nested match, innermost element first, like jQuery #521). fix(blaze): only fire capturing event handlers for a matching target #518 gives capture mode its own createCaptureWrapper. That wrapper defines currentTarget the same way and needs the same treatment: its Object.defineProperty(...) + handler.call(elem, event) becomes withCurrentTarget(event, event.target, () => handler.call(elem, event)). The nested-matches PR does the same per level in its dispatcher. I kept this PR based on release-3.1.0 so it stands alone and fixes the leak for both handler kinds today. I suggest landing fix(blaze): only fire capturing event handlers for a matching target #518 first and then rebasing this PR onto it, which adds that one-line change to createCaptureWrapper as a follow-up commit. The nested-matches PR then rebases onto both and wraps each level in withCurrentTarget. I have that combination on a local branch with conflicts resolved (native 355/355, jQuery 359/359), so I can push whichever rebase is wanted.

🤖 Generated with Claude Code

…runs

The native (no-jQuery) backend shows handlers the matched element as
currentTarget by defining an own currentTarget property on the native
event, and never removed it. Every later listener of the same dispatch,
e.g. a document-level addEventListener, then read the element the Blaze
handler matched instead of its own currentTarget, and the event kept
reporting that element after dispatch, where the native getter returns
null. In jQuery mode handlers receive a jQuery event object and the native
event is never modified.

Delete the own property once the handler returns (also when it throws), so
the native getter is visible again. This covers both delegated and
capturing handlers, which share createWrapper.

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. Restoring currentTarget for the other listeners of the dispatch is right, but deleting it also takes it away from the Blaze handler once the dispatch is over. That breaks a common Meteor 3 pattern, an async handler that reads evt.currentTarget after an await:

Template.Card.events({
  async 'click .card'(evt) {
    await Meteor.callAsync('cards.open');
    evt.currentTarget.classList.add('open');
  },
});

In jQuery mode the handler receives a jQuery.Event, whose currentTarget is a plain field that jQuery never resets, so it still holds .card after the await. In native mode the handler receives the native event itself. Measured with a probe on Meteor 3.5.3:

evt.currentTarget after the await native jQuery
release-3.1.0 (beta.0) .card .card
this PR null .card

In native mode the handler's event and the native event are the same object, so both expectations can't hold. A getter keeps the two cases that matter: the handler sees the matched element, also after an await once eventPhase is NONE, and the other listeners of the same dispatch see their own currentTarget:

const nativeCurrentTarget =
    Object.getOwnPropertyDescriptor(Event.prototype, 'currentTarget').get;

const withCurrentTarget = (event, currentTarget, callback) => {
    let inHandler = true;
    Object.defineProperty(event, 'currentTarget', {
        configurable: true,
        get() {
            if (inHandler || this.eventPhase === Event.NONE) return currentTarget;
            return nativeCurrentTarget.call(this);
        },
    });
    try {
        return callback();
    } finally {
        inHandler = false;
    }
};

With it, the document listener in your test still sees document, and the async probe sees .card on both backends. The only change is your two final evt.currentTarget === null assertions: after the dispatch, the native event now reports the matched element, like the jQuery.Event the handlers get in jQuery mode. Full suite on Meteor 3.5.3 with my two probes added: jQuery 428/428, and native passes everything except those two assertions.

I'd rather keep async handlers working than keep null on the native event after the dispatch, which code outside Blaze rarely reads. What do you think?

Verified otherwise: this PR passes native 422/422 and jQuery 426/426, and both new tests fail against the beta's dombackend.js. Same results on Meteor 3.6-rc.0, including the null after the await.

@drone1

drone1 commented Oct 17, 2026

Copy link
Copy Markdown
Author

def should keep async handlers working. good catch. i'll have a look.

Deleting the own currentTarget once the handler returned also took it
away from the handler itself: an async handler that read
evt.currentTarget after an `await` got null on the native backend, while
the jQuery.Event it receives in jQuery mode keeps the matched element.

The handler's event and the native event are the same object here, so
currentTarget is now an own getter instead: the matched element while
the handler runs and once the dispatch is over (eventPhase NONE), and
the native value in between, so later listeners of the same dispatch
still see their own currentTarget. The native getter is looked up on the
event's own prototype chain, so events from a same-origin iframe use
their realm's getter.

The two existing tests now expect the matched element after the
dispatch, and a new test reads evt.currentTarget after an awaited timer.

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

Copy link
Copy Markdown

Thanks, bd237ea looks right, and looking the native getter up on the event's own prototype chain for iframe events is a good catch. On Meteor 3.5.3: native 423/423, jQuery 427/427, and the three new or updated tests fail against the previous dombackend.js (68da4ce). LGTM.

@jankapunkt jankapunkt added this to the 3.1 milestone Oct 18, 2026
@jankapunkt
jankapunkt changed the base branch from release-3.1.0 to fix/native-jquery-delegation October 19, 2026 08:35

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