From 2bc39860bb9d35779234e6ad49495389993e0073 Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Fri, 30 Nov 2018 17:45:04 +0100 Subject: [PATCH] feat(ivy): support inputs & outputs with aliases in component decorators (#27350) PR Close #27350 --- .../compiler/src/render3/view/compiler.ts | 4 +- packages/compiler/src/render3/view/util.ts | 12 +- packages/core/src/render3/instructions.ts | 37 ++--- packages/core/test/linker/integration_spec.ts | 126 +++++++++--------- 4 files changed, 90 insertions(+), 89 deletions(-) diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 908a16569f..084f98fddf 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -30,7 +30,7 @@ import {typeWithParameters} from '../util'; import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api'; import {StylingBuilder, StylingInstruction} from './styling'; import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template'; -import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, mapToExpression, temporaryAllocator} from './util'; +import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util'; const EMPTY_ARRAY: any[] = []; @@ -864,4 +864,4 @@ function parseNamedProperty(name: string): {propertyName: string, unit: string} } } return {propertyName, unit}; -} \ No newline at end of file +} diff --git a/packages/compiler/src/render3/view/util.ts b/packages/compiler/src/render3/view/util.ts index 1fe4a48618..88210b9365 100644 --- a/packages/compiler/src/render3/view/util.ts +++ b/packages/compiler/src/render3/view/util.ts @@ -8,8 +8,8 @@ import {ConstantPool} from '../../constant_pool'; import * as o from '../../output/output_ast'; +import {splitAtColon} from '../../util'; import * as t from '../r3_ast'; - import {R3QueryMetadata} from './api'; import {isI18nAttribute} from './i18n/util'; @@ -75,9 +75,13 @@ export function conditionallyCreateMapObjectLiteral(keys: {[key: string]: string return null; } -export function mapToExpression(map: {[key: string]: any}, quoted = false): o.Expression { - return o.literalMap( - Object.getOwnPropertyNames(map).map(key => ({key, quoted, value: asLiteral(map[key])}))); +function mapToExpression(map: {[key: string]: any}): o.Expression { + return o.literalMap(Object.getOwnPropertyNames(map).map(key => { + // canonical syntax: `dirProp: elProp` + // if there is no `:`, use dirProp = elProp + const parts = splitAtColon(key, [key, map[key]]); + return {key: parts[0], quoted: false, value: asLiteral(parts[1])}; + })); } /** diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index a6abc8d5f4..87f404d4e7 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -958,7 +958,7 @@ export function elementProperty( if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET); if (ngDevMode) { if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) { - setNgReflectProperties(lView, element, tNode.type, propName, value); + setNgReflectProperties(lView, element, tNode.type, dataValue, value); } } } else if (tNode.type === TNodeType.Element) { @@ -1033,20 +1033,23 @@ function setInputsForProperty(lView: LView, inputs: PropertyAliasValue, value: a } function setNgReflectProperties( - lView: LView, element: RElement | RComment, type: TNodeType, propName: string, value: any) { - const renderer = lView[RENDERER]; - const attrName = normalizeDebugBindingName(propName); - const debugValue = normalizeDebugBindingValue(value); - if (type === TNodeType.Element) { - isProceduralRenderer(renderer) ? - renderer.setAttribute((element as RElement), attrName, debugValue) : - (element as RElement).setAttribute(attrName, debugValue); - } else if (value !== undefined) { - const value = `bindings=${JSON.stringify({[attrName]: debugValue}, null, 2)}`; - if (isProceduralRenderer(renderer)) { - renderer.setValue((element as RComment), value); - } else { - (element as RComment).textContent = value; + lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue, + value: any) { + for (let i = 0; i < inputs.length; i += 2) { + const renderer = lView[RENDERER]; + const attrName = normalizeDebugBindingName(inputs[i + 1] as string); + const debugValue = normalizeDebugBindingValue(value); + if (type === TNodeType.Element) { + isProceduralRenderer(renderer) ? + renderer.setAttribute((element as RElement), attrName, debugValue) : + (element as RElement).setAttribute(attrName, debugValue); + } else if (value !== undefined) { + const value = `bindings=${JSON.stringify({[attrName]: debugValue}, null, 2)}`; + if (isProceduralRenderer(renderer)) { + renderer.setValue((element as RComment), value); + } else { + (element as RComment).textContent = value; + } } } } @@ -1054,8 +1057,8 @@ function setNgReflectProperties( /** * Consolidates all inputs or outputs of all directives on this logical node. * - * @param number tNodeFlags node flags - * @param Direction direction whether to consider inputs or outputs + * @param tNodeFlags node flags + * @param direction whether to consider inputs or outputs * @returns PropertyAliases|null aggregate of all properties if any, `null` otherwise */ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): PropertyAliases|null { diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index e91675213d..9b026df04a 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -238,44 +238,42 @@ function declareTests(config?: {useJit: boolean}) { expect(getDOM().getProperty(nativeEl, 'htmlFor')).toBe('foo'); }); - fixmeIvy('FW-587: Inputs with aliases in component decorators don\'t work') - .it('should consume directive watch expression change.', () => { - TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); - const template = '' + - '
' + - '
' + - '
' + - '
' + - '
'; - TestBed.overrideComponent(MyComp, {set: {template}}); - const fixture = TestBed.createComponent(MyComp); + it('should consume directive watch expression change.', () => { + TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); + const template = '' + + '
' + + '
' + + '
' + + '
' + + '
'; + TestBed.overrideComponent(MyComp, {set: {template}}); + const fixture = TestBed.createComponent(MyComp); - fixture.componentInstance.ctxProp = 'Hello World!'; - fixture.detectChanges(); + fixture.componentInstance.ctxProp = 'Hello World!'; + fixture.detectChanges(); - const containerSpan = fixture.debugElement.children[0]; + const containerSpan = fixture.debugElement.children[0]; - expect(containerSpan.children[0].injector.get(MyDir).dirProp).toEqual('Hello World!'); - expect(containerSpan.children[1].injector.get(MyDir).dirProp).toEqual('Hi there!'); - expect(containerSpan.children[2].injector.get(MyDir).dirProp).toEqual('Hi there!'); - expect(containerSpan.children[3].injector.get(MyDir).dirProp) - .toEqual('One more Hello World!'); - }); + expect(containerSpan.children[0].injector.get(MyDir).dirProp).toEqual('Hello World!'); + expect(containerSpan.children[1].injector.get(MyDir).dirProp).toEqual('Hi there!'); + expect(containerSpan.children[2].injector.get(MyDir).dirProp).toEqual('Hi there!'); + expect(containerSpan.children[3].injector.get(MyDir).dirProp) + .toEqual('One more Hello World!'); + }); describe('pipes', () => { - fixmeIvy('FW-587: Inputs with aliases in component decorators don\'t work') - .it('should support pipes in bindings', () => { - TestBed.configureTestingModule({declarations: [MyComp, MyDir, DoublePipe]}); - const template = '
'; - TestBed.overrideComponent(MyComp, {set: {template}}); - const fixture = TestBed.createComponent(MyComp); + it('should support pipes in bindings', () => { + TestBed.configureTestingModule({declarations: [MyComp, MyDir, DoublePipe]}); + const template = '
'; + TestBed.overrideComponent(MyComp, {set: {template}}); + const fixture = TestBed.createComponent(MyComp); - fixture.componentInstance.ctxProp = 'a'; - fixture.detectChanges(); + fixture.componentInstance.ctxProp = 'a'; + fixture.detectChanges(); - const dir = fixture.debugElement.children[0].references !['dir']; - expect(dir.dirProp).toEqual('aa'); - }); + const dir = fixture.debugElement.children[0].references !['dir']; + expect(dir.dirProp).toEqual('aa'); + }); }); it('should support nested components.', () => { @@ -290,21 +288,20 @@ function declareTests(config?: {useJit: boolean}) { }); // GH issue 328 - https://github.com/angular/angular/issues/328 - fixmeIvy('FW-587: Inputs with aliases in component decorators don\'t work') - .it('should support different directive types on a single node', () => { - TestBed.configureTestingModule({declarations: [MyComp, ChildComp, MyDir]}); - const template = ''; - TestBed.overrideComponent(MyComp, {set: {template}}); - const fixture = TestBed.createComponent(MyComp); + it('should support different directive types on a single node', () => { + TestBed.configureTestingModule({declarations: [MyComp, ChildComp, MyDir]}); + const template = ''; + TestBed.overrideComponent(MyComp, {set: {template}}); + const fixture = TestBed.createComponent(MyComp); - fixture.componentInstance.ctxProp = 'Hello World!'; - fixture.detectChanges(); + fixture.componentInstance.ctxProp = 'Hello World!'; + fixture.detectChanges(); - const tc = fixture.debugElement.children[0]; + const tc = fixture.debugElement.children[0]; - expect(tc.injector.get(MyDir).dirProp).toEqual('Hello World!'); - expect(tc.injector.get(ChildComp).dirProp).toEqual(null); - }); + expect(tc.injector.get(MyDir).dirProp).toEqual('Hello World!'); + expect(tc.injector.get(ChildComp).dirProp).toEqual(null); + }); it('should support directives where a binding attribute is not given', () => { TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); @@ -1688,21 +1685,20 @@ function declareTests(config?: {useJit: boolean}) { }); describe('logging property updates', () => { - fixmeIvy('FW-587: Inputs with aliases in component decorators don\'t work') - .it('should reflect property values as attributes', () => { - TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); - const template = '
' + - '
' + - '
'; - TestBed.overrideComponent(MyComp, {set: {template}}); - const fixture = TestBed.createComponent(MyComp); + it('should reflect property values as attributes', () => { + TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); + const template = '
' + + '
' + + '
'; + TestBed.overrideComponent(MyComp, {set: {template}}); + const fixture = TestBed.createComponent(MyComp); - fixture.componentInstance.ctxProp = 'hello'; - fixture.detectChanges(); + fixture.componentInstance.ctxProp = 'hello'; + fixture.detectChanges(); - expect(getDOM().getInnerHTML(fixture.nativeElement)) - .toContain('ng-reflect-dir-prop="hello"'); - }); + expect(getDOM().getInnerHTML(fixture.nativeElement)) + .toContain('ng-reflect-dir-prop="hello"'); + }); it(`should work with prop names containing '$'`, () => { TestBed.configureTestingModule({declarations: [ParentCmp, SomeCmpWithInput]}); @@ -1726,17 +1722,15 @@ function declareTests(config?: {useJit: boolean}) { .toContain('"ng\-reflect\-ng\-if"\: "true"'); }); - // also affected by FW-587: Inputs with aliases in component decorators don't work - fixmeIvy('FW-664: ng-reflect-* is not supported') - .it('should indicate when toString() throws', () => { - TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); - const template = '
'; - TestBed.overrideComponent(MyComp, {set: {template}}); - const fixture = TestBed.createComponent(MyComp); + it('should indicate when toString() throws', () => { + TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); + const template = '
'; + TestBed.overrideComponent(MyComp, {set: {template}}); + const fixture = TestBed.createComponent(MyComp); - fixture.detectChanges(); - expect(getDOM().getInnerHTML(fixture.nativeElement)).toContain('[ERROR]'); - }); + fixture.detectChanges(); + expect(getDOM().getInnerHTML(fixture.nativeElement)).toContain('[ERROR]'); + }); }); describe('property decorators', () => {