Skip to content

Commit

Permalink
feat(ivy): properly apply style="", [style], [style.foo] and [attr.st…
Browse files Browse the repository at this point in the history
…yle] bindings
  • Loading branch information
matsko committed Jun 21, 2018
1 parent cb31381 commit e544ee6
Show file tree
Hide file tree
Showing 16 changed files with 669 additions and 103 deletions.
8 changes: 6 additions & 2 deletions packages/compiler/src/render3/r3_identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@ export class Identifiers {

static elementClassNamed: o.ExternalReference = {name: 'ɵkn', moduleName: CORE};

static elementStyle: o.ExternalReference = {name: 'ɵs', moduleName: CORE};
static elementInitStyling: o.ExternalReference = {name: 'ɵis', moduleName: CORE};

static elementStyleNamed: o.ExternalReference = {name: 'ɵsn', moduleName: CORE};
static elementStyleMulti: o.ExternalReference = {name: 'ɵsm', moduleName: CORE};

static elementStyleSingle: o.ExternalReference = {name: 'ɵsi', moduleName: CORE};

static elementApplyStyling: o.ExternalReference = {name: 'ɵas', moduleName: CORE};

static containerCreate: o.ExternalReference = {name: 'ɵC', moduleName: CORE};

Expand Down
91 changes: 91 additions & 0 deletions packages/compiler/src/render3/view/styling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

const OPEN_PAREN = 40;
const CLOSE_PAREN = 41;
const SINGLE_QUOTE = 39;
const DOUBLE_QUOTE = 34;
const COLON = 58;
const SEMICOLON = 59;

export function parseStyleStringIntoObj(value: string): {[key: string]: any} {
const styles: {[key: string]: any} = {};

let i = 0;
let parenCount = 0;
let currentQuote = 0;
let valueStart = 0;
let propStart = 0;
let currentProp: string|null = null;
let valueHasQuotes = false;
while (i < value.length) {
const token = value.charCodeAt(i++);
switch (token) {
case OPEN_PAREN:
parenCount++;
break;
case CLOSE_PAREN:
parenCount--;
break;
case SINGLE_QUOTE:
// valueStart needs to be there since prop values don't
// have quotes in CSS
valueHasQuotes = valueHasQuotes || valueStart > 0;
currentQuote = currentQuote == SINGLE_QUOTE ? 0 : SINGLE_QUOTE;
break;
case DOUBLE_QUOTE:
// same logic as above
valueHasQuotes = valueHasQuotes || valueStart > 0;
currentQuote = currentQuote == DOUBLE_QUOTE ? 0 : DOUBLE_QUOTE;
break;
case COLON:
if (!currentProp && !parenCount && !currentQuote) {
currentProp = deCamelCase(value.substring(propStart, i - 1).trim());
valueStart = i;
}
break;
case SEMICOLON:
if (currentProp && valueStart && !parenCount && !currentQuote) {
const styleVal = value.substring(valueStart, i - 1).trim();
styles[currentProp] = valueHasQuotes ? chompStartAndEndQuotes(styleVal) : styleVal;
propStart = i;
valueStart = 0;
currentProp = null;
valueHasQuotes = false;
}
break;
}
}

if (currentProp && valueStart) {
const styleVal = value.substr(valueStart).trim();
styles[currentProp] = valueHasQuotes ? chompStartAndEndQuotes(styleVal) : styleVal;
}

return styles;
}

function chompStartAndEndQuotes(value: string): string {
const qS = value.charCodeAt(0);
const qE = value.charCodeAt(value.length - 1);
if (qS == qE && (qS == SINGLE_QUOTE || qS == DOUBLE_QUOTE)) {
const tempValue = value.substring(1, value.length - 1);
// special case to avoid using a multi-quoted string that was just chomped
// (e.g. `font-family: "Verdana", "sans-serif"`)
if (tempValue.indexOf('\'') == -1 && tempValue.indexOf('"') == -1) {
value = tempValue;
}
}
return value;
}

function deCamelCase(value: string): string {
return value.replace(/[a-z][A-Z]/, v => {
return v.charAt(0) + '-' + v.charAt(1);
}).toLowerCase();
}
89 changes: 74 additions & 15 deletions packages/compiler/src/render3/view/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,20 @@ import {Identifiers as R3} from '../r3_identifiers';
import {htmlAstToRender3Ast} from '../r3_template_transform';

import {R3QueryMetadata} from './api';
import {parseStyleStringIntoObj} from './styling';
import {CONTEXT_NAME, I18N_ATTR, I18N_ATTR_PREFIX, ID_SEPARATOR, IMPLICIT_REFERENCE, MEANING_SEPARATOR, REFERENCE_PREFIX, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, getQueryPredicate, invalid, mapToExpression, noop, temporaryAllocator, trimTrailingNulls, unsupported} from './util';

const BINDING_INSTRUCTION_MAP: {[type: number]: o.ExternalReference} = {
[BindingType.Property]: R3.elementProperty,
[BindingType.Attribute]: R3.elementAttribute,
[BindingType.Class]: R3.elementClassNamed,
[BindingType.Style]: R3.elementStyleNamed,
[BindingType.Class]: R3.elementClassNamed
};

// `className` is used below instead of `class` because the interception
// code (where this map is used) deals with DOM element property values
// (like elm.propName) and not component bindining properties (like [propName]).
const SPECIAL_CASED_PROPERTIES_INSTRUCTION_MAP: {[index: string]: o.ExternalReference} = {
'className': R3.elementClass,
'style': R3.elementStyle
'className': R3.elementClass
};

export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver {
Expand Down Expand Up @@ -308,16 +307,24 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Add the attributes
const i18nMessages: o.Statement[] = [];
const attributes: o.Expression[] = [];
const initialStyles: {key: string, quoted: boolean, value: o.Expression}[] = [];

Object.getOwnPropertyNames(outputAttrs).forEach(name => {
const value = outputAttrs[name];
attributes.push(o.literal(name));
if (attrI18nMetas.hasOwnProperty(name)) {
const meta = parseI18nMeta(attrI18nMetas[name]);
const variable = this.constantPool.getTranslation(value, meta);
attributes.push(variable);
if (name == 'style') {
const styles = parseStyleStringIntoObj(value);
Object.keys(styles).forEach(key => {
initialStyles.push({key, value: o.literal(styles[key]), quoted: key.indexOf('-') >= 0});
});
} else {
attributes.push(o.literal(value));
attributes.push(o.literal(name));
if (attrI18nMetas.hasOwnProperty(name)) {
const meta = parseI18nMeta(attrI18nMetas[name]);
const variable = this.constantPool.getTranslation(value, meta);
attributes.push(variable);
} else {
attributes.push(o.literal(value));
}
}
});

Expand Down Expand Up @@ -361,7 +368,28 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver

const implicit = o.variable(CONTEXT_NAME);

if (isEmptyElement) {
const styleInputs: t.BoundAttribute[] = [];
const allOtherInputs: t.BoundAttribute[] = [];
element.inputs.forEach((input: t.BoundAttribute) => {
// [attr.style] should not be treated as a styling-based
// binding since it is intended to write directly to the attr
// and therefore will skip all style resolution that is present
// with style="", [style]="" and [style.prop]="" asignments
if (input.name == 'style' && input.type == BindingType.Property) {
// this should always go first in the compilation (for [style])
styleInputs.splice(0, 0, input);
} else if (input.type == BindingType.Style) {
styleInputs.push(input);
} else {
allOtherInputs.push(input);
}
});

const elementStyleIndex =
(initialStyles.length || styleInputs.length) ? this.allocateDataSlot() : 0;
const createSelfClosingInstruction = initialStyles.length == 0 && isEmptyElement;

if (createSelfClosingInstruction) {
this.instruction(
this._creationCode, element.sourceSpan, R3.element, ...trimTrailingNulls(parameters));
} else {
Expand All @@ -373,6 +401,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this._creationCode, element.sourceSpan, R3.elementStart,
...trimTrailingNulls(parameters));

// initial styling for static style="..." attributes
if (elementStyleIndex) {
this._creationCode.push(
o.importExpr(R3.elementInitStyling)
.callFn([o.literal(elementStyleIndex), o.literalMap(initialStyles)])
.toStmt());
}

// Generate Listeners (outputs)
element.outputs.forEach((outputAst: t.BoundEvent) => {
const elName = sanitizeIdentifier(element.name);
Expand All @@ -396,11 +432,32 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
});
}

if (styleInputs.length && elementStyleIndex) {
const indexLiteral = o.literal(elementStyleIndex);
styleInputs.forEach((input, i) => {
const isMulti = i == 0 && input.name == 'style';
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true);
if (isMulti) {
this.instruction(
this._bindingCode, input.sourceSpan, R3.elementStyleMulti, indexLiteral,
convertedBinding);
} else {
this.instruction(
this._bindingCode, input.sourceSpan, R3.elementStyleSingle, indexLiteral,
o.literal(input.name), convertedBinding);
}
});

const spanEnd = styleInputs[styleInputs.length - 1].sourceSpan;
this.instruction(this._bindingCode, spanEnd, R3.elementApplyStyling, indexLiteral);
}

// Generate element input bindings
element.inputs.forEach((input: t.BoundAttribute) => {
allOtherInputs.forEach((input: t.BoundAttribute) => {
if (input.type === BindingType.Animation) {
this._unsupported('animations');
}

const convertedBinding = this.convertPropertyBinding(implicit, input.value);
const specialInstruction = SPECIAL_CASED_PROPERTIES_INSTRUCTION_MAP[input.name];
if (specialInstruction) {
Expand Down Expand Up @@ -434,7 +491,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
t.visitAll(this, element.children);
}

if (!isEmptyElement) {
if (!createSelfClosingInstruction) {
// Finish element construction mode.
this.instruction(
this._creationCode, element.endSourceSpan || element.sourceSpan, R3.elementEnd);
Expand Down Expand Up @@ -560,7 +617,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
statements.push(o.importExpr(reference, null, span).callFn(params, span).toStmt());
}

private convertPropertyBinding(implicit: o.Expression, value: AST): o.Expression {
private convertPropertyBinding(implicit: o.Expression, value: AST, skipBindFn?: boolean):
o.Expression {
const pipesConvertedValue = value.visit(this._valueConverter);
if (pipesConvertedValue instanceof Interpolation) {
const convertedPropertyBinding = convertPropertyBinding(
Expand All @@ -573,7 +631,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this, implicit, pipesConvertedValue, this.bindingContext(), BindingForm.TrySimple,
() => error('Unexpected interpolation'));
this._bindingCode.push(...convertedPropertyBinding.stmts);
return o.importExpr(R3.bind).callFn([convertedPropertyBinding.currValExpr]);
const valExpr = convertedPropertyBinding.currValExpr;
return skipBindFn ? valExpr : o.importExpr(R3.bind).callFn([valExpr]);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,9 @@ describe('compiler compliance', () => {
$r3$.ɵEe(0, 'div');
}
if (rf & 2) {
$r3$.ɵsi(1, 'background-color', ctx.color);
$r3$.ɵas(1);
$r3$.ɵkn(0, 'error', $r3$.ɵb(ctx.error));
$r3$.ɵsn(0, 'background-color', $r3$.ɵb(ctx.color));
}
}
`;
Expand Down
53 changes: 51 additions & 2 deletions packages/compiler/test/render3/r3_view_compiler_styling_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('compiler compliance: styling', () => {
compileCommon: true,
});

describe('[style]', () => {
describe('[style] and [style.prop]', () => {
it('should create style instructions on the element', () => {
const files = {
app: {
Expand All @@ -43,14 +43,63 @@ describe('compiler compliance: styling', () => {
$r3$.ɵEe(0, 'div');
}
if (rf & 2) {
$r3$.ɵs(0,$r3$.ɵb($ctx$.myStyleExp));
$r3$.ɵsm(1, $ctx$.myStyleExp);
$r3$.ɵas(1);
}
}
`;

const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});

it('should place initial, multi, singular and application followed by attribute styling instructions in the template code in that order',
() => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div style="opacity:1"
[attr.style]="'border-width: 10px'"
[style.width]="myWidth"
[style]="myStyleExp"
[style.height]="myHeight"></div>\`
})
export class MyComponent {
myStyleExp = [{color:'red'}, {color:'blue', duration:1000}]
myWidth = '100px';
myHeight = '100px';
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};

const template = `
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵis(1, { opacity: '1' });
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsm(1, $ctx$.myStyleExp);
$r3$.ɵsi(1, 'width', $ctx$.myWidth);
$r3$.ɵsi(1, 'height', $ctx$.myHeight);
$r3$.ɵas(1);
$r3$.ɵa(0, 'style', $r3$.ɵb('border-width: 10px'));
}
}
`;

const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});

describe('[class]', () => {
Expand Down

0 comments on commit e544ee6

Please sign in to comment.