diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 4890c89868..bf18bfe0c0 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -361,7 +361,6 @@ describe('compiler compliance', () => { // `$r3$.ɵɵproperty("ternary", (ctx.cond ? $r3$.ɵɵpureFunction1(8, $c0$, ctx.a): $c1$));` /////////////// - const $e0_attrs$ = []; const factory = 'factory: function MyComponent_Factory(t) { return new (t || MyComponent)(); }'; const template = ` @@ -371,10 +370,7 @@ describe('compiler compliance', () => { $r3$.ɵɵpipe(1,"pipe"); } if (rf & 2) { - $r3$.ɵɵproperty("ternary", ctx.cond ? $r3$.ɵɵpureFunction1(8, $c0$, ctx.a): $c1$); - $r3$.ɵɵproperty("pipe", $r3$.ɵɵpipeBind3(1, 4, ctx.value, 1, 2)); - $r3$.ɵɵproperty("and", ctx.cond && $r3$.ɵɵpureFunction1(10, $c0$, ctx.b)); - $r3$.ɵɵproperty("or", ctx.cond || $r3$.ɵɵpureFunction1(12, $c0$, ctx.c)); + $r3$.ɵɵproperty("ternary", ctx.cond ? $r3$.ɵɵpureFunction1(8, $c0$, ctx.a): $c1$)("pipe", $r3$.ɵɵpipeBind3(1, 4, ctx.value, 1, 2))("and", ctx.cond && $r3$.ɵɵpureFunction1(10, $c0$, ctx.b))("or", ctx.cond || $r3$.ɵɵpureFunction1(12, $c0$, ctx.c)); } } `; diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts index 4faccf475c..b522b764cb 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts @@ -180,6 +180,232 @@ describe('compiler compliance: bindings', () => { expectEmit(result.source, template, 'Incorrect template'); }); + it('should chain multiple property bindings into a single instruction', () => { + const files = { + app: { + 'example.ts': ` + import {Component} from '@angular/core'; + + @Component({ + template: '' + }) + export class MyComponent { + myTitle = 'hello'; + buttonId = 'special-button'; + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + template: function MyComponent_Template(rf, ctx) { + … + if (rf & 2) { + $r3$.ɵɵproperty("title", ctx.myTitle)("id", ctx.buttonId)("tabindex", 1); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain property bindings in the presence of other instructions', () => { + const files = { + app: { + 'example.ts': ` + import {Component} from '@angular/core'; + + @Component({ + template: '' + }) + export class MyComponent {}` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + template: function MyComponent_Template(rf, ctx) { + … + if (rf & 2) { + $r3$.ɵɵattribute("id", 2); + $r3$.ɵɵpropertyInterpolate("aria-label", 1 + 3); + $r3$.ɵɵproperty("title", 1)("tabindex", 3); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should not add interpolated properties to the property instruction chain', () => { + const files = { + app: { + 'example.ts': ` + import {Component} from '@angular/core'; + + @Component({ + template: '' + }) + export class MyComponent {}` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + template: function MyComponent_Template(rf, ctx) { + … + if (rf & 2) { + $r3$.ɵɵpropertyInterpolate("tabindex", 0 + 3); + $r3$.ɵɵpropertyInterpolate2("aria-label", "hello-", 1 + 3, "-", 2 + 3, ""); + $r3$.ɵɵproperty("title", 1)("id", 2); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain synthetic property bindings together with regular property bindings', () => { + const files = { + app: { + 'example.ts': ` + import {Component} from '@angular/core'; + + @Component({ + template: \` + + \` + }) + export class MyComponent { + expansionState = 'expanded'; + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + template: function MyComponent_Template(rf, ctx) { + … + if (rf & 2) { + $r3$.ɵɵproperty("title", ctx.myTitle)("@expand", ctx.expansionState)("tabindex", 1)("@fade", "out"); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain multiple property bindings on an ng-template', () => { + const files = { + app: { + 'example.ts': ` + import {Component} from '@angular/core'; + + @Component({ + template: '' + }) + export class MyComponent { + myTitle = 'hello'; + buttonId = 'custom-id'; + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + template: function MyComponent_Template(rf, ctx) { + … + if (rf & 2) { + $r3$.ɵɵproperty("title", ctx.myTitle)("id", ctx.buttonId)("tabindex", 1); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain multiple property bindings when there are multiple elements', () => { + const files = { + app: { + 'example.ts': ` + import {Component} from '@angular/core'; + + @Component({ + template: \` + + + + \` + }) + export class MyComponent { + myTitle = 'hello'; + buttonId = 'special-button'; + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + template: function MyComponent_Template(rf, ctx) { + … + if (rf & 2) { + $r3$.ɵɵproperty("title", ctx.myTitle)("id", ctx.buttonId)("tabindex", 1); + $r3$.ɵɵselect(1); + $r3$.ɵɵproperty("id", 1)("title", "hello")("someProp", 1 + 2); + $r3$.ɵɵselect(2); + $r3$.ɵɵproperty("prop", "one")("otherProp", 2); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain multiple property bindings when there are child elements', () => { + const files = { + app: { + 'example.ts': ` + import {Component} from '@angular/core'; + + @Component({ + template: \` + \` + }) + export class MyComponent { + myTitle = 'hello'; + buttonId = 'special-button'; + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + template: function MyComponent_Template(rf, ctx) { + … + if (rf & 2) { + $r3$.ɵɵproperty("title", ctx.myTitle)("id", ctx.buttonId)("tabindex", 1); + $r3$.ɵɵselect(1); + $r3$.ɵɵproperty("id", 1)("title", "hello")("someProp", 1 + 2); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + }); describe('host bindings', () => { diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts index 22e570148e..68cc862a12 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts @@ -154,8 +154,7 @@ describe('r3_view_compiler', () => { $i0$.ɵɵelement(0, "div"); } if (rf & 2) { - $i0$.ɵɵproperty("@attr", …); - $i0$.ɵɵproperty("@binding", …); + $i0$.ɵɵproperty("@attr", …)("@binding", …); } }`; const result = compile(files, angularFiles); diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 4a6d09e1e6..96bb460792 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -712,6 +712,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // special value to symbolize that there is no RHS to this binding // TODO (matsko): revisit this once FW-959 is approached const emptyValueBindInstruction = o.literal(undefined); + const propertyBindings: ChainablePropertyBinding[] = []; // Generate element input bindings allOtherInputs.forEach((input: t.BoundAttribute) => { @@ -729,14 +730,12 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // defined in... const hasValue = value instanceof LiteralPrimitive ? !!value.value : true; this.allocateBindingSlots(value); - const bindingName = prepareSyntheticPropertyName(input.name); - this.updateInstruction(elementIndex, input.sourceSpan, R3.property, () => { - return [ - o.literal(bindingName), - (hasValue ? this.convertPropertyBinding(value, /* skipBindFn */ true) : - emptyValueBindInstruction), - ]; + propertyBindings.push({ + name: prepareSyntheticPropertyName(input.name), + input, + value: () => hasValue ? this.convertPropertyBinding(value, /* skipBindFn */ true) : + emptyValueBindInstruction }); } else { // we must skip attributes with associated i18n context, since these attributes are handled @@ -771,8 +770,12 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver params); } else { // [prop]="value" - this.boundUpdateInstruction( - R3.property, elementIndex, attrName, input, value, params); + // Collect all the properties so that we can chain into a single function at the end. + propertyBindings.push({ + name: attrName, + input, + value: () => this.convertPropertyBinding(value, true), params + }); } } else if (inputType === BindingType.Attribute) { if (value instanceof Interpolation && getInterpolationArgsLength(value) > 1) { @@ -799,6 +802,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } }); + if (propertyBindings.length > 0) { + this.propertyInstructionChain(elementIndex, propertyBindings); + } + // Traverse element child nodes t.visitAll(this, element.children); @@ -912,12 +919,12 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); // handle property bindings e.g. ɵɵproperty('ngForOf', ctx.items), et al; - this.templatePropertyBindings(template, templateIndex, template.templateAttrs); + this.templatePropertyBindings(templateIndex, template.templateAttrs); // Only add normal input/output binding instructions on explicit ng-template elements. if (template.tagName === NG_TEMPLATE_TAG_NAME) { // Add the input bindings - this.templatePropertyBindings(template, templateIndex, template.inputs); + this.templatePropertyBindings(templateIndex, template.inputs); // Generate listeners for directive output template.outputs.forEach((outputAst: t.BoundEvent) => { this.creationInstruction( @@ -1024,21 +1031,26 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver private bindingContext() { return `${this._bindingContext++}`; } private templatePropertyBindings( - template: t.Template, templateIndex: number, attrs: (t.BoundAttribute|t.TextAttribute)[]) { + templateIndex: number, attrs: (t.BoundAttribute|t.TextAttribute)[]) { + const propertyBindings: ChainablePropertyBinding[] = []; attrs.forEach(input => { if (input instanceof t.BoundAttribute) { const value = input.value.visit(this._valueConverter); if (value !== undefined) { this.allocateBindingSlots(value); - this.updateInstruction( - templateIndex, template.sourceSpan, R3.property, - () => - [o.literal(input.name), - this.convertPropertyBinding(value, /* skipBindFn */ true)]); + propertyBindings.push({ + name: input.name, + input, + value: () => this.convertPropertyBinding(value, /* skipBindFn */ true) + }); } } }); + + if (propertyBindings.length > 0) { + this.propertyInstructionChain(templateIndex, propertyBindings); + } } // Bindings must only be resolved after all local refs have been visited, so all @@ -1077,13 +1089,37 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver private updateInstruction( nodeIndex: number, span: ParseSourceSpan|null, reference: o.ExternalReference, paramsOrFn?: o.Expression[]|(() => o.Expression[])) { + this.addSelectInstructionIfNecessary(nodeIndex, span); + this.instructionFn(this._updateCodeFns, span, reference, paramsOrFn || []); + } + + private updateInstructionChain( + nodeIndex: number, span: ParseSourceSpan|null, reference: o.ExternalReference, + callsOrFn?: o.Expression[][]|(() => o.Expression[][])) { + this.addSelectInstructionIfNecessary(nodeIndex, span); + this._updateCodeFns.push(() => { + const calls = typeof callsOrFn === 'function' ? callsOrFn() : callsOrFn; + return chainedInstruction(span, reference, calls || []).toStmt(); + }); + } + + private propertyInstructionChain( + nodeIndex: number, propertyBindings: ChainablePropertyBinding[]) { + this.updateInstructionChain( + nodeIndex, propertyBindings.length ? propertyBindings[0].input.sourceSpan : null, + R3.property, () => { + return propertyBindings.map( + property => [o.literal(property.name), property.value(), ...(property.params || [])]); + }); + } + + private addSelectInstructionIfNecessary(nodeIndex: number, span: ParseSourceSpan|null) { if (this._lastNodeIndexWithFlush < nodeIndex) { if (nodeIndex > 0) { this.instructionFn(this._updateCodeFns, span, R3.select, [o.literal(nodeIndex)]); } this._lastNodeIndexWithFlush = nodeIndex; } - this.instructionFn(this._updateCodeFns, span, reference, paramsOrFn || []); } private allocatePureFunctionSlots(numSlots: number): number { @@ -1388,6 +1424,22 @@ function instruction( return o.importExpr(reference, null, span).callFn(params, span); } +function chainedInstruction( + span: ParseSourceSpan | null, reference: o.ExternalReference, calls: o.Expression[][]) { + let expression = o.importExpr(reference, null, span) as o.Expression; + + if (calls.length > 0) { + for (let i = 0; i < calls.length; i++) { + expression = expression.callFn(calls[i], span); + } + } else { + // Add a blank invocation, in case the `calls` array is empty. + expression = expression.callFn([], span); + } + + return expression; +} + // e.g. x(2); function generateNextContextExpr(relativeLevelDiff: number): o.Expression { return o.importExpr(R3.nextContext) @@ -1979,3 +2031,10 @@ function isTextNode(node: t.Node): boolean { function hasTextChildrenOnly(children: t.Node[]): boolean { return children.every(isTextNode); } + +interface ChainablePropertyBinding { + name: string; + input: t.BoundAttribute; + value: () => o.Expression; + params?: any[]; +}