refactor(compiler): add support for class property bindings (#50805)

Adds support for bindings of the form `[class.some-class]="isActive"`

PR Close #50805
This commit is contained in:
Miles Malerba 2023-06-20 14:57:22 +09:00 committed by Jessica Janiuk
parent 021025964a
commit 2f7072dc3f
11 changed files with 71 additions and 8 deletions

View file

@ -158,10 +158,10 @@ import * as i0 from "@angular/core";
export class MyComponent {
}
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, selector: "my-component", ngImport: i0, template: `<div [style.color]></div>`, isInline: true });
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, selector: "my-component", ngImport: i0, template: `<div [class.color]></div>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
type: Component,
args: [{ selector: 'my-component', template: `<div [style.color]></div>` }]
args: [{ selector: 'my-component', template: `<div [class.color]></div>` }]
}] });
export class MyModule {
}

View file

@ -41,8 +41,7 @@
"failureMessage": "Incorrect template",
"files": ["empty_class_bindings.js"]
}
],
"skipForTemplatePipeline": true
]
}
]
}

View file

@ -1,6 +1,6 @@
import {Component, NgModule} from '@angular/core';
@Component({selector: 'my-component', template: `<div [style.color]></div>`})
@Component({selector: 'my-component', template: `<div [class.color]></div>`})
export class MyComponent {
}

View file

@ -88,6 +88,11 @@ export enum OpKind {
*/
StyleProp,
/**
* An operation to bind an expression to a class property of an element.
*/
ClassProp,
/**
* An operation to bind an expression to the styles of an element.
*/

View file

@ -737,6 +737,7 @@ export function transformExpressionsInOp(
case OpKind.Property:
case OpKind.StyleProp:
case OpKind.StyleMap:
case OpKind.ClassProp:
case OpKind.ClassMap:
op.expression = transformExpressionsInExpression(op.expression, transform, flags);
break;

View file

@ -17,7 +17,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|AttributeOp|StylePropOp|
export type UpdateOp =
ListEndOp<UpdateOp>|StatementOp<UpdateOp>|PropertyOp|AttributeOp|StylePropOp|ClassPropOp|
StyleMapOp|ClassMapOp|InterpolatePropertyOp|InterpolateAttributeOp|InterpolateStylePropOp|
InterpolateStyleMapOp|InterpolateClassMapOp|InterpolateTextOp|AdvanceOp|VariableOp<UpdateOp>;
@ -154,6 +155,44 @@ export function createStylePropOp(
};
}
/**
* A logical operation representing binding to a class property in the update IR.
*/
export interface ClassPropOp extends Op<UpdateOp>, ConsumesVarsTrait, DependsOnSlotContextOpTrait {
kind: OpKind.ClassProp;
/**
* Reference to the element on which the property is bound.
*/
target: XrefId;
/**
* Name of the bound property.
*/
name: string;
/**
* Expression which is bound to the property.
*/
expression: o.Expression;
}
/**
* Create a `ClassPropOp`.
*/
export function createClassPropOp(
xref: XrefId, name: string, expression: o.Expression): ClassPropOp {
return {
kind: OpKind.ClassProp,
target: xref,
name,
expression,
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
...TRAIT_CONSUMES_VARS,
...NEW_OP,
};
}
/**
* A logical operation representing binding to a style map in the update IR.
*/

View file

@ -300,8 +300,11 @@ function ingestPropertyBinding(
value.expressions.map(expr => convertAst(expr, view.tpl)));
view.update.push(attributeInterpolate);
break;
case e.BindingType.Class:
throw Error('Unexpected interpolation in class property binding');
// TODO: implement remaining binding types.
case e.BindingType.Animation:
default:
// TODO: implement remaining binding types.
throw Error(`Interpolated property binding type not handled: ${type}`);
}
} else {
@ -336,8 +339,15 @@ function ingestPropertyBinding(
const attrOp = ir.createAttributeOp(xref, bindingKind, name, convertAst(value, view.tpl));
view.update.push(attrOp);
break;
case e.BindingType.Class:
if (bindingKind !== ir.ElementAttributeKind.Binding) {
throw Error('Unexpected class binding on ng-template');
}
view.update.push(ir.createClassPropOp(xref, name, convertAst(value, view.tpl)));
break;
// TODO: implement remaining binding types.
case e.BindingType.Animation:
default:
// TODO: implement remaining binding types.
throw Error(`Property binding type not handled: ${type}`);
}
}

View file

@ -152,6 +152,10 @@ export function styleProp(name: string, expression: o.Expression, unit: string|n
return call(Identifiers.styleProp, args);
}
export function classProp(name: string, expression: o.Expression): ir.UpdateOp {
return call(Identifiers.classProp, [o.literal(name), expression]);
}
export function styleMap(expression: o.Expression): ir.UpdateOp {
return call(Identifiers.styleMap, [expression]);
}

View file

@ -90,6 +90,7 @@ function populateElementAttributes(view: ViewCompilation, compatibility: boolean
break;
case ir.OpKind.StyleProp:
case ir.OpKind.ClassProp:
ownerOp = lookupElement(elements, op.target);
ir.assertIsElementAttributes(ownerOp.attributes);

View file

@ -124,6 +124,9 @@ function reifyUpdateOperations(_view: ViewCompilation, ops: ir.OpList<ir.UpdateO
case ir.OpKind.StyleProp:
ir.OpList.replace(op, ng.styleProp(op.name, op.expression, op.unit));
break;
case ir.OpKind.ClassProp:
ir.OpList.replace(op, ng.classProp(op.name, op.expression));
break;
case ir.OpKind.StyleMap:
ir.OpList.replace(op, ng.styleMap(op.expression));
break;

View file

@ -64,6 +64,7 @@ function varsUsedByOp(op: (ir.CreateOp|ir.UpdateOp)&ir.ConsumesVarsTrait): numbe
case ir.OpKind.Property:
case ir.OpKind.StyleProp:
case ir.OpKind.StyleMap:
case ir.OpKind.ClassProp:
// Property bindings use 1 variable slot.
return 1;
case ir.OpKind.Attribute: