refactor(ivy): ensure directive host bindings use the styling algorithm (#27145)

PR Close #27145
This commit is contained in:
Matias Niemelä 2018-11-16 15:22:12 +01:00 committed by Misko Hevery
parent 4222b63639
commit 20ea5b5634
6 changed files with 398 additions and 80 deletions

View File

@ -724,4 +724,179 @@ describe('compiler compliance: styling', () => {
expectEmit(result.source, template, 'Incorrect template'); 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: '
<div myWidthDir myHeightDir></div>
',
})
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');
});
});
}); });

View File

@ -481,8 +481,7 @@ describe('ngtsc behavioral tests', () => {
expect(jsContents) expect(jsContents)
.toContain(`i0.ɵelementProperty(elIndex, "prop", i0.ɵbind(i0.ɵload(dirIndex).bar));`); .toContain(`i0.ɵelementProperty(elIndex, "prop", i0.ɵbind(i0.ɵload(dirIndex).bar));`);
expect(jsContents) expect(jsContents)
.toContain( .toContain('i0.ɵelementClassProp(elIndex, 0, i0.ɵload(dirIndex).someClass, dirIndex)');
'i0.ɵelementProperty(elIndex, "class.someclass", i0.ɵbind(i0.ɵload(dirIndex).someClass))');
const factoryDef = ` const factoryDef = `
factory: function FooCmp_Factory(t) { factory: function FooCmp_Factory(t) {

View File

@ -12,7 +12,7 @@ import {CompileReflector} from '../../compile_reflector';
import {BindingForm, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter'; import {BindingForm, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
import {ConstantPool, DefinitionKind} from '../../constant_pool'; import {ConstantPool, DefinitionKind} from '../../constant_pool';
import * as core from '../../core'; import * as core from '../../core';
import {ParsedEvent} from '../../expression_parser/ast'; import {AST, ParsedEvent} from '../../expression_parser/ast';
import {LifecycleHooks} from '../../lifecycle_reflector'; import {LifecycleHooks} from '../../lifecycle_reflector';
import * as o from '../../output/output_ast'; import * as o from '../../output/output_ast';
import {typeSourceSpan} from '../../parse_util'; import {typeSourceSpan} from '../../parse_util';
@ -27,6 +27,7 @@ import {Render3ParseResult} from '../r3_template_transform';
import {typeWithParameters} from '../util'; import {typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api'; import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
import {StylingBuilder, StylingInstruction} from './styling';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template'; import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, mapToExpression, temporaryAllocator} from './util'; import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, mapToExpression, temporaryAllocator} from './util';
@ -65,10 +66,38 @@ function baseDirectiveFields(
// Initialize hostVars to number of bound host properties (interpolations illegal) // Initialize hostVars to number of bound host properties (interpolations illegal)
let hostVars = Object.keys(meta.host.properties).length; 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) => { ... } // e.g. `hostBindings: (dirIndex, elIndex) => { ... }
definitionMap.set( definitionMap.set(
'hostBindings', 'hostBindings',
createHostBindingsFunction(meta, bindingParser, constantPool, (slots: number) => { createHostBindingsFunction(
meta, elVarExp, dirVarExp, styleBuilder, bindingParser, constantPool, (slots: number) => {
const originalSlots = hostVars; const originalSlots = hostVars;
hostVars += slots; hostVars += slots;
return originalSlots; return originalSlots;
@ -79,9 +108,6 @@ function baseDirectiveFields(
definitionMap.set('hostVars', o.literal(hostVars)); definitionMap.set('hostVars', o.literal(hostVars));
} }
// e.g. `attributes: ['role', 'listbox']`
definitionMap.set('attributes', createHostAttributesArray(meta));
// e.g 'inputs: {a: 'a'}` // e.g 'inputs: {a: 'a'}`
definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs)); definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs));
@ -466,9 +492,8 @@ function createDirectiveSelector(selector: string): o.Expression {
return asLiteral(core.parseSelectorToR3Selector(selector)); return asLiteral(core.parseSelectorToR3Selector(selector));
} }
function createHostAttributesArray(meta: R3DirectiveMetadata): o.Expression|null { function createHostAttributesArray(attributes: any): o.Expression|null {
const values: o.Expression[] = []; const values: o.Expression[] = [];
const attributes = meta.host.attributes;
for (let key of Object.getOwnPropertyNames(attributes)) { for (let key of Object.getOwnPropertyNames(attributes)) {
const value = attributes[key]; const value = attributes[key];
values.push(o.literal(key), o.literal(value)); 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. // Return a host binding function or null if one is not necessary.
function createHostBindingsFunction( 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 { allocatePureFunctionSlots: (slots: number) => number): o.Expression|null {
const statements: o.Statement[] = []; const statements: o.Statement[] = [];
@ -621,7 +647,13 @@ function createHostBindingsFunction(
// Calculate the host property bindings // Calculate the host property bindings
const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan); 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) { if (bindings) {
const valueConverter = new ValueConverter( const valueConverter = new ValueConverter(
constantPool, constantPool,
@ -629,18 +661,25 @@ function createHostBindingsFunction(
/* pipes are illegal here */ () => error('Unexpected pipe')); /* pipes are illegal here */ () => error('Unexpected pipe'));
for (const binding of bindings) { for (const binding of bindings) {
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 // resolve literal arrays and literal objects
const value = binding.expression.visit(valueConverter); const value = binding.expression.visit(valueConverter);
const bindingExpr = convertPropertyBinding( const bindingExpr = bindingFn(bindingContext, value);
null, bindingContext, value, 'b', BindingForm.TrySimple,
() => error('Unexpected interpolation'));
const {bindingName, instruction} = getBindingNameAndInstruction(binding.name); const {bindingName, instruction} = getBindingNameAndInstruction(name);
statements.push(...bindingExpr.stmts); statements.push(...bindingExpr.stmts);
statements.push(o.importExpr(instruction) statements.push(o.importExpr(instruction)
.callFn([ .callFn([
o.variable('elIndex'), elVarExp,
o.literal(bindingName), o.literal(bindingName),
o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]), o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]),
]) ])
@ -648,12 +687,26 @@ function createHostBindingsFunction(
} }
} }
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);
});
}
}
if (statements.length > 0) { if (statements.length > 0) {
const typeName = meta.name; const typeName = meta.name;
return o.fn( return o.fn(
[ [
new o.FnParam('dirIndex', o.NUMBER_TYPE), new o.FnParam(dirVarExp.name !, o.NUMBER_TYPE),
new o.FnParam('elIndex', o.NUMBER_TYPE), new o.FnParam(elVarExp.name !, o.NUMBER_TYPE),
], ],
statements, o.INFERRED_TYPE, null, typeName ? `${typeName}_HostBindings` : null); statements, o.INFERRED_TYPE, null, typeName ? `${typeName}_HostBindings` : null);
} }
@ -661,6 +714,14 @@ function createHostBindingsFunction(
return null; 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): function getBindingNameAndInstruction(bindingName: string):
{bindingName: string, instruction: o.ExternalReference} { {bindingName: string, instruction: o.ExternalReference} {
let instruction !: o.ExternalReference; let instruction !: o.ExternalReference;
@ -768,3 +829,19 @@ function compileStyles(styles: string[], selector: string, hostSelector: string)
const shadowCss = new ShadowCss(); const shadowCss = new ShadowCss();
return styles.map(style => { return shadowCss !.shimCssText(style, selector, hostSelector); }); 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};
}

View File

@ -7,7 +7,7 @@
*/ */
import {ConstantPool} from '../../constant_pool'; import {ConstantPool} from '../../constant_pool';
import {InitialStylingFlags} from '../../core'; 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 * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util'; import {ParseSourceSpan} from '../../parse_util';
import * as t from '../r3_ast'; 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 {parse as parseStyle} from './style_parser';
import {ValueConverter} from './template'; import {ValueConverter} from './template';
/** /**
* A styling expression summary that is to be processed by the compiler * 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[]; 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) * Produces creation/update instructions for all styling bindings (class and style)
* *
@ -53,12 +65,11 @@ export interface StylingInstruction {
export class StylingBuilder { export class StylingBuilder {
public readonly hasBindingsOrInitialValues = false; public readonly hasBindingsOrInitialValues = false;
private _indexLiteral: o.LiteralExpr; private _classMapInput: BoundStylingEntry|null = null;
private _classMapInput: t.BoundAttribute|null = null; private _styleMapInput: BoundStylingEntry|null = null;
private _styleMapInput: t.BoundAttribute|null = null; private _singleStyleInputs: BoundStylingEntry[]|null = null;
private _singleStyleInputs: t.BoundAttribute[]|null = null; private _singleClassInputs: BoundStylingEntry[]|null = null;
private _singleClassInputs: t.BoundAttribute[]|null = null; private _lastStylingInput: BoundStylingEntry|null = null;
private _lastStylingInput: t.BoundAttribute|null = null;
// maps are used instead of hash maps because a Map will // maps are used instead of hash maps because a Map will
// retain the ordering of the keys // retain the ordering of the keys
@ -69,46 +80,69 @@ export class StylingBuilder {
private _useDefaultSanitizer = false; private _useDefaultSanitizer = false;
private _applyFnRequired = 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, // [attr.style] or [attr.class] are skipped in the code below,
// they should not be treated as styling-based bindings since // they should not be treated as styling-based bindings since
// they are intended to be written directly to the attr and // they are intended to be written directly to the attr and
// will therefore skip all style/class resolution that is present // will therefore skip all style/class resolution that is present
// with style="", [style]="" and [style.prop]="", class="", // with style="", [style]="" and [style.prop]="", class="",
// [class.prop]="". [class]="" assignments // [class.prop]="". [class]="" assignments
let registered = false;
const name = input.name; const name = input.name;
let binding: BoundStylingEntry|null = null;
switch (input.type) { switch (input.type) {
case BindingType.Property: case BindingType.Property:
if (name == 'style') { if (name == 'style') {
this._styleMapInput = input; binding = this.registerStyleInput(null, input.value, '', input.sourceSpan);
this._useDefaultSanitizer = true; } else if (isClassBinding(input.name)) {
registered = true; binding = this.registerClassInput(null, input.value, input.sourceSpan);
} else if (isClassBinding(input)) {
this._classMapInput = input;
registered = true;
} }
break; break;
case BindingType.Style: case BindingType.Style:
(this._singleStyleInputs = this._singleStyleInputs || []).push(input); binding = this.registerStyleInput(input.name, input.value, input.unit, input.sourceSpan);
this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(name);
registerIntoMap(this._stylesIndex, name);
registered = true;
break; break;
case BindingType.Class: case BindingType.Class:
(this._singleClassInputs = this._singleClassInputs || []).push(input); binding = this.registerClassInput(input.name, input.value, input.sourceSpan);
registerIntoMap(this._classesIndex, name);
registered = true;
break; break;
} }
if (registered) { return binding ? true : false;
this._lastStylingInput = input; }
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;
} else {
this._useDefaultSanitizer = true;
this._styleMapInput = entry;
}
this._lastStylingInput = entry;
(this as any).hasBindingsOrInitialValues = true; (this as any).hasBindingsOrInitialValues = true;
this._applyFnRequired = true; this._applyFnRequired = true;
return entry;
} }
return registered;
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) { registerStyleAttr(value: string) {
@ -153,7 +187,7 @@ export class StylingBuilder {
return exprs.length ? o.literalArr(exprs) : null; return exprs.length ? o.literalArr(exprs) : null;
} }
buildCreateLevelInstruction(sourceSpan: ParseSourceSpan, constantPool: ConstantPool): buildCreateLevelInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
StylingInstruction|null { StylingInstruction|null {
if (this.hasBindingsOrInitialValues) { if (this.hasBindingsOrInitialValues) {
const initialClasses = this._buildInitExpr(this._classesIndex, this._initialClassValues); 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 // 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). // a constant because the inital style values do not change (since they're static).
params.push(constantPool.getConstLiteral(initialStyles, true)); 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 // no point in having an extra `null` value unless there are follow-up params
params.push(o.NULL_EXPR); params.push(o.NULL_EXPR);
} }
if (useSanitizer) { if (useSanitizer || this._directiveIndexExpr) {
params.push(o.importExpr(R3.defaultStyleSanitizer)); params.push(useSanitizer ? o.importExpr(R3.defaultStyleSanitizer) : o.NULL_EXPR);
if (this._directiveIndexExpr) {
params.push(this._directiveIndexExpr);
}
} }
return {sourceSpan, reference: R3.elementStyling, buildParams: () => params}; return {sourceSpan, reference: R3.elementStyling, buildParams: () => params};
@ -213,7 +250,7 @@ export class StylingBuilder {
sourceSpan: stylingInput.sourceSpan, sourceSpan: stylingInput.sourceSpan,
reference: R3.elementStylingMap, reference: R3.elementStylingMap,
buildParams: (convertFn: (value: any) => o.Expression) => { buildParams: (convertFn: (value: any) => o.Expression) => {
const params: o.Expression[] = [this._indexLiteral]; const params: o.Expression[] = [this._elementIndexExpr];
if (mapBasedClassValue) { if (mapBasedClassValue) {
params.push(convertFn(mapBasedClassValue)); params.push(convertFn(mapBasedClassValue));
@ -223,6 +260,12 @@ export class StylingBuilder {
if (mapBasedStyleValue) { if (mapBasedStyleValue) {
params.push(convertFn(mapBasedStyleValue)); params.push(convertFn(mapBasedStyleValue));
} else if (this._directiveIndexExpr) {
params.push(o.NULL_EXPR);
}
if (this._directiveIndexExpr) {
params.push(this._directiveIndexExpr);
} }
return params; return params;
@ -233,8 +276,8 @@ export class StylingBuilder {
} }
private _buildSingleInputs( private _buildSingleInputs(
reference: o.ExternalReference, inputs: t.BoundAttribute[], mapIndex: Map<string, number>, reference: o.ExternalReference, inputs: BoundStylingEntry[], mapIndex: Map<string, number>,
valueConverter: ValueConverter): StylingInstruction[] { allowUnits: boolean, valueConverter: ValueConverter): StylingInstruction[] {
return inputs.map(input => { return inputs.map(input => {
const bindingIndex: number = mapIndex.get(input.name) !; const bindingIndex: number = mapIndex.get(input.name) !;
const value = input.value.visit(valueConverter); const value = input.value.visit(valueConverter);
@ -242,9 +285,17 @@ export class StylingBuilder {
sourceSpan: input.sourceSpan, sourceSpan: input.sourceSpan,
reference, reference,
buildParams: (convertFn: (value: any) => o.Expression) => { buildParams: (convertFn: (value: any) => o.Expression) => {
const params = [this._indexLiteral, o.literal(bindingIndex), convertFn(value)]; const params = [this._elementIndexExpr, o.literal(bindingIndex), convertFn(value)];
if (input.unit != null) { if (allowUnits) {
if (input.unit) {
params.push(o.literal(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; return params;
} }
@ -255,7 +306,7 @@ export class StylingBuilder {
private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] { private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] {
if (this._singleClassInputs) { if (this._singleClassInputs) {
return this._buildSingleInputs( return this._buildSingleInputs(
R3.elementClassProp, this._singleClassInputs, this._classesIndex, valueConverter); R3.elementClassProp, this._singleClassInputs, this._classesIndex, false, valueConverter);
} }
return []; return [];
} }
@ -263,7 +314,7 @@ export class StylingBuilder {
private _buildStyleInputs(valueConverter: ValueConverter): StylingInstruction[] { private _buildStyleInputs(valueConverter: ValueConverter): StylingInstruction[] {
if (this._singleStyleInputs) { if (this._singleStyleInputs) {
return this._buildSingleInputs( return this._buildSingleInputs(
R3.elementStyleProp, this._singleStyleInputs, this._stylesIndex, valueConverter); R3.elementStyleProp, this._singleStyleInputs, this._stylesIndex, true, valueConverter);
} }
return []; return [];
} }
@ -272,7 +323,13 @@ export class StylingBuilder {
return { return {
sourceSpan: this._lastStylingInput ? this._lastStylingInput.sourceSpan : null, sourceSpan: this._lastStylingInput ? this._lastStylingInput.sourceSpan : null,
reference: R3.elementStylingApply, 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 { function isClassBinding(name: string): boolean {
return input.name == 'className' || input.name == 'class'; return name == 'className' || name == 'class';
} }
function registerIntoMap(map: Map<string, number>, key: string) { function registerIntoMap(map: Map<string, number>, key: string) {

View File

@ -431,7 +431,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
visitElement(element: t.Element) { visitElement(element: t.Element) {
const elementIndex = this.allocateDataSlot(); const elementIndex = this.allocateDataSlot();
const stylingBuilder = new StylingBuilder(elementIndex); const stylingBuilder = new StylingBuilder(o.literal(elementIndex), null);
let isNonBindableMode: boolean = false; let isNonBindableMode: boolean = false;
const isI18nRootElement: boolean = isI18nRootNode(element.i18n); const isI18nRootElement: boolean = isI18nRootNode(element.i18n);
@ -476,7 +476,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const allOtherInputs: t.BoundAttribute[] = []; const allOtherInputs: t.BoundAttribute[] = [];
element.inputs.forEach((input: t.BoundAttribute) => { element.inputs.forEach((input: t.BoundAttribute) => {
if (!stylingBuilder.registerInput(input)) { if (!stylingBuilder.registerBoundInput(input)) {
if (input.type == BindingType.Property) { if (input.type == BindingType.Property) {
if (input.i18n) { if (input.i18n) {
i18nAttrs.push(input); i18nAttrs.push(input);

View File

@ -1072,9 +1072,11 @@ function generatePropertyAliases(
* @param className Name of class to toggle. Because it is going to DOM, this is not subject to * @param className Name of class to toggle. Because it is going to DOM, this is not subject to
* renaming as part of minification. * renaming as part of minification.
* @param value A value indicating if a given class should be added or removed. * @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( export function elementClassProp(
index: number, stylingIndex: number, value: boolean | PlayerFactory): void { index: number, stylingIndex: number, value: boolean | PlayerFactory,
directiveIndex?: number): void {
const val = const val =
(value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory<boolean>) : (!!value); (value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory<boolean>) : (!!value);
updateElementClassProp(getStylingContext(index, getViewData()), stylingIndex, val); 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). * 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) * @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). * 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( export function elementStyling(
classDeclarations?: (string | boolean | InitialStylingFlags)[] | null, classDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
styleDeclarations?: (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 tNode = getPreviousOrParentTNode();
const inputData = initializeTNodeInputs(tNode); 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 * (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 * specifically for element styling--the index must be the next index after the element
* index.) * 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 viewData = getViewData();
const isFirstRender = (viewData[FLAGS] & LViewFlags.CreationMode) !== 0; const isFirstRender = (viewData[FLAGS] & LViewFlags.CreationMode) !== 0;
const totalPlayersQueued = renderStyleAndClassBindings( 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`. * @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 * Note that when a suffix is provided then the underlying sanitizer will
* be ignored. * be ignored.
* @param directiveIndex the index for the directive that is attempting to change styling.
*/ */
export function elementStyleProp( export function elementStyleProp(
index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, 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; let valueToAdd: string|null = null;
if (value) { if (value) {
if (suffix) { 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. * @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 * Any missing styles (that have already been applied to the element beforehand) will be
* removed (unset) from the element's styling. * removed (unset) from the element's styling.
* @param directiveIndex the index for the directive that is attempting to change styling.
*/ */
export function elementStylingMap<T>( export function elementStylingMap<T>(
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, 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 viewData = getViewData();
const tNode = getTNode(index, viewData); const tNode = getTNode(index, viewData);
const stylingContext = getStylingContext(index, viewData); const stylingContext = getStylingContext(index, viewData);