Skip to content

Native (no-jQuery) event backend silently drops delegated events: scope heuristic compares view.name strings, not view identity #512

Description

@dupontbertrand

Affected: blaze@3.1.0-alpha.0 / release-3.1.0 only, and only when jQuery is absent (native event backend). Introduced in #497 (fb5dbe3b), which exists only on release-3.1.0. Stable 3.0.x is not affected — it has no native backend at all (dombackend.js throws "jQuery not found" when jQuery is missing), and the jQuery backend is unchanged. In other words: a pre-release regression caught during the 3.1.0-alpha test window, fixable before 3.1.0 final. Reporting it as part of the alpha testing call.

Symptom

A delegated event handler declared by an included template never fires — no error, no warning. Found in a real app running 3.1.0-alpha.0 without jQuery: adding a plain <div data-tour="…"> wrapper around a {{> pager}} inclusion silently killed the pager clicks (caught by a pre-existing behavioural test).

Minimal shape (case C of the verified matrix below):

<template name="Parent">
  {{#if show}}
    <div class="wrapper">{{> Child}}</div>   <!-- the wrapper is what breaks it -->
  {{/if}}
</template>

<template name="Child">
  {{#if ready}}
    <nav><button type="button" class="js-hit">click me</button></nav>
  {{/if}}
</template>
Template.Parent.helpers({ show: () => true });
Template.Child.helpers({ ready: () => true });
Template.Child.events({
  'click .js-hit'() { console.log('never runs on the native backend'); },
});

Remove the wrapper (or use the jQuery backend) and the handler fires.

Root cause

The native delegation wrapper (dombackend.js, createWrapper) has an "in-scope" heuristic: starting from event.target, it walks up while the selector matches, then compares $blaze_range.view.name on the stop node vs. on the delegation root, and silently drops the event when the two name strings are equal:

let node = origin;
while (node && node !== elem && node instanceof Element && node.matches(selector)) {
    node = node.parentElement;
}
const root = elem?.['$blaze_range']?.view?.name;
const scope = node?.['$blaze_range']?.view?.name;
let inScope = true;
if (root && scope && root === scope) {
    inScope = false;
}

Two independent problems compound:

  1. domrange.js _memberIn tags only direct element members of a range with $blaze_range, so the delegation root (range.parentElement of the handler-owning template, cf. _addEventMap) is tagged whenever the inclusion is wrapped by an element sitting at the top level of any block or template — and block helper views are all named generically ('if', 'unless', 'each', 'with', 'let'builtins.js). Same name ⇒ dropped, across completely unrelated views: in the snippet above the wrapper is tagged 'if' by the parent's block, the <nav> is tagged 'if' by the child's block, 'if' === 'if' → the click is discarded before the child's handler runs.
  2. In the degenerate case where the clicked element is a direct child of the delegation root, the walk stops on the root itself, so both names are read off the same element — the event is dropped regardless of any name collision, for any tagged wrapper (no enclosing {{#if}} needed at all).

Verified matrix

Self-testing repro app (8 Parent/Child pairs rendered into neutral untagged containers, programmatic clicks on .js-hit, native backend, release-3.1.0 packages — happy to publish the repro repo if useful):

# Setup Result
A {{#if}}<div>{{> Child}}</div>{{/if}}, Child = {{#if}}<button> dropped (degenerate: stop node = root)
B same, but wrapper at Parent's top level, no {{#if}} dropped (outer block not even needed)
C wrapper in {{#if}}, Child = {{#if}}<nav><button></nav> dropped ('if' === 'if' across templates)
D control for C: wrapper at Parent's top level delivered ('Template.X''if')
E no wrapper: {{#if}}{{> Child}}{{/if}} delivered (root untagged)
F same as A, click lands on a <span> inside the button delivered (target doesn't match selector — heuristic bypassed)
G same as C, outer block {{#unless}} delivered ('unless''if' — it's a name collision, not structure)
H control: handler owned by the button's own template delivered

B, F and G are the telling ones: the drop doesn't depend on any "same view nested in itself" situation — it depends on which name strings happen to coincide (B/C vs. D/G) and on the exact element the click lands on (A vs. F).

Why it matters

"Wrap an inclusion for styling/anchoring" is an utterly ordinary pattern, and the failure is a silent event drop that varies with the exact click target — so it ships unnoticed unless a behavioural test covers that exact click.

Suggested fix

The identity-based scope check already exists and already runs on every delegated event: _addEventMap (view.js) calls range.containsElement(evt.currentTarget, …), which walks DOM + parentRange pointers and compares by range === this — no name strings involved. The createWrapper name heuristic looks like a strictly less precise duplicate of it, so it may be removable outright (keeping #497's delegation tests green). If some case genuinely needs a pre-filter, alternatives are comparing view/range identity or ancestry instead of view.name, or qualifying builtin block view names per call site.

For what it's worth, this was flagged as a concern during the #497 review ("relies on view.name equality … could produce false positives when two different template instances share the same name; a more robust check would compare view identity") — this issue is that false positive materializing, just with block-helper views instead of template instances.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions