fix(ivy): adding event listeners for global objects (window, document, body) (#27772)
This update introduces support for global object (window, document, body) listeners, that can be defined via host listeners on Components and Directives. PR Close #27772
This commit is contained in:

committed by
Kara Erickson

parent
917c09cfc8
commit
6e7c46af1b
@ -845,52 +845,48 @@ function declareTests(config?: {useJit: boolean}) {
|
||||
dir.triggerChange('two');
|
||||
}));
|
||||
|
||||
fixmeIvy(
|
||||
'FW-743: Registering events on global objects (document, window, body) is not supported')
|
||||
.it('should support render events', () => {
|
||||
TestBed.configureTestingModule({declarations: [MyComp, DirectiveListeningDomEvent]});
|
||||
const template = '<div listener></div>';
|
||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
it('should support render events', () => {
|
||||
TestBed.configureTestingModule({declarations: [MyComp, DirectiveListeningDomEvent]});
|
||||
const template = '<div listener></div>';
|
||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
|
||||
const tc = fixture.debugElement.children[0];
|
||||
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
||||
const tc = fixture.debugElement.children[0];
|
||||
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
||||
|
||||
dispatchEvent(tc.nativeElement, 'domEvent');
|
||||
dispatchEvent(tc.nativeElement, 'domEvent');
|
||||
|
||||
expect(listener.eventTypes).toEqual([
|
||||
'domEvent', 'body_domEvent', 'document_domEvent', 'window_domEvent'
|
||||
]);
|
||||
expect(listener.eventTypes).toEqual([
|
||||
'domEvent', 'body_domEvent', 'document_domEvent', 'window_domEvent'
|
||||
]);
|
||||
|
||||
fixture.destroy();
|
||||
listener.eventTypes = [];
|
||||
dispatchEvent(tc.nativeElement, 'domEvent');
|
||||
expect(listener.eventTypes).toEqual([]);
|
||||
});
|
||||
fixture.destroy();
|
||||
listener.eventTypes = [];
|
||||
dispatchEvent(tc.nativeElement, 'domEvent');
|
||||
expect(listener.eventTypes).toEqual([]);
|
||||
});
|
||||
|
||||
fixmeIvy(
|
||||
'FW-743: Registering events on global objects (document, window, body) is not supported')
|
||||
.it('should support render global events', () => {
|
||||
TestBed.configureTestingModule({declarations: [MyComp, DirectiveListeningDomEvent]});
|
||||
const template = '<div listener></div>';
|
||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
const doc = TestBed.get(DOCUMENT);
|
||||
it('should support render global events', () => {
|
||||
TestBed.configureTestingModule({declarations: [MyComp, DirectiveListeningDomEvent]});
|
||||
const template = '<div listener></div>';
|
||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
const doc = TestBed.get(DOCUMENT);
|
||||
|
||||
const tc = fixture.debugElement.children[0];
|
||||
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||
expect(listener.eventTypes).toEqual(['window_domEvent']);
|
||||
const tc = fixture.debugElement.children[0];
|
||||
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||
expect(listener.eventTypes).toEqual(['window_domEvent']);
|
||||
|
||||
listener.eventTypes = [];
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'document'), 'domEvent');
|
||||
expect(listener.eventTypes).toEqual(['document_domEvent', 'window_domEvent']);
|
||||
listener.eventTypes = [];
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'document'), 'domEvent');
|
||||
expect(listener.eventTypes).toEqual(['document_domEvent', 'window_domEvent']);
|
||||
|
||||
fixture.destroy();
|
||||
listener.eventTypes = [];
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'body'), 'domEvent');
|
||||
expect(listener.eventTypes).toEqual([]);
|
||||
});
|
||||
fixture.destroy();
|
||||
listener.eventTypes = [];
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'body'), 'domEvent');
|
||||
expect(listener.eventTypes).toEqual([]);
|
||||
});
|
||||
|
||||
it('should support updating host element via hostAttributes on root elements', () => {
|
||||
@Component({host: {'role': 'button'}, template: ''})
|
||||
@ -1027,44 +1023,41 @@ function declareTests(config?: {useJit: boolean}) {
|
||||
});
|
||||
}
|
||||
|
||||
fixmeIvy(
|
||||
'FW-743: Registering events on global objects (document, window, body) is not supported')
|
||||
.it('should support render global events from multiple directives', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [MyComp, DirectiveListeningDomEvent, DirectiveListeningDomEventOther]
|
||||
});
|
||||
const template = '<div *ngIf="ctxBoolProp" listener listenerother></div>';
|
||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
const doc = TestBed.get(DOCUMENT);
|
||||
it('should support render global events from multiple directives', () => {
|
||||
TestBed.configureTestingModule(
|
||||
{declarations: [MyComp, DirectiveListeningDomEvent, DirectiveListeningDomEventOther]});
|
||||
const template = '<div *ngIf="ctxBoolProp" listener listenerother></div>';
|
||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
const doc = TestBed.get(DOCUMENT);
|
||||
|
||||
globalCounter = 0;
|
||||
fixture.componentInstance.ctxBoolProp = true;
|
||||
fixture.detectChanges();
|
||||
globalCounter = 0;
|
||||
fixture.componentInstance.ctxBoolProp = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const tc = fixture.debugElement.children[0];
|
||||
const tc = fixture.debugElement.children[0];
|
||||
|
||||
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
||||
const listenerother = tc.injector.get(DirectiveListeningDomEventOther);
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||
expect(listener.eventTypes).toEqual(['window_domEvent']);
|
||||
expect(listenerother.eventType).toEqual('other_domEvent');
|
||||
expect(globalCounter).toEqual(1);
|
||||
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
||||
const listenerother = tc.injector.get(DirectiveListeningDomEventOther);
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||
expect(listener.eventTypes).toEqual(['window_domEvent']);
|
||||
expect(listenerother.eventType).toEqual('other_domEvent');
|
||||
expect(globalCounter).toEqual(1);
|
||||
|
||||
|
||||
fixture.componentInstance.ctxBoolProp = false;
|
||||
fixture.detectChanges();
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||
expect(globalCounter).toEqual(1);
|
||||
fixture.componentInstance.ctxBoolProp = false;
|
||||
fixture.detectChanges();
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||
expect(globalCounter).toEqual(1);
|
||||
|
||||
fixture.componentInstance.ctxBoolProp = true;
|
||||
fixture.detectChanges();
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||
expect(globalCounter).toEqual(2);
|
||||
fixture.componentInstance.ctxBoolProp = true;
|
||||
fixture.detectChanges();
|
||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||
expect(globalCounter).toEqual(2);
|
||||
|
||||
// need to destroy to release all remaining global event listeners
|
||||
fixture.destroy();
|
||||
});
|
||||
// need to destroy to release all remaining global event listeners
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
describe('ViewContainerRef', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -6,17 +6,21 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {bind, defineComponent, defineDirective, markDirty, reference, textBinding} from '../../src/render3/index';
|
||||
import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util';
|
||||
|
||||
import {bind, defineComponent, defineDirective, markDirty, reference, resolveBody, resolveDocument, textBinding} from '../../src/render3/index';
|
||||
import {container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, getCurrentView, listener, text} from '../../src/render3/instructions';
|
||||
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
||||
import {GlobalTargetResolver} from '../../src/render3/interfaces/renderer';
|
||||
import {restoreView} from '../../src/render3/state';
|
||||
|
||||
import {getRendererFactory2} from './imported_renderer2';
|
||||
import {ComponentFixture, containerEl, createComponent, getDirectiveOnNode, renderToHtml, requestAnimationFrame} from './render_util';
|
||||
import {ComponentFixture, TemplateFixture, containerEl, createComponent, getDirectiveOnNode, renderToHtml, requestAnimationFrame} from './render_util';
|
||||
|
||||
|
||||
describe('event listeners', () => {
|
||||
let comps: MyComp[] = [];
|
||||
let comps: any[] = [];
|
||||
let events: any[] = [];
|
||||
|
||||
class MyComp {
|
||||
showing = true;
|
||||
@ -48,6 +52,67 @@ describe('event listeners', () => {
|
||||
});
|
||||
}
|
||||
|
||||
class MyCompWithGlobalListeners {
|
||||
/* @HostListener('document:custom') */
|
||||
onDocumentCustomEvent() { events.push('component - document:custom'); }
|
||||
|
||||
/* @HostListener('body:click') */
|
||||
onBodyClick() { events.push('component - body:click'); }
|
||||
|
||||
static ngComponentDef = defineComponent({
|
||||
type: MyCompWithGlobalListeners,
|
||||
selectors: [['comp']],
|
||||
consts: 1,
|
||||
vars: 0,
|
||||
template: function CompTemplate(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
text(0, 'Some text');
|
||||
}
|
||||
},
|
||||
factory: () => {
|
||||
let comp = new MyCompWithGlobalListeners();
|
||||
comps.push(comp);
|
||||
return comp;
|
||||
},
|
||||
hostBindings: function HostListenerDir_HostBindings(
|
||||
rf: RenderFlags, ctx: any, elIndex: number) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
listener('custom', function() {
|
||||
return ctx.onDocumentCustomEvent();
|
||||
}, false, resolveDocument as GlobalTargetResolver);
|
||||
listener('click', function() {
|
||||
return ctx.onBodyClick();
|
||||
}, false, resolveBody as GlobalTargetResolver);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class GlobalHostListenerDir {
|
||||
/* @HostListener('document:custom') */
|
||||
onDocumentCustomEvent() { events.push('directive - document:custom'); }
|
||||
|
||||
/* @HostListener('body:click') */
|
||||
onBodyClick() { events.push('directive - body:click'); }
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: GlobalHostListenerDir,
|
||||
selectors: [['', 'hostListenerDir', '']],
|
||||
factory: function HostListenerDir_Factory() { return new GlobalHostListenerDir(); },
|
||||
hostBindings: function HostListenerDir_HostBindings(
|
||||
rf: RenderFlags, ctx: any, elIndex: number) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
listener('custom', function() {
|
||||
return ctx.onDocumentCustomEvent();
|
||||
}, false, resolveDocument as GlobalTargetResolver);
|
||||
listener('click', function() {
|
||||
return ctx.onBodyClick();
|
||||
}, false, resolveBody as GlobalTargetResolver);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class PreventDefaultComp {
|
||||
handlerReturnValue: any = true;
|
||||
// TODO(issue/24571): remove '!'.
|
||||
@ -84,7 +149,10 @@ describe('event listeners', () => {
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => { comps = []; });
|
||||
beforeEach(() => {
|
||||
comps = [];
|
||||
events = [];
|
||||
});
|
||||
|
||||
it('should call function on event emit', () => {
|
||||
const fixture = new ComponentFixture(MyComp);
|
||||
@ -477,6 +545,7 @@ describe('event listeners', () => {
|
||||
|
||||
const fixture = new ComponentFixture(MyComp);
|
||||
const host = fixture.hostElement;
|
||||
|
||||
host.click();
|
||||
expect(events).toEqual(['click!']);
|
||||
|
||||
@ -484,6 +553,20 @@ describe('event listeners', () => {
|
||||
expect(events).toEqual(['click!', 'click!']);
|
||||
});
|
||||
|
||||
it('should support global host listeners on components', () => {
|
||||
const fixture = new ComponentFixture(MyCompWithGlobalListeners);
|
||||
const doc = fixture.hostElement.ownerDocument !;
|
||||
|
||||
dispatchEvent(doc, 'custom');
|
||||
expect(events).toEqual(['component - document:custom']);
|
||||
|
||||
dispatchEvent(doc.body, 'click');
|
||||
expect(events).toEqual(['component - document:custom', 'component - body:click']);
|
||||
|
||||
// invoke destroy for this fixture to cleanup all listeners setup for global objects
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should support host listeners on directives', () => {
|
||||
let events: string[] = [];
|
||||
|
||||
@ -504,16 +587,14 @@ describe('event listeners', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function Template(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
elementStart(0, 'button', ['hostListenerDir', '']);
|
||||
text(1, 'Click');
|
||||
elementEnd();
|
||||
}
|
||||
}
|
||||
const fixture = new TemplateFixture(() => {
|
||||
elementStart(0, 'button', ['hostListenerDir', '']);
|
||||
text(1, 'Click');
|
||||
elementEnd();
|
||||
}, () => {}, 2, 0, [HostListenerDir]);
|
||||
|
||||
const button = fixture.hostElement.querySelector('button') !;
|
||||
|
||||
renderToHtml(Template, {}, 2, 0, [HostListenerDir]);
|
||||
const button = containerEl.querySelector('button') !;
|
||||
button.click();
|
||||
expect(events).toEqual(['click!']);
|
||||
|
||||
@ -521,6 +602,23 @@ describe('event listeners', () => {
|
||||
expect(events).toEqual(['click!', 'click!']);
|
||||
});
|
||||
|
||||
it('should support global host listeners on directives', () => {
|
||||
const fixture = new TemplateFixture(() => {
|
||||
element(0, 'div', ['hostListenerDir', '']);
|
||||
}, () => {}, 1, 0, [GlobalHostListenerDir]);
|
||||
|
||||
const doc = fixture.hostElement.ownerDocument !;
|
||||
|
||||
dispatchEvent(doc, 'custom');
|
||||
expect(events).toEqual(['directive - document:custom']);
|
||||
|
||||
dispatchEvent(doc.body, 'click');
|
||||
expect(events).toEqual(['directive - document:custom', 'directive - body:click']);
|
||||
|
||||
// invoke destroy for this fixture to cleanup all listeners setup for global objects
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should support listeners with specified set of args', () => {
|
||||
class MyComp {
|
||||
counter = 0;
|
||||
@ -673,6 +771,40 @@ describe('event listeners', () => {
|
||||
expect(comps[1] !.counter).toEqual(1);
|
||||
});
|
||||
|
||||
it('should destroy global listeners in component views', () => {
|
||||
const ctx = {showing: true};
|
||||
|
||||
const fixture = new TemplateFixture(
|
||||
() => { container(0); },
|
||||
() => {
|
||||
containerRefreshStart(0);
|
||||
{
|
||||
if (ctx.showing) {
|
||||
let rf1 = embeddedViewStart(0, 1, 0);
|
||||
if (rf1 & RenderFlags.Create) {
|
||||
element(0, 'comp');
|
||||
}
|
||||
embeddedViewEnd();
|
||||
}
|
||||
}
|
||||
containerRefreshEnd();
|
||||
},
|
||||
1, 0, [MyCompWithGlobalListeners]);
|
||||
|
||||
const body = fixture.hostElement.ownerDocument !.body;
|
||||
|
||||
body.click();
|
||||
expect(events).toEqual(['component - body:click']);
|
||||
|
||||
// the child view listener should be removed when the parent view is removed
|
||||
ctx.showing = false;
|
||||
fixture.update();
|
||||
|
||||
body.click();
|
||||
// expecting no changes in events array
|
||||
expect(events).toEqual(['component - body:click']);
|
||||
});
|
||||
|
||||
it('should support listeners with sibling nested containers', () => {
|
||||
/**
|
||||
* % if (condition) {
|
||||
|
@ -31,6 +31,8 @@ import {DirectiveDefList, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeTyp
|
||||
import {PlayerHandler} from '../../src/render3/interfaces/player';
|
||||
import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, RendererFactory3, RendererStyleFlags3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
||||
import {HEADER_OFFSET, LView} from '../../src/render3/interfaces/view';
|
||||
import {destroyLView} from '../../src/render3/node_manipulation';
|
||||
import {getRootView} from '../../src/render3/util';
|
||||
import {Sanitizer} from '../../src/sanitization/security';
|
||||
import {Type} from '../../src/type';
|
||||
|
||||
@ -130,6 +132,11 @@ export class TemplateFixture extends BaseFixture {
|
||||
this.hostElement, updateBlock || this.updateBlock, 0, this.vars, null !,
|
||||
this._rendererFactory, this.hostView, this._directiveDefs, this._pipeDefs, this._sanitizer);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.containerElement.removeChild(this.hostElement);
|
||||
destroyLView(this.hostView);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -171,6 +178,11 @@ export class ComponentFixture<T> extends BaseFixture {
|
||||
tick(this.component);
|
||||
this.requestAnimationFrame.flush();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.containerElement.removeChild(this.hostElement);
|
||||
destroyLView(getRootView(this.component));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
Reference in New Issue
Block a user