Skip to content

Commit

Permalink
fix(ivy): Add style="{{exp}}" based interpolation (#34202)
Browse files Browse the repository at this point in the history
Fixes #33575

Add support for interpolation in styles as shown:
```
<div style="color: {{exp1}}; width: {{exp2}};">
```

PR Close #34202
  • Loading branch information
mhevery committed Feb 20, 2020
1 parent a4b388d commit d63ba9c
Show file tree
Hide file tree
Showing 16 changed files with 689 additions and 72 deletions.
180 changes: 151 additions & 29 deletions packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts
Expand Up @@ -377,7 +377,7 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵstyleMap($ctx$.myStyleExp, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($ctx$.myStyleExp);
}
}
`;
Expand Down Expand Up @@ -510,7 +510,7 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵelement(0, "div", 0);
}
if (rf & 2) {
$r3$.ɵɵstyleMap($ctx$.myStyleExp, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($ctx$.myStyleExp);
$r3$.ɵɵstyleProp("width", $ctx$.myWidth)("height", $ctx$.myHeight);
$r3$.ɵɵattribute("style", "border-width: 10px", $r3$.ɵɵsanitizeStyle);
}
Expand Down Expand Up @@ -813,7 +813,7 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵstyleMap($ctx$.myStyleExp, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($ctx$.myStyleExp);
$r3$.ɵɵclassMap($ctx$.myClassExp);
}
}
Expand Down Expand Up @@ -854,7 +854,7 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind1(1, 4, $ctx$.myStyleExp), $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind1(1, 4, $ctx$.myStyleExp));
$r3$.ɵɵclassMap($r3$.ɵɵpipeBind1(2, 6, $ctx$.myClassExp));
}
}
Expand Down Expand Up @@ -906,7 +906,7 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 11, $ctx$.myStyleExp, 1000), $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 11, $ctx$.myStyleExp, 1000));
$r3$.ɵɵclassMap($r3$.ɵɵpureFunction0(23, _c0));
$r3$.ɵɵstyleProp("bar", $r3$.ɵɵpipeBind2(2, 14, $ctx$.barExp, 3000))("baz", $r3$.ɵɵpipeBind2(3, 17, $ctx$.bazExp, 4000));
$r3$.ɵɵclassProp("foo", $r3$.ɵɵpipeBind2(4, 20, $ctx$.fooExp, 2000));
Expand Down Expand Up @@ -1009,7 +1009,7 @@ describe('compiler compliance: styling', () => {
hostVars: 8,
hostBindings: function MyComponent_HostBindings(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵstyleMap(ctx.myStyle, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyle);
$r3$.ɵɵclassMap(ctx.myClass);
$r3$.ɵɵstyleProp("color", ctx.myColorProp);
$r3$.ɵɵclassProp("foo", ctx.myFooClass);
Expand Down Expand Up @@ -1064,7 +1064,7 @@ describe('compiler compliance: styling', () => {
hostVars: 12,
hostBindings: function MyComponent_HostBindings(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵstyleMap(ctx.myStyle, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyle);
$r3$.ɵɵclassMap(ctx.myClasses);
$r3$.ɵɵstyleProp("height", ctx.myHeightProp, "pt")("width", ctx.myWidthProp);
$r3$.ɵɵclassProp("bar", ctx.myBarClass)("foo", ctx.myFooClass);
Expand Down Expand Up @@ -1121,7 +1121,7 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵstyleMap(ctx.myStyleExp, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyleExp);
$r3$.ɵɵclassMap(ctx.myClassExp);
$r3$.ɵɵstyleProp("height", ctx.myHeightExp);
$r3$.ɵɵclassProp("bar", ctx.myBarClassExp);
Expand All @@ -1133,7 +1133,7 @@ describe('compiler compliance: styling', () => {
hostVars: 8,
hostBindings: function MyComponent_HostBindings(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵstyleMap(ctx.myStyleExp, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyleExp);
$r3$.ɵɵclassMap(ctx.myClassExp);
$r3$.ɵɵstyleProp("width", ctx.myWidthExp);
$r3$.ɵɵclassProp("foo", ctx.myFooClassExp);
Expand All @@ -1146,6 +1146,146 @@ describe('compiler compliance: styling', () => {
expectEmit(result.source, template, 'Incorrect template');
});

it('should support class interpolation', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, HostBinding} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<div class="A{{p1}}B"></div>
<div class="A{{p1}}B{{p2}}C"></div>
<div class="A{{p1}}B{{p2}}C{{p3}}D"></div>
<div class="A{{p1}}B{{p2}}C{{p3}}D{{p4}}E"></div>
<div class="A{{p1}}B{{p2}}C{{p3}}D{{p4}}E{{p5}}F"></div>
<div class="A{{p1}}B{{p2}}C{{p3}}D{{p4}}E{{p5}}F{{p6}}G"></div>
<div class="A{{p1}}B{{p2}}C{{p3}}D{{p4}}E{{p5}}F{{p6}}G{{p7}}H"></div>
<div class="A{{p1}}B{{p2}}C{{p3}}D{{p4}}E{{p5}}F{{p6}}G{{p7}}H{{p8}}I"></div>
<div class="A{{p1}}B{{p2}}C{{p3}}D{{p4}}E{{p5}}F{{p6}}G{{p7}}H{{p8}}I{{p9}}J"></div>
\`,
})
export class MyComponent {
p1 = 100;
p2 = 100;
p3 = 100;
p4 = 100;
p5 = 100;
p6 = 100;
p6 = 100;
p7 = 100;
p8 = 100;
p9 = 100;
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};

const template = `
function MyComponent_Template(rf, ctx) {
if (rf & 1) {
}
if (rf & 2) {
$r3$.ɵɵclassMapInterpolate1("A", ctx.p1, "B");
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate2("A", ctx.p1, "B", ctx.p2, "C");
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate3("A", ctx.p1, "B", ctx.p2, "C", ctx.p3, "D");
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate4("A", ctx.p1, "B", ctx.p2, "C", ctx.p3, "D", ctx.p4, "E");
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate5("A", ctx.p1, "B", ctx.p2, "C", ctx.p3, "D", ctx.p4, "E", ctx.p5, "F");
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate6("A", ctx.p1, "B", ctx.p2, "C", ctx.p3, "D", ctx.p4, "E", ctx.p5, "F", ctx.p6, "G");
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate7("A", ctx.p1, "B", ctx.p2, "C", ctx.p3, "D", ctx.p4, "E", ctx.p5, "F", ctx.p6, "G", ctx.p7, "H");
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate8("A", ctx.p1, "B", ctx.p2, "C", ctx.p3, "D", ctx.p4, "E", ctx.p5, "F", ctx.p6, "G", ctx.p7, "H", ctx.p8, "I");
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolateV(["A", ctx.p1, "B", ctx.p2, "C", ctx.p3, "D", ctx.p4, "E", ctx.p5, "F", ctx.p6, "G", ctx.p7, "H", ctx.p8, "I", ctx.p9, "J"]);
}
},
`;

const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});

it('should support style interpolation', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, HostBinding} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<div style="p1:{{p1}};"></div>
<div style="p1:{{p1}};p2:{{p2}};"></div>
<div style="p1:{{p1}};p2:{{p2}};p3:{{p3}};"></div>
<div style="p1:{{p1}};p2:{{p2}};p3:{{p3}};p4:{{p4}};"></div>
<div style="p1:{{p1}};p2:{{p2}};p3:{{p3}};p4:{{p4}};p5:{{p5}};"></div>
<div style="p1:{{p1}};p2:{{p2}};p3:{{p3}};p4:{{p4}};p5:{{p5}};p6:{{p6}};"></div>
<div style="p1:{{p1}};p2:{{p2}};p3:{{p3}};p4:{{p4}};p5:{{p5}};p6:{{p6}};p7:{{p7}};"></div>
<div style="p1:{{p1}};p2:{{p2}};p3:{{p3}};p4:{{p4}};p5:{{p5}};p6:{{p6}};p7:{{p7}};p8:{{p8}};"></div>
<div style="p1:{{p1}};p2:{{p2}};p3:{{p3}};p4:{{p4}};p5:{{p5}};p6:{{p6}};p7:{{p7}};p8:{{p8}};p9:{{p9}};"></div>
\`,
})
export class MyComponent {
p1 = 100;
p2 = 100;
p3 = 100;
p4 = 100;
p5 = 100;
p6 = 100;
p6 = 100;
p7 = 100;
p8 = 100;
p9 = 100;
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};

const template = `
function MyComponent_Template(rf, ctx) {
if (rf & 1) {
}
if (rf & 2) {
$r3$.ɵɵstyleMapInterpolate1("p1:", ctx.p1, ";");
$r3$.ɵɵadvance(1);
$r3$.ɵɵstyleMapInterpolate2("p1:", ctx.p1, ";p2:", ctx.p2, ";");
$r3$.ɵɵadvance(1);
$r3$.ɵɵstyleMapInterpolate3("p1:", ctx.p1, ";p2:", ctx.p2, ";p3:", ctx.p3, ";");
$r3$.ɵɵadvance(1);
$r3$.ɵɵstyleMapInterpolate4("p1:", ctx.p1, ";p2:", ctx.p2, ";p3:", ctx.p3, ";p4:", ctx.p4, ";");
$r3$.ɵɵadvance(1);
$r3$.ɵɵstyleMapInterpolate5("p1:", ctx.p1, ";p2:", ctx.p2, ";p3:", ctx.p3, ";p4:", ctx.p4, ";p5:", ctx.p5, ";");
$r3$.ɵɵadvance(1);
$r3$.ɵɵstyleMapInterpolate6("p1:", ctx.p1, ";p2:", ctx.p2, ";p3:", ctx.p3, ";p4:", ctx.p4, ";p5:", ctx.p5, ";p6:", ctx.p6, ";");
$r3$.ɵɵadvance(1);
$r3$.ɵɵstyleMapInterpolate7("p1:", ctx.p1, ";p2:", ctx.p2, ";p3:", ctx.p3, ";p4:", ctx.p4, ";p5:", ctx.p5, ";p6:", ctx.p6, ";p7:", ctx.p7, ";");
$r3$.ɵɵadvance(1);
$r3$.ɵɵstyleMapInterpolate8("p1:", ctx.p1, ";p2:", ctx.p2, ";p3:", ctx.p3, ";p4:", ctx.p4, ";p5:", ctx.p5, ";p6:", ctx.p6, ";p7:", ctx.p7, ";p8:", ctx.p8, ";");
$r3$.ɵɵadvance(1);
$r3$.ɵɵstyleMapInterpolateV(["p1:", ctx.p1, ";p2:", ctx.p2, ";p3:", ctx.p3, ";p4:", ctx.p4, ";p5:", ctx.p5, ";p6:", ctx.p6, ";p7:", ctx.p7, ";p8:", ctx.p8, ";p9:", ctx.p9, ";"]);
}
},
`;

const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});

it('should generate styling instructions for multiple directives that contain host binding definitions',
() => {
const files = {
Expand Down Expand Up @@ -1281,24 +1421,6 @@ describe('compiler compliance: styling', () => {
expectEmit(result.source, template, 'Incorrect handling of interpolated classes');
});

it('should throw for interpolations inside `style`', () => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: '<div style="color:{{red}}"></div>'
})
export class MyComponent {
}
`
}
};

