fix(ivy): register to directive outputs on ng-template / ng-container (#25698)

Runtime part of #25697

PR Close #25698
This commit is contained in:
Pawel Kozlowski 2018-08-28 15:31:09 +02:00 committed by Misko Hevery
parent 3809e0fcae
commit 371df35624
3 changed files with 89 additions and 41 deletions

View File

@ -1202,7 +1202,6 @@ export function hostElement(
return node;
}
/**
* Adds an event listener to the current node.
*
@ -1215,29 +1214,35 @@ export function hostElement(
*/
export function listener(
eventName: string, listenerFn: (e?: any) => any, useCapture = false): void {
ngDevMode && assertPreviousIsParent();
ngDevMode && assertNodeOfPossibleTypes(previousOrParentNode, TNodeType.Element);
ngDevMode &&
assertNodeOfPossibleTypes(
previousOrParentNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer);
const node = previousOrParentNode;
const native = node.native as RElement;
ngDevMode && ngDevMode.rendererAddEventListener++;
// In order to match current behavior, native DOM event listeners must be added for all
// events (including outputs).
if (isProceduralRenderer(renderer)) {
const wrappedListener = wrapListenerWithDirtyLogic(viewData, listenerFn);
const cleanupFn = renderer.listen(native, eventName, wrappedListener);
storeCleanupFn(viewData, cleanupFn);
} else {
const wrappedListener = wrapListenerWithDirtyAndDefault(viewData, listenerFn);
native.addEventListener(eventName, wrappedListener, useCapture);
const cleanupInstances = getCleanup(viewData);
cleanupInstances.push(wrappedListener);
if (firstTemplatePass) {
getTViewCleanup(viewData).push(
eventName, node.tNode.index, cleanupInstances !.length - 1, useCapture);
// add native event listener - applicable to elements only
if (previousOrParentNode.tNode.type === TNodeType.Element) {
const native = node.native as RElement;
ngDevMode && ngDevMode.rendererAddEventListener++;
// In order to match current behavior, native DOM event listeners must be added for all
// events (including outputs).
if (isProceduralRenderer(renderer)) {
const wrappedListener = wrapListenerWithDirtyLogic(viewData, listenerFn);
const cleanupFn = renderer.listen(native, eventName, wrappedListener);
storeCleanupFn(viewData, cleanupFn);
} else {
const wrappedListener = wrapListenerWithDirtyAndDefault(viewData, listenerFn);
native.addEventListener(eventName, wrappedListener, useCapture);
const cleanupInstances = getCleanup(viewData);
cleanupInstances.push(wrappedListener);
if (firstTemplatePass) {
getTViewCleanup(viewData).push(
eventName, node.tNode.index, cleanupInstances !.length - 1, useCapture);
}
}
}
// subscribe to directive outputs
let tNode: TNode|null = node.tNode;
if (tNode.outputs === undefined) {
// if we create TNode here, inputs must be undefined so we know they still need to be

View File

@ -8,10 +8,11 @@
import {EventEmitter} from '@angular/core';
import {AttributeMarker, defineDirective} from '../../src/render3/index';
import {bind, element, elementEnd, elementProperty, elementStart, listener, loadDirective} from '../../src/render3/instructions';
import {AttributeMarker, RenderFlags, defineDirective} from '../../src/render3/index';
import {TemplateFixture} from './render_util';
import {bind, element, elementEnd, elementProperty, elementStart, listener, template, elementContainerStart, elementContainerEnd} from '../../src/render3/instructions';
import {ComponentFixture, TemplateFixture, createComponent} from './render_util';
describe('directive', () => {
@ -149,6 +150,67 @@ describe('directive', () => {
expect(fixture.html).toEqual('<span></span>');
expect(directiveInstance !).not.toBeUndefined();
});
});
describe('outputs', () => {
let directiveInstance: Directive;
class Directive {
static ngDirectiveDef = defineDirective({
type: Directive,
selectors: [['', 'out', '']],
factory: () => directiveInstance = new Directive,
outputs: {out: 'out'}
});
out = new EventEmitter();
}
it('should allow outputs of directive on ng-template', () => {
/**
* <ng-template (out)="value = true"></ng-template>
*/
const Cmpt = createComponent('Cmpt', function(rf: RenderFlags, ctx: {value: any}) {
if (rf & RenderFlags.Create) {
template(0, null, 0, 0, null, [AttributeMarker.SelectOnly, 'out']);
listener('out', () => { ctx.value = true; });
}
}, 1, 0, [Directive]);
const fixture = new ComponentFixture(Cmpt);
expect(directiveInstance !).not.toBeUndefined();
expect(fixture.component.value).toBeFalsy();
directiveInstance !.out.emit();
fixture.update();
expect(fixture.component.value).toBeTruthy();
});
it('should allow outputs of directive on ng-container', () => {
/**
* <ng-container (out)="value = true"></ng-container>
*/
const Cmpt = createComponent('Cmpt', function(rf: RenderFlags, ctx: {value: any}) {
if (rf & RenderFlags.Create) {
elementContainerStart(0, [AttributeMarker.SelectOnly, 'out']);
{
listener('out', () => { ctx.value = true; });
}
elementContainerEnd();
}
}, 1, 0, [Directive]);
const fixture = new ComponentFixture(Cmpt);
expect(directiveInstance !).not.toBeUndefined();
expect(fixture.component.value).toBeFalsy();
directiveInstance !.out.emit();
fixture.update();
expect(fixture.component.value).toBeTruthy();
});
});
});

View File

@ -862,25 +862,6 @@ describe('render3 integration test', () => {
expect(fixture.html).toEqual('<div></div>');
});
it('should throw when trying to add event listener', () => {
/**
* <div><ng-container (click)="..."></ng-container></div>
*/
function Template() {
elementStart(0, 'div');
{
elementContainerStart(1);
{
listener('click', function() {});
}
elementContainerEnd();
}
elementEnd();
}
expect(() => { new TemplateFixture(Template, () => {}, 2); }).toThrow();
});
});
describe('tree', () => {