From ba3eb8b654d77b504988f8f98eaa9e4b7c256b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 11 Jul 2018 09:56:47 -0700 Subject: [PATCH] feat(ivy): properly apply class="", [class], [class.foo] and [attr.class] bindings (#24822) PR Close #24822 --- .../src/largetable/render3/table.ts | 8 +- modules/benchmarks/src/tree/render3/tree.ts | 28 +- packages/compiler/src/core.ts | 4 +- .../compiler/src/render3/r3_identifiers.ts | 6 +- .../compiler/src/render3/view/template.ts | 188 ++- .../render3/r3_compiler_compliance_spec.ts | 28 +- .../render3/r3_view_compiler_styling_spec.ts | 136 +- .../core/src/core_render3_private_export.ts | 3 +- packages/core/src/render3/assert.ts | 3 +- packages/core/src/render3/index.ts | 5 +- packages/core/src/render3/instructions.ts | 127 +- .../core/src/render3/interfaces/definition.ts | 4 +- packages/core/src/render3/jit/environment.ts | 3 +- .../core/src/render3/node_manipulation.ts | 8 +- packages/core/src/render3/styling.ts | 522 ++++-- packages/core/src/render3/util.ts | 28 + .../hello_world/bundle.golden_symbols.json | 3 + .../bundling/todo/bundle.golden_symbols.json | 104 +- .../compiler_canonical/elements_spec.ts | 33 +- .../compiler_canonical/sanitize_spec.ts | 12 +- packages/core/test/render3/exports_spec.ts | 9 +- .../core/test/render3/instructions_spec.ts | 32 +- .../core/test/render3/integration_spec.ts | 26 +- packages/core/test/render3/styling_spec.ts | 1414 ++++++++++------- 24 files changed, 1806 insertions(+), 928 deletions(-) diff --git a/modules/benchmarks/src/largetable/render3/table.ts b/modules/benchmarks/src/largetable/render3/table.ts index bbd822dcc1..852538c4b2 100644 --- a/modules/benchmarks/src/largetable/render3/table.ts +++ b/modules/benchmarks/src/largetable/render3/table.ts @@ -48,13 +48,13 @@ export class LargeTableComponent { { if (rf2 & RenderFlags.Create) { E(0, 'td'); - s(1, c0); - { T(2); } + s(c0); + { T(1); } e(); } if (rf2 & RenderFlags.Update) { - sp(1, 0, cell.row % 2 ? '' : 'grey'); - t(2, b(cell.value)); + sp(0, 0, cell.row % 2 ? '' : 'grey'); + t(1, b(cell.value)); } } v(); diff --git a/modules/benchmarks/src/tree/render3/tree.ts b/modules/benchmarks/src/tree/render3/tree.ts index 17ef84672b..3fc03b2f81 100644 --- a/modules/benchmarks/src/tree/render3/tree.ts +++ b/modules/benchmarks/src/tree/render3/tree.ts @@ -41,16 +41,16 @@ export class TreeComponent { template: function(rf: RenderFlags, ctx: TreeComponent) { if (rf & RenderFlags.Create) { E(0, 'span'); - s(1, c0); - { T(2); } + s(c0); + { T(1); } e(); + C(2); C(3); - C(4); } if (rf & RenderFlags.Update) { - sp(1, 0, ctx.data.depth % 2 ? '' : 'grey'); - t(2, i1(' ', ctx.data.value, ' ')); - cR(3); + sp(0, 0, ctx.data.depth % 2 ? '' : 'grey'); + t(1, i1(' ', ctx.data.value, ' ')); + cR(2); { if (ctx.data.left != null) { let rf0 = V(0); @@ -67,7 +67,7 @@ export class TreeComponent { } } cr(); - cR(4); + cR(3); { if (ctx.data.right != null) { let rf0 = V(0); @@ -114,18 +114,18 @@ export function TreeTpl(rf: RenderFlags, ctx: TreeNode) { E(0, 'tree'); { E(1, 'span'); - s(2, c1); - { T(3); } + s(c1); + { T(2); } e(); + C(3); C(4); - C(5); } e(); } if (rf & RenderFlags.Update) { - sp(2, 0, ctx.depth % 2 ? '' : 'grey'); - t(3, i1(' ', ctx.value, ' ')); - cR(4); + sp(1, 0, ctx.depth % 2 ? '' : 'grey'); + t(2, i1(' ', ctx.value, ' ')); + cR(3); { if (ctx.left != null) { let rf0 = V(0); @@ -134,7 +134,7 @@ export function TreeTpl(rf: RenderFlags, ctx: TreeNode) { } } cr(); - cR(5); + cR(4); { if (ctx.right != null) { let rf0 = V(0); diff --git a/packages/compiler/src/core.ts b/packages/compiler/src/core.ts index 353645a8e6..e34aa4c1c0 100644 --- a/packages/compiler/src/core.ts +++ b/packages/compiler/src/core.ts @@ -380,8 +380,6 @@ export const enum RenderFlags { 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, + VALUES_MODE = 0b1, } diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 12f6375229..698326b6a8 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -33,13 +33,11 @@ export class Identifiers { static elementAttribute: o.ExternalReference = {name: 'ɵa', moduleName: CORE}; - static elementClass: o.ExternalReference = {name: 'ɵk', moduleName: CORE}; - - static elementClassNamed: o.ExternalReference = {name: 'ɵkn', moduleName: CORE}; + static elementClassProp: o.ExternalReference = {name: 'ɵcp', moduleName: CORE}; static elementStyling: o.ExternalReference = {name: 'ɵs', moduleName: CORE}; - static elementStyle: o.ExternalReference = {name: 'ɵsm', moduleName: CORE}; + static elementStylingMap: o.ExternalReference = {name: 'ɵsm', moduleName: CORE}; static elementStyleProp: o.ExternalReference = {name: 'ɵsp', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index ed55319205..e50f2a5ce9 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -40,19 +40,12 @@ function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefin case BindingType.Attribute: return R3.elementAttribute; case BindingType.Class: - return R3.elementClassNamed; + return R3.elementClassProp; default: return undefined; } } -// `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 -}; - export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver { private _dataIndex = 0; private _bindingContext = 0; @@ -310,33 +303,59 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const i18nMessages: o.Statement[] = []; const attributes: o.Expression[] = []; const initialStyleDeclarations: o.Expression[] = []; + const initialClassDeclarations: o.Expression[] = []; const styleInputs: t.BoundAttribute[] = []; + const classInputs: 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); + switch (input.type) { + // [attr.style] or [attr.class] should not be treated as styling-based + // bindings since they are intended to be written directly to the attr + // and therefore will skip all style/class resolution that is present + // with style="", [style]="" and [style.prop]="", class="", + // [class.prop]="". [class]="" assignments + case BindingType.Property: + if (input.name == 'style') { + // this should always go first in the compilation (for [style]) + styleInputs.splice(0, 0, input); + } else if (isClassBinding(input)) { + // this should always go first in the compilation (for [class]) + classInputs.splice(0, 0, input); + } else { + allOtherInputs.push(input); + } + break; + case BindingType.Style: + styleInputs.push(input); + break; + case BindingType.Class: + classInputs.push(input); + break; + default: + allOtherInputs.push(input); + break; } }); let currStyleIndex = 0; + let currClassIndex = 0; let staticStylesMap: {[key: string]: any}|null = null; + let staticClassesMap: {[key: string]: boolean}|null = null; const stylesIndexMap: {[key: string]: number} = {}; + const classesIndexMap: {[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 if (name == 'class') { + staticClassesMap = {}; + value.split(/\s+/g).forEach(className => { + classesIndexMap[className] = currClassIndex++; + staticClassesMap ![className] = true; + }); } else { attributes.push(o.literal(name)); if (attrI18nMetas.hasOwnProperty(name)) { @@ -357,6 +376,14 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } } + for (let i = 0; i < classInputs.length; i++) { + const input = classInputs[i]; + const isMapBasedClassBinding = i === 0 && isClassBinding(input); + if (!isMapBasedClassBinding && !stylesIndexMap.hasOwnProperty(input.name)) { + classesIndexMap[input.name] = currClassIndex++; + } + } + // 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 => { @@ -364,7 +391,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); if (staticStylesMap) { - initialStyleDeclarations.push(o.literal(core.InitialStylingFlags.INITIAL_STYLES)); + initialStyleDeclarations.push(o.literal(core.InitialStylingFlags.VALUES_MODE)); Object.keys(staticStylesMap).forEach(prop => { initialStyleDeclarations.push(o.literal(prop)); @@ -373,6 +400,22 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); } + Object.keys(classesIndexMap).forEach(prop => { + initialClassDeclarations.push(o.literal(prop)); + }); + + if (staticClassesMap) { + initialClassDeclarations.push(o.literal(core.InitialStylingFlags.VALUES_MODE)); + + Object.keys(staticClassesMap).forEach(className => { + initialClassDeclarations.push(o.literal(className)); + initialClassDeclarations.push(o.literal(true)); + }); + } + + const hasStylingInstructions = initialStyleDeclarations.length || styleInputs.length || + initialClassDeclarations.length || classInputs.length; + const attrArg: o.Expression = attributes.length > 0 ? this.constantPool.getConstLiteral(o.literalArr(attributes), true) : o.TYPED_NULL_EXPR; @@ -411,10 +454,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const implicit = o.variable(CONTEXT_NAME); - const elementStyleIndex = - (initialStyleDeclarations.length || styleInputs.length) ? this.allocateDataSlot() : 0; const createSelfClosingInstruction = - elementStyleIndex === 0 && element.children.length === 0 && element.outputs.length === 0; + !hasStylingInstructions && element.children.length === 0 && element.outputs.length === 0; if (createSelfClosingInstruction) { this.instruction( @@ -429,16 +470,30 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver ...trimTrailingNulls(parameters)); // initial styling for static style="..." attributes - if (elementStyleIndex > 0) { - let paramsList: (o.Expression)[] = [o.literal(elementStyleIndex)]; + if (hasStylingInstructions) { + const paramsList: (o.Expression)[] = []; + if (initialStyleDeclarations.length) { - // the template compiler handles initial styling (e.g. style="foo") values + // the template compiler handles initial style (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)); + } else if (initialClassDeclarations.length) { + // no point in having an extra `null` value unless there are follow-up params + paramsList.push(o.NULL_EXPR); } + + if (initialClassDeclarations.length) { + // the template compiler handles initial class styling (e.g. class="foo") values + // in a special command called `elementClass` so that the initial class + // can be processed during runtime. These initial class values are bound to + // a constant because the inital class values do not change (since they're static). + paramsList.push( + this.constantPool.getConstLiteral(o.literalArr(initialClassDeclarations), true)); + } + this._creationCode.push(o.importExpr(R3.elementStyling).callFn(paramsList).toStmt()); } @@ -465,25 +520,64 @@ export class TemplateDefinitionBuilder implements t.Visitor, 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 { + if ((styleInputs.length || classInputs.length) && hasStylingInstructions) { + const indexLiteral = o.literal(elementIndex); + + const firstStyle = styleInputs[0]; + const mapBasedStyleInput = firstStyle && firstStyle.name == 'style' ? firstStyle : null; + + const firstClass = classInputs[0]; + const mapBasedClassInput = firstClass && isClassBinding(firstClass) ? firstClass : null; + + const stylingInput = mapBasedStyleInput || mapBasedClassInput; + if (stylingInput) { + const params: o.Expression[] = []; + if (mapBasedStyleInput) { + params.push(this.convertPropertyBinding(implicit, mapBasedStyleInput.value, true)); + } else if (mapBasedClassInput) { + params.push(o.NULL_EXPR); + } + if (mapBasedClassInput) { + params.push(this.convertPropertyBinding(implicit, mapBasedClassInput.value, true)); + } + this.instruction( + this._bindingCode, stylingInput.sourceSpan, R3.elementStylingMap, indexLiteral, + ...params); + } + + let lastInputCommand: t.BoundAttribute|null = null; + if (styleInputs.length) { + let i = mapBasedStyleInput ? 1 : 0; + for (i; i < styleInputs.length; i++) { + const input = styleInputs[i]; + const convertedBinding = this.convertPropertyBinding(implicit, input.value, true); const key = input.name; - let styleIndex: number = stylesIndexMap[key] !; + const 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); + lastInputCommand = styleInputs[styleInputs.length - 1]; + } + + if (classInputs.length) { + let i = mapBasedClassInput ? 1 : 0; + for (i; i < classInputs.length; i++) { + const input = classInputs[i]; + const convertedBinding = this.convertPropertyBinding(implicit, input.value, true); + const key = input.name; + const classIndex: number = classesIndexMap[key] !; + this.instruction( + this._bindingCode, input.sourceSpan, R3.elementClassProp, indexLiteral, + o.literal(classIndex), convertedBinding); + } + + lastInputCommand = classInputs[classInputs.length - 1]; + } + + this.instruction( + this._bindingCode, lastInputCommand !.sourceSpan, R3.elementStylingApply, indexLiteral); } // Generate element input bindings @@ -494,18 +588,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } const convertedBinding = this.convertPropertyBinding(implicit, input.value); - const specialInstruction = SPECIAL_CASED_PROPERTIES_INSTRUCTION_MAP[input.name]; - if (specialInstruction) { - // special case for [style] and [class] bindings since they are not handled as - // standard properties within this implementation. Instead they are - // handed off to special cased instruction handlers which will then - // delegate them as animation sequences (or input bindings for dirs/cmps) - this.instruction( - this._bindingCode, input.sourceSpan, specialInstruction, o.literal(elementIndex), - convertedBinding); - return; - } - const instruction = mapBindingToInstruction(input.type); if (instruction) { // TODO(chuckj): runtime: security context? @@ -975,3 +1057,7 @@ export function makeBindingParser(): BindingParser { new Parser(new Lexer()), DEFAULT_INTERPOLATION_CONFIG, new DomElementSchemaRegistry(), null, []); } + +function isClassBinding(input: t.BoundAttribute): boolean { + return input.name == 'className' || input.name == 'class'; +} diff --git a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts index 47a041a224..007c103bc4 100644 --- a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts +++ b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts @@ -6,9 +6,12 @@ * 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'; + /* These tests are codified version of the tests in compiler_canonical_spec.ts. Every * test in compiler_canonical_spec.ts should have a corresponding test here. */ @@ -44,15 +47,17 @@ describe('compiler compliance', () => { // The template should look like this (where IDENT is a wild card for an identifier): const template = ` - const $c1$ = ['class', 'my-app', 'title', 'Hello']; - const $c2$ = ['cx', '20', 'cy', '30', 'r', '50']; + const $c1$ = ['title', 'Hello']; + const $c2$ = ['my-app', ${InitialStylingFlags.VALUES_MODE}, 'my-app', true]; + const $c3$ = ['cx', '20', 'cy', '30', 'r', '50']; … template: function MyComponent_Template(rf: IDENT, ctx: IDENT) { if (rf & 1) { $r3$.ɵE(0, 'div', $c1$); + $r3$.ɵs((null as any), $c2$); $r3$.ɵNS(); $r3$.ɵE(1, 'svg'); - $r3$.ɵEe(2, 'circle', $c2$); + $r3$.ɵEe(2, 'circle', $c3$); $r3$.ɵe(); $r3$.ɵNH(); $r3$.ɵE(3, 'p'); @@ -93,11 +98,13 @@ describe('compiler compliance', () => { // The template should look like this (where IDENT is a wild card for an identifier): const template = ` - const $c1$ = ['class', 'my-app', 'title', 'Hello']; + const $c1$ = ['title', 'Hello']; + const $c2$ = ['my-app', ${InitialStylingFlags.VALUES_MODE}, 'my-app', true]; … template: function MyComponent_Template(rf: IDENT, ctx: IDENT) { if (rf & 1) { $r3$.ɵE(0, 'div', $c1$); + $r3$.ɵs((null as any), $c2$); $r3$.ɵNM(); $r3$.ɵE(1, 'math'); $r3$.ɵEe(2, 'infinity'); @@ -141,11 +148,13 @@ describe('compiler compliance', () => { // The template should look like this (where IDENT is a wild card for an identifier): const template = ` - const $c1$ = ['class', 'my-app', 'title', 'Hello']; + const $c1$ = ['title', 'Hello']; + const $c2$ = ['my-app', ${InitialStylingFlags.VALUES_MODE}, 'my-app', true]; … template: function MyComponent_Template(rf: IDENT, ctx: IDENT) { if (rf & 1) { $r3$.ɵE(0, 'div', $c1$); + $r3$.ɵs((null as any), $c2$); $r3$.ɵT(1, 'Hello '); $r3$.ɵE(2, 'b'); $r3$.ɵT(3, 'World'); @@ -322,6 +331,7 @@ describe('compiler compliance', () => { const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }'; const template = ` const _c0 = ['background-color']; + const _c1 = ['error']; class MyComponent { static ngComponentDef = i0.ɵdefineComponent({type:MyComponent,selectors:[['my-component']], factory:function MyComponent_Factory(){ @@ -329,13 +339,13 @@ describe('compiler compliance', () => { },template:function MyComponent_Template(rf:number,ctx:any){ if (rf & 1) { $r3$.ɵE(0, 'div'); - $r3$.ɵs(1, _c0); + $r3$.ɵs(_c0, _c1); $r3$.ɵe(); } if (rf & 2) { - $r3$.ɵsp(1, 0, ctx.color); - $r3$.ɵsa(1); - $r3$.ɵkn(0, 'error', $r3$.ɵb(ctx.error)); + $r3$.ɵsp(0, 0, ctx.color); + $r3$.ɵcp(0, 0, ctx.error); + $r3$.ɵsa(0); } } `; diff --git a/packages/compiler/test/render3/r3_view_compiler_styling_spec.ts b/packages/compiler/test/render3/r3_view_compiler_styling_spec.ts index eae327c642..63b29ae47a 100644 --- a/packages/compiler/test/render3/r3_view_compiler_styling_spec.ts +++ b/packages/compiler/test/render3/r3_view_compiler_styling_spec.ts @@ -43,12 +43,12 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) { if (rf & 1) { $r3$.ɵE(0, 'div'); - $r3$.ɵs(1); + $r3$.ɵs(); $r3$.ɵe(); } if (rf & 2) { - $r3$.ɵsm(1, $ctx$.myStyleExp); - $r3$.ɵsa(1); + $r3$.ɵsm(0, $ctx$.myStyleExp); + $r3$.ɵsa(0); } } `; @@ -57,7 +57,7 @@ describe('compiler compliance: styling', () => { 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', + it('should place initial, multi, singular and application followed by attribute style instructions in the template code in that order', () => { const files = { app: { @@ -85,7 +85,7 @@ describe('compiler compliance: styling', () => { }; const template = ` - const _c0 = ['opacity','width','height',${InitialStylingFlags.INITIAL_STYLES},'opacity','1']; + const _c0 = ['opacity','width','height',${InitialStylingFlags.VALUES_MODE},'opacity','1']; class MyComponent { static ngComponentDef = i0.ɵdefineComponent({ type: MyComponent, @@ -96,14 +96,14 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) { if (rf & 1) { $r3$.ɵE(0, 'div'); - $r3$.ɵs(1, _c0); + $r3$.ɵs(_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$.ɵsm(0, $ctx$.myStyleExp); + $r3$.ɵsp(0, 1, $ctx$.myWidth); + $r3$.ɵsp(0, 2, $ctx$.myHeight); + $r3$.ɵsa(0); $r3$.ɵa(0, 'style', $r3$.ɵb('border-width: 10px')); } } @@ -127,7 +127,7 @@ describe('compiler compliance: styling', () => { template: \`
\` }) export class MyComponent { - myClassExp = [{color:'orange'}, {color:'green', duration:1000}] + myClassExp = {'foo':true} } @NgModule({declarations: [MyComponent]}) @@ -139,10 +139,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(); + $r3$.ɵe(); } if (rf & 2) { - $r3$.ɵk(0,$r3$.ɵb($ctx$.myClassExp)); + $r3$.ɵsm(0,(null as any),$ctx$.myClassExp); + $r3$.ɵsa(0); } } `; @@ -150,5 +153,112 @@ 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 class instructions in the template code in that order', + () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \`
\` + }) + export class MyComponent { + myClassExp = {a:true, b:true}; + yesToApple = true; + yesToOrange = true; + } + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const template = ` + const _c0 = ['grape','apple','orange',${InitialStylingFlags.VALUES_MODE},'grape',true]; + 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((null as any), _c0); + $r3$.ɵe(); + } + if (rf & 2) { + $r3$.ɵsm(0, (null as any), $ctx$.myClassExp); + $r3$.ɵcp(0, 1, $ctx$.yesToApple); + $r3$.ɵcp(0, 2, $ctx$.yesToOrange); + $r3$.ɵsa(0); + $r3$.ɵa(0, 'class', $r3$.ɵb('banana')); + } + } + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should not generate the styling apply instruction if there are only static style/class attributes', + () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \`
\` + }) + export class MyComponent {} + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const template = ` + const _c0 = ['width',${InitialStylingFlags.VALUES_MODE},'width','100px']; + const _c1 = ['foo',${InitialStylingFlags.VALUES_MODE},'foo',true]; + 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(_c0, _c1); + $r3$.ɵe(); + } + if (rf & 2) { + $r3$.ɵa(0, 'class', $r3$.ɵb('round')); + $r3$.ɵa(0, 'style', $r3$.ɵb('height:100px')); + } + } + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); }); }); diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index a6853410d9..ee98d506bd 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -80,8 +80,7 @@ export { sm as ɵsm, sp as ɵsp, sa as ɵsa, - k as ɵk, - kn as ɵkn, + cp as ɵcp, t as ɵt, v as ɵv, st as ɵst, diff --git a/packages/core/src/render3/assert.ts b/packages/core/src/render3/assert.ts index 121217cb58..dc3d97311d 100644 --- a/packages/core/src/render3/assert.ts +++ b/packages/core/src/render3/assert.ts @@ -63,6 +63,7 @@ export function assertComponentType( msg: string = 'Type passed in is not ComponentType, it does not have \'ngComponentDef\' property.') { if (!actual.ngComponentDef) { + debugger; throwError(msg); } } @@ -70,4 +71,4 @@ export function assertComponentType( function throwError(msg: string): never { debugger; // Left intentionally for better debugger experience. throw new Error(`ASSERTION ERROR: ${msg}`); -} \ No newline at end of file +} diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 5734e884ed..e022bc4144 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -51,14 +51,13 @@ export { element as Ee, elementAttribute as a, - elementClass as k, - elementClassNamed as kn, + elementClassProp as cp, elementEnd as e, elementProperty as p, elementStart as E, elementStyling as s, - elementStyle as sm, + elementStylingMap as sm, elementStyleProp as sp, elementStylingApply as sa, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index d5f0e2c4f4..a226cbb499 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -15,7 +15,7 @@ import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotE import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container'; -import {ComponentDefInternal, ComponentQuery, ComponentTemplate, DirectiveDefInternal, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; +import {ComponentDefInternal, ComponentQuery, ComponentTemplate, DirectiveDefInternal, DirectiveDefListOrFactory, InitialStylingFlags, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {LInjector} from './interfaces/injector'; import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node'; import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; @@ -25,8 +25,8 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, Curre import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; -import {StylingContext, allocStylingContext, createStylingContextTemplate, renderStyles as renderElementStyles, updateStyleMap as updateElementStyleMap, updateStyleProp as updateElementStyleProp} from './styling'; -import {isDifferent, stringify} from './util'; +import {StylingContext, StylingIndex, allocStylingContext, createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling'; +import {assertDataInRangeInternal, isDifferent, loadElementInternal, loadInternal, stringify} from './util'; import {ViewRef} from './view_ref'; @@ -96,6 +96,7 @@ export const CIRCULAR = '__CIRCULAR__'; */ let renderer: Renderer3; let rendererFactory: RendererFactory3; +let currentElementNode: LElementNode|null = null; export function getRenderer(): Renderer3 { // top level variables should not be exported for performance reasons (PERF_NOTES.md) @@ -668,6 +669,7 @@ export function elementStart( const node: LElementNode = createLNode(index, TNodeType.Element, native !, name, attrs || null, null); + currentElementNode = node; if (attrs) { setUpAttributes(native, attrs); @@ -1104,6 +1106,7 @@ export function elementEnd() { const queries = previousOrParentNode.queries; queries && queries.addNode(previousOrParentNode); queueLifecycleHooks(previousOrParentNode.tNode.flags, tView); + currentElementNode = null; } /** @@ -1118,7 +1121,7 @@ export function elementEnd() { export function elementAttribute( index: number, name: string, value: any, sanitizer?: SanitizerFn): void { if (value !== NO_CHANGE) { - const element: LElementNode = load(index); + const element = loadElement(index); if (value == null) { ngDevMode && ngDevMode.rendererRemoveAttribute++; isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) : @@ -1149,7 +1152,7 @@ export function elementAttribute( export function elementProperty( index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): void { if (value === NO_CHANGE) return; - const node = load(index) as LElementNode; + const node = loadElement(index) as LElementNode; const tNode = node.tNode; // if tNode.inputs is undefined, a listener has created outputs, but inputs haven't // yet been checked @@ -1268,44 +1271,9 @@ function generatePropertyAliases( * renaming as part of minification. * @param value A value indicating if a given class should be added or removed. */ -export function elementClassNamed(index: number, className: string, value: T | NO_CHANGE): void { - if (value !== NO_CHANGE) { - const lElement = load(index) as LElementNode; - if (value) { - ngDevMode && ngDevMode.rendererAddClass++; - isProceduralRenderer(renderer) ? renderer.addClass(lElement.native, className) : - lElement.native.classList.add(className); - - } else { - ngDevMode && ngDevMode.rendererRemoveClass++; - isProceduralRenderer(renderer) ? renderer.removeClass(lElement.native, className) : - lElement.native.classList.remove(className); - } - } -} - -/** - * Set the `className` property on a DOM element. - * - * This instruction is meant to handle the `[class]="exp"` usage. - * - * `elementClass` instruction writes the value to the "element's" `className` property. - * - * @param index The index of the element to update in the data array - * @param value A value indicating a set of classes which should be applied. The method overrides - * any existing classes. The value is stringified (`toString`) before it is applied to the - * element. - */ -export function elementClass(index: number, value: T | NO_CHANGE): void { - if (value !== NO_CHANGE) { - // TODO: This is a naive implementation which simply writes value to the `className`. In the - // future - // we will add logic here which would work with the animation code. - const lElement: LElementNode = load(index); - ngDevMode && ngDevMode.rendererSetClassName++; - isProceduralRenderer(renderer) ? renderer.setProperty(lElement.native, 'className', value) : - lElement.native['className'] = stringify(value); - } +export function elementClassProp( + index: number, stylingIndex: number, value: T | NO_CHANGE): void { + updateElementClassProp(getStylingContext(index), stylingIndex, value ? true : false); } /** @@ -1323,22 +1291,29 @@ export function elementClass(index: number, value: T | NO_CHANGE): void { * (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. + * @param styleDeclarations A key/value array 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). + * @param classDeclarations A key/value array of CSS classes 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 classes placed on the element by multiple (`[class]`) or singular (`[class.named]`) + * bindings. If a class binding changes its value to a falsy value then the matching initial + * class value that are passed in here will be applied to the element (if matched). */ -export function elementStyling(index: number, styles?: (string | number)[] | null): void { - const tNode = load(index - 1).tNode; +export function elementStyling( + styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, + classDeclarations?: (string | boolean | InitialStylingFlags)[] | null): void { + const lElement = currentElementNode !; + const tNode = lElement.tNode; if (!tNode.stylingTemplate) { // initialize the styling template. - tNode.stylingTemplate = createStylingContextTemplate(styles); + tNode.stylingTemplate = createStylingContextTemplate(styleDeclarations, classDeclarations); } - // Allocate space but leave null for lazy creation. - viewData[index + HEADER_OFFSET] = null; - if (styles && styles.length) { - elementStylingApply(index); + if (styleDeclarations && styleDeclarations.length || + classDeclarations && classDeclarations.length) { + elementStylingApply(tNode.index - HEADER_OFFSET); } } @@ -1354,12 +1329,13 @@ export function elementStyling(index: number, styles?: (string | number)[] | */ function getStylingContext(index: number): StylingContext { let stylingContext = load(index); - if (!stylingContext) { - const lElement: LElementNode = load(index - 1); + if (!Array.isArray(stylingContext)) { + const lElement = stylingContext as any as LElementNode; const tNode = lElement.tNode; ngDevMode && assertDefined(tNode.stylingTemplate, 'getStylingContext() called before elementStyling()'); - stylingContext = viewData[index + HEADER_OFFSET] = allocStylingContext(tNode.stylingTemplate !); + stylingContext = viewData[index + HEADER_OFFSET] = + allocStylingContext(lElement, tNode.stylingTemplate !); } return stylingContext; } @@ -1379,7 +1355,7 @@ function getStylingContext(index: number): StylingContext { * index.) */ export function elementStylingApply(index: number): void { - renderElementStyles(load(index - 1), getStylingContext(index), renderer); + renderElementStyles(getStylingContext(index), renderer); } /** @@ -1436,12 +1412,17 @@ export function elementStyleProp( * (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 null, then the style is removed. + * @param styles A key/value style map of the styles that will be applied to the given element. + * Any missing styles (that have already been applied to the element beforehand) will be + * removed (unset) from the element's styling. + * @param classes A key/value style map of CSS classes that will be added to the given element. + * Any missing classes (that have already been applied to the element beforehand) will be + * removed (unset) from the element's list of CSS classes. */ -export function elementStyle(index: number, value: {[styleName: string]: any} | null): void { - updateElementStyleMap(getStylingContext(index), value); +export function elementStylingMap( + index: number, styles: {[styleName: string]: any} | null, + classes?: {[key: string]: any} | string | null): void { + updateStylingMap(getStylingContext(index), styles, classes); } ////////////////////////// @@ -1476,7 +1457,7 @@ export function text(index: number, value?: any): void { export function textBinding(index: number, value: T | NO_CHANGE): void { if (value !== NO_CHANGE) { ngDevMode && assertDataInRange(index + HEADER_OFFSET); - const existingNode = load(index) as LTextNode; + const existingNode = loadElement(index) as any as LTextNode; ngDevMode && assertDefined(existingNode, 'LNode should exist'); ngDevMode && assertDefined(existingNode.native, 'native element should exist'); ngDevMode && ngDevMode.rendererSetText++; @@ -1758,7 +1739,7 @@ export function container( * @param index The index of the container in the data array */ export function containerRefreshStart(index: number): void { - previousOrParentNode = load(index) as LNode; + previousOrParentNode = loadElement(index) as LNode; ngDevMode && assertNodeType(previousOrParentNode, TNodeType.Container); isParent = true; (previousOrParentNode as LContainerNode).data[ACTIVE_INDEX] = 0; @@ -2541,17 +2522,6 @@ export function store(index: number, value: T): void { viewData[adjustedIndex] = value; } -/** Retrieves a value from current `viewData`. */ -export function load(index: number): T { - return loadInternal(index, viewData); -} - -/** Retrieves a value from any `LViewData`. */ -export function loadInternal(index: number, arr: LViewData): T { - ngDevMode && assertDataInRange(index + HEADER_OFFSET, arr); - return arr[index + HEADER_OFFSET]; -} - /** Retrieves a value from the `directives` array. */ export function loadDirective(index: number): T { ngDevMode && assertDefined(directives, 'Directives array should be defined if reading a dir.'); @@ -2568,6 +2538,15 @@ export function loadQueryList(queryListIdx: number): QueryList { return viewData[CONTENT_QUERIES] ![queryListIdx]; } +/** Retrieves a value from current `viewData`. */ +export function load(index: number): T { + return loadInternal(index, viewData); +} + +export function loadElement(index: number): LElementNode { + return loadElementInternal(index, viewData); +} + /** Gets the current binding value and increments the binding index. */ export function consumeBinding(): any { ngDevMode && assertDataInRange(viewData[BINDING_INDEX]); @@ -2645,7 +2624,7 @@ function assertHasParent() { function assertDataInRange(index: number, arr?: any[]) { if (arr == null) arr = viewData; - assertLessThan(index, arr ? arr.length : 0, 'index expected to be a valid data index'); + assertDataInRangeInternal(index, arr || viewData); } function assertDataNext(index: number, arr?: any[]) { diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 20ddafad12..27cedbdefb 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -299,8 +299,6 @@ export type PipeTypeList = // 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, + VALUES_MODE = 0b1, } diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index b0d3f7b8ae..b5808dd6ad 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -63,8 +63,7 @@ export const angularCoreEnv: {[name: string]: Function} = { 'ɵi7': r3.i7, 'ɵi8': r3.i8, 'ɵiV': r3.iV, - 'ɵk': r3.k, - 'ɵkn': r3.kn, + 'ɵcp': r3.cp, 'ɵL': r3.L, 'ɵld': r3.ld, 'ɵP': r3.P, diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 41c9f14cc5..56f99d8e78 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -14,7 +14,7 @@ import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection' import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {CLEANUP, CONTAINER_INDEX, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, HookData, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; -import {stringify} from './util'; +import {readElementValue, stringify} from './util'; const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4 + unused5; @@ -32,7 +32,7 @@ export function getNextLNode(node: LNode): LNode|null { export function getChildLNode(node: LNode): LNode|null { if (node.tNode.child) { const viewData = node.tNode.type === TNodeType.View ? node.data as LViewData : node.view; - return viewData[node.tNode.child.index]; + return readElementValue(viewData[node.tNode.child.index]); } return null; } @@ -50,7 +50,7 @@ export function getParentLNode(node: LNode): LElementNode|LContainerNode|LViewNo return containerHostIndex === -1 ? null : node.view[containerHostIndex].dynamicLContainerNode; } const parent = node.tNode.parent; - return parent ? node.view[parent.index] : node.view[HOST_NODE]; + return readElementValue(parent ? node.view[parent.index] : node.view[HOST_NODE]); } const enum WalkLNodeTreeAction { @@ -463,7 +463,7 @@ function removeListeners(viewData: LViewData): void { for (let i = 0; i < cleanup.length - 1; i += 2) { if (typeof cleanup[i] === 'string') { // This is a listener with the native renderer - const native = viewData[cleanup[i + 1]].native; + const native = readElementValue(viewData[cleanup[i + 1]]).native; const listener = viewData[CLEANUP] ![cleanup[i + 2]]; native.removeEventListener(cleanup[i], listener, cleanup[i + 3]); i += 2; diff --git a/packages/core/src/render3/styling.ts b/packages/core/src/render3/styling.ts index ddae3e798a..d4e2d049da 100644 --- a/packages/core/src/render3/styling.ts +++ b/packages/core/src/render3/styling.ts @@ -12,66 +12,81 @@ import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces /** * 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. + * styling properties have been assigned via the provided `updateStylingMap`, `updateStyleProp` + * and `updateClassProp` 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] = mutation/type flag for the style/class 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. + * value of this array is always `null` even if no initial styling exists. * 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) + * single: any styles that are updated using `updateStyleProp` or `updateClassProp` (fixed set) * - * multi: any styles that are updated using `updateStyleMap` (dynamic set) + * multi: any styles that are updated using `updateStylingMap` (dynamic set) * - * Note that context is only used to collect style information. Only when `renderStyles` + * Note that context is only used to collect style information. Only when `renderStyling` * 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: + * When the context is created, depending on what initial styling values 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;" + * class="foo" * * 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 + * [null, '100px', '200px', true], // property names are not needed since they have already been * written to DOM. * + * 1, // this instructs how many `style` values there are so that class index values can be + * offsetted + * * configMasterVal, * - * // 2 + * // 3 * 'width', - * pointers(1, 8); // Point to static `width`: `100px` and multi `width`. + * pointers(1, 12); // Point to static `width`: `100px` and multi `width`. * null, * - * // 5 + * // 6 * 'height', - * pointers(2, 11); // Point to static `height`: `200px` and multi `height`. + * pointers(2, 15); // Point to static `height`: `200px` and multi `height`. * null, * - * // 8 + * // 9 + * 'foo', + * pointers(1, 18); // Point to static `foo`: `true` and multi `foo`. + * null, + * + * // 12 * 'width', - * pointers(1, 2); // Point to static `width`: `100px` and single `width`. + * pointers(1, 3); // Point to static `width`: `100px` and single `width`. * null, * - * // 11 + * // 15 * 'height', - * pointers(2, 5); // Point to static `height`: `200px` and single `height`. + * pointers(2, 6); // Point to static `height`: `200px` and single `height`. + * null, + * + * // 18 + * 'foo', + * pointers(3, 9); // Point to static `foo`: `true` and single `foo`. * null, * ] * @@ -82,30 +97,50 @@ import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces * } * ``` * - * 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). + * The values are duplicated so that space is set aside for both multi ([style] and [class]) + * and single ([style.prop] or [class.named]) 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). + * This means that every time `updateStyleProp` or `updateClassProp` are called then they + * must be called using an index value (not a property string) which references the index + * value of the initial style prop/class when the context was created. This also means that + * `updateStyleProp` or `updateClassProp` cannot be called with a new property (only + * `updateStylingMap` can include new CSS properties that will be added to the context). */ -export interface StylingContext extends Array { +export interface StylingContext extends + Array { + /** + * Location of element that is used as a target for this context. + */ + [0]: LElementNode|null; + /** * Location of initial data shared by all instances of this style. */ - [0]: InitialStyles; + [1]: 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; + [2]: number; + + /** + * A numeric value representing the class index offset value. Whenever a single class is + * applied (using `elementClassProp`) it should have an styling index value that doesn't + * need to take into account any style values that exist in the context. + */ + [3]: number; + + /** + * The last CLASS STRING VALUE that was interpreted by elementStylingMap. This is cached + * So that the algorithm can exit early incase the string has not changed. + */ + [4]: string|null; } /** @@ -116,7 +151,7 @@ export interface StylingContext extends Array * 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 { [0]: null; } +export interface InitialStyles extends Array { [0]: null; } /** * Used to set the context to be dirty or not both on the master flag (position 1) @@ -124,21 +159,31 @@ export interface InitialStyles extends Array { [0]: null; } */ export const enum StylingFlags { // Implies no configurations - None = 0b0, + None = 0b00, // Whether or not the entry or context itself is dirty - Dirty = 0b1, + Dirty = 0b01, + // Whether or not this is a class-based assignment + Class = 0b10, // The max amount of bits used to represent these configuration values - BitCountSize = 1, + BitCountSize = 2, + // There are only two bits here + BitMask = 0b11 } /** 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, + ElementPosition = 0, + // Position of where the initial styles are stored in the styling context + InitialStylesPosition = 1, // Index of location where the start of single properties are stored. (`updateStyleProp`) - MasterFlagPosition = 1, + MasterFlagPosition = 2, + // Index of location where the class index offset value is located + ClassOffsetPosition = 3, + // Position of where the last string-based CSS class value was stored + CachedCssClassString = 4, // Location of single (prop) value entries are stored within the context - SingleStylesStartPosition = 2, + SingleStylesStartPosition = 5, // Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue FlagsOffset = 0, PropertyOffset = 1, @@ -157,9 +202,12 @@ export const enum StylingIndex { * 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 { +export function allocStylingContext( + lElement: LElementNode | null, templateStyleContext: StylingContext): StylingContext { // each instance gets a copy - return templateStyleContext.slice() as any as StylingContext; + const context = templateStyleContext.slice() as any as StylingContext; + context[StylingIndex.ElementPosition] = lElement; + return context; } /** @@ -176,38 +224,74 @@ export function allocStylingContext(templateStyleContext: StylingContext): Styli * -> ['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`. + * + * @param initialClassDeclarations a list of class declarations and initial class values + * that are used later within the styling context. + * + * -> ['foo', 'bar', SPECIAL_ENUM_VAL, 'foo', true] + * This implies that `foo` and `bar` will be later styled and that the `foo` + * class will be applied to the element as an initial class since it's true */ export function createStylingContextTemplate( - initialStyleDeclarations?: (string | InitialStylingFlags)[] | null): StylingContext { - const initialStyles: InitialStyles = [null]; - const context: StylingContext = [initialStyles, 0]; + initialStyleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, + initialClassDeclarations?: (string | boolean | InitialStylingFlags)[] | null): StylingContext { + const initialStylingValues: InitialStyles = [null]; + const context: StylingContext = [null, initialStylingValues, 0, 0, null]; - const indexLookup: {[key: string]: number} = {}; + // we use two maps since a class name might collide with a CSS style prop + const stylesLookup: {[key: string]: number} = {}; + const classesLookup: {[key: string]: number} = {}; + + let totalStyleDeclarations = 0; 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) { + if (v === InitialStylingFlags.VALUES_MODE) { hasPassedDeclarations = true; } else { const prop = v as string; if (hasPassedDeclarations) { const value = initialStyleDeclarations[++i] as string; - initialStyles.push(value); - indexLookup[prop] = initialStyles.length - 1; + initialStylingValues.push(value); + stylesLookup[prop] = initialStylingValues.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; + totalStyleDeclarations++; + stylesLookup[prop] = 0; } } } } - const allProps = Object.keys(indexLookup); - const totalProps = allProps.length; + // make where the class offsets begin + context[StylingIndex.ClassOffsetPosition] = totalStyleDeclarations; + + if (initialClassDeclarations) { + let hasPassedDeclarations = false; + for (let i = 0; i < initialClassDeclarations.length; i++) { + const v = initialClassDeclarations[i] as string | boolean | InitialStylingFlags; + // this flag value marks where the declarations end the initial values begin + if (v === InitialStylingFlags.VALUES_MODE) { + hasPassedDeclarations = true; + } else { + const className = v as string; + if (hasPassedDeclarations) { + const value = initialClassDeclarations[++i] as boolean; + initialStylingValues.push(value); + classesLookup[className] = initialStylingValues.length - 1; + } else { + classesLookup[className] = 0; + } + } + } + } + + const styleProps = Object.keys(stylesLookup); + const classNames = Object.keys(classesLookup); + const classNamesIndexStart = styleProps.length; + const totalProps = styleProps.length + classNames.length; // *2 because we are filling for both single and multi style spaces const maxLength = totalProps * StylingIndex.Size * 2 + StylingIndex.SingleStylesStartPosition; @@ -222,86 +306,140 @@ export function createStylingContextTemplate( 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]; + for (let i = 0; i < totalProps; i++) { + const isClassBased = i >= classNamesIndexStart; + const prop = isClassBased ? classNames[i - classNamesIndexStart] : styleProps[i]; + const indexForInitial = isClassBased ? classesLookup[prop] : stylesLookup[prop]; + const initialValue = initialStylingValues[indexForInitial]; - const indexForInitial = indexLookup[prop]; const indexForMulti = i * StylingIndex.Size + multiStart; const indexForSingle = i * StylingIndex.Size + singleStart; + const initialFlag = isClassBased ? StylingFlags.Class : StylingFlags.None; - setFlag(context, indexForSingle, pointers(StylingFlags.None, indexForInitial, indexForMulti)); + setFlag(context, indexForSingle, pointers(initialFlag, indexForInitial, indexForMulti)); setProp(context, indexForSingle, prop); setValue(context, indexForSingle, null); - setFlag(context, indexForMulti, pointers(StylingFlags.Dirty, indexForInitial, indexForSingle)); + const flagForMulti = + initialFlag | (initialValue !== null ? StylingFlags.Dirty : StylingFlags.None); + setFlag(context, indexForMulti, pointers(flagForMulti, 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 + // 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); + setContextDirty(context, initialStylingValues.length > 1); return context; } const EMPTY_ARR: any[] = []; +const EMPTY_OBJ: {[key: string]: any} = {}; /** - * Sets and resolves all `multi` styles on an `StylingContext` so that they can be - * applied to the element once `renderStyles` is called. + * Sets and resolves all `multi` styling on an `StylingContext` so that they can be + * applied to the element once `renderStyling` 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. + * All missing styles/class (any values that are not provided in the new `styles` + * or `classes` params) 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. + * @param classes The key/value map of CSS class names 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; +export function updateStylingMap( + context: StylingContext, styles: {[key: string]: any} | null, + classes?: {[key: string]: any} | string | null): void { + let classNames: string[] = EMPTY_ARR; + let applyAllClasses = false; + let ignoreAllClassUpdates = false; + + // each time a string-based value pops up then it shouldn't require a deep + // check of what's changed. + if (typeof classes == 'string') { + const cachedClassString = context[StylingIndex.CachedCssClassString] as string | null; + if (cachedClassString && cachedClassString === classes) { + ignoreAllClassUpdates = true; + } else { + context[StylingIndex.CachedCssClassString] = classes; + classNames = classes.split(/\s+/); + // this boolean is used to avoid having to create a key/value map of `true` values + // since a classname string implies that all those classes are added + applyAllClasses = true; + } + } else { + classNames = classes ? Object.keys(classes) : EMPTY_ARR; + context[StylingIndex.CachedCssClassString] = null; + } + + classes = (classes || EMPTY_OBJ) as{[key: string]: any}; + + const styleProps = styles ? Object.keys(styles) : EMPTY_ARR; + styles = styles || EMPTY_OBJ; + + const classesStartIndex = styleProps.length; const multiStartIndex = getMultiStartIndex(context); let dirty = false; let ctxIndex = multiStartIndex; + let propIndex = 0; + const propLimit = styleProps.length + classNames.length; // 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); + // styles differ with respect to the context. Later if the context/styles/classes + // are off-balance then they will be dealt in another loop after this one + while (ctxIndex < context.length && propIndex < propLimit) { + const isClassBased = propIndex >= classesStartIndex; - const newProp = propsToApply[propIndex]; - const newValue = styles ![newProp]; - if (prop === newProp) { - if (value !== newValue) { - setValue(context, ctxIndex, newValue); - const initialValue = getInitialValue(context, flag); + // when there is a cache-hit for a string-based class then we should + // avoid doing any work diffing any of the changes + if (!ignoreAllClassUpdates || !isClassBased) { + const adjustedPropIndex = isClassBased ? propIndex - classesStartIndex : propIndex; + const newProp: string = + isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex]; + const newValue: string|boolean = + isClassBased ? (applyAllClasses ? true : classes[newProp]) : styles[newProp]; - // 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); + const prop = getProp(context, ctxIndex); + if (prop === newProp) { + const value = getValue(context, ctxIndex); if (value !== newValue) { setValue(context, ctxIndex, newValue); - dirty = true; + + const flag = getPointers(context, ctxIndex); + 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 { - // 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; + const indexOfEntry = findEntryPositionByProp(context, newProp, ctxIndex); + if (indexOfEntry > 0) { + // it was found at a later point ... just swap the values + const valueToCompare = getValue(context, indexOfEntry); + const flagToCompare = getPointers(context, indexOfEntry); + swapMultiContextEntries(context, ctxIndex, indexOfEntry); + if (valueToCompare !== newValue) { + const initialValue = getInitialValue(context, flagToCompare); + setValue(context, ctxIndex, newValue); + if (initialValue !== newValue) { + setDirty(context, ctxIndex, true); + dirty = true; + } + } + } else { + // we only care to do this if the insertion is in the middle + insertNewMultiProperty(context, ctxIndex, isClassBased, newProp, newValue); + dirty = true; + } } } @@ -310,11 +448,16 @@ export function updateStyleMap(context: StylingContext, styles: {[key: string]: } // 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) + // were not included in the provided styles/classes 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) { + const flag = getPointers(context, ctxIndex); + const isClassBased = (flag & StylingFlags.Class) === StylingFlags.Class; + if (ignoreAllClassUpdates && isClassBased) break; + + const value = getValue(context, ctxIndex); + const doRemoveValue = valueExists(value, isClassBased); + if (doRemoveValue) { setDirty(context, ctxIndex, true); setValue(context, ctxIndex, null); dirty = true; @@ -322,13 +465,19 @@ export function updateStyleMap(context: StylingContext, styles: {[key: string]: ctxIndex += StylingIndex.Size; } - // this means that there are left-over property in the context that + // this means that there are left-over properties 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); + while (propIndex < propLimit) { + const isClassBased = propIndex >= classesStartIndex; + if (ignoreAllClassUpdates && isClassBased) break; + + const adjustedPropIndex = isClassBased ? propIndex - classesStartIndex : propIndex; + const prop = isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex]; + const value: string|boolean = + isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop]; + const flag = StylingFlags.Dirty | (isClassBased ? StylingFlags.Class : StylingFlags.None); + context.push(flag, prop, value); propIndex++; dirty = true; } @@ -339,13 +488,13 @@ export function updateStyleMap(context: StylingContext, styles: {[key: string]: } /** - * 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. + * Sets and resolves a single styling property/value on the provided `StylingContext` so + * that they can be applied to the element once `renderStyling` 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). + * Note that prop-level styling values are considered higher priority than any styling that + * has been applied using `updateStylingMap`, therefore, when styling values are rendered + * then any styles/classes 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. @@ -353,7 +502,7 @@ export function updateStyleMap(context: StylingContext, styles: {[key: string]: * @param value The CSS style value that will be assigned */ export function updateStyleProp( - context: StylingContext, index: number, value: string | null): void { + context: StylingContext, index: number, value: string | boolean | null): void { const singleIndex = StylingIndex.SingleStylesStartPosition + index * StylingIndex.Size; const currValue = getValue(context, singleIndex); const currFlag = getPointers(context, singleIndex); @@ -370,8 +519,10 @@ export function updateStyleProp( let multiDirty = false; let singleDirty = true; + const isClassBased = (currFlag & StylingFlags.Class) === StylingFlags.Class; + // only when the value is set to `null` should the multi-value get flagged - if (value == null && valueForMulti) { + if (!valueExists(value, isClassBased) && valueExists(valueForMulti, isClassBased)) { multiDirty = true; singleDirty = false; } @@ -384,12 +535,28 @@ export function updateStyleProp( } /** - * Renders all queued styles using a renderer onto the given element. + * This method will toggle the referenced CSS class (by the provided index) + * within the given context. + * + * @param context The styling context that will be updated with the + * newly provided class value. + * @param index The index of the CSS class which is being updated. + * @param addOrRemove Whether or not to add or remove the CSS class + */ +export function updateClassProp( + context: StylingContext, index: number, addOrRemove: boolean): void { + const adjustedIndex = index + context[StylingIndex.ClassOffsetPosition]; + updateStyleProp(context, adjustedIndex, addOrRemove); +} + +/** + * Renders all queued styling 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. + * using `updateStylingMap`) and any classes (that have been applied using + * `updateStyleProp`) onto the provided element using the provided renderer. + * Just before the styles/classes are rendered a final key/value style map + * will be assembled (if `styleStore` or `classStore` are provided). * * @param lElement the element that the styles will be rendered on * @param context The styling context that will be used to determine @@ -397,13 +564,14 @@ export function updateStyleProp( * @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'}`. + * @param classStore if provided, the updated class values will be applied + * to this key/value map instead of being renderered via the renderer. */ -export function renderStyles( - lElement: LElementNode, context: StylingContext, renderer: Renderer3, - styleStore?: {[key: string]: any}) { +export function renderStyling( + context: StylingContext, renderer: Renderer3, styleStore?: {[key: string]: any}, + classStore?: {[key: string]: boolean}) { if (isContextDirty(context)) { - const native = lElement.native; + const native = context[StylingIndex.ElementPosition] !.native; const multiStartIndex = getMultiStartIndex(context); for (let i = StylingIndex.SingleStylesStartPosition; i < context.length; i += StylingIndex.Size) { @@ -412,27 +580,35 @@ export function renderStyles( const prop = getProp(context, i); const value = getValue(context, i); const flag = getPointers(context, i); + const isClassBased = flag & StylingFlags.Class ? true : false; const isInSingleRegion = i < multiStartIndex; - let styleToApply: string|null = value; + let valueToApply: string|boolean|null = value; - // STYLE DEFER CASE 1: Use a multi value instead of a null single value + // VALUE 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) { + if (isInSingleRegion && !valueExists(valueToApply, isClassBased)) { // single values ALWAYS have a reference to a multi index const multiIndex = getMultiOrSingleIndex(flag); - styleToApply = getValue(context, multiIndex); + valueToApply = getValue(context, multiIndex); } - // STYLE DEFER CASE 2: Use the initial value if all else fails (is null) + // VALUE DEFER CASE 2: Use the initial value if all else fails (is falsy) // 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); + // note that this should always be a falsy check since `false` is used + // for both class and style comparisons (styles can't be false and false + // classes are turned off and should therefore defer to their initial values) + if (!valueExists(valueToApply, isClassBased)) { + valueToApply = getInitialValue(context, flag); } - setStyle(native, prop, styleToApply, renderer, styleStore); + if (isClassBased) { + setClass(native, prop, valueToApply ? true : false, renderer, classStore); + } else { + setStyle(native, prop, valueToApply as string | null, renderer, styleStore); + } setDirty(context, i, false); } } @@ -443,7 +619,7 @@ export function renderStyles( /** * This function renders a given CSS prop/value entry using the - * provided renderer. If a `styleStore` value is provided then + * provided renderer. If a `store` value is provided then * that will be used a render context instead of the provided * renderer. * @@ -451,23 +627,51 @@ export function renderStyles( * @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 + * @param store 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 { + store?: {[key: string]: any}) { + if (store) { + store[prop] = value; + } else if (value) { ngDevMode && ngDevMode.rendererSetStyle++; isProceduralRenderer(renderer) ? renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) : native['style'].setProperty(prop, value); + } else { + ngDevMode && ngDevMode.rendererRemoveStyle++; + isProceduralRenderer(renderer) ? + renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) : + native['style'].removeProperty(prop); + } +} + +/** + * This function renders a given CSS class value using the provided + * renderer (by adding or removing it from the provided element). + * If a `store` 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 store an optional key/value map that will be used as a context to render styles on + */ +function setClass( + native: any, className: string, add: boolean, renderer: Renderer3, + store?: {[key: string]: boolean}) { + if (store) { + store[className] = add; + } else if (add) { + ngDevMode && ngDevMode.rendererAddClass++; + isProceduralRenderer(renderer) ? renderer.addClass(native, className) : + native['classList'].add(className); + } else { + ngDevMode && ngDevMode.rendererRemoveClass++; + isProceduralRenderer(renderer) ? renderer.removeClass(native, className) : + native['classList'].remove(className); } } @@ -487,8 +691,14 @@ function isDirty(context: StylingContext, index: number): boolean { return ((context[adjustedIndex] as number) & StylingFlags.Dirty) == StylingFlags.Dirty; } +function isClassBased(context: StylingContext, index: number): boolean { + const adjustedIndex = + index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index; + return ((context[adjustedIndex] as number) & StylingFlags.Class) == StylingFlags.Class; +} + function pointers(configFlag: number, staticIndex: number, dynamicIndex: number) { - return (configFlag & StylingFlags.Dirty) | (staticIndex << StylingFlags.BitCountSize) | + return (configFlag & StylingFlags.BitMask) | (staticIndex << StylingFlags.BitCountSize) | (dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize)); } @@ -515,7 +725,7 @@ function setProp(context: StylingContext, index: number, prop: string) { context[index + StylingIndex.PropertyOffset] = prop; } -function setValue(context: StylingContext, index: number, value: string | null) { +function setValue(context: StylingContext, index: number, value: string | null | boolean) { context[index + StylingIndex.ValueOffset] = value; } @@ -531,8 +741,8 @@ function getPointers(context: StylingContext, index: number): number { return context[adjustedIndex] as number; } -function getValue(context: StylingContext, index: number): string|null { - return context[index + StylingIndex.ValueOffset] as string | null; +function getValue(context: StylingContext, index: number): string|boolean|null { + return context[index + StylingIndex.ValueOffset] as string | boolean | null; } function getProp(context: StylingContext, index: number): string { @@ -597,20 +807,23 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition: if (singleIndex > 0) { const singleFlag = getPointers(context, singleIndex); const initialIndexForSingle = getInitialIndex(singleFlag); - const updatedFlag = pointers( - isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None, - initialIndexForSingle, i); + const flagValue = (isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None) | + (isClassBased(context, singleIndex) ? StylingFlags.Class : StylingFlags.None); + const updatedFlag = pointers(flagValue, initialIndexForSingle, i); setFlag(context, singleIndex, updatedFlag); } } } function insertNewMultiProperty( - context: StylingContext, index: number, name: string, value: string): void { + context: StylingContext, index: number, classBased: boolean, name: string, + value: string | boolean): void { const doShift = index < context.length; // prop does not exist in the list, add it in - context.splice(index, 0, StylingFlags.Dirty, name, value); + context.splice( + index, 0, StylingFlags.Dirty | (classBased ? StylingFlags.Class : StylingFlags.None), name, + value); if (doShift) { // because the value was inserted midway into the array then we @@ -619,3 +832,10 @@ function insertNewMultiProperty( updateSinglePointerValues(context, index + StylingIndex.Size); } } + +function valueExists(value: string | null | boolean, isClassBased?: boolean) { + if (isClassBased) { + return value ? true : false; + } + return value !== null; +} diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index 3688f68c89..22e73caeb2 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -5,6 +5,10 @@ * 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 {assertLessThan} from './assert'; +import {LElementNode} from './interfaces/node'; +import {HEADER_OFFSET, LViewData} from './interfaces/view'; + /** * Must use this method for CD (instead of === ) since NaN !== NaN @@ -56,3 +60,27 @@ export function flatten(list: any[]): any[] { return result; } + +/** Retrieves a value from any `LViewData`. */ +export function loadInternal(index: number, arr: LViewData): T { + ngDevMode && assertDataInRangeInternal(index + HEADER_OFFSET, arr); + return arr[index + HEADER_OFFSET]; +} + +export function assertDataInRangeInternal(index: number, arr: any[]) { + assertLessThan(index, arr ? arr.length : 0, 'index expected to be a valid data index'); +} + +/** Retrieves an element value from the provided `viewData`. + * + * Elements that are read may be wrapped in a style context, + * therefore reading the value may involve unwrapping that. + */ +export function loadElementInternal(index: number, arr: LViewData): LElementNode { + const value = loadInternal(index, arr); + return readElementValue(value); +} + +export function readElementValue(value: LElementNode | any[]): LElementNode { + return (Array.isArray(value) ? (value as any as any[])[0] : value) as LElementNode; +} diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index f2c007b7b3..5914081fe2 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -191,6 +191,9 @@ { "name": "namespaceHTML" }, + { + "name": "readElementValue" + }, { "name": "refreshChildComponents" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index ba013de043..86469d9074 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -119,6 +119,9 @@ { "name": "RecordViewTuple" }, + { + "name": "RendererStyleFlags3" + }, { "name": "SANITIZER" }, @@ -206,6 +209,15 @@ { "name": "_c16" }, + { + "name": "_c17" + }, + { + "name": "_c18" + }, + { + "name": "_c19" + }, { "name": "_c2" }, @@ -260,6 +272,9 @@ { "name": "addToViewTree" }, + { + "name": "allocStylingContext" + }, { "name": "appendChild" }, @@ -332,6 +347,9 @@ { "name": "createRootContext" }, + { + "name": "createStylingContextTemplate" + }, { "name": "createTNode" }, @@ -344,6 +362,9 @@ { "name": "createViewQuery" }, + { + "name": "currentElementNode" + }, { "name": "defineComponent" }, @@ -378,7 +399,7 @@ "name": "domRendererFactory3" }, { - "name": "elementClassNamed" + "name": "elementClassProp" }, { "name": "elementEnd" @@ -389,6 +410,12 @@ { "name": "elementStart" }, + { + "name": "elementStyling" + }, + { + "name": "elementStylingApply" + }, { "name": "enterView" }, @@ -446,9 +473,21 @@ { "name": "getCurrentSanitizer" }, + { + "name": "getInitialIndex" + }, + { + "name": "getInitialValue" + }, { "name": "getLViewChild" }, + { + "name": "getMultiOrSingleIndex" + }, + { + "name": "getMultiStartIndex" + }, { "name": "getNextLNode" }, @@ -479,12 +518,18 @@ { "name": "getParentState" }, + { + "name": "getPointers" + }, { "name": "getPreviousIndex" }, { "name": "getPreviousOrParentNode" }, + { + "name": "getProp" + }, { "name": "getRenderFlags" }, @@ -494,6 +539,9 @@ { "name": "getRootView" }, + { + "name": "getStylingContext" + }, { "name": "getSymbolIterator" }, @@ -506,6 +554,9 @@ { "name": "getTypeNameForDebugging$1" }, + { + "name": "getValue" + }, { "name": "hostElement" }, @@ -536,6 +587,9 @@ { "name": "invertObject" }, + { + "name": "isContextDirty" + }, { "name": "isCssClassMatching" }, @@ -545,6 +599,9 @@ { "name": "isDifferent" }, + { + "name": "isDirty" + }, { "name": "isJsObject" }, @@ -575,6 +632,12 @@ { "name": "load" }, + { + "name": "loadElement" + }, + { + "name": "loadElementInternal" + }, { "name": "loadInternal" }, @@ -602,6 +665,9 @@ { "name": "notImplemented" }, + { + "name": "pointers" + }, { "name": "projectionNodeStack" }, @@ -626,6 +692,9 @@ { "name": "queueViewHooks" }, + { + "name": "readElementValue" + }, { "name": "refreshChildComponents" }, @@ -653,6 +722,9 @@ { "name": "renderEmbeddedTemplate" }, + { + "name": "renderStyling" + }, { "name": "resetApplicationState" }, @@ -677,9 +749,21 @@ { "name": "searchMatchesQueuedForCreation" }, + { + "name": "setClass" + }, + { + "name": "setContextDirty" + }, { "name": "setCurrentInjector" }, + { + "name": "setDirty" + }, + { + "name": "setFlag" + }, { "name": "setHostBindings" }, @@ -689,9 +773,18 @@ { "name": "setInputsFromAttrs" }, + { + "name": "setProp" + }, + { + "name": "setStyle" + }, { "name": "setUpAttributes" }, + { + "name": "setValue" + }, { "name": "storeCleanupFn" }, @@ -725,9 +818,18 @@ { "name": "trackByIdentity" }, + { + "name": "updateClassProp" + }, + { + "name": "updateStyleProp" + }, { "name": "updateViewQuery" }, + { + "name": "valueExists" + }, { "name": "viewAttached" }, diff --git a/packages/core/test/render3/compiler_canonical/elements_spec.ts b/packages/core/test/render3/compiler_canonical/elements_spec.ts index 06ee01368b..1d1c8dd41a 100644 --- a/packages/core/test/render3/compiler_canonical/elements_spec.ts +++ b/packages/core/test/render3/compiler_canonical/elements_spec.ts @@ -15,7 +15,6 @@ import {ComponentDefInternal, InitialStylingFlags} from '../../../src/render3/in import {ComponentFixture, renderComponent, toHtml} from '../render_util'; - /// See: `normative.md` describe('elements', () => { // Saving type as $any$, etc to simplify testing for compiler, as types aren't saved @@ -271,6 +270,7 @@ describe('elements', () => { }); it('should bind to a specific class', () => { + const c1: (string | InitialStylingFlags | boolean)[] = ['foo']; type $MyComponent$ = MyComponent; @Component({selector: 'my-component', template: `
`}) @@ -283,10 +283,13 @@ 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(null, c1); + $r3$.ɵe(); } if (rf & 2) { - $r3$.ɵkn(0, 'foo', $r3$.ɵb(ctx.someFlag)); + $r3$.ɵcp(0, 0, ctx.someFlag); + $r3$.ɵsa(0); } } }); @@ -320,13 +323,13 @@ describe('elements', () => { template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { if (rf & 1) { $r3$.ɵE(0, 'div'); - $r3$.ɵs(1, c0); + $r3$.ɵs(c0); $r3$.ɵe(); } if (rf & 2) { - $r3$.ɵsp(1, 0, ctx.someColor); - $r3$.ɵsp(1, 1, ctx.someWidth, 'px'); - $r3$.ɵsa(1); + $r3$.ɵsp(0, 0, ctx.someColor); + $r3$.ɵsp(0, 1, ctx.someWidth, 'px'); + $r3$.ɵsa(0); } } }); @@ -353,7 +356,9 @@ describe('elements', () => { it('should bind to many and keep order', () => { type $MyComponent$ = MyComponent; - const c0 = ['color', InitialStylingFlags.INITIAL_STYLES, 'color', 'red']; + const c0 = ['color', InitialStylingFlags.VALUES_MODE, 'color', 'red']; + const c1 = ['foo']; + @Component({ selector: 'my-component', template: @@ -369,12 +374,13 @@ describe('elements', () => { template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { if (rf & 1) { $r3$.ɵE(0, 'div'); - $r3$.ɵs(1, c0); + $r3$.ɵs(c0, c1); $r3$.ɵe(); } if (rf & 2) { $r3$.ɵp(0, 'id', $r3$.ɵb(ctx.someString + 1)); - $r3$.ɵkn(0, 'foo', $r3$.ɵb(ctx.someString == 'initial')); + $r3$.ɵcp(0, 0, ctx.someString == 'initial'); + $r3$.ɵsa(0); } } }); @@ -406,13 +412,12 @@ describe('elements', () => { template: function StyleComponent_Template(rf: $RenderFlags$, ctx: $StyleComponent$) { if (rf & 1) { $r3$.ɵE(0, 'div'); - $r3$.ɵs(1); + $r3$.ɵs(); $r3$.ɵe(); } if (rf & 2) { - $r3$.ɵk(0, $r3$.ɵb(ctx.classExp)); - $r3$.ɵsm(1, ctx.styleExp); - $r3$.ɵsa(1); + $r3$.ɵsm(0, ctx.styleExp, ctx.classExp); + $r3$.ɵsa(0); } } }); diff --git a/packages/core/test/render3/compiler_canonical/sanitize_spec.ts b/packages/core/test/render3/compiler_canonical/sanitize_spec.ts index ea57ab7674..43fea1b77b 100644 --- a/packages/core/test/render3/compiler_canonical/sanitize_spec.ts +++ b/packages/core/test/render3/compiler_canonical/sanitize_spec.ts @@ -44,17 +44,17 @@ describe('compiler sanitization', () => { template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { if (rf & 1) { $r3$.ɵE(0, 'div'); - $r3$.ɵs(1, ['background-image']); + $r3$.ɵs(['background-image']); $r3$.ɵe(); - $r3$.ɵEe(2, 'img'); + $r3$.ɵEe(1, 'img'); } if (rf & 2) { $r3$.ɵp(0, 'innerHTML', $r3$.ɵb(ctx.innerHTML), $r3$.ɵsanitizeHtml); $r3$.ɵp(0, 'hidden', $r3$.ɵb(ctx.hidden)); - $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); + $r3$.ɵsp(0, 0, ctx.style, $r3$.ɵsanitizeStyle); + $r3$.ɵsa(0); + $r3$.ɵp(1, 'src', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl); + $r3$.ɵa(1, 'srcset', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl); } } }); diff --git a/packages/core/test/render3/exports_spec.ts b/packages/core/test/render3/exports_spec.ts index 824b76e6c6..8db0c81559 100644 --- a/packages/core/test/render3/exports_spec.ts +++ b/packages/core/test/render3/exports_spec.ts @@ -7,8 +7,9 @@ */ import {defineComponent, defineDirective} from '../../src/render3/index'; -import {bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, load, text, textBinding} from '../../src/render3/instructions'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; +import {bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassProp, elementEnd, elementProperty, elementStart, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, load, text, textBinding} from '../../src/render3/instructions'; +import {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/definition'; + import {ComponentFixture, createComponent, renderToHtml} from './render_util'; describe('exports', () => { @@ -212,13 +213,15 @@ describe('exports', () => { function Template(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'div'); + elementStyling(null, [InitialStylingFlags.VALUES_MODE, 'red', true]); elementEnd(); elementStart(1, 'input', ['type', 'checkbox', 'checked', 'true'], ['myInput', '']); elementEnd(); } const tmp = load(2) as any; if (rf & RenderFlags.Update) { - elementClassNamed(0, 'red', bind(tmp.checked)); + elementClassProp(0, 0, tmp.checked); + elementStylingApply(0); } } diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index e8a9339480..2e3f74a812 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -10,7 +10,7 @@ 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, elementStyleProp, elementStyling, elementStylingApply, interpolation1, renderTemplate, text, textBinding} from '../../src/render3/instructions'; +import {bind, container, element, elementAttribute, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, elementStylingMap, 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'; @@ -23,13 +23,13 @@ import {ComponentFixture, TemplateFixture} from './render_util'; describe('instructions', () => { function createAnchor() { elementStart(0, 'a'); - elementStyling(1); + elementStyling(); elementEnd(); } function createDiv(initialStyles?: (string | number)[]) { elementStart(0, 'div'); - elementStyling(1, initialStyles && Array.isArray(initialStyles) ? initialStyles : null); + elementStyling(initialStyles && Array.isArray(initialStyles) ? initialStyles : null); elementEnd(); } @@ -193,15 +193,15 @@ describe('instructions', () => { it('should use sanitizer function', () => { const t = new TemplateFixture(() => { return createDiv(['background-image']); }); t.update(() => { - elementStyleProp(1, 0, 'url("http://server")', sanitizeStyle); - elementStylingApply(1); + elementStyleProp(0, 0, 'url("http://server")', sanitizeStyle); + elementStylingApply(0); }); // nothing is set because sanitizer suppresses it. expect(t.html).toEqual('
'); t.update(() => { - elementStyleProp(1, 0, bypassSanitizationTrustStyle('url("http://server")'), sanitizeStyle); - elementStylingApply(1); + elementStyleProp(0, 0, bypassSanitizationTrustStyle('url("http://server")'), sanitizeStyle); + elementStylingApply(0); }); expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image')) .toEqual('url("http://server")'); @@ -211,25 +211,33 @@ describe('instructions', () => { describe('elementStyleMap', () => { function createDivWithStyle() { elementStart(0, 'div'); - elementStyling(1, ['height', InitialStylingFlags.INITIAL_STYLES, 'height', '10px']); + elementStyling(['height', InitialStylingFlags.VALUES_MODE, 'height', '10px']); elementEnd(); } it('should add style', () => { const fixture = new TemplateFixture(createDivWithStyle); fixture.update(() => { - elementStyle(1, {'background-color': 'red'}); - elementStylingApply(1); + elementStylingMap(0, {'background-color': 'red'}); + elementStylingApply(0); }); expect(fixture.html).toEqual('
'); }); }); describe('elementClass', () => { + function createDivWithStyling() { + elementStart(0, 'div'); + elementStyling(); + elementEnd(); + } it('should add class', () => { - const fixture = new TemplateFixture(createDiv); - fixture.update(() => elementClass(0, 'multiple classes')); + const fixture = new TemplateFixture(createDivWithStyling); + fixture.update(() => { + elementStylingMap(0, null, 'multiple classes'); + elementStylingApply(0); + }); expect(fixture.html).toEqual('
'); }); }); diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index e334b2a96c..1f1182e012 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -9,7 +9,8 @@ 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, 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 {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassProp, 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 {InitialStylingFlags} from '../../src/render3/interfaces/definition'; import {HEADER_OFFSET} from '../../src/render3/interfaces/view'; import {sanitizeUrl} from '../../src/sanitization/sanitization'; import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; @@ -747,12 +748,12 @@ describe('render3 integration test', () => { function Template(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'span'); - elementStyling(1, ['border-color']); + elementStyling(['border-color']); elementEnd(); } if (rf & RenderFlags.Update) { - elementStyleProp(1, 0, ctx); - elementStylingApply(1); + elementStyleProp(0, 0, ctx); + elementStylingApply(0); } } @@ -766,12 +767,12 @@ describe('render3 integration test', () => { function Template(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'span'); - elementStyling(1, ['font-size']); + elementStyling(['font-size']); elementEnd(); } if (rf & RenderFlags.Update) { - elementStyleProp(1, 0, ctx, 'px'); - elementStylingApply(1); + elementStyleProp(0, 0, ctx, 'px'); + elementStylingApply(0); } } @@ -787,10 +788,12 @@ describe('render3 integration test', () => { function Template(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'span'); + elementStyling(null, ['active']); elementEnd(); } if (rf & RenderFlags.Update) { - elementClassNamed(0, 'active', bind(ctx)); + elementClassProp(0, 0, ctx); + elementStylingApply(0); } } @@ -809,11 +812,14 @@ describe('render3 integration test', () => { it('should work correctly with existing static classes', () => { function Template(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - elementStart(0, 'span', ['class', 'existing']); + elementStart(0, 'span'); + elementStyling( + null, ['existing', 'active', InitialStylingFlags.VALUES_MODE, 'existing', true]); elementEnd(); } if (rf & RenderFlags.Update) { - elementClassNamed(0, 'active', bind(ctx)); + elementClassProp(0, 1, ctx); + elementStylingApply(0); } } diff --git a/packages/core/test/render3/styling_spec.ts b/packages/core/test/render3/styling_spec.ts index 0522d3a424..c7c558d6f1 100644 --- a/packages/core/test/render3/styling_spec.ts +++ b/packages/core/test/render3/styling_spec.ts @@ -5,37 +5,64 @@ * 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 {elementEnd, elementStart, elementStyleProp, elementStyling, elementStylingApply, elementStylingMap} 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 {StylingContext, StylingFlags, StylingIndex, allocStylingContext, createStylingContextTemplate, isContextDirty, renderStyling as _renderStyling, setContextDirty, updateClassProp, updateStyleProp, updateStylingMap} from '../../src/render3/styling'; import {renderToHtml} from './render_util'; describe('styling', () => { - let lElement: LElementNode|null = null; - beforeEach(() => { lElement = { native: {} } as any; }); + let element: LElementNode|null = null; + beforeEach(() => { element = {} as any; }); - function initContext(styles?: (number | string)[]): StylingContext { - return allocStylingContext(createStylingContextTemplate(styles)); + function initContext( + styles?: (number | string)[] | null, + classes?: (string | number | boolean)[] | null): StylingContext { + return allocStylingContext(element, createStylingContextTemplate(styles, classes)); } function renderStyles(context: StylingContext, renderer?: Renderer3) { const styles: {[key: string]: any} = {}; - _renderStyles(lElement !, context, (renderer || {}) as Renderer3, styles); + _renderStyling(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); + _renderStyling(context, (renderer || {}) as Renderer3, styles); return styles; }; } - function clean(a: number = 0, b: number = 0): number { + function trackClassesFactory() { + const classes: {[className: string]: boolean} = {}; + return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} { + _renderStyling(context, (renderer || {}) as Renderer3, {}, classes); + return classes; + }; + } + + function trackStylesAndClasses() { + const classes: {[className: string]: boolean} = {}; + const styles: {[prop: string]: any} = {}; + return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} { + _renderStyling(context, (renderer || {}) as Renderer3, styles, classes); + return [styles, classes]; + }; + } + + function updateClasses(context: StylingContext, classes: string | {[key: string]: any} | null) { + updateStylingMap(context, null, classes); + } + + function cleanStyle(a: number = 0, b: number = 0): number { return _clean(a, b, false); } + + function cleanClass(a: number, b: number) { return _clean(a, b, true); } + + function _clean(a: number = 0, b: number = 0, isClassBased: boolean): number { let num = 0; if (a) { num |= a << StylingFlags.BitCountSize; @@ -43,614 +70,913 @@ describe('styling', () => { if (b) { num |= b << (StylingFlags.BitCountSize + StylingIndex.BitCountSize); } + if (isClassBased) { + num |= StylingFlags.Class; + } return num; } - function dirty(a: number = 0, b: number = 0): number { return clean(a, b) | StylingFlags.Dirty; } + function _dirty(a: number = 0, b: number = 0, isClassBased: boolean): number { + return _clean(a, b, isClassBased) | StylingFlags.Dirty; + } - describe('createStylingContextTemplate', () => { - it('should initialize empty template', () => { - const template = createStylingContextTemplate(); - expect(template).toEqual([ - [null], - clean(0, 2), - ]); + function dirtyStyle(a: number = 0, b: number = 0): number { + return _dirty(a, b, false) | StylingFlags.Dirty; + } + + function dirtyClass(a: number, b: number) { return _dirty(a, b, true); } + + describe('styles', () => { + describe('createStylingContextTemplate', () => { + it('should initialize empty template', () => { + const template = initContext(); + expect(template).toEqual([element, [null], cleanStyle(0, 5), 0, null]); + }); + + it('should initialize static styles', () => { + const template = + initContext([InitialStylingFlags.VALUES_MODE, 'color', 'red', 'width', '10px']); + expect(template).toEqual([ + element, + [null, 'red', '10px'], + dirtyStyle(0, 11), // + 0, + null, + + // #5 + cleanStyle(1, 11), + 'color', + null, + + // #8 + cleanStyle(2, 14), + 'width', + null, + + // #11 + dirtyStyle(1, 5), + 'color', + null, + + // #14 + dirtyStyle(2, 8), + 'width', + null, + ]); + }); }); - 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), // + 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([ + 'width', 'height', 'opacity', // + InitialStylingFlags.VALUES_MODE, 'width', '100px', 'height', '100px', 'opacity', + '0.5' + ]); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementStylingMap(0, ctx.myStyles); + elementStyleProp(0, 0, ctx.myWidth); + elementStylingApply(0); + } + } - // #2 - clean(1, 8), - 'color', - null, + expect(renderToHtml(Template, { + myStyles: {width: '200px', height: '200px'}, + myWidth: '300px' + })).toEqual(''); - // #5 - clean(2, 11), - 'width', - null, + expect(renderToHtml(Template, {myStyles: {width: '200px', height: null}, myWidth: null})) + .toEqual(''); + }); + }); - // #8 - dirty(1, 2), - 'color', - null, + describe('helper functions', () => { + it('should build a list of multiple styling values', () => { + const getStyles = trackStylesFactory(); + const stylingContext = initContext(); + updateStylingMap(stylingContext, { + width: '100px', + height: '100px', + }); + updateStylingMap(stylingContext, {height: '200px'}); + expect(getStyles(stylingContext)).toEqual({width: null, height: '200px'}); + }); - // #11 - dirty(2, 5), - 'width', - null, - ]); + it('should evaluate the delta between style changes when rendering occurs', () => { + const stylingContext = + initContext(['width', 'height', InitialStylingFlags.VALUES_MODE, 'width', '100px']); + updateStylingMap(stylingContext, { + height: '200px', + }); + expect(renderStyles(stylingContext)).toEqual({width: '100px', height: '200px'}); + expect(renderStyles(stylingContext)).toEqual({}); + updateStylingMap(stylingContext, { + width: '100px', + height: '100px', + }); + expect(renderStyles(stylingContext)).toEqual({height: '100px'}); + updateStyleProp(stylingContext, 1, '100px'); + expect(renderStyles(stylingContext)).toEqual({}); + updateStylingMap(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']); + updateStylingMap(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(); + + updateStylingMap(stylingContext, { + width: '100px', + height: '100px', + }); + expect(isContextDirty(stylingContext)).toBeTruthy(); + + setContextDirty(stylingContext, false); + + updateStylingMap(stylingContext, { + width: '100px', + height: '100px', + }); + expect(isContextDirty(stylingContext)).toBeFalsy(); + + updateStylingMap(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']); + updateStylingMap(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', InitialStylingFlags.VALUES_MODE, 'width', '100px', 'height', + '100px', 'opacity', '0' + ]); + + expect(getStyles(stylingContext)).toEqual({ + width: '100px', + height: '100px', + opacity: '0', + }); + + updateStylingMap(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', + }); + + updateStylingMap(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(); + + updateStylingMap(stylingContext, {width: '100px', height: '100px'}); + + expect(stylingContext).toEqual([ + element, + [null], + dirtyStyle(0, 11), // + 2, + null, + + // #5 + cleanStyle(0, 11), + 'width', + null, + + // #8 + cleanStyle(0, 14), + 'height', + null, + + // #11 + dirtyStyle(0, 5), + 'width', + '100px', + + // #14 + dirtyStyle(0, 8), + 'height', + '100px', + ]); + + getStyles(stylingContext); + updateStylingMap(stylingContext, {width: '200px', opacity: '0'}); + + expect(stylingContext).toEqual([ + element, + [null], + dirtyStyle(0, 11), // + 2, + null, + + // #5 + cleanStyle(0, 11), + 'width', + null, + + // #8 + cleanStyle(0, 17), + 'height', + null, + + // #11 + dirtyStyle(0, 5), + 'width', + '200px', + + // #14 + dirtyStyle(), + 'opacity', + '0', + + // #17 + dirtyStyle(0, 8), + 'height', + null, + ]); + + getStyles(stylingContext); + expect(stylingContext).toEqual([ + element, + [null], + cleanStyle(0, 11), // + 2, + null, + + // #5 + cleanStyle(0, 11), + 'width', + null, + + // #8 + cleanStyle(0, 17), + 'height', + null, + + // #11 + cleanStyle(0, 5), + 'width', + '200px', + + // #14 + cleanStyle(), + 'opacity', + '0', + + // #17 + cleanStyle(0, 8), + 'height', + null, + ]); + + updateStylingMap(stylingContext, {width: null}); + updateStyleProp(stylingContext, 0, '300px'); + + expect(stylingContext).toEqual([ + element, + [null], + dirtyStyle(0, 11), // + 2, + null, + + // #5 + dirtyStyle(0, 11), + 'width', + '300px', + + // #8 + cleanStyle(0, 17), + 'height', + null, + + // #11 + cleanStyle(0, 5), + 'width', + null, + + // #14 + dirtyStyle(), + 'opacity', + null, + + // #17 + cleanStyle(0, 8), + 'height', + null, + ]); + + getStyles(stylingContext); + + updateStyleProp(stylingContext, 0, null); + expect(stylingContext).toEqual([ + element, + [null], + dirtyStyle(0, 11), // + 2, + null, + + // #5 + dirtyStyle(0, 11), + 'width', + null, + + // #8 + cleanStyle(0, 17), + 'height', + null, + + // #11 + cleanStyle(0, 5), + 'width', + null, + + // #14 + cleanStyle(), + 'opacity', + null, + + // #17 + cleanStyle(0, 8), + '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(); + + updateStylingMap(stylingContext, {width: '100px', height: '100px', opacity: '0.5'}); + + expect(stylingContext).toEqual([ + element, + [null], + dirtyStyle(0, 8), // + 1, + null, + + // #5 + cleanStyle(0, 17), + 'lineHeight', + null, + + // #8 + dirtyStyle(), + 'width', + '100px', + + // #11 + dirtyStyle(), + 'height', + '100px', + + // #14 + dirtyStyle(), + 'opacity', + '0.5', + + // #17 + cleanStyle(0, 5), + 'lineHeight', + null, + ]); + + getStyles(stylingContext); + + updateStylingMap(stylingContext, {}); + expect(stylingContext).toEqual([ + element, + [null], + dirtyStyle(0, 8), // + 1, + null, + + // #5 + cleanStyle(0, 17), + 'lineHeight', + null, + + // #8 + dirtyStyle(), + 'width', + null, + + // #11 + dirtyStyle(), + 'height', + null, + + // #14 + dirtyStyle(), + 'opacity', + null, + + // #17 + cleanStyle(0, 5), + 'lineHeight', + null, + ]); + + getStyles(stylingContext); + updateStylingMap(stylingContext, { + borderWidth: '5px', + }); + + expect(stylingContext).toEqual([ + element, + [null], + dirtyStyle(0, 8), // + 1, + null, + + // #5 + cleanStyle(0, 20), + 'lineHeight', + null, + + // #8 + dirtyStyle(), + 'borderWidth', + '5px', + + // #11 + cleanStyle(), + 'width', + null, + + // #14 + cleanStyle(), + 'height', + null, + + // #17 + cleanStyle(), + 'opacity', + null, + + // #20 + cleanStyle(0, 5), + 'lineHeight', + null, + ]); + + updateStyleProp(stylingContext, 0, '200px'); + + expect(stylingContext).toEqual([ + element, + [null], + dirtyStyle(0, 8), // + 1, + null, + + // #5 + dirtyStyle(0, 20), + 'lineHeight', + '200px', + + // #8 + dirtyStyle(), + 'borderWidth', + '5px', + + // #11 + cleanStyle(), + 'width', + null, + + // #14 + cleanStyle(), + 'height', + null, + + // #17 + cleanStyle(), + 'opacity', + null, + + // #20 + cleanStyle(0, 5), + 'lineHeight', + null, + ]); + + updateStylingMap(stylingContext, {borderWidth: '15px', borderColor: 'red'}); + + expect(stylingContext).toEqual([ + element, + [null], + dirtyStyle(0, 8), // + 1, + null, + + // #5 + dirtyStyle(0, 23), + 'lineHeight', + '200px', + + // #8 + dirtyStyle(), + 'borderWidth', + '15px', + + // #11 + dirtyStyle(), + 'borderColor', + 'red', + + // #14 + cleanStyle(), + 'width', + null, + + // #17 + cleanStyle(), + 'height', + null, + + // #20 + cleanStyle(), + 'opacity', + null, + + // #23 + cleanStyle(0, 5), + 'lineHeight', + null, + ]); + }); + + it('should render all data as not being dirty after the styles are built', () => { + const getStyles = trackStylesFactory(); + const stylingContext = initContext(['height']); + + updateStylingMap(stylingContext, { + width: '100px', + }); + + updateStyleProp(stylingContext, 0, '200px'); + + expect(stylingContext).toEqual([ + element, + [null], + dirtyStyle(0, 8), // + 1, + null, + + // #5 + dirtyStyle(0, 11), + 'height', + '200px', + + // #5 + dirtyStyle(), + 'width', + '100px', + + // #11 + cleanStyle(0, 5), + 'height', + null, + ]); + + getStyles(stylingContext); + + expect(stylingContext).toEqual([ + element, + [null], + cleanStyle(0, 8), // + 1, + null, + + // #5 + cleanStyle(0, 11), + 'height', + '200px', + + // #5 + cleanStyle(), + 'width', + '100px', + + // #11 + cleanStyle(0, 5), + 'height', + null, + ]); + }); }); }); - describe('instructions', () => { - it('should handle a combination of initial, multi and singular style values (in that order)', + describe('classes', () => { + it('should initialize with the provided classes', () => { + const template = + initContext(null, [InitialStylingFlags.VALUES_MODE, 'one', true, 'two', true]); + expect(template).toEqual([ + element, [null, true, true], dirtyStyle(0, 11), // + 0, null, + + // #5 + cleanClass(1, 11), 'one', null, + + // #8 + cleanClass(2, 14), 'two', null, + + // #11 + dirtyClass(1, 5), 'one', null, + + // #14 + dirtyClass(2, 8), 'two', null + ]); + }); + + it('should update multi class properties against the static classes', () => { + const getClasses = trackClassesFactory(); + const stylingContext = initContext(null, ['bar']); + expect(getClasses(stylingContext)).toEqual({}); + updateClasses(stylingContext, {foo: true, bar: false}); + expect(getClasses(stylingContext)).toEqual({'foo': true, 'bar': false}); + updateClasses(stylingContext, 'bar'); + expect(getClasses(stylingContext)).toEqual({'foo': false, 'bar': true}); + }); + + it('should update single class properties against the static classes', () => { + const getClasses = trackClassesFactory(); + const stylingContext = + initContext(null, ['bar', 'foo', InitialStylingFlags.VALUES_MODE, 'bar', true]); + expect(getClasses(stylingContext)).toEqual({'bar': true}); + + updateClassProp(stylingContext, 0, true); + updateClassProp(stylingContext, 1, true); + expect(getClasses(stylingContext)).toEqual({'bar': true, 'foo': true}); + + updateClassProp(stylingContext, 0, false); + updateClassProp(stylingContext, 1, false); + expect(getClasses(stylingContext)).toEqual({'bar': true, 'foo': false}); + }); + + it('should understand updating multi-classes using a string-based value while respecting single class-based props', () => { - 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); - } - } + const getClasses = trackClassesFactory(); + const stylingContext = initContext(null, ['guy']); + expect(getClasses(stylingContext)).toEqual({}); - expect(renderToHtml(Template, { - myStyles: {width: '200px', height: '200px'}, - myWidth: '300px' - })).toEqual(''); + updateStylingMap(stylingContext, null, 'foo bar guy'); + expect(getClasses(stylingContext)).toEqual({'foo': true, 'bar': true, 'guy': true}); - expect(renderToHtml(Template, {myStyles: {width: '200px', height: null}, myWidth: null})) - .toEqual(''); + updateStylingMap(stylingContext, null, 'foo man'); + updateClassProp(stylingContext, 0, true); + expect(getClasses(stylingContext)) + .toEqual({'foo': true, 'man': true, 'bar': false, 'guy': true}); }); - }); - - 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'}); + it('should house itself inside the context alongside styling in harmony', () => { + const getStylesAndClasses = trackStylesAndClasses(); + const initialStyles = ['width', 'height', InitialStylingFlags.VALUES_MODE, 'width', '100px']; + const initialClasses = ['wide', 'tall', InitialStylingFlags.VALUES_MODE, 'wide', true]; + const stylingContext = initContext(initialStyles, initialClasses); expect(stylingContext).toEqual([ - [null], - dirty(0, 8), // - - // #2 - clean(0, 8), - 'width', + element, + [null, '100px', true], + dirtyStyle(0, 17), // + 2, null, // #5 - clean(0, 11), - 'height', + cleanStyle(1, 17), + 'width', null, // #8 - dirty(0, 2), - 'width', - '100px', + cleanStyle(0, 20), + 'height', + null, // #11 - dirty(0, 5), - 'height', - '100px', - ]); + cleanClass(2, 23), + 'wide', + null, - getStyles(stylingContext); - updateStyleMap(stylingContext, {width: '200px', opacity: '0'}); + // #14 + cleanClass(0, 26), + 'tall', + null, - expect(stylingContext).toEqual([ - [null], - dirty(0, 8), // - - // #2 - clean(0, 8), + // #17 + dirtyStyle(1, 5), 'width', null, - // #5 - clean(0, 14), + // #20 + cleanStyle(0, 8), 'height', null, + // #23 + dirtyClass(2, 11), + 'wide', + null, + + // #26 + cleanClass(0, 14), + 'tall', + null, + ]); + + expect(getStylesAndClasses(stylingContext)).toEqual([{width: '100px'}, {wide: true}]); + + updateStylingMap(stylingContext, {width: '200px', opacity: '0.5'}, 'tall round'); + expect(stylingContext).toEqual([ + element, + [null, '100px', true], + dirtyStyle(0, 17), // + 2, + 'tall round', + + // #5 + cleanStyle(1, 17), + 'width', + null, + // #8 - dirty(0, 2), + cleanStyle(0, 32), + 'height', + null, + + // #11 + cleanClass(2, 29), + 'wide', + null, + + // #14 + cleanClass(0, 23), + 'tall', + null, + + // #17 + dirtyStyle(1, 5), 'width', '200px', - // #11 - dirty(), + // #20 + dirtyStyle(0, 0), 'opacity', - '0', + '0.5', - // #14 - dirty(0, 5), + // #23 + dirtyClass(0, 14), + 'tall', + true, + + // #26 + dirtyClass(0, 0), + 'round', + true, + + // #29 + cleanClass(2, 11), + 'wide', + null, + + // #32 + cleanStyle(0, 8), '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, + expect(getStylesAndClasses(stylingContext)).toEqual([ + {width: '200px', opacity: '0.5'}, {tall: true, round: true, wide: true} ]); - updateStyleMap(stylingContext, {width: null}); + updateStylingMap(stylingContext, {width: '500px'}, {tall: true, wide: true}); updateStyleProp(stylingContext, 0, '300px'); expect(stylingContext).toEqual([ - [null], - dirty(0, 8), // + element, + [null, '100px', true], + dirtyStyle(0, 17), // + 2, + null, - // #2 - dirty(0, 8), + // #5 + dirtyStyle(1, 17), 'width', '300px', - // #5 - clean(0, 14), - 'height', - null, - // #8 - clean(0, 2), - 'width', + cleanStyle(0, 32), + 'height', null, // #11 - dirty(), - 'opacity', + cleanClass(2, 23), + 'wide', null, // #14 - clean(0, 5), - 'height', + cleanClass(0, 20), + 'tall', null, - ]); - getStyles(stylingContext); - - updateStyleProp(stylingContext, 0, null); - expect(stylingContext).toEqual([ - [null], - dirty(0, 8), // - - // #2 - dirty(0, 8), + // #17 + cleanStyle(1, 5), 'width', + '500px', + + // #20 + cleanClass(0, 14), + 'tall', + true, + + // #23 + cleanClass(2, 11), + 'wide', + true, + + // #26 + dirtyClass(0, 0), + 'round', null, - // #5 - clean(0, 14), - 'height', - null, - - // #8 - clean(0, 2), - 'width', - null, - - // #11 - clean(), + // #29 + dirtyStyle(0, 0), '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), + // #32 + cleanStyle(0, 8), '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, + expect(getStylesAndClasses(stylingContext)).toEqual([ + {width: '300px', opacity: null}, {tall: true, round: false, wide: true} ]); }); });