Skip to content

Commit

Permalink
refactor(compiler): add support for interpolation in style properties (
Browse files Browse the repository at this point in the history
…angular#50489)

Add support for interpolation in style property bindings in the template
pipeline

PR Close angular#50489
  • Loading branch information
mmalerba authored and AndrewKushnir committed Jun 26, 2023
1 parent 3627e4c commit 3c1feed
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,54 @@
"cases": [
{
"description": "should generate the proper update instructions for interpolated classes",
"inputFiles": [
"class_interpolations.ts"
],
"inputFiles": ["class_interpolations.ts"],
"expectations": [
{
"failureMessage": "Incorrect handling of interpolated classes",
"files": [
"class_interpolations.js"
]
"files": ["class_interpolations.js"]
}
],
"skipForTemplatePipeline": true
},
{
"description": "should generate the proper update instructions for interpolated style properties",
"inputFiles": [
"style_properties.ts"
],
"inputFiles": ["style_properties.ts"],
"expectations": [
{
"failureMessage": "Incorrect handling of interpolated style properties",
"files": [
"style_properties.js"
]
"files": ["style_properties.js"]
}
],
"skipForTemplatePipeline": true
},
{
"description": "should generate update instructions for interpolated style properties with a suffix",
"inputFiles": [
"style_binding_suffixed.ts"
],
"inputFiles": ["style_binding_suffixed.ts"],
"expectations": [
{
"failureMessage": "Incorrect handling of interpolated style properties",
"files": [
"style_binding_suffixed.js"
]
"files": ["style_binding_suffixed.js"]
}
],
"skipForTemplatePipeline": true
]
},
{
"description": "should generate update instructions for interpolated style properties with a sanitizer",
"inputFiles": [
"style_binding_sanitizer.ts"
],
"inputFiles": ["style_binding_sanitizer.ts"],
"expectations": [
{
"failureMessage": "Incorrect handling of interpolated style properties",
"files": [
"style_binding_sanitizer.js"
]
"files": ["style_binding_sanitizer.js"]
}
],
"skipForTemplatePipeline": true
},
{
"description": "should generate update instructions for interpolated style properties with !important",
"inputFiles": [
"style_binding_important.ts"
],
"inputFiles": ["style_binding_important.ts"],
"expectations": [
{
"failureMessage": "Incorrect handling of interpolated style properties",
"files": [
"style_binding_important.js"
]
"files": ["style_binding_important.js"]
}
],
"skipForTemplatePipeline": true
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler/src/template/pipeline/ir/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ export enum OpKind {
*/
InterpolateProperty,

/**
* An operation to interpolate text into a style property binding.
*/
InterpolateStyleProp,

/**
* An operation to advance the runtime's implicit slot context during the update phase of a view.
*/
Expand Down
7 changes: 2 additions & 5 deletions packages/compiler/src/template/pipeline/ir/src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,8 @@ export function transformExpressionsInOp(
op.expression = transformExpressionsInExpression(op.expression, transform, flags);
break;
case OpKind.InterpolateProperty:
case OpKind.InterpolateStyleProp:
case OpKind.InterpolateText:
for (let i = 0; i < op.expressions.length; i++) {
op.expressions[i] = transformExpressionsInExpression(op.expressions[i], transform, flags);
}
Expand All @@ -675,11 +677,6 @@ export function transformExpressionsInOp(
case OpKind.Variable:
op.initializer = transformExpressionsInExpression(op.initializer, transform, flags);
break;
case OpKind.InterpolateText:
for (let i = 0; i < op.expressions.length; i++) {
op.expressions[i] = transformExpressionsInExpression(op.expressions[i], transform, flags);
}
break;
case OpKind.Listener:
for (const innerOp of op.handlerOps) {
transformExpressionsInOp(innerOp, transform, flags | VisitorContextFlag.InChildOperation);
Expand Down
59 changes: 57 additions & 2 deletions packages/compiler/src/template/pipeline/ir/src/ops/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {ListEndOp, NEW_OP, StatementOp, VariableOp} from './shared';
* An operation usable on the update side of the IR.
*/
export type UpdateOp = ListEndOp<UpdateOp>|StatementOp<UpdateOp>|PropertyOp|StylePropOp|StyleMapOp|
InterpolatePropertyOp|AttributeOp|InterpolateTextOp|AdvanceOp|VariableOp<UpdateOp>;
InterpolatePropertyOp|InterpolateStylePropOp|AttributeOp|InterpolateTextOp|AdvanceOp|
VariableOp<UpdateOp>;

/**
* A logical operation to perform string interpolation on a text node.
Expand Down Expand Up @@ -133,7 +134,7 @@ export interface StylePropOp extends Op<UpdateOp>, ConsumesVarsTrait, DependsOnS
expression: o.Expression;

/**
* The unit of the expression value.
* The unit of the bound value.
*/
unit: string|null;
}
Expand Down Expand Up @@ -281,7 +282,61 @@ export function createInterpolatePropertyOp(
};
}

/**
* A logical operation representing binding an interpolation to a style property in the update IR.
*/
export interface InterpolateStylePropOp extends Op<UpdateOp>, ConsumesVarsTrait,
DependsOnSlotContextOpTrait {
kind: OpKind.InterpolateStyleProp;

/**
* Reference to the element on which the property is bound.
*/
target: XrefId;

/**
* Name of the bound property.
*/
name: string;

/**
* All of the literal strings in the property interpolation, in order.
*
* Conceptually interwoven around the `expressions`.
*/
strings: string[];

/**
* All of the dynamic expressions in the property interpolation, in order.
*
* Conceptually interwoven in between the `strings`.
*/
expressions: o.Expression[];

/**
* The unit of the bound value.
*/
unit: string|null;
}

/**
* Create a `InterpolateStyleProp`.
*/
export function createInterpolateStylePropOp(
xref: XrefId, name: string, strings: string[], expressions: o.Expression[],
unit: string|null): InterpolateStylePropOp {
return {
kind: OpKind.InterpolateStyleProp,
target: xref,
name,
strings,
expressions,
unit,
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
...TRAIT_CONSUMES_VARS,
...NEW_OP,
};
}

/**
* Logical operation to advance the runtime's internal slot pointer in the update IR.
Expand Down
8 changes: 8 additions & 0 deletions packages/compiler/src/template/pipeline/src/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ function ingestPropertyBinding(
xref, bindingKind, name, value.strings,
value.expressions.map(expr => convertAst(expr, view.tpl))));
break;
case e.BindingType.Style:
if (bindingKind !== ir.ElementAttributeKind.Binding) {
throw Error('Unexpected style binding on ng-template');
}
view.update.push(ir.createInterpolateStylePropOp(
xref, name, value.strings, value.expressions.map(expr => convertAst(expr, view.tpl)),
unit));
break;
default:
// TODO: implement remaining binding types.
throw Error(`Interpolated property binding type not handled: ${type}`);
Expand Down
92 changes: 72 additions & 20 deletions packages/compiler/src/template/pipeline/src/instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,39 @@ export function textInterpolate(strings: string[], expressions: o.Expression[]):

export function propertyInterpolate(
name: string, strings: string[], expressions: o.Expression[]): ir.UpdateOp {
const interpolationArgs = collateInterpolationArgs(strings, expressions);

return callVariadicInstruction(PROPERTY_INTERPOLATE_CONFIG, [o.literal(name)], interpolationArgs);
}

export function stylePropInterpolate(
name: string, strings: string[], expressions: o.Expression[], unit: string|null): ir.UpdateOp {
const interpolationArgs = collateInterpolationArgs(strings, expressions);
const extraArgs: o.Expression[] = [];
if (unit !== null) {
extraArgs.push(o.literal(unit));
}

return callVariadicInstruction(
STYLE_PROP_INTERPOLATE_CONFIG, [o.literal(name)], interpolationArgs, extraArgs);
}

export function pureFunction(
varOffset: number, fn: o.Expression, args: o.Expression[]): o.Expression {
return callVariadicInstructionExpr(
PURE_FUNCTION_CONFIG,
[
o.literal(varOffset),
fn,
],
args,
);
}

/**
* Collates the string an expression arguments for an interpolation instruction.
*/
function collateInterpolationArgs(strings: string[], expressions: o.Expression[]): o.Expression[] {
if (strings.length < 1 || expressions.length !== strings.length - 1) {
throw new Error(
`AssertionError: expected specific shape of args for strings/expressions in interpolation`);
Expand All @@ -221,19 +254,7 @@ export function propertyInterpolate(
interpolationArgs.push(o.literal(strings[idx]));
}

return callVariadicInstruction(PROPERTY_INTERPOLATE_CONFIG, [o.literal(name)], interpolationArgs);
}

export function pureFunction(
varOffset: number, fn: o.Expression, args: o.Expression[]): o.Expression {
return callVariadicInstructionExpr(
PURE_FUNCTION_CONFIG,
[
o.literal(varOffset),
fn,
],
args,
);
return interpolationArgs;
}

function call<OpT extends ir.CreateOp|ir.UpdateOp>(
Expand Down Expand Up @@ -300,6 +321,33 @@ const PROPERTY_INTERPOLATE_CONFIG: VariadicInstructionConfig = {
},
};

/**
* `InterpolationConfig` for the `stylePropInterpolate` instruction.
*/
const STYLE_PROP_INTERPOLATE_CONFIG: VariadicInstructionConfig = {
constant: [
null!, // Interpolation with 0 variables is not supported.
Identifiers.stylePropInterpolate1,
Identifiers.stylePropInterpolate2,
Identifiers.stylePropInterpolate3,
Identifiers.stylePropInterpolate4,
Identifiers.stylePropInterpolate5,
Identifiers.stylePropInterpolate6,
Identifiers.stylePropInterpolate7,
Identifiers.stylePropInterpolate8,
],
variable: Identifiers.stylePropInterpolateV,
mapping: n => {
if (n % 2 === 0) {
throw new Error(`Expected odd number of arguments`);
}
if (n < 3) {
throw new Error(`Expected at least 3 arguments`);
}
return (n - 1) / 2;
},
};

const PURE_FUNCTION_CONFIG: VariadicInstructionConfig = {
constant: [
Identifiers.pureFunction0,
Expand All @@ -317,23 +365,27 @@ const PURE_FUNCTION_CONFIG: VariadicInstructionConfig = {
};

function callVariadicInstructionExpr(
config: VariadicInstructionConfig, baseArgs: o.Expression[],
interpolationArgs: o.Expression[]): o.Expression {
config: VariadicInstructionConfig, baseArgs: o.Expression[], interpolationArgs: o.Expression[],
extraArgs: o.Expression[] = []): o.Expression {
const n = config.mapping(interpolationArgs.length);
if (n < config.constant.length) {
// Constant calling pattern.
return o.importExpr(config.constant[n]).callFn([...baseArgs, ...interpolationArgs]);
return o.importExpr(config.constant[n]).callFn([
...baseArgs, ...interpolationArgs, ...extraArgs
]);
} else if (config.variable !== null) {
// Variable calling pattern.
return o.importExpr(config.variable).callFn([...baseArgs, o.literalArr(interpolationArgs)]);
return o.importExpr(config.variable).callFn([
...baseArgs, o.literalArr(interpolationArgs), ...extraArgs
]);
} else {
throw new Error(`AssertionError: unable to call variadic function`);
}
}

function callVariadicInstruction(
config: VariadicInstructionConfig, baseArgs: o.Expression[],
interpolationArgs: o.Expression[]): ir.UpdateOp {
config: VariadicInstructionConfig, baseArgs: o.Expression[], interpolationArgs: o.Expression[],
extraArgs: o.Expression[] = []): ir.UpdateOp {
return ir.createStatementOp(
callVariadicInstructionExpr(config, baseArgs, interpolationArgs).toStmt());
callVariadicInstructionExpr(config, baseArgs, interpolationArgs, extraArgs).toStmt());
}
4 changes: 4 additions & 0 deletions packages/compiler/src/template/pipeline/src/phases/reify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ function reifyUpdateOperations(_view: ViewCompilation, ops: ir.OpList<ir.UpdateO
case ir.OpKind.InterpolateProperty:
ir.OpList.replace(op, ng.propertyInterpolate(op.name, op.strings, op.expressions));
break;
case ir.OpKind.InterpolateStyleProp:
ir.OpList.replace(
op, ng.stylePropInterpolate(op.name, op.strings, op.expressions, op.unit));
break;
case ir.OpKind.InterpolateText:
ir.OpList.replace(op, ng.textInterpolate(op.strings, op.expressions));
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ function varsUsedByOp(op: (ir.CreateOp|ir.UpdateOp)&ir.ConsumesVarsTrait): numbe
// `ir.InterpolateTextOp`s use a variable slot for each dynamic expression.
return op.expressions.length;
case ir.OpKind.InterpolateProperty:
case ir.OpKind.InterpolateStyleProp:
// `ir.InterpolatePropertyOp`s use a variable slot for each dynamic expression, plus one for
// the result.
return 1 + op.expressions.length;
Expand Down

0 comments on commit 3c1feed

Please sign in to comment.