Skip to content

Commit 32b72f3

Browse files
atscottAndrewKushnir
authored andcommitted
fix(ivy): ensure eventListeners added outside angular context are not called... (angular#34514)
by DebugElement.triggerEventHandler. ZoneJS tracks the eventListeners on a node but we need to be able to differentiate between those added by Angular and those that were added outside the Angular context. This fix aligns with the behavior that was present in View Engine (not calling those listeners). If we decide later that we want to call those listeners, we still need a way to differentiate between those that we have wrapped in dom_renderer and those that were not (because they were added outside the Angular context). PR Close angular#34514
1 parent d15cf60 commit 32b72f3

File tree

3 files changed

+42
-8
lines changed

3 files changed

+42
-8
lines changed

packages/core/src/debug/debug_node.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
410410
this.listeners.forEach(listener => {
411411
if (listener.name === eventName) {
412412
const callback = listener.callback;
413-
callback(eventObj);
413+
callback.call(node, eventObj);
414414
invokedListeners.push(callback);
415415
}
416416
});
@@ -419,11 +419,20 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
419419
// that Zone.js only adds to `EventTarget` in browser environments.
420420
if (typeof node.eventListeners === 'function') {
421421
// Note that in Ivy we wrap event listeners with a call to `event.preventDefault` in some
422-
// cases. We use `Function` as a special token that gives us access to the actual event
422+
// cases. We use '__ngUnwrap__' as a special token that gives us access to the actual event
423423
// listener.
424424
node.eventListeners(eventName).forEach((listener: Function) => {
425-
const unwrappedListener = listener(Function);
426-
return invokedListeners.indexOf(unwrappedListener) === -1 && unwrappedListener(eventObj);
425+
// In order to ensure that we can detect the special __ngUnwrap__ token described above, we
426+
// use `toString` on the listener and see if it contains the token. We use this approach to
427+
// ensure that it still worked with compiled code since it cannot remove or rename string
428+
// literals. We also considered using a special function name (i.e. if(listener.name ===
429+
// special)) but that was more cumbersome and we were also concerned the compiled code could
430+
// strip the name, turning the condition in to ("" === "") and always returning true.
431+
if (listener.toString().indexOf('__ngUnwrap__') !== -1) {
432+
const unwrappedListener = listener('__ngUnwrap__');
433+
return invokedListeners.indexOf(unwrappedListener) === -1 &&
434+
unwrappedListener.call(node, eventObj);
435+
}
427436
});
428437
}
429438
}

packages/core/test/debug/debug_node_spec.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {Component, DebugElement, DebugNode, Directive, ElementRef, EmbeddedViewR
1212
import {NgZone} from '@angular/core/src/zone';
1313
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
1414
import {By} from '@angular/platform-browser/src/dom/debug/by';
15-
import {hasClass} from '@angular/platform-browser/testing/src/browser_util';
15+
import {createMouseEvent, hasClass} from '@angular/platform-browser/testing/src/browser_util';
1616
import {expect} from '@angular/platform-browser/testing/src/matchers';
1717
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
1818

@@ -1253,4 +1253,23 @@ class TestCmptWithPropInterpolation {
12531253
expect(fixture.debugElement.query(e => e.name === 'myComponent')).toBeTruthy();
12541254
expect(fixture.debugElement.query(e => e.name === 'div')).toBeTruthy();
12551255
});
1256+
1257+
it('does not call event listeners added outside angular context', () => {
1258+
let listenerCalled = false;
1259+
const eventToTrigger = createMouseEvent('mouseenter');
1260+
function listener() { listenerCalled = true; }
1261+
@Component({template: ''})
1262+
class MyComp {
1263+
constructor(private readonly zone: NgZone, private readonly element: ElementRef) {}
1264+
ngOnInit() {
1265+
this.zone.runOutsideAngular(
1266+
() => { this.element.nativeElement.addEventListener('mouseenter', listener); });
1267+
}
1268+
}
1269+
const fixture =
1270+
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
1271+
fixture.detectChanges();
1272+
fixture.debugElement.triggerEventHandler('mouseenter', eventToTrigger);
1273+
expect(listenerCalled).toBe(false);
1274+
});
12561275
}

packages/platform-browser/src/dom/dom_renderer.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,16 @@ export function flattenStyles(
5050
}
5151

5252
function decoratePreventDefault(eventHandler: Function): Function {
53+
// `DebugNode.triggerEventHandler` needs to know if the listener was created with
54+
// decoratePreventDefault or is a listener added outside the Angular context so it can handle the
55+
// two differently. In the first case, the special '__ngUnwrap__' token is passed to the unwrap
56+
// the listener (see below).
5357
return (event: any) => {
54-
// Ivy uses `Function` as a special token that allows us to unwrap the function
55-
// so that it can be invoked programmatically by `DebugNode.triggerEventHandler`.
56-
if (event === Function) {
58+
// Ivy uses '__ngUnwrap__' as a special token that allows us to unwrap the function
59+
// so that it can be invoked programmatically by `DebugNode.triggerEventHandler`. The debug_node
60+
// can inspect the listener toString contents for the existence of this special token. Because
61+
// the token is a string literal, it is ensured to not be modified by compiled code.
62+
if (event === '__ngUnwrap__') {
5763
return eventHandler;
5864
}
5965

0 commit comments

Comments
 (0)