Skip to content

Commit ba3eb8b

Browse files
committed
feat(ivy): properly apply class="", [class], [class.foo] and [attr.class] bindings (angular#24822)
PR Close angular#24822
1 parent c8ad965 commit ba3eb8b

File tree

24 files changed

+1765
-887
lines changed

24 files changed

+1765
-887
lines changed

modules/benchmarks/src/largetable/render3/table.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ export class LargeTableComponent {
4848
{
4949
if (rf2 & RenderFlags.Create) {
5050
E(0, 'td');
51-
s(1, c0);
52-
{ T(2); }
51+
s(c0);
52+
{ T(1); }
5353
e();
5454
}
5555
if (rf2 & RenderFlags.Update) {
56-
sp(1, 0, cell.row % 2 ? '' : 'grey');
57-
t(2, b(cell.value));
56+
sp(0, 0, cell.row % 2 ? '' : 'grey');
57+
t(1, b(cell.value));
5858
}
5959
}
6060
v();

modules/benchmarks/src/tree/render3/tree.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ export class TreeComponent {
4141
template: function(rf: RenderFlags, ctx: TreeComponent) {
4242
if (rf & RenderFlags.Create) {
4343
E(0, 'span');
44-
s(1, c0);
45-
{ T(2); }
44+
s(c0);
45+
{ T(1); }
4646
e();
47+
C(2);
4748
C(3);
48-
C(4);
4949
}
5050
if (rf & RenderFlags.Update) {
51-
sp(1, 0, ctx.data.depth % 2 ? '' : 'grey');
52-
t(2, i1(' ', ctx.data.value, ' '));
53-
cR(3);
51+
sp(0, 0, ctx.data.depth % 2 ? '' : 'grey');
52+
t(1, i1(' ', ctx.data.value, ' '));
53+
cR(2);
5454
{
5555
if (ctx.data.left != null) {
5656
let rf0 = V(0);
@@ -67,7 +67,7 @@ export class TreeComponent {
6767
}
6868
}
6969
cr();
70-
cR(4);
70+
cR(3);
7171
{
7272
if (ctx.data.right != null) {
7373
let rf0 = V(0);
@@ -114,18 +114,18 @@ export function TreeTpl(rf: RenderFlags, ctx: TreeNode) {
114114
E(0, 'tree');
115115
{
116116
E(1, 'span');
117-
s(2, c1);
118-
{ T(3); }
117+
s(c1);
118+
{ T(2); }
119119
e();
120+
C(3);
120121
C(4);
121-
C(5);
122122
}
123123
e();
124124
}
125125
if (rf & RenderFlags.Update) {
126-
sp(2, 0, ctx.depth % 2 ? '' : 'grey');
127-
t(3, i1(' ', ctx.value, ' '));
128-
cR(4);
126+
sp(1, 0, ctx.depth % 2 ? '' : 'grey');
127+
t(2, i1(' ', ctx.value, ' '));
128+
cR(3);
129129
{
130130
if (ctx.left != null) {
131131
let rf0 = V(0);
@@ -134,7 +134,7 @@ export function TreeTpl(rf: RenderFlags, ctx: TreeNode) {
134134
}
135135
}
136136
cr();
137-
cR(5);
137+
cR(4);
138138
{
139139
if (ctx.right != null) {
140140
let rf0 = V(0);

packages/compiler/src/core.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,6 @@ export const enum RenderFlags {
380380
Update = 0b10
381381
}
382382

383-
// Note this will expand once `class` is introduced to styling
384383
export const enum InitialStylingFlags {
385-
/** Mode for matching initial style values */
386-
INITIAL_STYLES = 0b00,
384+
VALUES_MODE = 0b1,
387385
}

packages/compiler/src/render3/r3_identifiers.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,11 @@ export class Identifiers {
3333

3434
static elementAttribute: o.ExternalReference = {name: 'ɵa', moduleName: CORE};
3535

36-
static elementClass: o.ExternalReference = {name: 'ɵk', moduleName: CORE};
37-
38-
static elementClassNamed: o.ExternalReference = {name: 'ɵkn', moduleName: CORE};
36+
static elementClassProp: o.ExternalReference = {name: 'ɵcp', moduleName: CORE};
3937

4038
static elementStyling: o.ExternalReference = {name: 'ɵs', moduleName: CORE};
4139

42-
static elementStyle: o.ExternalReference = {name: 'ɵsm', moduleName: CORE};
40+
static elementStylingMap: o.ExternalReference = {name: 'ɵsm', moduleName: CORE};
4341

4442
static elementStyleProp: o.ExternalReference = {name: 'ɵsp', moduleName: CORE};
4543

packages/compiler/src/render3/view/template.ts

Lines changed: 137 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,12 @@ function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefin
4040
case BindingType.Attribute:
4141
return R3.elementAttribute;
4242
case BindingType.Class:
43-
return R3.elementClassNamed;
43+
return R3.elementClassProp;
4444
default:
4545
return undefined;
4646
}
4747
}
4848

49-
// `className` is used below instead of `class` because the interception
50-
// code (where this map is used) deals with DOM element property values
51-
// (like elm.propName) and not component bindining properties (like [propName]).
52-
const SPECIAL_CASED_PROPERTIES_INSTRUCTION_MAP: {[index: string]: o.ExternalReference} = {
53-
'className': R3.elementClass
54-
};
55-
5649
export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver {
5750
private _dataIndex = 0;
5851
private _bindingContext = 0;
@@ -310,33 +303,59 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
310303
const i18nMessages: o.Statement[] = [];
311304
const attributes: o.Expression[] = [];
312305
const initialStyleDeclarations: o.Expression[] = [];
306+
const initialClassDeclarations: o.Expression[] = [];
313307

314308
const styleInputs: t.BoundAttribute[] = [];
309+
const classInputs: t.BoundAttribute[] = [];
315310
const allOtherInputs: t.BoundAttribute[] = [];
316311

317312
element.inputs.forEach((input: t.BoundAttribute) => {
318-
// [attr.style] should not be treated as a styling-based
319-
// binding since it is intended to write directly to the attr
320-
// and therefore will skip all style resolution that is present
321-
// with style="", [style]="" and [style.prop]="" assignments
322-
if (input.name == 'style' && input.type == BindingType.Property) {
323-
// this should always go first in the compilation (for [style])
324-
styleInputs.splice(0, 0, input);
325-
} else if (input.type == BindingType.Style) {
326-
styleInputs.push(input);
327-
} else {
328-
allOtherInputs.push(input);
313+
switch (input.type) {
314+
// [attr.style] or [attr.class] should not be treated as styling-based
315+
// bindings since they are intended to be written directly to the attr
316+
// and therefore will skip all style/class resolution that is present
317+
// with style="", [style]="" and [style.prop]="", class="",
318+
// [class.prop]="". [class]="" assignments
319+
case BindingType.Property:
320+
if (input.name == 'style') {
321+
// this should always go first in the compilation (for [style])
322+
styleInputs.splice(0, 0, input);
323+
} else if (isClassBinding(input)) {
324+
// this should always go first in the compilation (for [class])
325+
classInputs.splice(0, 0, input);
326+
} else {
327+
allOtherInputs.push(input);
328+
}
329+
break;
330+
case BindingType.Style:
331+
styleInputs.push(input);
332+
break;
333+
case BindingType.Class:
334+
classInputs.push(input);
335+
break;
336+
default:
337+
allOtherInputs.push(input);
338+
break;
329339
}
330340
});
331341

332342
let currStyleIndex = 0;
343+
let currClassIndex = 0;
333344
let staticStylesMap: {[key: string]: any}|null = null;
345+
let staticClassesMap: {[key: string]: boolean}|null = null;
334346
const stylesIndexMap: {[key: string]: number} = {};
347+
const classesIndexMap: {[key: string]: number} = {};
335348
Object.getOwnPropertyNames(outputAttrs).forEach(name => {
336349
const value = outputAttrs[name];
337350
if (name == 'style') {
338351
staticStylesMap = parseStyle(value);
339352
Object.keys(staticStylesMap).forEach(prop => { stylesIndexMap[prop] = currStyleIndex++; });
353+
} else if (name == 'class') {
354+
staticClassesMap = {};
355+
value.split(/\s+/g).forEach(className => {
356+
classesIndexMap[className] = currClassIndex++;
357+
staticClassesMap ![className] = true;
358+
});
340359
} else {
341360
attributes.push(o.literal(name));
342361
if (attrI18nMetas.hasOwnProperty(name)) {
@@ -357,14 +376,22 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
357376
}
358377
}
359378

379+
for (let i = 0; i < classInputs.length; i++) {
380+
const input = classInputs[i];
381+
const isMapBasedClassBinding = i === 0 && isClassBinding(input);
382+
if (!isMapBasedClassBinding && !stylesIndexMap.hasOwnProperty(input.name)) {
383+
classesIndexMap[input.name] = currClassIndex++;
384+
}
385+
}
386+
360387
// this will build the instructions so that they fall into the following syntax
361388
// => [prop1, prop2, prop3, 0, prop1, value1, prop2, value2]
362389
Object.keys(stylesIndexMap).forEach(prop => {
363390
initialStyleDeclarations.push(o.literal(prop));
364391
});
365392

366393
if (staticStylesMap) {
367-
initialStyleDeclarations.push(o.literal(core.InitialStylingFlags.INITIAL_STYLES));
394+
initialStyleDeclarations.push(o.literal(core.InitialStylingFlags.VALUES_MODE));
368395

369396
Object.keys(staticStylesMap).forEach(prop => {
370397
initialStyleDeclarations.push(o.literal(prop));
@@ -373,6 +400,22 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
373400
});
374401
}
375402

403+
Object.keys(classesIndexMap).forEach(prop => {
404+
initialClassDeclarations.push(o.literal(prop));
405+
});
406+
407+
if (staticClassesMap) {
408+
initialClassDeclarations.push(o.literal(core.InitialStylingFlags.VALUES_MODE));
409+
410+
Object.keys(staticClassesMap).forEach(className => {
411+
initialClassDeclarations.push(o.literal(className));
412+
initialClassDeclarations.push(o.literal(true));
413+
});
414+
}
415+
416+
const hasStylingInstructions = initialStyleDeclarations.length || styleInputs.length ||
417+
initialClassDeclarations.length || classInputs.length;
418+
376419
const attrArg: o.Expression = attributes.length > 0 ?
377420
this.constantPool.getConstLiteral(o.literalArr(attributes), true) :
378421
o.TYPED_NULL_EXPR;
@@ -411,10 +454,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
411454

412455
const implicit = o.variable(CONTEXT_NAME);
413456

414-
const elementStyleIndex =
415-
(initialStyleDeclarations.length || styleInputs.length) ? this.allocateDataSlot() : 0;
416457
const createSelfClosingInstruction =
417-
elementStyleIndex === 0 && element.children.length === 0 && element.outputs.length === 0;
458+
!hasStylingInstructions && element.children.length === 0 && element.outputs.length === 0;
418459

419460
if (createSelfClosingInstruction) {
420461
this.instruction(
@@ -429,16 +470,30 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
429470
...trimTrailingNulls(parameters));
430471

431472
// initial styling for static style="..." attributes
432-
if (elementStyleIndex > 0) {
433-
let paramsList: (o.Expression)[] = [o.literal(elementStyleIndex)];
473+
if (hasStylingInstructions) {
474+
const paramsList: (o.Expression)[] = [];
475+
434476
if (initialStyleDeclarations.length) {
435-
// the template compiler handles initial styling (e.g. style="foo") values
477+
// the template compiler handles initial style (e.g. style="foo") values
436478
// in a special command called `elementStyle` so that the initial styles
437479
// can be processed during runtime. These initial styles values are bound to
438480
// a constant because the inital style values do not change (since they're static).
439481
paramsList.push(
440482
this.constantPool.getConstLiteral(o.literalArr(initialStyleDeclarations), true));
483+
} else if (initialClassDeclarations.length) {
484+
// no point in having an extra `null` value unless there are follow-up params
485+
paramsList.push(o.NULL_EXPR);
486+
}
487+
488+
if (initialClassDeclarations.length) {
489+
// the template compiler handles initial class styling (e.g. class="foo") values
490+
// in a special command called `elementClass` so that the initial class
491+
// can be processed during runtime. These initial class values are bound to
492+
// a constant because the inital class values do not change (since they're static).
493+
paramsList.push(
494+
this.constantPool.getConstLiteral(o.literalArr(initialClassDeclarations), true));
441495
}
496+
442497
this._creationCode.push(o.importExpr(R3.elementStyling).callFn(paramsList).toStmt());
443498
}
444499

@@ -465,25 +520,64 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
465520
});
466521
}
467522

468-
if (styleInputs.length && elementStyleIndex > 0) {
469-
const indexLiteral = o.literal(elementStyleIndex);
470-
styleInputs.forEach((input, i) => {
471-
const isMapBasedStyleBinding = i == 0 && input.name == 'style';
472-
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true);
473-
if (isMapBasedStyleBinding) {
474-
this.instruction(
475-
this._bindingCode, input.sourceSpan, R3.elementStyle, indexLiteral, convertedBinding);
476-
} else {
523+
if ((styleInputs.length || classInputs.length) && hasStylingInstructions) {
524+
const indexLiteral = o.literal(elementIndex);
525+
526+
const firstStyle = styleInputs[0];
527+
const mapBasedStyleInput = firstStyle && firstStyle.name == 'style' ? firstStyle : null;
528+
529+
const firstClass = classInputs[0];
530+
const mapBasedClassInput = firstClass && isClassBinding(firstClass) ? firstClass : null;
531+
532+
const stylingInput = mapBasedStyleInput || mapBasedClassInput;
533+
if (stylingInput) {
534+
const params: o.Expression[] = [];
535+
if (mapBasedStyleInput) {
536+
params.push(this.convertPropertyBinding(implicit, mapBasedStyleInput.value, true));
537+
} else if (mapBasedClassInput) {
538+
params.push(o.NULL_EXPR);
539+
}
540+
if (mapBasedClassInput) {
541+
params.push(this.convertPropertyBinding(implicit, mapBasedClassInput.value, true));
542+
}
543+
this.instruction(
544+
this._bindingCode, stylingInput.sourceSpan, R3.elementStylingMap, indexLiteral,
545+
...params);
546+
}
547+
548+
let lastInputCommand: t.BoundAttribute|null = null;
549+
if (styleInputs.length) {
550+
let i = mapBasedStyleInput ? 1 : 0;
551+
for (i; i < styleInputs.length; i++) {
552+
const input = styleInputs[i];
553+
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true);
477554
const key = input.name;
478-
let styleIndex: number = stylesIndexMap[key] !;
555+
const styleIndex: number = stylesIndexMap[key] !;
479556
this.instruction(
480557
this._bindingCode, input.sourceSpan, R3.elementStyleProp, indexLiteral,
481558
o.literal(styleIndex), convertedBinding);
482559
}
483-
});
484560

485-
const spanEnd = styleInputs[styleInputs.length - 1].sourceSpan;
486-
this.instruction(this._bindingCode, spanEnd, R3.elementStylingApply, indexLiteral);
561+
lastInputCommand = styleInputs[styleInputs.length - 1];
562+
}
563+
564+
if (classInputs.length) {
565+
let i = mapBasedClassInput ? 1 : 0;
566+
for (i; i < classInputs.length; i++) {
567+
const input = classInputs[i];
568+
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true);
569+
const key = input.name;
570+
const classIndex: number = classesIndexMap[key] !;
571+
this.instruction(
572+
this._bindingCode, input.sourceSpan, R3.elementClassProp, indexLiteral,
573+
o.literal(classIndex), convertedBinding);
574+
}
575+
576+
lastInputCommand = classInputs[classInputs.length - 1];
577+
}
578+
579+
this.instruction(
580+
this._bindingCode, lastInputCommand !.sourceSpan, R3.elementStylingApply, indexLiteral);
487581
}
488582

489583
// Generate element input bindings
@@ -494,18 +588,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
494588
}
495589

496590
const convertedBinding = this.convertPropertyBinding(implicit, input.value);
497-
const specialInstruction = SPECIAL_CASED_PROPERTIES_INSTRUCTION_MAP[input.name];
498-
if (specialInstruction) {
499-
// special case for [style] and [class] bindings since they are not handled as
500-
// standard properties within this implementation. Instead they are
501-
// handed off to special cased instruction handlers which will then
502-
// delegate them as animation sequences (or input bindings for dirs/cmps)
503-
this.instruction(
504-
this._bindingCode, input.sourceSpan, specialInstruction, o.literal(elementIndex),
505-
convertedBinding);
506-
return;
507-
}
508-
509591
const instruction = mapBindingToInstruction(input.type);
510592
if (instruction) {
511593
// TODO(chuckj): runtime: security context?
@@ -975,3 +1057,7 @@ export function makeBindingParser(): BindingParser {
9751057
new Parser(new Lexer()), DEFAULT_INTERPOLATION_CONFIG, new DomElementSchemaRegistry(), null,
9761058
[]);
9771059
}
1060+
1061+
function isClassBinding(input: t.BoundAttribute): boolean {
1062+
return input.name == 'className' || input.name == 'class';
1063+
}

0 commit comments

Comments
 (0)