From 20ea5b5634325f323612a8e03d658ff7e87bfb07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Fri, 16 Nov 2018 15:22:12 +0100 Subject: [PATCH] refactor(ivy): ensure directive host bindings use the styling algorithm (#27145) PR Close #27145 --- .../r3_view_compiler_styling_spec.ts | 175 ++++++++++++++++++ .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 3 +- .../compiler/src/render3/view/compiler.ts | 135 +++++++++++--- packages/compiler/src/render3/view/styling.ts | 141 +++++++++----- .../compiler/src/render3/view/template.ts | 4 +- packages/core/src/render3/instructions.ts | 20 +- 6 files changed, 398 insertions(+), 80 deletions(-) diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index df66bc6240..548fb6feec 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -724,4 +724,179 @@ describe('compiler compliance: styling', () => { expectEmit(result.source, template, 'Incorrect template'); }); }); + + describe('@Component host styles/classes', () => { + it('should generate style/class instructions for a host component creation definition', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule, HostBinding} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: '', + host: { + 'style': 'width:200px; height:500px', + 'class': 'foo baz' + } + }) + export class MyComponent { + @HostBinding('style') + myStyle = {width:'100px'}; + + @HostBinding('class') + myClass = {bar:false}; + + @HostBinding('style.color') + myColorProp = 'red'; + + @HostBinding('class.foo') + myFooClass = 'red'; + } + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const template = ` + const _c0 = ["foo", "baz", ${InitialStylingFlags.VALUES_MODE}, "foo", true, "baz", true]; + const _c1 = ["width", "height", "color", ${InitialStylingFlags.VALUES_MODE}, "width", "200px", "height", "500px"]; + … + hostBindings: function MyComponent_HostBindings(dirIndex, elIndex) { + $r3$.ɵelementStyling(_c0, _c1, $r3$.ɵdefaultStyleSanitizer, dirIndex); + $r3$.ɵelementStylingMap(elIndex, $r3$.ɵload(dirIndex).myClass, $r3$.ɵload(dirIndex).myStyle, dirIndex); + $r3$.ɵelementStyleProp(elIndex, 2, $r3$.ɵload(dirIndex).myColorProp, null, dirIndex); + $r3$.ɵelementClassProp(elIndex, 0, $r3$.ɵload(dirIndex).myFooClass, dirIndex); + $r3$.ɵelementStylingApply(elIndex, dirIndex); + } + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should generate style/class instructions for multiple host binding definitions', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule, HostBinding} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: '', + host: { + '[style.height.pt]': 'myHeightProp', + '[class.bar]': 'myBarClass' + } + }) + export class MyComponent { + myHeightProp = 20; + myBarClass = true; + + @HostBinding('style') + myStyle = {}; + + @HostBinding('style.width') + myWidthProp = '500px'; + + @HostBinding('class.foo') + myFooClass = true; + + @HostBinding('class') + myClasses = {a:true, b:true}; + } + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const template = ` + const _c0 = ["bar", "foo"]; + const _c1 = ["height", "width"]; + … + hostBindings: function MyComponent_HostBindings(dirIndex, elIndex) { + $r3$.ɵelementStyling(_c0, _c1, $r3$.ɵdefaultStyleSanitizer, dirIndex); + $r3$.ɵelementStylingMap(elIndex, $r3$.ɵload(dirIndex).myClasses, $r3$.ɵload(dirIndex).myStyle, dirIndex); + $r3$.ɵelementStyleProp(elIndex, 0, $r3$.ɵload(dirIndex).myHeightProp, "pt", dirIndex); + $r3$.ɵelementStyleProp(elIndex, 1, $r3$.ɵload(dirIndex).myWidthProp, null, dirIndex); + $r3$.ɵelementClassProp(elIndex, 0, $r3$.ɵload(dirIndex).myBarClass, dirIndex); + $r3$.ɵelementClassProp(elIndex, 1, $r3$.ɵload(dirIndex).myFooClass, dirIndex); + $r3$.ɵelementStylingApply(elIndex, dirIndex); + } + `; + + 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 = { + app: { + 'spec.ts': ` + import {Directive, Component, NgModule, HostBinding} from '@angular/core'; + + @Directive({selector: '[myWidthDir]'}) + export class WidthDirective { + @HostBinding('style.width') + myWidth = 200; + + @HostBinding('class.foo') + myFooClass = true; + } + + @Directive({selector: '[myHeightDir]'}) + export class HeightDirective { + @HostBinding('style.height') + myHeight = 200; + + @HostBinding('class.bar') + myBarClass = true; + } + + @Component({ + selector: 'my-component', + template: ' +
+ ', + }) + export class MyComponent { + } + + @NgModule({declarations: [MyComponent, WidthDirective, HeightDirective]}) + export class MyModule {} + ` + } + }; + + const template = ` + const _c0 = ["foo"]; + const _c1 = ["width"]; + const _c2 = ["bar"]; + const _c3 = ["height"]; + … + function WidthDirective_HostBindings(dirIndex, elIndex) { + $r3$.ɵelementStyling(_c0, _c1, null, dirIndex); + $r3$.ɵelementStyleProp(elIndex, 0, $r3$.ɵload(dirIndex).myWidth, null, dirIndex); + $r3$.ɵelementClassProp(elIndex, 0, $r3$.ɵload(dirIndex).myFooClass, dirIndex); + $r3$.ɵelementStylingApply(elIndex, dirIndex); + } + … + function HeightDirective_HostBindings(dirIndex, elIndex) { + $r3$.ɵelementStyling(_c2, _c3, null, dirIndex); + $r3$.ɵelementStyleProp(elIndex, 0, $r3$.ɵload(dirIndex).myHeight, null, dirIndex); + $r3$.ɵelementClassProp(elIndex, 0, $r3$.ɵload(dirIndex).myBarClass, dirIndex); + $r3$.ɵelementStylingApply(elIndex, dirIndex); + } + … + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); + }); }); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 873fd03d67..639838712d 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -481,8 +481,7 @@ describe('ngtsc behavioral tests', () => { expect(jsContents) .toContain(`i0.ɵelementProperty(elIndex, "prop", i0.ɵbind(i0.ɵload(dirIndex).bar));`); expect(jsContents) - .toContain( - 'i0.ɵelementProperty(elIndex, "class.someclass", i0.ɵbind(i0.ɵload(dirIndex).someClass))'); + .toContain('i0.ɵelementClassProp(elIndex, 0, i0.ɵload(dirIndex).someClass, dirIndex)'); const factoryDef = ` factory: function FooCmp_Factory(t) { diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index ea8c32d01c..285167b14d 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -12,7 +12,7 @@ import {CompileReflector} from '../../compile_reflector'; import {BindingForm, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter'; import {ConstantPool, DefinitionKind} from '../../constant_pool'; import * as core from '../../core'; -import {ParsedEvent} from '../../expression_parser/ast'; +import {AST, ParsedEvent} from '../../expression_parser/ast'; import {LifecycleHooks} from '../../lifecycle_reflector'; import * as o from '../../output/output_ast'; import {typeSourceSpan} from '../../parse_util'; @@ -27,6 +27,7 @@ import {Render3ParseResult} from '../r3_template_transform'; import {typeWithParameters} from '../util'; import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api'; +import {StylingBuilder, StylingInstruction} from './styling'; import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template'; import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, mapToExpression, temporaryAllocator} from './util'; @@ -65,23 +66,48 @@ function baseDirectiveFields( // Initialize hostVars to number of bound host properties (interpolations illegal) let hostVars = Object.keys(meta.host.properties).length; + const elVarExp = o.variable('elIndex'); + const dirVarExp = o.variable('dirIndex'); + const styleBuilder = new StylingBuilder(elVarExp, dirVarExp); + + const allOtherAttributes: any = {}; + const attrNames = Object.getOwnPropertyNames(meta.host.attributes); + for (let i = 0; i < attrNames.length; i++) { + const attr = attrNames[i]; + const value = meta.host.attributes[attr]; + switch (attr) { + // style attributes are handled in the styling context + case 'style': + styleBuilder.registerStyleAttr(value); + break; + // class attributes are handled in the styling context + case 'class': + styleBuilder.registerClassAttr(value); + break; + default: + allOtherAttributes[attr] = value; + break; + } + } + + // e.g. `attributes: ['role', 'listbox']` + definitionMap.set('attributes', createHostAttributesArray(allOtherAttributes)); + // e.g. `hostBindings: (dirIndex, elIndex) => { ... } definitionMap.set( 'hostBindings', - createHostBindingsFunction(meta, bindingParser, constantPool, (slots: number) => { - const originalSlots = hostVars; - hostVars += slots; - return originalSlots; - })); + createHostBindingsFunction( + meta, elVarExp, dirVarExp, styleBuilder, bindingParser, constantPool, (slots: number) => { + const originalSlots = hostVars; + hostVars += slots; + return originalSlots; + })); if (hostVars) { // e.g. `hostVars: 2 definitionMap.set('hostVars', o.literal(hostVars)); } - // e.g. `attributes: ['role', 'listbox']` - definitionMap.set('attributes', createHostAttributesArray(meta)); - // e.g 'inputs: {a: 'a'}` definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs)); @@ -466,9 +492,8 @@ function createDirectiveSelector(selector: string): o.Expression { return asLiteral(core.parseSelectorToR3Selector(selector)); } -function createHostAttributesArray(meta: R3DirectiveMetadata): o.Expression|null { +function createHostAttributesArray(attributes: any): o.Expression|null { const values: o.Expression[] = []; - const attributes = meta.host.attributes; for (let key of Object.getOwnPropertyNames(attributes)) { const value = attributes[key]; values.push(o.literal(key), o.literal(value)); @@ -611,7 +636,8 @@ function createViewQueriesFunction( // Return a host binding function or null if one is not necessary. function createHostBindingsFunction( - meta: R3DirectiveMetadata, bindingParser: BindingParser, constantPool: ConstantPool, + meta: R3DirectiveMetadata, elVarExp: o.ReadVarExpr, dirVarExp: o.ReadVarExpr, + styleBuilder: StylingBuilder, bindingParser: BindingParser, constantPool: ConstantPool, allocatePureFunctionSlots: (slots: number) => number): o.Expression|null { const statements: o.Statement[] = []; @@ -621,7 +647,13 @@ function createHostBindingsFunction( // Calculate the host property bindings const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan); - const bindingContext = o.importExpr(R3.load).callFn([o.variable('dirIndex')]); + const bindingContext = o.importExpr(R3.load).callFn([dirVarExp]); + + const bindingFn = (implicit: any, value: AST) => { + return convertPropertyBinding( + null, implicit, value, 'b', BindingForm.TrySimple, () => error('Unexpected interpolation')); + }; + if (bindings) { const valueConverter = new ValueConverter( constantPool, @@ -629,22 +661,43 @@ function createHostBindingsFunction( /* pipes are illegal here */ () => error('Unexpected pipe')); for (const binding of bindings) { - // resolve literal arrays and literal objects - const value = binding.expression.visit(valueConverter); - const bindingExpr = convertPropertyBinding( - null, bindingContext, value, 'b', BindingForm.TrySimple, - () => error('Unexpected interpolation')); + const name = binding.name; + const stylePrefix = name.substring(0, 5).toLowerCase(); + if (stylePrefix === 'style') { + const {propertyName, unit} = parseNamedProperty(name); + styleBuilder.registerStyleInput(propertyName, binding.expression, unit, binding.sourceSpan); + } else if (stylePrefix === 'class') { + styleBuilder.registerClassInput( + parseNamedProperty(name).propertyName, binding.expression, binding.sourceSpan); + } else { + // resolve literal arrays and literal objects + const value = binding.expression.visit(valueConverter); + const bindingExpr = bindingFn(bindingContext, value); - const {bindingName, instruction} = getBindingNameAndInstruction(binding.name); + const {bindingName, instruction} = getBindingNameAndInstruction(name); - statements.push(...bindingExpr.stmts); - statements.push(o.importExpr(instruction) - .callFn([ - o.variable('elIndex'), - o.literal(bindingName), - o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]), - ]) - .toStmt()); + statements.push(...bindingExpr.stmts); + statements.push(o.importExpr(instruction) + .callFn([ + elVarExp, + o.literal(bindingName), + o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]), + ]) + .toStmt()); + } + } + + if (styleBuilder.hasBindingsOrInitialValues) { + const createInstruction = styleBuilder.buildCreateLevelInstruction(null, constantPool); + if (createInstruction) { + const createStmt = createStylingStmt(createInstruction, bindingContext, bindingFn); + statements.push(createStmt); + } + + styleBuilder.buildUpdateLevelInstructions(valueConverter).forEach(instruction => { + const updateStmt = createStylingStmt(instruction, bindingContext, bindingFn); + statements.push(updateStmt); + }); } } @@ -652,8 +705,8 @@ function createHostBindingsFunction( const typeName = meta.name; return o.fn( [ - new o.FnParam('dirIndex', o.NUMBER_TYPE), - new o.FnParam('elIndex', o.NUMBER_TYPE), + new o.FnParam(dirVarExp.name !, o.NUMBER_TYPE), + new o.FnParam(elVarExp.name !, o.NUMBER_TYPE), ], statements, o.INFERRED_TYPE, null, typeName ? `${typeName}_HostBindings` : null); } @@ -661,6 +714,14 @@ function createHostBindingsFunction( return null; } +function createStylingStmt( + instruction: StylingInstruction, bindingContext: any, bindingFn: Function): o.Statement { + const params = instruction.buildParams(value => bindingFn(bindingContext, value).currValExpr); + return o.importExpr(instruction.reference, null, instruction.sourceSpan) + .callFn(params, instruction.sourceSpan) + .toStmt(); +} + function getBindingNameAndInstruction(bindingName: string): {bindingName: string, instruction: o.ExternalReference} { let instruction !: o.ExternalReference; @@ -768,3 +829,19 @@ function compileStyles(styles: string[], selector: string, hostSelector: string) const shadowCss = new ShadowCss(); return styles.map(style => { return shadowCss !.shimCssText(style, selector, hostSelector); }); } + +function parseNamedProperty(name: string): {propertyName: string, unit: string} { + let unit = ''; + let propertyName = ''; + const index = name.indexOf('.'); + if (index > 0) { + const unitIndex = name.lastIndexOf('.'); + if (unitIndex !== index) { + unit = name.substring(unitIndex + 1, name.length); + propertyName = name.substring(index + 1, unitIndex); + } else { + propertyName = name.substring(index + 1, name.length); + } + } + return {propertyName, unit}; +} \ No newline at end of file diff --git a/packages/compiler/src/render3/view/styling.ts b/packages/compiler/src/render3/view/styling.ts index a0ecaf3252..98cdbbc821 100644 --- a/packages/compiler/src/render3/view/styling.ts +++ b/packages/compiler/src/render3/view/styling.ts @@ -7,7 +7,7 @@ */ import {ConstantPool} from '../../constant_pool'; import {InitialStylingFlags} from '../../core'; -import {BindingType} from '../../expression_parser/ast'; +import {AST, BindingType, ParseSpan} from '../../expression_parser/ast'; import * as o from '../../output/output_ast'; import {ParseSourceSpan} from '../../parse_util'; import * as t from '../r3_ast'; @@ -16,6 +16,7 @@ import {Identifiers as R3} from '../r3_identifiers'; import {parse as parseStyle} from './style_parser'; import {ValueConverter} from './template'; + /** * A styling expression summary that is to be processed by the compiler */ @@ -25,6 +26,17 @@ export interface StylingInstruction { buildParams(convertFn: (value: any) => o.Expression): o.Expression[]; } +/** + * An internal record of the input data for a styling binding + */ +interface BoundStylingEntry { + name: string; + unit: string|null; + sourceSpan: ParseSourceSpan; + value: AST; +} + + /** * Produces creation/update instructions for all styling bindings (class and style) * @@ -53,12 +65,11 @@ export interface StylingInstruction { export class StylingBuilder { public readonly hasBindingsOrInitialValues = false; - private _indexLiteral: o.LiteralExpr; - private _classMapInput: t.BoundAttribute|null = null; - private _styleMapInput: t.BoundAttribute|null = null; - private _singleStyleInputs: t.BoundAttribute[]|null = null; - private _singleClassInputs: t.BoundAttribute[]|null = null; - private _lastStylingInput: t.BoundAttribute|null = null; + private _classMapInput: BoundStylingEntry|null = null; + private _styleMapInput: BoundStylingEntry|null = null; + private _singleStyleInputs: BoundStylingEntry[]|null = null; + private _singleClassInputs: BoundStylingEntry[]|null = null; + private _lastStylingInput: BoundStylingEntry|null = null; // maps are used instead of hash maps because a Map will // retain the ordering of the keys @@ -69,46 +80,69 @@ export class StylingBuilder { private _useDefaultSanitizer = false; private _applyFnRequired = false; - constructor(elementIndex: number) { this._indexLiteral = o.literal(elementIndex); } + constructor( + private _elementIndexExpr: o.Expression, private _directiveIndexExpr: o.Expression|null) {} - registerInput(input: t.BoundAttribute): boolean { + registerBoundInput(input: t.BoundAttribute): boolean { // [attr.style] or [attr.class] are skipped in the code below, // they should not be treated as styling-based bindings since // they are intended to be written directly to the attr and // will therefore skip all style/class resolution that is present // with style="", [style]="" and [style.prop]="", class="", // [class.prop]="". [class]="" assignments - let registered = false; const name = input.name; + let binding: BoundStylingEntry|null = null; switch (input.type) { case BindingType.Property: if (name == 'style') { - this._styleMapInput = input; - this._useDefaultSanitizer = true; - registered = true; - } else if (isClassBinding(input)) { - this._classMapInput = input; - registered = true; + binding = this.registerStyleInput(null, input.value, '', input.sourceSpan); + } else if (isClassBinding(input.name)) { + binding = this.registerClassInput(null, input.value, input.sourceSpan); } break; case BindingType.Style: - (this._singleStyleInputs = this._singleStyleInputs || []).push(input); - this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(name); - registerIntoMap(this._stylesIndex, name); - registered = true; + binding = this.registerStyleInput(input.name, input.value, input.unit, input.sourceSpan); break; case BindingType.Class: - (this._singleClassInputs = this._singleClassInputs || []).push(input); - registerIntoMap(this._classesIndex, name); - registered = true; + binding = this.registerClassInput(input.name, input.value, input.sourceSpan); break; } - if (registered) { - this._lastStylingInput = input; + return binding ? true : false; + } + + registerStyleInput( + propertyName: string|null, value: AST, unit: string|null, + sourceSpan: ParseSourceSpan): BoundStylingEntry { + const entry = { name: propertyName, unit, value, sourceSpan } as BoundStylingEntry; + if (propertyName) { + (this._singleStyleInputs = this._singleStyleInputs || []).push(entry); + this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(propertyName); + registerIntoMap(this._stylesIndex, propertyName); (this as any).hasBindingsOrInitialValues = true; - this._applyFnRequired = true; + } else { + this._useDefaultSanitizer = true; + this._styleMapInput = entry; } - return registered; + this._lastStylingInput = entry; + (this as any).hasBindingsOrInitialValues = true; + this._applyFnRequired = true; + return entry; + } + + registerClassInput(className: string|null, value: AST, sourceSpan: ParseSourceSpan): + BoundStylingEntry { + const entry = { name: className, value, sourceSpan } as BoundStylingEntry; + if (className) { + (this._singleClassInputs = this._singleClassInputs || []).push(entry); + (this as any).hasBindingsOrInitialValues = true; + registerIntoMap(this._classesIndex, className); + } else { + this._classMapInput = entry; + } + this._lastStylingInput = entry; + (this as any).hasBindingsOrInitialValues = true; + this._applyFnRequired = true; + return entry; } registerStyleAttr(value: string) { @@ -153,7 +187,7 @@ export class StylingBuilder { return exprs.length ? o.literalArr(exprs) : null; } - buildCreateLevelInstruction(sourceSpan: ParseSourceSpan, constantPool: ConstantPool): + buildCreateLevelInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool): StylingInstruction|null { if (this.hasBindingsOrInitialValues) { const initialClasses = this._buildInitExpr(this._classesIndex, this._initialClassValues); @@ -183,13 +217,16 @@ export class StylingBuilder { // can be processed during runtime. These initial styles values are bound to // a constant because the inital style values do not change (since they're static). params.push(constantPool.getConstLiteral(initialStyles, true)); - } else if (useSanitizer) { + } else if (useSanitizer || this._directiveIndexExpr) { // no point in having an extra `null` value unless there are follow-up params params.push(o.NULL_EXPR); } - if (useSanitizer) { - params.push(o.importExpr(R3.defaultStyleSanitizer)); + if (useSanitizer || this._directiveIndexExpr) { + params.push(useSanitizer ? o.importExpr(R3.defaultStyleSanitizer) : o.NULL_EXPR); + if (this._directiveIndexExpr) { + params.push(this._directiveIndexExpr); + } } return {sourceSpan, reference: R3.elementStyling, buildParams: () => params}; @@ -213,7 +250,7 @@ export class StylingBuilder { sourceSpan: stylingInput.sourceSpan, reference: R3.elementStylingMap, buildParams: (convertFn: (value: any) => o.Expression) => { - const params: o.Expression[] = [this._indexLiteral]; + const params: o.Expression[] = [this._elementIndexExpr]; if (mapBasedClassValue) { params.push(convertFn(mapBasedClassValue)); @@ -223,6 +260,12 @@ export class StylingBuilder { if (mapBasedStyleValue) { params.push(convertFn(mapBasedStyleValue)); + } else if (this._directiveIndexExpr) { + params.push(o.NULL_EXPR); + } + + if (this._directiveIndexExpr) { + params.push(this._directiveIndexExpr); } return params; @@ -233,8 +276,8 @@ export class StylingBuilder { } private _buildSingleInputs( - reference: o.ExternalReference, inputs: t.BoundAttribute[], mapIndex: Map, - valueConverter: ValueConverter): StylingInstruction[] { + reference: o.ExternalReference, inputs: BoundStylingEntry[], mapIndex: Map, + allowUnits: boolean, valueConverter: ValueConverter): StylingInstruction[] { return inputs.map(input => { const bindingIndex: number = mapIndex.get(input.name) !; const value = input.value.visit(valueConverter); @@ -242,9 +285,17 @@ export class StylingBuilder { sourceSpan: input.sourceSpan, reference, buildParams: (convertFn: (value: any) => o.Expression) => { - const params = [this._indexLiteral, o.literal(bindingIndex), convertFn(value)]; - if (input.unit != null) { - params.push(o.literal(input.unit)); + const params = [this._elementIndexExpr, o.literal(bindingIndex), convertFn(value)]; + if (allowUnits) { + if (input.unit) { + params.push(o.literal(input.unit)); + } else if (this._directiveIndexExpr) { + params.push(o.NULL_EXPR); + } + } + + if (this._directiveIndexExpr) { + params.push(this._directiveIndexExpr); } return params; } @@ -255,7 +306,7 @@ export class StylingBuilder { private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] { if (this._singleClassInputs) { return this._buildSingleInputs( - R3.elementClassProp, this._singleClassInputs, this._classesIndex, valueConverter); + R3.elementClassProp, this._singleClassInputs, this._classesIndex, false, valueConverter); } return []; } @@ -263,7 +314,7 @@ export class StylingBuilder { private _buildStyleInputs(valueConverter: ValueConverter): StylingInstruction[] { if (this._singleStyleInputs) { return this._buildSingleInputs( - R3.elementStyleProp, this._singleStyleInputs, this._stylesIndex, valueConverter); + R3.elementStyleProp, this._singleStyleInputs, this._stylesIndex, true, valueConverter); } return []; } @@ -272,7 +323,13 @@ export class StylingBuilder { return { sourceSpan: this._lastStylingInput ? this._lastStylingInput.sourceSpan : null, reference: R3.elementStylingApply, - buildParams: () => [this._indexLiteral] + buildParams: () => { + const params: o.Expression[] = [this._elementIndexExpr]; + if (this._directiveIndexExpr) { + params.push(this._directiveIndexExpr); + } + return params; + } }; } @@ -293,8 +350,8 @@ export class StylingBuilder { } } -function isClassBinding(input: t.BoundAttribute): boolean { - return input.name == 'className' || input.name == 'class'; +function isClassBinding(name: string): boolean { + return name == 'className' || name == 'class'; } function registerIntoMap(map: Map, key: string) { diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index da83a9fbf6..aed01583e0 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -431,7 +431,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver visitElement(element: t.Element) { const elementIndex = this.allocateDataSlot(); - const stylingBuilder = new StylingBuilder(elementIndex); + const stylingBuilder = new StylingBuilder(o.literal(elementIndex), null); let isNonBindableMode: boolean = false; const isI18nRootElement: boolean = isI18nRootNode(element.i18n); @@ -476,7 +476,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const allOtherInputs: t.BoundAttribute[] = []; element.inputs.forEach((input: t.BoundAttribute) => { - if (!stylingBuilder.registerInput(input)) { + if (!stylingBuilder.registerBoundInput(input)) { if (input.type == BindingType.Property) { if (input.i18n) { i18nAttrs.push(input); diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 20a8b3cb20..df1d347ee4 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -1072,9 +1072,11 @@ function generatePropertyAliases( * @param className Name of class to toggle. Because it is going to DOM, this is not subject to * renaming as part of minification. * @param value A value indicating if a given class should be added or removed. + * @param directiveIndex the index for the directive that is attempting to change styling. */ export function elementClassProp( - index: number, stylingIndex: number, value: boolean | PlayerFactory): void { + index: number, stylingIndex: number, value: boolean | PlayerFactory, + directiveIndex?: number): void { const val = (value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory) : (!!value); updateElementClassProp(getStylingContext(index, getViewData()), stylingIndex, val); @@ -1107,11 +1109,13 @@ export function elementClassProp( * values that are passed in here will be applied to the element (if matched). * @param styleSanitizer An optional sanitizer function that will be used (if provided) * to sanitize the any CSS property values that are applied to the element (during rendering). + * @param directiveIndex the index for the directive that is attempting to change styling. */ export function elementStyling( classDeclarations?: (string | boolean | InitialStylingFlags)[] | null, styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, - styleSanitizer?: StyleSanitizeFn | null): void { + styleSanitizer?: StyleSanitizeFn | null, directiveIndex?: number): void { + if (directiveIndex) return; // supported in next PR const tNode = getPreviousOrParentTNode(); const inputData = initializeTNodeInputs(tNode); @@ -1152,8 +1156,10 @@ export function elementStyling( * (Note that this is not the element index, but rather an index value allocated * specifically for element styling--the index must be the next index after the element * index.) + * @param directiveIndex the index for the directive that is attempting to change styling. */ -export function elementStylingApply(index: number): void { +export function elementStylingApply(index: number, directiveIndex?: number): void { + if (directiveIndex) return; // supported in next PR const viewData = getViewData(); const isFirstRender = (viewData[FLAGS] & LViewFlags.CreationMode) !== 0; const totalPlayersQueued = renderStyleAndClassBindings( @@ -1183,10 +1189,12 @@ export function elementStylingApply(index: number): void { * @param suffix Optional suffix. Used with scalar values to add unit such as `px`. * Note that when a suffix is provided then the underlying sanitizer will * be ignored. + * @param directiveIndex the index for the directive that is attempting to change styling. */ export function elementStyleProp( index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, - suffix?: string): void { + suffix?: string, directiveIndex?: number): void { + if (directiveIndex) return; // supported in next PR let valueToAdd: string|null = null; if (value) { if (suffix) { @@ -1224,10 +1232,12 @@ export function elementStyleProp( * @param styles A key/value style map of the styles that will be applied to the given element. * Any missing styles (that have already been applied to the element beforehand) will be * removed (unset) from the element's styling. + * @param directiveIndex the index for the directive that is attempting to change styling. */ export function elementStylingMap( index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, - styles?: {[styleName: string]: any} | NO_CHANGE | null): void { + styles?: {[styleName: string]: any} | NO_CHANGE | null, directiveIndex?: number): void { + if (directiveIndex) return; // supported in next PR const viewData = getViewData(); const tNode = getTNode(index, viewData); const stylingContext = getStylingContext(index, viewData);