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:
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.
- 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.
Affected:
blaze@3.1.0-alpha.0/release-3.1.0only, and only when jQuery is absent (native event backend). Introduced in #497 (fb5dbe3b), which exists only onrelease-3.1.0. Stable 3.0.x is not affected — it has no native backend at all (dombackend.jsthrows"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.0without 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):
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 fromevent.target, it walks up while the selector matches, then compares$blaze_range.view.nameon the stop node vs. on the delegation root, and silently drops the event when the two name strings are equal:Two independent problems compound:
domrange.js_memberIntags only direct element members of a range with$blaze_range, so the delegation root (range.parentElementof 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.{{#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.0packages — happy to publish the repro repo if useful):{{#if}}<div>{{> Child}}</div>{{/if}}, Child ={{#if}}<button>{{#if}}{{#if}}, Child ={{#if}}<nav><button></nav>'if'==='if'across templates)'Template.X'≠'if'){{#if}}{{> Child}}{{/if}}<span>inside the button{{#unless}}'unless'≠'if'— it's a name collision, not structure)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) callsrange.containsElement(evt.currentTarget, …), which walks DOM +parentRangepointers and compares byrange === this— no name strings involved. ThecreateWrappername 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 ofview.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.nameequality … 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.