expect(() => compile(files, angularFiles)).toThrowError(/Unexpected interpolation/);
});

it('should throw for interpolations inside individual class bindings', () => {
const files = {
app: {
Expand Down Expand Up @@ -1864,7 +1986,7 @@ describe('compiler compliance: styling', () => {
hostBindings: function MyComponent_HostBindings(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title);
$r3$.ɵɵstyleMap(ctx.myStyle, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyle);
$r3$.ɵɵclassMap(ctx.myClass);
}
}
Expand Down Expand Up @@ -1972,7 +2094,7 @@ describe('compiler compliance: styling', () => {
template: function MyAppComp_Template(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵstyleMap(ctx.mapExp, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.mapExp);
}
}
Expand Down
19 changes: 19 additions & 0 deletions packages/compiler/src/render3/r3_identifiers.ts
Expand Up @@ -71,6 +71,25 @@ export class Identifiers {

static styleMap: o.ExternalReference = {name: 'ɵɵstyleMap', moduleName: CORE};

static styleMapInterpolate1:
o.ExternalReference = {name: 'ɵɵstyleMapInterpolate1', moduleName: CORE};
static styleMapInterpolate2:
o.ExternalReference = {name: 'ɵɵstyleMapInterpolate2', moduleName: CORE};
static styleMapInterpolate3:
o.ExternalReference = {name: 'ɵɵstyleMapInterpolate3', moduleName: CORE};
static styleMapInterpolate4:
o.ExternalReference = {name: 'ɵɵstyleMapInterpolate4', moduleName: CORE};
static styleMapInterpolate5:
o.ExternalReference = {name: 'ɵɵstyleMapInterpolate5', moduleName: CORE};
static styleMapInterpolate6:
o.ExternalReference = {name: 'ɵɵstyleMapInterpolate6', moduleName: CORE};
static styleMapInterpolate7:
o.ExternalReference = {name: 'ɵɵstyleMapInterpolate7', moduleName: CORE};
static styleMapInterpolate8:
o.ExternalReference = {name: 'ɵɵstyleMapInterpolate8', moduleName: CORE};
static styleMapInterpolateV:
o.ExternalReference = {name: 'ɵɵstyleMapInterpolateV', moduleName: CORE};

static classMap: o.ExternalReference = {name: 'ɵɵclassMap', moduleName: CORE};

static classMapInterpolate1:
Expand Down
44 changes: 34 additions & 10 deletions packages/compiler/src/render3/view/styling_builder.ts
Expand Up @@ -81,7 +81,7 @@ export interface StylingInstruction {

export interface StylingInstructionCall {
sourceSpan: ParseSourceSpan|null;
supportsInterpolation?: boolean;
supportsInterpolation: boolean;
allocateBindingSlots: number;
params: ((convertFn: (value: any) => o.Expression | o.Expression[]) => o.Expression[]);
}
Expand Down Expand Up @@ -372,28 +372,23 @@ export class StylingBuilder {
// pipes can be picked up in time before the template is built
const mapValue = stylingInput.value.visit(valueConverter);
let reference: o.ExternalReference;
if (mapValue instanceof Interpolation && isClassBased) {
if (mapValue instanceof Interpolation) {
totalBindingSlotsRequired += mapValue.expressions.length;
reference = getClassMapInterpolationExpression(mapValue);
reference = isClassBased ? getClassMapInterpolationExpression(mapValue) :
getStyleMapInterpolationExpression(mapValue);
} else {
reference = isClassBased ? R3.classMap : R3.styleMap;
}

return {
reference,
calls: [{
supportsInterpolation: isClassBased,
supportsInterpolation: true,
sourceSpan: stylingInput.sourceSpan,
allocateBindingSlots: totalBindingSlotsRequired,
params: (convertFn: (value: any) => o.Expression | o.Expression[]) => {
const convertResult = convertFn(mapValue);
const params = Array.isArray(convertResult) ? convertResult : [convertResult];

// [style] instructions will sanitize all their values. For this reason we
// need to include the sanitizer as a param.
if (!isClassBased) {
params.push(o.importExpr(R3.defaultStyleSanitizer));
}
return params;
}
}]
Expand Down Expand Up @@ -588,6 +583,35 @@ function getClassMapInterpolationExpression(interpolation: Interpolation): o.Ext
}
}

/**
* Gets the instruction to generate for an interpolated style map.
* @param interpolation An Interpolation AST
*/
function getStyleMapInterpolationExpression(interpolation: Interpolation): o.ExternalReference {
switch (getInterpolationArgsLength(interpolation)) {
case 1:
return R3.styleMap;
case 3:
return R3.styleMapInterpolate1;
case 5:
return R3.styleMapInterpolate2;
case 7:
return R3.styleMapInterpolate3;
case 9:
return R3.styleMapInterpolate4;
case 11:
return R3.styleMapInterpolate5;
case 13:
return R3.styleMapInterpolate6;
case 15:
return R3.styleMapInterpolate7;
case 17:
return R3.styleMapInterpolate8;
default:
return R3.styleMapInterpolateV;
}
}

/**
* Gets the instruction to generate for an interpolated style prop.
* @param interpolation An Interpolation AST
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/core_render3_private_export.ts
Expand Up @@ -121,6 +121,15 @@ export {
ɵɵelementContainerEnd,
ɵɵelementContainer,
ɵɵstyleMap,
ɵɵstyleMapInterpolate1,
ɵɵstyleMapInterpolate2,
ɵɵstyleMapInterpolate3,
ɵɵstyleMapInterpolate4,
ɵɵstyleMapInterpolate5,
ɵɵstyleMapInterpolate6,
ɵɵstyleMapInterpolate7,
ɵɵstyleMapInterpolate8,
ɵɵstyleMapInterpolateV,
ɵɵstyleSanitizer,
ɵɵclassMap,
ɵɵclassMapInterpolate1,
Expand Down

0 comments on commit d63ba9c

Please sign in to comment.