feat(ivy): properly apply style="", [style], [style.foo] and [attr.style] bindings (#24602)

PR Close #24602
This commit is contained in:
Matias Niemelä 2018-06-19 12:45:00 -07:00 committed by Miško Hevery
parent 52d43a99ef
commit 3980640d53
22 changed files with 1904 additions and 143 deletions

View File

@ -6,11 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ɵC as C, ɵE as E, ɵRenderFlags as RenderFlags, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as detectChanges, ɵe as e, ɵsn as sn, ɵt as t, ɵv as v} from '@angular/core';
import {ɵC as C, ɵE as E, ɵRenderFlags as RenderFlags, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as detectChanges, ɵe as e, ɵi1 as i1, ɵp as p, ɵs as s, ɵsa as sa, ɵsm as sm, ɵsp as sp, ɵt as t, ɵv as v} from '@angular/core';
import {ComponentDefInternal} from '@angular/core/src/render3/interfaces/definition';
import {TableCell, buildTable, emptyTable} from '../util';
const c0 = ['background-color'];
export class LargeTableComponent {
data: TableCell[][] = emptyTable;
@ -47,12 +48,13 @@ export class LargeTableComponent {
{
if (rf2 & RenderFlags.Create) {
E(0, 'td');
{ T(1); }
s(1, c0);
{ T(2); }
e();
}
if (rf2 & RenderFlags.Update) {
sn(0, 'background-color', b(cell.row % 2 ? '' : 'grey'));
t(1, b(cell.value));
sp(1, 0, cell.row % 2 ? '' : 'grey');
t(2, b(cell.value));
}
}
v();

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ɵC as C, ɵE as E, ɵRenderFlags as RenderFlags, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as _detectChanges, ɵe as e, ɵi1 as i1, ɵp as p, ɵsn as sn, ɵt as t, ɵv as v} from '@angular/core';
import {ɵC as C, ɵE as E, ɵRenderFlags as RenderFlags, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as _detectChanges, ɵe as e, ɵi1 as i1, ɵp as p, ɵs as s, ɵsa as sa, ɵsm as sm, ɵsp as sp, ɵt as t, ɵv as v} from '@angular/core';
import {TreeNode, buildTree, emptyTree} from '../util';
@ -30,6 +30,7 @@ export function detectChanges(component: TreeComponent) {
numberOfChecksEl.textContent = `${detectChangesRuns}`;
}
const c0 = ['background-color'];
export class TreeComponent {
data: TreeNode = emptyTree;
@ -40,15 +41,16 @@ export class TreeComponent {
template: function(rf: RenderFlags, ctx: TreeComponent) {
if (rf & RenderFlags.Create) {
E(0, 'span');
{ T(1); }
s(1, c0);
{ T(2); }
e();
C(2);
C(3);
C(4);
}
if (rf & RenderFlags.Update) {
sn(0, 'background-color', b(ctx.data.depth % 2 ? '' : 'grey'));
t(1, i1(' ', ctx.data.value, ' '));
cR(2);
sp(1, 0, ctx.data.depth % 2 ? '' : 'grey');
t(2, i1(' ', ctx.data.value, ' '));
cR(3);
{
if (ctx.data.left != null) {
let rf0 = V(0);
@ -65,7 +67,7 @@ export class TreeComponent {
}
}
cr();
cR(3);
cR(4);
{
if (ctx.data.right != null) {
let rf0 = V(0);
@ -106,22 +108,24 @@ export class TreeFunction {
});
}
const c1 = ['background-color'];
export function TreeTpl(rf: RenderFlags, ctx: TreeNode) {
if (rf & RenderFlags.Create) {
E(0, 'tree');
{
E(1, 'span');
{ T(2); }
s(2, c1);
{ T(3); }
e();
C(3);
C(4);
C(5);
}
e();
}
if (rf & RenderFlags.Update) {
sn(1, 'background-color', b(ctx.depth % 2 ? '' : 'grey'));
t(2, i1(' ', ctx.value, ' '));
cR(3);
sp(2, 0, ctx.depth % 2 ? '' : 'grey');
t(3, i1(' ', ctx.value, ' '));
cR(4);
{
if (ctx.left != null) {
let rf0 = V(0);
@ -130,7 +134,7 @@ export function TreeTpl(rf: RenderFlags, ctx: TreeNode) {
}
}
cr();
cR(4);
cR(5);
{
if (ctx.right != null) {
let rf0 = V(0);

View File

@ -379,3 +379,9 @@ export const enum RenderFlags {
/* Whether to run the update block (e.g. refresh bindings) */
Update = 0b10
}
// Note this will expand once `class` is introduced to styling
export const enum InitialStylingFlags {
/** Mode for matching initial style values */
INITIAL_STYLES = 0b00,
}

View File

@ -37,9 +37,13 @@ export class Identifiers {
static elementClassNamed: o.ExternalReference = {name: 'ɵkn', moduleName: CORE};
static elementStyle: o.ExternalReference = {name: 'ɵs', moduleName: CORE};
static elementStyling: o.ExternalReference = {name: 'ɵs', moduleName: CORE};
static elementStyleNamed: o.ExternalReference = {name: 'ɵsn', moduleName: CORE};
static elementStyle: o.ExternalReference = {name: 'ɵsm', moduleName: CORE};
static elementStyleProp: o.ExternalReference = {name: 'ɵsp', moduleName: CORE};
static elementStylingApply: o.ExternalReference = {name: 'ɵsa', moduleName: CORE};
static containerCreate: o.ExternalReference = {name: 'ɵC', moduleName: CORE};

View File

@ -0,0 +1,111 @@
/**
* @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 enum Char {
OpenParen = 40,
CloseParen = 41,
Colon = 58,
Semicolon = 59,
BackSlash = 92,
QuoteNone = 0, // indicating we are not inside a quote
QuoteDouble = 34,
QuoteSingle = 39,
}
/**
* Parses string representation of a style and converts it into object literal.
*
* @param value string representation of style as used in the `style` attribute in HTML.
* Example: `color: red; height: auto`.
* @returns an object literal. `{ color: 'red', height: 'auto'}`.
*/
export function parseStyle(value: string): {[key: string]: any} {
const styles: {[key: string]: any} = {};
let i = 0;
let parenDepth = 0;
let quote: Char = Char.QuoteNone;
let valueStart = 0;
let propStart = 0;
let currentProp: string|null = null;
let valueHasQuotes = false;
while (i < value.length) {
const token = value.charCodeAt(i++) as Char;
switch (token) {
case Char.OpenParen:
parenDepth++;
break;
case Char.CloseParen:
parenDepth--;
break;
case Char.QuoteSingle:
// valueStart needs to be there since prop values don't
// have quotes in CSS
valueHasQuotes = valueHasQuotes || valueStart > 0;
if (quote === Char.QuoteNone) {
quote = Char.QuoteSingle;
} else if (quote === Char.QuoteSingle && value.charCodeAt(i - 1) !== Char.BackSlash) {
quote = Char.QuoteNone;
}
break;
case Char.QuoteDouble:
// same logic as above
valueHasQuotes = valueHasQuotes || valueStart > 0;
if (quote === Char.QuoteNone) {
quote = Char.QuoteDouble;
} else if (quote === Char.QuoteDouble && value.charCodeAt(i - 1) !== Char.BackSlash) {
quote = Char.QuoteNone;
}
break;
case Char.Colon:
if (!currentProp && parenDepth === 0 && quote === Char.QuoteNone) {
currentProp = hyphenate(value.substring(propStart, i - 1).trim());
valueStart = i;
}
break;
case Char.Semicolon:
if (currentProp && valueStart > 0 && parenDepth === 0 && quote === Char.QuoteNone) {
const styleVal = value.substring(valueStart, i - 1).trim();
styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal;
propStart = i;
valueStart = 0;
currentProp = null;
valueHasQuotes = false;
}
break;
}
}
if (currentProp && valueStart) {
const styleVal = value.substr(valueStart).trim();
styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal;
}
return styles;
}
export function stripUnnecessaryQuotes(value: string): string {
const qS = value.charCodeAt(0);
const qE = value.charCodeAt(value.length - 1);
if (qS == qE && (qS == Char.QuoteSingle || qS == Char.QuoteDouble)) {
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;
}
export function hyphenate(value: string): string {
return value.replace(/[a-z][A-Z]/g, v => {
return v.charAt(0) + '-' + v.charAt(1);
}).toLowerCase();
}

View File

@ -30,6 +30,7 @@ import {Identifiers as R3} from '../r3_identifiers';
import {htmlAstToRender3Ast} from '../r3_template_transform';
import {R3QueryMetadata} from './api';
import {parseStyle} 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';
function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined {
@ -40,8 +41,6 @@ function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefin
return R3.elementAttribute;
case BindingType.Class:
return R3.elementClassNamed;
case BindingType.Style:
return R3.elementStyleNamed;
default:
return undefined;
}
@ -51,8 +50,7 @@ function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefin
// 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 {
@ -316,9 +314,35 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Add the attributes
const i18nMessages: o.Statement[] = [];
const attributes: o.Expression[] = [];
const initialStyleDeclarations: o.Expression[] = [];
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]="" assignments
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);
}
});
let currStyleIndex = 0;
let staticStylesMap: {[key: string]: any}|null = null;
const stylesIndexMap: {[key: string]: number} = {};
Object.getOwnPropertyNames(outputAttrs).forEach(name => {
const value = outputAttrs[name];
if (name == 'style') {
staticStylesMap = parseStyle(value);
Object.keys(staticStylesMap).forEach(prop => { stylesIndexMap[prop] = currStyleIndex++; });
} else {
attributes.push(o.literal(name));
if (attrI18nMetas.hasOwnProperty(name)) {
const meta = parseI18nMeta(attrI18nMetas[name]);
@ -327,8 +351,33 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} else {
attributes.push(o.literal(value));
}
}
});
for (let i = 0; i < styleInputs.length; i++) {
const input = styleInputs[i];
const isMapBasedStyleBinding = i === 0 && input.name === 'style';
if (!isMapBasedStyleBinding && !stylesIndexMap.hasOwnProperty(input.name)) {
stylesIndexMap[input.name] = currStyleIndex++;
}
}
// this will build the instructions so that they fall into the following syntax
// => [prop1, prop2, prop3, 0, prop1, value1, prop2, value2]
Object.keys(stylesIndexMap).forEach(prop => {
initialStyleDeclarations.push(o.literal(prop));
});
if (staticStylesMap) {
initialStyleDeclarations.push(o.literal(core.InitialStylingFlags.INITIAL_STYLES));
Object.keys(staticStylesMap).forEach(prop => {
initialStyleDeclarations.push(o.literal(prop));
const value = staticStylesMap ![prop];
initialStyleDeclarations.push(o.literal(value));
});
}
const attrArg: o.Expression = attributes.length > 0 ?
this.constantPool.getConstLiteral(o.literalArr(attributes), true) :
o.TYPED_NULL_EXPR;
@ -365,11 +414,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this.addNamespaceInstruction(currentNamespace, element);
}
const isEmptyElement = element.children.length === 0 && element.outputs.length === 0;
const implicit = o.variable(CONTEXT_NAME);
if (isEmptyElement) {
const elementStyleIndex =
(initialStyleDeclarations.length || styleInputs.length) ? this.allocateDataSlot() : 0;
const createSelfClosingInstruction =
elementStyleIndex === 0 && element.children.length === 0 && element.outputs.length === 0;
if (createSelfClosingInstruction) {
this.instruction(
this._creationCode, element.sourceSpan, R3.element, ...trimTrailingNulls(parameters));
} else {
@ -381,6 +433,20 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this._creationCode, element.sourceSpan, R3.elementStart,
...trimTrailingNulls(parameters));
// initial styling for static style="..." attributes
if (elementStyleIndex > 0) {
let paramsList: (o.Expression)[] = [o.literal(elementStyleIndex)];
if (initialStyleDeclarations.length) {
// the template compiler handles initial styling (e.g. style="foo") values
// in a special command called `elementStyle` so that the initial styles
// 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).
paramsList.push(
this.constantPool.getConstLiteral(o.literalArr(initialStyleDeclarations), true));
}
this._creationCode.push(o.importExpr(R3.elementStyling).callFn(paramsList).toStmt());
}
// Generate Listeners (outputs)
element.outputs.forEach((outputAst: t.BoundEvent) => {
const elName = sanitizeIdentifier(element.name);
@ -404,11 +470,33 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
});
}
if (styleInputs.length && elementStyleIndex > 0) {
const indexLiteral = o.literal(elementStyleIndex);
styleInputs.forEach((input, i) => {
const isMapBasedStyleBinding = i == 0 && input.name == 'style';
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true);
if (isMapBasedStyleBinding) {
this.instruction(
this._bindingCode, input.sourceSpan, R3.elementStyle, indexLiteral, convertedBinding);
} else {
const key = input.name;
let styleIndex: number = stylesIndexMap[key] !;
this.instruction(
this._bindingCode, input.sourceSpan, R3.elementStyleProp, indexLiteral,
o.literal(styleIndex), convertedBinding);
}
});
const spanEnd = styleInputs[styleInputs.length - 1].sourceSpan;
this.instruction(this._bindingCode, spanEnd, R3.elementStylingApply, 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) {
@ -442,7 +530,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);
@ -568,7 +656,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(
@ -581,7 +670,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]);
}
}
}

View File

@ -321,13 +321,21 @@ describe('compiler compliance', () => {
const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }';
const template = `
template: function MyComponent_Template(rf: IDENT, ctx: IDENT) {
const _c0 = ['background-color'];
class MyComponent {
static ngComponentDef = i0.ɵdefineComponent({type:MyComponent,selectors:[['my-component']],
factory:function MyComponent_Factory(){
return new MyComponent();
},template:function MyComponent_Template(rf:number,ctx:any){
if (rf & 1) {
$r3$.ɵEe(0, 'div');
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, _c0);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsp(1, 0, ctx.color);
$r3$.ɵsa(1);
$r3$.ɵkn(0, 'error', $r3$.ɵb(ctx.error));
$r3$.ɵsn(0, 'background-color', $r3$.ɵb(ctx.color));
}
}
`;

View File

@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {InitialStylingFlags} from '../../src/core';
import {MockDirectory, setup} from '../aot/test_util';
import {compile, expectEmit} from './mock_compile';
describe('compiler compliance: styling', () => {
@ -16,7 +18,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: {
@ -40,10 +42,13 @@ describe('compiler compliance: styling', () => {
const template = `
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
if (rf & 1) {
$r3$.ɵEe(0, 'div');
$r3$.ɵE(0, 'div');
$r3$.ɵs(1);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵs(0,$r3$.ɵb($ctx$.myStyleExp));
$r3$.ɵsm(1, $ctx$.myStyleExp);
$r3$.ɵsa(1);
}
}
`;
@ -51,6 +56,63 @@ describe('compiler compliance: styling', () => {
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 = `
const _c0 = ['opacity','width','height',${InitialStylingFlags.INITIAL_STYLES},'opacity','1'];
class MyComponent {
static ngComponentDef = i0.ɵdefineComponent({
type: MyComponent,
selectors:[['my-component']],
factory:function MyComponent_Factory(){
return new MyComponent();
},
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, _c0);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsm(1, $ctx$.myStyleExp);
$r3$.ɵsp(1, 1, $ctx$.myWidth);
$r3$.ɵsp(1, 2, $ctx$.myHeight);
$r3$.ɵsa(1);
$r3$.ɵa(0, 'style', $r3$.ɵb('border-width: 10px'));
}
}
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});
describe('[class]', () => {

View File

@ -0,0 +1,93 @@
/**
* @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
*/
import {hyphenate, parseStyle, stripUnnecessaryQuotes} from '../../src/render3/view/styling';
describe('inline css style parsing', () => {
it('should parse empty or blank strings', () => {
const result1 = parseStyle('');
expect(result1).toEqual({});
const result2 = parseStyle(' ');
expect(result2).toEqual({});
});
it('should parse a string into a key/value map', () => {
const result = parseStyle('width:100px;height:200px;opacity:0');
expect(result).toEqual({width: '100px', height: '200px', opacity: '0'});
});
it('should trim values and properties', () => {
const result = parseStyle('width :333px ; height:666px ; opacity: 0.5;');
expect(result).toEqual({width: '333px', height: '666px', opacity: '0.5'});
});
it('should chomp out start/end quotes', () => {
const result = parseStyle(
'content: "foo"; opacity: \'0.5\'; font-family: "Verdana", Helvetica, "sans-serif"');
expect(result).toEqual(
{content: 'foo', opacity: '0.5', 'font-family': '"Verdana", Helvetica, "sans-serif"'});
});
it('should not mess up with quoted strings that contain [:;] values', () => {
const result = parseStyle('content: "foo; man: guy"; width: 100px');
expect(result).toEqual({content: 'foo; man: guy', width: '100px'});
});
it('should not mess up with quoted strings that contain inner quote values', () => {
const quoteStr = '"one \'two\' three \"four\" five"';
const result = parseStyle(`content: ${quoteStr}; width: 123px`);
expect(result).toEqual({content: quoteStr, width: '123px'});
});
it('should respect parenthesis that are placed within a style', () => {
const result = parseStyle('background-image: url("foo.jpg")');
expect(result).toEqual({'background-image': 'url("foo.jpg")'});
});
it('should respect multi-level parenthesis that contain special [:;] characters', () => {
const result = parseStyle('color: rgba(calc(50 * 4), var(--cool), :5;); height: 100px;');
expect(result).toEqual({color: 'rgba(calc(50 * 4), var(--cool), :5;)', height: '100px'});
});
it('should hyphenate style properties from camel case', () => {
const result = parseStyle('borderWidth: 200px');
expect(result).toEqual({
'border-width': '200px',
});
});
describe('quote chomping', () => {
it('should remove the start and end quotes', () => {
expect(stripUnnecessaryQuotes('\'foo bar\'')).toEqual('foo bar');
expect(stripUnnecessaryQuotes('"foo bar"')).toEqual('foo bar');
});
it('should not remove quotes if the quotes are not at the start and end', () => {
expect(stripUnnecessaryQuotes('foo bar')).toEqual('foo bar');
expect(stripUnnecessaryQuotes(' foo bar ')).toEqual(' foo bar ');
expect(stripUnnecessaryQuotes('\'foo\' bar')).toEqual('\'foo\' bar');
expect(stripUnnecessaryQuotes('foo "bar"')).toEqual('foo "bar"');
});
it('should not remove quotes if there are inner quotes', () => {
const str = '"Verdana", "Helvetica"';
expect(stripUnnecessaryQuotes(str)).toEqual(str);
});
});
describe('camelCasing => hyphenation', () => {
it('should convert a camel-cased value to a hyphenated value', () => {
expect(hyphenate('fooBar')).toEqual('foo-bar');
expect(hyphenate('fooBarMan')).toEqual('foo-bar-man');
expect(hyphenate('-fooBar-man')).toEqual('-foo-bar-man');
});
it('should make everything lowercase',
() => { expect(hyphenate('-WebkitAnimation')).toEqual('-webkit-animation'); });
});
});

View File

@ -75,7 +75,9 @@ export {
rS as ɵrS,
a as ɵa,
s as ɵs,
sn as ɵsn,
sm as ɵsm,
sp as ɵsp,
sa as ɵsa,
k as ɵk,
kn as ɵkn,
t as ɵt,

View File

@ -56,8 +56,11 @@ export {
elementEnd as e,
elementProperty as p,
elementStart as E,
elementStyle as s,
elementStyleNamed as sn,
elementStyling as s,
elementStyle as sm,
elementStyleProp as sp,
elementStylingApply as sa,
listener as L,
store as st,

View File

@ -27,6 +27,7 @@ import {ComponentDefInternal, ComponentTemplate, ComponentQuery, DirectiveDefInt
import {RComment, RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer';
import {isDifferent, stringify} from './util';
import {ViewRef} from './view_ref';
import {StylingContext, allocStylingContext, createStylingContextTemplate, updateStyleMap as updateElementStyleMap, updateStyleProp as updateElementStyleProp, renderStyles as renderElementStyles} from './styling';
/**
* Directive (D) sets a property on all component instances using this constant as a key and the
@ -652,7 +653,9 @@ export function elementStart(
const node: LElementNode =
createLNode(index, TNodeType.Element, native !, name, attrs || null, null);
if (attrs) setUpAttributes(native, attrs);
if (attrs) {
setUpAttributes(native, attrs);
}
appendChild(getParentLNode(node), native, viewData);
createDirectivesAndLocals(localRefs);
return native;
@ -1185,7 +1188,8 @@ export function createTNode(
child: null,
parent: parent,
dynamicContainerNode: null,
detached: null
detached: null,
stylingTemplate: null
};
}
@ -1287,9 +1291,94 @@ export function elementClass<T>(index: number, value: T | NO_CHANGE): void {
}
/**
* Update a given style on an Element.
* Assign any inline style values to the element during creation mode.
*
* @param index Index of the element to change in the data array
* This instruction is meant to be called during creation mode to apply all styling
* (e.g. `style="..."`) values to the element. This is also where the provided index
* value is allocated for the styling details for its corresponding element (the element
* index is the previous index value from this one).
*
* (Note this function calls `elementStylingApply` immediately when called.)
*
*
* @param index Index value which will be allocated to store styling data for the element.
* (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 styles A key/value map of CSS styles that will be registered on the element.
* Each individual style will be used on the element as long as it is not overridden
* by any styles placed on the element by multiple (`[style]`) or singular (`[style.prop]`)
* bindings. If a style binding changes its value to null then the initial styling
* values that are passed in here will be applied to the element (if matched).
*/
export function elementStyling<T>(index: number, styles?: (string | number)[] | null): void {
const tNode = load<LElementNode>(index - 1).tNode;
if (!tNode.stylingTemplate) {
// initialize the styling template.
tNode.stylingTemplate = createStylingContextTemplate(styles);
}
// Allocate space but leave null for lazy creation.
viewData[index + HEADER_OFFSET] = null;
if (styles && styles.length) {
elementStylingApply(index);
}
}
/**
* Retrieve the `StylingContext` at a given index.
*
* This method lazily creates the `StylingContext`. This is because in most cases
* we have styling without any bindings. Creating `StylingContext` eagerly would mean that
* every style declaration such as `<div style="color: 'red' ">` would result `StyleContext`
* which would create unnecessary memory pressure.
*
* @param index Index of the style allocation. See: `elementStyling`.
*/
function getStylingContext(index: number): StylingContext {
let stylingContext = load<StylingContext>(index);
if (!stylingContext) {
const lElement: LElementNode = load(index - 1);
const tNode = lElement.tNode;
ngDevMode &&
assertDefined(tNode.stylingTemplate, 'getStylingContext() called before elementStyling()');
stylingContext = viewData[index + HEADER_OFFSET] = allocStylingContext(tNode.stylingTemplate !);
}
return stylingContext;
}
/**
* Apply all styling values to the element which have been queued by any styling instructions.
*
* This instruction is meant to be run once one or more `elementStyle` and/or `elementStyleProp`
* have been issued against the element. This function will also determine if any styles have
* changed and will then skip the operation if there is nothing new to render.
*
* Once called then all queued styles will be flushed.
*
* @param index Index of the element's styling storage that will be rendered.
* (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.)
*/
export function elementStylingApply<T>(index: number): void {
renderElementStyles(load<LElementNode>(index - 1), getStylingContext(index), renderer);
}
/**
* Queue a given style to be rendered on an Element.
*
* If the style value is `null` then it will be removed from the element
* (or assigned a different value depending if there are any styles placed
* on the element with `elementStyle` or any styles that are present
* from when the element was created (with `elementStyling`).
*
* (Note that the styling instruction will not be applied until `elementStylingApply` is called.)
*
* @param index Index of the element's styling storage to change in the data array.
* (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 styleIndex Index of the style property on this element. (Monotonically increasing.)
* @param styleName Name of property. Because it is going to DOM this is not subject to
* renaming as part of minification.
* @param value New value to write (null to remove).
@ -1297,69 +1386,44 @@ export function elementClass<T>(index: number, value: T | NO_CHANGE): void {
* @param sanitizer An optional function used to transform the value typically used for
* sanitization.
*/
export function elementStyleNamed<T>(
index: number, styleName: string, value: T | NO_CHANGE, suffix?: string): void;
export function elementStyleNamed<T>(
index: number, styleName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): void;
export function elementStyleNamed<T>(
index: number, styleName: string, value: T | NO_CHANGE,
export function elementStyleProp<T>(
index: number, styleIndex: number, value: T | null, suffix?: string): void;
export function elementStyleProp<T>(
index: number, styleIndex: number, value: T | null, sanitizer?: SanitizerFn): void;
export function elementStyleProp<T>(
index: number, styleIndex: number, value: T | null,
suffixOrSanitizer?: string | SanitizerFn): void {
if (value !== NO_CHANGE) {
const lElement: LElementNode = load(index);
if (value == null) {
ngDevMode && ngDevMode.rendererRemoveStyle++;
isProceduralRenderer(renderer) ?
renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) :
lElement.native['style'].removeProperty(styleName);
} else {
let strValue =
let valueToAdd: string|null = null;
if (value) {
valueToAdd =
typeof suffixOrSanitizer == 'function' ? suffixOrSanitizer(value) : stringify(value);
if (typeof suffixOrSanitizer == 'string') strValue = strValue + suffixOrSanitizer;
ngDevMode && ngDevMode.rendererSetStyle++;
isProceduralRenderer(renderer) ?
renderer.setStyle(lElement.native, styleName, strValue, RendererStyleFlags3.DashCase) :
lElement.native['style'].setProperty(styleName, strValue);
if (typeof suffixOrSanitizer == 'string') {
valueToAdd = valueToAdd + suffixOrSanitizer;
}
}
updateElementStyleProp(getStylingContext(index), styleIndex, valueToAdd);
}
/**
* Set the `style` property on a DOM element.
* Queue a key/value map of styles to be rendered on an Element.
*
* This instruction is meant to handle the `[style]="exp"` usage.
* This instruction is meant to handle the `[style]="exp"` usage. When styles are applied to
* the Element they will then be placed with respect to any styles set with `elementStyleProp`.
* If any styles are set to `null` then they will be removed from the element (unless the same
* style properties have been assigned to the element during creation using `elementStyling`).
*
* (Note that the styling instruction will not be applied until `elementStylingApply` is called.)
*
* @param index The index of the element to update in the LViewData array
* @param index Index of the element's styling storage to change in the data array.
* (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 value A value indicating if a given style should be added or removed.
* The expected shape of `value` is an object where keys are style names and the values
* are their corresponding values to set. If value is falsy, then the style is removed. An absence
* of style does not cause that style to be removed. `NO_CHANGE` implies that no update should be
* performed.
* are their corresponding values to set. If value is null, then the style is removed.
*/
export function elementStyle<T>(
index: number, value: {[styleName: string]: any} | NO_CHANGE): void {
if (value !== NO_CHANGE) {
// TODO: This is a naive implementation which simply writes value to the `style`. In the future
// we will add logic here which would work with the animation code.
const lElement = load(index) as LElementNode;
if (isProceduralRenderer(renderer)) {
ngDevMode && ngDevMode.rendererSetStyle++;
renderer.setProperty(lElement.native, 'style', value);
} else {
const style = lElement.native['style'];
for (let i = 0, keys = Object.keys(value); i < keys.length; i++) {
const styleName: string = keys[i];
const styleValue: any = (value as any)[styleName];
if (styleValue == null) {
ngDevMode && ngDevMode.rendererRemoveStyle++;
style.removeProperty(styleName);
} else {
ngDevMode && ngDevMode.rendererSetStyle++;
style.setProperty(styleName, styleValue);
}
}
}
}
export function elementStyle<T>(index: number, value: {[styleName: string]: any} | null): void {
updateElementStyleMap(getStylingContext(index), value);
}
//////////////////////////

View File

@ -290,3 +290,9 @@ export type PipeTypeList =
// Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types.
export const unusedValueExportToPlacateAjd = 1;
// Note this will expand once `class` is introduced to styling
export const enum InitialStylingFlags {
/** Mode for matching initial style values */
INITIAL_STYLES = 0b00,
}

View File

@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {StylingContext} from '../styling';
import {LContainer} from './container';
import {LInjector} from './injector';
import {LProjection} from './projection';
@ -341,6 +343,8 @@ export interface TNode {
* If this node is not part of an i18n block, this field is null.
*/
detached: boolean|null;
stylingTemplate: StylingContext|null;
}
/** Static data for an LElementNode */

View File

@ -80,8 +80,9 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵqR': r3.qR,
'ɵrS': r3.rS,
'ɵs': r3.s,
'ɵsn': r3.sn,
'ɵst': r3.st,
'ɵsm': r3.sm,
'ɵsp': r3.sp,
'ɵsa': r3.sa,
'ɵT': r3.T,
'ɵt': r3.t,
'ɵV': r3.V,

View File

@ -0,0 +1,621 @@
/**
* @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
*/
import {InitialStylingFlags} from './interfaces/definition';
import {LElementNode} from './interfaces/node';
import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer';
/**
* The styling context acts as a styling manifest (shaped as an array) for determining which
* styling properties have been assigned via the provided `updateStyleMap` and `updateStyleProp`
* functions. There are also two initialization functions `allocStylingContext` and
* `createStylingContextTemplate` which are used to initialize and/or clone the context.
*
* The context is an array where the first two cells are used for static data (initial styling)
* and dirty flags / index offsets). The remaining set of cells is used for multi (map) and single
* (prop) style values.
*
* each value from here onwards is mapped as so:
* [i] = mutation/type flag for the style value
* [i + 1] = prop string (or null incase it has been removed)
* [i + 2] = value string (or null incase it has been removed)
*
* There are three types of styling types stored in this context:
* initial: any styles that are passed in once the context is created
* (these are stored in the first cell of the array and the first
* value of this array is always `null` even if no initial styles exist.
* the `null` value is there so that any new styles have a parent to point
* to. This way we can always assume that there is a parent.)
*
* single: any styles that are updated using `updateStyleProp` (fixed set)
*
* multi: any styles that are updated using `updateStyleMap` (dynamic set)
*
* Note that context is only used to collect style information. Only when `renderStyles`
* is called is when the styling payload will be rendered (or built as a key/value map).
*
* When the context is created, depending on what initial styles are passed in, the context itself
* will be pre-filled with slots based on the initial style properties. Say for example we have a
* series of initial styles that look like so:
*
* style="width:100px; height:200px;"
*
* Then the initial state of the context (once initialized) will look like so:
*
* ```
* context = [
* [null, '100px', '200px'], // property names are not needed since they have already been
* written to DOM.
*
* configMasterVal,
*
* // 2
* 'width',
* pointers(1, 8); // Point to static `width`: `100px` and multi `width`.
* null,
*
* // 5
* 'height',
* pointers(2, 11); // Point to static `height`: `200px` and multi `height`.
* null,
*
* // 8
* 'width',
* pointers(1, 2); // Point to static `width`: `100px` and single `width`.
* null,
*
* // 11
* 'height',
* pointers(2, 5); // Point to static `height`: `200px` and single `height`.
* null,
* ]
*
* function pointers(staticIndex: number, dynamicIndex: number) {
* // combine the two indices into a single word.
* return (staticIndex << StylingFlags.BitCountSize) |
* (dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize));
* }
* ```
*
* The values are duplicated so that space is set aside for both multi ([style])
* and single ([style.prop]) values. The respective config values (configValA, configValB, etc...)
* are a combination of the StylingFlags with two index values: the `initialIndex` (which points to
* the index location of the style value in the initial styles array in slot 0) and the
* `dynamicIndex` (which points to the matching single/multi index position in the context array
* for the same prop).
*
* This means that every time `updateStyleProp` is called it must be called using an index value
* (not a property string) which references the index value of the initial style when the context
* was created. This also means that `updateStyleProp` cannot be called with a new property
* (only `updateStyleMap` can include new CSS properties that will be added to the context).
*/
export interface StylingContext extends Array<InitialStyles|number|string|null> {
/**
* Location of initial data shared by all instances of this style.
*/
[0]: InitialStyles;
/**
* A numeric value representing the configuration status (whether the context is dirty or not)
* mixed together (using bit shifting) with a index value which tells the starting index value
* of where the multi style entries begin.
*/
[1]: number;
}
/**
* The initial styles is populated whether or not there are any initial styles passed into
* the context during allocation. The 0th value must be null so that index values of `0` within
* the context flags can always point to a null value safely when nothing is set.
*
* All other entries in this array are of `string` value and correspond to the values that
* were extracted from the `style=""` attribute in the HTML code for the provided template.
*/
export interface InitialStyles extends Array<string|null> { [0]: null; }
/**
* Used to set the context to be dirty or not both on the master flag (position 1)
* or for each single/multi property that exists in the context.
*/
export const enum StylingFlags {
// Implies no configurations
None = 0b0,
// Whether or not the entry or context itself is dirty
Dirty = 0b1,
// The max amount of bits used to represent these configuration values
BitCountSize = 1,
}
/** Used as numeric pointer values to determine what cells to update in the `StylingContext` */
export const enum StylingIndex {
// Position of where the initial styles are stored in the styling context
InitialStylesPosition = 0,
// Index of location where the start of single properties are stored. (`updateStyleProp`)
MasterFlagPosition = 1,
// Location of single (prop) value entries are stored within the context
SingleStylesStartPosition = 2,
// Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue
FlagsOffset = 0,
PropertyOffset = 1,
ValueOffset = 2,
// Size of each multi or single entry (flag + prop + value)
Size = 3,
// Each flag has a binary digit length of this value
BitCountSize = 15, // (32 - 1) / 2 = ~15
// The binary digit value as a mask
BitMask = 0b111111111111111 // 15 bits
}
/**
* Used clone a copy of a pre-computed template of a styling context.
*
* A pre-computed template is designed to be computed once for a given element
* (instructions.ts has logic for caching this).
*/
export function allocStylingContext(templateStyleContext: StylingContext): StylingContext {
// each instance gets a copy
return templateStyleContext.slice() as any as StylingContext;
}
/**
* Creates a styling context template where styling information is stored.
* Any styles that are later referenced using `updateStyleProp` must be
* passed in within this function. Initial values for those styles are to
* be declared after all initial style properties are declared (this change in
* mode between declarations and initial styles is made possible using a special
* enum value found in `definition.ts`).
*
* @param initialStyleDeclarations a list of style declarations and initial style values
* that are used later within the styling context.
*
* -> ['width', 'height', SPECIAL_ENUM_VAL, 'width', '100px']
* This implies that `width` and `height` will be later styled and that the `width`
* property has an initial value of `100px`.
*/
export function createStylingContextTemplate(
initialStyleDeclarations?: (string | InitialStylingFlags)[] | null): StylingContext {
const initialStyles: InitialStyles = [null];
const context: StylingContext = [initialStyles, 0];
const indexLookup: {[key: string]: number} = {};
if (initialStyleDeclarations) {
let hasPassedDeclarations = false;
for (let i = 0; i < initialStyleDeclarations.length; i++) {
const v = initialStyleDeclarations[i] as string | InitialStylingFlags;
// this flag value marks where the declarations end the initial values begin
if (v === InitialStylingFlags.INITIAL_STYLES) {
hasPassedDeclarations = true;
} else {
const prop = v as string;
if (hasPassedDeclarations) {
const value = initialStyleDeclarations[++i] as string;
initialStyles.push(value);
indexLookup[prop] = initialStyles.length - 1;
} else {
// it's safe to use `0` since the default initial value for
// each property will always be null (which is at position 0)
indexLookup[prop] = 0;
}
}
}
}
const allProps = Object.keys(indexLookup);
const totalProps = allProps.length;
// *2 because we are filling for both single and multi style spaces
const maxLength = totalProps * StylingIndex.Size * 2 + StylingIndex.SingleStylesStartPosition;
// we need to fill the array from the start so that we can access
// both the multi and the single array positions in the same loop block
for (let i = StylingIndex.SingleStylesStartPosition; i < maxLength; i++) {
context.push(null);
}
const singleStart = StylingIndex.SingleStylesStartPosition;
const multiStart = totalProps * StylingIndex.Size + StylingIndex.SingleStylesStartPosition;
// fill single and multi-level styles
for (let i = 0; i < allProps.length; i++) {
const prop = allProps[i];
const indexForInitial = indexLookup[prop];
const indexForMulti = i * StylingIndex.Size + multiStart;
const indexForSingle = i * StylingIndex.Size + singleStart;
setFlag(context, indexForSingle, pointers(StylingFlags.None, indexForInitial, indexForMulti));
setProp(context, indexForSingle, prop);
setValue(context, indexForSingle, null);
setFlag(context, indexForMulti, pointers(StylingFlags.Dirty, indexForInitial, indexForSingle));
setProp(context, indexForMulti, prop);
setValue(context, indexForMulti, null);
}
// there is no initial value flag for the master index since it doesn't reference an initial style
// value
setFlag(context, StylingIndex.MasterFlagPosition, pointers(0, 0, multiStart));
setContextDirty(context, initialStyles.length > 1);
return context;
}
const EMPTY_ARR: any[] = [];
/**
* Sets and resolves all `multi` styles on an `StylingContext` so that they can be
* applied to the element once `renderStyles` is called.
*
* All missing styles (any values that are not provided in the new `styles` param)
* will resolve to `null` within their respective positions in the context.
*
* @param context The styling context that will be updated with the
* newly provided style values.
* @param styles The key/value map of CSS styles that will be used for the update.
*/
export function updateStyleMap(context: StylingContext, styles: {[key: string]: any} | null): void {
const propsToApply = styles ? Object.keys(styles) : EMPTY_ARR;
const multiStartIndex = getMultiStartIndex(context);
let dirty = false;
let ctxIndex = multiStartIndex;
let propIndex = 0;
// the main loop here will try and figure out how the shape of the provided
// styles differ with respect to the context. Later if the context/styles are
// off-balance then they will be dealt in another loop after this one
while (ctxIndex < context.length && propIndex < propsToApply.length) {
const flag = getPointers(context, ctxIndex);
const prop = getProp(context, ctxIndex);
const value = getValue(context, ctxIndex);
const newProp = propsToApply[propIndex];
const newValue = styles ![newProp];
if (prop === newProp) {
if (value !== newValue) {
setValue(context, ctxIndex, newValue);
const initialValue = getInitialValue(context, flag);
// there is no point in setting this to dirty if the previously
// rendered value was being referenced by the initial style (or null)
if (initialValue !== newValue) {
setDirty(context, ctxIndex, true);
dirty = true;
}
}
} else {
const indexOfEntry = findEntryPositionByProp(context, newProp, ctxIndex);
if (indexOfEntry > 0) {
// it was found at a later point ... just swap the values
swapMultiContextEntries(context, ctxIndex, indexOfEntry);
if (value !== newValue) {
setValue(context, ctxIndex, newValue);
dirty = true;
}
} else {
// we only care to do this if the insertion is in the middle
const doShift = ctxIndex < context.length;
insertNewMultiProperty(context, ctxIndex, newProp, newValue);
dirty = true;
}
}
ctxIndex += StylingIndex.Size;
propIndex++;
}
// this means that there are left-over values in the context that
// were not included in the provided styles and in this case the
// goal is to "remove" them from the context (by nullifying)
while (ctxIndex < context.length) {
const value = context[ctxIndex + StylingIndex.ValueOffset];
if (value !== null) {
setDirty(context, ctxIndex, true);
setValue(context, ctxIndex, null);
dirty = true;
}
ctxIndex += StylingIndex.Size;
}
// this means that there are left-over property in the context that
// were not detected in the context during the loop above. In that
// case we want to add the new entries into the list
while (propIndex < propsToApply.length) {
const prop = propsToApply[propIndex];
const value = styles ![prop];
context.push(StylingFlags.Dirty, prop, value);
propIndex++;
dirty = true;
}
if (dirty) {
setContextDirty(context, true);
}
}
/**
* Sets and resolves a single CSS style on a property on an `StylingContext` so that they
* can be applied to the element once `renderElementStyles` is called.
*
* Note that prop-level styles are considered higher priority than styles that are applied
* using `updateStyleMap`, therefore, when styles are rendered then any styles that
* have been applied using this function will be considered first (then multi values second
* and then initial values as a backup).
*
* @param context The styling context that will be updated with the
* newly provided style value.
* @param index The index of the property which is being updated.
* @param value The CSS style value that will be assigned
*/
export function updateStyleProp(
context: StylingContext, index: number, value: string | null): void {
const singleIndex = StylingIndex.SingleStylesStartPosition + index * StylingIndex.Size;
const currValue = getValue(context, singleIndex);
const currFlag = getPointers(context, singleIndex);
// didn't change ... nothing to make a note of
if (currValue !== value) {
// the value will always get updated (even if the dirty flag is skipped)
setValue(context, singleIndex, value);
const indexForMulti = getMultiOrSingleIndex(currFlag);
// if the value is the same in the multi-area then there's no point in re-assembling
const valueForMulti = getValue(context, indexForMulti);
if (!valueForMulti || valueForMulti !== value) {
let multiDirty = false;
let singleDirty = true;
// only when the value is set to `null` should the multi-value get flagged
if (value == null && valueForMulti) {
multiDirty = true;
singleDirty = false;
}
setDirty(context, indexForMulti, multiDirty);
setDirty(context, singleIndex, singleDirty);
setContextDirty(context, true);
}
}
}
/**
* Renders all queued styles using a renderer onto the given element.
*
* This function works by rendering any styles (that have been applied
* using `updateStyleMap` and `updateStyleProp`) onto the
* provided element using the provided renderer. Just before the styles
* are rendered a final key/value style map will be assembled.
*
* @param lElement the element that the styles will be rendered on
* @param context The styling context that will be used to determine
* what styles will be rendered
* @param renderer the renderer that will be used to apply the styling
* @param styleStore if provided, the updated style values will be applied
* to this key/value map instead of being renderered via the renderer.
* @returns an object literal. `{ color: 'red', height: 'auto'}`.
*/
export function renderStyles(
lElement: LElementNode, context: StylingContext, renderer: Renderer3,
styleStore?: {[key: string]: any}) {
if (isContextDirty(context)) {
const native = lElement.native;
const multiStartIndex = getMultiStartIndex(context);
for (let i = StylingIndex.SingleStylesStartPosition; i < context.length;
i += StylingIndex.Size) {
// there is no point in rendering styles that have not changed on screen
if (isDirty(context, i)) {
const prop = getProp(context, i);
const value = getValue(context, i);
const flag = getPointers(context, i);
const isInSingleRegion = i < multiStartIndex;
let styleToApply: string|null = value;
// STYLE DEFER CASE 1: Use a multi value instead of a null single value
// this check implies that a single value was removed and we
// should now defer to a multi value and use that (if set).
if (isInSingleRegion && styleToApply == null) {
// single values ALWAYS have a reference to a multi index
const multiIndex = getMultiOrSingleIndex(flag);
styleToApply = getValue(context, multiIndex);
}
// STYLE DEFER CASE 2: Use the initial value if all else fails (is null)
// the initial value will always be a string or null,
// therefore we can safely adopt it incase there's nothing else
if (styleToApply == null) {
styleToApply = getInitialValue(context, flag);
}
setStyle(native, prop, styleToApply, renderer, styleStore);
setDirty(context, i, false);
}
}
setContextDirty(context, false);
}
}
/**
* This function renders a given CSS prop/value entry using the
* provided renderer. If a `styleStore` value is provided then
* that will be used a render context instead of the provided
* renderer.
*
* @param native the DOM Element
* @param prop the CSS style property that will be rendered
* @param value the CSS style value that will be rendered
* @param renderer
* @param styleStore an optional key/value map that will be used as a context to render styles on
*/
function setStyle(
native: any, prop: string, value: string | null, renderer: Renderer3,
styleStore?: {[key: string]: any}) {
if (styleStore) {
styleStore[prop] = value;
} else if (value == null) {
ngDevMode && ngDevMode.rendererRemoveStyle++;
isProceduralRenderer(renderer) ?
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) :
native['style'].removeProperty(prop);
} else {
ngDevMode && ngDevMode.rendererSetStyle++;
isProceduralRenderer(renderer) ?
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) :
native['style'].setProperty(prop, value);
}
}
function setDirty(context: StylingContext, index: number, isDirtyYes: boolean) {
const adjustedIndex =
index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index;
if (isDirtyYes) {
(context[adjustedIndex] as number) |= StylingFlags.Dirty;
} else {
(context[adjustedIndex] as number) &= ~StylingFlags.Dirty;
}
}
function isDirty(context: StylingContext, index: number): boolean {
const adjustedIndex =
index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index;
return ((context[adjustedIndex] as number) & StylingFlags.Dirty) == StylingFlags.Dirty;
}
function pointers(configFlag: number, staticIndex: number, dynamicIndex: number) {
return (configFlag & StylingFlags.Dirty) | (staticIndex << StylingFlags.BitCountSize) |
(dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize));
}
function getInitialValue(context: StylingContext, flag: number): string|null {
const index = getInitialIndex(flag);
return context[StylingIndex.InitialStylesPosition][index] as null | string;
}
function getInitialIndex(flag: number): number {
return (flag >> StylingFlags.BitCountSize) & StylingIndex.BitMask;
}
function getMultiOrSingleIndex(flag: number): number {
const index =
(flag >> (StylingIndex.BitCountSize + StylingFlags.BitCountSize)) & StylingIndex.BitMask;
return index >= StylingIndex.SingleStylesStartPosition ? index : -1;
}
function getMultiStartIndex(context: StylingContext): number {
return getMultiOrSingleIndex(context[StylingIndex.MasterFlagPosition]) as number;
}
function setProp(context: StylingContext, index: number, prop: string) {
context[index + StylingIndex.PropertyOffset] = prop;
}
function setValue(context: StylingContext, index: number, value: string | null) {
context[index + StylingIndex.ValueOffset] = value;
}
function setFlag(context: StylingContext, index: number, flag: number) {
const adjustedIndex =
index === StylingIndex.MasterFlagPosition ? index : (index + StylingIndex.FlagsOffset);
context[adjustedIndex] = flag;
}
function getPointers(context: StylingContext, index: number): number {
const adjustedIndex =
index === StylingIndex.MasterFlagPosition ? index : (index + StylingIndex.FlagsOffset);
return context[adjustedIndex] as number;
}
function getValue(context: StylingContext, index: number): string|null {
return context[index + StylingIndex.ValueOffset] as string | null;
}
function getProp(context: StylingContext, index: number): string {
return context[index + StylingIndex.PropertyOffset] as string;
}
export function isContextDirty(context: StylingContext): boolean {
return isDirty(context, StylingIndex.MasterFlagPosition);
}
export function setContextDirty(context: StylingContext, isDirtyYes: boolean): void {
setDirty(context, StylingIndex.MasterFlagPosition, isDirtyYes);
}
function findEntryPositionByProp(
context: StylingContext, prop: string, startIndex?: number): number {
for (let i = (startIndex || 0) + StylingIndex.PropertyOffset; i < context.length;
i += StylingIndex.Size) {
const thisProp = context[i];
if (thisProp == prop) {
return i - StylingIndex.PropertyOffset;
}
}
return -1;
}
function swapMultiContextEntries(context: StylingContext, indexA: number, indexB: number) {
const tmpValue = getValue(context, indexA);
const tmpProp = getProp(context, indexA);
const tmpFlag = getPointers(context, indexA);
let flagA = tmpFlag;
let flagB = getPointers(context, indexB);
const singleIndexA = getMultiOrSingleIndex(flagA);
if (singleIndexA >= 0) {
const _flag = getPointers(context, singleIndexA);
const _initial = getInitialIndex(_flag);
setFlag(context, singleIndexA, pointers(_flag, _initial, indexB));
}
const singleIndexB = getMultiOrSingleIndex(flagB);
if (singleIndexB >= 0) {
const _flag = getPointers(context, singleIndexB);
const _initial = getInitialIndex(_flag);
setFlag(context, singleIndexB, pointers(_flag, _initial, indexA));
}
setValue(context, indexA, getValue(context, indexB));
setProp(context, indexA, getProp(context, indexB));
setFlag(context, indexA, getPointers(context, indexB));
setValue(context, indexB, tmpValue);
setProp(context, indexB, tmpProp);
setFlag(context, indexB, tmpFlag);
}
function updateSinglePointerValues(context: StylingContext, indexStartPosition: number) {
for (let i = indexStartPosition; i < context.length; i += StylingIndex.Size) {
const multiFlag = getPointers(context, i);
const singleIndex = getMultiOrSingleIndex(multiFlag);
if (singleIndex > 0) {
const singleFlag = getPointers(context, singleIndex);
const initialIndexForSingle = getInitialIndex(singleFlag);
const updatedFlag = pointers(
isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None,
initialIndexForSingle, i);
setFlag(context, singleIndex, updatedFlag);
}
}
}
function insertNewMultiProperty(
context: StylingContext, index: number, name: string, value: string): void {
const doShift = index < context.length;
// prop does not exist in the list, add it in
context.splice(index, 0, StylingFlags.Dirty, name, value);
if (doShift) {
// because the value was inserted midway into the array then we
// need to update all the shifted multi values' single value
// pointers to point to the newly shifted location
updateSinglePointerValues(context, index + StylingIndex.Size);
}
}

View File

@ -11,7 +11,7 @@ import {browserDetection} from '@angular/platform-browser/testing/src/browser_ut
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core';
import * as $r3$ from '../../../src/core_render3_private_export';
import {AttributeMarker} from '../../../src/render3';
import {ComponentDefInternal} from '../../../src/render3/interfaces/definition';
import {ComponentDefInternal, InitialStylingFlags} from '../../../src/render3/interfaces/definition';
import {ComponentFixture, renderComponent, toHtml} from '../render_util';
@ -304,6 +304,7 @@ describe('elements', () => {
it('should bind to a specific style', () => {
type $MyComponent$ = MyComponent;
const c0 = ['color', 'width'];
@Component({
selector: 'my-component',
template: `<div [style.color]="someColor" [style.width.px]="someWidth"></div>`
@ -318,11 +319,14 @@ describe('elements', () => {
factory: function MyComponent_Factory() { return new MyComponent(); },
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) {
$r3$.ɵEe(0, 'div');
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, c0);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsn(0, 'color', $r3$.ɵb(ctx.someColor));
$r3$.ɵsn(0, 'width', $r3$.ɵb(ctx.someWidth), 'px');
$r3$.ɵsp(1, 0, ctx.someColor);
$r3$.ɵsp(1, 1, ctx.someWidth, 'px');
$r3$.ɵsa(1);
}
}
});
@ -349,10 +353,7 @@ describe('elements', () => {
it('should bind to many and keep order', () => {
type $MyComponent$ = MyComponent;
// NORMATIVE
const $e0_attrs$ = ['style', 'color: red;'];
// /NORMATIVE
const c0 = ['color', InitialStylingFlags.INITIAL_STYLES, 'color', 'red'];
@Component({
selector: 'my-component',
template:
@ -367,7 +368,9 @@ describe('elements', () => {
factory: function MyComponent_Factory() { return new MyComponent(); },
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) {
$r3$.ɵEe(0, 'div', $e0_attrs$);
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, c0);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵp(0, 'id', $r3$.ɵb(ctx.someString + 1));
@ -402,11 +405,14 @@ describe('elements', () => {
factory: function StyleComponent_Factory() { return new StyleComponent(); },
template: function StyleComponent_Template(rf: $RenderFlags$, ctx: $StyleComponent$) {
if (rf & 1) {
$r3$.ɵEe(0, 'div');
$r3$.ɵE(0, 'div');
$r3$.ɵs(1);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵk(0, $r3$.ɵb(ctx.classExp));
$r3$.ɵs(0, $r3$.ɵb(ctx.styleExp));
$r3$.ɵsm(1, ctx.styleExp);
$r3$.ɵsa(1);
}
}
});

View File

@ -43,15 +43,18 @@ describe('compiler sanitization', () => {
factory: function MyComponent_Factory() { return new MyComponent(); },
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) {
$r3$.ɵEe(0, 'div');
$r3$.ɵEe(1, 'img');
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, ['background-image']);
$r3$.ɵe();
$r3$.ɵEe(2, 'img');
}
if (rf & 2) {
$r3$.ɵp(0, 'innerHTML', $r3$.ɵb(ctx.innerHTML), $r3$.ɵsanitizeHtml);
$r3$.ɵp(0, 'hidden', $r3$.ɵb(ctx.hidden));
$r3$.ɵsn(1, 'background-image', $r3$.ɵb(ctx.style), $r3$.ɵsanitizeStyle);
$r3$.ɵp(1, 'src', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);
$r3$.ɵa(1, 'srcset', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);
$r3$.ɵsp(1, 0, ctx.style, $r3$.ɵsanitizeStyle);
$r3$.ɵsa(1);
$r3$.ɵp(2, 'src', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);
$r3$.ɵa(2, 'srcset', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);
}
}
});

View File

@ -10,7 +10,8 @@ import {NgForOfContext} from '@angular/common';
import {RenderFlags, directiveInject} from '../../src/render3';
import {defineComponent} from '../../src/render3/definition';
import {bind, container, element, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleNamed, interpolation1, renderTemplate, text, textBinding} from '../../src/render3/instructions';
import {bind, container, element, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleProp, elementStyling, elementStylingApply, interpolation1, renderTemplate, text, textBinding} from '../../src/render3/instructions';
import {InitialStylingFlags} from '../../src/render3/interfaces/definition';
import {AttributeMarker, LElementNode, LNode} from '../../src/render3/interfaces/node';
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {TrustedString, bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
@ -22,11 +23,13 @@ import {ComponentFixture, TemplateFixture} from './render_util';
describe('instructions', () => {
function createAnchor() {
elementStart(0, 'a');
elementStyling(1);
elementEnd();
}
function createDiv() {
function createDiv(initialStyles?: (string | number)[]) {
elementStart(0, 'div');
elementStyling(1, initialStyles && Array.isArray(initialStyles) ? initialStyles : null);
elementEnd();
}
@ -186,32 +189,38 @@ describe('instructions', () => {
});
});
describe('elementStyleNamed', () => {
describe('elementStyleProp', () => {
it('should use sanitizer function', () => {
const t = new TemplateFixture(createDiv);
t.update(
() => elementStyleNamed(0, 'background-image', 'url("http://server")', sanitizeStyle));
const t = new TemplateFixture(() => { return createDiv(['background-image']); });
t.update(() => {
elementStyleProp(1, 0, 'url("http://server")', sanitizeStyle);
elementStylingApply(1);
});
// nothing is set because sanitizer suppresses it.
expect(t.html).toEqual('<div></div>');
t.update(
() => elementStyleNamed(
0, 'background-image', bypassSanitizationTrustStyle('url("http://server")'),
sanitizeStyle));
t.update(() => {
elementStyleProp(1, 0, bypassSanitizationTrustStyle('url("http://server")'), sanitizeStyle);
elementStylingApply(1);
});
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('url("http://server")');
});
});
describe('elementStyle', () => {
describe('elementStyleMap', () => {
function createDivWithStyle() {
elementStart(0, 'div', ['style', 'height: 10px']);
elementStart(0, 'div');
elementStyling(1, ['height', InitialStylingFlags.INITIAL_STYLES, 'height', '10px']);
elementEnd();
}
it('should add style', () => {
const fixture = new TemplateFixture(createDivWithStyle);
fixture.update(() => elementStyle(0, {'background-color': 'red'}));
fixture.update(() => {
elementStyle(1, {'background-color': 'red'});
elementStylingApply(1);
});
expect(fixture.html).toEqual('<div style="height: 10px; background-color: red;"></div>');
});
});

View File

@ -9,7 +9,7 @@
import {RenderFlags} from '@angular/core/src/render3';
import {defineComponent, defineDirective} from '../../src/render3/index';
import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, elementStyleNamed, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, loadDirective, projection, projectionDef, text, textBinding,} from '../../src/render3/instructions';
import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, loadDirective, projection, projectionDef, text, textBinding} from '../../src/render3/instructions';
import {HEADER_OFFSET} from '../../src/render3/interfaces/view';
import {sanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
@ -747,10 +747,12 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementStyling(1, ['border-color']);
elementEnd();
}
if (rf & RenderFlags.Update) {
elementStyleNamed(0, 'border-color', bind(ctx));
elementStyleProp(1, 0, ctx);
elementStylingApply(1);
}
}
@ -764,10 +766,12 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementStyling(1, ['font-size']);
elementEnd();
}
if (rf & RenderFlags.Update) {
elementStyleNamed(0, 'font-size', bind(ctx), 'px');
elementStyleProp(1, 0, ctx, 'px');
elementStylingApply(1);
}
}

View File

@ -25,7 +25,8 @@ function testLStaticData(tagName: string, attrs: TAttributes | null): TNode {
child: null,
parent: null,
dynamicContainerNode: null,
detached: null
detached: null,
stylingTemplate: null
};
}

View File

@ -0,0 +1,657 @@
/**
* @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
*/
import {elementEnd, elementStart, elementStyle, elementStyleProp, elementStyling, elementStylingApply} from '../../src/render3/instructions';
import {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/definition';
import {LElementNode} from '../../src/render3/interfaces/node';
import {Renderer3} from '../../src/render3/interfaces/renderer';
import {StylingContext, StylingFlags, StylingIndex, allocStylingContext, createStylingContextTemplate, isContextDirty, renderStyles as _renderStyles, setContextDirty, updateStyleMap, updateStyleProp} from '../../src/render3/styling';
import {renderToHtml} from './render_util';
describe('styling', () => {
let lElement: LElementNode|null = null;
beforeEach(() => { lElement = { native: {} } as any; });
function initContext(styles?: (number | string)[]): StylingContext {
return allocStylingContext(createStylingContextTemplate(styles));
}
function renderStyles(context: StylingContext, renderer?: Renderer3) {
const styles: {[key: string]: any} = {};
_renderStyles(lElement !, context, (renderer || {}) as Renderer3, styles);
return styles;
}
function trackStylesFactory() {
const styles: {[key: string]: any} = {};
return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} {
_renderStyles(lElement !, context, (renderer || {}) as Renderer3, styles);
return styles;
};
}
function clean(a: number = 0, b: number = 0): number {
let num = 0;
if (a) {
num |= a << StylingFlags.BitCountSize;
}
if (b) {
num |= b << (StylingFlags.BitCountSize + StylingIndex.BitCountSize);
}
return num;
}
function dirty(a: number = 0, b: number = 0): number { return clean(a, b) | StylingFlags.Dirty; }
describe('createStylingContextTemplate', () => {
it('should initialize empty template', () => {
const template = createStylingContextTemplate();
expect(template).toEqual([
[null],
clean(0, 2),
]);
});
it('should initialize static styles', () => {
debugger;
const template = createStylingContextTemplate(
[InitialStylingFlags.INITIAL_STYLES, 'color', 'red', 'width', '10px']);
expect(template).toEqual([
[null, 'red', '10px'],
dirty(0, 8), //
// #2
clean(1, 8),
'color',
null,
// #5
clean(2, 11),
'width',
null,
// #8
dirty(1, 2),
'color',
null,
// #11
dirty(2, 5),
'width',
null,
]);
});
});
describe('instructions', () => {
it('should handle a combination of initial, multi and singular style values (in that order)',
() => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementStyling(1, [
'width', 'height', 'opacity', //
0, 'width', '100px', 'height', '100px', 'opacity', '0.5'
]);
elementEnd();
}
if (rf & RenderFlags.Update) {
elementStyle(1, ctx.myStyles);
elementStyleProp(1, 0, ctx.myWidth);
elementStylingApply(1);
}
}
expect(renderToHtml(Template, {
myStyles: {width: '200px', height: '200px'},
myWidth: '300px'
})).toEqual('<span style="width: 300px; height: 200px; opacity: 0.5;"></span>');
expect(renderToHtml(Template, {myStyles: {width: '200px', height: null}, myWidth: null}))
.toEqual('<span style="width: 200px; height: 100px; opacity: 0.5;"></span>');
});
});
describe('helper functions', () => {
it('should build a list of multiple styling values', () => {
const getStyles = trackStylesFactory();
const stylingContext = initContext();
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
updateStyleMap(stylingContext, {height: '200px'});
expect(getStyles(stylingContext)).toEqual({width: null, height: '200px'});
});
it('should evaluate the delta between style changes when rendering occurs', () => {
const stylingContext = initContext(['width', 'height', 0, 'width', '100px']);
updateStyleMap(stylingContext, {
height: '200px',
});
expect(renderStyles(stylingContext)).toEqual({width: '100px', height: '200px'});
expect(renderStyles(stylingContext)).toEqual({});
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
expect(renderStyles(stylingContext)).toEqual({height: '100px'});
updateStyleProp(stylingContext, 1, '100px');
expect(renderStyles(stylingContext)).toEqual({});
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
expect(renderStyles(stylingContext)).toEqual({});
});
it('should update individual values on a set of styles', () => {
const getStyles = trackStylesFactory();
const stylingContext = initContext(['width', 'height']);
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
updateStyleProp(stylingContext, 1, '200px');
expect(getStyles(stylingContext)).toEqual({width: '100px', height: '200px'});
});
it('should only mark itself as updated when one or more properties have been applied', () => {
const stylingContext = initContext();
expect(isContextDirty(stylingContext)).toBeFalsy();
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
expect(isContextDirty(stylingContext)).toBeTruthy();
setContextDirty(stylingContext, false);
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
expect(isContextDirty(stylingContext)).toBeFalsy();
updateStyleMap(stylingContext, {
width: '200px',
height: '100px',
});
expect(isContextDirty(stylingContext)).toBeTruthy();
});
it('should only mark itself as updated when any single properties have been applied', () => {
const stylingContext = initContext(['height']);
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
setContextDirty(stylingContext, false);
updateStyleProp(stylingContext, 0, '100px');
expect(isContextDirty(stylingContext)).toBeFalsy();
setContextDirty(stylingContext, false);
updateStyleProp(stylingContext, 0, '200px');
expect(isContextDirty(stylingContext)).toBeTruthy();
});
it('should prioritize multi and single styles over initial styles', () => {
const getStyles = trackStylesFactory();
const stylingContext = initContext(
['width', 'height', 'opacity', 0, 'width', '100px', 'height', '100px', 'opacity', '0']);
expect(getStyles(stylingContext)).toEqual({
width: '100px',
height: '100px',
opacity: '0',
});
updateStyleMap(stylingContext, {width: '200px', height: '200px'});
expect(getStyles(stylingContext)).toEqual({
width: '200px',
height: '200px',
opacity: '0',
});
updateStyleProp(stylingContext, 0, '300px');
expect(getStyles(stylingContext)).toEqual({
width: '300px',
height: '200px',
opacity: '0',
});
updateStyleProp(stylingContext, 0, null);
expect(getStyles(stylingContext)).toEqual({
width: '200px',
height: '200px',
opacity: '0',
});
updateStyleMap(stylingContext, {});
expect(getStyles(stylingContext)).toEqual({
width: '100px',
height: '100px',
opacity: '0',
});
});
it('should cleanup removed styles from the context once the styles are built', () => {
const stylingContext = initContext(['width', 'height']);
const getStyles = trackStylesFactory();
updateStyleMap(stylingContext, {width: '100px', height: '100px'});
expect(stylingContext).toEqual([
[null],
dirty(0, 8), //
// #2
clean(0, 8),
'width',
null,
// #5
clean(0, 11),
'height',
null,
// #8
dirty(0, 2),
'width',
'100px',
// #11
dirty(0, 5),
'height',
'100px',
]);
getStyles(stylingContext);
updateStyleMap(stylingContext, {width: '200px', opacity: '0'});
expect(stylingContext).toEqual([
[null],
dirty(0, 8), //
// #2
clean(0, 8),
'width',
null,
// #5
clean(0, 14),
'height',
null,
// #8
dirty(0, 2),
'width',
'200px',
// #11
dirty(),
'opacity',
'0',
// #14
dirty(0, 5),
'height',
null,
]);
getStyles(stylingContext);
expect(stylingContext).toEqual([
[null],
clean(0, 8), //
// #2
clean(0, 8),
'width',
null,
// #5
clean(0, 14),
'height',
null,
// #8
clean(0, 2),
'width',
'200px',
// #11
clean(),
'opacity',
'0',
// #14
clean(0, 5),
'height',
null,
]);
updateStyleMap(stylingContext, {width: null});
updateStyleProp(stylingContext, 0, '300px');
expect(stylingContext).toEqual([
[null],
dirty(0, 8), //
// #2
dirty(0, 8),
'width',
'300px',
// #5
clean(0, 14),
'height',
null,
// #8
clean(0, 2),
'width',
null,
// #11
dirty(),
'opacity',
null,
// #14
clean(0, 5),
'height',
null,
]);
getStyles(stylingContext);
updateStyleProp(stylingContext, 0, null);
expect(stylingContext).toEqual([
[null],
dirty(0, 8), //
// #2
dirty(0, 8),
'width',
null,
// #5
clean(0, 14),
'height',
null,
// #8
clean(0, 2),
'width',
null,
// #11
clean(),
'opacity',
null,
// #14
clean(0, 5),
'height',
null,
]);
});
it('should find the next available space in the context when data is added after being removed before',
() => {
const stylingContext = initContext(['lineHeight']);
const getStyles = trackStylesFactory();
updateStyleMap(stylingContext, {width: '100px', height: '100px', opacity: '0.5'});
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
clean(0, 14),
'lineHeight',
null,
// #5
dirty(),
'width',
'100px',
// #8
dirty(),
'height',
'100px',
// #11
dirty(),
'opacity',
'0.5',
// #14
dirty(0, 2),
'lineHeight',
null,
]);
getStyles(stylingContext);
updateStyleMap(stylingContext, {});
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
clean(0, 14),
'lineHeight',
null,
// #5
dirty(),
'width',
null,
// #8
dirty(),
'height',
null,
// #11
dirty(),
'opacity',
null,
// #14
clean(0, 2),
'lineHeight',
null,
]);
getStyles(stylingContext);
updateStyleMap(stylingContext, {
borderWidth: '5px',
});
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
clean(0, 17),
'lineHeight',
null,
// #5
dirty(),
'borderWidth',
'5px',
// #8
clean(),
'width',
null,
// #11
clean(),
'height',
null,
// #14
clean(),
'opacity',
null,
// #17
clean(0, 2),
'lineHeight',
null,
]);
updateStyleProp(stylingContext, 0, '200px');
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
dirty(0, 17),
'lineHeight',
'200px',
// #5
dirty(),
'borderWidth',
'5px',
// #8
clean(),
'width',
null,
// #11
clean(),
'height',
null,
// #14
clean(),
'opacity',
null,
// #17
clean(0, 2),
'lineHeight',
null,
]);
updateStyleMap(stylingContext, {borderWidth: '15px', borderColor: 'red'});
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
dirty(0, 20),
'lineHeight',
'200px',
// #5
dirty(),
'borderWidth',
'15px',
// #8
dirty(),
'borderColor',
'red',
// #11
clean(),
'width',
null,
// #14
clean(),
'height',
null,
// #17
clean(),
'opacity',
null,
// #20
clean(0, 2),
'lineHeight',
null,
]);
});
it('should render all data as not being dirty after the styles are built', () => {
const getStyles = trackStylesFactory();
const stylingContext = initContext(['height']);
updateStyleMap(stylingContext, {
width: '100px',
});
updateStyleProp(stylingContext, 0, '200px');
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
dirty(0, 8),
'height',
'200px',
// #2
dirty(),
'width',
'100px',
// #8
clean(0, 2),
'height',
null,
]);
getStyles(stylingContext);
expect(stylingContext).toEqual([
[null],
clean(0, 5), //
// #2
clean(0, 8),
'height',
'200px',
// #2
clean(),
'width',
'100px',
// #8
clean(0, 2),
'height',
null,
]);
});
});
});