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[];
+}