diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 4ca1f7181e..6fd84a85de 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AttributeMarker, InitialStylingFlags} from '@angular/compiler/src/core'; +import {AttributeMarker} from '@angular/compiler/src/core'; import {setup} from '@angular/compiler/test/aot/test_util'; import {compile, expectEmit} from './mock_compile'; @@ -48,17 +48,15 @@ describe('compiler compliance', () => { // The template should look like this (where IDENT is a wild card for an identifier): const template = ` - const $c1$ = ["title", "Hello"]; - const $c2$ = ["my-app", ${InitialStylingFlags.VALUES_MODE}, "my-app", true]; - const $c3$ = ["cx", "20", "cy", "30", "r", "50"]; + const $c1$ = ["title", "Hello", ${AttributeMarker.Classes}, "my-app"]; + const $c2$ = ["cx", "20", "cy", "30", "r", "50"]; … template: function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵelementStart(0, "div", $c1$); - $r3$.ɵelementStyling($c2$); $r3$.ɵnamespaceSVG(); $r3$.ɵelementStart(1, "svg"); - $r3$.ɵelement(2, "circle", $c3$); + $r3$.ɵelement(2, "circle", $c2$); $r3$.ɵelementEnd(); $r3$.ɵnamespaceHTML(); $r3$.ɵelementStart(3, "p"); @@ -100,13 +98,11 @@ describe('compiler compliance', () => { // The template should look like this (where IDENT is a wild card for an identifier): const template = ` - const $c1$ = ["title", "Hello"]; - const $c2$ = ["my-app", ${InitialStylingFlags.VALUES_MODE}, "my-app", true]; + const $c1$ = ["title", "Hello", ${AttributeMarker.Classes}, "my-app"]; … template: function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵelementStart(0, "div", $c1$); - $r3$.ɵelementStyling($c2$); $r3$.ɵnamespaceMathML(); $r3$.ɵelementStart(1, "math"); $r3$.ɵelement(2, "infinity"); @@ -150,13 +146,11 @@ describe('compiler compliance', () => { // The template should look like this (where IDENT is a wild card for an identifier): const template = ` - const $c1$ = ["title", "Hello"]; - const $c2$ = ["my-app", ${InitialStylingFlags.VALUES_MODE}, "my-app", true]; + const $c1$ = ["title", "Hello", ${AttributeMarker.Classes}, "my-app"]; … template: function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵelementStart(0, "div", $c1$); - $r3$.ɵelementStyling($c2$); $r3$.ɵtext(1, "Hello "); $r3$.ɵelementStart(2, "b"); $r3$.ɵtext(3, "World"); @@ -486,8 +480,8 @@ describe('compiler compliance', () => { const factory = 'factory: function MyComponent_Factory(t) { return new (t || MyComponent)(); }'; const template = ` - const _c0 = ["error"]; - const _c1 = ["background-color"]; + const $e0_classBindings$ = ["error"]; + const $e0_styleBindings$ = ["background-color"]; … MyComponent.ngComponentDef = i0.ɵdefineComponent({type:MyComponent,selectors:[["my-component"]], factory: function MyComponent_Factory(t){ @@ -498,7 +492,7 @@ describe('compiler compliance', () => { template: function MyComponent_Template(rf,ctx){ if (rf & 1) { $r3$.ɵelementStart(0, "div"); - $r3$.ɵelementStyling(_c0, _c1); + $r3$.ɵelementStyling($e0_classBindings$, $e0_styleBindings$); $r3$.ɵelementEnd(); } if (rf & 2) { @@ -1204,7 +1198,7 @@ describe('compiler compliance', () => { } }; const output = ` - const $_c0$ = [1, "ngIf"]; + const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"]; const $_c1$ = ["id", "second"]; function Cmp_div_Template_0(rf, ctx) { if (rf & 1) { $r3$.ɵelementStart(0, "div", $_c1$); @@ -1310,7 +1304,6 @@ describe('compiler compliance', () => { const {source} = compile(files, angularFiles); expectEmit(source, output, 'Invalid content projection instructions generated'); }); - }); describe('queries', () => { diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_directives_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_directives_spec.ts index 055f50be49..b9f96de301 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_directives_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_directives_spec.ts @@ -236,7 +236,7 @@ describe('compiler compliance: directives', () => { const MyComponentDefinition = ` … - const $_c0$ = [1, "ngIf"]; + const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"]; const $_c1$ = ["directiveA", ""]; function MyComponent_ng_container_Template_0(rf, ctx) { if (rf & 1) { diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts index 03afb5932b..32d4232117 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {AttributeMarker} from '@angular/compiler/src/core'; import {setup} from '@angular/compiler/test/aot/test_util'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../compiler/src/compiler'; @@ -393,7 +394,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = ["ngFor", "", 1, "ngForOf"]; + const $_c0$ = ["ngFor", "", ${AttributeMarker.SelectOnly}, "ngForOf"]; /** * @desc d * @meaning m @@ -522,7 +523,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = ["ngFor", "", 1, "ngForOf"]; + const $_c0$ = ["ngFor", "", ${AttributeMarker.SelectOnly}, "ngForOf"]; /** * @desc d * @meaning m @@ -922,7 +923,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = [1, "ngIf"]; + const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"]; const $MSG_EXTERNAL_7679414751795588050$$APP_SPEC_TS__1$ = goog.getMsg(" Some other content {$interpolation} {$startTagDiv} More nested levels with bindings {$interpolation_1} {$closeTagDiv}", { "interpolation": "\uFFFD0\uFFFD", "startTagDiv": "\uFFFD#3\uFFFD", @@ -976,7 +977,7 @@ describe('i18n support in the view compiler', () => { const output = String.raw ` const $_c0$ = ["src", "logo.png"]; - const $_c1$ = [1, "ngIf"]; + const $_c1$ = [${AttributeMarker.SelectOnly}, "ngIf"]; function MyComponent_img_Template_1(rf, ctx) { if (rf & 1) { $r3$.ɵelement(0, "img", $_c0$); @@ -1043,7 +1044,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = [1, "ngIf"]; + const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"]; function MyComponent_div_div_Template_4(rf, ctx) { if (rf & 1) { $r3$.ɵi18nStart(0, $I18N_EXTERNAL_1221890473527419724$$APP_SPEC_TS_0$, 2); @@ -1136,7 +1137,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = [1, "ngIf"]; + const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"]; const $MSG_EXTERNAL_119975189388320493$$APP_SPEC_TS__1$ = goog.getMsg("Some other content {$startTagSpan}{$interpolation}{$closeTagSpan}", { "startTagSpan": "\uFFFD#2\uFFFD", "interpolation": "\uFFFD0\uFFFD", @@ -1259,23 +1260,21 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = ["myClass", 1, "myClass", true]; + const $_c0$ = [${AttributeMarker.Classes}, "myClass"]; const $MSG_EXTERNAL_5295701706185791735$$APP_SPEC_TS_0$ = goog.getMsg("Text #1"); - const $_c1$ = ["padding", 1, "padding", "10px"]; + const $_c1$ = [${AttributeMarker.Styles}, "padding", "10px"]; const $MSG_EXTERNAL_4722270221386399294$$APP_SPEC_TS_2$ = goog.getMsg("Text #2"); … consts: 4, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵelementStart(0, "span"); + $r3$.ɵelementStart(0, "span", $_c0$); $r3$.ɵi18nStart(1, $MSG_EXTERNAL_5295701706185791735$$APP_SPEC_TS_0$); - $r3$.ɵelementStyling($_c0$); $r3$.ɵi18nEnd(); $r3$.ɵelementEnd(); - $r3$.ɵelementStart(2, "span"); + $r3$.ɵelementStart(2, "span", $_c1$); $r3$.ɵi18nStart(3, $MSG_EXTERNAL_4722270221386399294$$APP_SPEC_TS_2$); - $r3$.ɵelementStyling(null, $_c1$); $r3$.ɵi18nEnd(); $r3$.ɵelementEnd(); } @@ -1701,7 +1700,7 @@ describe('i18n support in the view compiler', () => { const $I18N_EXTERNAL_7842238767399919809$$APP_SPEC_TS_0$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS_0$, { "VAR_SELECT": "\uFFFD0\uFFFD" }); - const $_c0$ = [1, "ngIf"]; + const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"]; const $_c1$ = ["title", "icu only"]; const $MSG_EXTERNAL_8806993169187953163$$APP_SPEC_TS__3$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}"); const $I18N_EXTERNAL_8806993169187953163$$APP_SPEC_TS__3$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_8806993169187953163$$APP_SPEC_TS__3$, { @@ -1942,7 +1941,7 @@ describe('i18n support in the view compiler', () => { const $I18N_APP_SPEC_TS_2$ = $r3$.ɵi18nPostprocess($MSG_APP_SPEC_TS_2$, { "VAR_SELECT": "\uFFFD1\uFFFD" }); - const $_c3$ = [1, "ngIf"]; + const $_c3$ = [${AttributeMarker.SelectOnly}, "ngIf"]; const $MSG_APP_SPEC_TS__4$ = goog.getMsg("{VAR_SELECT, select, male {male} female {female} other {other}}"); const $I18N_APP_SPEC_TS__4$ = $r3$.ɵi18nPostprocess($MSG_APP_SPEC_TS__4$, { "VAR_SELECT": "\uFFFD0:1\uFFFD" @@ -2050,7 +2049,7 @@ describe('i18n support in the view compiler', () => { const $I18N_EXTERNAL_7842238767399919809$$APP_SPEC_TS_1$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS_1$, { "VAR_SELECT": "\uFFFD0\uFFFD" }); - const $_c0$ = [1, "ngIf"]; + const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"]; const $MSG_EXTERNAL_7068143081688428291$$APP_SPEC_TS__3$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other}}"); const $I18N_EXTERNAL_7068143081688428291$$APP_SPEC_TS__3$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7068143081688428291$$APP_SPEC_TS__3$, { "VAR_SELECT": "\uFFFD0:1\uFFFD" @@ -2113,7 +2112,7 @@ describe('i18n support in the view compiler', () => { const $I18N_EXTERNAL_7825031864601787094$$APP_SPEC_TS_1$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7825031864601787094$$APP_SPEC_TS_1$, { "VAR_SELECT": "\uFFFD0\uFFFD" }); - const $_c0$ = [1, "ngIf"]; + const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"]; const $MSG_EXTERNAL_2310343208266678305$$APP_SPEC_TS__3$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {$interpolation}}}", { "interpolation": "\uFFFD1:1\uFFFD" }); diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index fd59463428..3827ff7294 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AttributeMarker, InitialStylingFlags, ViewEncapsulation} from '@angular/compiler/src/core'; +import {AttributeMarker, ViewEncapsulation} from '@angular/compiler/src/core'; import {setup} from '@angular/compiler/test/aot/test_util'; import {compile, expectEmit} from './mock_compile'; @@ -366,8 +366,8 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $e0_attrs$ = [${AttributeMarker.SelectOnly}, "style"]; - const $e0_styling$ = ["opacity","width","height",${InitialStylingFlags.VALUES_MODE},"opacity","1"]; + const $_c0$ = [${AttributeMarker.Styles}, "opacity", "1", ${AttributeMarker.SelectOnly}, "style"]; + const $_c1$ = ["width", "height"]; … MyComponent.ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, @@ -379,14 +379,14 @@ describe('compiler compliance: styling', () => { vars: 1, template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵelementStart(0, "div", $e0_attrs$); - $r3$.ɵelementStyling(null, $e0_styling$, $r3$.ɵdefaultStyleSanitizer); + $r3$.ɵelementStart(0, "div", $_c0$); + $r3$.ɵelementStyling(null, $_c1$, $r3$.ɵdefaultStyleSanitizer); $r3$.ɵelementEnd(); } if (rf & 2) { $r3$.ɵelementStylingMap(0, null, $ctx$.myStyleExp); - $r3$.ɵelementStyleProp(0, 1, $ctx$.myWidth); - $r3$.ɵelementStyleProp(0, 2, $ctx$.myHeight); + $r3$.ɵelementStyleProp(0, 0, $ctx$.myWidth); + $r3$.ɵelementStyleProp(0, 1, $ctx$.myHeight); $r3$.ɵelementStylingApply(0); $r3$.ɵelementAttribute(0, "style", $r3$.ɵbind("border-width: 10px"), $r3$.ɵsanitizeStyle); } @@ -421,7 +421,7 @@ describe('compiler compliance: styling', () => { }; const template = ` - const _c0 = ["background-image"]; + const $_c0$ = ["background-image"]; export class MyComponent { constructor() { this.myImage = 'url(foo.jpg)'; @@ -456,7 +456,6 @@ describe('compiler compliance: styling', () => { }); it('should support [style.foo.suffix] style bindings with a suffix', () => { - const files = { app: { 'spec.ts': ` @@ -476,7 +475,7 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $e0_styles$= ["font-size"]; + const $e0_styles$ = ["font-size"]; … template: function MyComponent_Template(rf, ctx) { if (rf & 1) { @@ -564,8 +563,8 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $e0_attrs$ = [${AttributeMarker.SelectOnly}, "class"]; - const $e0_cd$ = ["grape","apple","orange",${InitialStylingFlags.VALUES_MODE},"grape",true]; + const $e0_attrs$ = [${AttributeMarker.Classes}, "grape", ${AttributeMarker.SelectOnly}, "class"]; + const $e0_bindings$ = ["apple", "orange"]; … MyComponent.ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, @@ -578,13 +577,13 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵelementStart(0, "div", $e0_attrs$); - $r3$.ɵelementStyling($e0_cd$); + $r3$.ɵelementStyling($e0_bindings$); $r3$.ɵelementEnd(); } if (rf & 2) { $r3$.ɵelementStylingMap(0, $ctx$.myClassExp); - $r3$.ɵelementClassProp(0, 1, $ctx$.yesToApple); - $r3$.ɵelementClassProp(0, 2, $ctx$.yesToOrange); + $r3$.ɵelementClassProp(0, 0, $ctx$.yesToApple); + $r3$.ɵelementClassProp(0, 1, $ctx$.yesToOrange); $r3$.ɵelementStylingApply(0); $r3$.ɵelementAttribute(0, "class", $r3$.ɵbind("banana")); } @@ -606,7 +605,7 @@ describe('compiler compliance: styling', () => { @Component({ selector: 'my-component', - template: \`
\` @@ -620,9 +619,7 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $e0_attrs$ = [${AttributeMarker.SelectOnly}, "class", "style"]; - const $e0_cd$ = ["foo",${InitialStylingFlags.VALUES_MODE},"foo",true]; - const $e0_sd$ = ["width",${InitialStylingFlags.VALUES_MODE},"width","100px"]; + const $e0_attrs$ = [${AttributeMarker.Classes}, "foo", ${AttributeMarker.Styles}, "width", "100px", ${AttributeMarker.SelectOnly}, "class", "style"]; … MyComponent.ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, @@ -635,7 +632,6 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵelementStart(0, "div", $e0_attrs$); - $r3$.ɵelementStyling($e0_cd$, $e0_sd$); $r3$.ɵelementEnd(); } if (rf & 2) { @@ -765,10 +761,13 @@ describe('compiler compliance: styling', () => { }; const template = ` + const $e0_classBindings$ = ["foo"]; + const $e0_styleBindings$ = ["bar", "baz"]; + … template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵelementStart(0, "div"); - $r3$.ɵelementStyling($e0_styling$, $e1_styling$, $r3$.ɵdefaultStyleSanitizer); + $r3$.ɵelementStyling($e0_classBindings$, $e0_styleBindings$, $r3$.ɵdefaultStyleSanitizer); $r3$.ɵpipe(1, "pipe"); $r3$.ɵpipe(2, "pipe"); $r3$.ɵpipe(3, "pipe"); @@ -828,16 +827,18 @@ describe('compiler compliance: styling', () => { }; const template = ` - const _c0 = ["foo", "baz", ${InitialStylingFlags.VALUES_MODE}, "foo", true, "baz", true]; - const _c1 = ["width", "height", "color", ${InitialStylingFlags.VALUES_MODE}, "width", "200px", "height", "500px"]; + const $e0_attrs$ = [${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"]; + const $e0_classBindings$ = ["foo"]; + const $e0_styleBindings$ = ["color"]; … hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - $r3$.ɵelementStyling(_c0, _c1, $r3$.ɵdefaultStyleSanitizer, ctx); + $r3$.ɵelementHostAttrs(ctx, $e0_attrs$); + $r3$.ɵelementStyling($e0_classBindings$, $e0_styleBindings$, $r3$.ɵdefaultStyleSanitizer, ctx); } if (rf & 2) { $r3$.ɵelementStylingMap(elIndex, ctx.myClass, ctx.myStyle, ctx); - $r3$.ɵelementStyleProp(elIndex, 2, ctx.myColorProp, null, ctx); + $r3$.ɵelementStyleProp(elIndex, 0, ctx.myColorProp, null, ctx); $r3$.ɵelementClassProp(elIndex, 0, ctx.myFooClass, ctx); $r3$.ɵelementStylingApply(elIndex, ctx); } @@ -959,10 +960,10 @@ describe('compiler compliance: styling', () => { }; const template = ` - const _c0 = ["foo"]; - const _c1 = ["width"]; - const _c2 = ["bar"]; - const _c3 = ["height"]; + const $widthDir_classes$ = ["foo"]; + const $widthDir_styles$ = ["width"]; + const $heightDir_classes$ = ["bar"]; + const $heightDir_styles$ = ["height"]; … function ClassDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { @@ -976,7 +977,7 @@ describe('compiler compliance: styling', () => { … function WidthDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - $r3$.ɵelementStyling(_c0, _c1, null, ctx); + $r3$.ɵelementStyling($widthDir_classes$, $widthDir_styles$, null, ctx); } if (rf & 2) { $r3$.ɵelementStyleProp(elIndex, 0, ctx.myWidth, null, ctx); @@ -987,7 +988,7 @@ describe('compiler compliance: styling', () => { … function HeightDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - $r3$.ɵelementStyling(_c2, _c3, null, ctx); + $r3$.ɵelementStyling($heightDir_classes$, $heightDir_styles$, null, ctx); } if (rf & 2) { $r3$.ɵelementStyleProp(elIndex, 0, ctx.myHeight, null, ctx); @@ -1014,7 +1015,8 @@ describe('compiler compliance: styling', () => { template: '', host: { 'style': 'width:200px; height:500px', - 'class': 'foo baz' + 'class': 'foo baz', + 'title': 'foo title' } }) export class MyComponent { @@ -1029,6 +1031,9 @@ describe('compiler compliance: styling', () => { @HostBinding('title') title = 'some title'; + + @Input('name') + name = ''; } @NgModule({declarations: [MyComponent]}) @@ -1038,13 +1043,13 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $_c0$ = ["foo", "baz", ${InitialStylingFlags.VALUES_MODE}, "foo", true, "baz", true]; - const $_c1$ = ["width", "height", ${InitialStylingFlags.VALUES_MODE}, "width", "200px", "height", "500px"]; + const $_c0$ = [${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"]; … hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { if (rf & 1) { $r3$.ɵallocHostVars(2); - $r3$.ɵelementStyling($_c0$, $_c1$, $r3$.ɵdefaultStyleSanitizer, ctx); + $r3$.ɵelementHostAttrs(ctx, $_c0$); + $r3$.ɵelementStyling(null, null, $r3$.ɵdefaultStyleSanitizer, ctx); } if (rf & 2) { $r3$.ɵelementProperty(elIndex, "id", $r3$.ɵbind(ctx.id), null, true); diff --git a/packages/compiler/src/core.ts b/packages/compiler/src/core.ts index b67674a648..2b3081f46e 100644 --- a/packages/compiler/src/core.ts +++ b/packages/compiler/src/core.ts @@ -379,13 +379,9 @@ export const enum RenderFlags { Update = 0b10 } -export const enum InitialStylingFlags { - VALUES_MODE = 0b1, -} - // Pasted from render3/interfaces/node.ts /** - * A set of marker values to be used in the attributes arrays. Those markers indicate that some + * A set of marker values to be used in the attributes arrays. These markers indicate that some * items are not regular attributes and the processing should be adapted accordingly. */ export const enum AttributeMarker { @@ -396,11 +392,48 @@ export const enum AttributeMarker { */ NamespaceURI = 0, + /** + * Signals class declaration. + * + * Each value following `Classes` designates a class name to include on the element. + * ## Example: + * + * Given: + * ``` + *
... + * ``` + * + * the generated code is: + * ``` + * var _c1 = [AttributeMarker.Classes, 'foo', 'bar', 'baz']; + * ``` + */ + Classes = 1, + + /** + * Signals style declaration. + * + * Each pair of values following `Styles` designates a style name and value to include on the + * element. + * ## Example: + * + * Given: + * ``` + *
...
+ * ``` + * + * the generated code is: + * ``` + * var _c1 = [AttributeMarker.Styles, 'width', '100px', 'height'. '200px', 'color', 'red']; + * ``` + */ + Styles = 2, + /** * This marker indicates that the following attribute names were extracted from bindings (ex.: * [foo]="exp") and / or event handlers (ex. (bar)="doSth()"). * Taking the above bindings and outputs as an example an attributes array could look as follows: * ['class', 'fade in', AttributeMarker.SelectOnly, 'foo', 'bar'] */ - SelectOnly = 1 -} \ No newline at end of file + SelectOnly = 3, +} diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 79a29effdf..33422adde6 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -43,6 +43,8 @@ export class Identifiers { static elementStyling: o.ExternalReference = {name: 'ɵelementStyling', moduleName: CORE}; + static elementHostAttrs: o.ExternalReference = {name: 'ɵelementHostAttrs', moduleName: CORE}; + static elementStylingMap: o.ExternalReference = {name: 'ɵelementStylingMap', moduleName: CORE}; static elementStyleProp: o.ExternalReference = {name: 'ɵelementStyleProp', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 22705300ff..9b37d76c2b 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -28,7 +28,7 @@ import {Render3ParseResult} from '../r3_template_transform'; import {typeWithParameters} from '../util'; import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api'; -import {StylingBuilder, StylingInstruction} from './styling'; +import {StylingBuilder, StylingInstruction} from './styling_builder'; import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template'; import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util'; @@ -709,16 +709,35 @@ function createHostBindingsFunction( } } - if (styleBuilder.hasBindingsOrInitialValues) { - const createInstruction = styleBuilder.buildCreateLevelInstruction(null, constantPool); - if (createInstruction) { - const createStmt = createStylingStmt(createInstruction, bindingContext, bindingFn); - createStatements.push(createStmt); + if (styleBuilder.hasBindingsOrInitialValues()) { + // since we're dealing with directives here and directives have a hostBinding + // function, we need to generate special instructions that deal with styling + // (both bindings and initial values). The instruction below will instruct + // all initial styling (styling that is inside of a host binding within a + // directive) to be attached to the host element of the directive. + const hostAttrsInstruction = + styleBuilder.buildDirectiveHostAttrsInstruction(null, constantPool); + if (hostAttrsInstruction) { + createStatements.push(createStylingStmt(hostAttrsInstruction, bindingContext, bindingFn)); } + // singular style/class bindings (things like `[style.prop]` and `[class.name]`) + // MUST be registered on a given element within the component/directive + // templateFn/hostBindingsFn functions. The instruction below will figure out + // what all the bindings are and then generate the statements required to register + // those bindings to the element via `elementStyling`. + const elementStylingInstruction = + styleBuilder.buildElementStylingInstruction(null, constantPool); + if (elementStylingInstruction) { + createStatements.push( + createStylingStmt(elementStylingInstruction, bindingContext, bindingFn)); + } + + // finally each binding that was registered in the statement above will need to be added to + // the update block of a component/directive templateFn/hostBindingsFn so that the bindings + // are evaluated and updated for the element. styleBuilder.buildUpdateLevelInstructions(valueConverter).forEach(instruction => { - const updateStmt = createStylingStmt(instruction, bindingContext, bindingFn); - updateStatements.push(updateStmt); + updateStatements.push(createStylingStmt(instruction, bindingContext, bindingFn)); }); } } diff --git a/packages/compiler/src/render3/view/style_parser.ts b/packages/compiler/src/render3/view/style_parser.ts index 28d38c6f4f..97b3f8c83f 100644 --- a/packages/compiler/src/render3/view/style_parser.ts +++ b/packages/compiler/src/render3/view/style_parser.ts @@ -23,10 +23,15 @@ const enum Char { * * @param value string representation of style as used in the `style` attribute in HTML. * Example: `color: red; height: auto`. - * @returns an object literal. `{ color: 'red', height: 'auto'}`. + * @returns An array of style property name and value pairs, e.g. `['color', 'red', 'height', + * 'auto']` */ -export function parse(value: string): {[key: string]: any} { - const styles: {[key: string]: any} = {}; +export function parse(value: string): string[] { + // we use a string array here instead of a string map + // because a string-map is not guaranteed to retain the + // order of the entries whereas a string array can be + // construted in a [key, value, key, value] format. + const styles: string[] = []; let i = 0; let parenDepth = 0; @@ -72,7 +77,7 @@ export function parse(value: string): {[key: string]: any} { case Char.Semicolon: if (currentProp && valueStart > 0 && parenDepth === 0 && quote === Char.QuoteNone) { const styleVal = value.substring(valueStart, i - 1).trim(); - styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal; + styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal); propStart = i; valueStart = 0; currentProp = null; @@ -84,7 +89,7 @@ export function parse(value: string): {[key: string]: any} { if (currentProp && valueStart) { const styleVal = value.substr(valueStart).trim(); - styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal; + styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal); } return styles; diff --git a/packages/compiler/src/render3/view/styling.ts b/packages/compiler/src/render3/view/styling_builder.ts similarity index 53% rename from packages/compiler/src/render3/view/styling.ts rename to packages/compiler/src/render3/view/styling_builder.ts index 983a5f1db3..aebc989304 100644 --- a/packages/compiler/src/render3/view/styling.ts +++ b/packages/compiler/src/render3/view/styling_builder.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ import {ConstantPool} from '../../constant_pool'; -import {InitialStylingFlags} from '../../core'; -import {AST, BindingType, ParseSpan} from '../../expression_parser/ast'; +import {AttributeMarker} from '../../core'; +import {AST, BindingType} from '../../expression_parser/ast'; import * as o from '../../output/output_ast'; import {ParseSourceSpan} from '../../parse_util'; import * as t from '../r3_ast'; @@ -40,6 +40,10 @@ interface BoundStylingEntry { /** * Produces creation/update instructions for all styling bindings (class and style) * + * It also produces the creation instruction to register all initial styling values + * (which are all the static class="..." and style="..." attribute values that exist + * on an element within a template). + * * The builder class below handles producing instructions for the following cases: * * - Static style/class attributes (style="..." and class="...") @@ -63,25 +67,57 @@ interface BoundStylingEntry { * The creation/update methods within the builder class produce these instructions. */ export class StylingBuilder { - public readonly hasBindingsOrInitialValues = false; + /** Whether or not there are any static styling values present */ + private _hasInitialValues = false; + /** + * Whether or not there are any styling bindings present + * (i.e. `[style]`, `[class]`, `[style.prop]` or `[class.name]`) + */ + private _hasBindings = false; + /** the input for [class] (if it exists) */ private _classMapInput: BoundStylingEntry|null = null; + /** the input for [style] (if it exists) */ private _styleMapInput: BoundStylingEntry|null = null; + /** an array of each [style.prop] input */ private _singleStyleInputs: BoundStylingEntry[]|null = null; + /** an array of each [class.name] input */ private _singleClassInputs: BoundStylingEntry[]|null = null; private _lastStylingInput: BoundStylingEntry|null = null; // maps are used instead of hash maps because a Map will // retain the ordering of the keys + + /** + * Represents the location of each style binding in the template + * (e.g. `
` implies + * that `width=0` and `height=1`) + */ private _stylesIndex = new Map(); + + /** + * Represents the location of each class binding in the template + * (e.g. `
` implies + * that `big=0` and `hidden=1`) + */ private _classesIndex = new Map(); - private _initialStyleValues: {[propName: string]: string} = {}; - private _initialClassValues: {[className: string]: boolean} = {}; + private _initialStyleValues: string[] = []; + private _initialClassValues: string[] = []; + + // certain style properties ALWAYS need sanitization + // this is checked each time new styles are encountered private _useDefaultSanitizer = false; - private _applyFnRequired = false; constructor(private _elementIndexExpr: o.Expression, private _directiveExpr: o.Expression|null) {} + hasBindingsOrInitialValues() { return this._hasBindings || this._hasInitialValues; } + + /** + * Registers a given input to the styling builder to be later used when producing AOT code. + * + * The code below will only accept the input if it is somehow tied to styling (whether it be + * style/class bindings or static style/class attributes). + */ registerBoundInput(input: t.BoundAttribute): boolean { // [attr.style] or [attr.class] are skipped in the code below, // they should not be treated as styling-based bindings since @@ -117,14 +153,12 @@ export class StylingBuilder { (this._singleStyleInputs = this._singleStyleInputs || []).push(entry); this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(propertyName); registerIntoMap(this._stylesIndex, propertyName); - (this as any).hasBindingsOrInitialValues = true; } else { this._useDefaultSanitizer = true; this._styleMapInput = entry; } this._lastStylingInput = entry; - (this as any).hasBindingsOrInitialValues = true; - this._applyFnRequired = true; + this._hasBindings = true; return entry; } @@ -133,107 +167,152 @@ export class StylingBuilder { const entry = { name: className, value, sourceSpan } as BoundStylingEntry; if (className) { (this._singleClassInputs = this._singleClassInputs || []).push(entry); - (this as any).hasBindingsOrInitialValues = true; registerIntoMap(this._classesIndex, className); } else { this._classMapInput = entry; } this._lastStylingInput = entry; - (this as any).hasBindingsOrInitialValues = true; - this._applyFnRequired = true; + this._hasBindings = true; return entry; } + /** + * Registers the element's static style string value to the builder. + * + * @param value the style string (e.g. `width:100px; height:200px;`) + */ registerStyleAttr(value: string) { this._initialStyleValues = parseStyle(value); - Object.keys(this._initialStyleValues).forEach(prop => { - registerIntoMap(this._stylesIndex, prop); - (this as any).hasBindingsOrInitialValues = true; - }); + this._hasInitialValues = true; } + /** + * Registers the element's static class string value to the builder. + * + * @param value the className string (e.g. `disabled gold zoom`) + */ registerClassAttr(value: string) { - this._initialClassValues = {}; - value.split(/\s+/g).forEach(className => { - this._initialClassValues[className] = true; - registerIntoMap(this._classesIndex, className); - (this as any).hasBindingsOrInitialValues = true; - }); + this._initialClassValues = value.trim().split(/\s+/g); + this._hasInitialValues = true; } - private _buildInitExpr(registry: Map, initialValues: {[key: string]: any}): - o.Expression|null { - const exprs: o.Expression[] = []; - const nameAndValueExprs: o.Expression[] = []; - - // _c0 = [prop, prop2, prop3, ...] - registry.forEach((value, key) => { - const keyLiteral = o.literal(key); - exprs.push(keyLiteral); - const initialValue = initialValues[key]; - if (initialValue) { - nameAndValueExprs.push(keyLiteral, o.literal(initialValue)); + /** + * Appends all styling-related expressions to the provided attrs array. + * + * @param attrs an existing array where each of the styling expressions + * will be inserted into. + */ + populateInitialStylingAttrs(attrs: o.Expression[]): void { + // [CLASS_MARKER, 'foo', 'bar', 'baz' ...] + if (this._initialClassValues.length) { + attrs.push(o.literal(AttributeMarker.Classes)); + for (let i = 0; i < this._initialClassValues.length; i++) { + attrs.push(o.literal(this._initialClassValues[i])); } - }); - - if (nameAndValueExprs.length) { - // _c0 = [... MARKER ...] - exprs.push(o.literal(InitialStylingFlags.VALUES_MODE)); - // _c0 = [prop, VALUE, prop2, VALUE2, ...] - exprs.push(...nameAndValueExprs); } - return exprs.length ? o.literalArr(exprs) : null; + // [STYLE_MARKER, 'width', '200px', 'height', '100px', ...] + if (this._initialStyleValues.length) { + attrs.push(o.literal(AttributeMarker.Styles)); + for (let i = 0; i < this._initialStyleValues.length; i += 2) { + attrs.push( + o.literal(this._initialStyleValues[i]), o.literal(this._initialStyleValues[i + 1])); + } + } } - buildCreateLevelInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool): + /** + * Builds an instruction with all the expressions and parameters for `elementHostAttrs`. + * + * The instruction generation code below is used for producing the AOT statement code which is + * responsible for registering initial styles (within a directive hostBindings' creation block) + * to the directive host element. + */ + buildDirectiveHostAttrsInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool): StylingInstruction|null { - if (this.hasBindingsOrInitialValues) { - const initialClasses = this._buildInitExpr(this._classesIndex, this._initialClassValues); - const initialStyles = this._buildInitExpr(this._stylesIndex, this._initialStyleValues); - - // in the event that a [style] binding is used then sanitization will - // always be imported because it is not possible to know ahead of time - // whether style bindings will use or not use any sanitizable properties - // that isStyleSanitizable() will detect - const useSanitizer = this._useDefaultSanitizer; - const params: (o.Expression)[] = []; - - if (initialClasses) { - // 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). - params.push(constantPool.getConstLiteral(initialClasses, true)); - } else if (initialStyles || useSanitizer || this._directiveExpr) { - // no point in having an extra `null` value unless there are follow-up params - params.push(o.NULL_EXPR); - } - - if (initialStyles) { - // 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). - params.push(constantPool.getConstLiteral(initialStyles, true)); - } else if (useSanitizer || this._directiveExpr) { - // no point in having an extra `null` value unless there are follow-up params - params.push(o.NULL_EXPR); - } - - if (useSanitizer || this._directiveExpr) { - params.push(useSanitizer ? o.importExpr(R3.defaultStyleSanitizer) : o.NULL_EXPR); - if (this._directiveExpr) { - params.push(this._directiveExpr); + if (this._hasInitialValues && this._directiveExpr) { + return { + sourceSpan, + reference: R3.elementHostAttrs, + buildParams: () => { + const attrs: o.Expression[] = []; + this.populateInitialStylingAttrs(attrs); + return [this._directiveExpr !, getConstantLiteralFromArray(constantPool, attrs)]; } - } - - return {sourceSpan, reference: R3.elementStyling, buildParams: () => params}; + }; } return null; } - private _buildStylingMap(valueConverter: ValueConverter): StylingInstruction|null { + /** + * Builds an instruction with all the expressions and parameters for `elementStyling`. + * + * The instruction generation code below is used for producing the AOT statement code which is + * responsible for registering style/class bindings to an element. + */ + buildElementStylingInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool): + StylingInstruction|null { + if (this._hasBindings) { + return { + sourceSpan, + reference: R3.elementStyling, + buildParams: () => { + // a string array of every style-based binding + const styleBindingProps = + this._singleStyleInputs ? this._singleStyleInputs.map(i => o.literal(i.name)) : []; + // a string array of every class-based binding + const classBindingNames = + this._singleClassInputs ? this._singleClassInputs.map(i => o.literal(i.name)) : []; + + // to salvage space in the AOT generated code, there is no point in passing + // in `null` into a param if any follow-up params are not used. Therefore, + // only when a trailing param is used then it will be filled with nulls in between + // (otherwise a shorter amount of params will be filled). The code below helps + // determine how many params are required in the expression code. + // + // min params => elementStyling() + // max params => elementStyling(classBindings, styleBindings, sanitizer, directive) + let expectedNumberOfArgs = 0; + if (this._directiveExpr) { + expectedNumberOfArgs = 4; + } else if (this._useDefaultSanitizer) { + expectedNumberOfArgs = 3; + } else if (styleBindingProps.length) { + expectedNumberOfArgs = 2; + } else if (classBindingNames.length) { + expectedNumberOfArgs = 1; + } + + const params: o.Expression[] = []; + addParam( + params, classBindingNames.length > 0, + getConstantLiteralFromArray(constantPool, classBindingNames), 1, + expectedNumberOfArgs); + addParam( + params, styleBindingProps.length > 0, + getConstantLiteralFromArray(constantPool, styleBindingProps), 2, + expectedNumberOfArgs); + addParam( + params, this._useDefaultSanitizer, o.importExpr(R3.defaultStyleSanitizer), 3, + expectedNumberOfArgs); + if (this._directiveExpr) { + params.push(this._directiveExpr); + } + return params; + } + }; + } + return null; + } + + /** + * Builds an instruction with all the expressions and parameters for `elementStylingMap`. + * + * The instruction data will contain all expressions for `elementStylingMap` to function + * which include the `[style]` and `[class]` expression params (if they exist) as well as + * the sanitizer and directive reference expression. + */ + buildElementStylingMapInstruction(valueConverter: ValueConverter): StylingInstruction|null { if (this._classMapInput || this._styleMapInput) { const stylingInput = this._classMapInput ! || this._styleMapInput !; @@ -332,18 +411,20 @@ export class StylingBuilder { }; } + /** + * Constructs all instructions which contain the expressions that will be placed + * into the update block of a template function or a directive hostBindings function. + */ buildUpdateLevelInstructions(valueConverter: ValueConverter) { const instructions: StylingInstruction[] = []; - if (this.hasBindingsOrInitialValues) { - const mapInstruction = this._buildStylingMap(valueConverter); + if (this._hasBindings) { + const mapInstruction = this.buildElementStylingMapInstruction(valueConverter); if (mapInstruction) { instructions.push(mapInstruction); } instructions.push(...this._buildStyleInputs(valueConverter)); instructions.push(...this._buildClassInputs(valueConverter)); - if (this._applyFnRequired) { - instructions.push(this._buildApplyFn()); - } + instructions.push(this._buildApplyFn()); } return instructions; } @@ -363,3 +444,26 @@ function isStyleSanitizable(prop: string): boolean { return prop === 'background-image' || prop === 'background' || prop === 'border-image' || prop === 'filter' || prop === 'list-style' || prop === 'list-style-image'; } + +/** + * Simple helper function to either provide the constant literal that will house the value + * here or a null value if the provided values are empty. + */ +function getConstantLiteralFromArray( + constantPool: ConstantPool, values: o.Expression[]): o.Expression { + return values.length ? constantPool.getConstLiteral(o.literalArr(values), true) : o.NULL_EXPR; +} + +/** + * Simple helper function that adds a parameter or does nothing at all depending on the provided + * predicate and totalExpectedArgs values + */ +function addParam( + params: o.Expression[], predicate: boolean, value: o.Expression, argNumber: number, + totalExpectedArgs: number) { + if (predicate) { + params.push(value); + } else if (argNumber < totalExpectedArgs) { + params.push(o.NULL_EXPR); + } +} diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 9ec9f62ab8..188c0e4712 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -35,7 +35,7 @@ import {I18nContext} from './i18n/context'; import {I18nMetaVisitor} from './i18n/meta'; import {getSerializedI18nContent} from './i18n/serializer'; import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util'; -import {StylingBuilder, StylingInstruction} from './styling'; +import {StylingBuilder, StylingInstruction} from './styling_builder'; import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util'; function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined { @@ -532,7 +532,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // this will build the instructions so that they fall into the following syntax // add attributes for directive matching purposes - attributes.push(...this.prepareSyntheticAndSelectOnlyAttrs(allOtherInputs, element.outputs)); + attributes.push(...this.prepareSyntheticAndSelectOnlyAttrs( + allOtherInputs, element.outputs, stylingBuilder)); parameters.push(this.toAttrsParam(attributes)); // local refs (ex.:
) @@ -562,11 +563,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver return element.children.length > 0; }; - const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues && + const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues() && !isNgContainer && element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren(); const createSelfClosingI18nInstruction = !createSelfClosingInstruction && - !stylingBuilder.hasBindingsOrInitialValues && hasTextChildrenOnly(element.children); + !stylingBuilder.hasBindingsOrInitialValues() && hasTextChildrenOnly(element.children); if (createSelfClosingInstruction) { this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters)); @@ -616,10 +617,16 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } } - // initial styling for static style="..." and class="..." attributes + // The style bindings code is placed into two distinct blocks within the template function AOT + // code: creation and update. The creation code contains the `elementStyling` instructions + // which will apply the collected binding values to the element. `elementStyling` is + // designed to run inside of `elementStart` and `elementEnd`. The update instructions + // (things like `elementStyleProp`, `elementClassProp`, etc..) are applied later on in this + // file this.processStylingInstruction( implicit, - stylingBuilder.buildCreateLevelInstruction(element.sourceSpan, this.constantPool), true); + stylingBuilder.buildElementStylingInstruction(element.sourceSpan, this.constantPool), + true); // Generate Listeners (outputs) element.outputs.forEach((outputAst: t.BoundEvent) => { @@ -629,6 +636,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); } + // the code here will collect all update-level styling instructions and add them to the + // update block of the template function AOT code. Instructions like `elementStyleProp`, + // `elementStylingMap`, `elementClassProp` and `elementStylingApply` are all generated + // and assign in the code below. stylingBuilder.buildUpdateLevelInstructions(this._valueConverter).forEach(instruction => { this.processStylingInstruction(implicit, instruction, false); }); @@ -934,8 +945,26 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } } - private prepareSyntheticAndSelectOnlyAttrs(inputs: t.BoundAttribute[], outputs: t.BoundEvent[]): - o.Expression[] { + /** + * Prepares all attribute expression values for the `TAttributes` array. + * + * The purpose of this function is to properly construct an attributes array that + * is passed into the `elementStart` (or just `element`) functions. Because there + * are many different types of attributes, the array needs to be constructed in a + * special way so that `elementStart` can properly evaluate them. + * + * The format looks like this: + * + * ``` + * attrs = [prop, value, prop2, value2, + * CLASSES, class1, class2, + * STYLES, style1, value1, style2, value2, + * SELECT_ONLY, name1, name2, name2, ...] + * ``` + */ + private prepareSyntheticAndSelectOnlyAttrs( + inputs: t.BoundAttribute[], outputs: t.BoundEvent[], + styles?: StylingBuilder): o.Expression[] { const attrExprs: o.Expression[] = []; const nonSyntheticInputs: t.BoundAttribute[] = []; @@ -954,6 +983,13 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); } + // it's important that this occurs before SelectOnly because once `elementStart` + // comes across the SelectOnly marker then it will continue reading each value as + // as single property value cell by cell. + if (styles) { + styles.populateInitialStylingAttrs(attrExprs); + } + if (nonSyntheticInputs.length || outputs.length) { attrExprs.push(o.literal(core.AttributeMarker.SelectOnly)); nonSyntheticInputs.forEach((i: t.BoundAttribute) => attrExprs.push(asLiteral(i.name))); diff --git a/packages/compiler/test/render3/style_parser_spec.ts b/packages/compiler/test/render3/style_parser_spec.ts index d0f9e4ae11..9fe365bca9 100644 --- a/packages/compiler/test/render3/style_parser_spec.ts +++ b/packages/compiler/test/render3/style_parser_spec.ts @@ -10,55 +10,53 @@ import {hyphenate, parse as parseStyle, stripUnnecessaryQuotes} from '../../src/ describe('style parsing', () => { it('should parse empty or blank strings', () => { const result1 = parseStyle(''); - expect(result1).toEqual({}); + expect(result1).toEqual([]); const result2 = parseStyle(' '); - expect(result2).toEqual({}); + expect(result2).toEqual([]); }); it('should parse a string into a key/value map', () => { const result = parseStyle('width:100px;height:200px;opacity:0'); - expect(result).toEqual({width: '100px', height: '200px', opacity: '0'}); + expect(result).toEqual(['width', '100px', 'height', '200px', 'opacity', '0']); }); it('should trim values and properties', () => { const result = parseStyle('width :333px ; height:666px ; opacity: 0.5;'); - expect(result).toEqual({width: '333px', height: '666px', opacity: '0.5'}); + expect(result).toEqual(['width', '333px', 'height', '666px', 'opacity', '0.5']); }); it('should chomp out start/end quotes', () => { const result = parseStyle( 'content: "foo"; opacity: \'0.5\'; font-family: "Verdana", Helvetica, "sans-serif"'); expect(result).toEqual( - {content: 'foo', opacity: '0.5', 'font-family': '"Verdana", Helvetica, "sans-serif"'}); + ['content', 'foo', 'opacity', '0.5', 'font-family', '"Verdana", Helvetica, "sans-serif"']); }); it('should not mess up with quoted strings that contain [:;] values', () => { const result = parseStyle('content: "foo; man: guy"; width: 100px'); - expect(result).toEqual({content: 'foo; man: guy', width: '100px'}); + expect(result).toEqual(['content', 'foo; man: guy', 'width', '100px']); }); it('should not mess up with quoted strings that contain inner quote values', () => { const quoteStr = '"one \'two\' three \"four\" five"'; const result = parseStyle(`content: ${quoteStr}; width: 123px`); - expect(result).toEqual({content: quoteStr, width: '123px'}); + expect(result).toEqual(['content', quoteStr, 'width', '123px']); }); it('should respect parenthesis that are placed within a style', () => { const result = parseStyle('background-image: url("foo.jpg")'); - expect(result).toEqual({'background-image': 'url("foo.jpg")'}); + expect(result).toEqual(['background-image', 'url("foo.jpg")']); }); it('should respect multi-level parenthesis that contain special [:;] characters', () => { const result = parseStyle('color: rgba(calc(50 * 4), var(--cool), :5;); height: 100px;'); - expect(result).toEqual({color: 'rgba(calc(50 * 4), var(--cool), :5;)', height: '100px'}); + expect(result).toEqual(['color', 'rgba(calc(50 * 4), var(--cool), :5;)', 'height', '100px']); }); it('should hyphenate style properties from camel case', () => { const result = parseStyle('borderWidth: 200px'); - expect(result).toEqual({ - 'border-width': '200px', - }); + expect(result).toEqual(['border-width', '200px']); }); describe('quote chomping', () => { diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index cca9436342..f384eba657 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -92,6 +92,7 @@ export { elementContainerStart as ɵelementContainerStart, elementContainerEnd as ɵelementContainerEnd, elementStyling as ɵelementStyling, + elementHostAttrs as ɵelementHostAttrs, elementStylingMap as ɵelementStylingMap, elementStyleProp as ɵelementStyleProp, elementStylingApply as ɵelementStylingApply, diff --git a/packages/core/src/debug/debug_node.ts b/packages/core/src/debug/debug_node.ts index 9623b44846..54c2c6cd70 100644 --- a/packages/core/src/debug/debug_node.ts +++ b/packages/core/src/debug/debug_node.ts @@ -12,7 +12,7 @@ import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, import {TNode} from '../render3/interfaces/node'; import {StylingIndex} from '../render3/interfaces/styling'; import {TVIEW} from '../render3/interfaces/view'; -import {getProp, getValue, isClassBased} from '../render3/styling/class_and_style_bindings'; +import {getProp, getValue, isClassBasedValue} from '../render3/styling/class_and_style_bindings'; import {getStylingContext} from '../render3/styling/util'; import {DebugContext} from '../view/index'; @@ -273,7 +273,7 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme if (stylingContext) { for (let i = StylingIndex.SingleStylesStartPosition; i < lNode.length; i += StylingIndex.Size) { - if (isClassBased(lNode, i)) { + if (isClassBasedValue(lNode, i)) { const className = getProp(lNode, i); const value = getValue(lNode, i); if (typeof value == 'boolean') { @@ -303,7 +303,7 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme if (stylingContext) { for (let i = StylingIndex.SingleStylesStartPosition; i < lNode.length; i += StylingIndex.Size) { - if (!isClassBased(lNode, i)) { + if (!isClassBasedValue(lNode, i)) { const styleName = getProp(lNode, i); const value = getValue(lNode, i) as string | null; if (value !== null) { diff --git a/packages/core/src/render3/context_discovery.ts b/packages/core/src/render3/context_discovery.ts index 6ce8791ba4..651f37cf20 100644 --- a/packages/core/src/render3/context_discovery.ts +++ b/packages/core/src/render3/context_discovery.ts @@ -7,12 +7,13 @@ */ import './ng_dev_mode'; import {assertDomNode} from './assert'; -import {EMPTY_ARRAY} from './definition'; +import {EMPTY_ARRAY} from './empty'; import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context'; import {TNode, TNodeFlags} from './interfaces/node'; import {RElement} from './interfaces/renderer'; import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view'; -import {getComponentViewByIndex, getNativeByTNode, getTNode, readElementValue, readPatchedData} from './util'; +import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util'; + /** Returns the matching `LContext` data for a given DOM node, directive or component instance. diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index 52843b367e..3d1d0d3aea 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -9,22 +9,15 @@ import './ng_dev_mode'; import {ChangeDetectionStrategy} from '../change_detection/constants'; -import {Provider} from '../di/provider'; import {NgModuleDef} from '../metadata/ng_module'; import {ViewEncapsulation} from '../metadata/view'; import {Mutable, Type} from '../type'; import {noSideEffects, stringify} from '../util'; - +import {EMPTY_ARRAY, EMPTY_OBJ} from './empty'; import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields'; import {BaseDef, ComponentDef, ComponentDefFeature, ComponentQuery, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFeature, DirectiveType, DirectiveTypesOrFactory, HostBindingsFunction, PipeDef, PipeType, PipeTypesOrFactory} from './interfaces/definition'; -import {CssSelectorList, SelectorFlags} from './interfaces/projection'; +import {CssSelectorList} from './interfaces/projection'; -export const EMPTY: {} = {}; -export const EMPTY_ARRAY: any[] = []; -if (typeof ngDevMode !== 'undefined' && ngDevMode) { - Object.freeze(EMPTY); - Object.freeze(EMPTY_ARRAY); -} let _renderCompCount = 0; /** @@ -389,7 +382,7 @@ export function defineNgModule(def: {type: T} & Partial>): nev */ function invertObject(obj: any, secondary?: any): any { - if (obj == null) return EMPTY; + if (obj == null) return EMPTY_OBJ; const newLookup: any = {}; for (const minifiedKey in obj) { if (obj.hasOwnProperty(minifiedKey)) { diff --git a/packages/core/src/render3/discovery_utils.ts b/packages/core/src/render3/discovery_utils.ts index 66fe7b29e9..08ccd32fc4 100644 --- a/packages/core/src/render3/discovery_utils.ts +++ b/packages/core/src/render3/discovery_utils.ts @@ -7,6 +7,7 @@ */ import {Injector} from '../di/injector'; + import {assertDefined} from './assert'; import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from './context_discovery'; import {NodeInjector} from './di'; @@ -14,7 +15,8 @@ import {LContext} from './interfaces/context'; import {DirectiveDef} from './interfaces/definition'; import {TElementNode, TNode, TNodeProviderIndexes} from './interfaces/node'; import {CLEANUP, CONTEXT, FLAGS, HOST, LView, LViewFlags, PARENT, RootContext, TVIEW} from './interfaces/view'; -import {readPatchedLView, stringify} from './util'; +import {readElementValue, readPatchedLView, stringify} from './util'; + /** @@ -327,7 +329,7 @@ export function getListeners(element: Element): Listener[] { const secondParam = tCleanup[i++]; if (typeof firstParam === 'string') { const name: string = firstParam; - const listenerElement: Element = lView[secondParam]; + const listenerElement = readElementValue(lView[secondParam]) as any as Element; const callback: (value: any) => any = lCleanup[tCleanup[i++]]; const useCaptureOrIndx = tCleanup[i++]; // if useCaptureOrIndx is boolean then report it as is. diff --git a/packages/core/src/render3/empty.ts b/packages/core/src/render3/empty.ts new file mode 100644 index 0000000000..46dc61bdf6 --- /dev/null +++ b/packages/core/src/render3/empty.ts @@ -0,0 +1,24 @@ +/** +* @license +* Copyright Google Inc. All Rights Reserved. +* +* Use of this source code is governed by an MIT-style license that can be +* found in the LICENSE file at https://angular.io/license +*/ +import './ng_dev_mode'; + +/** + * This file contains reuseable "empty" symbols that can be used as default return values + * in different parts of the rendering code. Because the same symbols are returned, this + * allows for identity checks against these values to be consistently used by the framework + * code. + */ + +export const EMPTY_OBJ: {} = {}; +export const EMPTY_ARRAY: any[] = []; + +// freezing the values prevents any code from accidentally inserting new values in +if (typeof ngDevMode !== 'undefined' && ngDevMode) { + Object.freeze(EMPTY_OBJ); + Object.freeze(EMPTY_ARRAY); +} diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts index 33e76644b2..5e2f7cc775 100644 --- a/packages/core/src/render3/features/inherit_definition_feature.ts +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -8,8 +8,8 @@ import {Type} from '../../type'; import {fillProperties} from '../../util/property'; -import {EMPTY, EMPTY_ARRAY} from '../definition'; -import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition'; +import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; +import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition'; @@ -178,7 +178,7 @@ export function InheritDefinitionFeature(definition: DirectiveDef| Componen function maybeUnwrapEmpty(value: T[]): T[]; function maybeUnwrapEmpty(value: T): T; function maybeUnwrapEmpty(value: any): any { - if (value === EMPTY) { + if (value === EMPTY_OBJ) { return {}; } else if (value === EMPTY_ARRAY) { return []; diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 9128b9901d..f1971992b5 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -48,8 +48,8 @@ export { elementContainerStart, elementContainerEnd, - elementStyling, + elementHostAttrs, elementStylingMap, elementStyleProp, elementStylingApply, @@ -79,7 +79,7 @@ export { directiveInject, injectAttribute, - + getCurrentView } from './instructions'; diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 4ca0b1aae1..6ed84dd888 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -15,6 +15,7 @@ import {Sanitizer} from '../sanitization/security'; import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {Type} from '../type'; import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../util/ng_reflect'; + import {assertDataInRange, assertDefined, assertEqual, assertHasParent, assertLessThan, assertNotEqual, assertPreviousIsParent} from './assert'; import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from './bindings'; import {attachPatchData, getComponentViewByInstance} from './context_discovery'; @@ -22,7 +23,7 @@ import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreat import {throwMultipleComponentError} from './errors'; import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container'; -import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, InitialStylingFlags, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; +import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from './interfaces/injector'; import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from './interfaces/node'; import {PlayerFactory} from './interfaces/player'; @@ -30,19 +31,19 @@ import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection' import {LQueries} from './interfaces/query'; import {ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer'; import {SanitizerFn} from './interfaces/sanitization'; -import {StylingIndex} from './interfaces/styling'; import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, createTextNode, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state'; -import {createStylingContextTemplate, renderStyleAndClassBindings, setStyle, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; +import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; import {BoundPlayerFactory} from './styling/player_factory'; -import {getStylingContext, isAnimationProp} from './styling/util'; +import {createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util'; import {NO_CHANGE} from './tokens'; import {findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, loadInternal, readElementValue, readPatchedLView, stringify} from './util'; + /** * A permanent marker promise which signifies that the current CD tree is * clean. @@ -456,7 +457,8 @@ export function namespaceHTML() { * * @param index Index of the element in the data array * @param name Name of the DOM Node - * @param attrs Statically bound set of attributes to be written into the DOM element on creation. + * @param attrs Statically bound set of attributes, classes, and styles to be written into the DOM + * element on creation. Use [AttributeMarker] to denote the meaning of this array. * @param localRefs A set of local reference bindings on the element. */ export function element( @@ -526,7 +528,8 @@ export function elementContainerEnd(): void { * * @param index Index of the element in the LView array * @param name Name of the DOM Node - * @param attrs Statically bound set of attributes to be written into the DOM element on creation. + * @param attrs Statically bound set of attributes, classes, and styles to be written into the DOM + * element on creation. Use [AttributeMarker] to denote the meaning of this array. * @param localRefs A set of local reference bindings on the element. * * Attributes and localRefs are passed as an array of strings where elements with an even index @@ -550,6 +553,14 @@ export function elementStart( const tNode = createNodeAtIndex(index, TNodeType.Element, native !, name, attrs || null); if (attrs) { + // it's important to only prepare styling-related datastructures once for a given + // tNode and not each time an element is created. Also, the styling code is designed + // to be patched and constructed at various points, but only up until the first element + // is created. Then the styling context is locked and can only be instantiated for each + // successive element that is created. + if (tView.firstTemplatePass && !tNode.stylingTemplate && hasStyling(attrs)) { + tNode.stylingTemplate = initializeStaticStylingContext(attrs); + } setUpAttributes(native, attrs); } @@ -563,6 +574,23 @@ export function elementStart( attachPatchData(native, lView); } increaseElementDepthCount(); + + // if a directive contains a host binding for "class" then all class-based data will + // flow through that (except for `[class.prop]` bindings). This also includes initial + // static class values as well. (Note that this will be fixed once map-based `[style]` + // and `[class]` bindings work for multiple directives.) + if (tView.firstTemplatePass) { + const inputData = initializeTNodeInputs(tNode); + if (inputData && inputData.hasOwnProperty('class')) { + tNode.flags |= TNodeFlags.hasClassInput; + } + } + + // There is no point in rendering styles when a class directive is present since + // it will take that over for us (this will be removed once #FW-882 is in). + if (tNode.stylingTemplate && (tNode.flags & TNodeFlags.hasClassInput) === 0) { + renderInitialStylesAndClasses(native, tNode.stylingTemplate, lView[RENDERER]); + } } /** @@ -721,25 +749,28 @@ function setUpAttributes(native: RElement, attrs: TAttributes): void { let i = 0; while (i < attrs.length) { - const attrName = attrs[i]; - if (attrName === AttributeMarker.SelectOnly) break; - if (attrName === NG_PROJECT_AS_ATTR_NAME) { - i += 2; - } else { - ngDevMode && ngDevMode.rendererSetAttribute++; + const attrName = attrs[i++]; + if (typeof attrName == 'number') { if (attrName === AttributeMarker.NamespaceURI) { // Namespaced attributes - const namespaceURI = attrs[i + 1] as string; - const attrName = attrs[i + 2] as string; - const attrVal = attrs[i + 3] as string; + const namespaceURI = attrs[i++] as string; + const attrName = attrs[i++] as string; + const attrVal = attrs[i++] as string; + ngDevMode && ngDevMode.rendererSetAttribute++; isProc ? (renderer as ProceduralRenderer3) .setAttribute(native, attrName, attrVal, namespaceURI) : native.setAttributeNS(namespaceURI, attrName, attrVal); - i += 4; } else { + // All other `AttributeMarker`s are ignored here. + break; + } + } else { + /// attrName is string; + const attrVal = attrs[i++]; + if (attrName !== NG_PROJECT_AS_ATTR_NAME) { // Standard attributes - const attrVal = attrs[i + 1]; + ngDevMode && ngDevMode.rendererSetAttribute++; if (isAnimationProp(attrName)) { if (isProc) { (renderer as ProceduralRenderer3).setProperty(native, attrName, attrVal); @@ -750,7 +781,6 @@ function setUpAttributes(native: RElement, attrs: TAttributes): void { .setAttribute(native, attrName as string, attrVal as string) : native.setAttribute(attrName as string, attrVal as string); } - i += 2; } } } @@ -902,6 +932,15 @@ export function elementEnd(): void { queueLifecycleHooks(getLView()[TVIEW], previousOrParentTNode); decreaseElementDepthCount(); + + // this is fired at the end of elementEnd because ALL of the stylingBindings code + // (for directives and the template) have now executed which means the styling + // context can be instantiated properly. + if (hasClassInput(previousOrParentTNode)) { + const stylingContext = getStylingContext(previousOrParentTNode.index, lView); + setInputsForProperty( + lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext)); + } } /** @@ -1096,119 +1135,84 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro return propStore; } -/** - * Add or remove a class in a `classList` on a DOM element. - * - * This instruction is meant to handle the [class.foo]="exp" case - * - * @param index The index of the element to update in the data array - * @param classIndex Index of class to toggle. Because it is going to DOM, this is not subject to - * renaming as part of minification. - * @param value A value indicating if a given class should be added or removed. - * @param directive the ref to the directive that is attempting to change styling. - */ -export function elementClassProp( - index: number, classIndex: number, value: boolean | PlayerFactory, directive?: {}): void { - if (directive != undefined) { - return hackImplementationOfElementClassProp( - index, classIndex, value, directive); // proper supported in next PR - } - const val = - (value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory) : (!!value); - updateElementClassProp(getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, val); -} - /** * Assign any inline style values to the element during creation mode. * - * This instruction is meant to be called during creation mode to apply all styling - * (e.g. `style="..."`) values to the element. This is also where the provided index - * value is allocated for the styling details for its corresponding element (the element - * index is the previous index value from this one). + * This instruction is meant to be called during creation mode to register all + * dynamic style and class bindings on the element. Note for static values (no binding) + * see `elementStart` and `elementHostAttrs`. * - * (Note this function calls `elementStylingApply` immediately when called.) + * @param classBindingNames An array containing bindable class names. + * The `elementClassProp` refers to the class name by index in this array. + * (i.e. `['foo', 'bar']` means `foo=0` and `bar=1`). + * @param styleBindingNames An array containing bindable style properties. + * The `elementStyleProp` refers to the class name by index in this array. + * (i.e. `['width', 'height']` means `width=0` and `height=1`). + * @param styleSanitizer An optional sanitizer function that will be used to sanitize any CSS + * property values that are applied to the element (during rendering). + * Note that the sanitizer instance itself is tied to the `directive` (if provided). + * @param directive A directive instance the styling is associated with. If not provided + * current view's controller instance is assumed. * - * - * @param index Index value which will be allocated to store styling data for the element. - * (Note that this is not the element index, but rather an index value allocated - * specifically for element styling--the index must be the next index after the element - * index.) - * @param 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). - * @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 styleSanitizer An optional sanitizer function that will be used (if provided) - * to sanitize the any CSS property values that are applied to the element (during rendering). - * @param directive the ref to the directive that is attempting to change styling. + * @publicApi */ export function elementStyling( - classDeclarations?: (string | boolean | InitialStylingFlags)[] | null, - styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, + classBindingNames?: string[] | null, styleBindingNames?: string[] | null, styleSanitizer?: StyleSanitizeFn | null, directive?: {}): void { - if (directive != undefined) { - isCreationMode() && - hackImplementationOfElementStyling( - classDeclarations || null, styleDeclarations || null, styleSanitizer || null, - directive); // supported in next PR - return; - } const tNode = getPreviousOrParentTNode(); - const inputData = initializeTNodeInputs(tNode); - if (!tNode.stylingTemplate) { - const hasClassInput = inputData && inputData.hasOwnProperty('class') ? true : false; - if (hasClassInput) { - tNode.flags |= TNodeFlags.hasClassInput; - } - - // initialize the styling template. - tNode.stylingTemplate = createStylingContextTemplate( - classDeclarations, styleDeclarations, styleSanitizer, hasClassInput); - } - - if (styleDeclarations && styleDeclarations.length || - classDeclarations && classDeclarations.length) { - const index = tNode.index; - if (delegateToClassInput(tNode)) { - const lView = getLView(); - const stylingContext = getStylingContext(index, lView); - const initialClasses = stylingContext[StylingIndex.PreviousOrCachedMultiClassValue] as string; - setInputsForProperty(lView, tNode.inputs !['class'] !, initialClasses); - } - elementStylingApply(index - HEADER_OFFSET); + tNode.stylingTemplate = createEmptyStylingContext(); } + updateContextWithBindings( + tNode.stylingTemplate !, directive || null, classBindingNames, styleBindingNames, + styleSanitizer, hasClassInput(tNode)); } +/** + * Assign static styling values to a host element. + * + * NOTE: This instruction is meant to used from `hostBindings` function only. + * + * @param directive A directive instance the styling is associated with. + * @param attrs An array containing class and styling information. The values must be marked with + * `AttributeMarker`. + * + * ``` + * var attrs = [AttributeMarker.Classes, 'foo', 'bar', + * AttributeMarker.Styles, 'width', '100px', 'height, '200px'] + * elementHostAttrs(directive, attrs); + * ``` + * + * @publicApi + */ +export function elementHostAttrs(directive: any, attrs: TAttributes) { + const tNode = getPreviousOrParentTNode(); + if (!tNode.stylingTemplate) { + tNode.stylingTemplate = initializeStaticStylingContext(attrs); + } + patchContextWithStaticAttrs(tNode.stylingTemplate, attrs, directive); +} /** - * Apply all styling values to the element which have been queued by any styling instructions. + * Apply styling binding to the element. * - * This instruction is meant to be run once one or more `elementStyle` and/or `elementStyleProp` - * have been issued against the element. This function will also determine if any styles have - * changed and will then skip the operation if there is nothing new to render. + * This instruction is meant to be run after `elementStyle` and/or `elementStyleProp`. + * if any styling bindings have changed then the changes are flushed to the element. * - * Once called then all queued styles will be flushed. * - * @param index Index of the element's styling storage that will be rendered. - * (Note that this is not the element index, but rather an index value allocated - * specifically for element styling--the index must be the next index after the element - * index.) - * @param directive the ref to the directive that is attempting to change styling. + * @param index Index of the element's with which styling is associated. + * @param directive Directive instance that is attempting to change styling. (Defaults to the + * component of the current view). +components + * + * @publicApi */ -export function elementStylingApply(index: number, directive?: {}): void { - if (directive != undefined) { - return hackImplementationOfElementStylingApply(index, directive); // supported in next PR - } +export function elementStylingApply(index: number, directive?: any): void { const lView = getLView(); const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0; - const totalPlayersQueued = renderStyleAndClassBindings( - getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender); + const totalPlayersQueued = renderStyling( + getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender, null, + null, directive); if (totalPlayersQueued > 0) { const rootContext = getRootContext(lView); scheduleTick(rootContext, RootContextFlags.FlushPlayers); @@ -1216,29 +1220,34 @@ export function elementStylingApply(index: number, directive?: {}): void { } /** - * Queue a given style to be rendered on an Element. + * Update a style bindings value on an element. * * If the style value is `null` then it will be removed from the element * (or assigned a different value depending if there are any styles placed * on the element with `elementStyle` or any styles that are present * from when the element was created (with `elementStyling`). * - * (Note that the styling instruction will not be applied until `elementStylingApply` is called.) + * (Note that the styling element is updated as part of `elementStylingApply`.) * - * @param index Index of the element's styling storage to change in the data array. - * (Note that this is not the element index, but rather an index value allocated - * specifically for element styling--the index must be the next index after the element - * index.) - * @param styleIndex Index of the style property on this element. (Monotonically increasing.) - * @param value New value to write (null to remove). + * @param index Index of the element's with which styling is associated. + * @param styleIndex Index of style to update. This index value refers to the + * index of the style in the style bindings array that was passed into + * `elementStlyingBindings`. + * @param value New value to write (null to remove). Note that if a directive also + * attempts to write to the same binding value then it will only be able to + * do so if the template binding value is `null` (or doesn't exist at all). * @param suffix Optional suffix. Used with scalar values to add unit such as `px`. * Note that when a suffix is provided then the underlying sanitizer will * be ignored. - * @param directive the ref to the directive that is attempting to change styling. + * @param directive Directive instance that is attempting to change styling. (Defaults to the + * component of the current view). +components + * + * @publicApi */ export function elementStyleProp( index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, - suffix?: string, directive?: {}): void { + suffix?: string | null, directive?: {}): void { let valueToAdd: string|null = null; if (value !== null) { if (suffix) { @@ -1253,35 +1262,59 @@ export function elementStyleProp( valueToAdd = value as any as string; } } - if (directive != undefined) { - hackImplementationOfElementStyleProp(index, styleIndex, valueToAdd, suffix, directive); - } else { - updateElementStyleProp( - getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd); - } + updateElementStyleProp( + getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, directive); } /** - * Queue a key/value map of styles to be rendered on an Element. + * Add or remove a class via a class binding on a DOM element. * - * This instruction is meant to handle the `[style]="exp"` usage. When styles are applied to - * the Element they will then be placed with respect to any styles set with `elementStyleProp`. - * If any styles are set to `null` then they will be removed from the element (unless the same - * style properties have been assigned to the element during creation using `elementStyling`). + * This instruction is meant to handle the [class.foo]="exp" case and, therefore, + * the class itself must already be applied using `elementStyling` within + * the creation block. + * + * @param index Index of the element's with which styling is associated. + * @param classIndex Index of class to toggle. This index value refers to the + * index of the class in the class bindings array that was passed into + * `elementStlyingBindings` (which is meant to be called before this + * function is). + * @param value A true/false value which will turn the class on or off. + * @param directive Directive instance that is attempting to change styling. (Defaults to the + * component of the current view). +components + * + * @publicApi + */ +export function elementClassProp( + index: number, classIndex: number, value: boolean | PlayerFactory, directive?: {}): void { + const onOrOffClassValue = + (value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory) : (!!value); + updateElementClassProp( + getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, onOrOffClassValue, + directive); +} + +/** + * Update style and/or class bindings using object literal. + * + * This instruction is meant apply styling via the `[style]="exp"` and `[class]="exp"` template + * bindings. When styles are applied to the Element they will then be placed with respect to + * any styles set with `elementStyleProp`. If any styles are set to `null` then they will be + * removed from the element. * * (Note that the styling instruction will not be applied until `elementStylingApply` is called.) * - * @param index Index of the element's styling storage to change in the data array. - * (Note that this is not the element index, but rather an index value allocated - * specifically for element styling--the index must be the next index after the element - * index.) + * @param index Index of the element's with which styling is associated. * @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. * @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 directive the ref to the directive that is attempting to change styling. + * @param directive Directive instance that is attempting to change styling. (Defaults to the + * component of the current view). + * + * @publicApi */ export function elementStylingMap( index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, @@ -1292,113 +1325,24 @@ export function elementStylingMap( const lView = getLView(); const tNode = getTNode(index, lView); const stylingContext = getStylingContext(index + HEADER_OFFSET, lView); - if (delegateToClassInput(tNode) && classes !== NO_CHANGE) { - const initialClasses = stylingContext[StylingIndex.PreviousOrCachedMultiClassValue] as string; + if (hasClassInput(tNode) && classes !== NO_CHANGE) { + const initialClasses = getInitialClassNameValue(stylingContext); const classInputVal = (initialClasses.length ? (initialClasses + ' ') : '') + (classes as string); setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal); + } else { + updateStylingMap(stylingContext, classes, styles); } - updateStylingMap(stylingContext, classes, styles); } /* START OF HACK BLOCK */ -/* - * HACK - * The code below is a quick and dirty implementation of the host style binding so that we can make - * progress on TestBed. Once the correct implementation is created this code should be removed. - */ -interface HostStylingHack { - classDeclarations: string[]; - styleDeclarations: string[]; - styleSanitizer: StyleSanitizeFn|null; -} -type HostStylingHackMap = Map<{}, HostStylingHack>; - -function hackImplementationOfElementStyling( - classDeclarations: (string | boolean | InitialStylingFlags)[] | null, - styleDeclarations: (string | boolean | InitialStylingFlags)[] | null, - styleSanitizer: StyleSanitizeFn | null, directive: {}): void { - const node = getNativeByTNode(getPreviousOrParentTNode(), getLView()) as RElement; - ngDevMode && assertDefined(node, 'expecting parent DOM node'); - const hostStylingHackMap: HostStylingHackMap = - ((node as any).hostStylingHack || ((node as any).hostStylingHack = new Map())); - const squashedClassDeclarations = hackSquashDeclaration(classDeclarations); - hostStylingHackMap.set(directive, { - classDeclarations: squashedClassDeclarations, - styleDeclarations: hackSquashDeclaration(styleDeclarations), styleSanitizer - }); - hackSetStaticClasses(node, squashedClassDeclarations); -} - -function hackSetStaticClasses(node: RElement, classDeclarations: (string | boolean)[]) { - // Static classes need to be set here because static classes don't generate - // elementClassProp instructions. - const lView = getLView(); - const staticClassStartIndex = - classDeclarations.indexOf(InitialStylingFlags.VALUES_MODE as any) + 1; - const renderer = lView[RENDERER]; - - for (let i = staticClassStartIndex; i < classDeclarations.length; i += 2) { - const className = classDeclarations[i] as string; - const value = classDeclarations[i + 1]; - // if value is true, then this is a static class and we should set it now. - // class bindings are set separately in elementClassProp. - if (value === true) { - if (isProceduralRenderer(renderer)) { - renderer.addClass(node, className); - } else { - const classList = (node as HTMLElement).classList; - classList.add(className); - } - } - } -} - -function hackSquashDeclaration(declarations: (string | boolean | InitialStylingFlags)[] | null): - string[] { - // assume the array is correct. This should be fine for View Engine compatibility. - return declarations || [] as any; -} - -function hackImplementationOfElementClassProp( - index: number, classIndex: number, value: boolean | PlayerFactory, directive: {}): void { - const lView = getLView(); - const node = getNativeByIndex(index, lView); - ngDevMode && assertDefined(node, 'could not locate node'); - const hostStylingHack: HostStylingHack = (node as any).hostStylingHack.get(directive); - const className = hostStylingHack.classDeclarations[classIndex]; - const renderer = lView[RENDERER]; - if (isProceduralRenderer(renderer)) { - value ? renderer.addClass(node, className) : renderer.removeClass(node, className); - } else { - const classList = (node as HTMLElement).classList; - value ? classList.add(className) : classList.remove(className); - } -} - -function hackImplementationOfElementStylingApply(index: number, directive?: {}): void { - // Do nothing because the hack implementation is eager. -} - -function hackImplementationOfElementStyleProp( - index: number, styleIndex: number, value: string | null, suffix?: string, - directive?: {}): void { - const lView = getLView(); - const node = getNativeByIndex(index, lView); - ngDevMode && assertDefined(node, 'could not locate node'); - const hostStylingHack: HostStylingHack = (node as any).hostStylingHack.get(directive); - const styleName = hostStylingHack.styleDeclarations[styleIndex]; - const renderer = lView[RENDERER]; - setStyle(node, styleName, value as string, renderer, null); -} - function hackImplementationOfElementStylingMap( index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, styles?: {[styleName: string]: any} | NO_CHANGE | null, directive?: {}): void { throw new Error('unimplemented. Should not be needed by ViewEngine compatibility'); } - /* END OF HACK BLOCK */ + ////////////////////////// //// Text ////////////////////////// @@ -2885,10 +2829,6 @@ function initializeTNodeInputs(tNode: TNode | null) { return null; } -export function delegateToClassInput(tNode: TNode) { - return tNode.flags & TNodeFlags.hasClassInput; -} - /** * Returns the current OpaqueViewState instance. diff --git a/packages/core/src/render3/interfaces/container.ts b/packages/core/src/render3/interfaces/container.ts index 5a0e0e50be..a8378c2c9c 100644 --- a/packages/core/src/render3/interfaces/container.ts +++ b/packages/core/src/render3/interfaces/container.ts @@ -23,6 +23,14 @@ export const VIEWS = 1; // As we already have these constants in LView, we don't need to re-create them. export const NATIVE = 6; export const RENDER_PARENT = 7; +// Because interfaces in TS/JS cannot be instanceof-checked this means that we +// need to rely on predictable characteristics of data-structures to check if they +// are what we expect for them to be. The `LContainer` interface code below has a +// fixed length and the constant value below references that. Using the length value +// below we can predictably gaurantee that we are dealing with an `LContainer` array. +// This value MUST be kept up to date with the length of the `LContainer` array +// interface below so that runtime type checking can work. +export const LCONTAINER_LENGTH = 8; /** * The state associated with a container. diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 356ad5d5bb..f530b5cea2 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -344,8 +344,4 @@ export type PipeTypeList = // Note: This hack is necessary so we don't erroneously get a circular dependency // failure based on types. -export const unusedValueExportToPlacateAjd = 1; - -export const enum InitialStylingFlags { - VALUES_MODE = 0b1, -} +export const unusedValueExportToPlacateAjd = 1; \ No newline at end of file diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index ae8b49682f..dcf5468846 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -54,7 +54,7 @@ export const enum TNodeProviderIndexes { CptViewProvidersCountShifter = 0b00000000000000010000000000000000, } /** - * A set of marker values to be used in the attributes arrays. Those markers indicate that some + * A set of marker values to be used in the attributes arrays. These markers indicate that some * items are not regular attributes and the processing should be adapted accordingly. */ export const enum AttributeMarker { @@ -65,13 +65,50 @@ export const enum AttributeMarker { */ NamespaceURI = 0, + /** + * Signals class declaration. + * + * Each value following `Classes` designates a class name to include on the element. + * ## Example: + * + * Given: + * ``` + *
... + * ``` + * + * the generated code is: + * ``` + * var _c1 = [AttributeMarker.Classes, 'foo', 'bar', 'baz']; + * ``` + */ + Classes = 1, + + /** + * Signals style declaration. + * + * Each pair of values following `Styles` designates a style name and value to include on the + * element. + * ## Example: + * + * Given: + * ``` + *
...
+ * ``` + * + * the generated code is: + * ``` + * var _c1 = [AttributeMarker.Styles, 'width', '100px', 'height'. '200px', 'color', 'red']; + * ``` + */ + Styles = 2, + /** * This marker indicates that the following attribute names were extracted from bindings (ex.: * [foo]="exp") and / or event handlers (ex. (bar)="doSth()"). * Taking the above bindings and outputs as an example an attributes array could look as follows: * ['class', 'fade in', AttributeMarker.SelectOnly, 'foo', 'bar'] */ - SelectOnly = 1 + SelectOnly = 3, } /** diff --git a/packages/core/src/render3/interfaces/styling.ts b/packages/core/src/render3/interfaces/styling.ts index 7f1c6349a8..fa2091b342 100644 --- a/packages/core/src/render3/interfaces/styling.ts +++ b/packages/core/src/render3/interfaces/styling.ts @@ -9,130 +9,199 @@ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {RElement} from '../interfaces/renderer'; import {PlayerContext} from './player'; - /** * The styling context acts as a styling manifest (shaped as an array) for determining which * 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. + * and `updateClassProp` functions. It also stores the static style/class values that were + * extracted from the template by the compiler. * - * 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. + * A context is created by Angular when: + * 1. An element contains static styling values (like style="..." or class="...") + * 2. An element contains single property binding values (like [style.prop]="x" or + * [class.prop]="y") + * 3. An element contains multi property binding values (like [style]="x" or [class]="y") + * 4. A directive contains host bindings for static, single or multi styling properties/bindings. + * 5. An animation player is added to an element via `addPlayer` * - * each value from here onwards is mapped as so: - * [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 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` or `updateClassProp` (fixed set) - * - * multi: any styles that are updated using `updateStylingMap` (dynamic set) - * - * 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 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: + * Note that even if an element contains static styling then this context will be created and + * attached to it. The reason why this happens (instead of treating styles/classes as regular + * HTML attributes) is because the style/class bindings must be able to default themselves back + * to their respective static values when they are set to null. * + * Say for example we have this: * ``` + * + *
+ * ``` + * + * Even in the situation where there are no bindings, the static styling is still placed into the + * context because there may be another directive on the same element that has styling. + * + * When Angular initializes styling data for an element then it will first register the static + * styling values on the element using one of these two instructions: + * + * 1. elementStart or element (within the template function of a component) + * 2. elementHostAttrs (for directive host bindings) + * + * In either case, a styling context will be created and stored within an element's LViewData. Once + * the styling context is created then single and multi properties can stored within it. For this to + * happen, the following function needs to be called: + * + * `elementStyling` (called with style properties, class properties and a sanitizer + a directive + * instance). + * + * When this instruction is called it will populate the styling context with the provided style + * and class names into the context. + * + * The context itself looks like this: + * * context = [ - * element, - * playerContext | null, - * styleSanitizer | null, - * [null, '100px', '200px', true], // property names are not needed since they have already been - * written to DOM. - * - * configMasterVal, - * 1, // this instructs how many `style` values there are so that class index values can be - * offsetted - * { classOne: true, classTwo: false } | 'classOne classTwo' | null // last class value provided - * into updateStylingMap - * { styleOne: '100px', styleTwo: 0 } | null // last style value provided into updateStylingMap - * - * // 8 - * 'width', - * pointers(1, 15); // Point to static `width`: `100px` and multi `width`. - * null, - * - * // 11 - * 'height', - * pointers(2, 18); // Point to static `height`: `200px` and multi `height`. - * null, - * - * // 14 - * 'foo', - * pointers(1, 21); // Point to static `foo`: `true` and multi `foo`. - * null, - * - * // 17 - * 'width', - * pointers(1, 6); // Point to static `width`: `100px` and single `width`. - * null, - * - * // 21 - * 'height', - * pointers(2, 9); // Point to static `height`: `200px` and single `height`. - * null, - * - * // 24 - * 'foo', - * pointers(3, 12); // Point to static `foo`: `true` and single `foo`. - * null, + * // 0-8: header values (about 8 entries of configuration data) + * // 9+: this is where each entry is stored: * ] * - * function pointers(staticIndex: number, dynamicIndex: number) { - * // combine the two indices into a single word. - * return (staticIndex << StylingFlags.BitCountSize) | - * (dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize)); + * Let's say we have the following template code: + * + * ``` + *
+ * ``` + * + * The context generated from these values will look like this (note that + * for each binding name (the class and style bindings) the values will + * be inserted twice into the array (once for single property entries) and + * another for multi property entries). + * + * context = [ + * // 0-8: header values (about 8 entries of configuration data) + * // 9+: this is where each entry is stored: + * + * // SINGLE PROPERTIES + * configForWidth, + * 'width' + * myWidthExp, // the binding value not the binding itself + * 0, // the directive owner + * + * configForHeight, + * 'height' + * myHeightExp, // the binding value not the binding itself + * 0, // the directive owner + * + * configForBazClass, + * 'baz + * myBazClassExp, // the binding value not the binding itself + * 0, // the directive owner + * + * // MULTI PROPERTIES + * configForWidth, + * 'width' + * myWidthExp, // the binding value not the binding itself + * 0, // the directive owner + * + * configForHeight, + * 'height' + * myHeightExp, // the binding value not the binding itself + * 0, // the directive owner + * + * configForBazClass, + * 'baz + * myBazClassExp, // the binding value not the binding itself + * 0, // the directive owner + * ] + * + * The configuration values are left out of the example above because + * the ordering of them could change between code patches. Please read the + * documentation below to get a better understand of what the configuration + * values are and how they work. + * + * Each time a binding property is updated (whether it be through a single + * property instruction like `elementStyleProp`, `elementClassProp` or + * `elementStylingMap`) then the values in the context will be updated as + * well. + * + * If for example `[style.width]` updates to `555px` then its value will be reflected + * in the context as so: + * + * context = [ + * // ... + * configForWidth, // this will be marked DIRTY + * 'width' + * '555px', + * 0, + * //.. + * ] + * + * The context and directive data will also be marked dirty. + * + * Despite the context being updated, nothing has been rendered on screen (not styles or + * classes have been set on the element). To kick off rendering for an element the following + * function needs to be run `elementStylingApply`. + * + * `elementStylingApply` will run through the context and find each dirty value and render them onto + * the element. Once complete, all styles/classes will be set to clean. Because of this, the render + * function will now know not to rerun itself again if called again unless new style/class values + * have changed. + * + * ## Directives + * Directives style values (which are provided through host bindings) are also supported and + * housed within the same styling context as are template-level style/class properties/bindings. + * Both directive-level and template-level styling bindings share the same context. + * + * Each of the following instructions supports accepting a directive instance as an input parameter: + * + * - `elementHostAttrs` + * - `elementStyling` + * - `elementStyleProp` + * - `elementClassProp` + * - `elementStylingMap` + * - `elementStylingApply` + * + * Each time a directiveRef is passed in, it will be converted into an index by examining the + * directive registry (which lives in the context configuration area). The index is then used + * to help single style properties figure out where a value is located in the context. + * + * If two directives or a directive + a template binding both write to the same style/class + * binding then the styling context code will decide which one wins based on the following + * rule: + * + * 1. If the template binding has a value then it always wins + * 2. If not then whichever first-registered directive that has that value first will win + * + * The code example helps make this clear: + * + * ``` + *
+ * @Directive({ selector: '[my-width-directive' ]}) + * class MyWidthDirective { + * @Input('my-width-directive') + * @HostBinding('style.width') + * public width = null; * } * ``` * - * 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). + * Since there is a style binding for width present on the element (`[style.width]`) then + * it will always win over the width binding that is present as a host binding within + * the `MyWidthDirective`. However, if `[style.width]` renders as `null` (so `myWidth=null`) + * then the `MyWidthDirective` will be able to write to the `width` style within the context. + * Simply put, whichever directive writes to a value ends up having ownership of it. * - * 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). + * The way in which the ownership is facilitated is through index value. The earliest directives + * get the smallest index values (with 0 being reserved for the template element bindings). Each + * time a value is written from a directive or the template bindings, the value itself gets + * assigned the directive index value in its data. If another directive writes a value again then + * its directive index gets compared against the directive index that exists on the element. Only + * when the new value's directive index is less than the existing directive index then the new + * value will be written to the context. + * + * Each directive also has its own sanitizer and dirty flags. These values are consumed within the + * rendering function. */ -export interface StylingContext extends Array { - /** - * Location of animation context (which contains the active players) for this element styling - * context. - */ - [StylingIndex.PlayerContext]: PlayerContext|null; - - /** - * The style sanitizer that is used within this context - */ - [StylingIndex.StyleSanitizerPosition]: StyleSanitizeFn|null; - - /** - * Location of initial data shared by all instances of this style. - */ - [StylingIndex.InitialStylesPosition]: InitialStyles; - +export interface StylingContext extends + Array<{[key: string]: any}|number|string|boolean|RElement|StyleSanitizeFn|PlayerContext|null> { /** * 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 @@ -140,12 +209,27 @@ export interface StylingContext extends Array { [0]: null; } +export interface InitialStylingValues extends Array { [0]: null; } + +/** + * Used as an offset/position index to figure out where initial styling + * values are located. + * + * Used as a reference point to provide markers to all static styling + * values (the initial style and class values on an element) within an + * array within the StylingContext. This array contains key/value pairs + * where the key is the style property name or className and the value is + * the style value or whether or not a class is present on the elment. + * + * The first value is also always null so that a initial index value of + * `0` will always point to a null value. + * + * If a
elements contains a list of static styling values like so: + * + *
+ * + * Then the initial styles for that will look like so: + * + * Styles: + * StylingContext[InitialStylesIndex] = [ + * null, 'width', '100px', height, '200px' + * ] + * + * Classes: + * StylingContext[InitialStylesIndex] = [ + * null, 'foo', true, 'bar', true, 'baz', true + * ] + * + * Initial style and class entries have their own arrays. This is because + * it's easier to add to the end of one array and not then have to update + * every context entries' pointer index to the newly offseted values. + * + * When property bindinds are added to a context then initial style/class + * values will also be inserted into the array. This is to create a space + * in the situation when a follow-up directive inserts static styling into + * the array. By default style values are `null` and class values are + * `false` when inserted by property bindings. + * + * For example: + *
+ * + * Will construct initial styling values that look like: + * + * Styles: + * StylingContext[InitialStylesIndex] = [ + * null, 'width', '100px', height, '200px', 'opacity', null + * ] + * + * Classes: + * StylingContext[InitialStylesIndex] = [ + * null, 'foo', true, 'bar', true, 'baz', true, 'car', false + * ] + * + * Now if a directive comes along and introduces `car` as a static + * class value or `opacity` then those values will be filled into + * the initial styles array. + * + * For example: + * + * @Directive({ + * selector: 'opacity-car-directive', + * host: { + * 'style': 'opacity:0.5', + * 'class': 'car' + * } + * }) + * class OpacityCarDirective {} + * + * This will render itself as: + * + * Styles: + * StylingContext[InitialStylesIndex] = [ + * null, 'width', '100px', height, '200px', 'opacity', null + * ] + * + * Classes: + * StylingContext[InitialStylesIndex] = [ + * null, 'foo', true, 'bar', true, 'baz', true, 'car', false + * ] + */ +export const enum InitialStylingValuesIndex { + KeyValueStartPosition = 1, + PropOffset = 0, + ValueOffset = 1, + Size = 2 +} + +/** + * An array located in the StylingContext that houses all directive instances and additional + * data about them. + * + * Each entry in this array represents a source of where style/class binding values could + * come from. By default, there is always at least one directive here with a null value and + * that represents bindings that live directly on an element (not host bindings). + * + * Each successive entry in the array is an actual instance of an array as well as some + * additional info. + * + * An entry within this array has the following values: + * [0] = The instance of the directive (or null when it is not a directive, but a template binding + * source) + * [1] = The pointer that tells where the single styling (stuff like [class.foo] and [style.prop]) + * offset values are located. This value will allow for a binding instruction to find exactly + * where a style is located. + * [2] = Whether or not the directive has any styling values that are dirty. This is used as + * reference within the renderClassAndStyleBindings function to decide whether to skip + * iterating through the context when rendering is executed. + * [3] = The styleSanitizer instance that is assigned to the directive. Although it's unlikely, + * a directive could introduce its own special style sanitizer and for this reach each + * directive will get its own space for it (if null then the very first sanitizer is used). + * + * Each time a new directive is added it will insert these four values at the end of the array. + * When this array is examined (using indexOf) then the resulting directiveIndex will be resolved + * by dividing the index value by the size of the array entries (so if DirA is at spot 8 then its + * index will be 2). + */ +export interface DirectiveRegistryValues extends Array { + [DirectiveRegistryValuesIndex.DirectiveValueOffset]: null; + [DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset]: number; + [DirectiveRegistryValuesIndex.DirtyFlagOffset]: boolean; + [DirectiveRegistryValuesIndex.StyleSanitizerOffset]: StyleSanitizeFn|null; +} + +/** + * An enum that outlines the offset/position values for each directive entry and its data + * that are housed inside of [DirectiveRegistryValues]. + */ +export const enum DirectiveRegistryValuesIndex { + DirectiveValueOffset = 0, + SinglePropValuesIndexOffset = 1, + DirtyFlagOffset = 2, + StyleSanitizerOffset = 3, + Size = 4 +} + +/** + * An array that contains the index pointer values for every single styling property + * that exists in the context and for every directive. It also contains the total + * single styles and single classes that exists in the context as the first two values. + * + * Let's say we have the following template code: + * + *
+ * directive-with-foo-bar-classes> + * + * We have two directive and template-binding sources, + * 2 + 1 styles and 1 + 1 classes. When the bindings are + * registered the SinglePropOffsets array will look like so: + * + * s_0/c_0 = template directive value + * s_1/c_1 = directive one (directive-with-opacity) + * s_2/c_2 = directive two (directive-with-foo-bar-classes) + * + * [3, 2, 2, 1, s_00, s01, c_01, 1, 0, s_10, 0, 1, c_20 + */ +export interface SinglePropOffsetValues extends Array { + [SinglePropOffsetValuesIndex.StylesCountPosition]: number; + [SinglePropOffsetValuesIndex.ClassesCountPosition]: number; +} + +/** + * An enum that outlines the offset/position values for each single prop/class entry + * that are housed inside of [SinglePropOffsetValues]. + */ +export const enum SinglePropOffsetValuesIndex { + StylesCountPosition = 0, + ClassesCountPosition = 1, + ValueStartPosition = 2 +} /** * Used to set the context to be dirty or not both on the master flag (position 1) @@ -181,47 +447,49 @@ export interface InitialStyles extends Array { [0]: null; } */ export const enum StylingFlags { // Implies no configurations - None = 0b00000, + None = 0b000000, // Whether or not the entry or context itself is dirty - Dirty = 0b00001, + Dirty = 0b000001, // Whether or not this is a class-based assignment - Class = 0b00010, + Class = 0b000010, // Whether or not a sanitizer was applied to this property - Sanitize = 0b00100, + Sanitize = 0b000100, // Whether or not any player builders within need to produce new players - PlayerBuildersDirty = 0b01000, + PlayerBuildersDirty = 0b001000, // If NgClass is present (or some other class handler) then it will handle the map expressions and // initial classes - OnlyProcessSingleClasses = 0b10000, + OnlyProcessSingleClasses = 0b010000, // The max amount of bits used to represent these configuration values - BitCountSize = 5, - // There are only five bits here - BitMask = 0b11111 + BindingAllocationLocked = 0b100000, + BitCountSize = 6, + // There are only six bits here + BitMask = 0b111111 } /** 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 - PlayerContext = 0, - // Position of where the style sanitizer is stored within the styling context - StyleSanitizerPosition = 1, - // Position of where the initial styles are stored in the styling context - InitialStylesPosition = 2, // Index of location where the start of single properties are stored. (`updateStyleProp`) - MasterFlagPosition = 3, + MasterFlagPosition = 0, + // Position of where the registered directives exist for this styling context + DirectiveRegistryPosition = 1, + // Position of where the initial styles are stored in the styling context + InitialStyleValuesPosition = 2, + InitialClassValuesPosition = 3, // Index of location where the class index offset value is located - ClassOffsetPosition = 4, + SinglePropOffsetPositions = 4, // Position of where the initial styles are stored in the styling context // This index must align with HOST, see interfaces/view.ts ElementPosition = 5, // Position of where the last string-based CSS class value was stored (or a cached version of the // initial styles when a [class] directive is present) - PreviousOrCachedMultiClassValue = 6, + CachedClassValueOrInitialClassString = 6, // Position of where the last string-based CSS class value was stored - PreviousMultiStyleValue = 7, - // Location of single (prop) value entries are stored within the context - SingleStylesStartPosition = 8, + CachedStyleValue = 7, // Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue + // Position of where the initial styles are stored in the styling context + PlayerContext = 8, + // Location of single (prop) value entries are stored within the context + SingleStylesStartPosition = 9, FlagsOffset = 0, PropertyOffset = 1, ValueOffset = 2, @@ -233,3 +501,16 @@ export const enum StylingIndex { // The binary digit value as a mask BitMask = 0b11111111111111, // 14 bits } + +/** + * An enum that outlines the bit flag data for directive owner and player index + * values that exist within en entry that lives in the StylingContext. + * + * The values here split a number value into two sets of bits: + * - The first 16 bits are used to store the directiveIndex that owns this style value + * - The other 16 bits are used to store the playerBuilderIndex that is attached to this style + */ +export const enum DirectiveOwnerAndPlayerBuilderIndex { + BitCountSize = 16, + BitMask = 0b1111111111111111 +} diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 383f124240..89e2696718 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -12,9 +12,9 @@ import {Component, Directive} from '../../metadata/directives'; import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading'; import {ViewEncapsulation} from '../../metadata/view'; import {Type} from '../../type'; -import {stringify} from '../../util'; -import {EMPTY_ARRAY} from '../definition'; +import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../fields'; +import {stringify} from '../util'; import {R3DirectiveMetadataFacade, getCompilerFacade} from './compiler_facade'; import {R3ComponentMetadataFacade, R3QueryMetadataFacade} from './compiler_facade_interface'; @@ -154,8 +154,6 @@ function directiveMetadata(type: Type, metadata: Directive): R3DirectiveMet }; } -const EMPTY_OBJ = {}; - function convertToR3QueryPredicate(selector: any): any|string[] { return typeof selector === 'string' ? splitByComma(selector) : selector; } diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index a40d6fa0fc..1a30e41a4c 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -90,6 +90,7 @@ export const angularCoreEnv: {[name: string]: Function} = { 'ɵregisterContentQuery': r3.registerContentQuery, 'ɵreference': r3.reference, 'ɵelementStyling': r3.elementStyling, + 'ɵelementHostAttrs': r3.elementHostAttrs, 'ɵelementStylingMap': r3.elementStylingMap, 'ɵelementStyleProp': r3.elementStyleProp, 'ɵelementStylingApply': r3.elementStylingApply, diff --git a/packages/core/src/render3/styling/class_and_style_bindings.ts b/packages/core/src/render3/styling/class_and_style_bindings.ts index bf2046dd86..a84d3b7041 100644 --- a/packages/core/src/render3/styling/class_and_style_bindings.ts +++ b/packages/core/src/render3/styling/class_and_style_bindings.ts @@ -1,15 +1,17 @@ /** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ +* @license +* Copyright Google Inc. All Rights Reserved. +* +* Use of this source code is governed by an MIT-style license that can be +* found in the LICENSE file at https://angular.io/license +*/ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; -import {InitialStylingFlags} from '../interfaces/definition'; +import {assertNotEqual} from '../assert'; +import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; +import {AttributeMarker, TAttributes} from '../interfaces/node'; import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player'; -import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer'; -import {InitialStyles, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling'; +import {RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer'; +import {DirectiveOwnerAndPlayerBuilderIndex, DirectiveRegistryValues, DirectiveRegistryValuesIndex, InitialStylingValues, InitialStylingValuesIndex, SinglePropOffsetValues, SinglePropOffsetValuesIndex, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling'; import {LView, RootContext} from '../interfaces/view'; import {NO_CHANGE} from '../tokens'; import {getRootContext} from '../util'; @@ -17,150 +19,434 @@ import {getRootContext} from '../util'; import {BoundPlayerFactory} from './player_factory'; import {addPlayerInternal, allocPlayerContext, createEmptyStylingContext, getPlayerContext} from './util'; -const EMPTY_ARR: any[] = []; -const EMPTY_OBJ: {[key: string]: any} = {}; + +/** + * This file includes the code to power all styling-binding operations in Angular. + * + * These include: + * [style]="myStyleObj" + * [class]="myClassObj" + * [style.prop]="myPropValue" + * [class.name]="myClassValue" + * + * There are many different ways in which these functions below are called. Please see + * `interfaces/styles.ts` to get a better idea of how the styling algorithm works. + */ + /** - * Creates a styling context template where styling information is stored. - * Any styles that are later referenced using `updateStyleProp` must be - * passed in within this function. Initial values for those styles are to - * be declared after all initial style properties are declared (this change in - * mode between declarations and initial styles is made possible using a special - * enum value found in `definition.ts`). - * - * @param initialStyleDeclarations a list of style declarations and initial style values - * that are used later within the styling context. - * - * -> ['width', 'height', SPECIAL_ENUM_VAL, 'width', '100px'] - * This implies that `width` and `height` will be later styled and that the `width` - * property has an initial value of `100px`. - * - * @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 + * Creates a new StylingContext an fills it with the provided static styling attribute values. */ -export function createStylingContextTemplate( - initialClassDeclarations?: (string | boolean | InitialStylingFlags)[] | null, - initialStyleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, - styleSanitizer?: StyleSanitizeFn | null, onlyProcessSingleClasses?: boolean): StylingContext { - const initialStylingValues: InitialStyles = [null]; - const context: StylingContext = - createEmptyStylingContext(null, styleSanitizer, initialStylingValues); +export function initializeStaticContext(attrs: TAttributes) { + const context = createEmptyStylingContext(); + const initialClasses: InitialStylingValues = context[StylingIndex.InitialClassValuesPosition] = + [null]; + const initialStyles: InitialStylingValues = context[StylingIndex.InitialStyleValuesPosition] = + [null]; - // 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.VALUES_MODE) { - hasPassedDeclarations = true; - } else { - const prop = v as string; - if (hasPassedDeclarations) { - const value = initialStyleDeclarations[++i] as string; - initialStylingValues.push(value); - stylesLookup[prop] = initialStylingValues.length - 1; - } else { - totalStyleDeclarations++; - stylesLookup[prop] = 0; - } - } + // The attributes array has marker values (numbers) indicating what the subsequent + // values represent. When we encounter a number, we set the mode to that type of attribute. + let mode = -1; + for (let i = 0; i < attrs.length; i++) { + const attr = attrs[i]; + if (typeof attr == 'number') { + mode = attr; + } else if (mode === AttributeMarker.Styles) { + initialStyles.push(attr as string, attrs[++i] as string); + } else if (mode === AttributeMarker.Classes) { + initialClasses.push(attr as string, true); + } else if (mode === AttributeMarker.SelectOnly) { + break; } } - // make where the class offsets begin - context[StylingIndex.ClassOffsetPosition] = totalStyleDeclarations; - - const initialStaticClasses: string[]|null = onlyProcessSingleClasses ? [] : null; - 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; - initialStaticClasses && initialStaticClasses.push(className); - } 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; - - // we need to fill the array from the start so that we can access - // both the multi and the single array positions in the same loop block - for (let i = StylingIndex.SingleStylesStartPosition; i < maxLength; i++) { - context.push(null); - } - - const singleStart = StylingIndex.SingleStylesStartPosition; - const multiStart = totalProps * StylingIndex.Size + StylingIndex.SingleStylesStartPosition; - - // fill single and multi-level styles - for (let i = 0; i < 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 indexForMulti = i * StylingIndex.Size + multiStart; - const indexForSingle = i * StylingIndex.Size + singleStart; - const initialFlag = prepareInitialFlag(prop, isClassBased, styleSanitizer || null); - - setFlag(context, indexForSingle, pointers(initialFlag, indexForInitial, indexForMulti)); - setProp(context, indexForSingle, prop); - setValue(context, indexForSingle, null); - setPlayerBuilderIndex(context, indexForSingle, 0); - - const flagForMulti = - initialFlag | (initialValue !== null ? StylingFlags.Dirty : StylingFlags.None); - setFlag(context, indexForMulti, pointers(flagForMulti, indexForInitial, indexForSingle)); - setProp(context, indexForMulti, prop); - setValue(context, indexForMulti, null); - setPlayerBuilderIndex(context, indexForMulti, 0); - } - - // there is no initial value flag for the master index since it doesn't - // reference an initial style value - const masterFlag = pointers(0, 0, multiStart) | - (onlyProcessSingleClasses ? StylingFlags.OnlyProcessSingleClasses : 0); - setFlag(context, StylingIndex.MasterFlagPosition, masterFlag); - setContextDirty(context, initialStylingValues.length > 1); - - if (initialStaticClasses) { - context[StylingIndex.PreviousOrCachedMultiClassValue] = initialStaticClasses.join(' '); - } - return context; } +/** + * Designed to update an existing styling context with new static styling + * data (classes and styles). + * + * @param context the existing styling context + * @param attrs an array of new static styling attributes that will be + * assigned to the context + * @param directive the directive instance with which static data is associated with. + */ +export function patchContextWithStaticAttrs( + context: StylingContext, attrs: TAttributes, directive: any): void { + // If the styling context has already been patched with the given directive's bindings, + // then there is no point in doing it again. The reason why this may happen (the directive + // styling being patched twice) is because the `stylingBinding` function is called each time + // an element is created (both within a template function and within directive host bindings). + const directives = context[StylingIndex.DirectiveRegistryPosition]; + if (getDirectiveRegistryValuesIndexOf(directives, directive) == -1) { + // this is a new directive which we have not seen yet. + directives.push(directive, -1, false, null); + + let initialClasses: InitialStylingValues|null = null; + let initialStyles: InitialStylingValues|null = null; + + let mode = -1; + for (let i = 0; i < attrs.length; i++) { + const attr = attrs[i]; + if (typeof attr == 'number') { + mode = attr; + } else if (mode == AttributeMarker.Classes) { + initialClasses = initialClasses || context[StylingIndex.InitialClassValuesPosition]; + patchInitialStylingValue(initialClasses, attr, true); + } else if (mode == AttributeMarker.Styles) { + initialStyles = initialStyles || context[StylingIndex.InitialStyleValuesPosition]; + patchInitialStylingValue(initialStyles, attr, attrs[++i]); + } + } + } +} + +/** + * Designed to add a style or class value into the existing set of initial styles. + * + * The function will search and figure out if a style/class value is already present + * within the provided initial styling array. If and when a style/class value is not + * present (or if it's value is falsy) then it will be inserted/updated in the list + * of initial styling values. + */ +function patchInitialStylingValue( + initialStyling: InitialStylingValues, prop: string, value: any): void { + // Even values are keys; Odd numbers are values; Search keys only + for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < initialStyling.length;) { + const key = initialStyling[i]; + if (key === prop) { + const existingValue = initialStyling[i + InitialStylingValuesIndex.ValueOffset]; + + // If there is no previous style value (when `null`) or no previous class + // applied (when `false`) then we update the the newly given value. + if (existingValue == null || existingValue == false) { + initialStyling[i + InitialStylingValuesIndex.ValueOffset] = value; + } + return; + } + i = i + InitialStylingValuesIndex.Size; + } + // We did not find existing key, add a new one. + initialStyling.push(prop, value); +} + +/** + * Runs through the initial styling data present in the context and renders + * them via the renderer on the element. + */ +export function renderInitialStylesAndClasses( + element: RElement, context: StylingContext, renderer: Renderer3) { + const initialClasses = context[StylingIndex.InitialClassValuesPosition]; + renderInitialStylingValues(element, renderer, initialClasses, true); + + const initialStyles = context[StylingIndex.InitialStyleValuesPosition]; + renderInitialStylingValues(element, renderer, initialStyles, false); +} + +/** + * This is a helper function designed to render each entry present within the + * provided list of initialStylingValues. + */ +function renderInitialStylingValues( + element: RElement, renderer: Renderer3, initialStylingValues: InitialStylingValues, + isEntryClassBased: boolean) { + for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < initialStylingValues.length; + i += InitialStylingValuesIndex.Size) { + const value = initialStylingValues[i + InitialStylingValuesIndex.ValueOffset]; + if (value) { + if (isEntryClassBased) { + setClass( + element, initialStylingValues[i + InitialStylingValuesIndex.PropOffset] as string, true, + renderer, null); + } else { + setStyle( + element, initialStylingValues[i + InitialStylingValuesIndex.PropOffset] as string, + value as string, renderer, null); + } + } + } +} + +export function allowNewBindingsForStylingContext(context: StylingContext): boolean { + return (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) === 0; +} + +/** + * Adds in new binding values to a styling context. + * + * If a directive value is provided then all provided class/style binding names will + * reference the provided directive. + * + * @param context the existing styling context + * @param directiveRef the directive that the new bindings will reference + * @param classBindingNames an array of class binding names that will be added to the context + * @param styleBindingNames an array of style binding names that will be added to the context + * @param styleSanitizer an optional sanitizer that handle all sanitization on for each of + * the bindings added to the context. Note that if a directive is provided then the sanitizer + * instance will only be active if and when the directive updates the bindings that it owns. + */ +export function updateContextWithBindings( + context: StylingContext, directiveRef: any | null, classBindingNames?: string[] | null, + styleBindingNames?: string[] | null, styleSanitizer?: StyleSanitizeFn | null, + onlyProcessSingleClasses?: boolean) { + if (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) return; + + // this means the context has already been patched with the directive's bindings + const directiveIndex = findOrPatchDirectiveIntoRegistry(context, directiveRef, styleSanitizer); + if (directiveIndex === -1) { + // this means the directive has already been patched in ... No point in doing anything + return; + } + + // there are alot of variables being used below to track where in the context the new + // binding values will be placed. Because the context consists of multiple types of + // entries (single classes/styles and multi classes/styles) alot of the index positions + // need to be computed ahead of time and the context needs to be extended before the values + // are inserted in. + const singlePropOffsetValues = context[StylingIndex.SinglePropOffsetPositions]; + const totalCurrentClassBindings = + singlePropOffsetValues[SinglePropOffsetValuesIndex.ClassesCountPosition]; + const totalCurrentStyleBindings = + singlePropOffsetValues[SinglePropOffsetValuesIndex.StylesCountPosition]; + + const classesOffset = totalCurrentClassBindings * StylingIndex.Size; + const stylesOffset = totalCurrentStyleBindings * StylingIndex.Size; + + const singleStylesStartIndex = StylingIndex.SingleStylesStartPosition; + let singleClassesStartIndex = singleStylesStartIndex + stylesOffset; + let multiStylesStartIndex = singleClassesStartIndex + classesOffset; + let multiClassesStartIndex = multiStylesStartIndex + stylesOffset; + + // because we're inserting more bindings into the context, this means that the + // binding values need to be referenced the singlePropOffsetValues array so that + // the template/directive can easily find them inside of the `elementStyleProp` + // and the `elementClassProp` functions without iterating through the entire context. + // The first step to setting up these reference points is to mark how many bindings + // are being added. Even if these bindings already exist in the context, the directive + // or template code will still call them unknowingly. Therefore the total values need + // to be registered so that we know how many bindings are assigned to each directive. + const currentSinglePropsLength = singlePropOffsetValues.length; + singlePropOffsetValues.push( + styleBindingNames ? styleBindingNames.length : 0, + classBindingNames ? classBindingNames.length : 0); + + // the code below will check to see if a new style binding already exists in the context + // if so then there is no point in inserting it into the context again. Whether or not it + // exists the styling offset code will now know exactly where it is + let insertionOffset = 0; + const filteredStyleBindingNames: string[] = []; + if (styleBindingNames && styleBindingNames.length) { + for (let i = 0; i < styleBindingNames.length; i++) { + const name = styleBindingNames[i]; + let singlePropIndex = + getMatchingBindingIndex(context, name, singleStylesStartIndex, singleClassesStartIndex); + if (singlePropIndex == -1) { + singlePropIndex = singleClassesStartIndex + insertionOffset; + insertionOffset += StylingIndex.Size; + filteredStyleBindingNames.push(name); + } + singlePropOffsetValues.push(singlePropIndex); + } + } + + // just like with the style binding loop above, the new class bindings get the same treatment... + const filteredClassBindingNames: string[] = []; + if (classBindingNames && classBindingNames.length) { + for (let i = 0; i < classBindingNames.length; i++) { + const name = classBindingNames[i]; + let singlePropIndex = + getMatchingBindingIndex(context, name, singleClassesStartIndex, multiStylesStartIndex); + if (singlePropIndex == -1) { + singlePropIndex = multiStylesStartIndex + insertionOffset; + insertionOffset += StylingIndex.Size; + filteredClassBindingNames.push(name); + } else { + singlePropIndex += filteredStyleBindingNames.length * StylingIndex.Size; + } + singlePropOffsetValues.push(singlePropIndex); + } + } + + // because new styles are being inserted, this means the existing collection of style offset + // index values are incorrect (they point to the wrong values). The code below will run through + // the entire offset array and update the existing set of index values to point to their new + // locations while taking the new binding values into consideration. + let i = SinglePropOffsetValuesIndex.ValueStartPosition; + if (filteredStyleBindingNames.length) { + while (i < currentSinglePropsLength) { + const totalStyles = + singlePropOffsetValues[i + SinglePropOffsetValuesIndex.StylesCountPosition]; + const totalClasses = + singlePropOffsetValues[i + SinglePropOffsetValuesIndex.ClassesCountPosition]; + if (totalClasses) { + const start = i + SinglePropOffsetValuesIndex.ValueStartPosition + totalStyles; + for (let j = start; j < start + totalClasses; j++) { + singlePropOffsetValues[j] += filteredStyleBindingNames.length * StylingIndex.Size; + } + } + + const total = totalStyles + totalClasses; + i += SinglePropOffsetValuesIndex.ValueStartPosition + total; + } + } + + const totalNewEntries = filteredClassBindingNames.length + filteredStyleBindingNames.length; + + // in the event that there are new style values being inserted, all existing class and style + // bindings need to have their pointer values offsetted with the new amount of space that is + // used for the new style/class bindings. + for (let i = singleStylesStartIndex; i < context.length; i += StylingIndex.Size) { + const isMultiBased = i >= multiStylesStartIndex; + const isClassBased = i >= (isMultiBased ? multiClassesStartIndex : singleClassesStartIndex); + const flag = getPointers(context, i); + const staticIndex = getInitialIndex(flag); + let singleOrMultiIndex = getMultiOrSingleIndex(flag); + if (isMultiBased) { + singleOrMultiIndex += + isClassBased ? (filteredStyleBindingNames.length * StylingIndex.Size) : 0; + } else { + singleOrMultiIndex += (totalNewEntries * StylingIndex.Size) + + ((isClassBased ? filteredStyleBindingNames.length : 0) * StylingIndex.Size); + } + setFlag(context, i, pointers(flag, staticIndex, singleOrMultiIndex)); + } + + // this is where we make space in the context for the new style bindings + for (let i = 0; i < filteredStyleBindingNames.length * StylingIndex.Size; i++) { + context.splice(multiClassesStartIndex, 0, null); + context.splice(singleClassesStartIndex, 0, null); + singleClassesStartIndex++; + multiStylesStartIndex++; + multiClassesStartIndex += 2; // both single + multi slots were inserted + } + + // this is where we make space in the context for the new class bindings + for (let i = 0; i < filteredClassBindingNames.length * StylingIndex.Size; i++) { + context.splice(multiStylesStartIndex, 0, null); + context.push(null); + multiStylesStartIndex++; + multiClassesStartIndex++; + } + + const initialClasses = context[StylingIndex.InitialClassValuesPosition]; + const initialStyles = context[StylingIndex.InitialStyleValuesPosition]; + + // the code below will insert each new entry into the context and assign the appropriate + // flags and index values to them. It's important this runs at the end of this function + // because the context, property offset and index values have all been computed just before. + for (let i = 0; i < totalNewEntries; i++) { + const entryIsClassBased = i >= filteredStyleBindingNames.length; + const adjustedIndex = entryIsClassBased ? (i - filteredStyleBindingNames.length) : i; + const propName = entryIsClassBased ? filteredClassBindingNames[adjustedIndex] : + filteredStyleBindingNames[adjustedIndex]; + + let multiIndex, singleIndex; + if (entryIsClassBased) { + multiIndex = multiClassesStartIndex + + ((totalCurrentClassBindings + adjustedIndex) * StylingIndex.Size); + singleIndex = singleClassesStartIndex + + ((totalCurrentClassBindings + adjustedIndex) * StylingIndex.Size); + } else { + multiIndex = + multiStylesStartIndex + ((totalCurrentStyleBindings + adjustedIndex) * StylingIndex.Size); + singleIndex = singleStylesStartIndex + + ((totalCurrentStyleBindings + adjustedIndex) * StylingIndex.Size); + } + + // if a property is not found in the initial style values list then it + // is ALWAYS added incase a follow-up directive introduces the same initial + // style/class value later on. + let initialValuesToLookup = entryIsClassBased ? initialClasses : initialStyles; + let indexForInitial = getInitialStylingValuesIndexOf(initialValuesToLookup, propName); + if (indexForInitial === -1) { + indexForInitial = initialValuesToLookup.length + InitialStylingValuesIndex.ValueOffset; + initialValuesToLookup.push(propName, entryIsClassBased ? false : null); + } else { + indexForInitial += InitialStylingValuesIndex.ValueOffset; + } + + const initialFlag = + prepareInitialFlag(context, propName, entryIsClassBased, styleSanitizer || null); + + setFlag(context, singleIndex, pointers(initialFlag, indexForInitial, multiIndex)); + setProp(context, singleIndex, propName); + setValue(context, singleIndex, null); + setPlayerBuilderIndex(context, singleIndex, 0, directiveIndex); + + setFlag(context, multiIndex, pointers(initialFlag, indexForInitial, singleIndex)); + setProp(context, multiIndex, propName); + setValue(context, multiIndex, null); + setPlayerBuilderIndex(context, multiIndex, 0, directiveIndex); + } + + // the total classes/style values are updated so the next time the context is patched + // additional style/class bindings from another directive then it knows exactly where + // to insert them in the context + singlePropOffsetValues[SinglePropOffsetValuesIndex.ClassesCountPosition] = + totalCurrentClassBindings + filteredClassBindingNames.length; + singlePropOffsetValues[SinglePropOffsetValuesIndex.StylesCountPosition] = + totalCurrentStyleBindings + filteredStyleBindingNames.length; + + // there is no initial value flag for the master index since it doesn't + // reference an initial style value + const masterFlag = pointers(0, 0, multiStylesStartIndex) | + (onlyProcessSingleClasses ? StylingFlags.OnlyProcessSingleClasses : 0); + setFlag(context, StylingIndex.MasterFlagPosition, masterFlag); +} + +/** + * Searches through the existing registry of directives + */ +function findOrPatchDirectiveIntoRegistry( + context: StylingContext, directiveRef: any, styleSanitizer?: StyleSanitizeFn | null) { + const directiveRefs = context[StylingIndex.DirectiveRegistryPosition]; + const nextOffsetInsertionIndex = context[StylingIndex.SinglePropOffsetPositions].length; + + let directiveIndex: number; + const detectedIndex = getDirectiveRegistryValuesIndexOf(directiveRefs, directiveRef); + + if (detectedIndex === -1) { + directiveIndex = directiveRefs.length / DirectiveRegistryValuesIndex.Size; + directiveRefs.push(directiveRef, nextOffsetInsertionIndex, false, styleSanitizer || null); + } else { + const singlePropStartPosition = + detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset; + if (directiveRefs[singlePropStartPosition] ! >= 0) { + // the directive has already been patched into the context + return -1; + } + + directiveIndex = detectedIndex / DirectiveRegistryValuesIndex.Size; + + // because the directive already existed this means that it was set during elementHostAttrs or + // elementStart which means that the binding values were not here. Therefore, the values below + // need to be applied so that single class and style properties can be assigned later. + const singlePropPositionIndex = + detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset; + directiveRefs[singlePropPositionIndex] = nextOffsetInsertionIndex; + + // the sanitizer is also apart of the binding process and will be used when bindings are + // applied. + const styleSanitizerIndex = detectedIndex + DirectiveRegistryValuesIndex.StyleSanitizerOffset; + directiveRefs[styleSanitizerIndex] = styleSanitizer || null; + } + + return directiveIndex; +} + +function getMatchingBindingIndex( + context: StylingContext, bindingName: string, start: number, end: number) { + for (let j = start; j < end; j += StylingIndex.Size) { + if (getProp(context, j) === bindingName) return j; + } + return -1; +} + /** * Sets and resolves all `multi` styling on an `StylingContext` so that they can be - * applied to the element once `renderStyleAndClassBindings` is called. + * applied to the element once `renderStyling` is called. * * 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 @@ -175,9 +461,11 @@ export function updateStylingMap( context: StylingContext, classesInput: {[key: string]: any} | string | BoundPlayerFactory| NO_CHANGE | null, stylesInput?: {[key: string]: any} | BoundPlayerFactory| NO_CHANGE | - null): void { + null, + directiveRef?: any): void { stylesInput = stylesInput || null; + const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null); const element = context[StylingIndex.ElementPosition] !as HTMLElement; const classesPlayerBuilder = classesInput instanceof BoundPlayerFactory ? new ClassAndStylePlayerBuilder(classesInput as any, element, BindingType.Class) : @@ -192,15 +480,15 @@ export function updateStylingMap( const stylesValue = stylesPlayerBuilder ? stylesInput !.value : stylesInput; // early exit (this is what's done to avoid using ctx.bind() to cache the value) const ignoreAllClassUpdates = limitToSingleClasses(context) || classesValue === NO_CHANGE || - classesValue === context[StylingIndex.PreviousOrCachedMultiClassValue]; + classesValue === context[StylingIndex.CachedClassValueOrInitialClassString]; const ignoreAllStyleUpdates = - stylesValue === NO_CHANGE || stylesValue === context[StylingIndex.PreviousMultiStyleValue]; + stylesValue === NO_CHANGE || stylesValue === context[StylingIndex.CachedStyleValue]; if (ignoreAllClassUpdates && ignoreAllStyleUpdates) return; - context[StylingIndex.PreviousOrCachedMultiClassValue] = classesValue; - context[StylingIndex.PreviousMultiStyleValue] = stylesValue; + context[StylingIndex.CachedClassValueOrInitialClassString] = classesValue; + context[StylingIndex.CachedStyleValue] = stylesValue; - let classNames: string[] = EMPTY_ARR; + let classNames: string[] = EMPTY_ARRAY; let applyAllClasses = false; let playerBuildersAreDirty = false; @@ -229,16 +517,16 @@ export function updateStylingMap( // since a classname string implies that all those classes are added applyAllClasses = true; } else { - classNames = classesValue ? Object.keys(classesValue) : EMPTY_ARR; + classNames = classesValue ? Object.keys(classesValue) : EMPTY_ARRAY; } } const classes = (classesValue || EMPTY_OBJ) as{[key: string]: any}; - const styleProps = stylesValue ? Object.keys(stylesValue) : EMPTY_ARR; + const styleProps = stylesValue ? Object.keys(stylesValue) : EMPTY_ARRAY; const styles = stylesValue || EMPTY_OBJ; const classesStartIndex = styleProps.length; - const multiStartIndex = getMultiStartIndex(context); + let multiStartIndex = getMultiStartIndex(context); let dirty = false; let ctxIndex = multiStartIndex; @@ -269,7 +557,7 @@ export function updateStylingMap( if (prop === newProp) { const value = getValue(context, ctxIndex); const flag = getPointers(context, ctxIndex); - setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex); + setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex); if (hasValueChanged(flag, value, newValue)) { setValue(context, ctxIndex, newValue); @@ -277,9 +565,13 @@ export function updateStylingMap( 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 (hasValueChanged(flag, initialValue, newValue)) { + // SKIP IF INITIAL CHECK + // If the former `value` is `null` then it means that an initial value + // could be being rendered on screen. If that is the case then there is + // no point in updating the value incase it matches. In other words if the + // new value is the exact same as the previously rendered value (which + // happens to be the initial value) then do nothing. + if (value != null || hasValueChanged(flag, initialValue, newValue)) { setDirty(context, ctxIndex, true); dirty = true; } @@ -294,7 +586,9 @@ export function updateStylingMap( if (hasValueChanged(flagToCompare, valueToCompare, newValue)) { const initialValue = getInitialValue(context, flagToCompare); setValue(context, ctxIndex, newValue); - if (hasValueChanged(flagToCompare, initialValue, newValue)) { + + // same if statement logic as above (look for SKIP IF INITIAL CHECK). + if (valueToCompare != null || hasValueChanged(flagToCompare, initialValue, newValue)) { setDirty(context, ctxIndex, true); playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex; dirty = true; @@ -302,10 +596,12 @@ export function updateStylingMap( } } else { // we only care to do this if the insertion is in the middle - const newFlag = prepareInitialFlag(newProp, isClassBased, getStyleSanitizer(context)); + const newFlag = prepareInitialFlag( + context, newProp, isClassBased, getStyleSanitizer(context, directiveIndex)); playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex; insertNewMultiProperty( - context, ctxIndex, isClassBased, newProp, newFlag, newValue, playerBuilderIndex); + context, ctxIndex, isClassBased, newProp, newFlag, newValue, directiveIndex, + playerBuilderIndex); dirty = true; } } @@ -335,7 +631,7 @@ export function updateStylingMap( // is a valid animation player instruction. const playerBuilderIndex = isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex; - setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex); + setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex); dirty = true; } } @@ -345,7 +641,7 @@ export function updateStylingMap( // 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 - const sanitizer = getStyleSanitizer(context); + const sanitizer = getStyleSanitizer(context, directiveIndex); while (propIndex < propLimit) { const isClassBased = propIndex >= classesStartIndex; const processValue = @@ -355,10 +651,12 @@ export function updateStylingMap( const prop = isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex]; const value: string|boolean = isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop]; - const flag = prepareInitialFlag(prop, isClassBased, sanitizer) | StylingFlags.Dirty; + const flag = prepareInitialFlag(context, prop, isClassBased, sanitizer) | StylingFlags.Dirty; const playerBuilderIndex = isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex; - context.push(flag, prop, value, playerBuilderIndex); + const ctxIndex = context.length; + context.push(flag, prop, value, 0); + setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex); dirty = true; } propIndex++; @@ -366,6 +664,7 @@ export function updateStylingMap( if (dirty) { setContextDirty(context, true); + setDirectiveDirty(context, directiveIndex, true); } if (playerBuildersAreDirty) { @@ -374,8 +673,23 @@ export function updateStylingMap( } /** - * Sets and resolves a single styling property/value on the provided `StylingContext` so - * that they can be applied to the element once `renderStyleAndClassBindings` is called. + * 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 offset 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, offset: number, addOrRemove: boolean | BoundPlayerFactory, + directiveRef?: any): void { + _updateSingleStylingValue(context, offset, addOrRemove, true, directiveRef); +} + +/** + * Sets and resolves a single style value on the provided `StylingContext` so + * that they can be applied to the element once `renderStyling` is called. * * 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 @@ -384,19 +698,33 @@ export function updateStylingMap( * * @param context The styling context that will be updated with the * newly provided style value. - * @param index The index of the property which is being updated. + * @param offset The index of the property which is being updated. * @param value The CSS style value that will be assigned + * @param directiveRef an optional reference to the directive responsible + * for this binding change. If present then style binding will only + * actualize if the directive has ownership over this binding + * (see styling.ts#directives for more information about the algorithm). */ export function updateStyleProp( - context: StylingContext, index: number, - input: string | boolean | null | BoundPlayerFactory): void { - const singleIndex = StylingIndex.SingleStylesStartPosition + index * StylingIndex.Size; + context: StylingContext, offset: number, + input: string | boolean | null | BoundPlayerFactory, + directiveRef?: any): void { + _updateSingleStylingValue(context, offset, input, false, directiveRef); +} + +function _updateSingleStylingValue( + context: StylingContext, offset: number, + input: string | boolean | null | BoundPlayerFactory, isClassBased: boolean, + directiveRef: any): void { + const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null); + const singleIndex = getSinglePropIndexValue(context, directiveIndex, offset, isClassBased); const currValue = getValue(context, singleIndex); const currFlag = getPointers(context, singleIndex); + const currDirective = getDirectiveIndexFromEntry(context, singleIndex); const value: string|boolean|null = (input instanceof BoundPlayerFactory) ? input.value : input; - // didn't change ... nothing to make a note of - if (hasValueChanged(currFlag, currValue, value)) { + if (hasValueChanged(currFlag, currValue, value) && + allowValueChange(currValue, value, currDirective, directiveIndex)) { const isClassBased = (currFlag & StylingFlags.Class) === StylingFlags.Class; const element = context[StylingIndex.ElementPosition] !as HTMLElement; const playerBuilder = input instanceof BoundPlayerFactory ? @@ -412,10 +740,19 @@ export function updateStyleProp( if (hasPlayerBuilderChanged(context, playerBuilder, currPlayerIndex)) { const newIndex = setPlayerBuilder(context, playerBuilder, currPlayerIndex); playerBuilderIndex = playerBuilder ? newIndex : 0; - setPlayerBuilderIndex(context, singleIndex, playerBuilderIndex); playerBuildersAreDirty = true; } + if (playerBuildersAreDirty || currDirective !== directiveIndex) { + setPlayerBuilderIndex(context, singleIndex, playerBuilderIndex, directiveIndex); + } + + if (currDirective !== directiveIndex) { + const prop = getProp(context, singleIndex); + const sanitizer = getStyleSanitizer(context, directiveIndex); + setSanitizeFlag(context, singleIndex, (sanitizer && sanitizer(prop)) ? true : false); + } + // the value will always get updated (even if the dirty flag is skipped) setValue(context, singleIndex, value); const indexForMulti = getMultiOrSingleIndex(currFlag); @@ -434,6 +771,7 @@ export function updateStyleProp( setDirty(context, indexForMulti, multiDirty); setDirty(context, singleIndex, singleDirty); + setDirectiveDirty(context, directiveIndex, true); setContextDirty(context, true); } @@ -443,21 +781,6 @@ export function updateStyleProp( } } -/** - * 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 | BoundPlayerFactory): void { - const adjustedIndex = index + context[StylingIndex.ClassOffsetPosition]; - updateStyleProp(context, adjustedIndex, addOrRemove); -} /** * Renders all queued styling using a renderer onto the given element. @@ -476,29 +799,41 @@ export function updateClassProp( * to this key/value map instead of being renderered via the renderer. * @param stylesStore if provided, the updated style values will be applied * to this key/value map instead of being renderered via the renderer. + * @param directiveRef an optional directive that will be used to target which + * styling values are rendered. If left empty, only the bindings that are + * registered on the template will be rendered. * @returns number the total amount of players that got queued for animation (if any) */ -export function renderStyleAndClassBindings( +export function renderStyling( context: StylingContext, renderer: Renderer3, rootOrView: RootContext | LView, - isFirstRender: boolean, classesStore?: BindingStore | null, - stylesStore?: BindingStore | null): number { + isFirstRender: boolean, classesStore?: BindingStore | null, stylesStore?: BindingStore | null, + directiveRef?: any): number { let totalPlayersQueued = 0; + const targetDirectiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null); - if (isContextDirty(context)) { + if (isContextDirty(context) && isDirectiveDirty(context, targetDirectiveIndex)) { const flushPlayerBuilders: any = context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty; const native = context[StylingIndex.ElementPosition] !; const multiStartIndex = getMultiStartIndex(context); - const styleSanitizer = getStyleSanitizer(context); const onlySingleClasses = limitToSingleClasses(context); + let stillDirty = false; for (let i = StylingIndex.SingleStylesStartPosition; i < context.length; i += StylingIndex.Size) { // there is no point in rendering styles that have not changed on screen if (isDirty(context, i)) { + const flag = getPointers(context, i); + const directiveIndex = getDirectiveIndexFromEntry(context, i); + if (targetDirectiveIndex !== directiveIndex) { + stillDirty = true; + continue; + } + const prop = getProp(context, i); const value = getValue(context, i); - const flag = getPointers(context, i); + const styleSanitizer = + (flag & StylingFlags.Sanitize) ? getStyleSanitizer(context, directiveIndex) : null; const playerBuilder = getPlayerBuilder(context, i); const isClassBased = flag & StylingFlags.Class ? true : false; const isInSingleRegion = i < multiStartIndex; @@ -521,7 +856,9 @@ export function renderStyleAndClassBindings( // 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) && readInitialValue) { + // Note that we ignore class-based deferals because otherwise a class can never + // be removed in the case that it exists as true in the initial classes list... + if (!isClassBased && !valueExists(valueToApply, isClassBased) && readInitialValue) { valueToApply = getInitialValue(context, flag); } @@ -535,9 +872,8 @@ export function renderStyleAndClassBindings( setClass( native, prop, valueToApply ? true : false, renderer, classesStore, playerBuilder); } else { - const sanitizer = (flag & StylingFlags.Sanitize) ? styleSanitizer : null; setStyle( - native, prop, valueToApply as string | null, renderer, sanitizer, stylesStore, + native, prop, valueToApply as string | null, renderer, styleSanitizer, stylesStore, playerBuilder); } } @@ -576,7 +912,9 @@ export function renderStyleAndClassBindings( } setContextPlayersDirty(context, false); } - setContextDirty(context, false); + + setDirectiveDirty(context, targetDirectiveIndex, false); + setContextDirty(context, stillDirty); } return totalPlayersQueued; @@ -607,6 +945,8 @@ export function setStyle( playerBuilder.setValue(prop, value); } } else if (value) { + value = value.toString(); // opacity, z-index and flexbox all have number values which may not + // assign as numbers ngDevMode && ngDevMode.rendererSetStyle++; isProceduralRenderer(renderer) ? renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) : @@ -652,6 +992,14 @@ function setClass( } } +function setSanitizeFlag(context: StylingContext, index: number, sanitizeYes: boolean) { + if (sanitizeYes) { + (context[index] as number) |= StylingFlags.Sanitize; + } else { + (context[index] as number) &= ~StylingFlags.Sanitize; + } +} + function setDirty(context: StylingContext, index: number, isDirtyYes: boolean) { const adjustedIndex = index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index; @@ -668,7 +1016,7 @@ function isDirty(context: StylingContext, index: number): boolean { return ((context[adjustedIndex] as number) & StylingFlags.Dirty) == StylingFlags.Dirty; } -export function isClassBased(context: StylingContext, index: number): boolean { +export function isClassBasedValue(context: StylingContext, index: number): boolean { const adjustedIndex = index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index; return ((context[adjustedIndex] as number) & StylingFlags.Class) == StylingFlags.Class; @@ -685,9 +1033,12 @@ function pointers(configFlag: number, staticIndex: number, dynamicIndex: number) (dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize)); } -function getInitialValue(context: StylingContext, flag: number): string|null { +function getInitialValue(context: StylingContext, flag: number): string|boolean|null { const index = getInitialIndex(flag); - return context[StylingIndex.InitialStylesPosition][index] as null | string; + const entryIsClassBased = flag & StylingFlags.Class; + const initialValues = entryIsClassBased ? context[StylingIndex.InitialClassValuesPosition] : + context[StylingIndex.InitialStyleValuesPosition]; + return initialValues[index]; } function getInitialIndex(flag: number): number { @@ -704,10 +1055,6 @@ function getMultiStartIndex(context: StylingContext): number { return getMultiOrSingleIndex(context[StylingIndex.MasterFlagPosition]) as number; } -function getStyleSanitizer(context: StylingContext): StyleSanitizeFn|null { - return context[StylingIndex.StyleSanitizerPosition]; -} - function setProp(context: StylingContext, index: number, prop: string) { context[index + StylingIndex.PropertyOffset] = prop; } @@ -744,12 +1091,21 @@ function setPlayerBuilder( return insertionIndex; } -function setPlayerBuilderIndex(context: StylingContext, index: number, playerBuilderIndex: number) { - context[index + StylingIndex.PlayerBuilderIndexOffset] = playerBuilderIndex; +export function directiveOwnerPointers(directiveIndex: number, playerIndex: number) { + return (playerIndex << DirectiveOwnerAndPlayerBuilderIndex.BitCountSize) | directiveIndex; +} + +function setPlayerBuilderIndex( + context: StylingContext, index: number, playerBuilderIndex: number, directiveIndex: number) { + const value = directiveOwnerPointers(directiveIndex, playerBuilderIndex); + context[index + StylingIndex.PlayerBuilderIndexOffset] = value; } function getPlayerBuilderIndex(context: StylingContext, index: number): number { - return (context[index + StylingIndex.PlayerBuilderIndexOffset] as number) || 0; + const flag = context[index + StylingIndex.PlayerBuilderIndexOffset] as number; + const playerBuilderIndex = (flag >> DirectiveOwnerAndPlayerBuilderIndex.BitCountSize) & + DirectiveOwnerAndPlayerBuilderIndex.BitMask; + return playerBuilderIndex; } function getPlayerBuilder(context: StylingContext, index: number): ClassAndStylePlayerBuilder| @@ -842,12 +1198,14 @@ function swapMultiContextEntries(context: StylingContext, indexA: number, indexB setValue(context, indexA, getValue(context, indexB)); setProp(context, indexA, getProp(context, indexB)); setFlag(context, indexA, getPointers(context, indexB)); - setPlayerBuilderIndex(context, indexA, getPlayerBuilderIndex(context, indexB)); + const playerIndexA = getPlayerBuilderIndex(context, indexB); + const directiveIndexA = 0; + setPlayerBuilderIndex(context, indexA, playerIndexA, directiveIndexA); setValue(context, indexB, tmpValue); setProp(context, indexB, tmpProp); setFlag(context, indexB, tmpFlag); - setPlayerBuilderIndex(context, indexB, tmpPlayerBuilderIndex); + setPlayerBuilderIndex(context, indexB, tmpPlayerBuilderIndex, directiveIndexA); } function updateSinglePointerValues(context: StylingContext, indexStartPosition: number) { @@ -858,7 +1216,7 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition: const singleFlag = getPointers(context, singleIndex); const initialIndexForSingle = getInitialIndex(singleFlag); const flagValue = (isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None) | - (isClassBased(context, singleIndex) ? StylingFlags.Class : StylingFlags.None) | + (isClassBasedValue(context, singleIndex) ? StylingFlags.Class : StylingFlags.None) | (isSanitizable(context, singleIndex) ? StylingFlags.Sanitize : StylingFlags.None); const updatedFlag = pointers(flagValue, initialIndexForSingle, i); setFlag(context, singleIndex, updatedFlag); @@ -868,13 +1226,14 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition: function insertNewMultiProperty( context: StylingContext, index: number, classBased: boolean, name: string, flag: number, - value: string | boolean, playerIndex: number): void { + value: string | boolean, directiveIndex: number, playerIndex: number): void { const doShift = index < context.length; // prop does not exist in the list, add it in context.splice( index, 0, flag | StylingFlags.Dirty | (classBased ? StylingFlags.Class : StylingFlags.None), - name, value, playerIndex); + name, value, 0); + setPlayerBuilderIndex(context, index, playerIndex, directiveIndex); if (doShift) { // because the value was inserted midway into the array then we @@ -892,13 +1251,22 @@ function valueExists(value: string | null | boolean, isClassBased?: boolean) { } function prepareInitialFlag( - name: string, isClassBased: boolean, sanitizer?: StyleSanitizeFn | null) { - if (isClassBased) { - return StylingFlags.Class; - } else if (sanitizer && sanitizer(name)) { - return StylingFlags.Sanitize; + context: StylingContext, prop: string, entryIsClassBased: boolean, + sanitizer?: StyleSanitizeFn | null) { + let flag = (sanitizer && sanitizer(prop)) ? StylingFlags.Sanitize : StylingFlags.None; + + let initialIndex: number; + if (entryIsClassBased) { + flag |= StylingFlags.Class; + initialIndex = + getInitialStylingValuesIndexOf(context[StylingIndex.InitialClassValuesPosition], prop); + } else { + initialIndex = + getInitialStylingValuesIndexOf(context[StylingIndex.InitialStyleValuesPosition], prop); } - return StylingFlags.None; + + initialIndex = initialIndex > 0 ? (initialIndex + InitialStylingValuesIndex.ValueOffset) : 0; + return pointers(flag, initialIndex, 0); } function hasValueChanged( @@ -949,3 +1317,223 @@ export class ClassAndStylePlayerBuilder implements PlayerBuilder { return undefined; } } + +/** + * Used to provide a summary of the state of the styling context. + * + * This is an internal interface that is only used inside of test tooling to + * help summarize what's going on within the styling context. None of this code + * is designed to be exported publicly and will, therefore, be tree-shaken away + * during runtime. + */ +export interface LogSummary { + name: string; // + staticIndex: number; // + dynamicIndex: number; // + value: number; // + flags: { + dirty: boolean; // + class: boolean; // + sanitize: boolean; // + playerBuildersDirty: boolean; // + onlyProcessSingleClasses: boolean; // + bindingAllocationLocked: boolean; // + }; +} + +/** + * This function is not designed to be used in production. + * It is a utility tool for debugging and testing and it + * will automatically be tree-shaken away during production. + */ +export function generateConfigSummary(source: number): LogSummary; +export function generateConfigSummary(source: StylingContext): LogSummary; +export function generateConfigSummary(source: StylingContext, index: number): LogSummary; +export function generateConfigSummary(source: number | StylingContext, index?: number): LogSummary { + let flag, name = 'config value for '; + if (Array.isArray(source)) { + if (index) { + name += 'index: ' + index; + } else { + name += 'master config'; + } + index = index || StylingIndex.MasterFlagPosition; + flag = source[index] as number; + } else { + flag = source; + name += 'index: ' + flag; + } + const dynamicIndex = getMultiOrSingleIndex(flag); + const staticIndex = getInitialIndex(flag); + return { + name, + staticIndex, + dynamicIndex, + value: flag, + flags: { + dirty: flag & StylingFlags.Dirty ? true : false, + class: flag & StylingFlags.Class ? true : false, + sanitize: flag & StylingFlags.Sanitize ? true : false, + playerBuildersDirty: flag & StylingFlags.PlayerBuildersDirty ? true : false, + onlyProcessSingleClasses: flag & StylingFlags.OnlyProcessSingleClasses ? true : false, + bindingAllocationLocked: flag & StylingFlags.BindingAllocationLocked ? true : false, + } + }; +} + +export function getDirectiveIndexFromEntry(context: StylingContext, index: number) { + const value = context[index + StylingIndex.PlayerBuilderIndexOffset] as number; + return value & DirectiveOwnerAndPlayerBuilderIndex.BitMask; +} + +function getDirectiveIndexFromRegistry(context: StylingContext, directive: any) { + const index = + getDirectiveRegistryValuesIndexOf(context[StylingIndex.DirectiveRegistryPosition], directive); + ngDevMode && + assertNotEqual( + index, -1, + `The provided directive ${directive} has not been allocated to the element\'s style/class bindings`); + return index > 0 ? index / DirectiveRegistryValuesIndex.Size : 0; + // return index / DirectiveRegistryValuesIndex.Size; +} + +function getDirectiveRegistryValuesIndexOf( + directives: DirectiveRegistryValues, directive: {}): number { + for (let i = 0; i < directives.length; i += DirectiveRegistryValuesIndex.Size) { + if (directives[i] === directive) { + return i; + } + } + return -1; +} + +function getInitialStylingValuesIndexOf(keyValues: InitialStylingValues, key: string): number { + for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < keyValues.length; + i += InitialStylingValuesIndex.Size) { + if (keyValues[i] === key) return i; + } + return -1; +} + +export function compareLogSummaries(a: LogSummary, b: LogSummary) { + const log: string[] = []; + const diffs: [string, any, any][] = []; + diffSummaryValues(diffs, 'staticIndex', 'staticIndex', a, b); + diffSummaryValues(diffs, 'dynamicIndex', 'dynamicIndex', a, b); + Object.keys(a.flags).forEach( + name => { diffSummaryValues(diffs, 'flags.' + name, name, a.flags, b.flags); }); + + if (diffs.length) { + log.push('Log Summaries for:'); + log.push(' A: ' + a.name); + log.push(' B: ' + b.name); + log.push('\n Differ in the following way (A !== B):'); + diffs.forEach(result => { + const [name, aVal, bVal] = result; + log.push(' => ' + name); + log.push(' => ' + aVal + ' !== ' + bVal + '\n'); + }); + } + + return log; +} + +function diffSummaryValues(result: any[], name: string, prop: string, a: any, b: any) { + const aVal = a[prop]; + const bVal = b[prop]; + if (aVal !== bVal) { + result.push([name, aVal, bVal]); + } +} + +function getSinglePropIndexValue( + context: StylingContext, directiveIndex: number, offset: number, isClassBased: boolean) { + const singlePropOffsetRegistryIndex = + context[StylingIndex.DirectiveRegistryPosition] + [(directiveIndex * DirectiveRegistryValuesIndex.Size) + + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] as number; + const offsets = context[StylingIndex.SinglePropOffsetPositions]; + const indexForOffset = singlePropOffsetRegistryIndex + + SinglePropOffsetValuesIndex.ValueStartPosition + + (isClassBased ? + offsets + [singlePropOffsetRegistryIndex + SinglePropOffsetValuesIndex.StylesCountPosition] : + 0) + + offset; + return offsets[indexForOffset]; +} + +function getStyleSanitizer(context: StylingContext, directiveIndex: number): StyleSanitizeFn|null { + const dirs = context[StylingIndex.DirectiveRegistryPosition]; + const value = dirs + [directiveIndex * DirectiveRegistryValuesIndex.Size + + DirectiveRegistryValuesIndex.StyleSanitizerOffset] || + dirs[DirectiveRegistryValuesIndex.StyleSanitizerOffset] || null; + return value as StyleSanitizeFn | null; +} + +function isDirectiveDirty(context: StylingContext, directiveIndex: number): boolean { + const dirs = context[StylingIndex.DirectiveRegistryPosition]; + return dirs + [directiveIndex * DirectiveRegistryValuesIndex.Size + + DirectiveRegistryValuesIndex.DirtyFlagOffset] as boolean; +} + +function setDirectiveDirty( + context: StylingContext, directiveIndex: number, dirtyYes: boolean): void { + const dirs = context[StylingIndex.DirectiveRegistryPosition]; + dirs + [directiveIndex * DirectiveRegistryValuesIndex.Size + + DirectiveRegistryValuesIndex.DirtyFlagOffset] = dirtyYes; +} + +function allowValueChange( + currentValue: string | boolean | null, newValue: string | boolean | null, + currentDirectiveOwner: number, newDirectiveOwner: number) { + // the code below relies the importance of directive's being tied to their + // index value. The index values for each directive are derived from being + // registered into the styling context directive registry. The most important + // directive is the parent component directive (the template) and each directive + // that is added after is considered less important than the previous entry. This + // prioritization of directives enables the styling algorithm to decide if a style + // or class should be allowed to be updated/replaced incase an earlier directive + // already wrote to the exact same style-property or className value. In other words + // ... this decides what to do if and when there is a collision. + if (currentValue) { + if (newValue) { + // if a directive index is lower than it always has priority over the + // previous directive's value... + return newDirectiveOwner <= currentDirectiveOwner; + } else { + // only write a null value incase it's the same owner writing it. + // this avoids having a higher-priority directive write to null + // only to have a lesser-priority directive change right to a + // non-null value immediately afterwards. + return currentDirectiveOwner === newDirectiveOwner; + } + } + return true; +} + +/** + * This function is only designed to be called for `[class]` bindings when + * `[ngClass]` (or something that uses `class` as an input) is present. Once + * directive host bindings fully work for `[class]` and `[style]` inputs + * then this can be deleted. + */ +export function getInitialClassNameValue(context: StylingContext): string { + let className = context[StylingIndex.CachedClassValueOrInitialClassString] as string; + if (className == null) { + className = ''; + const initialClassValues = context[StylingIndex.InitialClassValuesPosition]; + for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < initialClassValues.length; + i += InitialStylingValuesIndex.Size) { + const isPresent = initialClassValues[i + 1]; + if (isPresent) { + className += (className.length ? ' ' : '') + initialClassValues[i]; + } + } + context[StylingIndex.CachedClassValueOrInitialClassString] = className; + } + return className; +} diff --git a/packages/core/src/render3/styling/util.ts b/packages/core/src/render3/styling/util.ts index 8a41dcffd5..16eb3d6e20 100644 --- a/packages/core/src/render3/styling/util.ts +++ b/packages/core/src/render3/styling/util.ts @@ -9,12 +9,13 @@ import '../ng_dev_mode'; import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {getLContext} from '../context_discovery'; -import {ACTIVE_INDEX, LContainer} from '../interfaces/container'; +import {LContainer} from '../interfaces/container'; import {LContext} from '../interfaces/context'; +import {AttributeMarker, TAttributes, TNode, TNodeFlags} from '../interfaces/node'; import {PlayState, Player, PlayerContext, PlayerIndex} from '../interfaces/player'; import {RElement} from '../interfaces/renderer'; -import {InitialStyles, StylingContext, StylingIndex} from '../interfaces/styling'; -import {FLAGS, HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view'; +import {InitialStylingValues, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling'; +import {HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view'; import {getTNode} from '../util'; import {CorePlayerHandler} from './core_player_handler'; @@ -23,16 +24,18 @@ const ANIMATION_PROP_PREFIX = '@'; export function createEmptyStylingContext( element?: RElement | null, sanitizer?: StyleSanitizeFn | null, - initialStylingValues?: InitialStyles): StylingContext { + initialStyles?: InitialStylingValues | null, + initialClasses?: InitialStylingValues | null): StylingContext { return [ - null, // PlayerContext - sanitizer || null, // StyleSanitizer - initialStylingValues || [null], // InitialStyles - 0, // MasterFlags - 0, // ClassOffset - element || null, // Element - null, // PreviousMultiClassValue - null // PreviousMultiStyleValue + 0, // MasterFlags + [null, -1, false, sanitizer || null], // DirectiveRefs + initialStyles || [null], // InitialStyles + initialClasses || [null], // InitialClasses + [0, 0], // SinglePropOffsets + element || null, // Element + null, // PreviousMultiClassValue + null, // PreviousMultiStyleValue + null, // PlayerContext ]; } @@ -47,6 +50,9 @@ export function allocStylingContext( // each instance gets a copy const context = templateStyleContext.slice() as any as StylingContext; context[StylingIndex.ElementPosition] = element; + + // this will prevent any other directives from extending the context + context[StylingIndex.MasterFlagPosition] |= StylingFlags.BindingAllocationLocked; return context; } @@ -89,8 +95,8 @@ export function getStylingContext(index: number, viewData: LView): StylingContex export function isStylingContext(value: any): value is StylingContext { // Not an LView or an LContainer - return Array.isArray(value) && typeof value[FLAGS] !== 'number' && - typeof value[ACTIVE_INDEX] !== 'number'; + return Array.isArray(value) && typeof value[StylingIndex.MasterFlagPosition] === 'number' && + Array.isArray(value[StylingIndex.InitialStyleValuesPosition]); } export function isAnimationProp(name: string): boolean { @@ -182,3 +188,15 @@ export function allocPlayerContext(data: StylingContext): PlayerContext { export function throwInvalidRefError() { throw new Error('Only elements that exist in an Angular application can be used for animations'); } + +export function hasStyling(attrs: TAttributes): boolean { + for (let i = 0; i < attrs.length; i++) { + const attr = attrs[i]; + if (attr == AttributeMarker.Classes || attr == AttributeMarker.Styles) return true; + } + return false; +} + +export function hasClassInput(tNode: TNode) { + return tNode.flags & TNodeFlags.hasClassInput ? true : false; +} diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index 9cdd42d69b..c8c2d3132d 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -9,7 +9,7 @@ import {global} from '../util'; import {assertDataInRange, assertDefined, assertGreaterThan, assertLessThan} from './assert'; -import {ACTIVE_INDEX, LContainer} from './interfaces/container'; +import {ACTIVE_INDEX, LCONTAINER_LENGTH, LContainer} from './interfaces/container'; import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context'; import {ComponentDef, DirectiveDef} from './interfaces/definition'; import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector'; @@ -18,8 +18,6 @@ import {RComment, RElement, RText} from './interfaces/renderer'; import {StylingContext} from './interfaces/styling'; import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view'; - - /** * Returns whether the values are different from a change detection stand point. * @@ -127,7 +125,7 @@ export function isComponentDef(def: DirectiveDef): def is ComponentDef export function isLContainer(value: RElement | RComment | LContainer | StylingContext): boolean { // Styling contexts are also arrays, but their first index contains an element node - return Array.isArray(value) && typeof value[ACTIVE_INDEX] === 'number'; + return Array.isArray(value) && value.length === LCONTAINER_LENGTH; } export function isRootView(target: LView): boolean { diff --git a/packages/core/test/bundling/animation_world/index.ts b/packages/core/test/bundling/animation_world/index.ts index 1d4cca61cb..001c8b3ea7 100644 --- a/packages/core/test/bundling/animation_world/index.ts +++ b/packages/core/test/bundling/animation_world/index.ts @@ -9,7 +9,29 @@ import '@angular/core/test/bundling/util/src/reflect_metadata'; import {CommonModule} from '@angular/common'; -import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core'; +import {Component, Directive, ElementRef, HostBinding, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core'; + +@Directive({ + selector: '[make-color-grey]', + exportAs: 'makeColorGrey', + host: {'style': 'font-family: Times New Roman;'} +}) +class MakeColorGreyDirective { + @HostBinding('style.background-color') private _backgroundColor: string|null = null; + @HostBinding('style.color') private _textColor: string|null = null; + + on() { + this._backgroundColor = 'grey'; + this._textColor = 'black'; + } + + off() { + this._backgroundColor = null; + this._textColor = null; + } + + toggle() { this._backgroundColor ? this.off() : this.on(); } +} @Component({ selector: 'animation-world', @@ -20,21 +42,40 @@ import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as P
- {{ item }} + #makeColorGrey="makeColorGrey" + make-color-grey + *ngFor="let item of items" + class="record" + [style.transform]="item.active ? 'scale(1.5)' : 'none'" + [class]="makeClass(item)" + style="border-radius: 10px" + [style]="styles" + [style.color]="item.value == 4 ? 'red' : null" + [style.background-color]="item.value == 4 ? 'white' : null" + (click)="toggleActive(item, makeColorGrey)"> + {{ item.value }}
`, }) class AnimationWorldComponent { - items: any[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + items: any[] = [ + {value: 1, active: false}, {value: 2, active: false}, {value: 3, active: false}, + {value: 4, active: false}, {value: 5, active: false}, {value: 6, active: false}, + {value: 7, active: false}, {value: 8, active: false}, {value: 9, active: false} + ]; private _hostElement: HTMLElement; public styles: {[key: string]: any}|null = null; constructor(element: ElementRef) { this._hostElement = element.nativeElement; } - makeClass(index: number) { return `record-${index}`; } + makeClass(item: any) { return `record-${item.value}`; } + + toggleActive(item: any, makeColorGrey: MakeColorGreyDirective) { + item.active = !item.active; + makeColorGrey.toggle(); + markDirty(this); + } animateWithStyles() { this.styles = animateStyleFactory([{opacity: 0}, {opacity: 1}], 300, 'ease-out'); @@ -52,7 +93,8 @@ class AnimationWorldComponent { } } -@NgModule({declarations: [AnimationWorldComponent], imports: [CommonModule]}) +@NgModule( + {declarations: [AnimationWorldComponent, MakeColorGreyDirective], imports: [CommonModule]}) class AnimationWorldModule { } 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 c63e3016e5..71e8af4671 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -27,10 +27,10 @@ "name": "DECLARATION_VIEW" }, { - "name": "EMPTY" + "name": "EMPTY_ARRAY" }, { - "name": "EMPTY_ARRAY" + "name": "EMPTY_OBJ" }, { "name": "EmptyErrorImpl" diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 4ea9b4f786..9b295d6b60 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -45,10 +45,10 @@ "name": "DefaultIterableDifferFactory" }, { - "name": "EMPTY" + "name": "EMPTY_ARRAY" }, { - "name": "EMPTY_ARRAY" + "name": "EMPTY_OBJ" }, { "name": "ElementRef" @@ -86,6 +86,9 @@ { "name": "IterableDiffers" }, + { + "name": "LCONTAINER_LENGTH" + }, { "name": "MONKEY_PATCH_KEY_NAME" }, @@ -320,21 +323,9 @@ { "name": "_c18" }, - { - "name": "_c19" - }, { "name": "_c2" }, - { - "name": "_c20" - }, - { - "name": "_c21" - }, - { - "name": "_c22" - }, { "name": "_c3" }, @@ -371,6 +362,9 @@ { "name": "_symbolIterator" }, + { + "name": "_updateSingleStylingValue" + }, { "name": "addComponentLogic" }, @@ -389,6 +383,9 @@ { "name": "allocStylingContext" }, + { + "name": "allowValueChange" + }, { "name": "appendChild" }, @@ -491,9 +488,6 @@ { "name": "createRootContext" }, - { - "name": "createStylingContextTemplate" - }, { "name": "createTNode" }, @@ -530,9 +524,6 @@ { "name": "defineInjectable" }, - { - "name": "delegateToClassInput" - }, { "name": "destroyLView" }, @@ -554,6 +545,9 @@ { "name": "directiveInject" }, + { + "name": "directiveOwnerPointers" + }, { "name": "domRendererFactory3" }, @@ -614,6 +608,9 @@ { "name": "findDirectiveMatches" }, + { + "name": "findOrPatchDirectiveIntoRegistry" + }, { "name": "findViaComponent" }, @@ -668,6 +665,15 @@ { "name": "getDirectiveDef" }, + { + "name": "getDirectiveIndexFromEntry" + }, + { + "name": "getDirectiveIndexFromRegistry" + }, + { + "name": "getDirectiveRegistryValuesIndexOf" + }, { "name": "getElementDepthCount" }, @@ -686,9 +692,15 @@ { "name": "getHostTElementNode" }, + { + "name": "getInitialClassNameValue" + }, { "name": "getInitialIndex" }, + { + "name": "getInitialStylingValuesIndexOf" + }, { "name": "getInitialValue" }, @@ -710,6 +722,9 @@ { "name": "getLViewChild" }, + { + "name": "getMatchingBindingIndex" + }, { "name": "getMultiOrSingleIndex" }, @@ -791,6 +806,9 @@ { "name": "getRootView" }, + { + "name": "getSinglePropIndexValue" + }, { "name": "getStyleSanitizer" }, @@ -816,19 +834,7 @@ "name": "getValue" }, { - "name": "hackImplementationOfElementClassProp" - }, - { - "name": "hackImplementationOfElementStyling" - }, - { - "name": "hackImplementationOfElementStylingApply" - }, - { - "name": "hackSetStaticClasses" - }, - { - "name": "hackSquashDeclaration" + "name": "hasClassInput" }, { "name": "hasParentInjector" @@ -836,6 +842,9 @@ { "name": "hasPlayerBuilderChanged" }, + { + "name": "hasStyling" + }, { "name": "hasTagAndTypeMatch" }, @@ -851,6 +860,9 @@ { "name": "initNodeFlags" }, + { + "name": "initializeStaticContext" + }, { "name": "initializeTNodeInputs" }, @@ -911,6 +923,9 @@ { "name": "isDifferent" }, + { + "name": "isDirectiveDirty" + }, { "name": "isDirty" }, @@ -1083,7 +1098,13 @@ "name": "renderEmbeddedTemplate" }, { - "name": "renderStyleAndClassBindings" + "name": "renderInitialStylesAndClasses" + }, + { + "name": "renderInitialStylingValues" + }, + { + "name": "renderStyling" }, { "name": "resetComponentState" @@ -1127,6 +1148,9 @@ { "name": "setCurrentDirectiveDef" }, + { + "name": "setDirectiveDirty" + }, { "name": "setDirty" }, @@ -1166,6 +1190,9 @@ { "name": "setProp" }, + { + "name": "setSanitizeFlag" + }, { "name": "setStyle" }, @@ -1215,7 +1242,7 @@ "name": "updateClassProp" }, { - "name": "updateStyleProp" + "name": "updateContextWithBindings" }, { "name": "updateViewQuery" diff --git a/packages/core/test/render3/discovery_utils_spec.ts b/packages/core/test/render3/discovery_utils_spec.ts index 8d455009d1..f321ce6ead 100644 --- a/packages/core/test/render3/discovery_utils_spec.ts +++ b/packages/core/test/render3/discovery_utils_spec.ts @@ -542,7 +542,6 @@ describe('discovery utils deprecated', () => { }); it('should return a map of local refs for an element with styling context', () => { - class Comp { static ngComponentDef = defineComponent({ type: Comp, @@ -554,7 +553,6 @@ describe('discovery utils deprecated', () => { if (rf & RenderFlags.Create) { //
elementStart(0, 'div', null, ['elRef', '']); - elementStyling(['fooClass']); elementEnd(); } if (rf & RenderFlags.Update) { diff --git a/packages/core/test/render3/exports_spec.ts b/packages/core/test/render3/exports_spec.ts index a5f5524dd7..847ea9b30b 100644 --- a/packages/core/test/render3/exports_spec.ts +++ b/packages/core/test/render3/exports_spec.ts @@ -8,7 +8,7 @@ import {AttributeMarker, defineComponent, defineDirective} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementAttribute, elementClassProp, elementEnd, elementProperty, elementStart, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, interpolation2, nextContext, reference, template, text, textBinding} from '../../src/render3/instructions'; -import {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/definition'; +import {RenderFlags} from '../../src/render3/interfaces/definition'; import {NgIf} from './common_with_def'; import {ComponentFixture, createComponent, renderToHtml} from './render_util'; @@ -206,8 +206,8 @@ describe('exports', () => { /**
*/ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - elementStart(0, 'div'); - elementStyling([InitialStylingFlags.VALUES_MODE, 'red', true]); + elementStart(0, 'div', [AttributeMarker.Classes, 'red']); + elementStyling(['red']); elementEnd(); element(1, 'input', ['type', 'checkbox', 'checked', 'true'], ['myInput', '']); } diff --git a/packages/core/test/render3/host_binding_spec.ts b/packages/core/test/render3/host_binding_spec.ts index ec03af03e9..0f1e48ddb1 100644 --- a/packages/core/test/render3/host_binding_spec.ts +++ b/packages/core/test/render3/host_binding_spec.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {ElementRef, EventEmitter} from '@angular/core'; +import {ElementRef} from '@angular/core'; import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature, NgOnChangesFeature, QueryList} from '../../src/render3/index'; -import {allocHostVars, bind, directiveInject, element, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery} from '../../src/render3/instructions'; +import {allocHostVars, bind, directiveInject, element, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery, elementHostAttrs} from '../../src/render3/instructions'; import {query, queryRefresh} from '../../src/render3/query'; -import {RenderFlags, InitialStylingFlags} from '../../src/render3/interfaces/definition'; +import {RenderFlags} from '../../src/render3/interfaces/definition'; import {pureFunction1, pureFunction2} from '../../src/render3/pure_function'; import {ComponentFixture, TemplateFixture, createComponent, createDirective} from './render_util'; @@ -1141,9 +1141,8 @@ describe('host bindings', () => { vars: 0, hostBindings: (rf: RenderFlags, ctx: StaticHostClass, elIndex: number) => { if (rf & RenderFlags.Create) { - elementStyling( - ['mat-toolbar', InitialStylingFlags.VALUES_MODE, 'mat-toolbar', true], null, null, - ctx); + elementHostAttrs(ctx, [AttributeMarker.Classes, 'mat-toolbar']); + elementStyling(['mat-toolbar'], null, null, ctx); } if (rf & RenderFlags.Update) { elementStylingApply(0, ctx); @@ -1164,6 +1163,5 @@ describe('host bindings', () => { const hostBindingEl = fixture.hostElement.querySelector('static-host-class') as HTMLElement; expect(hostBindingEl.className).toEqual('mat-toolbar'); }); - }); }); diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index e82aa4fd5f..ebf4ddc476 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -11,7 +11,6 @@ import {NgForOfContext} from '@angular/common'; import {RenderFlags} from '../../src/render3'; import {defineComponent} from '../../src/render3/definition'; import {bind, element, elementAttribute, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, elementStylingMap, interpolation1, renderTemplate, template, text, textBinding} from '../../src/render3/instructions'; -import {InitialStylingFlags} from '../../src/render3/interfaces/definition'; import {AttributeMarker} from '../../src/render3/interfaces/node'; import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl} from '../../src/sanitization/bypass'; import {defaultStyleSanitizer, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization'; @@ -28,10 +27,19 @@ describe('instructions', () => { elementEnd(); } - function createDiv(initialStyles?: (string | number)[], styleSanitizer?: StyleSanitizeFn) { - elementStart(0, 'div'); - elementStyling( - [], initialStyles && Array.isArray(initialStyles) ? initialStyles : null, styleSanitizer); + function createDiv( + initialClasses?: string[] | null, classBindingNames?: string[] | null, + initialStyles?: string[] | null, styleBindingNames?: string[] | null, + styleSanitizer?: StyleSanitizeFn) { + const attrs: any[] = []; + if (initialClasses) { + attrs.push(AttributeMarker.Classes, ...initialClasses); + } + if (initialStyles) { + attrs.push(AttributeMarker.Styles, ...initialStyles); + } + elementStart(0, 'div', attrs); + elementStyling(classBindingNames || null, styleBindingNames || null, styleSanitizer); elementEnd(); } @@ -191,8 +199,9 @@ describe('instructions', () => { describe('elementStyleProp', () => { it('should automatically sanitize unless a bypass operation is applied', () => { - const t = new TemplateFixture( - () => { return createDiv(['background-image'], defaultStyleSanitizer); }, () => {}, 1); + const t = new TemplateFixture(() => { + return createDiv(null, null, null, ['background-image'], defaultStyleSanitizer); + }, () => {}, 1); t.update(() => { elementStyleProp(0, 0, 'url("http://server")'); elementStylingApply(0); @@ -211,8 +220,9 @@ describe('instructions', () => { it('should not re-apply the style value even if it is a newly bypassed again', () => { const sanitizerInterceptor = new MockSanitizerInterceptor(); const t = createTemplateFixtureWithSanitizer( - () => createDiv(['background-image'], sanitizerInterceptor.getStyleSanitizer()), 1, - sanitizerInterceptor); + () => createDiv( + null, null, null, ['background-image'], sanitizerInterceptor.getStyleSanitizer()), + 1, sanitizerInterceptor); t.update(() => { elementStyleProp(0, 0, bypassSanitizationTrustStyle('apple')); @@ -232,8 +242,8 @@ describe('instructions', () => { describe('elementStyleMap', () => { function createDivWithStyle() { - elementStart(0, 'div'); - elementStyling([], ['height', InitialStylingFlags.VALUES_MODE, 'height', '10px']); + elementStart(0, 'div', [AttributeMarker.Styles, 'height', '10px']); + elementStyling([], ['height']); elementEnd(); } @@ -251,7 +261,8 @@ describe('instructions', () => { const sanitizerInterceptor = new MockSanitizerInterceptor(value => { detectedValues.push(value); }); const fixture = createTemplateFixtureWithSanitizer( - () => createDiv([], sanitizerInterceptor.getStyleSanitizer()), 1, sanitizerInterceptor); + () => createDiv(null, null, null, null, sanitizerInterceptor.getStyleSanitizer()), 1, + sanitizerInterceptor); fixture.update(() => { elementStylingMap(0, null, { diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 3c981a96c4..b6fbe03438 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -7,13 +7,13 @@ */ import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core'; + import {RendererType2} from '../../src/render/api'; import {AttributeMarker, defineComponent, defineDirective, templateRefExtractor} from '../../src/render3/index'; -import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, element, elementAttribute, elementClassProp, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, projection, projectionDef, reference, text, textBinding, template, elementStylingMap, directiveInject} from '../../src/render3/instructions'; -import {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/definition'; +import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, elementStart, elementAttribute, elementClassProp, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, element, elementStyling, elementStylingApply, elementStyleProp, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, projection, projectionDef, reference, text, textBinding, template, elementStylingMap, directiveInject, elementHostAttrs} from '../../src/render3/instructions'; +import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer'; -import {NO_CHANGE} from '../../src/render3/tokens'; import {HEADER_OFFSET, CONTEXT} from '../../src/render3/interfaces/view'; import {enableBindings, disableBindings} from '../../src/render3/state'; import {sanitizeUrl} from '../../src/sanitization/sanitization'; @@ -1412,7 +1412,6 @@ describe('render3 integration test', () => { }); describe('elementStyle', () => { - it('should support binding to styles', () => { const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { @@ -1472,8 +1471,7 @@ describe('render3 integration test', () => { }); }); - describe('elementClass', () => { - + describe('class-based styling', () => { it('should support CSS class toggle', () => { /** */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { @@ -1519,9 +1517,8 @@ describe('render3 integration test', () => { it('should work correctly with existing static classes', () => { const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - elementStart(0, 'span'); - elementStyling( - ['existing', 'active', InitialStylingFlags.VALUES_MODE, 'existing', true]); + elementStart(0, 'span', [AttributeMarker.Classes, 'existing']); + elementStyling(['existing', 'active']); elementEnd(); } if (rf & RenderFlags.Update) { @@ -1553,7 +1550,7 @@ describe('render3 integration test', () => { const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'my-comp'); - { elementStyling(['active']); } + elementStyling(['active']); elementEnd(); } if (rf & RenderFlags.Update) { @@ -1572,7 +1569,6 @@ describe('render3 integration test', () => { expect(fixture.html).toEqual('Comp Content'); }); - it('should apply classes properly when nodes have LContainers', () => { let structuralComp !: StructuralComp; @@ -1656,17 +1652,17 @@ describe('render3 integration test', () => { set klass(value: string) { this.classesVal = value; } } - it('should delegate all initial classes to a [class] input binding if present on a directive on the same element', + it('should delegate initial classes to a [class] input binding if present on a directive on the same element', () => { /** - * + *
*/ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - elementStart(0, 'div', ['DirWithClass']); - elementStyling([ - InitialStylingFlags.VALUES_MODE, 'apple', true, 'orange', true, 'banana', true - ]); + elementStart( + 0, 'div', + ['DirWithClass', AttributeMarker.Classes, 'apple', 'orange', 'banana']); + elementStyling(); elementEnd(); } if (rf & RenderFlags.Update) { @@ -1681,7 +1677,7 @@ describe('render3 integration test', () => { it('should update `[class]` and bindings in the provided directive if the input is matched', () => { /** - * + *
*/ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { @@ -1698,6 +1694,223 @@ describe('render3 integration test', () => { const fixture = new ComponentFixture(App); expect(mockClassDirective !.classesVal).toEqual('cucumber grape'); }); + + it('should apply initial styling to the element that contains the directive with host styling', + () => { + class DirWithInitialStyling { + static ngDirectiveDef = defineDirective({ + type: DirWithInitialStyling, + selectors: [['', 'DirWithInitialStyling', '']], + factory: () => new DirWithInitialStyling(), + hostBindings: function( + rf: RenderFlags, ctx: DirWithInitialStyling, elementIndex: number) { + if (rf & RenderFlags.Create) { + elementHostAttrs(ctx, [ + AttributeMarker.Classes, 'heavy', 'golden', AttributeMarker.Styles, 'color', + 'purple', 'font-weight', 'bold' + ]); + } + } + }); + + public classesVal: string = ''; + } + + /** + *
+ */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'div', [ + 'DirWithInitialStyling', '', AttributeMarker.Classes, 'big', + AttributeMarker.Styles, 'color', 'black', 'font-size', '200px' + ]); + } + }, 1, 0, [DirWithInitialStyling]); + + const fixture = new ComponentFixture(App); + const target = fixture.hostElement.querySelector('div') !; + const classes = target.getAttribute('class') !.split(/\s+/).sort(); + expect(classes).toEqual(['big', 'golden', 'heavy']); + + expect(target.style.getPropertyValue('color')).toEqual('black'); + expect(target.style.getPropertyValue('font-size')).toEqual('200px'); + expect(target.style.getPropertyValue('font-weight')).toEqual('bold'); + }); + + it('should apply single styling bindings present within a directive onto the same element and defer the element\'s initial styling values when missing', + () => { + let dirInstance: DirWithSingleStylingBindings; + /** + * + */ + class DirWithSingleStylingBindings { + static ngDirectiveDef = defineDirective({ + type: DirWithSingleStylingBindings, + selectors: [['', 'DirWithSingleStylingBindings', '']], + factory: () => dirInstance = new DirWithSingleStylingBindings(), + hostBindings: function( + rf: RenderFlags, ctx: DirWithSingleStylingBindings, elementIndex: number) { + if (rf & RenderFlags.Create) { + elementHostAttrs( + ctx, + [AttributeMarker.Classes, 'def', AttributeMarker.Styles, 'width', '555px']); + elementStyling(['xyz'], ['width', 'height'], null, ctx); + } + if (rf & RenderFlags.Update) { + elementStyleProp(elementIndex, 0, ctx.width, null, ctx); + elementStyleProp(elementIndex, 1, ctx.height, null, ctx); + elementClassProp(elementIndex, 0, ctx.activateXYZClass, ctx); + elementStylingApply(elementIndex, ctx); + } + } + }); + + width: null|string = null; + height: null|string = null; + activateXYZClass: boolean = false; + } + + /** + *
+ */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'div', [ + 'DirWithSingleStylingBindings', '', AttributeMarker.Classes, 'abc', + AttributeMarker.Styles, 'width', '100px', 'height', '200px' + ]); + } + }, 1, 0, [DirWithSingleStylingBindings]); + + const fixture = new ComponentFixture(App); + const target = fixture.hostElement.querySelector('div') !; + expect(target.style.getPropertyValue('width')).toEqual('100px'); + expect(target.style.getPropertyValue('height')).toEqual('200px'); + expect(target.classList.contains('abc')).toBeTruthy(); + expect(target.classList.contains('def')).toBeTruthy(); + expect(target.classList.contains('xyz')).toBeFalsy(); + + dirInstance !.width = '444px'; + dirInstance !.height = '999px'; + dirInstance !.activateXYZClass = true; + fixture.update(); + + expect(target.style.getPropertyValue('width')).toEqual('444px'); + expect(target.style.getPropertyValue('height')).toEqual('999px'); + expect(target.classList.contains('abc')).toBeTruthy(); + expect(target.classList.contains('def')).toBeTruthy(); + expect(target.classList.contains('xyz')).toBeTruthy(); + + dirInstance !.width = null; + dirInstance !.height = null; + fixture.update(); + + expect(target.style.getPropertyValue('width')).toEqual('100px'); + expect(target.style.getPropertyValue('height')).toEqual('200px'); + expect(target.classList.contains('abc')).toBeTruthy(); + expect(target.classList.contains('def')).toBeTruthy(); + expect(target.classList.contains('xyz')).toBeTruthy(); + }); + + it('should properly prioritize style binding collision when they exist on multiple directives', + () => { + let dir1Instance: Dir1WithStyle; + /** + * Directive with host props: + * [style.width] + */ + class Dir1WithStyle { + static ngDirectiveDef = defineDirective({ + type: Dir1WithStyle, + selectors: [['', 'Dir1WithStyle', '']], + factory: () => dir1Instance = new Dir1WithStyle(), + hostBindings: function(rf: RenderFlags, ctx: Dir1WithStyle, elementIndex: number) { + if (rf & RenderFlags.Create) { + elementStyling(null, ['width'], null, ctx); + } + if (rf & RenderFlags.Update) { + elementStyleProp(elementIndex, 0, ctx.width, null, ctx); + elementStylingApply(elementIndex, ctx); + } + } + }); + width: null|string = null; + } + + let dir2Instance: Dir2WithStyle; + /** + * Directive with host props: + * [style.width] + * style="width:111px" + */ + class Dir2WithStyle { + static ngDirectiveDef = defineDirective({ + type: Dir2WithStyle, + selectors: [['', 'Dir2WithStyle', '']], + factory: () => dir2Instance = new Dir2WithStyle(), + hostBindings: function(rf: RenderFlags, ctx: Dir2WithStyle, elementIndex: number) { + if (rf & RenderFlags.Create) { + elementHostAttrs(ctx, [AttributeMarker.Styles, 'width', '111px']); + elementStyling(null, ['width'], null, ctx); + } + if (rf & RenderFlags.Update) { + elementStyleProp(elementIndex, 0, ctx.width, null, ctx); + elementStylingApply(elementIndex, ctx); + } + } + }); + width: null|string = null; + } + + /** + *
+ */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'div', ['Dir1WithStyle', '', 'Dir2WithStyle', '']); + elementStyling(null, ['width']); + } + if (rf & RenderFlags.Update) { + elementStyleProp(0, 0, ctx.width); + elementStylingApply(0); + } + }, 1, 0, [Dir1WithStyle, Dir2WithStyle]); + + const fixture = new ComponentFixture(App); + const target = fixture.hostElement.querySelector('div') !; + expect(target.style.getPropertyValue('width')).toEqual('111px'); + + fixture.component.width = '999px'; + dir1Instance !.width = '222px'; + dir2Instance !.width = '333px'; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('999px'); + + fixture.component.width = null; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('222px'); + + dir1Instance !.width = null; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('333px'); + + dir2Instance !.width = null; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('111px'); + + dir1Instance !.width = '666px'; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('666px'); + + fixture.component.width = '777px'; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('777px'); + }); }); }); @@ -2655,4 +2868,4 @@ class ProxyRenderer3Factory implements RendererFactory3 { this.lastCapturedType = rendererType; return domRendererFactory3.createRenderer(hostElement, rendererType); } -} \ No newline at end of file +} diff --git a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts index 5eb0643641..de3a76ee7c 100644 --- a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts +++ b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts @@ -7,18 +7,19 @@ */ import {createRootContext} from '../../../src/render3/component'; import {getLContext} from '../../../src/render3/context_discovery'; -import {defineComponent} from '../../../src/render3/index'; -import {createLView, createTView, elementClassProp, elementEnd, elementStart, elementStyleProp, elementStyling, elementStylingApply, elementStylingMap, namespaceSVG} from '../../../src/render3/instructions'; -import {InitialStylingFlags, RenderFlags} from '../../../src/render3/interfaces/definition'; -import {BindingStore, BindingType, PlayState, Player, PlayerFactory, PlayerHandler} from '../../../src/render3/interfaces/player'; +import {defineComponent, defineDirective} from '../../../src/render3/index'; +import {createLView, createTView, elementClassProp, elementEnd, elementHostAttrs, elementStart, elementStyleProp, elementStyling, elementStylingApply, elementStylingMap, namespaceSVG} from '../../../src/render3/instructions'; +import {RenderFlags} from '../../../src/render3/interfaces/definition'; +import {AttributeMarker, TAttributes} from '../../../src/render3/interfaces/node'; +import {BindingStore, BindingType, PlayState, Player, PlayerContext, PlayerFactory, PlayerHandler} from '../../../src/render3/interfaces/player'; import {RElement, Renderer3, domRendererFactory3} from '../../../src/render3/interfaces/renderer'; import {StylingContext, StylingFlags, StylingIndex} from '../../../src/render3/interfaces/styling'; import {CONTEXT, LView, LViewFlags, RootContext} from '../../../src/render3/interfaces/view'; import {addPlayer, getPlayers} from '../../../src/render3/players'; -import {ClassAndStylePlayerBuilder, createStylingContextTemplate, isContextDirty, renderStyleAndClassBindings as _renderStyling, setContextDirty, updateClassProp, updateStyleProp, updateStylingMap} from '../../../src/render3/styling/class_and_style_bindings'; +import {ClassAndStylePlayerBuilder, compareLogSummaries, directiveOwnerPointers, generateConfigSummary, getDirectiveIndexFromEntry, initializeStaticContext, isContextDirty, patchContextWithStaticAttrs, renderStyling as _renderStyling, setContextDirty, updateClassProp, updateContextWithBindings, updateStyleProp, updateStylingMap} from '../../../src/render3/styling/class_and_style_bindings'; import {CorePlayerHandler} from '../../../src/render3/styling/core_player_handler'; import {BoundPlayerFactory, bindPlayerFactory} from '../../../src/render3/styling/player_factory'; -import {allocStylingContext} from '../../../src/render3/styling/util'; +import {allocStylingContext, createEmptyStylingContext} from '../../../src/render3/styling/util'; import {defaultStyleSanitizer} from '../../../src/sanitization/sanitization'; import {StyleSanitizeFn} from '../../../src/sanitization/style_sanitizer'; import {ComponentFixture, renderToHtml} from '../render_util'; @@ -39,9 +40,37 @@ describe('style and class based bindings', () => { } function initContext( - styles?: (number | string)[] | null, classes?: (string | number | boolean)[] | null, + initialStyles?: (number | string)[] | null, styleBindings?: string[] | null, + initialClasses?: (string | number | boolean)[] | null, classBindings?: string[] | null, sanitizer?: StyleSanitizeFn | null): StylingContext { - return allocStylingContext(element, createStylingContextTemplate(classes, styles, sanitizer)); + const attrsWithStyling: TAttributes = []; + if (initialClasses) { + attrsWithStyling.push(AttributeMarker.Classes); + attrsWithStyling.push(...initialClasses as any); + } + if (initialStyles) { + attrsWithStyling.push(AttributeMarker.Styles); + attrsWithStyling.push(...initialStyles as any); + } + + const tpl = initializeStaticContext(attrsWithStyling) !; + updateContextWithBindings(tpl, null, classBindings || null, styleBindings || null, sanitizer); + return allocStylingContext(element, tpl); + } + + function patchContext( + context: StylingContext, styles?: string[] | null, classes?: string[] | null, + directiveRef?: any) { + const attrs: (string | AttributeMarker)[] = []; + if (classes && classes.length) { + attrs.push(AttributeMarker.Classes); + attrs.push(...classes); + } + if (styles && styles.length) { + attrs.push(AttributeMarker.Styles); + attrs.push(...styles); + } + patchContextWithStaticAttrs(context, attrs, directiveRef || null); } function getRootContextInternal(lView: LView) { return lView[CONTEXT] as RootContext; } @@ -60,14 +89,15 @@ describe('style and class based bindings', () => { function trackStylesFactory(store?: MockStylingStore) { store = store || new MockStylingStore(element as HTMLElement, BindingType.Style); const handler = new CorePlayerHandler(); - return function(context: StylingContext, firstRender?: boolean, renderer?: Renderer3): - {[key: string]: any} { - const lView = createMockViewData(handler, context); - _renderStyling( - context, (renderer || {}) as Renderer3, getRootContextInternal(lView), !!firstRender, - null, store); - return store !.getValues(); - }; + return function( + context: StylingContext, targetDirective?: any, firstRender?: boolean, + renderer?: Renderer3): {[key: string]: any} { + const lView = createMockViewData(handler, context); + _renderStyling( + context, (renderer || {}) as Renderer3, getRootContextInternal(lView), !!firstRender, + null, store, targetDirective); + return store !.getValues(); + }; } function trackClassesFactory(store?: MockStylingStore) { @@ -107,6 +137,18 @@ describe('style and class based bindings', () => { function cleanStyle(a: number = 0, b: number = 0): number { return _clean(a, b, false, false); } + function masterConfig(multiIndexStart: number, dirty: boolean = false, locked = true) { + let num = 0; + num |= multiIndexStart << (StylingFlags.BitCountSize + StylingIndex.BitCountSize); + if (dirty) { + num |= StylingFlags.Dirty; + } + if (locked) { + num |= StylingFlags.BindingAllocationLocked; + } + return num; + } + function cleanStyleWithSanitization(a: number = 0, b: number = 0): number { return _clean(a, b, false, true); } @@ -154,48 +196,111 @@ describe('style and class based bindings', () => { } describe('styles', () => { - describe('createStylingContextTemplate', () => { + describe('static styling properties within a context', () => { it('should initialize empty template', () => { const template = initContext(); - expect(template).toEqual([null, null, [null], cleanStyle(0, 8), 0, element, null, null]); - }); - - it('should initialize static styles', () => { - const template = - initContext([InitialStylingFlags.VALUES_MODE, 'color', 'red', 'width', '10px']); - expect(template).toEqual([ - null, - null, - [null, 'red', '10px'], - dirtyStyle(0, 16), // - 0, + assertContext(template, [ + masterConfig(9), + [null, 2, false, null], + [null], + [null], + [0, 0, 0, 0], element, null, null, + null, + ]); + }); - // #8 - cleanStyle(1, 16), + it('should initialize static styles and classes', () => { + const template = initContext(['color', 'red', 'width', '10px'], null, ['foo', 'bar']); + assertContext(template, [ + masterConfig(9), + [null, 2, false, null], + [null, 'color', 'red', 'width', '10px'], + [null, 'foo', true, 'bar', true], + [0, 0, 0, 0], + element, + null, + null, + null, + ]); + }); + + it('should initialize and then patch static styling inline with existing static styling', + () => { + const template = initContext(['color', 'red'], null, ['foo']); + expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ + null, + 'color', + 'red', + ]); + expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ + null, + 'foo', + true, + ]); + + patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo'], '1'); + expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ + null, 'color', 'red', 'height', '200px' + ]); + expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ + null, 'foo', true, 'bar', true + ]); + }); + + it('should only populate static styles for a given directive once', () => { + const template = initContext(['color', 'red'], null, ['foo']); + expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ + null, 'color', + 'red', + ]); + expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ null, - 0, + 'foo', + true, + ]); - // #12 - cleanStyle(2, 20), - 'width', + patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo']); + expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ null, - 0, - - // #16 - dirtyStyle(1, 8), 'color', + 'red', + ]); + expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ null, - 0, + 'foo', + true, + ]); - // #20 - dirtyStyle(2, 12), - 'width', + patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo'], '1'); + expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ null, - 0, + 'color', + 'red', + 'height', + '200px', + ]); + expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ + null, 'foo', true, 'bar', true + ]); + + patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo'], '1'); + expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ + null, + 'color', + 'red', + 'height', + '200px', + ]); + expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ + null, + 'foo', + true, + 'bar', + true, ]); }); }); @@ -205,16 +310,20 @@ describe('style and class based bindings', () => { () => { 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' + elementStart(0, 'span', [ + AttributeMarker.Styles, + 'width', + '200px', + 'height', + '100px', + 'opacity', + '0.5', ]); + elementStyling(null, ['width']); elementEnd(); } if (rf & RenderFlags.Update) { - elementStylingMap(0, ctx.myStyles); + elementStylingMap(0, null, ctx.myStyles); elementStyleProp(0, 0, ctx.myWidth); elementStylingApply(0); } @@ -273,7 +382,216 @@ describe('style and class based bindings', () => { }); }); - describe('helper functions', () => { + describe('dynamic styling properties within a styling context', () => { + it('should initialize a context with a series of styling bindings as well as single property offsets', + () => { + const ctx = createEmptyStylingContext(); + updateContextWithBindings(ctx, null, ['foo'], ['width']); + + assertContext(ctx, [ + masterConfig(17, false, false), // + [null, 2, false, null], + [null, 'width', null], + [null, 'foo', false], + [1, 1, 1, 1, 9, 13], + null, + null, + null, + null, + + // #9 + cleanStyle(2, 17), + 'width', + null, + 0, + + // #13 + cleanClass(2, 21), + 'foo', + null, + 0, + + // #17 + cleanStyle(2, 9), + 'width', + null, + 0, + + // #21 + cleanClass(2, 13), + 'foo', + null, + 0, + ]); + + updateContextWithBindings(ctx, 'SOME DIRECTIVE', ['bar'], ['width', 'height']); + + assertContext(ctx, [ + masterConfig(25, false, false), // + [null, 2, false, null, 'SOME DIRECTIVE', 6, false, null], + [null, 'width', null, 'height', null], + [null, 'foo', false, 'bar', false], + [2, 2, 1, 1, 9, 17, 2, 1, 9, 13, 21], + null, + null, + null, + null, + + // #9 + cleanStyle(2, 25), + 'width', + null, + 0, + + // #13 + cleanStyle(4, 29), + 'height', + null, + 1, + + // #17 + cleanClass(2, 33), + 'foo', + null, + 0, + + // #21 + cleanClass(4, 37), + 'bar', + null, + 1, + + // #25 + cleanStyle(2, 9), + 'width', + null, + 0, + + // #29 + cleanStyle(4, 13), + 'height', + null, + 1, + + // #33 + cleanClass(2, 17), + 'foo', + null, + 0, + + // #37 + cleanClass(4, 21), + 'bar', + null, + 1, + ]); + + updateContextWithBindings( + ctx, 'SOME DIRECTIVE 2', ['baz', 'bar', 'foo'], ['opacity', 'width', 'height']); + + assertContext(ctx, [ + masterConfig(33, false, false), // + [ + null, 2, false, null, 'SOME DIRECTIVE', 6, false, null, 'SOME DIRECTIVE 2', 11, + false, null + ], + [null, 'width', null, 'height', null, 'opacity', null], + [null, 'foo', false, 'bar', false, 'baz', false], + [3, 3, 1, 1, 9, 21, 2, 1, 9, 13, 25, 3, 3, 17, 9, 13, 29, 25, 21], + null, + null, + null, + null, + + // #9 + cleanStyle(2, 33), + 'width', + null, + 0, + + // #13 + cleanStyle(4, 37), + 'height', + null, + 1, + + // #17 + cleanStyle(6, 41), + 'opacity', + null, + 2, + + // #21 + cleanClass(2, 45), + 'foo', + null, + 0, + + // #25 + cleanClass(4, 49), + 'bar', + null, + 1, + + // #29 + cleanClass(6, 53), + 'baz', + null, + 2, + + // #33 + cleanStyle(2, 9), + 'width', + null, + 0, + + // #37 + cleanStyle(4, 13), + 'height', + null, + 1, + + // #41 + cleanStyle(6, 17), + 'opacity', + null, + 2, + + // #45 + cleanClass(2, 21), + 'foo', + null, + 0, + + // #49 + cleanClass(4, 25), + 'bar', + null, + 1, + + // #53 + cleanClass(6, 29), + 'baz', + null, + 2, + ]); + }); + + it('should only populate bindings for a given directive once', () => { + const ctx = createEmptyStylingContext(); + updateContextWithBindings(ctx, null, ['foo'], ['width']); + expect(ctx.length).toEqual(25); + + updateContextWithBindings(ctx, null, ['bar'], ['height']); + expect(ctx.length).toEqual(25); + + updateContextWithBindings(ctx, '1', ['bar'], ['height']); + expect(ctx.length).toEqual(41); + + updateContextWithBindings(ctx, '1', ['bar'], ['height']); + expect(ctx.length).toEqual(41); + }); + it('should build a list of multiple styling values', () => { const getStyles = trackStylesFactory(); const stylingContext = initContext(); @@ -282,16 +600,15 @@ describe('style and class based bindings', () => { height: '100px', }); updateStyles(stylingContext, {height: '200px'}); - expect(getStyles(stylingContext, true)).toEqual({height: '200px'}); + expect(getStyles(stylingContext, null, true)).toEqual({height: '200px'}); }); it('should evaluate the delta between style changes when rendering occurs', () => { - const stylingContext = - initContext(['width', 'height', InitialStylingFlags.VALUES_MODE, 'width', '100px']); + const stylingContext = initContext(['width', '100px'], ['width', 'height']); updateStyles(stylingContext, { height: '200px', }); - expect(renderStyles(stylingContext)).toEqual({width: '100px', height: '200px'}); + expect(renderStyles(stylingContext)).toEqual({height: '200px'}); expect(renderStyles(stylingContext)).toEqual({}); updateStyles(stylingContext, { width: '100px', @@ -309,7 +626,7 @@ describe('style and class based bindings', () => { it('should update individual values on a set of styles', () => { const getStyles = trackStylesFactory(); - const stylingContext = initContext(['width', 'height']); + const stylingContext = initContext(null, ['width', 'height']); updateStyles(stylingContext, { width: '100px', height: '100px', @@ -344,7 +661,7 @@ describe('style and class based bindings', () => { }); it('should only mark itself as updated when any single properties have been applied', () => { - const stylingContext = initContext(['height']); + const stylingContext = initContext(null, ['height']); updateStyles(stylingContext, { width: '100px', height: '100px', @@ -364,23 +681,16 @@ describe('style and class based bindings', () => { 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' - ]); + const stylingContext = initContext( + ['width', '100px', 'height', '100px', 'opacity', '0'], ['width', 'height', 'opacity']); - expect(getStyles(stylingContext)).toEqual({ - width: '100px', - height: '100px', - opacity: '0', - }); + expect(getStyles(stylingContext)).toEqual({}); updateStyles(stylingContext, {width: '200px', height: '200px'}); expect(getStyles(stylingContext)).toEqual({ width: '200px', height: '200px', - opacity: '0', }); updateStyleProp(stylingContext, 0, '300px'); @@ -388,7 +698,6 @@ describe('style and class based bindings', () => { expect(getStyles(stylingContext)).toEqual({ width: '300px', height: '200px', - opacity: '0', }); updateStyleProp(stylingContext, 0, null); @@ -396,7 +705,6 @@ describe('style and class based bindings', () => { expect(getStyles(stylingContext)).toEqual({ width: '200px', height: '200px', - opacity: '0', }); updateStyles(stylingContext, {}); @@ -404,46 +712,35 @@ describe('style and class based bindings', () => { 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 stylingContext = initContext(null, ['width', 'height']); const getStyles = trackStylesFactory(); - updateStyles(stylingContext, {width: '100px', height: '100px'}); - expect(stylingContext).toEqual([ - null, - null, - [null], - dirtyStyle(0, 16), // - 2, - element, - null, - {width: '100px', height: '100px'}, - - // #8 - cleanStyle(0, 16), + assertContextOnlyValues(stylingContext, [ + // #9 + cleanStyle(2, 17), 'width', null, 0, - // #12 - cleanStyle(0, 20), + // #13 + cleanStyle(4, 21), 'height', null, 0, - // #16 - dirtyStyle(0, 8), + // #17 + dirtyStyle(2, 9), 'width', '100px', 0, - // #20 - dirtyStyle(0, 12), + // #21 + dirtyStyle(4, 13), 'height', '100px', 0, @@ -452,84 +749,66 @@ describe('style and class based bindings', () => { getStyles(stylingContext); updateStyles(stylingContext, {width: '200px', opacity: '0'}); - expect(stylingContext).toEqual([ - null, - null, - [null], - dirtyStyle(0, 16), // - 2, - element, - null, - {width: '200px', opacity: '0'}, - - // #8 - cleanStyle(0, 16), + assertContextOnlyValues(stylingContext, [ + // #9 + cleanStyle(2, 17), 'width', null, 0, - // #12 - cleanStyle(0, 24), + // #13 + cleanStyle(4, 25), 'height', null, 0, - // #16 - dirtyStyle(0, 8), + // #17 + dirtyStyle(2, 9), 'width', '200px', 0, - // #20 + // #21 dirtyStyle(), 'opacity', '0', 0, - // #23 - dirtyStyle(0, 12), + // #25 + dirtyStyle(4, 13), 'height', null, 0, ]); getStyles(stylingContext); - expect(stylingContext).toEqual([ - null, - null, - [null], - cleanStyle(0, 16), // - 2, - element, - null, - {width: '200px', opacity: '0'}, - - // #8 - cleanStyle(0, 16), + assertContextOnlyValues(stylingContext, [ + // #9 + cleanStyle(2, 17), 'width', null, 0, - // #12 - cleanStyle(0, 24), + // #13 + cleanStyle(4, 25), 'height', null, 0, - // #16 - cleanStyle(0, 8), + // #17 + cleanStyle(2, 9), 'width', '200px', 0, - // #20 + // #21 cleanStyle(), 'opacity', '0', 0, // #23 - cleanStyle(0, 12), + cleanStyle(4, 13), 'height', null, 0, @@ -538,42 +817,33 @@ describe('style and class based bindings', () => { updateStyles(stylingContext, {width: null}); updateStyleProp(stylingContext, 0, '300px'); - expect(stylingContext).toEqual([ - null, - null, - [null], - dirtyStyle(0, 16), // - 2, - element, - null, - {width: null}, - - // #8 - dirtyStyle(0, 16), + assertContextOnlyValues(stylingContext, [ + // #9 + dirtyStyle(2, 17), 'width', '300px', 0, - // #12 - cleanStyle(0, 24), + // #13 + cleanStyle(4, 25), 'height', null, 0, - // #16 - cleanStyle(0, 8), + // #17 + cleanStyle(2, 9), 'width', null, 0, - // #20 + // #21 dirtyStyle(), 'opacity', null, 0, // #23 - cleanStyle(0, 12), + cleanStyle(4, 13), 'height', null, 0, @@ -582,42 +852,33 @@ describe('style and class based bindings', () => { getStyles(stylingContext); updateStyleProp(stylingContext, 0, null); - expect(stylingContext).toEqual([ - null, - null, - [null], - dirtyStyle(0, 16), // - 2, - element, - null, - {width: null}, - - // #8 - dirtyStyle(0, 16), + assertContextOnlyValues(stylingContext, [ + // #9 + dirtyStyle(2, 17), 'width', null, 0, - // #12 - cleanStyle(0, 24), + // #13 + cleanStyle(4, 25), 'height', null, 0, - // #16 - cleanStyle(0, 8), + // #17 + cleanStyle(2, 9), 'width', null, 0, - // #20 + // #21 cleanStyle(), 'opacity', null, 0, // #23 - cleanStyle(0, 12), + cleanStyle(4, 13), 'height', null, 0, @@ -626,47 +887,38 @@ describe('style and class based bindings', () => { it('should find the next available space in the context when data is added after being removed before', () => { - const stylingContext = initContext(['lineHeight']); + const stylingContext = initContext(null, ['lineHeight']); const getStyles = trackStylesFactory(); updateStyles(stylingContext, {width: '100px', height: '100px', opacity: '0.5'}); - expect(stylingContext).toEqual([ - null, - null, - [null], - dirtyStyle(0, 12), // - 1, - element, - null, - {width: '100px', height: '100px', opacity: '0.5'}, - - // #8 - cleanStyle(0, 24), + assertContextOnlyValues(stylingContext, [ + // #9 + cleanStyle(2, 25), 'lineHeight', null, 0, - // #12 + // #13 dirtyStyle(), 'width', '100px', 0, - // #16 + // #17 dirtyStyle(), 'height', '100px', 0, - // #20 + // #21 dirtyStyle(), 'opacity', '0.5', 0, // #23 - cleanStyle(0, 8), + cleanStyle(2, 9), 'lineHeight', null, 0, @@ -675,42 +927,33 @@ describe('style and class based bindings', () => { getStyles(stylingContext); updateStyles(stylingContext, {}); - expect(stylingContext).toEqual([ - null, - null, - [null], - dirtyStyle(0, 12), // - 1, - element, - null, - {}, - - // #8 - cleanStyle(0, 24), + assertContextOnlyValues(stylingContext, [ + // #9 + cleanStyle(2, 25), 'lineHeight', null, 0, - // #12 + // #13 dirtyStyle(), 'width', null, 0, - // #16 + // #17 dirtyStyle(), 'height', null, 0, - // #20 + // #21 dirtyStyle(), 'opacity', null, 0, // #23 - cleanStyle(0, 8), + cleanStyle(2, 9), 'lineHeight', null, 0, @@ -721,35 +964,26 @@ describe('style and class based bindings', () => { borderWidth: '5px', }); - expect(stylingContext).toEqual([ - null, - null, - [null], - dirtyStyle(0, 12), // - 1, - element, - null, - {borderWidth: '5px'}, - - // #8 - cleanStyle(0, 28), + assertContextOnlyValues(stylingContext, [ + // #9 + cleanStyle(2, 29), 'lineHeight', null, 0, - // #12 + // #13 dirtyStyle(), 'borderWidth', '5px', 0, - // #16 + // #17 cleanStyle(), 'width', null, 0, - // #20 + // #21 cleanStyle(), 'height', null, @@ -761,8 +995,8 @@ describe('style and class based bindings', () => { null, 0, - // #28 - cleanStyle(0, 8), + // #29 + cleanStyle(2, 9), 'lineHeight', null, 0, @@ -770,35 +1004,26 @@ describe('style and class based bindings', () => { updateStyleProp(stylingContext, 0, '200px'); - expect(stylingContext).toEqual([ - null, - null, - [null], - dirtyStyle(0, 12), // - 1, - element, - null, - {borderWidth: '5px'}, - - // #8 - dirtyStyle(0, 28), + assertContextOnlyValues(stylingContext, [ + // #9 + dirtyStyle(2, 29), 'lineHeight', '200px', 0, - // #12 + // #13 dirtyStyle(), 'borderWidth', '5px', 0, - // #16 + // #17 cleanStyle(), 'width', null, 0, - // #20 + // #21 cleanStyle(), 'height', null, @@ -810,8 +1035,8 @@ describe('style and class based bindings', () => { null, 0, - // #28 - cleanStyle(0, 8), + // #29 + cleanStyle(2, 9), 'lineHeight', null, 0, @@ -819,35 +1044,26 @@ describe('style and class based bindings', () => { updateStyles(stylingContext, {borderWidth: '15px', borderColor: 'red'}); - expect(stylingContext).toEqual([ - null, - null, - [null], - dirtyStyle(0, 12), // - 1, - element, - null, - {borderWidth: '15px', borderColor: 'red'}, - - // #8 - dirtyStyle(0, 32), + assertContextOnlyValues(stylingContext, [ + // #9 + dirtyStyle(2, 33), 'lineHeight', '200px', 0, - // #12 + // #13 dirtyStyle(), 'borderWidth', '15px', 0, - // #16 + // #17 dirtyStyle(), 'borderColor', 'red', 0, - // #20 + // #21 cleanStyle(), 'width', null, @@ -859,14 +1075,14 @@ describe('style and class based bindings', () => { null, 0, - // #28 + // #29 cleanStyle(), 'opacity', null, 0, - // #32 - cleanStyle(0, 8), + // #33 + cleanStyle(2, 9), 'lineHeight', null, 0, @@ -875,38 +1091,36 @@ describe('style and class based bindings', () => { it('should render all data as not being dirty after the styles are built', () => { const getStyles = trackStylesFactory(); - const stylingContext = initContext(['height']); - - updateStyles(stylingContext, { - width: '100px', - }); + const stylingContext = initContext(null, ['height']); + updateStyles(stylingContext, {width: '100px'}); updateStyleProp(stylingContext, 0, '200px'); - expect(stylingContext).toEqual([ - null, - null, + assertContext(stylingContext, [ + masterConfig(13, true), // + [null, 2, true, null], + [null, 'height', null], [null], - dirtyStyle(0, 12), // - 1, + [1, 0, 1, 0, 9], element, null, {width: '100px'}, + null, - // #8 - dirtyStyle(0, 16), + // #9 + dirtyStyle(2, 17), 'height', '200px', 0, - // #12 + // #13 dirtyStyle(), 'width', '100px', 0, - // #16 - cleanStyle(0, 8), + // #17 + cleanStyle(2, 9), 'height', null, 0, @@ -914,30 +1128,31 @@ describe('style and class based bindings', () => { getStyles(stylingContext); - expect(stylingContext).toEqual([ - null, - null, + assertContext(stylingContext, [ + masterConfig(13, false), // + [null, 2, false, null], + [null, 'height', null], [null], - cleanStyle(0, 12), // - 1, + [1, 0, 1, 0, 9], element, null, {width: '100px'}, + null, - // #8 - cleanStyle(0, 16), + // #9 + cleanStyle(2, 17), 'height', '200px', 0, - // #12 + // #13 cleanStyle(), 'width', '100px', 0, - // #16 - cleanStyle(0, 8), + // #17 + cleanStyle(2, 9), 'height', null, 0, @@ -947,43 +1162,34 @@ describe('style and class based bindings', () => { it('should mark styles that may contain url values as being sanitizable (when a sanitizer is passed in)', () => { const getStyles = trackStylesFactory(); - const initialStyles = ['border-image', 'border-width']; + const styleBindings = ['border-image', 'border-width']; const styleSanitizer = defaultStyleSanitizer; - const stylingContext = initContext(initialStyles, null, styleSanitizer); + const stylingContext = initContext(null, styleBindings, null, null, styleSanitizer); updateStyleProp(stylingContext, 0, 'url(foo.jpg)'); updateStyleProp(stylingContext, 1, '100px'); - expect(stylingContext).toEqual([ - null, - styleSanitizer, - [null], - dirtyStyle(0, 16), // - 2, - element, - null, - null, - - // #8 - dirtyStyleWithSanitization(0, 16), + assertContextOnlyValues(stylingContext, [ + // #9 + dirtyStyleWithSanitization(2, 17), 'border-image', 'url(foo.jpg)', 0, - // #12 - dirtyStyle(0, 20), + // #13 + dirtyStyle(4, 21), 'border-width', '100px', 0, - // #16 - cleanStyleWithSanitization(0, 8), + // #17 + cleanStyleWithSanitization(2, 9), 'border-image', null, 0, - // #20 - cleanStyle(0, 12), + // #21 + cleanStyle(4, 13), 'border-width', null, 0, @@ -991,42 +1197,33 @@ describe('style and class based bindings', () => { updateStyles(stylingContext, {'background-image': 'unsafe'}); - expect(stylingContext).toEqual([ - null, - styleSanitizer, - [null], - dirtyStyle(0, 16), // - 2, - element, - null, - {'background-image': 'unsafe'}, - - // #8 - dirtyStyleWithSanitization(0, 20), + assertContextOnlyValues(stylingContext, [ + // #9 + dirtyStyleWithSanitization(2, 21), 'border-image', 'url(foo.jpg)', 0, - // #12 - dirtyStyle(0, 24), + // #13 + dirtyStyle(4, 25), 'border-width', '100px', 0, - // #16 + // #17 dirtyStyleWithSanitization(0, 0), 'background-image', 'unsafe', 0, - // #20 - cleanStyleWithSanitization(0, 8), + // #21 + cleanStyleWithSanitization(2, 9), 'border-image', null, 0, // #23 - cleanStyle(0, 12), + cleanStyle(4, 13), 'border-width', null, 0, @@ -1034,59 +1231,166 @@ describe('style and class based bindings', () => { getStyles(stylingContext); - expect(stylingContext).toEqual([ - null, - styleSanitizer, - [null], - cleanStyle(0, 16), // - 2, - element, - null, - {'background-image': 'unsafe'}, - - // #8 - cleanStyleWithSanitization(0, 20), + assertContextOnlyValues(stylingContext, [ + // #9 + cleanStyleWithSanitization(2, 21), 'border-image', 'url(foo.jpg)', 0, - // #12 - cleanStyle(0, 24), + // #13 + cleanStyle(4, 25), 'border-width', '100px', 0, - // #16 + // #17 cleanStyleWithSanitization(0, 0), 'background-image', 'unsafe', 0, - // #20 - cleanStyleWithSanitization(0, 8), + // #21 + cleanStyleWithSanitization(2, 9), 'border-image', null, 0, // #23 - cleanStyle(0, 12), + cleanStyle(4, 13), 'border-width', null, 0, ]); }); + + it('should only update styling values for successive directives if null in a former directive', + () => { + const template = createEmptyStylingContext(); + + const dir1 = {}; + const dir2 = {}; + const dir3 = {}; + + updateContextWithBindings(template, dir1, null, ['width', 'height']); + updateContextWithBindings(template, dir2, null, ['width', 'color']); + updateContextWithBindings(template, dir3, null, ['color', 'opacity']); + + const ctx = allocStylingContext(element, template); + + // styles 0 = width, 1 = height, 2 = color within the context + const widthIndex = StylingIndex.SingleStylesStartPosition + StylingIndex.Size * 0; + const colorIndex = StylingIndex.SingleStylesStartPosition + StylingIndex.Size * 2; + + updateStyleProp(ctx, 0, '200px', dir1); + updateStyleProp(ctx, 0, '100px', dir2); + expect(ctx[widthIndex + StylingIndex.ValueOffset]).toEqual('200px'); + expect(getDirectiveIndexFromEntry(ctx, widthIndex)).toEqual(1); + + updateStyleProp(ctx, 0, 'blue', dir3); + updateStyleProp(ctx, 1, 'red', dir2); + expect(ctx[colorIndex + StylingIndex.ValueOffset]).toEqual('red'); + expect(getDirectiveIndexFromEntry(ctx, colorIndex)).toEqual(2); + + updateStyleProp(ctx, 0, null, dir1); + updateStyleProp(ctx, 0, '100px', dir2); + expect(ctx[widthIndex + StylingIndex.ValueOffset]).toEqual('100px'); + expect(getDirectiveIndexFromEntry(ctx, widthIndex)).toEqual(2); + + updateStyleProp(ctx, 1, null, dir2); + updateStyleProp(ctx, 0, 'blue', dir3); + updateStyleProp(ctx, 1, null, dir2); + expect(ctx[colorIndex + StylingIndex.ValueOffset]).toEqual('blue'); + expect(getDirectiveIndexFromEntry(ctx, colorIndex)).toEqual(3); + }); + + it('should throw an error if a directive is provided that isn\'t registered', () => { + const template = createEmptyStylingContext(); + const knownDir = {}; + const unknownDir = {}; + updateContextWithBindings(template, knownDir, null, ['color']); + + const ctx = allocStylingContext(element, template); + updateStyleProp(ctx, 0, 'blue', knownDir); + + expect(() => { updateStyleProp(ctx, 0, 'blue', unknownDir); }).toThrow(); + }); + + it('should use a different sanitizer when a different directive\'s binding is updated', + () => { + const getStyles = trackStylesFactory(); + + const makeSanitizer = (id: string) => { + return (function(prop: string, value?: string): string | boolean { + return `${value}-${id}`; + } as StyleSanitizeFn); + }; + + const template = createEmptyStylingContext(); + const dirWithSanitizer1 = {}; + const sanitizer1 = makeSanitizer('1'); + const dirWithSanitizer2 = {}; + const sanitizer2 = makeSanitizer('2'); + const dirWithoutSanitizer = {}; + updateContextWithBindings(template, dirWithSanitizer1, null, ['color'], sanitizer1); + updateContextWithBindings(template, dirWithSanitizer2, null, ['color'], sanitizer2); + updateContextWithBindings(template, dirWithoutSanitizer, null, ['color']); + + const ctx = allocStylingContext(element, template); + expect(ctx[StylingIndex.DirectiveRegistryPosition]).toEqual([ + null, // + -1, // + false, // + null, // + dirWithSanitizer1, // + 2, // + false, // + sanitizer1, // + dirWithSanitizer2, // + 5, // + false, // + sanitizer2, // + dirWithoutSanitizer, // + 8, // + false, // + null + ]); + + const colorIndex = StylingIndex.SingleStylesStartPosition; + expect(((ctx[colorIndex] as number) & StylingFlags.Sanitize) > 0).toBeTruthy(); + + updateStyleProp(ctx, 0, 'green', dirWithoutSanitizer); + expect(((ctx[colorIndex] as number) & StylingFlags.Sanitize) > 0).toBeFalsy(); + expect(getStyles(ctx, dirWithoutSanitizer)).toEqual({color: 'green'}); + + updateStyleProp(ctx, 0, 'blue', dirWithSanitizer1); + expect(((ctx[colorIndex] as number) & StylingFlags.Sanitize) > 0).toBeTruthy(); + expect(getStyles(ctx, dirWithSanitizer1)).toEqual({color: 'blue-1'}); + + updateStyleProp(ctx, 0, null, dirWithSanitizer1); + updateStyleProp(ctx, 0, 'red', dirWithSanitizer2); + expect(((ctx[colorIndex] as number) & StylingFlags.Sanitize) > 0).toBeTruthy(); + expect(getStyles(ctx, dirWithSanitizer2)).toEqual({color: 'red-2'}); + + updateStyleProp(ctx, 0, null, dirWithSanitizer2); + updateStyleProp(ctx, 0, 'green', dirWithoutSanitizer); + expect(((ctx[colorIndex] as number) & StylingFlags.Sanitize) > 0).toBeFalsy(); + expect(getStyles(ctx, dirWithoutSanitizer)).toEqual({color: 'green'}); + }); }); it('should skip issuing style updates if there is nothing to update upon first render', () => { - const stylingContext = initContext([InitialStylingFlags.VALUES_MODE, 'color', '']); + const stylingContext = initContext(null, ['color']); const store = new MockStylingStore(element as HTMLElement, BindingType.Class); const getStyles = trackStylesFactory(store); + const otherDirective = {}; let styles: any = {fontSize: ''}; updateStyleProp(stylingContext, 0, ''); updateStylingMap(stylingContext, null, styles); + patchContextWithStaticAttrs(stylingContext, [], otherDirective); - getStyles(stylingContext, true); + getStyles(stylingContext, otherDirective); expect(store.getValues()).toEqual({}); styles = {fontSize: '20px'}; @@ -1106,39 +1410,39 @@ describe('style and class based bindings', () => { }); describe('classes', () => { - it('should initialize with the provided classes', () => { - const template = - initContext(null, [InitialStylingFlags.VALUES_MODE, 'one', true, 'two', true]); - expect(template).toEqual([ - null, - null, - [null, true, true], - dirtyStyle(0, 16), // - 0, + it('should initialize with the provided class bindings', () => { + const template = initContext(null, null, null, ['one', 'two']); + assertContext(template, [ + masterConfig(17, false), // + [null, 2, false, null], + [null], + [null, 'one', false, 'two', false], + [0, 2, 0, 2, 9, 13], element, null, null, + null, - // #8 - cleanClass(1, 16), + // #9 + cleanClass(2, 17), 'one', null, 0, - // #12 - cleanClass(2, 20), + // #13 + cleanClass(4, 21), 'two', null, 0, - // #16 - dirtyClass(1, 8), + // #17 + cleanClass(2, 9), 'one', null, 0, - // #20 - dirtyClass(2, 12), + // #21 + cleanClass(4, 13), 'two', null, 0, @@ -1147,7 +1451,7 @@ describe('style and class based bindings', () => { it('should update multi class properties against the static classes', () => { const getClasses = trackClassesFactory(); - const stylingContext = initContext(null, ['bar']); + const stylingContext = initContext(null, null, ['bar'], ['bar', 'foo']); expect(getClasses(stylingContext)).toEqual({}); updateClasses(stylingContext, {foo: true, bar: false}); expect(getClasses(stylingContext)).toEqual({'foo': true, 'bar': false}); @@ -1155,11 +1459,10 @@ describe('style and class based bindings', () => { expect(getClasses(stylingContext)).toEqual({'foo': false, 'bar': true}); }); - it('should update single class properties against the static classes', () => { + it('should update single class properties despite static classes being present', () => { const getClasses = trackClassesFactory(); - const stylingContext = - initContext(null, ['bar', 'foo', InitialStylingFlags.VALUES_MODE, 'bar', true]); - expect(getClasses(stylingContext)).toEqual({'bar': true}); + const stylingContext = initContext(null, null, ['bar'], ['bar', 'foo']); + expect(getClasses(stylingContext)).toEqual({}); updateClassProp(stylingContext, 0, true); updateClassProp(stylingContext, 1, true); @@ -1167,246 +1470,255 @@ describe('style and class based bindings', () => { updateClassProp(stylingContext, 0, false); updateClassProp(stylingContext, 1, false); - expect(getClasses(stylingContext)).toEqual({'bar': true, 'foo': false}); + expect(getClasses(stylingContext)).toEqual({'bar': false, 'foo': false}); }); it('should understand updating multi-classes using a string-based value while respecting single class-based props', () => { const getClasses = trackClassesFactory(); - const stylingContext = initContext(null, ['guy']); + const stylingContext = initContext(null, null, null, ['baz']); expect(getClasses(stylingContext)).toEqual({}); - updateStylingMap(stylingContext, 'foo bar guy'); - expect(getClasses(stylingContext)).toEqual({'foo': true, 'bar': true, 'guy': true}); + updateStylingMap(stylingContext, 'foo bar baz'); + expect(getClasses(stylingContext)).toEqual({'foo': true, 'bar': true, 'baz': true}); - updateStylingMap(stylingContext, 'foo man'); + updateStylingMap(stylingContext, 'foo car'); updateClassProp(stylingContext, 0, true); expect(getClasses(stylingContext)) - .toEqual({'foo': true, 'man': true, 'bar': false, 'guy': true}); + .toEqual({'foo': true, 'car': true, 'bar': false, 'baz': true}); }); - 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, - null, - [null, '100px', true], - dirtyStyle(0, 24), // - 2, - element, - null, - null, + it('should place styles within the context and work alongside style-based values in harmony', + () => { + const getStylesAndClasses = trackStylesAndClasses(); + const stylingContext = + initContext(['width', '100px'], ['width', 'height'], ['wide'], ['wide', 'tall']); + assertContext(stylingContext, [ + masterConfig(25, false), // + [null, 2, false, null], + [null, 'width', '100px', 'height', null], + [null, 'wide', true, 'tall', false], + [2, 2, 2, 2, 9, 13, 17, 21], + element, + null, + null, + null, - // #8 - cleanStyle(1, 24), - 'width', - null, - 0, + // #9 + cleanStyle(2, 25), + 'width', + null, + 0, - // #12 - cleanStyle(0, 28), - 'height', - null, - 0, + // #13 + cleanStyle(4, 29), + 'height', + null, + 0, - // #16 - cleanClass(2, 32), - 'wide', - null, - 0, + // #17 + cleanClass(2, 33), + 'wide', + null, + 0, - // #20 - cleanClass(0, 36), - 'tall', - null, - 0, + // #21 + cleanClass(4, 37), + 'tall', + null, + 0, - // #23 - dirtyStyle(1, 8), - 'width', - null, - 0, + // #25 + cleanStyle(2, 9), + 'width', + null, + 0, - // #28 - cleanStyle(0, 12), - 'height', - null, - 0, + // #29 + cleanStyle(4, 13), + 'height', + null, + 0, - // #32 - dirtyClass(2, 16), - 'wide', - null, - 0, + // #33 + cleanClass(2, 17), + 'wide', + null, + 0, - // #36 - cleanClass(0, 20), - 'tall', - null, - 0, - ]); + // #37 + cleanClass(4, 21), + 'tall', + null, + 0, + ]); - expect(getStylesAndClasses(stylingContext)).toEqual([{wide: true}, {width: '100px'}]); + expect(getStylesAndClasses(stylingContext)).toEqual([{}, {}]); - updateStylingMap(stylingContext, 'tall round', {width: '200px', opacity: '0.5'}); - expect(stylingContext).toEqual([ - null, - null, - [null, '100px', true], - dirtyStyle(0, 24), // - 2, - element, - 'tall round', - {width: '200px', opacity: '0.5'}, + updateStylingMap(stylingContext, 'tall round', {width: '200px', opacity: '0.5'}); + assertContext(stylingContext, [ + masterConfig(25, true), // + [null, 2, true, null], + [null, 'width', '100px', 'height', null], + [null, 'wide', true, 'tall', false], + [2, 2, 2, 2, 9, 13, 17, 21], + element, + 'tall round', + {width: '200px', opacity: '0.5'}, + null, - // #8 - cleanStyle(1, 24), - 'width', - null, - 0, + // #9 + cleanStyle(2, 25), + 'width', + null, + 0, - // #12 - cleanStyle(0, 44), - 'height', - null, - 0, + // #13 + cleanStyle(4, 45), + 'height', + null, + 0, - // #16 - cleanClass(2, 40), - 'wide', - null, - 0, + // #17 + cleanClass(2, 41), + 'wide', + null, + 0, - // #20 - cleanClass(0, 32), - 'tall', - null, - 0, + // #21 + cleanClass(4, 33), + 'tall', + null, + 0, - // #23 - dirtyStyle(1, 8), - 'width', - '200px', - 0, + // #23 + dirtyStyle(2, 9), + 'width', + '200px', + 0, - // #28 - dirtyStyle(0, 0), - 'opacity', - '0.5', - 0, + // #29 + dirtyStyle(0, 0), + 'opacity', + '0.5', + 0, - // #32 - dirtyClass(0, 20), - 'tall', - true, - 0, + // #33 + dirtyClass(4, 21), + 'tall', + true, + 0, - // #36 - dirtyClass(0, 0), - 'round', - true, - 0, + // #37 + dirtyClass(0, 0), + 'round', + true, + 0, - // #40 - cleanClass(2, 16), - 'wide', - null, - 0, + // #41 + cleanClass(2, 17), + 'wide', + null, + 0, - // #44 - cleanStyle(0, 12), - 'height', - null, - 0, - ]); + // #45 + cleanStyle(4, 13), + 'height', + null, + 0, + ]); - expect(getStylesAndClasses(stylingContext)).toEqual([ - {tall: true, round: true, wide: true}, - {width: '200px', opacity: '0.5'}, - ]); + expect(getStylesAndClasses(stylingContext)).toEqual([ + {tall: true, round: true}, + {width: '200px', opacity: '0.5'}, + ]); - updateStylingMap(stylingContext, {tall: true, wide: true}, {width: '500px'}); - updateStyleProp(stylingContext, 0, '300px'); + updateStylingMap(stylingContext, {tall: true, wide: true}, {width: '500px'}); + updateStyleProp(stylingContext, 0, '300px'); - expect(stylingContext).toEqual([ - null, - null, - [null, '100px', true], - dirtyStyle(0, 24), // - 2, - element, - {tall: true, wide: true}, - {width: '500px'}, + assertContext(stylingContext, [ + masterConfig(25, true), // + [null, 2, true, null], + [null, 'width', '100px', 'height', null], + [null, 'wide', true, 'tall', false], + [2, 2, 2, 2, 9, 13, 17, 21], + element, + {tall: true, wide: true}, + {width: '500px'}, + null, - // #8 - dirtyStyle(1, 24), - 'width', - '300px', - 0, + // #9 + dirtyStyle(2, 25), + 'width', + '300px', + 0, - // #12 - cleanStyle(0, 44), - 'height', - null, - 0, + // #13 + cleanStyle(4, 45), + 'height', + null, + 0, - // #16 - cleanClass(2, 32), - 'wide', - null, - 0, + // #17 + cleanClass(2, 33), + 'wide', + null, + 0, - // #20 - cleanClass(0, 28), - 'tall', - null, - 0, + // #21 + cleanClass(4, 29), + 'tall', + null, + 0, - // #23 - cleanStyle(1, 8), - 'width', - '500px', - 0, + // #25 + cleanStyle(2, 9), + 'width', + '500px', + 0, - // #28 - cleanClass(0, 20), - 'tall', - true, - 0, + // #29 + cleanClass(4, 21), + 'tall', + true, + 0, - // #32 - cleanClass(2, 16), - 'wide', - true, - 0, + // #33 + cleanClass(2, 17), + 'wide', + true, + 0, - // #35 - dirtyClass(0, 0), - 'round', - null, - 0, + // #37 + dirtyClass(0, 0), + 'round', + null, + 0, - // #39 - dirtyStyle(0, 0), - 'opacity', - null, - 0, + // #41 + dirtyStyle(0, 0), + 'opacity', + null, + 0, - // #43 - cleanStyle(0, 12), - 'height', - null, - 0, - ]); + // #45 + cleanStyle(4, 13), + 'height', + null, + 0, + ]); - expect(getStylesAndClasses(stylingContext)).toEqual([ - {tall: true, round: false, wide: true}, - {width: '300px', opacity: null}, - ]); - }); + expect(getStylesAndClasses(stylingContext)).toEqual([ + {tall: true, round: false}, + {width: '300px', opacity: null}, + ]); + + updateStylingMap(stylingContext, {wide: false}); + + expect(getStylesAndClasses(stylingContext)).toEqual([ + {wide: false, tall: false, round: false}, {width: '100px', opacity: null} + ]); + }); it('should skip updating multi classes and styles if the input identity has not changed', () => { @@ -1420,27 +1732,22 @@ describe('style and class based bindings', () => { // apply the styles getStylesAndClasses(stylingContext); - expect(stylingContext).toEqual([ - null, - null, - [null], - cleanStyle(0, 8), // - 0, - element, - {foo: true}, - {width: '200px'}, + assertContext(stylingContext, [ + masterConfig(9, false), // + [null, 2, false, null], // + [null], // + [null], // + [0, 0, 0, 0], // + element, // + {foo: true}, // + {width: '200px'}, // + null, // - // #8 - cleanStyle(0, 0), - 'width', - '200px', - 0, + // #9 + cleanStyle(0, 0), 'width', '200px', 0, - // #11 - cleanClass(0, 0), - 'foo', - true, - 0, + // #13 + cleanClass(0, 0), 'foo', true, 0 ]); stylesMap.width = '300px'; @@ -1451,27 +1758,22 @@ describe('style and class based bindings', () => { // apply the styles getStylesAndClasses(stylingContext); - expect(stylingContext).toEqual([ - null, - null, - [null], - cleanStyle(0, 8), // - 0, - element, - {foo: false}, - {width: '300px'}, + assertContext(stylingContext, [ + masterConfig(9, false), // + [null, 2, false, null], // + [null], // + [null], // + [0, 0, 0, 0], // + element, // + {foo: false}, // + {width: '300px'}, // + null, // - // #8 - cleanStyle(0, 0), - 'width', - '200px', - 0, + // #9 + cleanStyle(0, 0), 'width', '200px', 0, - // #11 - cleanClass(0, 0), - 'foo', - true, - 0, + // #13 + cleanClass(0, 0), 'foo', true, 0 ]); }); @@ -1485,37 +1787,42 @@ describe('style and class based bindings', () => { // apply the styles expect(getClasses(stylingContext)).toEqual({apple: true, orange: true, banana: true}); - expect(stylingContext).toEqual([ - null, - null, + assertContext(stylingContext, [ + masterConfig(9, false), // + [null, 2, false, null], [null], - cleanStyle(0, 8), // - 0, + [null], + [0, 0, 0, 0], element, 'apple orange banana', null, + null, - // #8 + // #9 cleanClass(0, 0), 'apple', true, 0, - // #12 + // #13 cleanClass(0, 0), 'orange', true, 0, - // #16 + // #17 cleanClass(0, 0), 'banana', true, 0, ]); - stylingContext[13] = false; // no orange - stylingContext[16] = false; // no banana + stylingContext + [StylingIndex.SingleStylesStartPosition + 1 * StylingIndex.Size + + StylingIndex.ValueOffset] = false; // no orange + stylingContext + [StylingIndex.SingleStylesStartPosition + 2 * StylingIndex.Size + + StylingIndex.ValueOffset] = false; // no banana updateStylingMap(stylingContext, classes); // apply the styles @@ -1523,7 +1830,7 @@ describe('style and class based bindings', () => { }); it('should skip issuing class updates if there is nothing to update upon first render', () => { - const stylingContext = initContext(null, [InitialStylingFlags.VALUES_MODE, 'blue', false]); + const stylingContext = initContext(null, null, ['blue'], ['blue']); const store = new MockStylingStore(element as HTMLElement, BindingType.Class); const getClasses = trackClassesFactory(store); @@ -1553,7 +1860,7 @@ describe('style and class based bindings', () => { describe('players', () => { it('should build a player with the computed styles and classes', () => { - const context = initContext(null, []); + const context = initContext(); const styles = {width: '100px', height: '200px'}; const classes = 'foo bar'; @@ -1700,7 +2007,7 @@ describe('style and class based bindings', () => { it('should kick off single property change players alongside map-based ones and remove the players', () => { - const context = initContext(['width', 'height'], ['foo', 'bar']); + const context = initContext(null, ['width', 'height'], null, ['foo', 'bar']); const handler = new CorePlayerHandler(); const lView = createMockViewData(handler, context); @@ -1726,6 +2033,7 @@ describe('style and class based bindings', () => { styleMapFactory, element as HTMLElement, BindingType.Style); const classMapPlayerBuilder = new ClassAndStylePlayerBuilder( classMapFactory, element as HTMLElement, BindingType.Class); + updateStylingMap(context, classMapFactory, styleMapFactory); const widthFactory = bindPlayerFactory(styleBuildFn, '100px'); @@ -1796,7 +2104,7 @@ describe('style and class based bindings', () => { it('should destroy an existing player that was queued before it is flushed once the binding updates', () => { - const context = initContext(['width']); + const context = initContext(null, ['width']); const handler = new CorePlayerHandler(); const lView = createMockViewData(handler, context); @@ -1831,7 +2139,7 @@ describe('style and class based bindings', () => { it('should nullify style map and style property factories if any follow up expressions not use them', () => { - const context = initContext(['color'], ['foo']); + const context = initContext(null, ['color'], null, ['foo']); const handler = new CorePlayerHandler(); const lView = createMockViewData(handler, context); @@ -1849,36 +2157,37 @@ describe('style and class based bindings', () => { return player; }; - expect(context).toEqual([ - null, - null, - [null], - cleanStyle(0, 16), // - 1, - element, - null, - null, + assertContext(context, [ + masterConfig(17, false), // + [null, 2, false, null], // + [null, 'color', null], // + [null, 'foo', false], // + [1, 1, 1, 1, 9, 13], // + element, // + null, // + null, // + null, // - // #8 - cleanStyle(0, 16), + // #9 + cleanStyle(2, 17), 'color', null, 0, - // #12 - cleanClass(0, 20), + // #13 + cleanClass(2, 21), 'foo', null, 0, - // #16 - cleanStyle(0, 8), + // #17 + cleanStyle(2, 9), 'color', null, 0, - // #20 - cleanClass(0, 12), + // #21 + cleanClass(2, 13), 'foo', null, 0, @@ -1902,51 +2211,56 @@ describe('style and class based bindings', () => { const p2 = stylePlayers.shift(); const p3 = stylePlayers.shift(); const p4 = classPlayers.shift(); - expect(context).toEqual([ - ([ - 9, classMapPlayerBuilder, p1, styleMapPlayerBuilder, p2, colorPlayerBuilder, p3, - fooPlayerBuilder, p4 - ] as any), - null, - [null], - cleanStyle(0, 16), // - 1, - element, - {map: true}, - {opacity: '1'}, - // #8 - cleanStyle(0, 24), + let playerContext = context[StylingIndex.PlayerContext] !; + expect(playerContext).toEqual([ + 9, classMapPlayerBuilder, p1, styleMapPlayerBuilder, p2, colorPlayerBuilder, p3, + fooPlayerBuilder, p4 + ] as PlayerContext); + + assertContext(context, [ + masterConfig(17, false), // + [null, 2, false, null], // + [null, 'color', null], // + [null, 'foo', false], // + [1, 1, 1, 1, 9, 13], // + element, // + {map: true}, // + {opacity: '1'}, // + playerContext, + + // #9 + cleanStyle(2, 25), 'color', 'red', - 5, + directiveOwnerPointers(0, 5), - // #12 - cleanClass(0, 28), + // #13 + cleanClass(2, 29), 'foo', true, - 7, + directiveOwnerPointers(0, 7), - // #16 + // #17 cleanStyle(0, 0), 'opacity', '1', - 3, + directiveOwnerPointers(0, 3), - // #20 + // #21 cleanClass(0, 0), 'map', true, - 1, + directiveOwnerPointers(0, 1), // #23 - cleanStyle(0, 8), + cleanStyle(2, 9), 'color', null, 0, - // #28 - cleanClass(0, 12), + // #29 + cleanClass(2, 13), 'foo', null, 0, @@ -1962,48 +2276,54 @@ describe('style and class based bindings', () => { updateClassProp(context, 0, fooWithoutPlayerFactory); renderStyles(context, false, undefined, lView); - expect(context).toEqual([ - ([9, null, null, null, null, null, null, null, null] as any), - null, - [null], - cleanStyle(0, 16), // - 1, - element, - {map: true}, - {opacity: '1'}, + playerContext = context[StylingIndex.PlayerContext] !; + expect(playerContext).toEqual([ + 9, null, null, null, null, null, null, null, null + ] as PlayerContext); - // #8 - cleanStyle(0, 24), + assertContext(context, [ + masterConfig(17, false), // + [null, 2, false, null], // + [null, 'color', null], // + [null, 'foo', false], // + [1, 1, 1, 1, 9, 13], // + element, // + {map: true}, // + {opacity: '1'}, // + playerContext, + + // #9 + cleanStyle(2, 25), 'color', 'blue', 0, - // #12 - cleanClass(0, 28), + // #13 + cleanClass(2, 29), 'foo', false, 0, - // #16 + // #17 cleanStyle(0, 0), 'opacity', '1', 0, - // #20 + // #21 cleanClass(0, 0), 'map', true, 0, // #23 - cleanStyle(0, 8), + cleanStyle(2, 9), 'color', null, 0, - // #28 - cleanClass(0, 12), + // #29 + cleanClass(2, 13), 'foo', null, 0, @@ -2062,7 +2382,7 @@ describe('style and class based bindings', () => { it('should invoke a single prop player over a multi style player when present and delegate back if not', () => { - const context = initContext(['color']); + const context = initContext(null, ['color']); const handler = new CorePlayerHandler(); const lView = createMockViewData(handler, context); @@ -2139,7 +2459,7 @@ describe('style and class based bindings', () => { }); it('should return the old player for classes when a follow-up player is instantiated', () => { - const context = initContext([]); + const context = initContext(); const handler = new CorePlayerHandler(); const lView = createMockViewData(handler, context); @@ -2181,7 +2501,7 @@ describe('style and class based bindings', () => { } }) as StyleSanitizeFn; - const context = initContext([], [], sanitizer); + const context = initContext(null, null, null, null, sanitizer); const handler = new CorePlayerHandler(); const lView = createMockViewData(handler, context); @@ -2208,7 +2528,7 @@ describe('style and class based bindings', () => { it('should automatically destroy existing players when the follow-up binding is not apart of a factory', () => { - const context = initContext(['width'], ['foo', 'bar']); + const context = initContext(null, ['width'], null, ['foo', 'bar']); const handler = new CorePlayerHandler(); const lView = createMockViewData(handler, context); @@ -2400,6 +2720,97 @@ describe('style and class based bindings', () => { expect(otherRenderCaptures[2]).toEqual({type: BindingType.Style, value: {width: '50px'}}); expect(otherRenderCaptures[3]).toEqual({type: BindingType.Class, value: {foo: false}}); }); + + it('should render styling players on both template and directive host bindings', () => { + const players: MockPlayer[] = []; + const styleBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + player.data = value; + players.push(player); + return player; + }; + + const classBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + player.data = value; + players.push(player); + return player; + }; + + const widthFactory1 = bindPlayerFactory(styleBuildFn, '100px'); + const widthFactory2 = bindPlayerFactory(styleBuildFn, '200px'); + const fooFactory1 = bindPlayerFactory(classBuildFn, true); + const fooFactory2 = bindPlayerFactory(classBuildFn, true); + + class MyDir { + static ngDirectiveDef = defineDirective({ + type: MyDir, + selectors: [['', 'my-dir', '']], + factory: () => new MyDir(), + hostBindings: function(rf: RenderFlags, ctx: MyDir, elementIndex: number) { + if (rf & RenderFlags.Create) { + elementStyling(['foo'], ['width'], null, ctx); + } + if (rf & RenderFlags.Update) { + elementStyleProp(0, 0, ctx.widthFactory, null, ctx); + elementClassProp(0, 0, ctx.fooFactory, ctx); + elementStylingApply(0, ctx); + } + } + }); + + widthFactory = widthFactory2; + fooFactory = fooFactory2; + } + + class Comp { + static ngComponentDef = defineComponent({ + type: Comp, + selectors: [['comp']], + directives: [Comp, MyDir], + factory: () => new Comp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: Comp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'div', ['my-dir', '']); + elementStyling(['foo'], ['width']); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementStyleProp(0, 0, ctx.widthFactory); + elementClassProp(0, 0, ctx.fooFactory); + elementStylingApply(0); + } + } + }); + + widthFactory: any = widthFactory1; + fooFactory: any = fooFactory1; + } + + const fixture = new ComponentFixture(Comp); + const component = fixture.component; + fixture.update(); + + expect(players.length).toEqual(2); + const [p1, p2] = players; + players.length = 0; + + expect(p1.data).toEqual({width: '100px'}); + expect(p2.data).toEqual({foo: true}); + + component.fooFactory = null; + component.widthFactory = null; + + fixture.update(); + + expect(players.length).toEqual(2); + const [p3, p4] = players; + + expect(p3.data).toEqual({width: '200px'}); + expect(p4.data).toEqual({foo: true}); + }); }); }); @@ -2412,3 +2823,127 @@ class MockStylingStore implements BindingStore { getValues() { return this._values; } } + +function assertContextOnlyValues(actual: StylingContext, target: any[]) { + assertContext(actual, target as StylingContext, StylingIndex.SingleStylesStartPosition); +} + +function assertContext(actual: StylingContext, target: StylingContext, startIndex: number = 0) { + const errorPrefix = 'Assertion of styling context has failed: \n'; + if (startIndex === 0 && actual.length !== target.length) { + fail( + errorPrefix + + `=> Expected length of context to be ${target.length} (actual = ${actual.length})`); + return; + } + + const log: string[] = []; + for (let i = startIndex; i < actual.length; i++) { + const actualValue = actual[i]; + const targetValue = target[i - startIndex]; + if (isConfigValue(i)) { + const failures = compareLogSummaries( + generateConfigSummary(actualValue as number), + generateConfigSummary(targetValue as number)); + if (failures.length) { + log.push(`i=${i}: Expected config values to match`); + failures.forEach(f => { log.push(' ' + f); }); + } + } else { + let valueIsTheSame: boolean; + let stringError: string|null = null; + let fieldName: string; + switch (i) { + case StylingIndex.PlayerContext: + valueIsTheSame = valueEqualsValue(actualValue, targetValue); + stringError = !valueIsTheSame ? + generateArrayCompareError(actualValue as any[], targetValue as any[], ' ') : + null; + fieldName = 'Player Context'; + break; + case StylingIndex.ElementPosition: + valueIsTheSame = actualValue === targetValue; + stringError = !valueIsTheSame ? + generateElementCompareError(actualValue as Element, targetValue as Element) : + null; + fieldName = 'Element Position'; + break; + case StylingIndex.CachedClassValueOrInitialClassString: + case StylingIndex.CachedStyleValue: + valueIsTheSame = stringMapEqualsStringMap(actualValue, targetValue); + stringError = + !valueIsTheSame ? generateValueCompareError(actualValue, targetValue) : null; + fieldName = 'Cached Style/Class Value'; + break; + default: + valueIsTheSame = valueEqualsValue(actualValue, targetValue); + stringError = + !valueIsTheSame ? generateValueCompareError(actualValue, targetValue) : null; + fieldName = i > StylingIndex.SingleStylesStartPosition ? + `styling value #${Math.floor(i / StylingIndex.Size)}` : + 'config value'; + break; + } + if (!valueIsTheSame) { + log.push(`Error: i=${i}: (${fieldName}) ${stringError}`); + } + } + } + + if (log.length) { + fail(errorPrefix + log.join('\n')); + } +} + +function generateArrayCompareError(a: any[], b: any[], tab: string) { + const values: string[] = []; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + values.push(`${tab}a[${i}] !== b[${i}]`); + } else { + values.push(`${tab}a[${i}] === b[${i}]`); + } + } + return values.length ? '\n' + values.join('\n') : null; +} + +function generateElementCompareError(a: Element, b: Element) { + const aName = a.nodeName.toLowerCase() + a.className.replace(/ /g, '.').trim(); + const bName = b.nodeName.toLowerCase() + b.className.replace(/ /g, '.').trim(); + return `${aName} !== ${bName} (by instance)`; +} + +function generateValueCompareError(a: any, b: any) { + return `${JSON.stringify(a)} !== ${JSON.stringify(b)}`; +} + +function isConfigValue(index: number) { + if (index == StylingIndex.MasterFlagPosition) return true; + if (index >= StylingIndex.SingleStylesStartPosition) { + return (index - StylingIndex.SingleStylesStartPosition) % StylingIndex.Size === 0; + } +} + +function valueEqualsValue(a: any, b: any): boolean { + if (Array.isArray(a)) { + if (!Array.isArray(b)) return false; + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; + } + return a === b; +} + +function stringMapEqualsStringMap(a: any, b: any): boolean { + if (a && b) { + const k1 = Object.keys(a); + const k2 = Object.keys(b); + if (k1.length === k2.length) { + return k1.every(key => { return a[key] === b[key]; }); + } + return false; + } + return a == b; +} diff --git a/packages/core/test/render3/styling/mock_player.ts b/packages/core/test/render3/styling/mock_player.ts index c78ceed635..1c69bfbf15 100644 --- a/packages/core/test/render3/styling/mock_player.ts +++ b/packages/core/test/render3/styling/mock_player.ts @@ -10,6 +10,7 @@ import {PlayState, Player} from '../../../src/render3/interfaces/player'; export class MockPlayer implements Player { parent: Player|null = null; + data: any; log: string[] = []; state: PlayState = PlayState.Pending; private _listeners: {[state: string]: (() => any)[]} = {};