diff --git a/packages/core/test/acceptance/outputs_spec.ts b/packages/core/test/acceptance/outputs_spec.ts
new file mode 100644
index 0000000000..873b663388
--- /dev/null
+++ b/packages/core/test/acceptance/outputs_spec.ts
@@ -0,0 +1,282 @@
+/**
+ * @license
+ * Copyright Google Inc. 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 {CommonModule} from '@angular/common';
+import {Component, Directive, EventEmitter, Input, OnDestroy, Output, ViewChild} from '@angular/core';
+import {TestBed} from '@angular/core/testing';
+
+describe('outputs', () => {
+ @Component({selector: 'button-toggle', template: ''})
+ class ButtonToggle {
+ @Output('change')
+ change = new EventEmitter();
+
+ @Output('reset')
+ resetStream = new EventEmitter();
+ }
+
+ @Directive({selector: '[otherDir]'})
+ class OtherDir {
+ @Output('change')
+ changeStream = new EventEmitter();
+ }
+
+ @Component({selector: 'destroy-comp', template: ''})
+ class DestroyComp implements OnDestroy {
+ events: string[] = [];
+ ngOnDestroy() { this.events.push('destroy'); }
+ }
+
+ @Directive({selector: '[myButton]'})
+ class MyButton {
+ @Output()
+ click = new EventEmitter();
+ }
+
+ it('should call component output function when event is emitted', () => {
+ let counter = 0;
+
+ @Component({template: ''})
+ class App {
+ @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle;
+ onChange() { counter++; }
+ }
+ TestBed.configureTestingModule({declarations: [App, ButtonToggle]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ fixture.componentInstance.buttonToggle.change.next();
+ expect(counter).toBe(1);
+
+ fixture.componentInstance.buttonToggle.change.next();
+ expect(counter).toBe(2);
+ });
+
+ it('should support more than 1 output function on the same node', () => {
+ let counter = 0;
+ let resetCounter = 0;
+
+ @Component(
+ {template: ''})
+ class App {
+ @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle;
+ onChange() { counter++; }
+ onReset() { resetCounter++; }
+ }
+ TestBed.configureTestingModule({declarations: [App, ButtonToggle]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ fixture.componentInstance.buttonToggle.change.next();
+ expect(counter).toBe(1);
+
+ fixture.componentInstance.buttonToggle.resetStream.next();
+ expect(resetCounter).toBe(1);
+ });
+
+ it('should eval component output expression when event is emitted', () => {
+ @Component({template: ''})
+ class App {
+ @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle;
+ counter = 0;
+ }
+ TestBed.configureTestingModule({declarations: [App, ButtonToggle]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ fixture.componentInstance.buttonToggle.change.next();
+ expect(fixture.componentInstance.counter).toBe(1);
+
+ fixture.componentInstance.buttonToggle.change.next();
+ expect(fixture.componentInstance.counter).toBe(2);
+ });
+
+ it('should unsubscribe from output when view is destroyed', () => {
+ let counter = 0;
+
+ @Component(
+ {template: ''})
+ class App {
+ @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle;
+ condition = true;
+
+ onChange() { counter++; }
+ }
+ TestBed.configureTestingModule({imports: [CommonModule], declarations: [App, ButtonToggle]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+ const buttonToggle = fixture.componentInstance.buttonToggle;
+
+ buttonToggle.change.next();
+ expect(counter).toBe(1);
+
+ fixture.componentInstance.condition = false;
+ fixture.detectChanges();
+
+ buttonToggle.change.next();
+ expect(counter).toBe(1);
+ });
+
+ it('should unsubscribe from output in nested view', () => {
+ let counter = 0;
+
+ @Component({
+ template: `
+
+
+
+ `
+ })
+ class App {
+ @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle;
+ condition = true;
+ condition2 = true;
+
+ onChange() { counter++; }
+ }
+ TestBed.configureTestingModule({imports: [CommonModule], declarations: [App, ButtonToggle]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+ const buttonToggle = fixture.componentInstance.buttonToggle;
+
+ buttonToggle.change.next();
+ expect(counter).toBe(1);
+
+ fixture.componentInstance.condition = false;
+ fixture.detectChanges();
+
+ buttonToggle.change.next();
+ expect(counter).toBe(1);
+ });
+
+ it('should work properly when view also has listeners and destroys', () => {
+ let clickCounter = 0;
+ let changeCounter = 0;
+
+ @Component({
+ template: `
+
+
+
+
+
+ `
+ })
+ class App {
+ @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle;
+ @ViewChild(DestroyComp) destroyComp !: DestroyComp;
+ condition = true;
+
+ onClick() { clickCounter++; }
+ onChange() { changeCounter++; }
+ }
+ TestBed.configureTestingModule(
+ {imports: [CommonModule], declarations: [App, ButtonToggle, DestroyComp]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+ const {buttonToggle, destroyComp} = fixture.componentInstance;
+ const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
+
+ buttonToggle.change.next();
+ expect(changeCounter).toBe(1);
+ expect(clickCounter).toBe(0);
+
+ button.click();
+ expect(changeCounter).toBe(1);
+ expect(clickCounter).toBe(1);
+
+ fixture.componentInstance.condition = false;
+ fixture.detectChanges();
+
+ expect(destroyComp.events).toEqual(['destroy']);
+
+ buttonToggle.change.next();
+ button.click();
+ expect(changeCounter).toBe(1);
+ expect(clickCounter).toBe(1);
+ });
+
+ it('should fire event listeners along with outputs if they match', () => {
+ let counter = 0;
+
+ @Component({template: ''})
+ class App {
+ @ViewChild(MyButton) buttonDir !: MyButton;
+ onClick() { counter++; }
+ }
+ TestBed.configureTestingModule({declarations: [App, MyButton]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ // To match current Angular behavior, the click listener is still
+ // set up in addition to any matching outputs.
+ const button = fixture.nativeElement.querySelector('button');
+ button.click();
+ expect(counter).toBe(1);
+
+ fixture.componentInstance.buttonDir.click.next();
+ expect(counter).toBe(2);
+ });
+
+ it('should work with two outputs of the same name', () => {
+ let counter = 0;
+
+ @Component({template: ''})
+ class App {
+ @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle;
+ @ViewChild(OtherDir) otherDir !: OtherDir;
+ onChange() { counter++; }
+ }
+ TestBed.configureTestingModule({declarations: [App, ButtonToggle, OtherDir]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ fixture.componentInstance.buttonToggle.change.next();
+ expect(counter).toBe(1);
+
+ fixture.componentInstance.otherDir.changeStream.next();
+ expect(counter).toBe(2);
+ });
+
+ it('should work with an input and output of the same name', () => {
+ let counter = 0;
+
+ @Directive({selector: '[otherChangeDir]'})
+ class OtherChangeDir {
+ @Input()
+ change !: boolean;
+ }
+
+ @Component({
+ template:
+ ''
+ })
+ class App {
+ @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle;
+ @ViewChild(OtherChangeDir) otherDir !: OtherChangeDir;
+ change = true;
+
+ onChange() { counter++; }
+ }
+ TestBed.configureTestingModule({declarations: [App, ButtonToggle, OtherChangeDir]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+ const {buttonToggle, otherDir} = fixture.componentInstance;
+
+ expect(otherDir.change).toBe(true);
+
+ fixture.componentInstance.change = false;
+ fixture.detectChanges();
+
+ expect(otherDir.change).toBe(false);
+
+ buttonToggle.change.next();
+ expect(counter).toBe(1);
+ });
+
+});
diff --git a/packages/core/test/render3/outputs_spec.ts b/packages/core/test/render3/outputs_spec.ts
index 26970eaefe..58607ea713 100644
--- a/packages/core/test/render3/outputs_spec.ts
+++ b/packages/core/test/render3/outputs_spec.ts
@@ -9,15 +9,13 @@
import {EventEmitter} from '@angular/core';
import {ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index';
-import {ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵlistener, ɵɵtext} from '../../src/render3/instructions/all';
+import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵlistener, ɵɵtext} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
-import {containerEl, renderToHtml} from './render_util';
+import {renderToHtml} from './render_util';
describe('outputs', () => {
let buttonToggle: ButtonToggle;
- let destroyComp: DestroyComp;
- let buttonDir: MyButton;
class ButtonToggle {
change = new EventEmitter();
@@ -47,355 +45,8 @@ describe('outputs', () => {
});
}
- class DestroyComp {
- events: string[] = [];
- ngOnDestroy() { this.events.push('destroy'); }
- static ngComponentDef = ɵɵdefineComponent({
- type: DestroyComp,
- selectors: [['destroy-comp']],
- consts: 0,
- vars: 0,
- template: function(rf: RenderFlags, ctx: any) {},
- factory: () => destroyComp = new DestroyComp()
- });
- }
-
- /** */
- class MyButton {
- click = new EventEmitter();
-
- static ngDirectiveDef = ɵɵdefineDirective({
- type: MyButton,
- selectors: [['', 'myButton', '']],
- factory: () => buttonDir = new MyButton,
- outputs: {click: 'click'}
- });
- }
-
-
- const deps = [ButtonToggle, OtherDir, DestroyComp, MyButton];
-
- it('should call component output function when event is emitted', () => {
- /** */
- function Template(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelementStart(0, 'button-toggle');
- {
- ɵɵlistener('change', function() { return ctx.onChange(); });
- }
- ɵɵelementEnd();
- }
- }
-
- let counter = 0;
- const ctx = {onChange: () => counter++};
- renderToHtml(Template, ctx, 1, 0, deps);
-
- buttonToggle !.change.next();
- expect(counter).toEqual(1);
-
- buttonToggle !.change.next();
- expect(counter).toEqual(2);
- });
-
- it('should support more than 1 output function on the same node', () => {
- /** */
- function Template(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelementStart(0, 'button-toggle');
- {
- ɵɵlistener('change', function() { return ctx.onChange(); });
- ɵɵlistener('reset', function() { return ctx.onReset(); });
- }
- ɵɵelementEnd();
- }
- }
-
- let counter = 0;
- let resetCounter = 0;
- const ctx = {onChange: () => counter++, onReset: () => resetCounter++};
- renderToHtml(Template, ctx, 1, 0, deps);
-
- buttonToggle !.change.next();
- expect(counter).toEqual(1);
-
- buttonToggle !.resetStream.next();
- expect(resetCounter).toEqual(1);
- });
-
- it('should eval component output expression when event is emitted', () => {
- /** */
- function Template(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelementStart(0, 'button-toggle');
- {
- ɵɵlistener('change', function() { return ctx.counter++; });
- }
- ɵɵelementEnd();
- }
- }
-
- const ctx = {counter: 0};
- renderToHtml(Template, ctx, 1, 0, deps);
-
- buttonToggle !.change.next();
- expect(ctx.counter).toEqual(1);
-
- buttonToggle !.change.next();
- expect(ctx.counter).toEqual(2);
- });
-
- it('should unsubscribe from output when view is destroyed', () => {
-
- /**
- * % if (condition) {
- *
- * % }
- */
- function Template(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵcontainer(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵcontainerRefreshStart(0);
- {
- if (ctx.condition) {
- let rf1 = ɵɵembeddedViewStart(0, 1, 0);
- if (rf1 & RenderFlags.Create) {
- ɵɵelementStart(0, 'button-toggle');
- {
- ɵɵlistener('change', function() { return ctx.onChange(); });
- }
- ɵɵelementEnd();
- }
- ɵɵembeddedViewEnd();
- }
- }
- ɵɵcontainerRefreshEnd();
- }
- }
-
- let counter = 0;
- const ctx = {onChange: () => counter++, condition: true};
- renderToHtml(Template, ctx, 1, 0, deps);
-
- buttonToggle !.change.next();
- expect(counter).toEqual(1);
-
- ctx.condition = false;
- renderToHtml(Template, ctx, 1, 0, deps);
-
- buttonToggle !.change.next();
- expect(counter).toEqual(1);
- });
-
- it('should unsubscribe from output in nested view', () => {
-
- /**
- * % if (condition) {
- * % if (condition2) {
- *
- * % }
- * % }
- */
- function Template(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵcontainer(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵcontainerRefreshStart(0);
- {
- if (ctx.condition) {
- let rf1 = ɵɵembeddedViewStart(0, 1, 0);
- if (rf1 & RenderFlags.Create) {
- ɵɵcontainer(0);
- }
- ɵɵcontainerRefreshStart(0);
- {
- if (ctx.condition2) {
- let rf1 = ɵɵembeddedViewStart(0, 1, 0);
- if (rf1 & RenderFlags.Create) {
- ɵɵelementStart(0, 'button-toggle');
- {
- ɵɵlistener('change', function() { return ctx.onChange(); });
- }
- ɵɵelementEnd();
- }
- ɵɵembeddedViewEnd();
- }
- }
- ɵɵcontainerRefreshEnd();
- ɵɵembeddedViewEnd();
- }
- }
- ɵɵcontainerRefreshEnd();
- }
- }
-
- let counter = 0;
- const ctx = {onChange: () => counter++, condition: true, condition2: true};
- renderToHtml(Template, ctx, 1, 0, deps);
-
- buttonToggle !.change.next();
- expect(counter).toEqual(1);
-
- ctx.condition = false;
- renderToHtml(Template, ctx, 1, 0, deps);
-
- buttonToggle !.change.next();
- expect(counter).toEqual(1);
- });
-
- it('should work properly when view also has listeners and destroys', () => {
- /**
- * % if (condition) {
- *
- *
- *
- * % }
- */
- function Template(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵcontainer(0);
- }
- if (rf & RenderFlags.Update) {
- ɵɵcontainerRefreshStart(0);
- {
- if (ctx.condition) {
- let rf1 = ɵɵembeddedViewStart(0, 4, 0);
- if (rf1 & RenderFlags.Create) {
- ɵɵelementStart(0, 'button');
- {
- ɵɵlistener('click', function() { return ctx.onClick(); });
- ɵɵtext(1, 'Click me');
- }
- ɵɵelementEnd();
- ɵɵelementStart(2, 'button-toggle');
- {
- ɵɵlistener('change', function() { return ctx.onChange(); });
- }
- ɵɵelementEnd();
- ɵɵelement(3, 'destroy-comp');
- }
- ɵɵembeddedViewEnd();
- }
- }
- ɵɵcontainerRefreshEnd();
- }
- }
-
- let clickCounter = 0;
- let changeCounter = 0;
- const ctx = {condition: true, onChange: () => changeCounter++, onClick: () => clickCounter++};
- renderToHtml(Template, ctx, 1, 0, deps);
-
- buttonToggle !.change.next();
- expect(changeCounter).toEqual(1);
- expect(clickCounter).toEqual(0);
-
- const button = containerEl.querySelector('button');
- button !.click();
- expect(changeCounter).toEqual(1);
- expect(clickCounter).toEqual(1);
-
- ctx.condition = false;
- renderToHtml(Template, ctx, 1, 0, deps);
-
- expect(destroyComp !.events).toEqual(['destroy']);
-
- buttonToggle !.change.next();
- button !.click();
- expect(changeCounter).toEqual(1);
- expect(clickCounter).toEqual(1);
- });
-
- it('should fire event listeners along with outputs if they match', () => {
- function Template(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelementStart(0, 'button', ['myButton', '']);
- {
- ɵɵlistener('click', function() { return ctx.onClick(); });
- }
- ɵɵelementEnd();
- }
- }
-
- let counter = 0;
- renderToHtml(Template, {counter, onClick: () => counter++}, 1, 0, deps);
-
- // To match current Angular behavior, the click listener is still
- // set up in addition to any matching outputs.
- const button = containerEl.querySelector('button') !;
- button.click();
- expect(counter).toEqual(1);
-
- buttonDir !.click.next();
- expect(counter).toEqual(2);
- });
-
- it('should work with two outputs of the same name', () => {
- /** */
- function Template(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelementStart(0, 'button-toggle', ['otherDir', '']);
- {
- ɵɵlistener('change', function() { return ctx.onChange(); });
- }
- ɵɵelementEnd();
- }
- }
-
- let counter = 0;
- renderToHtml(Template, {counter, onChange: () => counter++}, 1, 0, deps);
-
- buttonToggle !.change.next();
- expect(counter).toEqual(1);
-
- otherDir !.changeStream.next();
- expect(counter).toEqual(2);
- });
-
- it('should work with an input and output of the same name', () => {
- let otherDir: OtherChangeDir;
-
- class OtherChangeDir {
- // TODO(issue/24571): remove '!'.
- change !: boolean;
-
- static ngDirectiveDef = ɵɵdefineDirective({
- type: OtherChangeDir,
- selectors: [['', 'otherChangeDir', '']],
- factory: () => otherDir = new OtherChangeDir,
- inputs: {change: 'change'}
- });
- }
-
- /** */
- function Template(rf: RenderFlags, ctx: any) {
- if (rf & RenderFlags.Create) {
- ɵɵelementStart(0, 'button-toggle', ['otherChangeDir', '']);
- {
- ɵɵlistener('change', function() { return ctx.onChange(); });
- }
- ɵɵelementEnd();
- }
- if (rf & RenderFlags.Update) {
- ɵɵelementProperty(0, 'change', ɵɵbind(ctx.change));
- }
- }
-
- let counter = 0;
- const deps = [ButtonToggle, OtherChangeDir];
- renderToHtml(Template, {counter, onChange: () => counter++, change: true}, 1, 1, deps);
- expect(otherDir !.change).toEqual(true);
-
- renderToHtml(Template, {counter, onChange: () => counter++, change: false}, 1, 1, deps);
- expect(otherDir !.change).toEqual(false);
-
- buttonToggle !.change.next();
- expect(counter).toEqual(1);
- });
+ const deps = [ButtonToggle, OtherDir];
it('should work with outputs at same index in if block', () => {
/**