Skip to content

Commit

Permalink
fix(core): global listeners not being bound on non-node host elements (
Browse files Browse the repository at this point in the history
…#42014)

We skip event listeners on non-element host nodes (e.g. `ng-container` or `ng-element`), because they don't map to a DOM node so there's nothing to bind the event to. The problem is that this also prevents listeners bound to global targets from being bound.

These changes add an extra condition to allow for the event to be bound if it has a custom event target resolver.

Fixes #14191.

PR Close #42014
  • Loading branch information
crisbeto authored and zarend committed May 14, 2021
1 parent 2e7eb27 commit 4bc5b4d
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 3 deletions.
7 changes: 5 additions & 2 deletions packages/core/src/render3/instructions/listener.ts
Expand Up @@ -131,8 +131,11 @@ function listenerInternal(

let processOutputs = true;

// add native event listener - applicable to elements only
if (tNode.type & TNodeType.AnyRNode) {
// Adding a native event listener is applicable when:
// - The corresponding TNode represents a DOM element.
// - The event target has a resolver (usually resulting in a global object,
// such as `window` or `document`).
if ((tNode.type & TNodeType.AnyRNode) || eventTargetResolver) {
const native = getNativeByTNode(tNode, lView) as RElement;
const target = eventTargetResolver ? eventTargetResolver(native) : native;
const lCleanupIndex = lCleanup.length;
Expand Down
105 changes: 104 additions & 1 deletion packages/core/test/acceptance/listener_spec.ts
Expand Up @@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Component, Directive, ErrorHandler, EventEmitter, HostListener, Input, Output, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {CommonModule} from '@angular/common';
import {Component, Directive, ErrorHandler, EventEmitter, HostListener, Input, OnInit, Output, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {onlyInIvy} from '@angular/private/testing';
Expand Down Expand Up @@ -397,6 +398,108 @@ describe('event listeners', () => {
expect(comp.counter).toBe(1);
});

onlyInIvy('global event listeners on non-node host elements are supported only in Ivy')
.it('should bind global event listeners on an ng-container directive host', () => {
let clicks = 0;

@Directive({selector: '[add-global-listener]'})
class AddGlobalListener {
@HostListener('document:click')
handleClick() {
clicks++;
}
}

@Component({
template: `
<ng-container add-global-listener>
<button>Click me!</button>
</ng-container>
`
})
class MyComp {
}

TestBed.configureTestingModule({declarations: [MyComp, AddGlobalListener]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
expect(clicks).toBe(1);
});

onlyInIvy('global event listeners on non-node host elements are supported only in Ivy')
.it('should bind global event listeners on an ng-template directive host', () => {
let clicks = 0;

@Directive({selector: '[add-global-listener]'})
class AddGlobalListener {
@HostListener('document:click')
handleClick() {
clicks++;
}
}

@Component({
template: `
<ng-template #template add-global-listener>
<button>Click me!</button>
</ng-template>
<ng-container [ngTemplateOutlet]="template"></ng-container>
`
})
class MyComp {
}

TestBed.configureTestingModule(
{declarations: [MyComp, AddGlobalListener], imports: [CommonModule]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
expect(clicks).toBe(1);
});

onlyInIvy('global event listeners on non-node host elements are supported only in Ivy')
.it('should bind global event listeners on a structural directive host', () => {
let clicks = 0;

@Directive({selector: '[add-global-listener]'})
class AddGlobalListener implements OnInit {
@HostListener('document:click')
handleClick() {
clicks++;
}

constructor(private _vcr: ViewContainerRef, private _templateRef: TemplateRef<any>) {}

ngOnInit() {
this._vcr.createEmbeddedView(this._templateRef);
}
}

@Component({
template: `
<div *add-global-listener>
<button>Click me!</button>
</div>
`
})
class MyComp {
}

TestBed.configureTestingModule({declarations: [MyComp, AddGlobalListener]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
expect(clicks).toBe(1);
});

onlyInIvy('issue has only been resolved for Ivy')
.it('should be able to access a property called $event using `this`', () => {
let eventVariable: number|undefined;
Expand Down

0 comments on commit 4bc5b4d

Please sign in to comment.