perf(ivy): chain styling instructions (#33837)

Adds support for chaining of `styleProp`, `classProp` and `stylePropInterpolateX` instructions whenever possible which should help generate less code. Note that one complication here is for `stylePropInterpolateX` instructions where we have to break into multiple chains if there are other styling instructions inbetween the interpolations which helps maintain the execution order.

PR Close #33837
This commit is contained in:
Kristiyan Kostadinov 2019-11-18 20:13:23 +01:00 committed by Alex Rickabaugh
parent f69c6e204a
commit 8a052dc858
6 changed files with 465 additions and 94 deletions

View File

@ -513,8 +513,7 @@ describe('compiler compliance: styling', () => {
if (rf & 2) { if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($ctx$.myStyleExp); $r3$.ɵɵstyleMap($ctx$.myStyleExp);
$r3$.ɵɵstyleProp("width", $ctx$.myWidth); $r3$.ɵɵstyleProp("width", $ctx$.myWidth)("height", $ctx$.myHeight);
$r3$.ɵɵstyleProp("height", $ctx$.myHeight);
$r3$.ɵɵattribute("style", "border-width: 10px", $r3$.ɵɵsanitizeStyle); $r3$.ɵɵattribute("style", "border-width: 10px", $r3$.ɵɵsanitizeStyle);
} }
}, },
@ -706,8 +705,7 @@ describe('compiler compliance: styling', () => {
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵɵclassMap($ctx$.myClassExp); $r3$.ɵɵclassMap($ctx$.myClassExp);
$r3$.ɵɵclassProp("apple", $ctx$.yesToApple); $r3$.ɵɵclassProp("apple", $ctx$.yesToApple)("orange", $ctx$.yesToOrange);
$r3$.ɵɵclassProp("orange", $ctx$.yesToOrange);
$r3$.ɵɵattribute("class", "banana"); $r3$.ɵɵattribute("class", "banana");
} }
}, },
@ -916,8 +914,7 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 8, $ctx$.myStyleExp, 1000)); $r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 8, $ctx$.myStyleExp, 1000));
$r3$.ɵɵclassMap($r3$.ɵɵpureFunction0(20, _c0)); $r3$.ɵɵclassMap($r3$.ɵɵpureFunction0(20, _c0));
$r3$.ɵɵstyleProp("bar", $r3$.ɵɵpipeBind2(2, 11, $ctx$.barExp, 3000)); $r3$.ɵɵstyleProp("bar", $r3$.ɵɵpipeBind2(2, 11, $ctx$.barExp, 3000))("baz", $r3$.ɵɵpipeBind2(3, 14, $ctx$.bazExp, 4000));
$r3$.ɵɵstyleProp("baz", $r3$.ɵɵpipeBind2(3, 14, $ctx$.bazExp, 4000));
$r3$.ɵɵclassProp("foo", $r3$.ɵɵpipeBind2(4, 17, $ctx$.fooExp, 2000)); $r3$.ɵɵclassProp("foo", $r3$.ɵɵpipeBind2(4, 17, $ctx$.fooExp, 2000));
$r3$.ɵɵadvance(5); $r3$.ɵɵadvance(5);
$r3$.ɵɵtextInterpolate1(" ", $ctx$.item, ""); $r3$.ɵɵtextInterpolate1(" ", $ctx$.item, "");
@ -1081,10 +1078,8 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyle); $r3$.ɵɵstyleMap(ctx.myStyle);
$r3$.ɵɵclassMap(ctx.myClasses); $r3$.ɵɵclassMap(ctx.myClasses);
$r3$.ɵɵstyleProp("height", ctx.myHeightProp, "pt"); $r3$.ɵɵstyleProp("height", ctx.myHeightProp, "pt")("width", ctx.myWidthProp);
$r3$.ɵɵstyleProp("width", ctx.myWidthProp); $r3$.ɵɵclassProp("bar", ctx.myBarClass)("foo", ctx.myFooClass);
$r3$.ɵɵclassProp("bar", ctx.myBarClass);
$r3$.ɵɵclassProp("foo", ctx.myFooClass);
} }
}, },
decls: 0, decls: 0,
@ -1461,6 +1456,351 @@ describe('compiler compliance: styling', () => {
}); });
describe('instruction chaining', () => {
it('should chain classProp instruction calls', () => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: \`<div [class.apple]="yesToApple"
[class.orange]="yesToOrange"
[class.tomato]="yesToTomato"></div>\`
})
export class MyComponent {
yesToApple = true;
yesToOrange = true;
tesToTomato = false;
}
`
}
};
const template = `
MyComponent.ɵcmp = $r3$.ɵɵdefineComponent({
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 2) {
$r3$.ɵɵclassProp("apple", $ctx$.yesToApple)("orange", $ctx$.yesToOrange)("tomato", $ctx$.yesToTomato);
}
},
encapsulation: 2
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should chain styleProp instruction calls', () => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: \`<div [style.color]="color"
[style.border]="border"
[style.transition]="transition"></div>\`
})
export class MyComponent {
color = 'red';
border = '1px solid purple';
transition = 'all 1337ms ease';
}
`
}
};
const template = `
MyComponent.ɵcmp = $r3$.ɵɵdefineComponent({
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 2) {
$r3$.ɵɵstyleProp("color", $ctx$.color)("border", $ctx$.border)("transition", $ctx$.transition);
}
},
encapsulation: 2
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should chain mixed styleProp and classProp calls', () => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: \`<div
[class.apple]="yesToApple"
[style.color]="color"
[class.orange]="yesToOrange"
[style.border]="border"
[class.tomato]="yesToTomato"
[style.transition]="transition"></div>\`
})
export class MyComponent {
color = 'red';
border = '1px solid purple';
transition = 'all 1337ms ease';
yesToApple = true;
yesToOrange = true;
tesToTomato = false;
}
`
}
};
const template = `
MyComponent.ɵcmp = $r3$.ɵɵdefineComponent({
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 2) {
$r3$.ɵɵstyleProp("color", $ctx$.color)("border", $ctx$.border)("transition", $ctx$.transition);
$r3$.ɵɵclassProp("apple", $ctx$.yesToApple)("orange", $ctx$.yesToOrange)("tomato", $ctx$.yesToTomato);
}
},
encapsulation: 2
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should chain style interpolations of the same kind', () => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: \`<div
style.color="a{{one}}b"
style.border="a{{one}}b"
style.transition="a{{one}}b"></div>\`
})
export class MyComponent {
}
`
}
};
const template = `
MyComponent.ɵcmp = $r3$.ɵɵdefineComponent({
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 2) {
$r3$.ɵɵstylePropInterpolate1("color", "a", ctx.one, "b")("border", "a", ctx.one, "b")("transition", "a", ctx.one, "b");
}
},
encapsulation: 2
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should chain style interpolations of multiple kinds', () => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: \`<div
style.color="a{{one}}b"
style.border="a{{one}}b"
style.transition="a{{one}}b{{two}}c"
style.width="a{{one}}b{{two}}c"
style.height="a{{one}}b{{two}}c{{three}}d"
style.top="a{{one}}b{{two}}c{{three}}d"></div>\`
})
export class MyComponent {
}
`
}
};
const template = `
MyComponent.ɵcmp = $r3$.ɵɵdefineComponent({
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 2) {
$r3$.ɵɵstylePropInterpolate1("color", "a", ctx.one, "b")("border", "a", ctx.one, "b");
$r3$.ɵɵstylePropInterpolate2("transition", "a", ctx.one, "b", ctx.two, "c")("width", "a", ctx.one, "b", ctx.two, "c");
$r3$.ɵɵstylePropInterpolate3("height", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d")("top", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d");
}
},
encapsulation: 2
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should break into multiple chains if there are other styling instructions in between',
() => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: \`<div
style.color="a{{one}}b"
style.border="a{{one}}b"
[class.apple]="yesToApple"
[style.transition]="transition"
[class.orange]="yesToOrange"
[style.width]="width"
style.height="a{{one}}b"
style.top="a{{one}}b"></div>\`
})
export class MyComponent {
transition = 'all 1337ms ease';
width = '42px';
yesToApple = true;
yesToOrange = true;
}
`
}
};
const template = `
MyComponent.ɵcmp = $r3$.ɵɵdefineComponent({
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 2) {
$r3$.ɵɵstylePropInterpolate1("color", "a", ctx.one, "b")("border", "a", ctx.one, "b");
$r3$.ɵɵstyleProp("transition", ctx.transition)("width", ctx.width);
$r3$.ɵɵstylePropInterpolate1("height", "a", ctx.one, "b")("top", "a", ctx.one, "b");
$r3$.ɵɵclassProp("apple", ctx.yesToApple)("orange", ctx.yesToOrange);
}
},
encapsulation: 2
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should break into multiple chains if there are other styling interpolation instructions in between',
() => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: \`<div
style.color="a{{one}}b"
style.border="a{{one}}b"
style.transition="a{{one}}b{{two}}c"
style.width="a{{one}}b{{two}}c{{three}}d"
style.height="a{{one}}b"
style.top="a{{one}}b"></div>\`
})
export class MyComponent {
transition = 'all 1337ms ease';
width = '42px';
}
`
}
};
const template = `
MyComponent.ɵcmp = $r3$.ɵɵdefineComponent({
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 2) {
$r3$.ɵɵstylePropInterpolate1("color", "a", ctx.one, "b")("border", "a", ctx.one, "b");
$r3$.ɵɵstylePropInterpolate2("transition", "a", ctx.one, "b", ctx.two, "c");
$r3$.ɵɵstylePropInterpolate3("width", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d");
$r3$.ɵɵstylePropInterpolate1("height", "a", ctx.one, "b")("top", "a", ctx.one, "b");
}
},
encapsulation: 2
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should chain styling instructions inside host bindings', () => {
const files = {
app: {
'spec.ts': `
import {Component, HostBinding} from '@angular/core';
@Component({
template: '',
host: {
'[class.apple]': 'yesToApple',
'[style.color]': 'color',
'[class.tomato]': 'yesToTomato',
'[style.transition]': 'transition'
}
})
export class MyComponent {
color = 'red';
transition = 'all 1337ms ease';
yesToApple = true;
tesToTomato = false;
@HostBinding('style.border')
border = '1px solid purple';
@HostBinding('class.orange')
yesToOrange = true;
}
`
}
};
const template = `
MyComponent.ɵcmp = $r3$.ɵɵdefineComponent({
hostBindings: function MyComponent_HostBindings(rf, $ctx$, elIndex) {
if (rf & 2) {
$r3$.ɵɵstyleProp("color", $ctx$.color)("transition", $ctx$.transition)("border", $ctx$.border);
$r3$.ɵɵclassProp("apple", $ctx$.yesToApple)("tomato", $ctx$.yesToTomato)("orange", $ctx$.yesToOrange);
}
},
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});
it('should count only non-style and non-class host bindings on Components', () => { it('should count only non-style and non-class host bindings on Components', () => {
const files = { const files = {
app: { app: {

View File

@ -28,7 +28,7 @@ import {Render3ParseResult} from '../r3_template_transform';
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util'; import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './api'; import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './api';
import {StylingBuilder, StylingInstruction} from './styling_builder'; import {StylingBuilder, StylingInstructionCall} from './styling_builder';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template'; import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, chainedInstruction, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util'; import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, chainedInstruction, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
@ -652,8 +652,12 @@ function createHostBindingsFunction(
// collected earlier. // collected earlier.
const hostAttrs = convertAttributesToExpressions(hostBindingsMetadata.attributes); const hostAttrs = convertAttributesToExpressions(hostBindingsMetadata.attributes);
const hostInstruction = styleBuilder.buildHostAttrsInstruction(null, hostAttrs, constantPool); const hostInstruction = styleBuilder.buildHostAttrsInstruction(null, hostAttrs, constantPool);
if (hostInstruction) { if (hostInstruction && hostInstruction.calls.length > 0) {
createStatements.push(createStylingStmt(hostInstruction, bindingContext, bindingFn)); createStatements.push(
chainedInstruction(
hostInstruction.reference,
hostInstruction.calls.map(call => convertStylingCall(call, bindingContext, bindingFn)))
.toStmt());
} }
if (styleBuilder.hasBindings) { if (styleBuilder.hasBindings) {
@ -661,11 +665,18 @@ function createHostBindingsFunction(
// the update block of a component/directive templateFn/hostBindingsFn so that the bindings // the update block of a component/directive templateFn/hostBindingsFn so that the bindings
// are evaluated and updated for the element. // are evaluated and updated for the element.
styleBuilder.buildUpdateLevelInstructions(getValueConverter()).forEach(instruction => { styleBuilder.buildUpdateLevelInstructions(getValueConverter()).forEach(instruction => {
// we subtract a value of `1` here because the binding slot was already if (instruction.calls.length > 0) {
// allocated at the top of this method when all the input bindings were const calls: o.Expression[][] = [];
// counted.
totalHostVarsCount += Math.max(instruction.allocateBindingSlots - 1, 0); instruction.calls.forEach(call => {
updateStatements.push(createStylingStmt(instruction, bindingContext, bindingFn)); // we subtract a value of `1` here because the binding slot was already allocated
// at the top of this method when all the input bindings were counted.
totalHostVarsCount += Math.max(call.allocateBindingSlots - 1, 0);
calls.push(convertStylingCall(call, bindingContext, bindingFn));
});
updateStatements.push(chainedInstruction(instruction.reference, calls).toStmt());
}
}); });
} }
@ -699,12 +710,9 @@ function bindingFn(implicit: any, value: AST) {
null, implicit, value, 'b', BindingForm.TrySimple, () => error('Unexpected interpolation')); null, implicit, value, 'b', BindingForm.TrySimple, () => error('Unexpected interpolation'));
} }
function createStylingStmt( function convertStylingCall(
instruction: StylingInstruction, bindingContext: any, bindingFn: Function): o.Statement { call: StylingInstructionCall, bindingContext: any, bindingFn: Function) {
const params = instruction.params(value => bindingFn(bindingContext, value).currValExpr); return call.params(value => bindingFn(bindingContext, value).currValExpr);
return o.importExpr(instruction.reference, null, instruction.sourceSpan)
.callFn(params, instruction.sourceSpan)
.toStmt();
} }
function getBindingNameAndInstruction(binding: ParsedProperty): function getBindingNameAndInstruction(binding: ParsedProperty):

View File

@ -11,7 +11,6 @@ import {AST, ASTWithSource, BindingPipe, BindingType, Interpolation} from '../..
import * as o from '../../output/output_ast'; import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util'; import {ParseSourceSpan} from '../../parse_util';
import {isEmptyExpression} from '../../template_parser/template_parser'; import {isEmptyExpression} from '../../template_parser/template_parser';
import {error} from '../../util';
import * as t from '../r3_ast'; import * as t from '../r3_ast';
import {Identifiers as R3} from '../r3_identifiers'; import {Identifiers as R3} from '../r3_identifiers';
@ -25,10 +24,15 @@ const IMPORTANT_FLAG = '!important';
* A styling expression summary that is to be processed by the compiler * A styling expression summary that is to be processed by the compiler
*/ */
export interface StylingInstruction { export interface StylingInstruction {
sourceSpan: ParseSourceSpan|null;
reference: o.ExternalReference; reference: o.ExternalReference;
allocateBindingSlots: number; /** Calls to individual styling instructions. Used when chaining calls to the same instruction. */
calls: StylingInstructionCall[];
}
export interface StylingInstructionCall {
sourceSpan: ParseSourceSpan|null;
supportsInterpolation?: boolean; supportsInterpolation?: boolean;
allocateBindingSlots: number;
params: ((convertFn: (value: any) => o.Expression | o.Expression[]) => o.Expression[]); params: ((convertFn: (value: any) => o.Expression | o.Expression[]) => o.Expression[]);
} }
@ -280,17 +284,19 @@ export class StylingBuilder {
constantPool: ConstantPool): StylingInstruction|null { constantPool: ConstantPool): StylingInstruction|null {
if (this._directiveExpr && (attrs.length || this._hasInitialValues)) { if (this._directiveExpr && (attrs.length || this._hasInitialValues)) {
return { return {
sourceSpan,
reference: R3.elementHostAttrs, reference: R3.elementHostAttrs,
allocateBindingSlots: 0, calls: [{
params: () => { sourceSpan,
// params => elementHostAttrs(attrs) allocateBindingSlots: 0,
this.populateInitialStylingAttrs(attrs); params: () => {
const attrArray = !attrs.some(attr => attr instanceof o.WrappedNodeExpr) ? // params => elementHostAttrs(attrs)
getConstantLiteralFromArray(constantPool, attrs) : this.populateInitialStylingAttrs(attrs);
o.literalArr(attrs); const attrArray = !attrs.some(attr => attr instanceof o.WrappedNodeExpr) ?
return [attrArray]; getConstantLiteralFromArray(constantPool, attrs) :
} o.literalArr(attrs);
return [attrArray];
}
}]
}; };
} }
return null; return null;
@ -344,14 +350,16 @@ export class StylingBuilder {
} }
return { return {
sourceSpan: stylingInput.sourceSpan,
reference, reference,
allocateBindingSlots: totalBindingSlotsRequired, calls: [{
supportsInterpolation: isClassBased, supportsInterpolation: isClassBased,
params: (convertFn: (value: any) => o.Expression | o.Expression[]) => { sourceSpan: stylingInput.sourceSpan,
const convertResult = convertFn(mapValue); allocateBindingSlots: totalBindingSlotsRequired,
return Array.isArray(convertResult) ? convertResult : [convertResult]; params: (convertFn: (value: any) => o.Expression | o.Expression[]) => {
} const convertResult = convertFn(mapValue);
return Array.isArray(convertResult) ? convertResult : [convertResult];
}
}]
}; };
} }
@ -360,25 +368,27 @@ export class StylingBuilder {
allowUnits: boolean, valueConverter: ValueConverter, allowUnits: boolean, valueConverter: ValueConverter,
getInterpolationExpressionFn?: (value: Interpolation) => o.ExternalReference): getInterpolationExpressionFn?: (value: Interpolation) => o.ExternalReference):
StylingInstruction[] { StylingInstruction[] {
let totalBindingSlotsRequired = 0; const instructions: StylingInstruction[] = [];
return inputs.map(input => {
const value = input.value.visit(valueConverter);
// each styling binding value is stored in the LView inputs.forEach(input => {
let totalBindingSlotsRequired = 1; const previousInstruction: StylingInstruction|undefined =
instructions[instructions.length - 1];
const value = input.value.visit(valueConverter);
let referenceForCall = reference;
let totalBindingSlotsRequired = 1; // each styling binding value is stored in the LView
if (value instanceof Interpolation) { if (value instanceof Interpolation) {
totalBindingSlotsRequired += value.expressions.length; totalBindingSlotsRequired += value.expressions.length;
if (getInterpolationExpressionFn) { if (getInterpolationExpressionFn) {
reference = getInterpolationExpressionFn(value); referenceForCall = getInterpolationExpressionFn(value);
} }
} }
return { const call = {
sourceSpan: input.sourceSpan, sourceSpan: input.sourceSpan,
allocateBindingSlots: totalBindingSlotsRequired,
supportsInterpolation: !!getInterpolationExpressionFn, supportsInterpolation: !!getInterpolationExpressionFn,
allocateBindingSlots: totalBindingSlotsRequired, reference,
params: (convertFn: (value: any) => o.Expression | o.Expression[]) => { params: (convertFn: (value: any) => o.Expression | o.Expression[]) => {
// params => stylingProp(propName, value) // params => stylingProp(propName, value)
const params: o.Expression[] = []; const params: o.Expression[] = [];
@ -398,7 +408,20 @@ export class StylingBuilder {
return params; return params;
} }
}; };
// If we ended up generating a call to the same instruction as the previous styling property
// we can chain the calls together safely to save some bytes, otherwise we have to generate
// a separate instruction call. This is primarily a concern with interpolation instructions
// where we may start off with one `reference`, but end up using another based on the
// number of interpolations.
if (previousInstruction && previousInstruction.reference === referenceForCall) {
previousInstruction.calls.push(call);
} else {
instructions.push({reference: referenceForCall, calls: [call]});
}
}); });
return instructions;
} }
private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] { private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] {
@ -420,10 +443,12 @@ export class StylingBuilder {
private _buildSanitizerFn(): StylingInstruction { private _buildSanitizerFn(): StylingInstruction {
return { return {
sourceSpan: this._firstStylingInput ? this._firstStylingInput.sourceSpan : null,
reference: R3.styleSanitizer, reference: R3.styleSanitizer,
allocateBindingSlots: 0, calls: [{
params: () => [o.importExpr(R3.defaultStyleSanitizer)] sourceSpan: this._firstStylingInput ? this._firstStylingInput.sourceSpan : null,
allocateBindingSlots: 0,
params: () => [o.importExpr(R3.defaultStyleSanitizer)]
}]
}; };
} }
@ -476,20 +501,6 @@ function getConstantLiteralFromArray(
return values.length ? constantPool.getConstLiteral(o.literalArr(values), true) : o.NULL_EXPR; return values.length ? constantPool.getConstLiteral(o.literalArr(values), true) : o.NULL_EXPR;
} }
/**
* Simple helper function that adds a parameter or does nothing at all depending on the provided
* predicate and totalExpectedArgs values
*/
function addParam(
params: o.Expression[], predicate: any, value: o.Expression | null, argNumber: number,
totalExpectedArgs: number) {
if (predicate && value) {
params.push(value);
} else if (argNumber < totalExpectedArgs) {
params.push(o.NULL_EXPR);
}
}
export function parseProperty(name: string): export function parseProperty(name: string):
{property: string, unit: string, hasOverrideFlag: boolean} { {property: string, unit: string, hasOverrideFlag: boolean} {
let hasOverrideFlag = false; let hasOverrideFlag = false;

View File

@ -708,8 +708,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const limit = stylingInstructions.length - 1; const limit = stylingInstructions.length - 1;
for (let i = 0; i <= limit; i++) { for (let i = 0; i <= limit; i++) {
const instruction = stylingInstructions[i]; const instruction = stylingInstructions[i];
this._bindingSlots += instruction.allocateBindingSlots; this._bindingSlots += this.processStylingUpdateInstruction(elementIndex, instruction);
this.processStylingInstruction(elementIndex, instruction, false);
} }
// the reason why `undefined` is used is because the renderer understands this as a // the reason why `undefined` is used is because the renderer understands this as a
@ -1071,25 +1070,30 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}); });
} }
private processStylingInstruction( private processStylingUpdateInstruction(
elementIndex: number, instruction: StylingInstruction|null, createMode: boolean) { elementIndex: number, instruction: StylingInstruction|null) {
let allocateBindingSlots = 0;
if (instruction) { if (instruction) {
if (createMode) { const calls: ChainableBindingInstruction[] = [];
this.creationInstruction(instruction.sourceSpan, instruction.reference, () => {
return instruction.params(value => this.convertPropertyBinding(value)) as o.Expression[]; instruction.calls.forEach(call => {
}); allocateBindingSlots += call.allocateBindingSlots;
} else { calls.push({
this.updateInstructionWithAdvance( sourceSpan: call.sourceSpan,
elementIndex, instruction.sourceSpan, instruction.reference, () => { value: () => {
return instruction return call
.params(value => { .params(
return (instruction.supportsInterpolation && value instanceof Interpolation) ? value => (call.supportsInterpolation && value instanceof Interpolation) ?
this.getUpdateInstructionArguments(value) : this.getUpdateInstructionArguments(value) :
this.convertPropertyBinding(value); this.convertPropertyBinding(value)) as o.Expression[];
}) as o.Expression[]; }
}); });
} });
this.updateInstructionChainWithAdvance(elementIndex, instruction.reference, calls);
} }
return allocateBindingSlots;
} }
private creationInstruction( private creationInstruction(
@ -1127,8 +1131,13 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this._updateCodeFns.push(() => { this._updateCodeFns.push(() => {
const calls = bindings.map(property => { const calls = bindings.map(property => {
const fnParams = [property.value(), ...(property.params || [])]; const value = property.value();
const fnParams = Array.isArray(value) ? value : [value];
if (property.params) {
fnParams.push(...property.params);
}
if (property.name) { if (property.name) {
// We want the property name to always be the first function parameter.
fnParams.unshift(o.literal(property.name)); fnParams.unshift(o.literal(property.name));
} }
return fnParams; return fnParams;
@ -2045,7 +2054,7 @@ function hasTextChildrenOnly(children: t.Node[]): boolean {
interface ChainableBindingInstruction { interface ChainableBindingInstruction {
name?: string; name?: string;
sourceSpan: ParseSourceSpan|null; sourceSpan: ParseSourceSpan|null;
value: () => o.Expression; value: () => o.Expression | o.Expression[];
params?: any[]; params?: any[];
} }

View File

@ -8,7 +8,7 @@
import {SafeValue} from '../../sanitization/bypass'; import {SafeValue} from '../../sanitization/bypass';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {throwErrorIfNoChangesMode} from '../errors'; import {throwErrorIfNoChangesMode} from '../errors';
import {setInputsForProperty} from '../instructions/shared'; import {TsickleIssue1009, setInputsForProperty} from '../instructions/shared';
import {AttributeMarker, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; import {AttributeMarker, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer'; import {RElement} from '../interfaces/renderer';
import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from '../interfaces/styling'; import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from '../interfaces/styling';
@ -77,8 +77,10 @@ export function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void {
* @codeGenApi * @codeGenApi
*/ */
export function ɵɵstyleProp( export function ɵɵstyleProp(
prop: string, value: string | number | SafeValue | null, suffix?: string | null): void { prop: string, value: string | number | SafeValue | null,
suffix?: string | null): TsickleIssue1009 {
stylePropInternal(getSelectedIndex(), prop, value, suffix); stylePropInternal(getSelectedIndex(), prop, value, suffix);
return ɵɵstyleProp;
} }
/** /**
@ -133,7 +135,7 @@ export function stylePropInternal(
* *
* @codeGenApi * @codeGenApi
*/ */
export function ɵɵclassProp(className: string, value: boolean | null): void { export function ɵɵclassProp(className: string, value: boolean | null): TsickleIssue1009 {
// if a value is interpolated then it may render a `NO_CHANGE` value. // if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index // in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values // still needs to be incremented because all styling binding values
@ -159,6 +161,7 @@ export function ɵɵclassProp(className: string, value: boolean | null): void {
ngDevMode.classPropCacheMiss++; ngDevMode.classPropCacheMiss++;
} }
} }
return ɵɵclassProp;
} }
/** /**

View File

@ -724,7 +724,7 @@ export declare function ɵɵclassMapInterpolate8(prefix: string, v0: any, i0: st
export declare function ɵɵclassMapInterpolateV(values: any[]): void; export declare function ɵɵclassMapInterpolateV(values: any[]): void;
export declare function ɵɵclassProp(className: string, value: boolean | null): void; export declare function ɵɵclassProp(className: string, value: boolean | null): TsickleIssue1009;
export declare type ɵɵComponentDefWithMeta<T, Selector extends String, ExportAs extends string[], InputMap extends { export declare type ɵɵComponentDefWithMeta<T, Selector extends String, ExportAs extends string[], InputMap extends {
[key: string]: string; [key: string]: string;
@ -1033,7 +1033,7 @@ export declare function ɵɵstyleMap(styles: {
[styleName: string]: any; [styleName: string]: any;
} | NO_CHANGE | null): void; } | NO_CHANGE | null): void;
export declare function ɵɵstyleProp(prop: string, value: string | number | SafeValue | null, suffix?: string | null): void; export declare function ɵɵstyleProp(prop: string, value: string | number | SafeValue | null, suffix?: string | null): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate1(prop: string, prefix: string, v0: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009; export declare function ɵɵstylePropInterpolate1(prop: string, prefix: string, v0: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009;