diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json index ecbc598ea1..db56e0cdf5 100755 --- a/aio/scripts/_payload-limits.json +++ b/aio/scripts/_payload-limits.json @@ -16,8 +16,8 @@ "uncompressed": { "runtime-es5": 3042, "runtime-es2015": 3048, - "main-es5": 492593, - "main-es2015": 438296, + "main-es5": 493330, + "main-es2015": 435296, "polyfills-es5": 131024, "polyfills-es2015": 52433 } @@ -28,7 +28,7 @@ "uncompressed": { "runtime-es5": 2932, "runtime-es2015": 2938, - "main-es5": 552068, + "main-es5": 550854, "main-es2015": 493320, "polyfills-es5": 131024, "polyfills-es2015": 52433 diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index ec9389789f..5d0ab0db8c 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime": 1440, - "main": 13604, + "main": 13264, "polyfills": 45340 } } diff --git a/modules/benchmarks/src/largetable/ng2/table.ts b/modules/benchmarks/src/largetable/ng2/table.ts index 55028bf29a..883d3af5c7 100644 --- a/modules/benchmarks/src/largetable/ng2/table.ts +++ b/modules/benchmarks/src/largetable/ng2/table.ts @@ -36,7 +36,7 @@ export class TableComponent { @NgModule({imports: [BrowserModule], bootstrap: [TableComponent], declarations: [TableComponent]}) export class AppModule { constructor(sanitizer: DomSanitizer) { - trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('white'); + trustedEmptyColor = sanitizer.bypassSecurityTrustStyle(''); trustedGreyColor = sanitizer.bypassSecurityTrustStyle('grey'); } } diff --git a/modules/benchmarks/src/tree/render3_function/index.ts b/modules/benchmarks/src/tree/render3_function/index.ts index 77296b8512..a55c79a48c 100644 --- a/modules/benchmarks/src/tree/render3_function/index.ts +++ b/modules/benchmarks/src/tree/render3_function/index.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ɵRenderFlags, ɵrenderComponent as renderComponent, ɵɵadvance, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵstyleProp, ɵɵtext, ɵɵtextInterpolate1} from '@angular/core'; +import {ɵRenderFlags, ɵrenderComponent as renderComponent, ɵɵadvance, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵstyleProp, ɵɵstyling, ɵɵstylingApply, ɵɵtext, ɵɵtextInterpolate1} from '@angular/core'; import {bindAction, profile} from '../../util'; import {createDom, destroyDom, detectChanges} from '../render3/tree'; @@ -39,6 +39,7 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) { ɵɵelementStart(0, 'tree'); { ɵɵelementStart(1, 'span'); + ɵɵstyling(); { ɵɵtext(2); } ɵɵelementEnd(); ɵɵcontainer(3); @@ -49,6 +50,7 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) { if (rf & ɵRenderFlags.Update) { ɵɵadvance(1); ɵɵstyleProp('background-color', ctx.depth % 2 ? '' : 'grey'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵtextInterpolate1(' ', ctx.value, ' '); ɵɵcontainerRefreshStart(3); diff --git a/packages/common/src/directives/ng_class.ts b/packages/common/src/directives/ng_class.ts index f2921d17b2..238a1a2c38 100644 --- a/packages/common/src/directives/ng_class.ts +++ b/packages/common/src/directives/ng_class.ts @@ -5,7 +5,7 @@ * 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 {Directive, DoCheck, Input, ɵRenderFlags, ɵɵallocHostVars, ɵɵclassMap, ɵɵdefineDirective} from '@angular/core'; +import {Directive, DoCheck, Input, ɵRenderFlags, ɵɵallocHostVars, ɵɵclassMap, ɵɵdefineDirective, ɵɵstyling, ɵɵstylingApply} from '@angular/core'; import {NgClassImpl, NgClassImplProvider} from './ng_class_impl'; @@ -35,9 +35,11 @@ export const ngClassDirectiveDef__POST_R3__ = ɵɵdefineDirective({ hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) { if (rf & ɵRenderFlags.Create) { ɵɵallocHostVars(1); + ɵɵstyling(); } if (rf & ɵRenderFlags.Update) { ɵɵclassMap(ctx.getValue()); + ɵɵstylingApply(); } } }); diff --git a/packages/common/src/directives/ng_style.ts b/packages/common/src/directives/ng_style.ts index ef70358d88..710b73afb9 100644 --- a/packages/common/src/directives/ng_style.ts +++ b/packages/common/src/directives/ng_style.ts @@ -5,7 +5,7 @@ * 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 {Directive, DoCheck, Input, ɵRenderFlags, ɵɵdefineDirective, ɵɵstyleMap} from '@angular/core'; +import {Directive, DoCheck, Input, ɵRenderFlags, ɵɵdefineDirective, ɵɵstyleMap, ɵɵstyling, ɵɵstylingApply} from '@angular/core'; import {NgStyleImpl, NgStyleImplProvider} from './ng_style_impl'; @@ -34,8 +34,12 @@ export const ngStyleDirectiveDef__POST_R3__ = ɵɵdefineDirective({ type: function() {} as any, selectors: null as any, hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) { + if (rf & ɵRenderFlags.Create) { + ɵɵstyling(); + } if (rf & ɵRenderFlags.Update) { ɵɵstyleMap(ctx.getValue()); + ɵɵstylingApply(); } } }); 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 fc6dea5f75..ebec1d3ac2 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -483,11 +483,14 @@ describe('compiler compliance', () => { vars: 2, template: function MyComponent_Template(rf,ctx){ if (rf & 1) { - $r3$.ɵɵelement(0, "div"); + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵstyleProp("background-color", ctx.color); $r3$.ɵɵclassProp("error", ctx.error); + $r3$.ɵɵstylingApply(); } }, encapsulation: 2 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 78c48aca42..fa23dff617 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 @@ -376,11 +376,14 @@ describe('compiler compliance: styling', () => { const template = ` template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelement(0, "div"); + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap($ctx$.myStyleExp); + $r3$.ɵɵstylingApply(); } } `; @@ -439,10 +442,13 @@ describe('compiler compliance: styling', () => { vars: 2, template: function MyComponentWithInterpolation_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelement(0, "div"); + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵclassMapInterpolate1("foo foo-", $ctx$.fooId, ""); + $r3$.ɵɵstylingApply(); } } … @@ -450,10 +456,13 @@ describe('compiler compliance: styling', () => { vars: 3, template: function MyComponentWithMuchosInterpolation_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelement(0, "div"); + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵclassMapInterpolate2("foo foo-", $ctx$.fooId, "-", $ctx$.fooUsername, ""); + $r3$.ɵɵstylingApply(); } } … @@ -461,10 +470,13 @@ describe('compiler compliance: styling', () => { vars: 1, template: function MyComponentWithoutInterpolation_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelement(0, "div"); + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵclassMap($ctx$.exp); + $r3$.ɵɵstylingApply(); } } `; @@ -510,13 +522,16 @@ describe('compiler compliance: styling', () => { vars: 4, template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelement(0, "div", $_c0$); + $r3$.ɵɵelementStart(0, "div", $_c0$); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap($ctx$.myStyleExp); $r3$.ɵɵstyleProp("width", $ctx$.myWidth); $r3$.ɵɵstyleProp("height", $ctx$.myHeight); + $r3$.ɵɵstylingApply(); $r3$.ɵɵattribute("style", "border-width: 10px", $r3$.ɵɵsanitizeStyle); } }, @@ -557,11 +572,14 @@ describe('compiler compliance: styling', () => { vars: 1, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵelement(0, "div"); + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleProp("background-image", ctx.myImage); + $r3$.ɵɵstylingApply(); } }, encapsulation: 2 @@ -594,10 +612,13 @@ describe('compiler compliance: styling', () => { const template = ` template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵelement(0, "div"); + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵstyleProp("font-size", 12, "px"); + $r3$.ɵɵstylingApply(); } } `; @@ -655,10 +676,13 @@ describe('compiler compliance: styling', () => { const template = ` template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelement(0, "div"); + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵclassMap($ctx$.myClassExp); + $r3$.ɵɵstylingApply(); } } `; @@ -704,12 +728,15 @@ describe('compiler compliance: styling', () => { vars: 4, template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelement(0, "div", $e0_attrs$); + $r3$.ɵɵelementStart(0, "div", $e0_attrs$); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵclassMap($ctx$.myClassExp); $r3$.ɵɵclassProp("apple", $ctx$.yesToApple); $r3$.ɵɵclassProp("orange", $ctx$.yesToOrange); + $r3$.ɵɵstylingApply(); $r3$.ɵɵattribute("class", "banana"); } }, @@ -817,12 +844,15 @@ describe('compiler compliance: styling', () => { const template = ` template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelement(0, "div"); + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap($ctx$.myStyleExp); $r3$.ɵɵclassMap($ctx$.myClassExp); + $r3$.ɵɵstylingApply(); } } `; @@ -857,6 +887,7 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); $r3$.ɵɵpipe(1, "stylePipe"); $r3$.ɵɵpipe(2, "classPipe"); $r3$.ɵɵelementEnd(); @@ -865,6 +896,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap($r3$.ɵɵpipeBind1(1, 2, $ctx$.myStyleExp)); $r3$.ɵɵclassMap($r3$.ɵɵpipeBind1(2, 4, $ctx$.myClassExp)); + $r3$.ɵɵstylingApply(); } } `; @@ -907,6 +939,7 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); $r3$.ɵɵpipe(1, "pipe"); $r3$.ɵɵpipe(2, "pipe"); $r3$.ɵɵpipe(3, "pipe"); @@ -921,8 +954,9 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵstyleProp("bar", $r3$.ɵɵpipeBind2(2, 9, $ctx$.barExp, 3000)); $r3$.ɵɵstyleProp("baz", $r3$.ɵɵpipeBind2(3, 12, $ctx$.bazExp, 4000)); $r3$.ɵɵclassProp("foo", $r3$.ɵɵpipeBind2(4, 15, $ctx$.fooExp, 2000)); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(5); - $r3$.ɵɵtextInterpolate1(" ", $ctx$.item, ""); + $r3$.ɵɵtextInterpolate1(" ", $ctx$.item, ""); } } `; @@ -965,12 +999,16 @@ describe('compiler compliance: styling', () => { … if (rf & 2) { $r3$.ɵɵstyleProp("width", $ctx$.w1); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵstyleProp("height", $ctx$.h1); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵclassProp("active", $ctx$.a1); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵclassProp("removed", $ctx$.r1); + $r3$.ɵɵstylingApply(); } } `; @@ -1020,6 +1058,7 @@ describe('compiler compliance: styling', () => { if (rf & 1) { $r3$.ɵɵallocHostVars(4); $r3$.ɵɵelementHostAttrs($e0_attrs$); + $r3$.ɵɵstyling(); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); @@ -1027,6 +1066,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵclassMap(ctx.myClass); $r3$.ɵɵstyleProp("color", ctx.myColorProp); $r3$.ɵɵclassProp("foo", ctx.myFooClass); + $r3$.ɵɵstylingApply(); } }, consts: 0, @@ -1078,6 +1118,7 @@ describe('compiler compliance: styling', () => { hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { if (rf & 1) { $r3$.ɵɵallocHostVars(6); + $r3$.ɵɵstyling(); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); @@ -1087,6 +1128,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵstyleProp("width", ctx.myWidthProp); $r3$.ɵɵclassProp("bar", ctx.myBarClass); $r3$.ɵɵclassProp("foo", ctx.myFooClass); + $r3$.ɵɵstylingApply(); } }, consts: 0, @@ -1137,7 +1179,9 @@ describe('compiler compliance: styling', () => { const template = ` function MyComponent_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵelement(0, "div"); + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵstyling(); + $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); @@ -1145,6 +1189,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵclassMap(ctx.myClassExp); $r3$.ɵɵstyleProp("height", ctx.myHeightExp); $r3$.ɵɵclassProp("bar", ctx.myBarClassExp); + $r3$.ɵɵstylingApply(); } }, `; @@ -1153,6 +1198,7 @@ describe('compiler compliance: styling', () => { hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { if (rf & 1) { $r3$.ɵɵallocHostVars(4); + $r3$.ɵɵstyling(); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); @@ -1160,6 +1206,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵclassMap(ctx.myClassExp); $r3$.ɵɵstyleProp("width", ctx.myWidthExp); $r3$.ɵɵclassProp("foo", ctx.myFooClassExp); + $r3$.ɵɵstylingApply(); } }, `; @@ -1219,29 +1266,35 @@ describe('compiler compliance: styling', () => { function ClassDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { $r3$.ɵɵallocHostVars(1); + $r3$.ɵɵstyling(); } if (rf & 2) { $r3$.ɵɵclassMap(ctx.myClassMap); + $r3$.ɵɵstylingApply(); } } … function WidthDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { $r3$.ɵɵallocHostVars(2); + $r3$.ɵɵstyling(); } if (rf & 2) { $r3$.ɵɵstyleProp("width", ctx.myWidth); $r3$.ɵɵclassProp("foo", ctx.myFooClass); + $r3$.ɵɵstylingApply(); } } … function HeightDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { $r3$.ɵɵallocHostVars(2); + $r3$.ɵɵstyling(); } if (rf & 2) { $r3$.ɵɵstyleProp("height", ctx.myHeight); $r3$.ɵɵclassProp("bar", ctx.myBarClass); + $r3$.ɵɵstylingApply(); } } … @@ -1283,24 +1336,34 @@ describe('compiler compliance: styling', () => { … if (rf & 2) { $r3$.ɵɵclassMapInterpolateV(["a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i", ctx.nine, "j"]); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵclassMapInterpolate8("a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵclassMapInterpolate7("a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵclassMapInterpolate6("a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵclassMapInterpolate5("a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵclassMapInterpolate4("a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵclassMapInterpolate3("a", ctx.one, "b", ctx.two, "c", ctx.three, "d"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵclassMapInterpolate2("a", ctx.one, "b", ctx.two, "c"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵclassMapInterpolate1("a", ctx.one, "b"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵclassMap(ctx.one); + $r3$.ɵɵstylingApply(); } … `; @@ -1375,24 +1438,34 @@ describe('compiler compliance: styling', () => { … if (rf & 2) { $r3$.ɵɵstylePropInterpolateV("color", ["a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i", ctx.nine, "j"]); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵstylePropInterpolate8("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵstylePropInterpolate7("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵstylePropInterpolate6("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵstylePropInterpolate5("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵstylePropInterpolate4("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵstylePropInterpolate3("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵstylePropInterpolate2("color", "a", ctx.one, "b", ctx.two, "c"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵstylePropInterpolate1("color", "a", ctx.one, "b"); + $r3$.ɵɵstylingApply(); $r3$.ɵɵadvance(1); $r3$.ɵɵstyleProp("color", ctx.one); + $r3$.ɵɵstylingApply(); } … `; @@ -1423,6 +1496,7 @@ describe('compiler compliance: styling', () => { … if (rf & 2) { $r3$.ɵɵstylePropInterpolate2("width", "a", ctx.one, "b", ctx.two, "c", "px"); + $r3$.ɵɵstylingApply(); } … `; @@ -1453,6 +1527,7 @@ describe('compiler compliance: styling', () => { … if (rf & 2) { $r3$.ɵɵstylePropInterpolate2("width", "a", ctx.one, "b", ctx.two, "c"); + $r3$.ɵɵstylingApply(); } … `; @@ -1508,12 +1583,14 @@ describe('compiler compliance: styling', () => { if (rf & 1) { $r3$.ɵɵallocHostVars(4); $r3$.ɵɵelementHostAttrs($_c0$); + $r3$.ɵɵstyling(); } if (rf & 2) { $r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title); $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap(ctx.myStyle); $r3$.ɵɵclassMap(ctx.myClass); + $r3$.ɵɵstylingApply(); } } `; @@ -1550,11 +1627,13 @@ describe('compiler compliance: styling', () => { hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { $r3$.ɵɵallocHostVars(4); + $r3$.ɵɵstyling(); } if (rf & 2) { $r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title); $r3$.ɵɵstyleProp("width", ctx.myWidth); $r3$.ɵɵclassProp("foo", ctx.myFooClass); + $r3$.ɵɵstylingApply(); } } `; @@ -1590,6 +1669,7 @@ describe('compiler compliance: styling', () => { if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleProp("background-image", ctx.bgExp); + $r3$.ɵɵstylingApply(); } … } @@ -1625,6 +1705,7 @@ describe('compiler compliance: styling', () => { if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap(ctx.mapExp); + $r3$.ɵɵstylingApply(); } … } @@ -1661,6 +1742,7 @@ describe('compiler compliance: styling', () => { if (rf & 2) { $r3$.ɵɵclassMap(ctx.mapExp); $r3$.ɵɵclassProp("name", ctx.nameExp); + $r3$.ɵɵstylingApply(); } … } @@ -1723,6 +1805,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵpureFunction2(6, _c1, ctx._animValue, $r3$.ɵɵpureFunction2(3, _c0, ctx._animParam1, ctx._animParam2))); $r3$.ɵɵclassProp("foo", ctx.foo); + $r3$.ɵɵstylingApply(); } } `; diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index c94c751234..2fabaee379 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -1867,11 +1867,13 @@ runInEachFileSystem(os => { i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); }); i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody); i0.ɵɵlistener("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); }); + i0.ɵɵstyling(); } if (rf & 2) { i0.ɵɵhostProperty("prop", ctx.bar); i0.ɵɵattribute("hello", ctx.foo); i0.ɵɵclassProp("someclass", ctx.someClass); + i0.ɵɵstylingApply(); } } `; diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 6b648511aa..5dc424cc6d 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -69,6 +69,8 @@ export class Identifiers { static elementContainer: o.ExternalReference = {name: 'ɵɵelementContainer', moduleName: CORE}; + static styling: o.ExternalReference = {name: 'ɵɵstyling', moduleName: CORE}; + static styleMap: o.ExternalReference = {name: 'ɵɵstyleMap', moduleName: CORE}; static classMap: o.ExternalReference = {name: 'ɵɵclassMap', moduleName: CORE}; @@ -113,6 +115,8 @@ export class Identifiers { static stylePropInterpolateV: o.ExternalReference = {name: 'ɵɵstylePropInterpolateV', moduleName: CORE}; + static stylingApply: o.ExternalReference = {name: 'ɵɵstylingApply', moduleName: CORE}; + static styleSanitizer: o.ExternalReference = {name: 'ɵɵstyleSanitizer', moduleName: CORE}; static elementHostAttrs: o.ExternalReference = {name: 'ɵɵelementHostAttrs', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 4f0c5ca0e3..eb4245b661 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -713,6 +713,16 @@ function createHostBindingsFunction( } if (styleBuilder.hasBindings) { + // 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 `styling`. + const stylingInstruction = styleBuilder.buildStylingInstruction(null, constantPool); + if (stylingInstruction) { + createStatements.push(createStylingStmt(stylingInstruction, 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. diff --git a/packages/compiler/src/render3/view/styling_builder.ts b/packages/compiler/src/render3/view/styling_builder.ts index 78c4afbe4c..ccf82f8b3b 100644 --- a/packages/compiler/src/render3/view/styling_builder.ts +++ b/packages/compiler/src/render3/view/styling_builder.ts @@ -7,7 +7,7 @@ */ import {ConstantPool} from '../../constant_pool'; import {AttributeMarker} from '../../core'; -import {AST, ASTWithSource, BindingPipe, BindingType, Interpolation} from '../../expression_parser/ast'; +import {AST, BindingType, Interpolation} from '../../expression_parser/ast'; import * as o from '../../output/output_ast'; import {ParseSourceSpan} from '../../parse_util'; import {isEmptyExpression} from '../../template_parser/template_parser'; @@ -68,6 +68,7 @@ interface BoundStylingEntry { * classMap(...) * styleProp(...) * classProp(...) + * stylingApply(...) * } * * The creation/update methods within the builder class produce these instructions. @@ -80,7 +81,6 @@ export class StylingBuilder { * (i.e. `[style]`, `[class]`, `[style.prop]` or `[class.name]`) */ public hasBindings = false; - public hasBindingsWithPipes = false; /** the input for [class] (if it exists) */ private _classMapInput: BoundStylingEntry|null = null; @@ -187,7 +187,6 @@ export class StylingBuilder { } this._lastStylingInput = entry; this._firstStylingInput = this._firstStylingInput || entry; - this._checkForPipes(value); this.hasBindings = true; return entry; } @@ -208,17 +207,10 @@ export class StylingBuilder { } this._lastStylingInput = entry; this._firstStylingInput = this._firstStylingInput || entry; - this._checkForPipes(value); this.hasBindings = true; return entry; } - private _checkForPipes(value: AST) { - if ((value instanceof ASTWithSource) && (value.ast instanceof BindingPipe)) { - this.hasBindingsWithPipes = true; - } - } - /** * Registers the element's static style string value to the builder. * @@ -292,6 +284,25 @@ export class StylingBuilder { return null; } + /** + * Builds an instruction with all the expressions and parameters for `styling`. + * + * The instruction generation code below is used for producing the AOT statement code which is + * responsible for registering style/class bindings to an element. + */ + buildStylingInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool): + StylingInstruction|null { + if (this.hasBindings) { + return { + sourceSpan, + allocateBindingSlots: 0, + reference: R3.styling, + params: () => [], + }; + } + return null; + } + /** * Builds an instruction with all the expressions and parameters for `classMap`. * @@ -411,6 +422,15 @@ export class StylingBuilder { return []; } + private _buildApplyFn(): StylingInstruction { + return { + sourceSpan: this._lastStylingInput ? this._lastStylingInput.sourceSpan : null, + reference: R3.stylingApply, + allocateBindingSlots: 0, + params: () => { return []; } + }; + } + private _buildSanitizerFn(): StylingInstruction { return { sourceSpan: this._firstStylingInput ? this._firstStylingInput.sourceSpan : null, @@ -440,6 +460,7 @@ export class StylingBuilder { } instructions.push(...this._buildStyleInputs(valueConverter)); instructions.push(...this._buildClassInputs(valueConverter)); + instructions.push(this._buildApplyFn()); } return instructions; } diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index e00bdd9c2f..45b888ae7e 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -623,10 +623,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const hasChildren = (!isI18nRootElement && this.i18n) ? !hasTextChildrenOnly(element.children) : element.children.length > 0; - const createSelfClosingInstruction = !stylingBuilder.hasBindingsWithPipes && + const createSelfClosingInstruction = !stylingBuilder.hasBindings && element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren; - const createSelfClosingI18nInstruction = - !createSelfClosingInstruction && hasTextChildrenOnly(element.children); + + const createSelfClosingI18nInstruction = !createSelfClosingInstruction && + !stylingBuilder.hasBindings && hasTextChildrenOnly(element.children); if (createSelfClosingInstruction) { this.creationInstruction( @@ -680,6 +681,16 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } } + // The style bindings code is placed into two distinct blocks within the template function AOT + // code: creation and update. The creation code contains the `styling` instructions + // which will apply the collected binding values to the element. `styling` is + // designed to run inside of `elementStart` and `elementEnd`. The update instructions + // (things like `styleProp`, `classProp`, etc..) are applied later on in this + // file + this.processStylingInstruction( + elementIndex, + stylingBuilder.buildStylingInstruction(element.sourceSpan, this.constantPool), true); + // Generate Listeners (outputs) element.outputs.forEach((outputAst: t.BoundEvent) => { this.creationInstruction( diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 81ea351910..32704199db 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -120,6 +120,7 @@ export { ɵɵelementContainerStart, ɵɵelementContainerEnd, ɵɵelementContainer, + ɵɵstyling, ɵɵstyleMap, ɵɵstyleSanitizer, ɵɵclassMap, @@ -142,6 +143,7 @@ export { ɵɵstylePropInterpolate7, ɵɵstylePropInterpolate8, ɵɵstylePropInterpolateV, + ɵɵstylingApply, ɵɵclassProp, ɵɵelementHostAttrs, diff --git a/packages/core/src/debug/debug_node.ts b/packages/core/src/debug/debug_node.ts index b1e6102f59..31c2051971 100644 --- a/packages/core/src/debug/debug_node.ts +++ b/packages/core/src/debug/debug_node.ts @@ -12,9 +12,10 @@ import {CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from '../render3/interfaces import {TElementNode, TNode, TNodeFlags, TNodeType} from '../render3/interfaces/node'; import {isComponentHost, isLContainer} from '../render3/interfaces/type_checks'; import {LView, PARENT, TData, TVIEW, T_HOST} from '../render3/interfaces/view'; -import {StylingMapArray, TStylingContext} from '../render3/styling_next/interfaces'; +import {TStylingContext} from '../render3/styling_next/interfaces'; +import {stylingMapToStringMap} from '../render3/styling_next/map_based_bindings'; import {NodeStylingDebug} from '../render3/styling_next/styling_debug'; -import {isStylingContext, stylingMapToStringMap} from '../render3/styling_next/util'; +import {isStylingContext} from '../render3/styling_next/util'; import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, getLocalRefs, isBrowserEvents, loadLContext} from '../render3/util/discovery_utils'; import {INTERPOLATION_DELIMITER, renderStringify} from '../render3/util/misc_utils'; import {findComponentView} from '../render3/util/view_traversal_utils'; @@ -430,11 +431,11 @@ function _getStylingDebugInfo(element: any, isClassBased: boolean) { if (isClassBased) { return isStylingContext(tNode.classes) ? new NodeStylingDebug(tNode.classes as TStylingContext, lView, true).values : - stylingMapToStringMap(tNode.classes as StylingMapArray | null); + stylingMapToStringMap(tNode.classes); } else { return isStylingContext(tNode.styles) ? new NodeStylingDebug(tNode.styles as TStylingContext, lView, false).values : - stylingMapToStringMap(tNode.styles as StylingMapArray | null); + stylingMapToStringMap(tNode.styles); } } diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index bd1e0306a7..cb5a26d071 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -23,7 +23,7 @@ import {TElementNode, TNode, TNodeType} from './interfaces/node'; import {PlayerHandler} from './interfaces/player'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; import {CONTEXT, HEADER_OFFSET, LView, LViewFlags, RootContext, RootContextFlags, TVIEW} from './interfaces/view'; -import {getPreviousOrParentTNode, incrementActiveDirectiveId, resetComponentState, selectView, setActiveHostElement} from './state'; +import {getPreviousOrParentTNode, resetComponentState, selectView, setActiveHostElement} from './state'; import {publishDefaultGlobalUtils} from './util/global_utils'; import {defaultScheduler, stringifyForError} from './util/misc_utils'; import {getRootContext} from './util/view_traversal_utils'; @@ -217,7 +217,6 @@ export function createRootComponent( if (tView.firstTemplatePass && componentDef.hostBindings) { const elementIndex = rootTNode.index - HEADER_OFFSET; setActiveHostElement(elementIndex); - incrementActiveDirectiveId(); const expando = tView.expandoInstructions !; invokeHostBindingsInCreationMode( diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts index fdee1c9a2d..8c329b8cc0 100644 --- a/packages/core/src/render3/features/inherit_definition_feature.ts +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -11,6 +11,7 @@ import {fillProperties} from '../../util/property'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; import {ComponentDef, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, HostBindingsFunction, RenderFlags, ViewQueriesFunction} from '../interfaces/definition'; import {isComponentDef} from '../interfaces/type_checks'; +import {adjustActiveDirectiveSuperClassDepthPosition} from '../state'; import {ɵɵNgOnChangesFeature} from './ng_onchanges_feature'; @@ -176,8 +177,24 @@ function inheritHostBindings( // to ensure we don't inherit it twice. if (superHostBindings !== prevHostBindings) { if (prevHostBindings) { + // because inheritance is unknown during compile time, the runtime code + // needs to be informed of the super-class depth so that instruction code + // can distinguish one host bindings function from another. The reason why + // relying on the directive uniqueId exclusively is not enough is because the + // uniqueId value and the directive instance stay the same between hostBindings + // calls throughout the directive inheritance chain. This means that without + // a super-class depth value, there is no way to know whether a parent or + // sub-class host bindings function is currently being executed. definition.hostBindings = (rf: RenderFlags, ctx: any, elementIndex: number) => { - superHostBindings(rf, ctx, elementIndex); + // The reason why we increment first and then decrement is so that parent + // hostBindings calls have a higher id value compared to sub-class hostBindings + // calls (this way the leaf directive is always at a super-class depth of 0). + adjustActiveDirectiveSuperClassDepthPosition(1); + try { + superHostBindings(rf, ctx, elementIndex); + } finally { + adjustActiveDirectiveSuperClassDepthPosition(-1); + } prevHostBindings(rf, ctx, elementIndex); }; } else { diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index d8aa32390d..1e01e3d2e5 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -114,6 +114,8 @@ export { ɵɵstylePropInterpolateV, ɵɵstyleSanitizer, + ɵɵstyling, + ɵɵstylingApply, ɵɵtemplate, ɵɵtext, diff --git a/packages/core/src/render3/instructions/advance.ts b/packages/core/src/render3/instructions/advance.ts index 86707b3781..293764cbb3 100644 --- a/packages/core/src/render3/instructions/advance.ts +++ b/packages/core/src/render3/instructions/advance.ts @@ -8,10 +8,7 @@ import {assertDataInRange, assertGreaterThan} from '../../util/assert'; import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks'; import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, TVIEW} from '../interfaces/view'; -import {ActiveElementFlags, executeElementExitFn, getCheckNoChangesMode, getLView, getSelectedIndex, hasActiveElementFlag, setSelectedIndex} from '../state'; -import {resetStylingState} from '../styling_next/state'; - - +import {getCheckNoChangesMode, getLView, getSelectedIndex, setSelectedIndex} from '../state'; /** * Advances to an element for later binding instructions. @@ -54,10 +51,6 @@ export function selectIndexInternal(lView: LView, index: number, checkNoChangesM ngDevMode && assertGreaterThan(index, -1, 'Invalid index'); ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET); - if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { - executeElementExitFn(); - } - // Flush the initial hooks for elements in the view that have been added up to this point. // PERF WARNING: do NOT extract this to a separate function without running benchmarks if (!checkNoChangesMode) { @@ -76,10 +69,6 @@ export function selectIndexInternal(lView: LView, index: number, checkNoChangesM } } - if (hasActiveElementFlag(ActiveElementFlags.ResetStylesOnExit)) { - resetStylingState(); - } - // We must set the selected index *after* running the hooks, because hooks may have side-effects // that cause other template functions to run, thus updating the selected index, which is global // state. If we run `setSelectedIndex` *before* we run the hooks, in some cases the selected index diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 980172e2c4..14b705dd17 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -138,11 +138,11 @@ export function ɵɵelementEnd(): void { } } - if (hasClassInput(tNode)) { + if (hasClassInput(tNode) && tNode.classes) { setDirectiveStylingInput(tNode.classes, lView, tNode.inputs !['class']); } - if (hasStyleInput(tNode)) { + if (hasStyleInput(tNode) && tNode.styles) { setDirectiveStylingInput(tNode.styles, lView, tNode.inputs !['style']); } } @@ -236,12 +236,11 @@ export function ɵɵelementHostAttrs(attrs: TAttributes) { } function setDirectiveStylingInput( - context: TStylingContext | StylingMapArray | null, lView: LView, - stylingInputs: (string | number)[]) { + context: TStylingContext | StylingMapArray, lView: LView, stylingInputs: (string | number)[]) { // older versions of Angular treat the input as `null` in the // event that the value does not exist at all. For this reason // we can't have a styling value be an empty string. - const value = (context && getInitialStylingValue(context)) || null; + const value = getInitialStylingValue(context) || null; // Ivy does an extra `[class]` write with a falsy value since the value // is applied during creation mode. This is a deviation from VE and should diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index fd824b542e..05da849d7b 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -29,9 +29,8 @@ import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRoo import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TData, TVIEW, TView, T_HOST} from '../interfaces/view'; import {assertNodeOfPossibleTypes} from '../node_assert'; import {isNodeMatchingSelectorList} from '../node_selector_matcher'; -import {ActiveElementFlags, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getLView, getPreviousOrParentTNode, getSelectedIndex, hasActiveElementFlag, incrementActiveDirectiveId, namespaceHTMLInternal, selectView, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state'; +import {getBindingsEnabled, getCheckNoChangesMode, getIsParent, getLView, getPreviousOrParentTNode, getSelectedIndex, incrementActiveDirectiveId, namespaceHTMLInternal, selectView, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state'; import {renderStylingMap} from '../styling_next/bindings'; -import {resetStylingState} from '../styling_next/state'; import {NO_CHANGE} from '../tokens'; import {ANIMATION_PROP_PREFIX, isAnimationProp} from '../util/attrs_utils'; import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils'; @@ -87,18 +86,16 @@ export function setHostBindings(tView: TView, viewData: LView): void { } else { // If it's not a number, it's a host binding function that needs to be executed. if (instruction !== null) { + viewData[BINDING_INDEX] = bindingRootIndex; + const hostCtx = unwrapRNode(viewData[currentDirectiveIndex]); + instruction(RenderFlags.Update, hostCtx, currentElementIndex); + // Each directive gets a uniqueId value that is the same for both // create and update calls when the hostBindings function is called. The // directive uniqueId is not set anywhere--it is just incremented between // each hostBindings call and is useful for helping instruction code // uniquely determine which directive is currently active when executed. - // It is important that this be called first before the actual instructions - // are run because this way the first directive ID value is not zero. incrementActiveDirectiveId(); - - viewData[BINDING_INDEX] = bindingRootIndex; - const hostCtx = unwrapRNode(viewData[currentDirectiveIndex]); - instruction(RenderFlags.Update, hostCtx, currentElementIndex); } currentDirectiveIndex++; } @@ -506,12 +503,6 @@ function executeTemplate( } templateFn(rf, context); } finally { - if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { - executeElementExitFn(); - } - if (hasActiveElementFlag(ActiveElementFlags.ResetStylesOnExit)) { - resetStylingState(); - } setSelectedIndex(prevSelectedIndex); } } @@ -1095,10 +1086,14 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod const def = tView.data[i] as DirectiveDef; const directive = viewData[i]; if (def.hostBindings) { - // It is important that this be called first before the actual instructions - // are run because this way the first directive ID value is not zero. - incrementActiveDirectiveId(); invokeHostBindingsInCreationMode(def, expando, directive, tNode, firstTemplatePass); + + // Each directive gets a uniqueId value that is the same for both + // create and update calls when the hostBindings function is called. The + // directive uniqueId is not set anywhere--it is just incremented between + // each hostBindings call and is useful for helping instruction code + // uniquely determine which directive is currently active when executed. + incrementActiveDirectiveId(); } else if (firstTemplatePass) { expando.push(null); } diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 8f60f5a4d8..0f7b88f3f9 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -116,6 +116,7 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵclassMapInterpolate7': r3.ɵɵclassMapInterpolate7, 'ɵɵclassMapInterpolate8': r3.ɵɵclassMapInterpolate8, 'ɵɵclassMapInterpolateV': r3.ɵɵclassMapInterpolateV, + 'ɵɵstyling': r3.ɵɵstyling, 'ɵɵstyleMap': r3.ɵɵstyleMap, 'ɵɵstyleProp': r3.ɵɵstyleProp, 'ɵɵstylePropInterpolate1': r3.ɵɵstylePropInterpolate1, @@ -128,6 +129,7 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵstylePropInterpolate8': r3.ɵɵstylePropInterpolate8, 'ɵɵstylePropInterpolateV': r3.ɵɵstylePropInterpolateV, 'ɵɵstyleSanitizer': r3.ɵɵstyleSanitizer, + 'ɵɵstylingApply': r3.ɵɵstylingApply, 'ɵɵclassProp': r3.ɵɵclassProp, 'ɵɵselect': r3.ɵɵselect, 'ɵɵadvance': r3.ɵɵadvance, diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index 0699a1e42a..0fbf4234ac 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -13,7 +13,7 @@ import {assertLViewOrUndefined} from './assert'; import {ComponentDef, DirectiveDef} from './interfaces/definition'; import {TElementNode, TNode, TViewNode} from './interfaces/node'; import {CONTEXT, DECLARATION_VIEW, LView, OpaqueViewState, TVIEW} from './interfaces/view'; -import {resetStylingState} from './styling_next/state'; +import {resetAllStylingState, resetStylingState} from './styling_next/state'; /** @@ -129,38 +129,31 @@ export function getLView(): LView { * The reason why this value is `1` instead of `0` is because the `0` * value is reserved for the template. */ -let activeDirectiveId = 0; +const MIN_DIRECTIVE_ID = 1; + +let activeDirectiveId = MIN_DIRECTIVE_ID; /** - * Flags used for an active element during change detection. + * Position depth (with respect from leaf to root) in a directive sub-class inheritance chain. + */ +let activeDirectiveSuperClassDepthPosition = 0; + +/** + * Total count of how many directives are a part of an inheritance chain. * - * These flags are used within other instructions to inform cleanup or - * exit operations to run when an element is being processed. + * When directives are sub-classed (extended) from one to another, Angular + * needs to keep track of exactly how many were encountered so it can accurately + * generate the next directive id (once the next directive id is visited). + * Normally the next directive id just a single incremented value from the + * previous one, however, if the previous directive is a part of an inheritance + * chain (a series of sub-classed directives) then the incremented value must + * also take into account the total amount of sub-classed values. * - * Note that these flags are reset each time an element changes (whether it - * happens when `advance()` is run or when change detection exits out of a template - * function or when all host bindings are processed for an element). + * Note that this value resets back to zero once the next directive is + * visited (when `incrementActiveDirectiveId` or `setActiveHostElement` + * is called). */ -export const enum ActiveElementFlags { - Initial = 0b00, - RunExitFn = 0b01, - ResetStylesOnExit = 0b10, - Size = 2, -} - -/** - * Determines whether or not a flag is currently set for the active element. - */ -export function hasActiveElementFlag(flag: ActiveElementFlags) { - return (_selectedIndex & flag) === flag; -} - -/** - * Sets a flag is for the active element. - */ -export function setActiveElementFlag(flag: ActiveElementFlags) { - _selectedIndex |= flag; -} +let activeDirectiveSuperClassHeight = 0; /** * Sets the active directive host element and resets the directive id value @@ -170,44 +163,14 @@ export function setActiveElementFlag(flag: ActiveElementFlags) { * the directive/component instance lives */ export function setActiveHostElement(elementIndex: number | null = null) { - if (getSelectedIndex() !== elementIndex) { - if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { - executeElementExitFn(); - } - if (hasActiveElementFlag(ActiveElementFlags.ResetStylesOnExit)) { - resetStylingState(); - } + if (_selectedIndex !== elementIndex) { setSelectedIndex(elementIndex === null ? -1 : elementIndex); - activeDirectiveId = 0; + activeDirectiveId = elementIndex === null ? 0 : MIN_DIRECTIVE_ID; + activeDirectiveSuperClassDepthPosition = 0; + activeDirectiveSuperClassHeight = 0; } } -let _elementExitFn: Function|null = null; -export function executeElementExitFn() { - _elementExitFn !(); - // TODO (matsko|misko): remove this unassignment once the state management of - // global variables are better managed. - _selectedIndex &= ~ActiveElementFlags.RunExitFn; -} - -/** - * Queues a function to be run once the element is "exited" in CD. - * - * Change detection will focus on an element either when the `advance()` - * instruction is called or when the template or host bindings instruction - * code is invoked. The element is then "exited" when the next element is - * selected or when change detection for the template or host bindings is - * complete. When this occurs (the element change operation) then an exit - * function will be invoked if it has been set. This function can be used - * to assign that exit function. - * - * @param fn - */ -export function setElementExitFn(fn: Function): void { - setActiveElementFlag(ActiveElementFlags.RunExitFn); - _elementExitFn = fn; -} - /** * Returns the current id value of the current directive. * @@ -248,12 +211,71 @@ export function getActiveDirectiveId() { * different set of directives). */ export function incrementActiveDirectiveId() { - // Each directive gets a uniqueId value that is the same for both - // create and update calls when the hostBindings function is called. The - // directive uniqueId is not set anywhere--it is just incremented between - // each hostBindings call and is useful for helping instruction code - // uniquely determine which directive is currently active when executed. - activeDirectiveId += 1; + activeDirectiveId += 1 + activeDirectiveSuperClassHeight; + + // because we are dealing with a new directive this + // means we have exited out of the inheritance chain + activeDirectiveSuperClassDepthPosition = 0; + activeDirectiveSuperClassHeight = 0; +} + +/** + * Set the current super class (reverse inheritance) position depth for a directive. + * + * For example we have two directives: Child and Other (but Child is a sub-class of Parent) + *
+ * + * // increment + * parentInstance->hostBindings() (depth = 1) + * // decrement + * childInstance->hostBindings() (depth = 0) + * otherInstance->hostBindings() (depth = 0 b/c it's a different directive) + * + * Note that this is only active when `hostBinding` functions are being processed. + */ +export function adjustActiveDirectiveSuperClassDepthPosition(delta: number) { + activeDirectiveSuperClassDepthPosition += delta; + + // we keep track of the height value so that when the next directive is visited + // then Angular knows to generate a new directive id value which has taken into + // account how many sub-class directives were a part of the previous directive. + activeDirectiveSuperClassHeight = + Math.max(activeDirectiveSuperClassHeight, activeDirectiveSuperClassDepthPosition); +} + +/** + * Returns he current depth of the super/sub class inheritance chain. + * + * This will return how many inherited directive/component classes + * exist in the current chain. + * + * ```typescript + * @Directive({ selector: '[super-dir]' }) + * class SuperDir {} + * + * @Directive({ selector: '[sub-dir]' }) + * class SubDir extends SuperDir {} + * + * // if `
` is used then the super class height is `1` + * // if `
` is used then the super class height is `0` + * ``` + */ +export function getActiveDirectiveSuperClassHeight() { + return activeDirectiveSuperClassHeight; +} + +/** + * Returns the current super class (reverse inheritance) depth for a directive. + * + * This is designed to help instruction code distinguish different hostBindings + * calls from each other when a directive has extended from another directive. + * Normally using the directive id value is enough, but with the case + * of parent/sub-class directive inheritance more information is required. + * + * Note that this is only active when `hostBinding` functions are being processed. + */ +export function getActiveDirectiveSuperClassDepth() { + return activeDirectiveSuperClassDepthPosition; } /** @@ -425,10 +447,10 @@ export function resetComponentState() { elementDepthCount = 0; bindingsEnabled = true; setCurrentStyleSanitizer(null); + resetAllStylingState(); } -/* tslint:disable */ -let _selectedIndex = -1 << ActiveElementFlags.Size; +let _selectedIndex = -1; /** * Gets the most recent index passed to {@link select} @@ -437,7 +459,7 @@ let _selectedIndex = -1 << ActiveElementFlags.Size; * current `LView` to act on. */ export function getSelectedIndex() { - return _selectedIndex >> ActiveElementFlags.Size; + return _selectedIndex; } /** @@ -445,12 +467,13 @@ export function getSelectedIndex() { * * Used with {@link property} instruction (and more in the future) to identify the index in the * current `LView` to act on. - * - * (Note that if an "exit function" was set earlier (via `setElementExitFn()`) then that will be - * run if and when the provided `index` value is different from the current selected index value.) */ export function setSelectedIndex(index: number) { - _selectedIndex = index << ActiveElementFlags.Size; + _selectedIndex = index; + + // we have now jumped to another element + // therefore the state is stale + resetStylingState(); } diff --git a/packages/core/src/render3/styling_next/bindings.ts b/packages/core/src/render3/styling_next/bindings.ts index 3231d7d91b..1a2d78fc14 100644 --- a/packages/core/src/render3/styling_next/bindings.ts +++ b/packages/core/src/render3/styling_next/bindings.ts @@ -5,14 +5,14 @@ * 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 {SafeValue, unwrapSafeValue} from '../../sanitization/bypass'; +import {SafeValue} from '../../sanitization/bypass'; import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer'; import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer'; import {NO_CHANGE} from '../tokens'; -import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces'; -import {getStylingState, resetStylingState} from './state'; -import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, getBindingValue, getConfig, getDefaultValue, getGuardMask, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from './util'; +import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces'; +import {BIT_MASK_START_VALUE, deleteStylingStateFromStorage, getStylingState, resetStylingState, storeStylingState} from './state'; +import {allowStylingFlush, getBindingValue, getGuardMask, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getValuesCount, hasValueChanged, isContextLocked, isSanitizationRequired, isStylingValueDefined, lockContext, setGuardMask, stateIsPersisted} from './util'; @@ -35,6 +35,13 @@ import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, * -------- */ +// The first bit value reflects a map-based binding value's bit. +// The reason why it's always activated for every entry in the map +// is so that if any map-binding values update then all other prop +// based bindings will pass the guard check automatically without +// any extra code or flags. +export const DEFAULT_GUARD_MASK_VALUE = 0b1; + /** * The guard/update mask bit index location for map-based bindings. * @@ -42,6 +49,26 @@ import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, */ const STYLING_INDEX_FOR_MAP_BINDING = 0; +/** + * Default fallback value for a styling binding. + * + * A value of `null` is used here which signals to the styling algorithm that + * the styling value is not present. This way if there are no other values + * detected then it will be removed once the style/class property is dirty and + * diffed within the styling algorithm present in `flushStyling`. + */ +const DEFAULT_BINDING_VALUE = null; + +/** + * Default size count value for a new entry in a context. + * + * A value of `1` is used here because each entry in the context has a default + * property. + */ +const DEFAULT_SIZE_VALUE = 1; + +let deferredBindingQueue: (TStylingContext | number | string | null | boolean)[] = []; + /** * Visits a class-based binding and updates the new value (if changed). * @@ -52,25 +79,23 @@ const STYLING_INDEX_FOR_MAP_BINDING = 0; * state each time it's called (which then allows the `TStylingContext` * and the bit mask values to be in sync). */ -export function updateClassViaContext( - context: TStylingContext, data: LStylingData, element: RElement, directiveIndex: number, - prop: string | null, bindingIndex: number, - value: boolean | string | null | undefined | StylingMapArray | NO_CHANGE, - forceUpdate?: boolean): boolean { +export function updateClassBinding( + context: TStylingContext, data: LStylingData, element: RElement, prop: string | null, + bindingIndex: number, value: boolean | string | null | undefined | StylingMapArray | NO_CHANGE, + deferRegistration: boolean, forceUpdate: boolean): boolean { const isMapBased = !prop; - const state = getStylingState(element, directiveIndex); - const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++; + const state = getStylingState(element, stateIsPersisted(context)); + const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++; if (value !== NO_CHANGE) { const updated = updateBindingData( - context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate, - false); + context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate, false); if (updated || forceUpdate) { // We flip the bit in the bitMask to reflect that the binding // at the `index` slot has changed. This identifies to the flushing // phase that the bindings for this particular CSS class need to be // applied again because on or more of the bindings for the CSS // class have changed. - state.classesBitMask |= 1 << countIndex; + state.classesBitMask |= 1 << index; return true; } } @@ -87,20 +112,19 @@ export function updateClassViaContext( * state each time it's called (which then allows the `TStylingContext` * and the bit mask values to be in sync). */ -export function updateStyleViaContext( - context: TStylingContext, data: LStylingData, element: RElement, directiveIndex: number, - prop: string | null, bindingIndex: number, +export function updateStyleBinding( + context: TStylingContext, data: LStylingData, element: RElement, prop: string | null, + bindingIndex: number, value: string | number | SafeValue | null | undefined | StylingMapArray | NO_CHANGE, - sanitizer: StyleSanitizeFn | null, forceUpdate?: boolean): boolean { + sanitizer: StyleSanitizeFn | null, deferRegistration: boolean, forceUpdate: boolean): boolean { const isMapBased = !prop; - const state = getStylingState(element, directiveIndex); - const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++; + const state = getStylingState(element, stateIsPersisted(context)); + const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++; if (value !== NO_CHANGE) { - const sanitizationRequired = isMapBased ? - true : + const sanitizationRequired = isMapBased || (sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false); const updated = updateBindingData( - context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate, + context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate, sanitizationRequired); if (updated || forceUpdate) { // We flip the bit in the bitMask to reflect that the binding @@ -108,7 +132,7 @@ export function updateStyleViaContext( // phase that the bindings for this particular property need to be // applied again because on or more of the bindings for the CSS // property have changed. - state.stylesBitMask |= 1 << countIndex; + state.stylesBitMask |= 1 << index; return true; } } @@ -120,6 +144,8 @@ export function updateStyleViaContext( * * This function is designed to be called from `updateStyleBinding` and `updateClassBinding`. * If called during the first update pass, the binding will be registered in the context. + * If the binding does get registered and the `deferRegistration` flag is true then the + * binding data will be queued up until the context is later flushed in `applyStyling`. * * This function will also update binding slot in the provided `LStylingData` with the * new binding entry (if it has changed). @@ -127,92 +153,74 @@ export function updateStyleViaContext( * @returns whether or not the binding value was updated in the `LStylingData`. */ function updateBindingData( - context: TStylingContext, data: LStylingData, counterIndex: number, sourceIndex: number, - prop: string | null, bindingIndex: number, - value: string | SafeValue | number | boolean | null | undefined | StylingMapArray, - forceUpdate?: boolean, sanitizationRequired?: boolean): boolean { - const hostBindingsMode = isHostStylingActive(sourceIndex); - if (!isContextLocked(context, hostBindingsMode)) { - // this will only happen during the first update pass of the - // context. The reason why we can't use `tNode.firstTemplatePass` - // here is because its not guaranteed to be true when the first - // update pass is executed (remember that all styling instructions - // are run in the update phase, and, as a result, are no more - // styling instructions that are run in the creation phase). - registerBinding(context, counterIndex, sourceIndex, prop, bindingIndex, sanitizationRequired); - patchConfig( - context, - hostBindingsMode ? TStylingConfig.HasHostBindings : TStylingConfig.HasTemplateBindings); - patchConfig(context, prop ? TStylingConfig.HasPropBindings : TStylingConfig.HasMapBindings); + context: TStylingContext, data: LStylingData, counterIndex: number, prop: string | null, + bindingIndex: number, + value: string | SafeValue | number | boolean | null | undefined | StylingMapArray | NO_CHANGE, + deferRegistration: boolean, forceUpdate: boolean, sanitizationRequired: boolean): boolean { + if (!isContextLocked(context)) { + if (deferRegistration) { + deferBindingRegistration(context, counterIndex, prop, bindingIndex, sanitizationRequired); + } else { + deferredBindingQueue.length && flushDeferredBindings(); + + // this will only happen during the first update pass of the + // context. The reason why we can't use `tNode.firstTemplatePass` + // here is because its not guaranteed to be true when the first + // update pass is executed (remember that all styling instructions + // are run in the update phase, and, as a result, are no more + // styling instructions that are run in the creation phase). + registerBinding(context, counterIndex, prop, bindingIndex, sanitizationRequired); + } } const changed = forceUpdate || hasValueChanged(data[bindingIndex], value); if (changed) { - setValue(data, bindingIndex, value); - const doSetValuesAsStale = (getConfig(context) & TStylingConfig.HasHostBindings) && - !hostBindingsMode && (prop ? !value : true); - if (doSetValuesAsStale) { - renderHostBindingsAsStale(context, data, prop, !prop); - } + data[bindingIndex] = value; } return changed; } /** - * Iterates over all host-binding values for the given `prop` value in the context and sets their - * corresponding binding values to `null`. + * Schedules a binding registration to be run at a later point. * - * Whenever a template binding changes its value to `null`, all host-binding values should be - * re-applied - * to the element when the host bindings are evaluated. This may not always happen in the event - * that none of the bindings changed within the host bindings code. For this reason this function - * is expected to be called each time a template binding becomes falsy or when a map-based template - * binding changes. + * The reasoning for this feature is to ensure that styling + * bindings are registered in the correct order for when + * directives/components have a super/sub class inheritance + * chains. Each directive's styling bindings must be + * registered into the context in reverse order. Therefore all + * bindings will be buffered in reverse order and then applied + * after the inheritance chain exits. */ -function renderHostBindingsAsStale( - context: TStylingContext, data: LStylingData, prop: string | null, isMapBased: boolean): void { - const valuesCount = getValuesCount(context); +function deferBindingRegistration( + context: TStylingContext, counterIndex: number, prop: string | null, bindingIndex: number, + sanitizationRequired: boolean) { + deferredBindingQueue.unshift(context, counterIndex, prop, bindingIndex, sanitizationRequired); +} - if (hasConfig(context, TStylingConfig.HasPropBindings)) { - const itemsPerRow = TStylingContextIndex.BindingsStartOffset + valuesCount; - - let i = TStylingContextIndex.ValuesStartPosition; - while (i < context.length) { - if (getProp(context, i) === prop) { - break; - } - i += itemsPerRow; - } - - const bindingsStart = i + TStylingContextIndex.BindingsStartOffset; - const valuesStart = bindingsStart + 1; // the first column is template bindings - const valuesEnd = bindingsStart + valuesCount - 1; - - for (let i = valuesStart; i < valuesEnd; i++) { - const bindingIndex = context[i] as number; - if (bindingIndex !== 0) { - setValue(data, bindingIndex, null); - } - } - } - - if (hasConfig(context, TStylingConfig.HasMapBindings)) { - const bindingsStart = - TStylingContextIndex.ValuesStartPosition + TStylingContextIndex.BindingsStartOffset; - const valuesStart = bindingsStart + 1; // the first column is template bindings - const valuesEnd = bindingsStart + valuesCount - 1; - for (let i = valuesStart; i < valuesEnd; i++) { - const stylingMap = getValue(data, context[i] as number); - if (stylingMap) { - setMapAsDirty(stylingMap); - } - } +/** + * Flushes the collection of deferred bindings and causes each entry + * to be registered into the context. + */ +function flushDeferredBindings() { + let i = 0; + while (i < deferredBindingQueue.length) { + const context = deferredBindingQueue[i++] as TStylingContext; + const count = deferredBindingQueue[i++] as number; + const prop = deferredBindingQueue[i++] as string; + const bindingIndex = deferredBindingQueue[i++] as number | null; + const sanitizationRequired = deferredBindingQueue[i++] as boolean; + registerBinding(context, count, prop, bindingIndex, sanitizationRequired); } + deferredBindingQueue.length = 0; } /** * Registers the provided binding (prop + bindingIndex) into the context. * + * This function is shared between bindings that are assigned immediately + * (via `updateBindingData`) and at a deferred stage. When called, it will + * figure out exactly where to place the binding data in the context. + * * It is needed because it will either update or insert a styling property * into the context at the correct spot. * @@ -238,79 +246,61 @@ function renderHostBindingsAsStale( * value. * * Note that this function is also used for map-based styling bindings. They are treated - * much the same as prop-based bindings, but, their property name value is set as `[MAP]`. + * much the same as prop-based bindings, but, because they do not have a property value + * (since it's a map), all map-based entries are stored in an already populated area of + * the context at the top (which is reserved for map-based entries). */ export function registerBinding( - context: TStylingContext, countId: number, sourceIndex: number, prop: string | null, - bindingValue: number | null | string | boolean, sanitizationRequired?: boolean): void { - let found = false; - prop = prop || MAP_BASED_ENTRY_PROP_NAME; - - const total = getTotalSources(context); - - // if a new source is detected then a new column needs to be allocated into - // the styling context. The column is basically a new allocation of binding - // sources that will be available to each property. - if (sourceIndex >= total) { - addNewSourceColumn(context); - } - - const isBindingIndexValue = typeof bindingValue === 'number'; - const entriesPerRow = TStylingContextIndex.BindingsStartOffset + getValuesCount(context); - let i = TStylingContextIndex.ValuesStartPosition; - - // all style/class bindings are sorted by property name - while (i < context.length) { - const p = getProp(context, i); - if (prop <= p) { - if (prop < p) { - allocateNewContextEntry(context, i, prop, sanitizationRequired); - } else if (isBindingIndexValue) { - patchConfig(context, TStylingConfig.HasCollisions); + context: TStylingContext, countId: number, prop: string | null, + bindingValue: number | null | string | boolean, sanitizationRequired?: boolean): boolean { + let registered = false; + if (prop) { + // prop-based bindings (e.g `
`) + let found = false; + let i = getPropValuesStartPosition(context); + while (i < context.length) { + const valuesCount = getValuesCount(context, i); + const p = getProp(context, i); + found = prop <= p; + if (found) { + // all style/class bindings are sorted by property name + if (prop < p) { + allocateNewContextEntry(context, i, prop, sanitizationRequired); + } + addBindingIntoContext(context, false, i, bindingValue, countId); + break; } - addBindingIntoContext(context, i, bindingValue, countId, sourceIndex); - found = true; - break; + i += TStylingContextIndex.BindingsStartOffset + valuesCount; } - i += entriesPerRow; - } - if (!found) { - allocateNewContextEntry(context, context.length, prop, sanitizationRequired); - addBindingIntoContext(context, i, bindingValue, countId, sourceIndex); + if (!found) { + allocateNewContextEntry(context, context.length, prop, sanitizationRequired); + addBindingIntoContext(context, false, i, bindingValue, countId); + registered = true; + } + } else { + // map-based bindings (e.g `
`) + // there is no need to allocate the map-based binding region into the context + // since it is already there when the context is first created. + addBindingIntoContext( + context, true, TStylingContextIndex.MapBindingsPosition, bindingValue, countId); + registered = true; } + return registered; } -/** - * Inserts a new row into the provided `TStylingContext` and assigns the provided `prop` value as - * the property entry. - */ function allocateNewContextEntry( - context: TStylingContext, index: number, prop: string, sanitizationRequired?: boolean): void { + context: TStylingContext, index: number, prop: string, sanitizationRequired?: boolean) { + // 1,2: splice index locations + // 3: each entry gets a config value (guard mask + flags) + // 4. each entry gets a size value (which is always one because there is always a default binding + // value) + // 5. the property that is getting allocated into the context + // 6. the default binding value (usually `null`) const config = sanitizationRequired ? TStylingContextPropConfigFlags.SanitizationRequired : TStylingContextPropConfigFlags.Default; - context.splice( - index, 0, - config, // 1) config value - DEFAULT_GUARD_MASK_VALUE, // 2) template bit mask - DEFAULT_GUARD_MASK_VALUE, // 3) host bindings bit mask - prop, // 4) prop value (e.g. `width`, `myClass`, etc...) - ); - - index += 4; // the 4 values above - - // 5...) default binding index for the template value - // depending on how many sources already exist in the context, - // multiple default index entries may need to be inserted for - // the new value in the context. - const totalBindingsPerEntry = getTotalSources(context); - for (let i = 0; i < totalBindingsPerEntry; i++) { - context.splice(index, 0, DEFAULT_BINDING_INDEX); - index++; - } - - // 6) default binding value for the new entry - context.splice(index, 0, DEFAULT_BINDING_VALUE); + context.splice(index, 0, config, DEFAULT_SIZE_VALUE, prop, DEFAULT_BINDING_VALUE); + setGuardMask(context, index, DEFAULT_GUARD_MASK_VALUE); } /** @@ -326,70 +316,75 @@ function allocateNewContextEntry( * * - Otherwise the binding value will update the default value for the property * and this will only happen if the default value is `null`. + * + * Note that this function also handles map-based bindings and will insert them + * at the top of the context. */ function addBindingIntoContext( - context: TStylingContext, index: number, bindingValue: number | string | boolean | null, - bitIndex: number, sourceIndex: number) { + context: TStylingContext, isMapBased: boolean, index: number, + bindingValue: number | string | boolean | null, countId: number) { + const valuesCount = getValuesCount(context, index); + + const firstValueIndex = index + TStylingContextIndex.BindingsStartOffset; + let lastValueIndex = firstValueIndex + valuesCount; + if (!isMapBased) { + // prop-based values all have default values, but map-based entries do not. + // we want to access the index for the default value in this case and not just + // the bindings... + lastValueIndex--; + } + if (typeof bindingValue === 'number') { - const hostBindingsMode = isHostStylingActive(sourceIndex); - const cellIndex = index + TStylingContextIndex.BindingsStartOffset + sourceIndex; - context[cellIndex] = bindingValue; - const updatedBitMask = getGuardMask(context, index, hostBindingsMode) | (1 << bitIndex); - setGuardMask(context, index, updatedBitMask, hostBindingsMode); - } else if (bindingValue !== null && getDefaultValue(context, index) === null) { - setDefaultValue(context, index, bindingValue); + // the loop here will check to see if the binding already exists + // for the property in the context. Why? The reason for this is + // because the styling context is not "locked" until the first + // flush has occurred. This means that if a repeated element + // registers its styling bindings then it will register each + // binding more than once (since its duplicated). This check + // will prevent that from happening. Note that this only happens + // when a binding is first encountered and not each time it is + // updated. + for (let i = firstValueIndex; i <= lastValueIndex; i++) { + const indexAtPosition = context[i]; + if (indexAtPosition === bindingValue) return; + } + + context.splice(lastValueIndex, 0, bindingValue); + (context[index + TStylingContextIndex.ValuesCountOffset] as number)++; + + // now that a new binding index has been added to the property + // the guard mask bit value (at the `countId` position) needs + // to be included into the existing mask value. + const guardMask = getGuardMask(context, index) | (1 << countId); + setGuardMask(context, index, guardMask); + } else if (bindingValue !== null && context[lastValueIndex] == null) { + context[lastValueIndex] = bindingValue; } } -/** - * Registers a new column into the provided `TStylingContext`. - * - * If and when a new source is detected then a new column needs to - * be allocated into the styling context. The column is basically - * a new allocation of binding sources that will be available to each - * property. - * - * Each column that exists in the styling context resembles a styling - * source. A styling source an either be the template or one or more - * components or directives all containing styling host bindings. - */ -function addNewSourceColumn(context: TStylingContext): void { - // we use -1 here because we want to insert right before the last value (the default value) - const insertOffset = TStylingContextIndex.BindingsStartOffset + getValuesCount(context) - 1; - - let index = TStylingContextIndex.ValuesStartPosition; - while (index < context.length) { - index += insertOffset; - context.splice(index++, 0, DEFAULT_BINDING_INDEX); - - // the value was inserted just before the default value, but the - // next entry in the context starts just after it. Therefore++. - index++; - } - context[TStylingContextIndex.TotalSourcesPosition]++; -} - /** * Applies all pending style and class bindings to the provided element. * * This function will attempt to flush styling via the provided `classesContext` * and `stylesContext` context values. This function is designed to be run from - * the internal `stylingApply` function (which is scheduled to run at the very - * end of change detection for an element if one or more style/class bindings - * were processed) and will rely on any state values that are set from when - * any of the styling bindings executed. + * the `stylingApply()` instruction (which is run at the very end of styling + * change detection) and will rely on any state values that are set from when + * any styling bindings update. * - * This function is designed to be called twice: one when change detection has - * processed an element within the template bindings (i.e. just as `advance()` - * is called) and when host bindings have been processed. In both cases the - * styles and classes in both contexts will be applied to the element, but the - * algorithm will selectively decide which bindings to run depending on the - * columns in the context. The provided `directiveIndex` value will help the - * algorithm determine which bindings to apply: either the template bindings or - * the host bindings (see `applyStylingToElement` for more information). + * This function may be called multiple times on the same element because it can + * be called from the template code as well as from host bindings. In order for + * styling to be successfully flushed to the element (which will only happen once + * despite this being called multiple times), the following criteria must be met: * - * Note that once this function is called all temporary styling state data - * (i.e. the `bitMask` and `counter` values for styles and classes will be cleared). + * - `flushStyling` is called from the very last directive that has styling for + * the element (see `allowStylingFlush()`). + * - one or more bindings for classes or styles has updated (this is checked by + * examining the classes or styles bit mask). + * + * If the style and class values are successfully applied to the element then + * the temporary state values for the element will be cleared. Otherwise, if + * this did not occur then the styling state is persisted (see `state.ts` for + * more information on how this works). */ export function flushStyling( renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, @@ -397,28 +392,53 @@ export function flushStyling( element: RElement, directiveIndex: number, styleSanitizer: StyleSanitizeFn | null): void { ngDevMode && ngDevMode.flushStyling++; - const state = getStylingState(element, directiveIndex); - const hostBindingsMode = isHostStylingActive(state.sourceIndex); + const persistState = classesContext ? stateIsPersisted(classesContext) : + (stylesContext ? stateIsPersisted(stylesContext) : false); + const allowFlushClasses = allowStylingFlush(classesContext, directiveIndex); + const allowFlushStyles = allowStylingFlush(stylesContext, directiveIndex); - if (stylesContext) { - if (!isContextLocked(stylesContext, hostBindingsMode)) { - lockAndFinalizeContext(stylesContext, hostBindingsMode); - } - applyStylingViaContext( - stylesContext, renderer, element, data, state.stylesBitMask, setStyle, styleSanitizer, - hostBindingsMode); + // deferred bindings are bindings which are scheduled to register with + // the context at a later point. These bindings can only registered when + // the context will be 100% flushed to the element. + if (deferredBindingQueue.length && (allowFlushClasses || allowFlushStyles)) { + flushDeferredBindings(); } - if (classesContext) { - if (!isContextLocked(classesContext, hostBindingsMode)) { - lockAndFinalizeContext(classesContext, hostBindingsMode); - } - applyStylingViaContext( - classesContext, renderer, element, data, state.classesBitMask, setClass, null, - hostBindingsMode); - } + const state = getStylingState(element, persistState); + const classesFlushed = maybeApplyStyling( + renderer, element, data, classesContext, allowFlushClasses, state.classesBitMask, setClass, + null); + const stylesFlushed = maybeApplyStyling( + renderer, element, data, stylesContext, allowFlushStyles, state.stylesBitMask, setStyle, + styleSanitizer); - resetStylingState(); + if (classesFlushed && stylesFlushed) { + resetStylingState(); + if (persistState) { + deleteStylingStateFromStorage(element); + } + } else if (persistState) { + storeStylingState(element, state); + } +} + +function maybeApplyStyling( + renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, data: LStylingData, + context: TStylingContext | null, allowFlush: boolean, bitMask: number, + styleSetter: ApplyStylingFn, styleSanitizer: any | null): boolean { + if (allowFlush && context) { + lockAndFinalizeContext(context); + if (contextHasUpdates(context, bitMask)) { + ngDevMode && (styleSanitizer ? ngDevMode.stylesApplied++ : ngDevMode.classesApplied++); + applyStyling(context !, renderer, element, data, bitMask, styleSetter, styleSanitizer); + return true; + } + } + return allowFlush; +} + +function contextHasUpdates(context: TStylingContext | null, bitMask: number) { + return context && bitMask > BIT_MASK_START_VALUE; } /** @@ -442,50 +462,14 @@ export function flushStyling( * be updated each time a host binding applies its static styling values (via `elementHostAttrs`) * so these values are only read at this point because this is the very last point before the * first style/class values are flushed to the element. - * - * Note that the `TStylingContext` styling context contains two locks: one for template bindings - * and another for host bindings. Either one of these locks will be set when styling is applied - * during the template binding flush and/or during the host bindings flush. */ -function lockAndFinalizeContext(context: TStylingContext, hostBindingsMode: boolean): void { - const initialValues = getStylingMapArray(context) !; - updateInitialStylingOnContext(context, initialValues); - lockContext(context, hostBindingsMode); -} - -/** - * Registers all initial styling entries into the provided context. - * - * This function will iterate over all entries in the provided `initialStyling` ar}ray and register - * them as default (initial) values in the provided context. Initial styling values in a context are - * the default values that are to be applied unless overwritten by a binding. - * - * The reason why this function exists and isn't a part of the context construction is because - * host binding is evaluated at a later stage after the element is created. This means that - * if a directive or component contains any initial styling code (i.e. `
`) - * then that initial styling data can only be applied once the styling for that element - * is first applied (at the end of the update phase). Once that happens then the context will - * update itself with the complete initial styling for the element. - */ -function updateInitialStylingOnContext( - context: TStylingContext, initialStyling: StylingMapArray): void { - // `-1` is used here because all initial styling data is not a apart - // of a binding (since it's static) - const COUNT_ID_FOR_STYLING = -1; - - let hasInitialStyling = false; - for (let i = StylingMapArrayIndex.ValuesStartPosition; i < initialStyling.length; - i += StylingMapArrayIndex.TupleSize) { - const value = getMapValue(initialStyling, i); - if (value) { - const prop = getMapProp(initialStyling, i); - registerBinding(context, COUNT_ID_FOR_STYLING, 0, prop, value, false); - hasInitialStyling = true; +function lockAndFinalizeContext(context: TStylingContext): void { + if (!isContextLocked(context)) { + const initialValues = getStylingMapArray(context); + if (initialValues) { + updateInitialStylingOnContext(context, initialValues); } - } - - if (hasInitialStyling) { - patchConfig(context, TStylingConfig.HasInitialStyling); + lockContext(context); } } @@ -513,74 +497,60 @@ function updateInitialStylingOnContext( * algorithm works for map-based styling bindings. * * Note that this function is not designed to be called in isolation (use - * the `flushStyling` function so that it can call this function for both - * the styles and classes contexts). + * `applyClasses` and `applyStyles` to actually apply styling values). */ -export function applyStylingViaContext( +export function applyStyling( context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, bindingData: LStylingData, bitMaskValue: number | boolean, applyStylingFn: ApplyStylingFn, - sanitizer: StyleSanitizeFn | null, hostBindingsMode: boolean): void { + sanitizer: StyleSanitizeFn | null) { const bitMask = normalizeBitMaskValue(bitMaskValue); - - let stylingMapsSyncFn: SyncStylingMapsFn|null = null; - let applyAllValues = false; - if (hasConfig(context, TStylingConfig.HasMapBindings)) { - stylingMapsSyncFn = getStylingMapsSyncFn(); - const mapsGuardMask = - getGuardMask(context, TStylingContextIndex.ValuesStartPosition, hostBindingsMode); - applyAllValues = (bitMask & mapsGuardMask) !== 0; - } - - const valuesCount = getValuesCount(context); - let totalBindingsToVisit = 1; - let mapsMode = + const stylingMapsSyncFn = getStylingMapsSyncFn(); + const mapsGuardMask = getGuardMask(context, TStylingContextIndex.MapBindingsPosition); + const applyAllValues = (bitMask & mapsGuardMask) > 0; + const mapsMode = applyAllValues ? StylingMapsSyncMode.ApplyAllValues : StylingMapsSyncMode.TraverseValues; - if (hostBindingsMode) { - mapsMode |= StylingMapsSyncMode.RecurseInnerMaps; - totalBindingsToVisit = valuesCount - 1; - } let i = getPropValuesStartPosition(context); while (i < context.length) { - const guardMask = getGuardMask(context, i, hostBindingsMode); + const valuesCount = getValuesCount(context, i); + const guardMask = getGuardMask(context, i); if (bitMask & guardMask) { let valueApplied = false; const prop = getProp(context, i); - const defaultValue = getDefaultValue(context, i); + const valuesCountUpToDefault = valuesCount - 1; + const defaultValue = getBindingValue(context, i, valuesCountUpToDefault) as string | null; - // Part 1: Visit the `[styling.prop]` value - for (let j = 0; j < totalBindingsToVisit; j++) { + // case 1: apply prop-based values + // try to apply the binding values and see if a non-null + // value gets set for the styling binding + for (let j = 0; j < valuesCountUpToDefault; j++) { const bindingIndex = getBindingValue(context, i, j) as number; - if (!valueApplied && bindingIndex !== 0) { - const value = getValue(bindingData, bindingIndex); - if (isStylingValueDefined(value)) { - const checkValueOnly = hostBindingsMode && j === 0; - if (!checkValueOnly) { - const finalValue = sanitizer && isSanitizationRequired(context, i) ? - sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) : - unwrapSafeValue(value); - applyStylingFn(renderer, element, prop, finalValue, bindingIndex); - } - valueApplied = true; - } - } - - // Part 2: Visit the `[style]` or `[class]` map-based value - if (stylingMapsSyncFn) { - // determine whether or not to apply the target property or to skip it - let mode = mapsMode | (valueApplied ? StylingMapsSyncMode.SkipTargetProp : - StylingMapsSyncMode.ApplyTargetProp); - if (hostBindingsMode && j === 0) { - mode |= StylingMapsSyncMode.CheckValuesOnly; - } - const valueAppliedWithinMap = stylingMapsSyncFn( - context, renderer, element, bindingData, j, applyStylingFn, sanitizer, mode, prop, - defaultValue); - valueApplied = valueApplied || valueAppliedWithinMap; + const value = bindingData[bindingIndex]; + if (isStylingValueDefined(value)) { + const finalValue = sanitizer && isSanitizationRequired(context, i) ? + sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) : + value; + applyStylingFn(renderer, element, prop, finalValue, bindingIndex); + valueApplied = true; + break; } } - // Part 3: apply the default value (e.g. `
` => `200px` gets applied) + // case 2: apply map-based values + // traverse through each map-based styling binding and update all values up to + // the provided `prop` value. If the property was not applied in the loop above + // then it will be attempted to be applied in the maps sync code below. + if (stylingMapsSyncFn) { + // determine whether or not to apply the target property or to skip it + const mode = mapsMode | (valueApplied ? StylingMapsSyncMode.SkipTargetProp : + StylingMapsSyncMode.ApplyTargetProp); + const valueAppliedWithinMap = stylingMapsSyncFn( + context, renderer, element, bindingData, applyStylingFn, sanitizer, mode, prop, + defaultValue); + valueApplied = valueApplied || valueAppliedWithinMap; + } + + // case 3: apply the default value // if the value has not yet been applied then a truthy value does not exist in the // prop-based or map-based bindings code. If and when this happens, just apply the // default value (even if the default value is `null`). @@ -596,92 +566,10 @@ export function applyStylingViaContext( // values. For this reason, one more call to the sync function // needs to be issued at the end. if (stylingMapsSyncFn) { - if (hostBindingsMode) { - mapsMode |= StylingMapsSyncMode.CheckValuesOnly; - } - stylingMapsSyncFn( - context, renderer, element, bindingData, 0, applyStylingFn, sanitizer, mapsMode); + stylingMapsSyncFn(context, renderer, element, bindingData, applyStylingFn, sanitizer, mapsMode); } } -/** - * Applies the provided styling map to the element directly (without context resolution). - * - * This function is designed to be run from the styling instructions and will be called - * automatically. This function is intended to be used for performance reasons in the - * event that there is no need to apply styling via context resolution. - * - * See `allowDirectStylingApply`. - * - * @returns whether or not the styling map was applied to the element. - */ -export function applyStylingMapDirectly( - renderer: any, context: TStylingContext, element: RElement, data: LStylingData, - bindingIndex: number, map: StylingMapArray, applyFn: ApplyStylingFn, - sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean): boolean { - if (forceUpdate || hasValueChanged(data[bindingIndex], map)) { - setValue(data, bindingIndex, map); - for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length; - i += StylingMapArrayIndex.TupleSize) { - const prop = getMapProp(map, i); - const value = getMapValue(map, i); - applyStylingValue(renderer, context, element, prop, value, applyFn, bindingIndex, sanitizer); - } - return true; - } - return false; -} - -/** - * Applies the provided styling prop/value to the element directly (without context resolution). - * - * This function is designed to be run from the styling instructions and will be called - * automatically. This function is intended to be used for performance reasons in the - * event that there is no need to apply styling via context resolution. - * - * See `allowDirectStylingApply`. - * - * @returns whether or not the prop/value styling was applied to the element. - */ -export function applyStylingValueDirectly( - renderer: any, context: TStylingContext, element: RElement, data: LStylingData, - bindingIndex: number, prop: string, value: any, applyFn: ApplyStylingFn, - sanitizer?: StyleSanitizeFn | null): boolean { - if (hasValueChanged(data[bindingIndex], value)) { - setValue(data, bindingIndex, value); - applyStylingValue(renderer, context, element, prop, value, applyFn, bindingIndex, sanitizer); - return true; - } - return false; -} - -function applyStylingValue( - renderer: any, context: TStylingContext, element: RElement, prop: string, value: any, - applyFn: ApplyStylingFn, bindingIndex: number, sanitizer?: StyleSanitizeFn | null) { - let valueToApply: string|null = unwrapSafeValue(value); - if (isStylingValueDefined(valueToApply)) { - valueToApply = - sanitizer ? sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) : valueToApply; - } else if (hasConfig(context, TStylingConfig.HasInitialStyling)) { - const initialStyles = getStylingMapArray(context); - if (initialStyles) { - valueToApply = findInitialStylingValue(initialStyles, prop); - } - } - applyFn(renderer, element, prop, valueToApply, bindingIndex); -} - -function findInitialStylingValue(map: StylingMapArray, prop: string): string|null { - for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length; - i += StylingMapArrayIndex.TupleSize) { - const p = getMapProp(map, i); - if (p >= prop) { - return p === prop ? getMapValue(map, i) : null; - } - } - return null; -} - function normalizeBitMaskValue(value: number | boolean): number { // if pass => apply all values (-1 implies that all bits are flipped to true) if (value === true) return -1; @@ -705,7 +593,7 @@ export function setStylingMapsSyncFn(fn: SyncStylingMapsFn) { /** * Assigns a style value to a style property for the given element. */ -export const setStyle: ApplyStylingFn = +const setStyle: ApplyStylingFn = (renderer: Renderer3 | null, native: RElement, prop: string, value: string | null) => { // the reason why this may be `null` is either because // it's a container element or it's a part of a test @@ -732,7 +620,7 @@ export const setStyle: ApplyStylingFn = /** * Adds/removes the provided className value to the provided element. */ -export const setClass: ApplyStylingFn = +const setClass: ApplyStylingFn = (renderer: Renderer3 | null, native: RElement, className: string, value: any) => { if (className !== '') { // the reason why this may be `null` is either because @@ -778,3 +666,33 @@ export function renderStylingMap( } } } + +/** + * Registers all initial styling entries into the provided context. + * + * This function will iterate over all entries in the provided `initialStyling` ar}ray and register + * them as default (initial) values in the provided context. Initial styling values in a context are + * the default values that are to be applied unless overwritten by a binding. + * + * The reason why this function exists and isn't a part of the context construction is because + * host binding is evaluated at a later stage after the element is created. This means that + * if a directive or component contains any initial styling code (i.e. `
`) + * then that initial styling data can only be applied once the styling for that element + * is first applied (at the end of the update phase). Once that happens then the context will + * update itself with the complete initial styling for the element. + */ +function updateInitialStylingOnContext( + context: TStylingContext, initialStyling: StylingMapArray): void { + // `-1` is used here because all initial styling data is not a spart + // of a binding (since it's static) + const INITIAL_STYLING_COUNT_ID = -1; + + for (let i = StylingMapArrayIndex.ValuesStartPosition; i < initialStyling.length; + i += StylingMapArrayIndex.TupleSize) { + const value = getMapValue(initialStyling, i); + if (value) { + const prop = getMapProp(initialStyling, i); + registerBinding(context, INITIAL_STYLING_COUNT_ID, prop, value, false); + } + } +} diff --git a/packages/core/src/render3/styling_next/instructions.ts b/packages/core/src/render3/styling_next/instructions.ts index f967d27e76..0ba7585067 100644 --- a/packages/core/src/render3/styling_next/instructions.ts +++ b/packages/core/src/render3/styling_next/instructions.ts @@ -10,17 +10,17 @@ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {setInputsForProperty} from '../instructions/shared'; import {AttributeMarker, TAttributes, TNode, TNodeType} from '../interfaces/node'; import {RElement} from '../interfaces/renderer'; -import {BINDING_INDEX, LView, RENDERER} from '../interfaces/view'; -import {ActiveElementFlags, getActiveDirectiveId, getCurrentStyleSanitizer, getLView, getSelectedIndex, setActiveElementFlag, setCurrentStyleSanitizer, setElementExitFn} from '../state'; +import {BINDING_INDEX, LView, RENDERER, TVIEW} from '../interfaces/view'; +import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getCurrentStyleSanitizer, getLView, getPreviousOrParentTNode, getSelectedIndex, setCurrentStyleSanitizer} from '../state'; import {NO_CHANGE} from '../tokens'; import {renderStringify} from '../util/misc_utils'; import {getNativeByTNode, getTNode} from '../util/view_utils'; -import {applyStylingMapDirectly, applyStylingValueDirectly, flushStyling, setClass, setStyle, updateClassViaContext, updateStyleViaContext} from './bindings'; +import {flushStyling, updateClassBinding, updateStyleBinding} from './bindings'; import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from './interfaces'; -import {activateStylingMapFeature} from './map_based_bindings'; +import {activateStylingMapFeature, addItemToStylingMap, normalizeIntoStylingMap, stylingMapToString} from './map_based_bindings'; import {attachStylingDebugObject} from './styling_debug'; -import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isHostStylingActive, isStylingContext, normalizeIntoStylingMap, setValue, stylingMapToString} from './util'; +import {allocTStylingContext, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isStylingContext, updateLastDirectiveIndex as _updateLastDirectiveIndex} from './util'; @@ -34,13 +34,34 @@ import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDi * -------- */ +/** + * Temporary function to bridge styling functionality between this new + * refactor (which is here inside of `styling_next/`) and the old + * implementation (which lives inside of `styling/`). + * + * This function is executed during the creation block of an element. + * Because the existing styling implementation issues a call to the + * `styling()` instruction, this instruction will also get run. The + * central idea here is that the directive index values are bound + * into the context. The directive index is temporary and is only + * required until the `select(n)` instruction is fully functional. + * + * @codeGenApi + */ +export function ɵɵstyling() { + const tView = getLView()[TVIEW]; + if (tView.firstTemplatePass) { + updateLastDirectiveIndex(getPreviousOrParentTNode(), getActiveDirectiveStylingIndex()); + } +} + /** * Sets the current style sanitizer function which will then be used * within all follow-up prop and map-based style binding instructions * for the given element. * * Note that once styling has been applied to the element (i.e. once - * `advance(n)` is executed or the hostBindings/template function exits) + * `select(n)` is executed or the hostBindings/template function exits) * then the active `sanitizerFn` will be set to `null`. This means that * once styling is applied to another element then a another call to * `styleSanitizer` will need to be made. @@ -71,7 +92,7 @@ export function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void { * be ignored. * * Note that this will apply the provided style value to the host element if this function is called - * within a host binding function. + * within a host binding. * * @codeGenApi */ @@ -80,15 +101,9 @@ export function ɵɵstyleProp( stylePropInternal(getSelectedIndex(), prop, value, suffix); } -/** - * Internal function for applying a single style to an element. - * - * The reason why this function has been separated from `ɵɵstyleProp` is because - * it is also called from `ɵɵstylePropInterpolate`. - */ export function stylePropInternal( elementIndex: number, prop: string, value: string | number | SafeValue | null, - suffix?: string | null | undefined): void { + suffix?: string | null | undefined) { const lView = getLView(); // if a value is interpolated then it may render a `NO_CHANGE` value. @@ -97,8 +112,9 @@ export function stylePropInternal( // are stored inside of the lView. const bindingIndex = lView[BINDING_INDEX]++; - const updated = - stylingProp(elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false); + const updated = _stylingProp( + elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false, + deferStylingUpdate()); if (ngDevMode) { ngDevMode.styleProp++; if (updated) { @@ -118,7 +134,7 @@ export function stylePropInternal( * @param value A true/false value which will turn the class on or off. * * Note that this will apply the provided class value to the host element if this function - * is called within a host binding function. + * is called within a host binding. * * @codeGenApi */ @@ -131,7 +147,8 @@ export function ɵɵclassProp(className: string, value: boolean | null): void { // are stored inside of the lView. const bindingIndex = lView[BINDING_INDEX]++; - const updated = stylingProp(getSelectedIndex(), bindingIndex, className, value, true); + const updated = + _stylingProp(getSelectedIndex(), bindingIndex, className, value, true, deferStylingUpdate()); if (ngDevMode) { ngDevMode.classProp++; if (updated) { @@ -142,57 +159,28 @@ export function ɵɵclassProp(className: string, value: boolean | null): void { /** * Shared function used to update a prop-based styling binding for an element. - * - * Depending on the state of the `tNode.styles` styles context, the style/prop - * value may be applied directly to the element instead of being processed - * through the context. The reason why this occurs is for performance and fully - * depends on the state of the context (i.e. whether or not there are duplicate - * bindings or whether or not there are map-based bindings and property bindings - * present together). */ -function stylingProp( +function _stylingProp( elementIndex: number, bindingIndex: number, prop: string, value: boolean | number | SafeValue | string | null | undefined | NO_CHANGE, - isClassBased: boolean): boolean { - let updated = false; - + isClassBased: boolean, defer: boolean): boolean { const lView = getLView(); const tNode = getTNode(elementIndex, lView); const native = getNativeByTNode(tNode, lView) as RElement; - const hostBindingsMode = isHostStyling(); - const context = isClassBased ? getClassesContext(tNode) : getStylesContext(tNode); - const sanitizer = isClassBased ? null : getCurrentStyleSanitizer(); - - // Direct Apply Case: bypass context resolution and apply the - // style/class value directly to the element - if (allowDirectStyling(context, hostBindingsMode)) { - const renderer = getRenderer(tNode, lView); - updated = applyStylingValueDirectly( - renderer, context, native, lView, bindingIndex, prop, value, - isClassBased ? setClass : setStyle, sanitizer); + let valueHasChanged = false; + if (isClassBased) { + valueHasChanged = updateClassBinding( + getClassesContext(tNode), lView, native, prop, bindingIndex, + value as string | boolean | null, defer, false); } else { - // Context Resolution (or first update) Case: save the value - // and defer to the context to flush and apply the style/class binding - // value to the element. - const directiveIndex = getActiveDirectiveId(); - if (isClassBased) { - updated = updateClassViaContext( - context, lView, native, directiveIndex, prop, bindingIndex, - value as string | boolean | null); - } else { - updated = updateStyleViaContext( - context, lView, native, directiveIndex, prop, bindingIndex, - value as string | SafeValue | null, sanitizer); - } - - if (updated) { - setElementExitFn(applyStyling); - } - markStylingStateAsDirty(); + const sanitizer = getCurrentStyleSanitizer(); + valueHasChanged = updateStyleBinding( + getStylesContext(tNode), lView, native, prop, bindingIndex, + value as string | SafeValue | null, sanitizer, defer, false); } - return updated; + return valueHasChanged; } /** @@ -219,6 +207,7 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu const lView = getLView(); const tNode = getTNode(index, lView); const context = getStylesContext(tNode); + const directiveIndex = getActiveDirectiveStylingIndex(); // if a value is interpolated then it may render a `NO_CHANGE` value. // in this case we do not need to do anything, but the binding index @@ -229,12 +218,12 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu // inputs are only evaluated from a template binding into a directive, therefore, // there should not be a situation where a directive host bindings function // evaluates the inputs (this should only happen in the template function) - if (!isHostStyling() && hasStyleInput(tNode) && styles !== NO_CHANGE) { + if (!directiveIndex && hasStyleInput(tNode) && styles !== NO_CHANGE) { updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false); styles = NO_CHANGE; } - const updated = _stylingMap(index, context, bindingIndex, styles, false); + const updated = _stylingMap(index, context, bindingIndex, styles, false, deferStylingUpdate()); if (ngDevMode) { ngDevMode.styleMap++; if (updated) { @@ -265,17 +254,12 @@ export function ɵɵclassMap(classes: {[className: string]: any} | NO_CHANGE | s classMapInternal(getSelectedIndex(), classes); } -/** - * Internal function for applying a class string or key/value map of classes to an element. - * - * The reason why this function has been separated from `ɵɵclassMap` is because - * it is also called from `ɵɵclassMapInterpolate`. - */ export function classMapInternal( - elementIndex: number, classes: {[className: string]: any} | NO_CHANGE | string | null): void { + elementIndex: number, classes: {[className: string]: any} | NO_CHANGE | string | null) { const lView = getLView(); const tNode = getTNode(elementIndex, lView); const context = getClassesContext(tNode); + const directiveIndex = getActiveDirectiveStylingIndex(); // if a value is interpolated then it may render a `NO_CHANGE` value. // in this case we do not need to do anything, but the binding index @@ -286,12 +270,13 @@ export function classMapInternal( // inputs are only evaluated from a template binding into a directive, therefore, // there should not be a situation where a directive host bindings function // evaluates the inputs (this should only happen in the template function) - if (!isHostStyling() && hasClassInput(tNode) && classes !== NO_CHANGE) { + if (!directiveIndex && hasClassInput(tNode) && classes !== NO_CHANGE) { updateDirectiveInputValue(context, lView, tNode, bindingIndex, classes, true); classes = NO_CHANGE; } - const updated = _stylingMap(elementIndex, context, bindingIndex, classes, true); + const updated = + _stylingMap(elementIndex, context, bindingIndex, classes, true, deferStylingUpdate()); if (ngDevMode) { ngDevMode.classMap++; if (updated) { @@ -308,52 +293,27 @@ export function classMapInternal( */ function _stylingMap( elementIndex: number, context: TStylingContext, bindingIndex: number, - value: {[key: string]: any} | string | null, isClassBased: boolean): boolean { - let updated = false; - + value: {[key: string]: any} | string | null, isClassBased: boolean, defer: boolean) { + activateStylingMapFeature(); const lView = getLView(); - const directiveIndex = getActiveDirectiveId(); + const tNode = getTNode(elementIndex, lView); const native = getNativeByTNode(tNode, lView) as RElement; - const oldValue = lView[bindingIndex] as StylingMapArray | null; - const hostBindingsMode = isHostStyling(); - const sanitizer = getCurrentStyleSanitizer(); - + const oldValue = lView[bindingIndex]; const valueHasChanged = hasValueChanged(oldValue, value); const stylingMapArr = value === NO_CHANGE ? NO_CHANGE : normalizeIntoStylingMap(oldValue, value, !isClassBased); - - // Direct Apply Case: bypass context resolution and apply the - // style/class map values directly to the element - if (allowDirectStyling(context, hostBindingsMode)) { - const renderer = getRenderer(tNode, lView); - updated = applyStylingMapDirectly( - renderer, context, native, lView, bindingIndex, stylingMapArr as StylingMapArray, - isClassBased ? setClass : setStyle, sanitizer, valueHasChanged); + if (isClassBased) { + updateClassBinding( + context, lView, native, null, bindingIndex, stylingMapArr, defer, valueHasChanged); } else { - // Context Resolution (or first update) Case: save the map value - // and defer to the context to flush and apply the style/class binding - // value to the element. - if (isClassBased) { - updateClassViaContext( - context, lView, native, directiveIndex, null, bindingIndex, stylingMapArr, - valueHasChanged); - } else { - updateStyleViaContext( - context, lView, native, directiveIndex, null, bindingIndex, stylingMapArr, sanitizer, - valueHasChanged); - } - - if (valueHasChanged) { - updated = true; - setElementExitFn(applyStyling); - } - - activateStylingMapFeature(); + const sanitizer = getCurrentStyleSanitizer(); + updateStyleBinding( + context, lView, native, null, bindingIndex, stylingMapArr, sanitizer, defer, + valueHasChanged); } - markStylingStateAsDirty(); - return updated; + return valueHasChanged; } /** @@ -378,15 +338,13 @@ function updateDirectiveInputValue( // even if the value has changed we may not want to emit it to the // directive input(s) in the event that it is falsy during the // first update pass. - if (newValue || isContextLocked(context, false)) { - const inputName = isClassBased ? 'class' : 'style'; - const inputs = tNode.inputs ![inputName] !; + if (newValue || isContextLocked(context)) { + const inputs = tNode.inputs ![isClassBased ? 'class' : 'style'] !; const initialValue = getInitialStylingValue(context); const value = normalizeStylingDirectiveInputValue(initialValue, newValue, isClassBased); setInputsForProperty(lView, inputs, value); } - setValue(lView, bindingIndex, newValue); - setElementExitFn(applyStyling); + lView[bindingIndex] = newValue; } } @@ -404,7 +362,7 @@ function normalizeStylingDirectiveInputValue( // we only concat values if there is an initial value, otherwise we return the value as is. // Note that this is to satisfy backwards-compatibility in Angular. - if (initialValue.length) { + if (initialValue.length > 0) { if (isClassBased) { value = concatString(initialValue, forceClassesAsString(bindingValue)); } else { @@ -419,21 +377,24 @@ function normalizeStylingDirectiveInputValue( /** * Flushes all styling code to the element. * - * This function is designed to be scheduled from any of the four styling instructions - * in this file. When called it will flush all style and class bindings to the element - * via the context resolution algorithm. + * This function is designed to be called from the template and hostBindings + * functions and may be called multiple times depending whether multiple + * sources of styling exist. If called multiple times, only the last call + * to `stlyingApply()` will render styling to the element. + * + * @codeGenApi */ -function applyStyling(): void { +export function ɵɵstylingApply() { const elementIndex = getSelectedIndex(); const lView = getLView(); const tNode = getTNode(elementIndex, lView); const renderer = getRenderer(tNode, lView); const native = getNativeByTNode(tNode, lView) as RElement; + const directiveIndex = getActiveDirectiveStylingIndex(); const sanitizer = getCurrentStyleSanitizer(); - const classesContext = isStylingContext(tNode.classes) ? tNode.classes as TStylingContext : null; - const stylesContext = isStylingContext(tNode.styles) ? tNode.styles as TStylingContext : null; flushStyling( - renderer, lView, classesContext, stylesContext, native, getActiveDirectiveId(), sanitizer); + renderer, lView, getClassesContext(tNode), getStylesContext(tNode), native, directiveIndex, + sanitizer); setCurrentStyleSanitizer(null); } @@ -456,12 +417,12 @@ export function registerInitialStylingOnTNode( if (typeof attr == 'number') { mode = attr; } else if (mode == AttributeMarker.Classes) { - classes = classes || allocStylingMapArray(); + classes = classes || ['']; addItemToStylingMap(classes, attr, true); hasAdditionalInitialStyling = true; } else if (mode == AttributeMarker.Styles) { const value = attrs[++i] as string | null; - styles = styles || allocStylingMapArray(); + styles = styles || ['']; addItemToStylingMap(styles, attr, value); hasAdditionalInitialStyling = true; } @@ -489,6 +450,33 @@ function updateRawValueOnContext(context: TStylingContext | StylingMapArray, val stylingMapArr[StylingMapArrayIndex.RawValuePosition] = value; } +export function getActiveDirectiveStylingIndex(): number { + // whenever a directive's hostBindings function is called a uniqueId value + // is assigned. Normally this is enough to help distinguish one directive + // from another for the styling context, but there are situations where a + // sub-class directive could inherit and assign styling in concert with a + // parent directive. To help the styling code distinguish between a parent + // sub-classed directive the inheritance depth is taken into account as well. + return getActiveDirectiveId() + getActiveDirectiveSuperClassDepth(); +} + +/** + * Temporary function that will update the max directive index value in + * both the classes and styles contexts present on the provided `tNode`. + * + * This code is only used because the `select(n)` code functionality is not + * yet 100% functional. The `select(n)` instruction cannot yet evaluate host + * bindings function code in sync with the associated template function code. + * For this reason the styling algorithm needs to track the last directive index + * value so that it knows exactly when to render styling to the element since + * `stylingApply()` is called multiple times per CD (`stylingApply` will be + * removed once `select(n)` is fixed). + */ +function updateLastDirectiveIndex(tNode: TNode, directiveIndex: number) { + _updateLastDirectiveIndex(getClassesContext(tNode), directiveIndex); + _updateLastDirectiveIndex(getStylesContext(tNode), directiveIndex); +} + function getStylesContext(tNode: TNode): TStylingContext { return getContext(tNode, false); } @@ -500,10 +488,10 @@ function getClassesContext(tNode: TNode): TStylingContext { /** * Returns/instantiates a styling context from/to a `tNode` instance. */ -function getContext(tNode: TNode, isClassBased: boolean): TStylingContext { +function getContext(tNode: TNode, isClassBased: boolean) { let context = isClassBased ? tNode.classes : tNode.styles; if (!isStylingContext(context)) { - context = allocTStylingContext(context as StylingMapArray | null); + context = allocTStylingContext(context); if (ngDevMode) { attachStylingDebugObject(context as TStylingContext); } @@ -538,14 +526,18 @@ function resolveStylePropValue( return resolvedValue; } -function markStylingStateAsDirty(): void { - setActiveElementFlag(ActiveElementFlags.ResetStylesOnExit); -} - /** - * Whether or not the style/class binding being applied was executed within a host bindings - * function. + * Whether or not a style/class binding update should be applied later. + * + * This function will decide whether a binding should be applied immediately + * or later (just before the styles/classes are flushed to the element). The + * reason why this feature exists is because of super/sub directive inheritance. + * Angular will evaluate host bindings on the super directive first and the sub + * directive, but the styling bindings on the sub directive are of higher priority + * than the super directive. For this reason all styling bindings that take place + * in this circumstance will need to be deferred until later so that they can be + * applied together and in a different order (the algorithm handles that part). */ -function isHostStyling(): boolean { - return isHostStylingActive(getActiveDirectiveId()); +function deferStylingUpdate(): boolean { + return getActiveDirectiveSuperClassHeight() > 0; } diff --git a/packages/core/src/render3/styling_next/interfaces.ts b/packages/core/src/render3/styling_next/interfaces.ts index b2597e19d8..e98b08ae4d 100644 --- a/packages/core/src/render3/styling_next/interfaces.ts +++ b/packages/core/src/render3/styling_next/interfaces.ts @@ -26,7 +26,8 @@ import {LView} from '../interfaces/view'; * The `TStylingContext` unites all template styling bindings (i.e. * `[class]` and `[style]` bindings) as well as all host-level * styling bindings (for components and directives) together into - * a single manifest + * a single manifest. It is used each time there are one or more + * styling bindings present for an element. * * The styling context is stored on a `TNode` on and there are * two instances of it: one for classes and another for styles. @@ -36,10 +37,6 @@ import {LView} from '../interfaces/view'; * tNode.classes = [ ... a context only for classes ... ]; * ``` * - * The styling context is created each time there are one or more - * styling bindings (style or class bindings) present for an element, - * but is only created once per `TNode`. - * * `tNode.styles` and `tNode.classes` can be an instance of the following: * * ```typescript @@ -65,48 +62,36 @@ import {LView} from '../interfaces/view'; * storing actual styling binding values, the lView binding index values * are stored within the context. (static nature means it is more compact.) * - * The code below shows a breakdown of two instances of `TStylingContext` - * (one for `tNode.styles` and another for `tNode.classes`): - * * ```typescript * //
// lView binding index = 22 - * // ... - * //
- * tNode.styles = [ - * 0, // the context config value (see `TStylingContextConfig`) - * 1, // the total amount of sources present (only `1` b/c there are only template - * bindings) - * [null], // initial values array (an instance of `StylingMapArray`) + * tNode.stylesContext = [ + * [], // initial values array + * 0, // the context config value * - * 0, // config entry for the property (see `TStylingContextPropConfigFlags`) - * 0b010, // template guard mask for height - * 0, // host bindings guard mask for height - * 'height', // the property name - * 22, // the binding location for the "y" binding in the lView - * null, // the default value for height + * 0b001, // guard mask for width + * 2, // total entries for width + * 'width', // the property name + * 21, // the binding location for the "x" binding in the lView + * null, * - * 0, // config entry for the property (see `TStylingContextPropConfigFlags`) - * 0b001, // template guard mask for width - * 0, // host bindings guard mask for width - * 'width', // the property name - * 21, // the binding location for the "x" binding in the lView - * null, // the default value for width + * 0b010, // guard mask for height + * 2, // total entries for height + * 'height', // the property name + * 22, // the binding location for the "y" binding in the lView + * null, * ]; * - * tNode.classes = [ - * 0, // the context config value (see `TStylingContextConfig`) - * 1, // the total amount of sources present (only `1` b/c there are only template - * bindings) - * [null], // initial values array (an instance of `StylingMapArray`) + * tNode.classesContext = [ + * [], // initial values array + * 0, // the context config value * - * 0, // config entry for the property (see `TStylingContextPropConfigFlags`) - * 0b001, // template guard mask for width - * 0, // host bindings guard mask for width - * 'active', // the property name - * 20, // the binding location for the "c" binding in the lView - * null, // the default value for the `active` class + * 0b001, // guard mask for active + * 2, // total entries for active + * 'active', // the property name + * 20, // the binding location for the "c" binding in the lView + * null, * ]; * ``` * @@ -115,25 +100,19 @@ import {LView} from '../interfaces/view'; * * ```typescript * context = [ + * CONFIG, // the styling context config value * //... - * configValue, - * templateGuardMask, - * hostBindingsGuardMask, + * guardMask, + * totalEntries, * propName, - * ...bindingIndices..., + * bindingIndices..., * defaultValue - * //... * ]; * ``` * * Below is a breakdown of each value: * - * - **configValue**: - * Property-specific configuration values. The only config setting - * that is implemented right now is whether or not to sanitize the - * value. - * - * - **templateGuardMask**: + * - **guardMask**: * A numeric value where each bit represents a binding index * location. Each binding index location is assigned based on * a local counter value that increments each time an instruction @@ -157,23 +136,18 @@ import {LView} from '../interfaces/view'; * efficient way to flip all bits on the mask when a special kind * of caching scenario occurs or when there are more than 32 bindings). * - * - **hostBindingsGuardMask**: - * Another instance of a guard mask that is specific to host bindings. - * This behaves exactly the same way as does the `templateGuardMask`, - * but will not contain any binding information processed in the template. - * The reason why there are two instances of guard masks (one for the - * template and another for host bindings) is because the template bindings - * are processed before host bindings and the state information is not - * carried over into the host bindings code. As soon as host bindings are - * processed for an element the counter and state-based bit mask values are - * set to `0`. + * - **totalEntries**: + * Each property present in the contains various binding sources of + * where the styling data could come from. This includes template + * level bindings, directive/component host bindings as well as the + * default value (or static value) all writing to the same property. + * This value depicts how many binding source entries exist for the + * property. * - * ``` - *
// binding index = 31 (counter index = 1) - * ``` + * The reason why the totalEntries value is needed is because the + * styling context is dynamic in size and it's not possible + * for the flushing or update algorithms to know when and where + * a property starts and ends without it. * * - **propName**: * The CSS property name or class name (e.g `width` or `active`). @@ -191,20 +165,15 @@ import {LView} from '../interfaces/view'; * - **defaultValue**: * This is the default that will always be applied to the element if * and when all other binding sources return a result that is null. - * Usually this value is `null` but it can also be a static value that + * Usually this value is null but it can also be a static value that * is intercepted when the tNode is first constructured (e.g. * `
` has a default value of `200px` for * the `width` property). * * Each time a new binding is encountered it is registered into the * context. The context then is continually updated until the first - * styling apply call has been called (which is automatically scheduled - * to be called once an element exits during change detection). Note that - * each entry in the context is stored in alphabetical order. - * - * Once styling has been flushed for the first time for an element the - * context will set as locked (this prevents bindings from being added - * to the context later on). + * styling apply call has been called (this is triggered by the + * `stylingApply()` instruction for the active element). * * # How Styles/Classes are Rendered * Each time a styling instruction (e.g. `[class.name]`, `[style.prop]`, @@ -221,24 +190,15 @@ import {LView} from '../interfaces/view'; * function updateStyleProp(prop: string, value: string) { * const lView = getLView(); * const bindingIndex = BINDING_INDEX++; - * - * // update the local counter value - * const indexForStyle = stylingState.stylesCount++; + * const indexForStyle = localStylesCounter++; * if (lView[bindingIndex] !== value) { * lView[bindingIndex] = value; - * - * // tell the local state that we have updated a style value - * // by updating the bit mask - * stylingState.bitMaskForStyles |= 1 << indexForStyle; + * localBitMaskForStyles |= 1 << indexForStyle; * } * } * ``` * - * Once all the bindings have updated a `bitMask` value will be populated. - * This `bitMask` value is used in the apply algorithm (which is called - * context resolution). - * - * ## The Apply Algorithm (Context Resolution) + * ## The Apply Algorithm * As explained above, each time a binding updates its value, the resulting * value is stored in the `lView` array. These styling values have yet to * be flushed to the element. @@ -323,147 +283,94 @@ import {LView} from '../interfaces/view'; */ export interface TStylingContext extends Array { - /** Configuration data for the context */ - [TStylingContextIndex.ConfigPosition]: TStylingConfig; - - /** The total amount of sources present in the context */ - [TStylingContextIndex.TotalSourcesPosition]: number; - /** Initial value position for static styles */ [TStylingContextIndex.InitialStylingValuePosition]: StylingMapArray; + + /** Configuration data for the context */ + [TStylingContextIndex.ConfigPosition]: TStylingConfigFlags; + + /** Temporary value used to track directive index entries until + the old styling code is fully removed. The reason why this + is required is to figure out which directive is last and, + when encountered, trigger a styling flush to happen */ + [TStylingContextIndex.LastDirectiveIndexPosition]: number; + + /** The bit guard value for all map-based bindings on an element */ + [TStylingContextIndex.MapBindingsBitGuardPosition]: number; + + /** The total amount of map-based bindings present on an element */ + [TStylingContextIndex.MapBindingsValuesCountPosition]: number; + + /** The prop value for map-based bindings (there actually isn't a + * value at all, but this is just used in the context to avoid + * having any special code to update the binding information for + * map-based entries). */ + [TStylingContextIndex.MapBindingsPropPosition]: string; } /** - * A series of flags used to configure the config value present within an instance of - * `TStylingContext`. + * A series of flags used to configure the config value present within a + * `TStylingContext` value. */ -export const enum TStylingConfig { +export const enum TStylingConfigFlags { /** - * The initial state of the styling context config. + * The initial state of the styling context config */ - Initial = 0b0000000, + Initial = 0b0, /** - * Whether or not there are prop-based bindings present. + * A flag which marks the context as being locked. * - * Examples include: - * 1. `
` - * 2. `
` - * 3. `@HostBinding('style.prop') x` - * 4. `@HostBinding('class.prop') x` + * The styling context is constructed across an element template + * function as well as any associated hostBindings functions. When + * this occurs, the context itself is open to mutation and only once + * it has been flushed once then it will be locked for good (no extra + * bindings can be added to it). */ - HasPropBindings = 0b0000001, + Locked = 0b1, /** - * Whether or not there are map-based bindings present. + * Whether or not to store the state between updates in a global storage map. * - * Examples include: - * 1. `
` - * 2. `
` - * 3. `@HostBinding('style') x` - * 4. `@HostBinding('class') x` + * This flag helps the algorithm avoid storing all state values temporarily in + * a storage map (that lives in `state.ts`). The flag is only flipped to true if + * and when an element contains style/class bindings that exist both on the + * template-level as well as within host bindings on the same element. This is a + * rare case, and a storage map is required so that the state values can be restored + * between the template code running and the host binding code executing. */ - HasMapBindings = 0b0000010, - - /** - * Whether or not there are map-based and prop-based bindings present. - * - * Examples include: - * 1. `
` - * 2. `
` - * 3. `
` - * 4. `
` - */ - HasPropAndMapBindings = 0b0000011, - - /** - * Whether or not there are two or more sources for a single property in the context. - * - * Examples include: - * 1. prop + prop: `
` - * 2. map + prop: `
` - * 3. map + map: `
` - */ - HasCollisions = 0b0000100, - - /** - * Whether or not the context contains initial styling values. - * - * Examples include: - * 1. `
` - * 2. `
` - * 3. `@Directive({ host: { 'style': 'width:200px' } })` - * 4. `@Directive({ host: { 'class': 'one two three' } })` - */ - HasInitialStyling = 0b00001000, - - /** - * Whether or not the context contains one or more template bindings. - * - * Examples include: - * 1. `
` - * 2. `
` - * 3. `
` - * 4. `
` - */ - HasTemplateBindings = 0b00010000, - - /** - * Whether or not the context contains one or more host bindings. - * - * Examples include: - * 1. `@HostBinding('style') x` - * 2. `@HostBinding('style.width') x` - * 3. `@HostBinding('class') x` - * 4. `@HostBinding('class.name') x` - */ - HasHostBindings = 0b00100000, - - /** - * Whether or not the template bindings are allowed to be registered in the context. - * - * This flag is after one or more template-based style/class bindings were - * set and processed for an element. Once the bindings are processed then a call - * to stylingApply is issued and the lock will be put into place. - * - * Note that this is only set once. - */ - TemplateBindingsLocked = 0b01000000, - - /** - * Whether or not the host bindings are allowed to be registered in the context. - * - * This flag is after one or more host-based style/class bindings were - * set and processed for an element. Once the bindings are processed then a call - * to stylingApply is issued and the lock will be put into place. - * - * Note that this is only set once. - */ - HostBindingsLocked = 0b10000000, + PersistStateValues = 0b10, /** A Mask of all the configurations */ - Mask = 0b11111111, + Mask = 0b11, /** Total amount of configuration bits used */ - TotalBits = 8, + TotalBits = 2, } /** - * An index of position and offset values used to navigate the `TStylingContext`. + * An index of position and offset values used to natigate the `TStylingContext`. */ export const enum TStylingContextIndex { - ConfigPosition = 0, - TotalSourcesPosition = 1, - InitialStylingValuePosition = 2, - ValuesStartPosition = 3, + InitialStylingValuePosition = 0, + ConfigPosition = 1, + LastDirectiveIndexPosition = 2, + + // index/offset values for map-based entries (i.e. `[style]` + // and `[class]` bindings). + MapBindingsPosition = 3, + MapBindingsBitGuardPosition = 3, + MapBindingsValuesCountPosition = 4, + MapBindingsPropPosition = 5, + MapBindingsBindingsStartPosition = 6, // each tuple entry in the context - // (config, templateBitGuard, hostBindingBitGuard, prop, ...bindings||default-value) - ConfigOffset = 0, - TemplateBitGuardOffset = 1, - HostBindingsBitGuardOffset = 2, - PropOffset = 3, - BindingsStartOffset = 4 + // (mask, count, prop, ...bindings||default-value) + ConfigAndGuardOffset = 0, + ValuesCountOffset = 1, + PropOffset = 2, + BindingsStartOffset = 3, + MinTupleLength = 4, } /** @@ -480,8 +387,8 @@ export const enum TStylingContextPropConfigFlags { * A function used to apply or remove styling from an element for a given property. */ export interface ApplyStylingFn { - (renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string, value: any, - bindingIndex?: number|null): void; + (renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string, + value: string|null, bindingIndex?: number|null): void; } /** @@ -512,12 +419,12 @@ export interface StylingMapArray extends Array<{}|string|number|null> { * An index of position and offset points for any data stored within a `StylingMapArray` instance. */ export const enum StylingMapArrayIndex { - /** Where the values start in the array */ - ValuesStartPosition = 1, - /** The location of the raw key/value map instance used last to populate the array entries */ RawValuePosition = 0, + /** Where the values start in the array */ + ValuesStartPosition = 1, + /** The size of each property/value entry */ TupleSize = 2, @@ -551,9 +458,8 @@ export const enum StylingMapArrayIndex { */ export interface SyncStylingMapsFn { (context: TStylingContext, renderer: Renderer3|ProceduralRenderer3|null, element: RElement, - data: LStylingData, sourceIndex: number, applyStylingFn: ApplyStylingFn, - sanitizer: StyleSanitizeFn|null, mode: StylingMapsSyncMode, targetProp?: string|null, - defaultValue?: boolean|string|null): boolean; + data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn|null, + mode: StylingMapsSyncMode, targetProp?: string|null, defaultValue?: string|null): boolean; } /** @@ -571,10 +477,4 @@ export const enum StylingMapsSyncMode { /** Skip applying the target prop/value entry */ SkipTargetProp = 0b100, - - /** Iterate over inner maps map values in the context */ - RecurseInnerMaps = 0b1000, - - /** Only check to see if a value was set somewhere in each map */ - CheckValuesOnly = 0b10000, } diff --git a/packages/core/src/render3/styling_next/map_based_bindings.ts b/packages/core/src/render3/styling_next/map_based_bindings.ts index 44f0230f8f..259ca917ac 100644 --- a/packages/core/src/render3/styling_next/map_based_bindings.ts +++ b/packages/core/src/render3/styling_next/map_based_bindings.ts @@ -5,13 +5,12 @@ * 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 {unwrapSafeValue} from '../../sanitization/bypass'; import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer'; import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer'; import {setStylingMapsSyncFn} from './bindings'; import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from './interfaces'; -import {getBindingValue, getMapProp, getMapValue, getValue, getValuesCount, isStylingValueDefined} from './util'; +import {concatString, getBindingValue, getMapProp, getMapValue, getValuesCount, hyphenate, isStylingValueDefined, setMapValue} from './util'; @@ -24,13 +23,6 @@ import {getBindingValue, getMapProp, getMapValue, getValue, getValuesCount, isSt * -------- */ -/** - * Enables support for map-based styling bindings (e.g. `[style]` and `[class]` bindings). - */ -export function activateStylingMapFeature() { - setStylingMapsSyncFn(syncStylingMap); -} - /** * Used to apply styling values presently within any map-based bindings on an element. * @@ -61,7 +53,7 @@ export function activateStylingMapFeature() { * value is marked as dirty. * * Styling values are applied once CD exits the element (which happens when - * the `advance(n)` instruction is called or the template function exits). When + * the `select(n)` instruction is called or the template function exits). When * this occurs, all prop-based bindings are applied. If a map-based binding is * present then a special flushing function (called a sync function) is made * available and it will be called each time a styling property is flushed. @@ -113,14 +105,14 @@ export function activateStylingMapFeature() { */ export const syncStylingMap: SyncStylingMapsFn = (context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, - data: LStylingData, sourceIndex: number, applyStylingFn: ApplyStylingFn, - sanitizer: StyleSanitizeFn | null, mode: StylingMapsSyncMode, targetProp?: string | null, - defaultValue?: string | boolean | null): boolean => { + data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn | null, + mode: StylingMapsSyncMode, targetProp?: string | null, + defaultValue?: string | null): boolean => { let targetPropValueWasApplied = false; // once the map-based styling code is activate it is never deactivated. For this reason a // check to see if the current styling context has any map based bindings is required. - const totalMaps = getValuesCount(context); + const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition); if (totalMaps) { let runTheSyncAlgorithm = true; const loopUntilEnd = !targetProp; @@ -129,7 +121,7 @@ export const syncStylingMap: SyncStylingMapsFn = // hasn't been flagged to apply values (it only traverses values) then // there is no point in iterating over the array because nothing will // be applied to the element. - if (loopUntilEnd && (mode & StylingMapsSyncMode.ApplyAllValues) === 0) { + if (loopUntilEnd && (mode & ~StylingMapsSyncMode.ApplyAllValues)) { runTheSyncAlgorithm = false; targetPropValueWasApplied = true; } @@ -137,7 +129,7 @@ export const syncStylingMap: SyncStylingMapsFn = if (runTheSyncAlgorithm) { targetPropValueWasApplied = innerSyncStylingMap( context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp || null, - sourceIndex, defaultValue || null); + 0, defaultValue || null); } if (loopUntilEnd) { @@ -161,104 +153,83 @@ function innerSyncStylingMap( context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn | null, mode: StylingMapsSyncMode, targetProp: string | null, currentMapIndex: number, - defaultValue: string | boolean | null): boolean { - const totalMaps = getValuesCount(context) - 1; // maps have no default value - const mapsLimit = totalMaps - 1; - const recurseInnerMaps = - currentMapIndex < mapsLimit && (mode & StylingMapsSyncMode.RecurseInnerMaps) !== 0; - const checkValuesOnly = (mode & StylingMapsSyncMode.CheckValuesOnly) !== 0; - - if (checkValuesOnly) { - // inner modes do not check values ever (that can only happen - // when sourceIndex === 0) - mode &= ~StylingMapsSyncMode.CheckValuesOnly; - } - + defaultValue: string | null): boolean { let targetPropValueWasApplied = false; - if (currentMapIndex <= mapsLimit) { - let cursor = getCurrentSyncCursor(currentMapIndex); + const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition); + if (currentMapIndex < totalMaps) { const bindingIndex = getBindingValue( - context, TStylingContextIndex.ValuesStartPosition, currentMapIndex) as number; - const stylingMapArr = getValue(data, bindingIndex); + context, TStylingContextIndex.MapBindingsPosition, currentMapIndex) as number; + const stylingMapArr = data[bindingIndex] as StylingMapArray; - if (stylingMapArr) { - while (cursor < stylingMapArr.length) { - const prop = getMapProp(stylingMapArr, cursor); - const iteratedTooFar = targetProp && prop > targetProp; - const isTargetPropMatched = !iteratedTooFar && prop === targetProp; - const value = getMapValue(stylingMapArr, cursor); - const valueIsDefined = isStylingValueDefined(value); + let cursor = getCurrentSyncCursor(currentMapIndex); + while (cursor < stylingMapArr.length) { + const prop = getMapProp(stylingMapArr, cursor); + const iteratedTooFar = targetProp && prop > targetProp; + const isTargetPropMatched = !iteratedTooFar && prop === targetProp; + const value = getMapValue(stylingMapArr, cursor); + const valueIsDefined = isStylingValueDefined(value); - // the recursive code is designed to keep applying until - // it reaches or goes past the target prop. If and when - // this happens then it will stop processing values, but - // all other map values must also catch up to the same - // point. This is why a recursive call is still issued - // even if the code has iterated too far. - const innerMode = - iteratedTooFar ? mode : resolveInnerMapMode(mode, valueIsDefined, isTargetPropMatched); + // the recursive code is designed to keep applying until + // it reaches or goes past the target prop. If and when + // this happens then it will stop processing values, but + // all other map values must also catch up to the same + // point. This is why a recursive call is still issued + // even if the code has iterated too far. + const innerMode = + iteratedTooFar ? mode : resolveInnerMapMode(mode, valueIsDefined, isTargetPropMatched); + const innerProp = iteratedTooFar ? targetProp : prop; + let valueApplied = innerSyncStylingMap( + context, renderer, element, data, applyStylingFn, sanitizer, innerMode, innerProp, + currentMapIndex + 1, defaultValue); - const innerProp = iteratedTooFar ? targetProp : prop; - let valueApplied = recurseInnerMaps ? - innerSyncStylingMap( - context, renderer, element, data, applyStylingFn, sanitizer, innerMode, innerProp, - currentMapIndex + 1, defaultValue) : - false; - - if (iteratedTooFar) { - if (!targetPropValueWasApplied) { - targetPropValueWasApplied = valueApplied; - } - break; + if (iteratedTooFar) { + if (!targetPropValueWasApplied) { + targetPropValueWasApplied = valueApplied; } - - if (!valueApplied && isValueAllowedToBeApplied(mode, isTargetPropMatched)) { - valueApplied = true; - - if (!checkValuesOnly) { - const useDefault = isTargetPropMatched && !valueIsDefined; - const bindingIndexToApply = isTargetPropMatched ? bindingIndex : null; - - let finalValue: any; - if (useDefault) { - finalValue = defaultValue; - } else { - finalValue = sanitizer ? - sanitizer(prop, value, StyleSanitizeMode.ValidateAndSanitize) : - (value ? unwrapSafeValue(value) : null); - } - - applyStylingFn(renderer, element, prop, finalValue, bindingIndexToApply); - } - } - - targetPropValueWasApplied = valueApplied && isTargetPropMatched; - cursor += StylingMapArrayIndex.TupleSize; + break; } - setCurrentSyncCursor(currentMapIndex, cursor); - // this is a fallback case in the event that the styling map is `null` for this - // binding but there are other map-based bindings that need to be evaluated - // afterwards. If the `prop` value is falsy then the intention is to cycle - // through all of the properties in the remaining maps as well. If the current - // styling map is too short then there are no values to iterate over. In either - // case the follow-up maps need to be iterated over. - if (recurseInnerMaps && - (stylingMapArr.length === StylingMapArrayIndex.ValuesStartPosition || !targetProp)) { - targetPropValueWasApplied = innerSyncStylingMap( - context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp, - currentMapIndex + 1, defaultValue); + if (!valueApplied && isValueAllowedToBeApplied(mode, isTargetPropMatched)) { + const useDefault = isTargetPropMatched && !valueIsDefined; + const valueToApply = useDefault ? defaultValue : value; + const bindingIndexToApply = useDefault ? bindingIndex : null; + const finalValue = sanitizer ? + sanitizer(prop, valueToApply, StyleSanitizeMode.ValidateAndSanitize) : + valueToApply; + applyStylingFn(renderer, element, prop, finalValue, bindingIndexToApply); + valueApplied = true; } - } else if (recurseInnerMaps) { - targetPropValueWasApplied = innerSyncStylingMap( + + targetPropValueWasApplied = valueApplied && isTargetPropMatched; + cursor += StylingMapArrayIndex.TupleSize; + } + setCurrentSyncCursor(currentMapIndex, cursor); + + // this is a fallback case in the event that the styling map is `null` for this + // binding but there are other map-based bindings that need to be evaluated + // afterwards. If the `prop` value is falsy then the intention is to cycle + // through all of the properties in the remaining maps as well. If the current + // styling map is too short then there are no values to iterate over. In either + // case the follow-up maps need to be iterated over. + if (stylingMapArr.length === StylingMapArrayIndex.ValuesStartPosition || !targetProp) { + return innerSyncStylingMap( context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp, currentMapIndex + 1, defaultValue); } } + return targetPropValueWasApplied; } + +/** + * Enables support for map-based styling bindings (e.g. `[style]` and `[class]` bindings). + */ +export function activateStylingMapFeature() { + setStylingMapsSyncFn(syncStylingMap); +} + /** * Used to determine the mode for the inner recursive call. * @@ -305,8 +276,8 @@ function resolveInnerMapMode( * - But do not allow if the current prop is set to be skipped. * 2. Otherwise if the current prop is permitted then allow. */ -function isValueAllowedToBeApplied(mode: StylingMapsSyncMode, isTargetPropMatched: boolean) { - let doApplyValue = (mode & StylingMapsSyncMode.ApplyAllValues) !== 0; +function isValueAllowedToBeApplied(mode: number, isTargetPropMatched: boolean) { + let doApplyValue = (mode & StylingMapsSyncMode.ApplyAllValues) > 0; if (!doApplyValue) { if (mode & StylingMapsSyncMode.ApplyTargetProp) { doApplyValue = isTargetPropMatched; @@ -349,3 +320,126 @@ function getCurrentSyncCursor(mapIndex: number) { function setCurrentSyncCursor(mapIndex: number, indexValue: number) { MAP_CURSORS[mapIndex] = indexValue; } + +/** + * Used to convert a {key:value} map into a `StylingMapArray` array. + * + * This function will either generate a new `StylingMapArray` instance + * or it will patch the provided `newValues` map value into an + * existing `StylingMapArray` value (this only happens if `bindingValue` + * is an instance of `StylingMapArray`). + * + * If a new key/value map is provided with an old `StylingMapArray` + * value then all properties will be overwritten with their new + * values or with `null`. This means that the array will never + * shrink in size (but it will also not be created and thrown + * away whenever the {key:value} map entries change). + */ +export function normalizeIntoStylingMap( + bindingValue: null | StylingMapArray, + newValues: {[key: string]: any} | string | null | undefined, + normalizeProps?: boolean): StylingMapArray { + const stylingMapArr: StylingMapArray = Array.isArray(bindingValue) ? bindingValue : [null]; + stylingMapArr[StylingMapArrayIndex.RawValuePosition] = newValues || null; + + // because the new values may not include all the properties + // that the old ones had, all values are set to `null` before + // the new values are applied. This way, when flushed, the + // styling algorithm knows exactly what style/class values + // to remove from the element (since they are `null`). + for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length; + j += StylingMapArrayIndex.TupleSize) { + setMapValue(stylingMapArr, j, null); + } + + let props: string[]|null = null; + let map: {[key: string]: any}|undefined|null; + let allValuesTrue = false; + if (typeof newValues === 'string') { // [class] bindings allow string values + if (newValues.length) { + props = newValues.split(/\s+/); + allValuesTrue = true; + } + } else { + props = newValues ? Object.keys(newValues) : null; + map = newValues; + } + + if (props) { + for (let i = 0; i < props.length; i++) { + const prop = props[i] as string; + const newProp = normalizeProps ? hyphenate(prop) : prop; + const value = allValuesTrue ? true : map ![prop]; + addItemToStylingMap(stylingMapArr, newProp, value, true); + } + } + + return stylingMapArr; +} + +/** + * Inserts the provided item into the provided styling array at the right spot. + * + * The `StylingMapArray` type is a sorted key/value array of entries. This means + * that when a new entry is inserted it must be placed at the right spot in the + * array. This function figures out exactly where to place it. + */ +export function addItemToStylingMap( + stylingMapArr: StylingMapArray, prop: string, value: string | boolean | null, + allowOverwrite?: boolean) { + for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length; + j += StylingMapArrayIndex.TupleSize) { + const propAtIndex = getMapProp(stylingMapArr, j); + if (prop <= propAtIndex) { + let applied = false; + if (propAtIndex === prop) { + const valueAtIndex = stylingMapArr[j]; + if (allowOverwrite || !isStylingValueDefined(valueAtIndex)) { + applied = true; + setMapValue(stylingMapArr, j, value); + } + } else { + applied = true; + stylingMapArr.splice(j, 0, prop, value); + } + return applied; + } + } + + stylingMapArr.push(prop, value); + return true; +} + +/** + * Converts the provided styling map array into a string. + * + * Classes => `one two three` + * Styles => `prop:value; prop2:value2` + */ +export function stylingMapToString(map: StylingMapArray, isClassBased: boolean): string { + let str = ''; + for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length; + i += StylingMapArrayIndex.TupleSize) { + const prop = getMapProp(map, i); + const value = getMapValue(map, i) as string; + const attrValue = concatString(prop, isClassBased ? '' : value, ':'); + str = concatString(str, attrValue, isClassBased ? ' ' : '; '); + } + return str; +} + +/** + * Converts the provided styling map array into a key value map. + */ +export function stylingMapToStringMap(map: StylingMapArray | null): {[key: string]: any} { + let stringMap: {[key: string]: any} = {}; + if (map) { + for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length; + i += StylingMapArrayIndex.TupleSize) { + const prop = getMapProp(map, i); + const value = getMapValue(map, i) as string; + stringMap[prop] = value; + } + } + return stringMap; +} diff --git a/packages/core/src/render3/styling_next/state.ts b/packages/core/src/render3/styling_next/state.ts index 8cf06b3ea9..41f559196a 100644 --- a/packages/core/src/render3/styling_next/state.ts +++ b/packages/core/src/render3/styling_next/state.ts @@ -5,12 +5,12 @@ * 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 {RElement} from '../interfaces/renderer'; -import {TEMPLATE_DIRECTIVE_INDEX} from './util'; /** * -------- * + * // TODO(matsko): add updateMask info + * * This file contains all state-based logic for styling in Angular. * * Styling in Angular is evaluated with a series of styling-specific @@ -23,92 +23,79 @@ import {TEMPLATE_DIRECTIVE_INDEX} from './util'; * exited in change detection (once all the instructions are run for * that element). * + * There are, however, situations where the state-based values + * need to be stored and used at a later point. This ONLY occurs when + * there are template-level as well as host-binding-level styling + * instructions on the same element. The example below shows exactly + * what could be: + * + * ```html + * + *
+ * ``` + * + * If and when this situation occurs, the current styling state is + * stored in a storage map value and then later accessed once the + * host bindings are evaluated. Once styling for the current element + * is over then the map entry will be cleared. + * * To learn more about the algorithm see `TStylingContext`. * * -------- */ +let _stylingState: StylingState|null = null; +const _stateStorage = new Map(); + +// this value is not used outside this file and is only here +// as a caching check for when the element changes. +let _stylingElement: any = null; + /** * Used as a state reference for update values between style/class binding instructions. - * - * In addition to storing the element and bit-mask related values, the state also - * stores the `sourceIndex` value. The `sourceIndex` value is an incremented value - * that identifies what "source" (i.e. the template, a specific directive by index or - * component) is currently applying its styling bindings to the element. */ export interface StylingState { - /** The element that is currently being processed */ - element: RElement|null; - - /** The directive index that is currently active (`0` === template) */ - directiveIndex: number; - - /** The source (column) index that is currently active (`0` === template) */ - sourceIndex: number; - - /** The classes update bit mask value that is processed during each class binding */ classesBitMask: number; - - /** The classes update bit index value that is processed during each class binding */ classesIndex: number; - - /** The styles update bit mask value that is processed during each style binding */ stylesBitMask: number; - - /** The styles update bit index value that is processed during each style binding */ stylesIndex: number; } -// these values will get filled in the very first time this is accessed... -const _state: StylingState = { - element: null, - directiveIndex: -1, - sourceIndex: -1, - classesBitMask: -1, - classesIndex: -1, - stylesBitMask: -1, - stylesIndex: -1, -}; +export const STYLING_INDEX_START_VALUE = 1; +export const BIT_MASK_START_VALUE = 0; -const BIT_MASK_START_VALUE = 0; - -// the `0` start value is reserved for [map]-based entries -const INDEX_START_VALUE = 1; - -/** - * Returns (or instantiates) the styling state for the given element. - * - * Styling state is accessed and processed each time a style or class binding - * is evaluated. - * - * If and when the provided `element` doesn't match the current element in the - * state then this means that styling was recently cleared or the element has - * changed in change detection. In both cases the styling state is fully reset. - * - * If and when the provided `directiveIndex` doesn't match the current directive - * index in the state then this means that a new source has introduced itself into - * the styling code (or, in other words, another directive or component has started - * to apply its styling host bindings to the element). - */ -export function getStylingState(element: RElement, directiveIndex: number): StylingState { - if (_state.element !== element) { - _state.element = element; - _state.directiveIndex = directiveIndex; - _state.sourceIndex = directiveIndex === TEMPLATE_DIRECTIVE_INDEX ? 0 : 1; - _state.classesBitMask = BIT_MASK_START_VALUE; - _state.classesIndex = INDEX_START_VALUE; - _state.stylesBitMask = BIT_MASK_START_VALUE; - _state.stylesIndex = INDEX_START_VALUE; - } else if (_state.directiveIndex !== directiveIndex) { - _state.directiveIndex = directiveIndex; - _state.sourceIndex++; +export function getStylingState(element: any, readFromMap?: boolean): StylingState { + if (!_stylingElement || element !== _stylingElement) { + _stylingElement = element; + if (readFromMap) { + _stylingState = _stateStorage.get(element) || null; + ngDevMode && ngDevMode.stylingReadPersistedState++; + } + _stylingState = _stylingState || { + classesBitMask: BIT_MASK_START_VALUE, + classesIndex: STYLING_INDEX_START_VALUE, + stylesBitMask: BIT_MASK_START_VALUE, + stylesIndex: STYLING_INDEX_START_VALUE, + }; } - return _state; + return _stylingState !; } -/** - * Clears the styling state so that it can be used by another element's styling code. - */ export function resetStylingState() { - _state.element = null; + _stylingState = null; + _stylingElement = null; +} + +export function storeStylingState(element: any, state: StylingState) { + ngDevMode && ngDevMode.stylingWritePersistedState++; + _stateStorage.set(element, state); +} + +export function deleteStylingStateFromStorage(element: any) { + _stateStorage.delete(element); +} + +export function resetAllStylingState() { + resetStylingState(); + _stateStorage.clear(); } diff --git a/packages/core/src/render3/styling_next/styling_debug.ts b/packages/core/src/render3/styling_next/styling_debug.ts index 652ad0be3f..04ec2d90c2 100644 --- a/packages/core/src/render3/styling_next/styling_debug.ts +++ b/packages/core/src/render3/styling_next/styling_debug.ts @@ -7,13 +7,14 @@ */ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {RElement} from '../interfaces/renderer'; +import {LView} from '../interfaces/view'; import {getCurrentStyleSanitizer} from '../state'; import {attachDebugObject} from '../util/debug_utils'; -import {applyStylingViaContext} from './bindings'; -import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from './interfaces'; +import {applyStyling} from './bindings'; +import {ApplyStylingFn, LStylingData, TStylingContext, TStylingContextIndex} from './interfaces'; import {activateStylingMapFeature} from './map_based_bindings'; -import {allowDirectStyling as _allowDirectStyling, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isMapBased, isSanitizationRequired} from './util'; +import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased, isSanitizationRequired} from './util'; @@ -52,32 +53,20 @@ export interface DebugStyling { /** The associated TStylingContext instance */ context: TStylingContext; - /** Which configuration flags are active (see `TStylingContextConfig`) */ - config: { - hasMapBindings: boolean; // - hasPropBindings: boolean; // - hasCollisions: boolean; // - hasTemplateBindings: boolean; // - hasHostBindings: boolean; // - templateBindingsLocked: boolean; // - hostBindingsLocked: boolean; // - allowDirectStyling: boolean; // - }; - /** * A summarization of each style/class property - * present in the context + * present in the context. */ - summary: {[propertyName: string]: LStylingSummary}; + summary: {[key: string]: LStylingSummary}; /** * A key/value map of all styling properties and their - * runtime values + * runtime values. */ - values: {[propertyName: string]: string | number | null | boolean}; + values: {[key: string]: string | number | null | boolean}; /** - * Overrides the sanitizer used to process styles + * Overrides the sanitizer used to process styles. */ overrideSanitizer(sanitizer: StyleSanitizeFn|null): void; } @@ -94,15 +83,9 @@ export interface TStylingTupleSummary { /** * The bit guard mask that is used to compare and protect against - * styling changes when any template style/class bindings update + * styling changes when and styling bindings update */ - templateBitMask: number; - - /** - * The bit guard mask that is used to compare and protect against - * styling changes when any host style/class bindings update - */ - hostBindingsBitMask: number; + guardMask: number; /** * Whether or not the entry requires sanitization @@ -110,18 +93,18 @@ export interface TStylingTupleSummary { sanitizationRequired: boolean; /** - * The default value that will be applied if any bindings are falsy + * The default value that will be applied if any bindings are falsy. */ defaultValue: string|boolean|null; /** - * All bindingIndex sources that have been registered for this style + * All bindingIndex sources that have been registered for this style. */ sources: (number|null|string)[]; } /** - * Instantiates and attaches an instance of `TStylingContextDebug` to the provided context + * Instantiates and attaches an instance of `TStylingContextDebug` to the provided context. */ export function attachStylingDebugObject(context: TStylingContext) { const debug = new TStylingContextDebug(context); @@ -138,8 +121,7 @@ export function attachStylingDebugObject(context: TStylingContext) { class TStylingContextDebug { constructor(public readonly context: TStylingContext) {} - get isTemplateLocked() { return isContextLocked(this.context, true); } - get isHostBindingsLocked() { return isContextLocked(this.context, false); } + get isLocked() { return isContextLocked(this.context); } /** * Returns a detailed summary of each styling entry in the context. @@ -148,36 +130,30 @@ class TStylingContextDebug { */ get entries(): {[prop: string]: TStylingTupleSummary} { const context = this.context; - const totalColumns = getValuesCount(context); const entries: {[prop: string]: TStylingTupleSummary} = {}; - const start = getPropValuesStartPosition(context); + const start = TStylingContextIndex.MapBindingsPosition; let i = start; while (i < context.length) { - const prop = getProp(context, i); - const templateBitMask = getGuardMask(context, i, false); - const hostBindingsBitMask = getGuardMask(context, i, true); - const defaultValue = getDefaultValue(context, i); - const sanitizationRequired = isSanitizationRequired(context, i); - const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset; + const valuesCount = getValuesCount(context, i); + // the context may contain placeholder values which are populated ahead of time, + // but contain no actual binding values. In this situation there is no point in + // classifying this as an "entry" since no real data is stored here yet. + if (valuesCount) { + const prop = getProp(context, i); + const guardMask = getGuardMask(context, i); + const defaultValue = getDefaultValue(context, i); + const sanitizationRequired = isSanitizationRequired(context, i); + const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset; - const sources: (number | string | null)[] = []; - - for (let j = 0; j < totalColumns; j++) { - const bindingIndex = context[bindingsStartPosition + j] as number | string | null; - if (bindingIndex !== 0) { - sources.push(bindingIndex); + const sources: (number | string | null)[] = []; + for (let j = 0; j < valuesCount; j++) { + sources.push(context[bindingsStartPosition + j] as number | string | null); } + + entries[prop] = {prop, guardMask, sanitizationRequired, valuesCount, defaultValue, sources}; } - entries[prop] = { - prop, - templateBitMask, - hostBindingsBitMask, - sanitizationRequired, - valuesCount: sources.length, defaultValue, sources, - }; - - i += TStylingContextIndex.BindingsStartOffset + totalColumns; + i += TStylingContextIndex.BindingsStartOffset + valuesCount; } return entries; } @@ -215,29 +191,6 @@ export class NodeStylingDebug implements DebugStyling { return entries; } - get config() { - const hasMapBindings = hasConfig(this.context, TStylingConfig.HasMapBindings); - const hasPropBindings = hasConfig(this.context, TStylingConfig.HasPropBindings); - const hasCollisions = hasConfig(this.context, TStylingConfig.HasCollisions); - const hasTemplateBindings = hasConfig(this.context, TStylingConfig.HasTemplateBindings); - const hasHostBindings = hasConfig(this.context, TStylingConfig.HasHostBindings); - const templateBindingsLocked = hasConfig(this.context, TStylingConfig.TemplateBindingsLocked); - const hostBindingsLocked = hasConfig(this.context, TStylingConfig.HostBindingsLocked); - const allowDirectStyling = - _allowDirectStyling(this.context, false) || _allowDirectStyling(this.context, true); - - return { - hasMapBindings, // - hasPropBindings, // - hasCollisions, // - hasTemplateBindings, // - hasHostBindings, // - templateBindingsLocked, // - hostBindingsLocked, // - allowDirectStyling, // - }; - } - /** * Returns a key/value map of all the styles/classes that were last applied to the element. */ @@ -252,23 +205,16 @@ export class NodeStylingDebug implements DebugStyling { // element is only used when the styling algorithm attempts to // style the value (and we mock out the stylingApplyFn anyway). const mockElement = {} as any; - const hasMaps = hasConfig(this.context, TStylingConfig.HasMapBindings); + const hasMaps = getValuesCount(this.context, TStylingContextIndex.MapBindingsPosition) > 0; if (hasMaps) { activateStylingMapFeature(); } const mapFn: ApplyStylingFn = (renderer: any, element: RElement, prop: string, value: string | null, - bindingIndex?: number | null) => fn(prop, value, bindingIndex || null); + bindingIndex?: number | null) => { fn(prop, value, bindingIndex || null); }; const sanitizer = this._isClassBased ? null : (this._sanitizer || getCurrentStyleSanitizer()); - - // run the template bindings - applyStylingViaContext( - this.context, null, mockElement, this._data, true, mapFn, sanitizer, false); - - // and also the host bindings - applyStylingViaContext( - this.context, null, mockElement, this._data, true, mapFn, sanitizer, true); + applyStyling(this.context, null, mockElement, this._data, true, mapFn, sanitizer); } } diff --git a/packages/core/src/render3/styling_next/util.ts b/packages/core/src/render3/styling_next/util.ts index 5a26befaed..5410c8840d 100644 --- a/packages/core/src/render3/styling_next/util.ts +++ b/packages/core/src/render3/styling_next/util.ts @@ -5,35 +5,12 @@ * 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 {unwrapSafeValue} from '../../sanitization/bypass'; import {TNode, TNodeFlags} from '../interfaces/node'; import {NO_CHANGE} from '../tokens'; +import {StylingMapArray, StylingMapArrayIndex, TStylingConfigFlags, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces'; -import {LStylingData, StylingMapArray, StylingMapArrayIndex, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces'; - -export const MAP_BASED_ENTRY_PROP_NAME = '[MAP]'; -export const TEMPLATE_DIRECTIVE_INDEX = 0; - -/** - * Default fallback value for a styling binding. - * - * A value of `null` is used here which signals to the styling algorithm that - * the styling value is not present. This way if there are no other values - * detected then it will be removed once the style/class property is dirty and - * diffed within the styling algorithm present in `flushStyling`. - */ -export const DEFAULT_BINDING_VALUE = null; - -export const DEFAULT_BINDING_INDEX = 0; - -const DEFAULT_TOTAL_SOURCES = 1; - -// The first bit value reflects a map-based binding value's bit. -// The reason why it's always activated for every entry in the map -// is so that if any map-binding values update then all other prop -// based bindings will pass the guard check automatically without -// any extra code or flags. -export const DEFAULT_GUARD_MASK_VALUE = 0b1; +const MAP_BASED_ENTRY_PROP_NAME = '--MAP--'; +const TEMPLATE_DIRECTIVE_INDEX = 0; /** * Creates a new instance of the `TStylingContext`. @@ -44,83 +21,84 @@ export const DEFAULT_GUARD_MASK_VALUE = 0b1; * `TStylingContext` with the initial values (see `interfaces.ts` for more info). */ export function allocTStylingContext(initialStyling?: StylingMapArray | null): TStylingContext { - initialStyling = initialStyling || allocStylingMapArray(); + // because map-based bindings deal with a dynamic set of values, there + // is no way to know ahead of time whether or not sanitization is required. + // For this reason the configuration will always mark sanitization as active + // (this means that when map-based values are applied then sanitization will + // be checked against each property). + const mapBasedConfig = TStylingContextPropConfigFlags.SanitizationRequired; return [ - TStylingConfig.Initial, // 1) config for the styling context - DEFAULT_TOTAL_SOURCES, // 2) total amount of styling sources (template, directives, etc...) - initialStyling, // 3) initial styling values + initialStyling || [''], // empty initial-styling map value + TStylingConfigFlags.Initial, + TEMPLATE_DIRECTIVE_INDEX, + mapBasedConfig, + 0, + MAP_BASED_ENTRY_PROP_NAME, ]; } -export function allocStylingMapArray(): StylingMapArray { - return ['']; +/** + * Sets the provided directive as the last directive index in the provided `TStylingContext`. + * + * Styling in Angular can be applied from the template as well as multiple sources of + * host bindings. This means that each binding function (the template function or the + * hostBindings functions) will generate styling instructions as well as a styling + * apply function (i.e. `stylingApply()`). Because host bindings functions and the + * template function are independent from one another this means that the styling apply + * function will be called multiple times. By tracking the last directive index (which + * is what happens in this function) the styling algorithm knows exactly when to flush + * styling (which is when the last styling apply function is executed). + */ +export function updateLastDirectiveIndex( + context: TStylingContext, lastDirectiveIndex: number): void { + if (lastDirectiveIndex === TEMPLATE_DIRECTIVE_INDEX) { + const currentValue = context[TStylingContextIndex.LastDirectiveIndexPosition]; + if (currentValue > TEMPLATE_DIRECTIVE_INDEX) { + // This means that a directive or two contained a host bindings function, but + // now the template function also contains styling. When this combination of sources + // comes up then we need to tell the context to store the state between updates + // (because host bindings evaluation happens after template binding evaluation). + markContextToPersistState(context); + } + } else { + context[TStylingContextIndex.LastDirectiveIndexPosition] = lastDirectiveIndex; + } } -export function getConfig(context: TStylingContext) { +function getConfig(context: TStylingContext) { return context[TStylingContextIndex.ConfigPosition]; } -export function hasConfig(context: TStylingContext, flag: TStylingConfig) { - return (getConfig(context) & flag) !== 0; -} - -/** - * Determines whether or not to apply styles/classes directly or via context resolution. - * - * There are three cases that are matched here: - * 1. context is locked for template or host bindings (depending on `hostBindingsMode`) - * 2. There are no collisions (i.e. properties with more than one binding) - * 3. There are only "prop" or "map" bindings present, but not both - */ -export function allowDirectStyling(context: TStylingContext, hostBindingsMode: boolean): boolean { - const config = getConfig(context); - return ((config & getLockedConfig(hostBindingsMode)) !== 0) && - ((config & TStylingConfig.HasCollisions) === 0) && - ((config & TStylingConfig.HasPropAndMapBindings) !== TStylingConfig.HasPropAndMapBindings); -} - -export function setConfig(context: TStylingContext, value: TStylingConfig): void { +export function setConfig(context: TStylingContext, value: number) { context[TStylingContextIndex.ConfigPosition] = value; } -export function patchConfig(context: TStylingContext, flag: TStylingConfig): void { - context[TStylingContextIndex.ConfigPosition] |= flag; -} - -export function getProp(context: TStylingContext, index: number): string { +export function getProp(context: TStylingContext, index: number) { return context[index + TStylingContextIndex.PropOffset] as string; } function getPropConfig(context: TStylingContext, index: number): number { - return (context[index + TStylingContextIndex.ConfigOffset] as number) & + return (context[index + TStylingContextIndex.ConfigAndGuardOffset] as number) & TStylingContextPropConfigFlags.Mask; } -export function isSanitizationRequired(context: TStylingContext, index: number): boolean { - return (getPropConfig(context, index) & TStylingContextPropConfigFlags.SanitizationRequired) !== - 0; +export function isSanitizationRequired(context: TStylingContext, index: number) { + return (getPropConfig(context, index) & TStylingContextPropConfigFlags.SanitizationRequired) > 0; } -export function getGuardMask( - context: TStylingContext, index: number, isHostBinding: boolean): number { - const position = index + (isHostBinding ? TStylingContextIndex.HostBindingsBitGuardOffset : - TStylingContextIndex.TemplateBitGuardOffset); - return context[position] as number; +export function getGuardMask(context: TStylingContext, index: number) { + const configGuardValue = context[index + TStylingContextIndex.ConfigAndGuardOffset] as number; + return configGuardValue >> TStylingContextPropConfigFlags.TotalBits; } -export function setGuardMask( - context: TStylingContext, index: number, maskValue: number, isHostBinding: boolean) { - const position = index + (isHostBinding ? TStylingContextIndex.HostBindingsBitGuardOffset : - TStylingContextIndex.TemplateBitGuardOffset); - context[position] = maskValue; +export function setGuardMask(context: TStylingContext, index: number, maskValue: number) { + const config = getPropConfig(context, index); + const guardMask = maskValue << TStylingContextPropConfigFlags.TotalBits; + context[index + TStylingContextIndex.ConfigAndGuardOffset] = config | guardMask; } -export function getValuesCount(context: TStylingContext): number { - return getTotalSources(context) + 1; -} - -export function getTotalSources(context: TStylingContext): number { - return context[TStylingContextIndex.TotalSourcesPosition]; +export function getValuesCount(context: TStylingContext, index: number) { + return context[index + TStylingContextIndex.ValuesCountOffset] as number; } export function getBindingValue(context: TStylingContext, index: number, offset: number) { @@ -128,44 +106,39 @@ export function getBindingValue(context: TStylingContext, index: number, offset: } export function getDefaultValue(context: TStylingContext, index: number): string|boolean|null { - return context[index + TStylingContextIndex.BindingsStartOffset + getTotalSources(context)] as - string | + const valuesCount = getValuesCount(context, index); + return context[index + TStylingContextIndex.BindingsStartOffset + valuesCount - 1] as string | boolean | null; } -export function setDefaultValue( - context: TStylingContext, index: number, value: string | boolean | null) { - return context[index + TStylingContextIndex.BindingsStartOffset + getTotalSources(context)] = - value; +/** + * Temporary function which determines whether or not a context is + * allowed to be flushed based on the provided directive index. + */ +export function allowStylingFlush(context: TStylingContext | null, index: number) { + return (context && index === context[TStylingContextIndex.LastDirectiveIndexPosition]) ? true : + false; } -export function setValue(data: LStylingData, bindingIndex: number, value: any) { - data[bindingIndex] = value; +export function lockContext(context: TStylingContext) { + setConfig(context, getConfig(context) | TStylingConfigFlags.Locked); } -export function getValue(data: LStylingData, bindingIndex: number): T|null { - return bindingIndex > 0 ? data[bindingIndex] as T : null; +export function isContextLocked(context: TStylingContext): boolean { + return (getConfig(context) & TStylingConfigFlags.Locked) > 0; } -export function lockContext(context: TStylingContext, hostBindingsMode: boolean): void { - patchConfig(context, getLockedConfig(hostBindingsMode)); +export function stateIsPersisted(context: TStylingContext): boolean { + return (getConfig(context) & TStylingConfigFlags.PersistStateValues) > 0; } -export function isContextLocked(context: TStylingContext, hostBindingsMode: boolean): boolean { - return hasConfig(context, getLockedConfig(hostBindingsMode)); -} - -export function getLockedConfig(hostBindingsMode: boolean) { - return hostBindingsMode ? TStylingConfig.HostBindingsLocked : - TStylingConfig.TemplateBindingsLocked; +export function markContextToPersistState(context: TStylingContext) { + setConfig(context, getConfig(context) | TStylingConfigFlags.PersistStateValues); } export function getPropValuesStartPosition(context: TStylingContext) { - let startPosition = TStylingContextIndex.ValuesStartPosition; - if (hasConfig(context, TStylingConfig.HasMapBindings)) { - startPosition += TStylingContextIndex.BindingsStartOffset + getValuesCount(context); - } - return startPosition; + return TStylingContextIndex.MapBindingsBindingsStartPosition + + context[TStylingContextIndex.MapBindingsValuesCountPosition]; } export function isMapBased(prop: string) { @@ -224,21 +197,15 @@ export function getStylingMapArray(value: TStylingContext | StylingMapArray | nu StylingMapArray|null { return isStylingContext(value) ? (value as TStylingContext)[TStylingContextIndex.InitialStylingValuePosition] : - value as StylingMapArray; + value; } export function isStylingContext(value: TStylingContext | StylingMapArray | null): boolean { - // the StylingMapArray is in the format of [initial, prop, string, prop, string] - // and this is the defining value to distinguish between arrays - return Array.isArray(value) && value.length >= TStylingContextIndex.ValuesStartPosition && - typeof value[1] !== 'string'; -} - -export function isStylingMapArray(value: TStylingContext | StylingMapArray | null): boolean { // the StylingMapArray is in the format of [initial, prop, string, prop, string] // and this is the defining value to distinguish between arrays return Array.isArray(value) && - (typeof(value as StylingMapArray)[StylingMapArrayIndex.ValuesStartPosition] === 'string'); + value.length >= TStylingContextIndex.MapBindingsBindingsStartPosition && + typeof value[1] !== 'string'; } export function getInitialStylingValue(context: TStylingContext | StylingMapArray | null): string { @@ -258,13 +225,6 @@ export function getMapProp(map: StylingMapArray, index: number): string { return map[index + StylingMapArrayIndex.PropOffset] as string; } -const MAP_DIRTY_VALUE = - typeof ngDevMode !== 'undefined' && ngDevMode ? {} : {MAP_DIRTY_VALUE: true}; - -export function setMapAsDirty(map: StylingMapArray): void { - map[StylingMapArrayIndex.RawValuePosition] = MAP_DIRTY_VALUE; -} - export function setMapValue( map: StylingMapArray, index: number, value: string | boolean | null): void { map[index + StylingMapArrayIndex.ValueOffset] = value; @@ -293,130 +253,3 @@ export function forceStylesAsString(styles: {[key: string]: any} | null | undefi } return str; } - -export function isHostStylingActive(directiveOrSourceId: number): boolean { - return directiveOrSourceId !== TEMPLATE_DIRECTIVE_INDEX; -} - -/** - * Converts the provided styling map array into a string. - * - * Classes => `one two three` - * Styles => `prop:value; prop2:value2` - */ -export function stylingMapToString(map: StylingMapArray, isClassBased: boolean): string { - let str = ''; - for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length; - i += StylingMapArrayIndex.TupleSize) { - const prop = getMapProp(map, i); - const value = getMapValue(map, i) as string; - const attrValue = concatString(prop, isClassBased ? '' : value, ':'); - str = concatString(str, attrValue, isClassBased ? ' ' : '; '); - } - return str; -} - -/** - * Converts the provided styling map array into a key value map. - */ -export function stylingMapToStringMap(map: StylingMapArray | null): {[key: string]: any} { - let stringMap: {[key: string]: any} = {}; - if (map) { - for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length; - i += StylingMapArrayIndex.TupleSize) { - const prop = getMapProp(map, i); - const value = getMapValue(map, i) as string; - stringMap[prop] = value; - } - } - return stringMap; -} - -/** - * Inserts the provided item into the provided styling array at the right spot. - * - * The `StylingMapArray` type is a sorted key/value array of entries. This means - * that when a new entry is inserted it must be placed at the right spot in the - * array. This function figures out exactly where to place it. - */ -export function addItemToStylingMap( - stylingMapArr: StylingMapArray, prop: string, value: string | boolean | null, - allowOverwrite?: boolean) { - for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length; - j += StylingMapArrayIndex.TupleSize) { - const propAtIndex = getMapProp(stylingMapArr, j); - if (prop <= propAtIndex) { - let applied = false; - if (propAtIndex === prop) { - const valueAtIndex = stylingMapArr[j]; - if (allowOverwrite || !isStylingValueDefined(valueAtIndex)) { - applied = true; - setMapValue(stylingMapArr, j, value); - } - } else { - applied = true; - stylingMapArr.splice(j, 0, prop, value); - } - return applied; - } - } - - stylingMapArr.push(prop, value); - return true; -} - -/** - * Used to convert a {key:value} map into a `StylingMapArray` array. - * - * This function will either generate a new `StylingMapArray` instance - * or it will patch the provided `newValues` map value into an - * existing `StylingMapArray` value (this only happens if `bindingValue` - * is an instance of `StylingMapArray`). - * - * If a new key/value map is provided with an old `StylingMapArray` - * value then all properties will be overwritten with their new - * values or with `null`. This means that the array will never - * shrink in size (but it will also not be created and thrown - * away whenever the `{key:value}` map entries change). - */ -export function normalizeIntoStylingMap( - bindingValue: null | StylingMapArray, - newValues: {[key: string]: any} | string | null | undefined, - normalizeProps?: boolean): StylingMapArray { - const stylingMapArr: StylingMapArray = Array.isArray(bindingValue) ? bindingValue : [null]; - stylingMapArr[StylingMapArrayIndex.RawValuePosition] = newValues || null; - - // because the new values may not include all the properties - // that the old ones had, all values are set to `null` before - // the new values are applied. This way, when flushed, the - // styling algorithm knows exactly what style/class values - // to remove from the element (since they are `null`). - for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length; - j += StylingMapArrayIndex.TupleSize) { - setMapValue(stylingMapArr, j, null); - } - - let props: string[]|null = null; - let map: {[key: string]: any}|undefined|null; - let allValuesTrue = false; - if (typeof newValues === 'string') { // [class] bindings allow string values - if (newValues.length) { - props = newValues.split(/\s+/); - allValuesTrue = true; - } - } else { - props = newValues ? Object.keys(newValues) : null; - map = newValues; - } - - if (props) { - for (let i = 0; i < props.length; i++) { - const prop = props[i] as string; - const newProp = normalizeProps ? hyphenate(prop) : prop; - const value = allValuesTrue ? true : map ![prop]; - addItemToStylingMap(stylingMapArr, newProp, value, true); - } - } - - return stylingMapArr; -} diff --git a/packages/core/src/sanitization/bypass.ts b/packages/core/src/sanitization/bypass.ts index 612923c650..fbfc88f44c 100644 --- a/packages/core/src/sanitization/bypass.ts +++ b/packages/core/src/sanitization/bypass.ts @@ -61,7 +61,9 @@ export interface SafeResourceUrl extends SafeValue {} abstract class SafeValueImpl implements SafeValue { - constructor(public changingThisBreaksApplicationSecurity: string) {} + constructor(public changingThisBreaksApplicationSecurity: string) { + // empty + } abstract getTypeName(): string; @@ -87,10 +89,10 @@ class SafeResourceUrlImpl extends SafeValueImpl implements SafeResourceUrl { getTypeName() { return BypassType.ResourceUrl; } } -export function unwrapSafeValue(value: string | SafeValue): string { +export function unwrapSafeValue(value: SafeValue): string { return value instanceof SafeValueImpl ? (value as SafeValueImpl).changingThisBreaksApplicationSecurity : - (value as string); + ''; } diff --git a/packages/core/src/util/ng_dev_mode.ts b/packages/core/src/util/ng_dev_mode.ts index 481643d8bc..505987072b 100644 --- a/packages/core/src/util/ng_dev_mode.ts +++ b/packages/core/src/util/ng_dev_mode.ts @@ -45,6 +45,8 @@ declare global { flushStyling: number; classesApplied: number; stylesApplied: number; + stylingWritePersistedState: number; + stylingReadPersistedState: number; } } @@ -85,6 +87,8 @@ export function ngDevModeResetPerfCounters(): NgDevModePerfCounters { flushStyling: 0, classesApplied: 0, stylesApplied: 0, + stylingWritePersistedState: 0, + stylingReadPersistedState: 0, }; // Make sure to refer to ngDevMode as ['ngDevMode'] for closure. diff --git a/packages/core/test/acceptance/styling_next_spec.ts b/packages/core/test/acceptance/styling_next_spec.ts index 25e60e790f..79aea9599d 100644 --- a/packages/core/test/acceptance/styling_next_spec.ts +++ b/packages/core/test/acceptance/styling_next_spec.ts @@ -15,8 +15,6 @@ import {expect} from '@angular/platform-browser/testing/src/matchers'; import {onlyInIvy} from '@angular/private/testing'; describe('new styling integration', () => { - beforeEach(() => resetStylingCounters()); - onlyInIvy('ivy resolves styling across directives, components and templates in unison') .it('should apply single property styles/classes to the element and default to any static styling values', () => { @@ -126,74 +124,6 @@ describe('new styling integration', () => { expect(element.style.width).toEqual('600px'); }); - onlyInIvy('ivy resolves styling across directives, components and templates in unison') - .it('should only run stylingFlush once when there are no collisions between styling properties', - () => { - @Directive({selector: '[dir-with-styling]'}) - class DirWithStyling { - @HostBinding('style.font-size') public fontSize = '100px'; - } - - @Component({selector: 'comp-with-styling'}) - class CompWithStyling { - @HostBinding('style.width') public width = '900px'; - - @HostBinding('style.height') public height = '900px'; - } - - @Component({ - template: ` - ... - ` - }) - class Cmp { - opacity: string|null = '0.5'; - @ViewChild(CompWithStyling, {static: true}) - compWithStyling: CompWithStyling|null = null; - @ViewChild(DirWithStyling, {static: true}) dirWithStyling: DirWithStyling|null = null; - } - - TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]}); - const fixture = TestBed.createComponent(Cmp); - fixture.detectChanges(); - - const component = fixture.componentInstance; - const element = fixture.nativeElement.querySelector('comp-with-styling'); - const node = getDebugNode(element) !; - - const styles = node.styles !; - const config = styles.config; - expect(config.hasCollisions).toBeFalsy(); - expect(config.hasMapBindings).toBeFalsy(); - expect(config.hasPropBindings).toBeTruthy(); - expect(config.allowDirectStyling).toBeTruthy(); - - expect(element.style.opacity).toEqual('0.5'); - expect(element.style.width).toEqual('900px'); - expect(element.style.height).toEqual('900px'); - expect(element.style.fontSize).toEqual('100px'); - - // once for the template flush and again for the host bindings - expect(ngDevMode !.flushStyling).toEqual(2); - resetStylingCounters(); - - component.opacity = '0.6'; - component.compWithStyling !.height = '100px'; - component.compWithStyling !.width = '100px'; - component.dirWithStyling !.fontSize = '50px'; - fixture.detectChanges(); - - expect(element.style.opacity).toEqual('0.6'); - expect(element.style.width).toEqual('100px'); - expect(element.style.height).toEqual('100px'); - expect(element.style.fontSize).toEqual('50px'); - - // there is no need to flush styling since the styles are applied directly - expect(ngDevMode !.flushStyling).toEqual(0); - }); - onlyInIvy('ivy resolves styling across directives, components and templates in unison') .it('should combine all styling across the template, directive and component host bindings', () => { @@ -310,7 +240,6 @@ describe('new styling integration', () => { fixture.componentInstance.w3 = null; fixture.detectChanges(); - expect(styles.values).toEqual({ 'width': '200px', }); @@ -380,22 +309,16 @@ describe('new styling integration', () => { .it('should apply map-based style and class entries', () => { @Component({template: '
'}) class Cmp { - public c: {[key: string]: any}|null = null; - updateClasses(classes: string) { - const c = this.c || (this.c = {}); - Object.keys(this.c).forEach(className => { c[className] = false; }); - classes.split(/\s+/).forEach(className => { c[className] = true; }); + public c !: {[key: string]: any}; + updateClasses(prop: string) { + this.c = {...this.c || {}}; + this.c[prop] = true; } - public s: {[key: string]: any}|null = null; + public s !: {[key: string]: any}; updateStyles(prop: string, value: string|number|null) { - const s = this.s || (this.s = {}); - Object.assign(s, {[prop]: value}); - } - - reset() { - this.s = null; - this.c = null; + this.s = {...this.s || {}}; + this.s[prop] = value; } } @@ -409,47 +332,22 @@ describe('new styling integration', () => { const element = fixture.nativeElement.querySelector('div'); const node = getDebugNode(element) !; - let styles = node.styles !; - let classes = node.classes !; + const styles = node.styles !; + const classes = node.classes !; - let stylesSummary = styles.summary; - let widthSummary = stylesSummary['width']; + const stylesSummary = styles.summary; + const widthSummary = stylesSummary['width']; expect(widthSummary.prop).toEqual('width'); expect(widthSummary.value).toEqual('100px'); - let heightSummary = stylesSummary['height']; + const heightSummary = stylesSummary['height']; expect(heightSummary.prop).toEqual('height'); expect(heightSummary.value).toEqual('200px'); - let classesSummary = classes.summary; - let abcSummary = classesSummary['abc']; + const classesSummary = classes.summary; + const abcSummary = classesSummary['abc']; expect(abcSummary.prop).toEqual('abc'); - expect(abcSummary.value).toBeTruthy(); - - comp.reset(); - comp.updateStyles('width', '500px'); - comp.updateStyles('height', null); - comp.updateClasses('def'); - fixture.detectChanges(); - - styles = node.styles !; - classes = node.classes !; - - stylesSummary = styles.summary; - widthSummary = stylesSummary['width']; - expect(widthSummary.value).toEqual('500px'); - - heightSummary = stylesSummary['height']; - expect(heightSummary.value).toEqual(null); - - classesSummary = classes.summary; - abcSummary = classesSummary['abc']; - expect(abcSummary.prop).toEqual('abc'); - expect(abcSummary.value).toBeFalsy(); - - let defSummary = classesSummary['def']; - expect(defSummary.prop).toEqual('def'); - expect(defSummary.value).toBeTruthy(); + expect(abcSummary.value as any).toEqual(true); }); onlyInIvy('ivy resolves styling across directives, components and templates in unison') @@ -487,7 +385,6 @@ describe('new styling integration', () => { const node = getDebugNode(element) !; const styles = node.styles !; - expect(styles.values).toEqual({ 'width': '555px', 'color': 'red', @@ -612,8 +509,7 @@ describe('new styling integration', () => { resetStylingCounters(); fixture.detectChanges(); - // the width is applied both in TEMPLATE and in HOST_BINDINGS mode - assertStyleCounters(2, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '999px'); assertStyle(element, 'height', '123px'); @@ -621,8 +517,8 @@ describe('new styling integration', () => { resetStylingCounters(); fixture.detectChanges(); - // the width is only applied once - assertStyleCounters(1, 0); + // both are applied because the map was altered + assertStyleCounters(2, 0); assertStyle(element, 'width', '0px'); assertStyle(element, 'height', '123px'); @@ -630,8 +526,8 @@ describe('new styling integration', () => { resetStylingCounters(); fixture.detectChanges(); - // only the width and color have changed - assertStyleCounters(2, 0); + // all three are applied because the map was altered + assertStyleCounters(3, 0); assertStyle(element, 'width', '1000px'); assertStyle(element, 'height', '123px'); assertStyle(element, 'color', 'red'); @@ -640,9 +536,7 @@ describe('new styling integration', () => { resetStylingCounters(); fixture.detectChanges(); - // height gets applied twice and all other - // values get applied - assertStyleCounters(4, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '1000px'); assertStyle(element, 'height', '1000px'); assertStyle(element, 'color', 'red'); @@ -651,7 +545,8 @@ describe('new styling integration', () => { resetStylingCounters(); fixture.detectChanges(); - assertStyleCounters(5, 0); + // all four are applied because the map was altered + assertStyleCounters(4, 0); assertStyle(element, 'width', '2000px'); assertStyle(element, 'height', '1000px'); assertStyle(element, 'color', 'blue'); @@ -662,13 +557,62 @@ describe('new styling integration', () => { fixture.detectChanges(); // all four are applied because the map was altered - assertStyleCounters(4, 1); + assertStyleCounters(3, 1); assertStyle(element, 'width', '2000px'); assertStyle(element, 'height', '1000px'); assertStyle(element, 'color', 'blue'); assertStyle(element, 'opacity', ''); }); + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should only persist state values in a local map if template AND host styling is used together', + () => { + @Directive({selector: '[dir-that-sets-styling]'}) + class Dir { + @HostBinding('style.width') w = '100px'; + } + + @Component({ + template: ` +
+
+
+ ` + }) + class Cmp { + w = '200px'; + @ViewChild('a', {read: Dir, static: true}) a !: Dir; + @ViewChild('c', {read: Dir, static: true}) c !: Dir; + } + + TestBed.configureTestingModule({declarations: [Cmp, Dir]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + resetStylingCounters(); + + comp.a.w = '999px'; + comp.w = '999px'; + comp.c.w = '999px'; + fixture.detectChanges(); + expect(ngDevMode !.stylingWritePersistedState).toEqual(totalUpdates(1)); + + comp.a.w = '888px'; + fixture.detectChanges(); + expect(ngDevMode !.stylingWritePersistedState).toEqual(totalUpdates(2)); + + comp.c.w = '777px'; + fixture.detectChanges(); + expect(ngDevMode !.stylingWritePersistedState).toEqual(totalUpdates(3)); + + function totalUpdates(value: number) { + // this is doubled because detectChanges is run twice to + // see to check for checkNoChanges + return value * 2; + } + }); + onlyInIvy('only ivy has style/class bindings debugging support') .it('should sanitize style values before writing them', () => { @Component({ @@ -966,6 +910,7 @@ describe('new styling integration', () => { TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]}); const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; fixture.detectChanges(); const dirOne = fixture.nativeElement.querySelector('dir-one'); @@ -992,10 +937,10 @@ describe('new styling integration', () => { @Component({ template: ` -
-
-
- ` +
+
+
+ ` }) class Cmp { w = 100; diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index acfb5eab31..3b2ea66ad6 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.ts @@ -628,6 +628,7 @@ describe('styling', () => { expect(childDir.parent).toBeAnInstanceOf(TestDir); expect(testDirDiv.classList).not.toContain('with-button'); expect(fixture.debugElement.nativeElement.textContent).toContain('Hello'); + }); }); diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index 7b06e707f8..ffd429d6d5 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -155,9 +155,6 @@ { "name": "_currentNamespace" }, - { - "name": "_elementExitFn" - }, { "name": "_global" }, @@ -168,7 +165,7 @@ "name": "_selectedIndex" }, { - "name": "_state" + "name": "_stateStorage" }, { "name": "addComponentLogic" @@ -179,9 +176,6 @@ { "name": "addToViewTree" }, - { - "name": "allocStylingMapArray" - }, { "name": "appendChild" }, @@ -260,9 +254,6 @@ { "name": "executeContentQueries" }, - { - "name": "executeElementExitFn" - }, { "name": "executeInitAndCheckHooks" }, @@ -404,9 +395,6 @@ { "name": "getStylingMapArray" }, - { - "name": "hasActiveElementFlag" - }, { "name": "hasClassInput" }, @@ -584,6 +572,9 @@ { "name": "renderView" }, + { + "name": "resetAllStylingState" + }, { "name": "resetComponentState" }, 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 441806495d..ffd2633f81 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -134,9 +134,6 @@ { "name": "__window" }, - { - "name": "_elementExitFn" - }, { "name": "_global" }, @@ -147,7 +144,7 @@ "name": "_selectedIndex" }, { - "name": "_state" + "name": "_stateStorage" }, { "name": "addToViewTree" @@ -212,9 +209,6 @@ { "name": "executeCheckHooks" }, - { - "name": "executeElementExitFn" - }, { "name": "executeInitAndCheckHooks" }, @@ -320,9 +314,6 @@ { "name": "getSelectedIndex" }, - { - "name": "hasActiveElementFlag" - }, { "name": "hasParentInjector" }, @@ -428,6 +419,9 @@ { "name": "renderView" }, + { + "name": "resetAllStylingState" + }, { "name": "resetComponentState" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 222b255db9..aa8573af09 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -44,9 +44,6 @@ { "name": "DECLARATION_VIEW" }, - { - "name": "DEFAULT_BINDING_INDEX" - }, { "name": "DEFAULT_BINDING_VALUE" }, @@ -54,7 +51,7 @@ "name": "DEFAULT_GUARD_MASK_VALUE" }, { - "name": "DEFAULT_TOTAL_SOURCES" + "name": "DEFAULT_SIZE_VALUE" }, { "name": "DefaultIterableDiffer" @@ -95,9 +92,6 @@ { "name": "HOST" }, - { - "name": "INDEX_START_VALUE" - }, { "name": "INJECTOR" }, @@ -117,7 +111,7 @@ "name": "MAP_BASED_ENTRY_PROP_NAME" }, { - "name": "MAP_DIRTY_VALUE" + "name": "MIN_DIRECTIVE_ID" }, { "name": "MONKEY_PATCH_KEY_NAME" @@ -221,6 +215,9 @@ { "name": "STYLING_INDEX_FOR_MAP_BINDING" }, + { + "name": "STYLING_INDEX_START_VALUE" + }, { "name": "SWITCH_ELEMENT_REF_FACTORY" }, @@ -230,9 +227,6 @@ { "name": "SWITCH_VIEW_CONTAINER_REF_FACTORY" }, - { - "name": "SafeValueImpl" - }, { "name": "SkipSelf" }, @@ -404,9 +398,6 @@ { "name": "_devMode" }, - { - "name": "_elementExitFn" - }, { "name": "_global" }, @@ -417,7 +408,16 @@ "name": "_selectedIndex" }, { - "name": "_state" + "name": "_stateStorage" + }, + { + "name": "_stylingElement" + }, + { + "name": "_stylingProp" + }, + { + "name": "_stylingState" }, { "name": "_symbolIterator" @@ -425,6 +425,12 @@ { "name": "activeDirectiveId" }, + { + "name": "activeDirectiveSuperClassDepthPosition" + }, + { + "name": "activeDirectiveSuperClassHeight" + }, { "name": "addBindingIntoContext" }, @@ -434,9 +440,6 @@ { "name": "addItemToStylingMap" }, - { - "name": "addNewSourceColumn" - }, { "name": "addRemoveViewFromContainer" }, @@ -446,9 +449,6 @@ { "name": "addToViewTree" }, - { - "name": "allocStylingMapArray" - }, { "name": "allocTStylingContext" }, @@ -456,7 +456,7 @@ "name": "allocateNewContextEntry" }, { - "name": "allowDirectStyling" + "name": "allowStylingFlush" }, { "name": "appendChild" @@ -473,15 +473,6 @@ { "name": "applyStyling" }, - { - "name": "applyStylingValue" - }, - { - "name": "applyStylingValueDirectly" - }, - { - "name": "applyStylingViaContext" - }, { "name": "applyToElementOrContainer" }, @@ -536,6 +527,9 @@ { "name": "containerInternal" }, + { + "name": "contextHasUpdates" + }, { "name": "contextLView" }, @@ -593,6 +587,18 @@ { "name": "defaultScheduler" }, + { + "name": "deferBindingRegistration" + }, + { + "name": "deferStylingUpdate" + }, + { + "name": "deferredBindingQueue" + }, + { + "name": "deleteStylingStateFromStorage" + }, { "name": "destroyLView" }, @@ -626,9 +632,6 @@ { "name": "executeContentQueries" }, - { - "name": "executeElementExitFn" - }, { "name": "executeInitAndCheckHooks" }, @@ -666,10 +669,10 @@ "name": "findExistingListener" }, { - "name": "findInitialStylingValue" + "name": "findViaComponent" }, { - "name": "findViaComponent" + "name": "flushDeferredBindings" }, { "name": "flushStyling" @@ -689,6 +692,15 @@ { "name": "getActiveDirectiveId" }, + { + "name": "getActiveDirectiveStylingIndex" + }, + { + "name": "getActiveDirectiveSuperClassDepth" + }, + { + "name": "getActiveDirectiveSuperClassHeight" + }, { "name": "getBeforeNodeForView" }, @@ -737,9 +749,6 @@ { "name": "getDebugContext" }, - { - "name": "getDefaultValue" - }, { "name": "getDirectiveDef" }, @@ -779,9 +788,6 @@ { "name": "getLViewParent" }, - { - "name": "getLockedConfig" - }, { "name": "getMapProp" }, @@ -890,33 +896,21 @@ { "name": "getTViewCleanup" }, - { - "name": "getTotalSources" - }, { "name": "getTypeName" }, { "name": "getTypeNameForDebugging" }, - { - "name": "getValue" - }, { "name": "getValuesCount" }, { "name": "handleError" }, - { - "name": "hasActiveElementFlag" - }, { "name": "hasClassInput" }, - { - "name": "hasConfig" - }, { "name": "hasDirectives" }, @@ -1022,12 +1016,6 @@ { "name": "isForwardRef" }, - { - "name": "isHostStyling" - }, - { - "name": "isHostStylingActive" - }, { "name": "isJsObject" }, @@ -1100,21 +1088,24 @@ { "name": "markAsComponentHost" }, + { + "name": "markContextToPersistState" + }, { "name": "markDirty" }, { "name": "markDirtyIfOnPush" }, - { - "name": "markStylingStateAsDirty" - }, { "name": "markViewDirty" }, { "name": "matchTemplateAttribute" }, + { + "name": "maybeApplyStyling" + }, { "name": "namespaceHTMLInternal" }, @@ -1151,9 +1142,6 @@ { "name": "normalizeBitMaskValue" }, - { - "name": "patchConfig" - }, { "name": "postProcessBaseDirective" }, @@ -1217,9 +1205,6 @@ { "name": "renderDetachView" }, - { - "name": "renderHostBindingsAsStale" - }, { "name": "renderInitialStyling" }, @@ -1232,6 +1217,9 @@ { "name": "renderView" }, + { + "name": "resetAllStylingState" + }, { "name": "resetComponentState" }, @@ -1265,9 +1253,6 @@ { "name": "selectView" }, - { - "name": "setActiveElementFlag" - }, { "name": "setActiveHostElement" }, @@ -1280,6 +1265,9 @@ { "name": "setClass" }, + { + "name": "setConfig" + }, { "name": "setCurrentDirectiveDef" }, @@ -1289,15 +1277,9 @@ { "name": "setCurrentStyleSanitizer" }, - { - "name": "setDefaultValue" - }, { "name": "setDirectiveStylingInput" }, - { - "name": "setElementExitFn" - }, { "name": "setGuardMask" }, @@ -1319,9 +1301,6 @@ { "name": "setIsNotParent" }, - { - "name": "setMapAsDirty" - }, { "name": "setMapValue" }, @@ -1340,15 +1319,18 @@ { "name": "setUpAttributes" }, - { - "name": "setValue" - }, { "name": "shouldSearchParent" }, + { + "name": "stateIsPersisted" + }, { "name": "storeCleanupFn" }, + { + "name": "storeStylingState" + }, { "name": "stringify" }, @@ -1358,9 +1340,6 @@ { "name": "stylingMapToString" }, - { - "name": "stylingProp" - }, { "name": "syncViewWithBlueprint" }, @@ -1382,23 +1361,26 @@ { "name": "unwrapRNode" }, - { - "name": "unwrapSafeValue" - }, { "name": "updateBindingData" }, { - "name": "updateClassViaContext" + "name": "updateClassBinding" }, { "name": "updateInitialStylingOnContext" }, + { + "name": "updateLastDirectiveIndex" + }, + { + "name": "updateLastDirectiveIndex" + }, { "name": "updateRawValueOnContext" }, { - "name": "updateStyleViaContext" + "name": "updateStyleBinding" }, { "name": "viewAttachedToChangeDetector" @@ -1457,6 +1439,12 @@ { "name": "ɵɵrestoreView" }, + { + "name": "ɵɵstyling" + }, + { + "name": "ɵɵstylingApply" + }, { "name": "ɵɵtemplate" }, diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index d275c8cd7a..27da6ea68e 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -9,7 +9,7 @@ import {NgForOfContext} from '@angular/common'; import {ɵɵdefineComponent} from '../../src/render3/definition'; -import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index'; +import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index'; import {AttributeMarker} from '../../src/render3/interfaces/node'; import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, unwrapSafeValue} from '../../src/sanitization/bypass'; import {ɵɵdefaultStyleSanitizer, ɵɵsanitizeHtml, ɵɵsanitizeResourceUrl, ɵɵsanitizeScript, ɵɵsanitizeStyle, ɵɵsanitizeUrl} from '../../src/sanitization/sanitization'; @@ -20,7 +20,11 @@ import {NgForOf} from './common_with_def'; import {ComponentFixture, TemplateFixture} from './render_util'; describe('instructions', () => { - function createAnchor() { ɵɵelement(0, 'a'); } + function createAnchor() { + ɵɵelementStart(0, 'a'); + ɵɵstyling(); + ɵɵelementEnd(); + } function createDiv(initialClasses?: string[] | null, initialStyles?: string[] | null) { const attrs: any[] = []; @@ -30,7 +34,9 @@ describe('instructions', () => { if (initialStyles) { attrs.push(AttributeMarker.Styles, ...initialStyles); } - ɵɵelement(0, 'div', attrs); + ɵɵelementStart(0, 'div', attrs); + ɵɵstyling(); + ɵɵelementEnd(); } function createScript() { ɵɵelement(0, 'script'); } @@ -150,6 +156,7 @@ describe('instructions', () => { t.update(() => { ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer); ɵɵstyleProp('background-image', 'url("http://server")'); + ɵɵstylingApply(); }); // nothing is set because sanitizer suppresses it. expect(t.html).toEqual('
'); @@ -157,6 +164,7 @@ describe('instructions', () => { t.update(() => { ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer); ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('url("http://server2")')); + ɵɵstylingApply(); }); expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image')) .toEqual('url("http://server2")'); @@ -165,12 +173,17 @@ describe('instructions', () => { describe('styleMap', () => { function createDivWithStyle() { - ɵɵelement(0, 'div', [AttributeMarker.Styles, 'height', '10px']); + ɵɵelementStart(0, 'div', [AttributeMarker.Styles, 'height', '10px']); + ɵɵstyling(); + ɵɵelementEnd(); } it('should add style', () => { const fixture = new TemplateFixture(createDivWithStyle, () => {}, 1); - fixture.update(() => { ɵɵstyleMap({'background-color': 'red'}); }); + fixture.update(() => { + ɵɵstyleMap({'background-color': 'red'}); + ɵɵstylingApply(); + }); expect(fixture.html).toEqual('
'); }); @@ -192,6 +205,7 @@ describe('instructions', () => { 'filter': 'filter', 'width': 'width' }); + ɵɵstylingApply(); }); const props = detectedValues.sort(); @@ -202,11 +216,18 @@ describe('instructions', () => { }); describe('elementClass', () => { - function createDivWithStyling() { ɵɵelement(0, 'div'); } + function createDivWithStyling() { + ɵɵelementStart(0, 'div'); + ɵɵstyling(); + ɵɵelementEnd(); + } it('should add class', () => { const fixture = new TemplateFixture(createDivWithStyling, () => {}, 1); - fixture.update(() => { ɵɵclassMap('multiple classes'); }); + fixture.update(() => { + ɵɵclassMap('multiple classes'); + ɵɵstylingApply(); + }); expect(fixture.html).toEqual('
'); }); }); diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index b13462bbe9..87106a41d8 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -9,7 +9,7 @@ import {RendererType2} from '../../src/render/api'; import {getLContext} from '../../src/render3/context_discovery'; import {AttributeMarker, ɵɵadvance, ɵɵattribute, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵhostProperty, ɵɵproperty} from '../../src/render3/index'; -import {ɵɵallocHostVars, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all'; +import {ɵɵallocHostVars, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵprojection, ɵɵprojectionDef, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all'; import {MONKEY_PATCH_KEY_NAME} from '../../src/render3/interfaces/context'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer'; @@ -630,7 +630,12 @@ describe('element discovery', () => { vars: 0, template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { - ɵɵelement(0, 'section'); + ɵɵelementStart(0, 'section'); + ɵɵstyling(); + ɵɵelementEnd(); + } + if (rf & RenderFlags.Update) { + ɵɵstylingApply(); } } }); diff --git a/packages/core/test/render3/perf/map_based_style_and_class_bindings/index.ts b/packages/core/test/render3/perf/map_based_style_and_class_bindings/index.ts index 75b7e91586..a268540c2d 100644 --- a/packages/core/test/render3/perf/map_based_style_and_class_bindings/index.ts +++ b/packages/core/test/render3/perf/map_based_style_and_class_bindings/index.ts @@ -6,11 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵɵadvance} from '../../../../src/render3/instructions/advance'; -import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element'; +import {ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element'; import {refreshView} from '../../../../src/render3/instructions/shared'; import {RenderFlags} from '../../../../src/render3/interfaces/definition'; import {TVIEW} from '../../../../src/render3/interfaces/view'; -import {ɵɵclassMap, ɵɵstyleMap} from '../../../../src/render3/styling_next/instructions'; +import {ɵɵclassMap, ɵɵstyleMap, ɵɵstyling, ɵɵstylingApply} from '../../../../src/render3/styling_next/instructions'; import {setupRootViewWithEmbeddedViews} from '../setup'; ` @@ -30,49 +30,79 @@ import {setupRootViewWithEmbeddedViews} from '../setup'; function testTemplate(rf: RenderFlags, ctx: any) { if (rf & 1) { ɵɵelementStart(0, 'div'); - ɵɵelement(1, 'div'); - ɵɵelement(2, 'div'); - ɵɵelement(3, 'div'); - ɵɵelement(4, 'div'); - ɵɵelement(5, 'div'); - ɵɵelement(6, 'div'); - ɵɵelement(7, 'div'); - ɵɵelement(8, 'div'); - ɵɵelement(9, 'div'); - ɵɵelement(10, 'div'); + ɵɵelementStart(1, 'div'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(2, 'div'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(3, 'div'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(4, 'div'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(5, 'div'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(6, 'div'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(7, 'div'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(8, 'div'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(9, 'div'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(10, 'div'); + ɵɵstyling(); + ɵɵelementEnd(); ɵɵelementEnd(); } if (rf & 2) { ɵɵadvance(1); ɵɵstyleMap({width: '0px', height: '0px'}); ɵɵclassMap('one two'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleMap({width: '10px', height: '100px'}); ɵɵclassMap('one two'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleMap({width: '20px', height: '200px'}); ɵɵclassMap('one two'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleMap({width: '30px', height: '300px'}); ɵɵclassMap('one two'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleMap({width: '40px', height: '400px'}); ɵɵclassMap('one two'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleMap({width: '50px', height: '500px'}); ɵɵclassMap('one two'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleMap({width: '60px', height: '600px'}); ɵɵclassMap('one two'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleMap({width: '70px', height: '700px'}); ɵɵclassMap('one two'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleMap({width: '80px', height: '800px'}); ɵɵclassMap('one two'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleMap({width: '90px', height: '900px'}); ɵɵclassMap('one two'); + ɵɵstylingApply(); } } diff --git a/packages/core/test/render3/perf/style_and_class_bindings/index.ts b/packages/core/test/render3/perf/style_and_class_bindings/index.ts index 3d9c790ad9..73b89f4e8a 100644 --- a/packages/core/test/render3/perf/style_and_class_bindings/index.ts +++ b/packages/core/test/render3/perf/style_and_class_bindings/index.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵɵadvance} from '../../../../src/render3/instructions/advance'; -import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element'; +import {ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element'; import {refreshView} from '../../../../src/render3/instructions/shared'; import {RenderFlags} from '../../../../src/render3/interfaces/definition'; import {AttributeMarker} from '../../../../src/render3/interfaces/node'; import {TVIEW} from '../../../../src/render3/interfaces/view'; -import {ɵɵclassProp, ɵɵstyleProp} from '../../../../src/render3/styling_next/instructions'; +import {ɵɵclassProp, ɵɵstyleProp, ɵɵstyling, ɵɵstylingApply} from '../../../../src/render3/styling_next/instructions'; import {setupRootViewWithEmbeddedViews} from '../setup'; ` @@ -31,50 +31,89 @@ import {setupRootViewWithEmbeddedViews} from '../setup'; function testTemplate(rf: RenderFlags, ctx: any) { if (rf & 1) { ɵɵelementStart(0, 'div', [AttributeMarker.Classes, 'list']); - ɵɵelement(1, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); - ɵɵelement(2, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); - ɵɵelement(3, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); - ɵɵelement(4, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); - ɵɵelement(5, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); - ɵɵelement(6, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); - ɵɵelement(7, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); - ɵɵelement(8, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); - ɵɵelement(9, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); - ɵɵelement( + ɵɵelementStart( + 1, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart( + 2, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart( + 3, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart( + 4, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart( + 5, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart( + 6, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart( + 7, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart( + 8, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart( + 9, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart( 10, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']); + ɵɵstyling(); + ɵɵelementEnd(); ɵɵelementEnd(); } if (rf & 2) { ɵɵadvance(1); ɵɵstyleProp('width', '0px'); ɵɵclassProp('scale', true); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('width', '100px'); ɵɵclassProp('scale', true); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('width', '200px'); ɵɵclassProp('scale', true); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('width', '300px'); ɵɵclassProp('scale', true); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('width', '400px'); ɵɵclassProp('scale', true); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('width', '500px'); ɵɵclassProp('scale', true); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('width', '600px'); ɵɵclassProp('scale', true); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('width', '700px'); ɵɵclassProp('scale', true); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('width', '800px'); ɵɵclassProp('scale', true); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('width', '900px'); ɵɵclassProp('scale', true); + ɵɵstylingApply(); } } diff --git a/packages/core/test/render3/perf/style_binding/index.ts b/packages/core/test/render3/perf/style_binding/index.ts index cf713841db..795753ab7e 100644 --- a/packages/core/test/render3/perf/style_binding/index.ts +++ b/packages/core/test/render3/perf/style_binding/index.ts @@ -6,11 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵɵadvance} from '../../../../src/render3/instructions/advance'; -import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element'; +import {ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element'; import {refreshView} from '../../../../src/render3/instructions/shared'; import {RenderFlags} from '../../../../src/render3/interfaces/definition'; import {TVIEW} from '../../../../src/render3/interfaces/view'; -import {ɵɵstyleProp} from '../../../../src/render3/styling_next/instructions'; +import {ɵɵstyleProp, ɵɵstyling, ɵɵstylingApply} from '../../../../src/render3/styling_next/instructions'; import {setupRootViewWithEmbeddedViews} from '../setup'; ` @@ -30,39 +30,69 @@ import {setupRootViewWithEmbeddedViews} from '../setup'; function testTemplate(rf: RenderFlags, ctx: any) { if (rf & 1) { ɵɵelementStart(0, 'div'); - ɵɵelement(1, 'button'); - ɵɵelement(2, 'button'); - ɵɵelement(3, 'button'); - ɵɵelement(4, 'button'); - ɵɵelement(5, 'button'); - ɵɵelement(6, 'button'); - ɵɵelement(7, 'button'); - ɵɵelement(8, 'button'); - ɵɵelement(9, 'button'); - ɵɵelement(10, 'button'); + ɵɵelementStart(1, 'button'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(2, 'button'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(3, 'button'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(4, 'button'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(5, 'button'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(6, 'button'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(7, 'button'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(8, 'button'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(9, 'button'); + ɵɵstyling(); + ɵɵelementEnd(); + ɵɵelementStart(10, 'button'); + ɵɵstyling(); + ɵɵelementEnd(); ɵɵelementEnd(); } if (rf & 2) { ɵɵadvance(1); ɵɵstyleProp('background-color', 'color1'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('background-color', 'color2'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('background-color', 'color3'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('background-color', 'color4'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('background-color', 'color5'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('background-color', 'color6'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('background-color', 'color7'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('background-color', 'color8'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('background-color', 'color9'); + ɵɵstylingApply(); ɵɵadvance(1); ɵɵstyleProp('background-color', 'color10'); + ɵɵstylingApply(); } } diff --git a/packages/core/test/render3/styling_next/map_based_bindings_spec.ts b/packages/core/test/render3/styling_next/map_based_bindings_spec.ts index 99161b8d84..550adad99b 100644 --- a/packages/core/test/render3/styling_next/map_based_bindings_spec.ts +++ b/packages/core/test/render3/styling_next/map_based_bindings_spec.ts @@ -5,7 +5,7 @@ * 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 {normalizeIntoStylingMap as createMap} from '../../../src/render3/styling_next/util'; +import {normalizeIntoStylingMap as createMap} from '../../../src/render3/styling_next/map_based_bindings'; describe('map-based bindings', () => { describe('StylingMapArray construction', () => { diff --git a/packages/core/test/render3/styling_next/styling_context_spec.ts b/packages/core/test/render3/styling_next/styling_context_spec.ts index 3d26c61f9e..db23feb0ee 100644 --- a/packages/core/test/render3/styling_next/styling_context_spec.ts +++ b/packages/core/test/render3/styling_next/styling_context_spec.ts @@ -5,10 +5,8 @@ * 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 {registerBinding} from '@angular/core/src/render3/styling_next/bindings'; +import {DEFAULT_GUARD_MASK_VALUE, registerBinding} from '@angular/core/src/render3/styling_next/bindings'; import {attachStylingDebugObject} from '@angular/core/src/render3/styling_next/styling_debug'; -import {DEFAULT_GUARD_MASK_VALUE} from '@angular/core/src/render3/styling_next/util'; - import {allocTStylingContext} from '../../../src/render3/styling_next/util'; describe('styling context', () => { @@ -17,36 +15,33 @@ describe('styling context', () => { const context = debug.context; expect(debug.entries).toEqual({}); - registerBinding(context, 1, 0, 'width', '100px'); + registerBinding(context, 1, 'width', '100px'); expect(debug.entries['width']).toEqual({ prop: 'width', valuesCount: 1, sanitizationRequired: false, - templateBitMask: buildGuardMask(), - hostBindingsBitMask: buildGuardMask(), + guardMask: buildGuardMask(), defaultValue: '100px', sources: ['100px'], }); - registerBinding(context, 2, 0, 'width', 20); + registerBinding(context, 2, 'width', 20); expect(debug.entries['width']).toEqual({ prop: 'width', sanitizationRequired: false, valuesCount: 2, - templateBitMask: buildGuardMask(2), - hostBindingsBitMask: buildGuardMask(), + guardMask: buildGuardMask(2), defaultValue: '100px', sources: [20, '100px'], }); - registerBinding(context, 3, 0, 'height', 10); - registerBinding(context, 4, 1, 'height', 15); + registerBinding(context, 3, 'height', 10); + registerBinding(context, 4, 'height', 15); expect(debug.entries['height']).toEqual({ prop: 'height', valuesCount: 3, sanitizationRequired: false, - templateBitMask: buildGuardMask(3), - hostBindingsBitMask: buildGuardMask(4), + guardMask: buildGuardMask(3, 4), defaultValue: null, sources: [10, 15, null], }); @@ -57,14 +52,13 @@ describe('styling context', () => { const context = debug.context; expect(debug.entries).toEqual({}); - registerBinding(context, 1, 0, 'width', 123); - registerBinding(context, 1, 0, 'width', 123); + registerBinding(context, 1, 'width', 123); + registerBinding(context, 1, 'width', 123); expect(debug.entries['width']).toEqual({ prop: 'width', valuesCount: 2, sanitizationRequired: false, - templateBitMask: buildGuardMask(1), - hostBindingsBitMask: buildGuardMask(), + guardMask: buildGuardMask(1), defaultValue: null, sources: [123, null], }); @@ -74,36 +68,33 @@ describe('styling context', () => { const debug = makeContextWithDebug(); const context = debug.context; - registerBinding(context, 1, 0, 'width', null); + registerBinding(context, 1, 'width', null); const x = debug.entries['width']; expect(debug.entries['width']).toEqual({ prop: 'width', valuesCount: 1, sanitizationRequired: false, - templateBitMask: buildGuardMask(), - hostBindingsBitMask: buildGuardMask(), + guardMask: buildGuardMask(), defaultValue: null, sources: [null] }); - registerBinding(context, 1, 0, 'width', '100px'); + registerBinding(context, 1, 'width', '100px'); expect(debug.entries['width']).toEqual({ prop: 'width', valuesCount: 1, sanitizationRequired: false, - templateBitMask: buildGuardMask(), - hostBindingsBitMask: buildGuardMask(), + guardMask: buildGuardMask(), defaultValue: '100px', sources: ['100px'] }); - registerBinding(context, 1, 0, 'width', '200px'); + registerBinding(context, 1, 'width', '200px'); expect(debug.entries['width']).toEqual({ prop: 'width', valuesCount: 1, sanitizationRequired: false, - templateBitMask: buildGuardMask(), - hostBindingsBitMask: buildGuardMask(), + guardMask: buildGuardMask(), defaultValue: '100px', sources: ['100px'] }); diff --git a/packages/core/test/render3/styling_next/styling_debug_spec.ts b/packages/core/test/render3/styling_next/styling_debug_spec.ts index 256e5c09c9..138def5d09 100644 --- a/packages/core/test/render3/styling_next/styling_debug_spec.ts +++ b/packages/core/test/render3/styling_next/styling_debug_spec.ts @@ -18,7 +18,7 @@ describe('styling debugging tools', () => { const data: any[] = []; const d = new NodeStylingDebug(context, data); - registerBinding(context, 0, 0, 'width', null); + registerBinding(context, 0, 'width', null); expect(d.summary).toEqual({ width: { prop: 'width', @@ -27,7 +27,7 @@ describe('styling debugging tools', () => { }, }); - registerBinding(context, 0, 0, 'width', '100px'); + registerBinding(context, 0, 'width', '100px'); expect(d.summary).toEqual({ width: { prop: 'width', @@ -39,7 +39,7 @@ describe('styling debugging tools', () => { const someBindingIndex1 = 1; data[someBindingIndex1] = '200px'; - registerBinding(context, 0, 0, 'width', someBindingIndex1); + registerBinding(context, 0, 'width', someBindingIndex1); expect(d.summary).toEqual({ width: { prop: 'width', @@ -51,7 +51,7 @@ describe('styling debugging tools', () => { const someBindingIndex2 = 2; data[someBindingIndex2] = '500px'; - registerBinding(context, 0, 1, 'width', someBindingIndex2); + registerBinding(context, 0, 'width', someBindingIndex2); expect(d.summary).toEqual({ width: { prop: 'width', diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 70919a41bc..35d5cccb3a 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -1076,6 +1076,10 @@ export declare function ɵɵstylePropInterpolateV(prop: string, values: any[], v export declare function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void; +export declare function ɵɵstyling(): void; + +export declare function ɵɵstylingApply(): void; + export declare function ɵɵtemplate(index: number, templateFn: ComponentTemplate | null, consts: number, vars: number, tagName?: string | null, attrs?: TAttributes | null, localRefs?: string[] | null, localRefExtractor?: LocalRefExtractor): void; export declare function ɵɵtemplateRefExtractor(tNode: TNode, currentView: LView): ViewEngine_TemplateRef | null;