Repository navigation
fix(blaze): restore the native event's currentTarget after a native-backend handler runs - #522
Conversation
…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>
dupontbertrand
left a comment
There was a problem hiding this comment.
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.
|
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>
Fixes #520
The native (no-jQuery) backend shows handlers the matched element by defining an own
currentTargetproperty on the native event (createWrapper), and never removed it. Every later listener of the same dispatch, e.g. adocument-leveladdEventListener, then read the element the Blaze handler had matched instead of its owncurrentTarget. The event also kept reporting that element after dispatch, where the native getter returnsnull. In jQuery mode handlers receive ajQuery.Eventand 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 afinally, so the nativeEvent.prototype.currentTargetgetter is visible again even if the handler throws.createWrappernow calls the handler through it. Onrelease-3.1.0that covers both delegated and capture-mode handlers, since the nativebindEventCaptureralso usescreateWrapper.Notes:
I added two regression tests at the end of
spacebars-tests/template_tests.js:'click .card'handler plus adocumentlistener. The handler must see.card, thedocumentlistener must seedocument, andcurrentTargetmust benullafter the dispatch.'mouseenter .card'.currentTargetmust benullafter 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
deleteon the configurable own property restores the prototype getter: inside a later listener it returns that listener's element, and after dispatch it returnsnull.I ran
blazeandspacebars-testslocally 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 definescurrentTargetthe same way and needs the same treatment: itsObject.defineProperty(...)+handler.call(elem, event)becomeswithCurrentTarget(event, event.target, () => handler.call(elem, event)). The nested-matches PR does the same per level in its dispatcher. I kept this PR based onrelease-3.1.0so 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 tocreateCaptureWrapperas a follow-up commit. The nested-matches PR then rebases onto both and wraps each level inwithCurrentTarget. 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