Skip to content

Commit 184d270

Browse files
crisbetoalxhub
authored andcommitted
fix(ivy): DebugElement.triggerEventHandler not picking up events registered via Renderer2 (angular#31845)
Fixes Ivy's `DebugElement.triggerEventHandler` to picking up events that have been registered through a `Renderer2`, unlike ViewEngine. This PR resolves FW-1480. PR Close angular#31845
1 parent a610d12 commit 184d270

File tree

5 files changed

+99
-5
lines changed

5 files changed

+99
-5
lines changed

packages/core/src/debug/debug_node.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,28 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
377377
}
378378

379379
triggerEventHandler(eventName: string, eventObj: any): void {
380-
this.listeners.forEach((listener) => {
380+
const node = this.nativeNode as any;
381+
const invokedListeners: Function[] = [];
382+
383+
this.listeners.forEach(listener => {
381384
if (listener.name === eventName) {
382-
listener.callback(eventObj);
385+
const callback = listener.callback;
386+
callback(eventObj);
387+
invokedListeners.push(callback);
383388
}
384389
});
390+
391+
// We need to check whether `eventListeners` exists, because it's something
392+
// that Zone.js only adds to `EventTarget` in browser environments.
393+
if (typeof node.eventListeners === 'function') {
394+
// Note that in Ivy we wrap event listeners with a call to `event.preventDefault` in some
395+
// cases. We use `Function` as a special token that gives us access to the actual event
396+
// listener.
397+
node.eventListeners(eventName).forEach((listener: Function) => {
398+
const unwrappedListener = listener(Function);
399+
return invokedListeners.indexOf(unwrappedListener) === -1 && unwrappedListener(eventObj);
400+
});
401+
}
385402
}
386403
}
387404

packages/core/src/render3/instructions/listener.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,13 @@ function wrapListener(
235235
wrapWithPreventDefault: boolean): EventListener {
236236
// Note: we are performing most of the work in the listener function itself
237237
// to optimize listener registration.
238-
return function wrapListenerIn_markDirtyAndPreventDefault(e: Event) {
238+
return function wrapListenerIn_markDirtyAndPreventDefault(e: any) {
239+
// Ivy uses `Function` as a special token that allows us to unwrap the function
240+
// so that it can be invoked programmatically by `DebugNode.triggerEventHandler`.
241+
if (e === Function) {
242+
return listenerFn;
243+
}
244+
239245
// In order to be backwards compatible with View Engine, events on component host nodes
240246
// must also mark the component view itself dirty (i.e. the view that it owns).
241247
const startView =

packages/core/test/debug/debug_node_spec.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88

99

1010
import {CommonModule, NgIfContext} from '@angular/common';
11-
import {Component, DebugNode, Directive, ElementRef, EmbeddedViewRef, EventEmitter, HostBinding, Injectable, Input, NO_ERRORS_SCHEMA, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
11+
import {Component, DebugNode, Directive, ElementRef, EmbeddedViewRef, EventEmitter, HostBinding, Injectable, Input, NO_ERRORS_SCHEMA, OnInit, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
1212
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
1313
import {By} from '@angular/platform-browser/src/dom/debug/by';
1414
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
1515
import {expect} from '@angular/platform-browser/testing/src/matchers';
16+
import {ivyEnabled} from '@angular/private/testing';
1617

1718
@Injectable()
1819
class Logger {
@@ -653,7 +654,6 @@ class TestCmptWithPropBindings {
653654

654655
fixture.debugElement.children[1].triggerEventHandler('myevent', <Event>{});
655656
expect(fixture.componentInstance.customed).toBe(true);
656-
657657
});
658658

659659
it('should include classes in properties.className', () => {
@@ -683,6 +683,61 @@ class TestCmptWithPropBindings {
683683
expect(button.properties).toEqual({disabled: true, tabIndex: 1337, title: 'hello'});
684684
});
685685

686+
it('should trigger events registered via Renderer2', () => {
687+
@Component({template: ''})
688+
class TestComponent implements OnInit {
689+
count = 0;
690+
eventObj: any;
691+
constructor(private renderer: Renderer2, private elementRef: ElementRef) {}
692+
693+
ngOnInit() {
694+
this.renderer.listen(this.elementRef.nativeElement, 'click', (event: any) => {
695+
this.count++;
696+
this.eventObj = event;
697+
});
698+
}
699+
}
700+
701+
TestBed.configureTestingModule({declarations: [TestComponent]});
702+
const fixture = TestBed.createComponent(TestComponent);
703+
704+
// Ivy depends on `eventListeners` to pick up events that haven't been registered through
705+
// Angular templates. At the time of writing Zone.js doesn't add `eventListeners` in Node
706+
// environments so we have to skip the test.
707+
if (!ivyEnabled || typeof fixture.debugElement.nativeElement.eventListeners === 'function') {
708+
const event = {value: true};
709+
fixture.detectChanges();
710+
fixture.debugElement.triggerEventHandler('click', event);
711+
expect(fixture.componentInstance.count).toBe(1);
712+
expect(fixture.componentInstance.eventObj).toBe(event);
713+
}
714+
});
715+
716+
it('should be able to trigger an event with a null value', () => {
717+
let value = undefined;
718+
719+
@Component({template: '<button (click)="handleClick($event)"></button>'})
720+
class TestComponent {
721+
handleClick(_event: any) {
722+
value = _event;
723+
724+
// Returning `false` is what causes the renderer to call `event.preventDefault`.
725+
return false;
726+
}
727+
}
728+
729+
TestBed.configureTestingModule({declarations: [TestComponent]});
730+
const fixture = TestBed.createComponent(TestComponent);
731+
const button = fixture.debugElement.query(By.css('button'));
732+
733+
expect(() => {
734+
button.triggerEventHandler('click', null);
735+
fixture.detectChanges();
736+
}).not.toThrow();
737+
738+
expect(value).toBeNull();
739+
});
740+
686741
describe('componentInstance on DebugNode', () => {
687742

688743
it('should return component associated with a node if a node is a component host', () => {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,20 @@ export function flattenStyles(
4949

5050
function decoratePreventDefault(eventHandler: Function): Function {
5151
return (event: any) => {
52+
// Ivy uses `Function` as a special token that allows us to unwrap the function
53+
// so that it can be invoked programmatically by `DebugNode.triggerEventHandler`.
54+
if (event === Function) {
55+
return eventHandler;
56+
}
57+
5258
const allowDefaultBehavior = eventHandler(event);
5359
if (allowDefaultBehavior === false) {
5460
// TODO(tbosch): move preventDefault into event plugins...
5561
event.preventDefault();
5662
event.returnValue = false;
5763
}
64+
65+
return undefined;
5866
};
5967
}
6068

packages/platform-server/src/server_renderer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,21 @@ class DefaultServerRenderer2 implements Renderer2 {
182182

183183
private decoratePreventDefault(eventHandler: Function): Function {
184184
return (event: any) => {
185+
// Ivy uses `Function` as a special token that allows us to unwrap the function
186+
// so that it can be invoked programmatically by `DebugNode.triggerEventHandler`.
187+
if (event === Function) {
188+
return eventHandler;
189+
}
190+
185191
// Run the event handler inside the ngZone because event handlers are not patched
186192
// by Zone on the server. This is required only for tests.
187193
const allowDefaultBehavior = this.ngZone.runGuarded(() => eventHandler(event));
188194
if (allowDefaultBehavior === false) {
189195
event.preventDefault();
190196
event.returnValue = false;
191197
}
198+
199+
return undefined;
192200
};
193201
}
194202
}

0 commit comments

Comments
 (0)