diff --git a/packages/core/test/acceptance/listener_spec.ts b/packages/core/test/acceptance/listener_spec.ts
index a3081c18bc3..f186c31caaf 100644
--- a/packages/core/test/acceptance/listener_spec.ts
+++ b/packages/core/test/acceptance/listener_spec.ts
@@ -16,7 +16,99 @@ function getNoOfNativeListeners(): number {
}
describe('event listeners', () => {
- describe('even handler', () => {
+ describe('even handling statements', () => {
+ it('should call function on event emit', () => {
+ @Component({
+ template: ``,
+ })
+ class MyComp {
+ counter = 0;
+
+ onClick() {
+ this.counter++;
+ }
+ }
+
+ const fixture = TestBed.createComponent(MyComp);
+ fixture.detectChanges();
+ expect(fixture.componentInstance.counter).toEqual(0);
+
+ const button = fixture.nativeElement.querySelector('button');
+ button.click();
+ expect(fixture.componentInstance.counter).toEqual(1);
+ });
+
+ it('should call function chain on event emit', () => {
+ @Component({
+ template: ``,
+ })
+ class MyComp {
+ counter = 0;
+ counter2 = 0;
+ onClick() {
+ this.counter++;
+ }
+ onClick2() {
+ this.counter2++;
+ }
+ }
+
+ const fixture = TestBed.createComponent(MyComp);
+ fixture.detectChanges();
+ expect(fixture.componentInstance.counter).toEqual(0);
+ expect(fixture.componentInstance.counter2).toEqual(0);
+
+ const button = fixture.nativeElement.querySelector('button');
+ button.click();
+ expect(fixture.componentInstance.counter).toEqual(1);
+ expect(fixture.componentInstance.counter2).toEqual(1);
+ });
+
+ it('should evaluate expression on event emit', () => {
+ @Component({
+ template: ``,
+ })
+ class MyComp {
+ showing = false;
+ }
+
+ const fixture = TestBed.createComponent(MyComp);
+ fixture.detectChanges();
+ expect(fixture.componentInstance.showing).toBeFalse();
+
+ const button = fixture.nativeElement.querySelector('button');
+ button.click();
+ expect(fixture.componentInstance.showing).toBeTrue();
+
+ button.click();
+ expect(fixture.componentInstance.showing).toBeFalse();
+ });
+
+ it('should support listeners with specified set of args', () => {
+ @Component({
+ template: ``,
+ })
+ class MyComp {
+ counter = 0;
+ data = {a: 1, b: 2};
+
+ onClick(a: any, b: any) {
+ this.counter += a + b;
+ }
+ }
+
+ const fixture = TestBed.createComponent(MyComp);
+ fixture.detectChanges();
+ expect(fixture.componentInstance.counter).toBe(0);
+
+ const button = fixture.nativeElement.querySelector('button');
+ button.click();
+ expect(fixture.componentInstance.counter).toBe(3);
+
+ button.click();
+ expect(fixture.componentInstance.counter).toBe(6);
+ });
+
it('should be able to access a property called $event using `this`', () => {
let eventVariable: number|undefined;
let eventObject: MouseEvent|undefined;
@@ -110,6 +202,82 @@ describe('event listeners', () => {
expect(instance.one.value).toBe('one!');
expect(instance.two.value).toBe('two!');
});
+
+ it('should support local refs in listeners', () => {
+ @Component({
+ selector: 'my-comp',
+ standalone: true,
+ template: ``,
+ })
+ class MyComp {
+ }
+
+ @Component({
+ standalone: true,
+ imports: [MyComp],
+ template: `
+
+
+ `,
+ })
+ class App {
+ comp: MyComp|null = null;
+
+ onClick(comp: MyComp) {
+ this.comp = comp;
+ }
+ }
+
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+ expect(fixture.componentInstance.comp).toBeNull();
+
+ const button = fixture.nativeElement.querySelector('button');
+ button.click();
+ expect(fixture.componentInstance.comp).toBeInstanceOf(MyComp);
+ });
+ });
+
+ describe('prevent default', () => {
+ it('should call prevent default when a handler returns false', () => {
+ @Component({
+ template: ``,
+ })
+ class MyComp {
+ handlerReturnValue: boolean|undefined;
+ event: Event|undefined;
+
+ onClick(e: any) {
+ this.event = e;
+
+ // stub preventDefault() to check whether it's called
+ Object.defineProperty(
+ this.event, 'preventDefault',
+ {value: jasmine.createSpy('preventDefault'), writable: true});
+
+ return this.handlerReturnValue;
+ }
+ }
+
+ const fixture = TestBed.createComponent(MyComp);
+ fixture.detectChanges();
+
+ const myComp = fixture.componentInstance;
+ const button = fixture.nativeElement.querySelector('button');
+
+ myComp.handlerReturnValue = undefined;
+ button.click();
+ expect(myComp.event!.preventDefault).not.toHaveBeenCalled();
+
+ myComp.handlerReturnValue = true;
+ button.click();
+ expect(myComp.event!.preventDefault).not.toHaveBeenCalled();
+
+ // Returning `false` is what causes the renderer to call `event.preventDefault`.
+ myComp.handlerReturnValue = false;
+ button.click();
+ expect(myComp.event!.preventDefault).toHaveBeenCalled();
+ });
});
describe('coalescing', () => {
@@ -490,7 +658,127 @@ describe('event listeners', () => {
});
});
- describe('global event listeners on special elements', () => {
+ describe('host listeners', () => {
+ it('should support host listeners on components', () => {
+ const events: string[] = [];
+
+ @Component({
+ template: ``,
+ })
+ class MyComp {
+ @HostListener('click')
+ onClick() {
+ events.push('click!');
+ }
+ }
+
+ const fixture = TestBed.createComponent(MyComp);
+ fixture.detectChanges();
+
+ const host = fixture.nativeElement;
+
+ host.click();
+ expect(events).toEqual(['click!']);
+
+ host.click();
+ expect(events).toEqual(['click!', 'click!']);
+ });
+
+ it('should support global host listeners on components', () => {
+ const events: string[] = [];
+
+ @Component({
+ template: ``,
+ })
+ class MyComp {
+ @HostListener('document:click')
+ onClick() {
+ events.push('global click!');
+ }
+ }
+
+ const fixture = TestBed.createComponent(MyComp);
+ fixture.detectChanges();
+
+ const host = fixture.nativeElement;
+
+ host.click();
+ expect(events).toEqual(['global click!']);
+
+ host.click();
+ expect(events).toEqual(['global click!', 'global click!']);
+ });
+
+ it('should support host listeners on directives', () => {
+ const events: string[] = [];
+
+ @Directive({
+ selector: '[hostListenerDir]',
+ standalone: true,
+ })
+ class HostListenerDir {
+ @HostListener('click')
+ onClick() {
+ events.push('click!');
+ }
+ }
+
+ @Component({
+ standalone: true,
+ imports: [HostListenerDir],
+ template: ``,
+ })
+ class MyComp {
+ }
+
+ const fixture = TestBed.createComponent(MyComp);
+ fixture.detectChanges();
+ expect(events).toEqual([]);
+
+ const button = fixture.nativeElement.querySelector('button');
+ button.click();
+ expect(events).toEqual(['click!']);
+
+ button.click();
+ expect(events).toEqual(['click!', 'click!']);
+ });
+
+ it('should support global host listeners on directives', () => {
+ const events: string[] = [];
+
+ @Directive({
+ selector: '[hostListenerDir]',
+ standalone: true,
+ })
+ class HostListenerDir {
+ @HostListener('document:click')
+ onClick() {
+ events.push('click!');
+ }
+ }
+
+ @Component({
+ standalone: true,
+ imports: [HostListenerDir],
+ template: ``,
+ })
+ class MyComp {
+ }
+
+ const fixture = TestBed.createComponent(MyComp);
+ fixture.detectChanges();
+ expect(events).toEqual([]);
+
+ const button = fixture.nativeElement.querySelector('button');
+ button.click();
+ expect(events).toEqual(['click!']);
+
+ button.click();
+ expect(events).toEqual(['click!', 'click!']);
+ });
+ });
+
+ describe('global host listeners on special element as directive hosts', () => {
it('should bind global event listeners on an ng-container directive host', () => {
let clicks = 0;
diff --git a/packages/core/test/render3/listeners_spec.ts b/packages/core/test/render3/listeners_spec.ts
deleted file mode 100644
index d71d6622eb2..00000000000
--- a/packages/core/test/render3/listeners_spec.ts
+++ /dev/null
@@ -1,514 +0,0 @@
-/**
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-
-import {HEADER_OFFSET} from '@angular/core/src/render3/interfaces/view';
-import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util';
-
-import {ɵɵdefineComponent, ɵɵdefineDirective, ɵɵreference, ɵɵresolveBody, ɵɵresolveDocument} from '../../src/render3/index';
-import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵgetCurrentView, ɵɵlistener, ɵɵtext} from '../../src/render3/instructions/all';
-import {RenderFlags} from '../../src/render3/interfaces/definition';
-import {GlobalTargetResolver} from '../../src/render3/interfaces/renderer';
-import {ɵɵresetView, ɵɵrestoreView} from '../../src/render3/state';
-
-import {getRendererFactory2} from './imported_renderer2';
-import {ComponentFixture, containerEl, createComponent, getDirectiveOnNode, renderToHtml, TemplateFixture} from './render_util';
-
-
-describe('event listeners', () => {
- let comps: any[] = [];
- let events: any[] = [];
-
- class MyComp {
- showing = true;
- counter = 0;
-
- onClick() {
- this.counter++;
- }
-
- static ɵfac =
- () => {
- let comp = new MyComp();
- comps.push(comp);
- return comp;
- }
-
- static ɵcmp = ɵɵdefineComponent({
- type: MyComp,
- selectors: [['comp']],
- decls: 2,
- vars: 0,
- /** */
- template:
- function CompTemplate(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelementStart(0, 'button');
- {
- ɵɵlistener('click', function() {
- return ctx.onClick();
- });
- ɵɵtext(1, 'Click me');
- }
- ɵɵelementEnd();
- }
- }
- });
- }
-
- class MyCompWithGlobalListeners {
- /* @HostListener('document:custom') */
- onDocumentCustomEvent() {
- events.push('component - document:custom');
- }
-
- /* @HostListener('body:click') */
- onBodyClick() {
- events.push('component - body:click');
- }
-
- static ɵfac =
- () => {
- let comp = new MyCompWithGlobalListeners();
- comps.push(comp);
- return comp;
- }
-
- static ɵcmp = ɵɵdefineComponent({
- type: MyCompWithGlobalListeners,
- selectors: [['comp']],
- decls: 1,
- vars: 0,
- template:
- function CompTemplate(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0, 'Some text');
- }
- },
- hostBindings:
- function HostListenerDir_HostBindings(rf: RenderFlags, ctx: any) {
- 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 ɵfac = function HostListenerDir_Factory() {
- return new GlobalHostListenerDir();
- };
- static ɵdir = ɵɵdefineDirective({
- type: GlobalHostListenerDir,
- selectors: [['', 'hostListenerDir', '']],
- hostBindings:
- function HostListenerDir_HostBindings(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵlistener('custom', function() {
- return ctx.onDocumentCustomEvent();
- }, false, ɵɵresolveDocument as GlobalTargetResolver)('click', function() {
- return ctx.onBodyClick();
- }, false, ɵɵresolveBody as GlobalTargetResolver);
- }
- }
- });
- }
-
- class PreventDefaultComp {
- handlerReturnValue: any = true;
- // TODO(issue/24571): remove '!'.
- event!: Event;
-
- onClick(e: any) {
- this.event = e;
-
- // stub preventDefault() to check whether it's called
- Object.defineProperty(
- this.event, 'preventDefault',
- {value: jasmine.createSpy('preventDefault'), writable: true});
-
- return this.handlerReturnValue;
- }
-
- static ɵfac = () => new PreventDefaultComp();
- static ɵcmp = ɵɵdefineComponent({
- type: PreventDefaultComp,
- selectors: [['prevent-default-comp']],
- decls: 2,
- vars: 0,
- /** */
- template:
- (rf: RenderFlags, ctx: PreventDefaultComp) => {
- if (rf & RenderFlags.Create) {
- ɵɵelementStart(0, 'button');
- {
- ɵɵlistener('click', function($event: any) {
- return ctx.onClick($event);
- });
- ɵɵtext(1, 'Click');
- }
- ɵɵelementEnd();
- }
- }
- });
- }
-
- beforeEach(() => {
- comps = [];
- events = [];
- });
-
- it('should call function on event emit', () => {
- const fixture = new ComponentFixture(MyComp);
- const comp = fixture.component;
- const button = fixture.hostElement.querySelector('button')!;
-
- button.click();
- expect(comp.counter).toEqual(1);
-
- button.click();
- expect(comp.counter).toEqual(2);
- });
-
- it('should retain event handler return values using document', () => {
- const fixture = new ComponentFixture(PreventDefaultComp);
- const preventDefaultComp = fixture.component;
- const button = fixture.hostElement.querySelector('button')!;
-
- button.click();
- expect(preventDefaultComp.event!.preventDefault).not.toHaveBeenCalled();
-
- preventDefaultComp.handlerReturnValue = undefined;
- button.click();
- expect(preventDefaultComp.event!.preventDefault).not.toHaveBeenCalled();
-
- preventDefaultComp.handlerReturnValue = false;
- button.click();
- expect(preventDefaultComp.event!.preventDefault).toHaveBeenCalled();
- });
-
- it('should retain event handler return values with renderer2', () => {
- const fixture =
- new ComponentFixture(PreventDefaultComp, {rendererFactory: getRendererFactory2(document)});
- const preventDefaultComp = fixture.component;
- const button = fixture.hostElement.querySelector('button')!;
-
- button.click();
- expect(preventDefaultComp.event!.preventDefault).not.toHaveBeenCalled();
-
- preventDefaultComp.handlerReturnValue = undefined;
- button.click();
- expect(preventDefaultComp.event!.preventDefault).not.toHaveBeenCalled();
-
- preventDefaultComp.handlerReturnValue = false;
- button.click();
- expect(preventDefaultComp.event!.preventDefault).toHaveBeenCalled();
- });
-
- it('should call function chain on event emit', () => {
- /** */
- function Template(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelementStart(0, 'button');
- {
- ɵɵlistener('click', function() {
- ctx.onClick();
- return ctx.onClick2();
- });
- ɵɵtext(1, 'Click me');
- }
- ɵɵelementEnd();
- }
- }
-
- const ctx = {
- counter: 0,
- counter2: 0,
- onClick: function() {
- this.counter++;
- },
- onClick2: function() {
- this.counter2++;
- }
- };
- renderToHtml(Template, ctx, 2);
- const button = containerEl.querySelector('button')!;
-
- button.click();
- expect(ctx.counter).toBe(1);
- expect(ctx.counter2).toBe(1);
-
- button.click();
- expect(ctx.counter).toBe(2);
- expect(ctx.counter2).toBe(2);
- });
-
- it('should evaluate expression on event emit', () => {
- /** */
- function Template(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelementStart(0, 'button');
- {
- ɵɵlistener('click', function() {
- return ctx.showing = !ctx.showing;
- });
- ɵɵtext(1, 'Click me');
- }
- ɵɵelementEnd();
- }
- }
-
- const ctx = {showing: false};
- renderToHtml(Template, ctx, 2);
- const button = containerEl.querySelector('button')!;
-
- button.click();
- expect(ctx.showing).toBe(true);
-
- button.click();
- expect(ctx.showing).toBe(false);
- });
-
- it('should support host listeners on components', () => {
- let events: string[] = [];
- class MyComp {
- /* @HostListener('click') */
- onClick() {
- events.push('click!');
- }
-
- static ɵfac =
- () => {
- return new MyComp();
- }
-
- static ɵcmp = ɵɵdefineComponent({
- type: MyComp,
- selectors: [['comp']],
- decls: 1,
- vars: 0,
- template:
- function CompTemplate(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵtext(0, 'Some text');
- }
- },
- hostBindings:
- function HostListenerDir_HostBindings(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵlistener('click', function() {
- return ctx.onClick();
- });
- }
- }
- });
- }
-
- const fixture = new ComponentFixture(MyComp);
- const host = fixture.hostElement;
-
- host.click();
- expect(events).toEqual(['click!']);
-
- host.click();
- 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[] = [];
-
- class HostListenerDir {
- /* @HostListener('click') */
- onClick() {
- events.push('click!');
- }
-
- static ɵfac = function HostListenerDir_Factory() {
- return new HostListenerDir();
- };
- static ɵdir = ɵɵdefineDirective({
- type: HostListenerDir,
- selectors: [['', 'hostListenerDir', '']],
- hostBindings:
- function HostListenerDir_HostBindings(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵlistener('click', function() {
- return ctx.onClick();
- });
- }
- }
- });
- }
-
- const fixture = new TemplateFixture({
- create: () => {
- ɵɵelementStart(0, 'button', 0);
- ɵɵtext(1, 'Click');
- ɵɵelementEnd();
- },
- decls: 2,
- directives: [HostListenerDir],
- consts: [['hostListenerDir', '']]
- });
-
- const button = fixture.hostElement.querySelector('button')!;
-
- button.click();
- expect(events).toEqual(['click!']);
-
- button.click();
- expect(events).toEqual(['click!', 'click!']);
- });
-
- it('should support global host listeners on directives', () => {
- const fixture = new TemplateFixture({
- create: () => {
- ɵɵelement(0, 'div', 0);
- },
- decls: 1,
- directives: [GlobalHostListenerDir],
- consts: [['hostListenerDir', '']]
- });
-
- 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;
- data = {a: 1, b: 2};
-
- onClick(a: any, b: any) {
- this.counter += a + b;
- }
-
- static ɵfac = () => new MyComp();
- static ɵcmp = ɵɵdefineComponent({
- type: MyComp,
- selectors: [['comp']],
- decls: 2,
- vars: 0,
- /** */
- template:
- function CompTemplate(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelementStart(0, 'button');
- {
- ɵɵlistener('click', function() {
- return ctx.onClick(ctx.data.a, ctx.data.b);
- });
- ɵɵtext(1, 'Click me');
- }
- ɵɵelementEnd();
- }
- }
- });
- }
-
- const fixture = new ComponentFixture(MyComp);
- const comp = fixture.component;
- const button = fixture.hostElement.querySelector('button')!;
-
- button.click();
- expect(comp.counter).toEqual(3);
-
- button.click();
- expect(comp.counter).toEqual(6);
- });
-
- it('should support local refs in listeners', () => {
- let compInstance: any;
-
- const Comp = createComponent('comp', (rf: RenderFlags, ctx: any) => {});
-
- /**
- *
- *
- */
- class App {
- comp: any = null;
-
- onClick(comp: any) {
- this.comp = comp;
- }
-
- static ɵfac = () => new App();
- static ɵcmp = ɵɵdefineComponent({
- type: App,
- selectors: [['app']],
- decls: 3,
- vars: 0,
- consts: [['comp', '']],
- template:
- (rf: RenderFlags, ctx: App) => {
- if (rf & RenderFlags.Create) {
- const state = ɵɵgetCurrentView();
- ɵɵelement(0, 'comp', null, 0);
- ɵɵelementStart(2, 'button');
- {
- ɵɵlistener('click', function() {
- ɵɵrestoreView(state);
- const comp = ɵɵreference(1);
- return ɵɵresetView(ctx.onClick(comp));
- });
- }
- ɵɵelementEnd();
- }
-
- // testing only
- compInstance = getDirectiveOnNode(HEADER_OFFSET);
- },
- dependencies: [Comp]
- });
- }
-
- const fixture = new ComponentFixture(App);
- expect(fixture.component.comp).toEqual(null);
-
- const button = fixture.hostElement.querySelector('button') as HTMLButtonElement;
- button.click();
- expect(fixture.component.comp).toEqual(compInstance);
- });
-});