From 4f414730480b49a816ed5a2e12a606c87376fbb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 9 Sep 2019 13:14:26 -0700 Subject: [PATCH] refactor(ivy): remove styling state storage and introduce direct style writing (#32591) This patch is a final major refactor in styling Angular. This PR includes three main fixes: All temporary state taht is persisted between template style/class application and style/class application in host bindings is now removed. Removes the styling() and stylingApply() instructions. Introduces a "direct apply" mode that is used apply prop-based style/class in the event that there are no map-based bindings as well as property collisions. PR Close #32259 PR Close #32591 --- aio/scripts/_payload-limits.json | 6 +- integration/_payload-limits.json | 2 +- .../benchmarks/src/largetable/ng2/table.ts | 2 +- .../src/tree/render3_function/index.ts | 4 +- packages/common/src/directives/ng_class.ts | 4 +- packages/common/src/directives/ng_style.ts | 5 +- .../compliance/r3_compiler_compliance_spec.ts | 5 +- .../r3_view_compiler_styling_spec.ts | 107 +-- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 2 - .../compiler/src/render3/r3_identifiers.ts | 4 - .../compiler/src/render3/view/compiler.ts | 10 - .../src/render3/view/styling_builder.ts | 41 +- .../compiler/src/render3/view/template.ts | 17 +- .../core/src/core_render3_private_export.ts | 2 - packages/core/src/debug/debug_node.ts | 9 +- packages/core/src/render3/component.ts | 3 +- .../features/inherit_definition_feature.ts | 19 +- packages/core/src/render3/index.ts | 2 - .../core/src/render3/instructions/advance.ts | 13 +- .../core/src/render3/instructions/element.ts | 9 +- .../core/src/render3/instructions/shared.ts | 29 +- packages/core/src/render3/jit/environment.ts | 2 - packages/core/src/render3/state.ts | 171 ++--- .../core/src/render3/styling_next/bindings.ts | 721 ++++++++++-------- .../src/render3/styling_next/instructions.ts | 260 ++++--- .../src/render3/styling_next/interfaces.ts | 326 +++++--- .../styling_next/map_based_bindings.ts | 294 +++---- .../core/src/render3/styling_next/state.ts | 127 +-- .../src/render3/styling_next/styling_debug.ts | 124 ++- .../core/src/render3/styling_next/util.ts | 323 ++++++-- packages/core/src/sanitization/bypass.ts | 8 +- packages/core/src/util/ng_dev_mode.ts | 4 - .../core/test/acceptance/styling_next_spec.ts | 232 ++++-- packages/core/test/acceptance/styling_spec.ts | 1 - .../cyclic_import/bundle.golden_symbols.json | 17 +- .../hello_world/bundle.golden_symbols.json | 14 +- .../bundling/todo/bundle.golden_symbols.json | 177 +++-- .../core/test/render3/instructions_spec.ts | 35 +- .../core/test/render3/integration_spec.ts | 9 +- .../index.ts | 54 +- .../perf/style_and_class_bindings/index.ts | 65 +- .../test/render3/perf/style_binding/index.ts | 54 +- .../styling_next/map_based_bindings_spec.ts | 2 +- .../styling_next/styling_context_spec.ts | 43 +- .../styling_next/styling_debug_spec.ts | 8 +- tools/public_api_guard/core/core.d.ts | 4 - 46 files changed, 1756 insertions(+), 1614 deletions(-) diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json index db56e0cdf5..ecbc598ea1 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": 493330, - "main-es2015": 435296, + "main-es5": 492593, + "main-es2015": 438296, "polyfills-es5": 131024, "polyfills-es2015": 52433 } @@ -28,7 +28,7 @@ "uncompressed": { "runtime-es5": 2932, "runtime-es2015": 2938, - "main-es5": 550854, + "main-es5": 552068, "main-es2015": 493320, "polyfills-es5": 131024, "polyfills-es2015": 52433 diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index cf3b8943c8..0086b1ca34 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime": 1440, - "main": 13264, + "main": 13604, "polyfills": 45340 } } diff --git a/modules/benchmarks/src/largetable/ng2/table.ts b/modules/benchmarks/src/largetable/ng2/table.ts index 883d3af5c7..55028bf29a 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(''); + trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('white'); 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 a55c79a48c..77296b8512 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, ɵɵstyling, ɵɵstylingApply, ɵɵtext, ɵɵtextInterpolate1} from '@angular/core'; +import {ɵRenderFlags, ɵrenderComponent as renderComponent, ɵɵadvance, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵstyleProp, ɵɵtext, ɵɵtextInterpolate1} from '@angular/core'; import {bindAction, profile} from '../../util'; import {createDom, destroyDom, detectChanges} from '../render3/tree'; @@ -39,7 +39,6 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) { ɵɵelementStart(0, 'tree'); { ɵɵelementStart(1, 'span'); - ɵɵstyling(); { ɵɵtext(2); } ɵɵelementEnd(); ɵɵcontainer(3); @@ -50,7 +49,6 @@ 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 238a1a2c38..f2921d17b2 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, ɵɵstyling, ɵɵstylingApply} from '@angular/core'; +import {Directive, DoCheck, Input, ɵRenderFlags, ɵɵallocHostVars, ɵɵclassMap, ɵɵdefineDirective} from '@angular/core'; import {NgClassImpl, NgClassImplProvider} from './ng_class_impl'; @@ -35,11 +35,9 @@ 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 710b73afb9..b6bd854158 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, ɵɵstyling, ɵɵstylingApply} from '@angular/core'; +import {Directive, DoCheck, Input, ɵRenderFlags, ɵɵallocHostVars, ɵɵdefineDirective, ɵɵstyleMap} from '@angular/core'; import {NgStyleImpl, NgStyleImplProvider} from './ng_style_impl'; @@ -35,11 +35,10 @@ export const ngStyleDirectiveDef__POST_R3__ = ɵɵdefineDirective({ selectors: null as any, hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) { if (rf & ɵRenderFlags.Create) { - ɵɵstyling(); + ɵɵallocHostVars(1); } 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 ebec1d3ac2..fc6dea5f75 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -483,14 +483,11 @@ describe('compiler compliance', () => { vars: 2, template: function MyComponent_Template(rf,ctx){ if (rf & 1) { - $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div"); } 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 fa23dff617..78c48aca42 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,14 +376,11 @@ describe('compiler compliance: styling', () => { const template = ` template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div"); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap($ctx$.myStyleExp); - $r3$.ɵɵstylingApply(); } } `; @@ -442,13 +439,10 @@ describe('compiler compliance: styling', () => { vars: 2, template: function MyComponentWithInterpolation_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div"); } if (rf & 2) { $r3$.ɵɵclassMapInterpolate1("foo foo-", $ctx$.fooId, ""); - $r3$.ɵɵstylingApply(); } } … @@ -456,13 +450,10 @@ describe('compiler compliance: styling', () => { vars: 3, template: function MyComponentWithMuchosInterpolation_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div"); } if (rf & 2) { $r3$.ɵɵclassMapInterpolate2("foo foo-", $ctx$.fooId, "-", $ctx$.fooUsername, ""); - $r3$.ɵɵstylingApply(); } } … @@ -470,13 +461,10 @@ describe('compiler compliance: styling', () => { vars: 1, template: function MyComponentWithoutInterpolation_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div"); } if (rf & 2) { $r3$.ɵɵclassMap($ctx$.exp); - $r3$.ɵɵstylingApply(); } } `; @@ -522,16 +510,13 @@ describe('compiler compliance: styling', () => { vars: 4, template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelementStart(0, "div", $_c0$); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div", $_c0$); } 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); } }, @@ -572,14 +557,11 @@ describe('compiler compliance: styling', () => { vars: 1, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div"); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleProp("background-image", ctx.myImage); - $r3$.ɵɵstylingApply(); } }, encapsulation: 2 @@ -612,13 +594,10 @@ describe('compiler compliance: styling', () => { const template = ` template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div"); } if (rf & 2) { $r3$.ɵɵstyleProp("font-size", 12, "px"); - $r3$.ɵɵstylingApply(); } } `; @@ -676,13 +655,10 @@ describe('compiler compliance: styling', () => { const template = ` template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div"); } if (rf & 2) { $r3$.ɵɵclassMap($ctx$.myClassExp); - $r3$.ɵɵstylingApply(); } } `; @@ -728,15 +704,12 @@ describe('compiler compliance: styling', () => { vars: 4, template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelementStart(0, "div", $e0_attrs$); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div", $e0_attrs$); } if (rf & 2) { $r3$.ɵɵclassMap($ctx$.myClassExp); $r3$.ɵɵclassProp("apple", $ctx$.yesToApple); $r3$.ɵɵclassProp("orange", $ctx$.yesToOrange); - $r3$.ɵɵstylingApply(); $r3$.ɵɵattribute("class", "banana"); } }, @@ -844,15 +817,12 @@ describe('compiler compliance: styling', () => { const template = ` template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { - $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div"); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap($ctx$.myStyleExp); $r3$.ɵɵclassMap($ctx$.myClassExp); - $r3$.ɵɵstylingApply(); } } `; @@ -887,7 +857,6 @@ 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(); @@ -896,7 +865,6 @@ 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(); } } `; @@ -939,7 +907,6 @@ 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"); @@ -954,9 +921,8 @@ 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, ""); } } `; @@ -999,16 +965,12 @@ 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(); } } `; @@ -1058,7 +1020,6 @@ describe('compiler compliance: styling', () => { if (rf & 1) { $r3$.ɵɵallocHostVars(4); $r3$.ɵɵelementHostAttrs($e0_attrs$); - $r3$.ɵɵstyling(); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); @@ -1066,7 +1027,6 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵclassMap(ctx.myClass); $r3$.ɵɵstyleProp("color", ctx.myColorProp); $r3$.ɵɵclassProp("foo", ctx.myFooClass); - $r3$.ɵɵstylingApply(); } }, consts: 0, @@ -1118,7 +1078,6 @@ 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); @@ -1128,7 +1087,6 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵstyleProp("width", ctx.myWidthProp); $r3$.ɵɵclassProp("bar", ctx.myBarClass); $r3$.ɵɵclassProp("foo", ctx.myFooClass); - $r3$.ɵɵstylingApply(); } }, consts: 0, @@ -1179,9 +1137,7 @@ describe('compiler compliance: styling', () => { const template = ` function MyComponent_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(); - $r3$.ɵɵelementEnd(); + $r3$.ɵɵelement(0, "div"); } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); @@ -1189,7 +1145,6 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵclassMap(ctx.myClassExp); $r3$.ɵɵstyleProp("height", ctx.myHeightExp); $r3$.ɵɵclassProp("bar", ctx.myBarClassExp); - $r3$.ɵɵstylingApply(); } }, `; @@ -1198,7 +1153,6 @@ 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); @@ -1206,7 +1160,6 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵclassMap(ctx.myClassExp); $r3$.ɵɵstyleProp("width", ctx.myWidthExp); $r3$.ɵɵclassProp("foo", ctx.myFooClassExp); - $r3$.ɵɵstylingApply(); } }, `; @@ -1266,35 +1219,29 @@ 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(); } } … @@ -1336,34 +1283,24 @@ 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(); } … `; @@ -1438,34 +1375,24 @@ 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(); } … `; @@ -1496,7 +1423,6 @@ describe('compiler compliance: styling', () => { … if (rf & 2) { $r3$.ɵɵstylePropInterpolate2("width", "a", ctx.one, "b", ctx.two, "c", "px"); - $r3$.ɵɵstylingApply(); } … `; @@ -1527,7 +1453,6 @@ describe('compiler compliance: styling', () => { … if (rf & 2) { $r3$.ɵɵstylePropInterpolate2("width", "a", ctx.one, "b", ctx.two, "c"); - $r3$.ɵɵstylingApply(); } … `; @@ -1583,14 +1508,12 @@ 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(); } } `; @@ -1627,13 +1550,11 @@ 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(); } } `; @@ -1669,7 +1590,6 @@ describe('compiler compliance: styling', () => { if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleProp("background-image", ctx.bgExp); - $r3$.ɵɵstylingApply(); } … } @@ -1705,7 +1625,6 @@ describe('compiler compliance: styling', () => { if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap(ctx.mapExp); - $r3$.ɵɵstylingApply(); } … } @@ -1742,7 +1661,6 @@ describe('compiler compliance: styling', () => { if (rf & 2) { $r3$.ɵɵclassMap(ctx.mapExp); $r3$.ɵɵclassProp("name", ctx.nameExp); - $r3$.ɵɵstylingApply(); } … } @@ -1805,7 +1723,6 @@ 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 2fabaee379..c94c751234 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -1867,13 +1867,11 @@ 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 5dc424cc6d..6b648511aa 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -69,8 +69,6 @@ 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}; @@ -115,8 +113,6 @@ 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 eb4245b661..4f0c5ca0e3 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -713,16 +713,6 @@ 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 ccf82f8b3b..78c4afbe4c 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, BindingType, Interpolation} from '../../expression_parser/ast'; +import {AST, ASTWithSource, BindingPipe, 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,7 +68,6 @@ interface BoundStylingEntry { * classMap(...) * styleProp(...) * classProp(...) - * stylingApply(...) * } * * The creation/update methods within the builder class produce these instructions. @@ -81,6 +80,7 @@ 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,6 +187,7 @@ export class StylingBuilder { } this._lastStylingInput = entry; this._firstStylingInput = this._firstStylingInput || entry; + this._checkForPipes(value); this.hasBindings = true; return entry; } @@ -207,10 +208,17 @@ 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. * @@ -284,25 +292,6 @@ 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`. * @@ -422,15 +411,6 @@ 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, @@ -460,7 +440,6 @@ 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 45b888ae7e..e00bdd9c2f 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -623,11 +623,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const hasChildren = (!isI18nRootElement && this.i18n) ? !hasTextChildrenOnly(element.children) : element.children.length > 0; - const createSelfClosingInstruction = !stylingBuilder.hasBindings && + const createSelfClosingInstruction = !stylingBuilder.hasBindingsWithPipes && element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren; - - const createSelfClosingI18nInstruction = !createSelfClosingInstruction && - !stylingBuilder.hasBindings && hasTextChildrenOnly(element.children); + const createSelfClosingI18nInstruction = + !createSelfClosingInstruction && hasTextChildrenOnly(element.children); if (createSelfClosingInstruction) { this.creationInstruction( @@ -681,16 +680,6 @@ 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 32704199db..81ea351910 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -120,7 +120,6 @@ export { ɵɵelementContainerStart, ɵɵelementContainerEnd, ɵɵelementContainer, - ɵɵstyling, ɵɵstyleMap, ɵɵstyleSanitizer, ɵɵclassMap, @@ -143,7 +142,6 @@ 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 31c2051971..b1e6102f59 100644 --- a/packages/core/src/debug/debug_node.ts +++ b/packages/core/src/debug/debug_node.ts @@ -12,10 +12,9 @@ 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 {TStylingContext} from '../render3/styling_next/interfaces'; -import {stylingMapToStringMap} from '../render3/styling_next/map_based_bindings'; +import {StylingMapArray, TStylingContext} from '../render3/styling_next/interfaces'; import {NodeStylingDebug} from '../render3/styling_next/styling_debug'; -import {isStylingContext} from '../render3/styling_next/util'; +import {isStylingContext, stylingMapToStringMap} 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'; @@ -431,11 +430,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); + stylingMapToStringMap(tNode.classes as StylingMapArray | null); } else { return isStylingContext(tNode.styles) ? new NodeStylingDebug(tNode.styles as TStylingContext, lView, false).values : - stylingMapToStringMap(tNode.styles); + stylingMapToStringMap(tNode.styles as StylingMapArray | null); } } diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index cb5a26d071..bd1e0306a7 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, resetComponentState, selectView, setActiveHostElement} from './state'; +import {getPreviousOrParentTNode, incrementActiveDirectiveId, 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,6 +217,7 @@ 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 8c329b8cc0..fdee1c9a2d 100644 --- a/packages/core/src/render3/features/inherit_definition_feature.ts +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -11,7 +11,6 @@ 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'; @@ -177,24 +176,8 @@ 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) => { - // 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); - } + superHostBindings(rf, ctx, elementIndex); prevHostBindings(rf, ctx, elementIndex); }; } else { diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 1e01e3d2e5..d8aa32390d 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -114,8 +114,6 @@ 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 293764cbb3..86707b3781 100644 --- a/packages/core/src/render3/instructions/advance.ts +++ b/packages/core/src/render3/instructions/advance.ts @@ -8,7 +8,10 @@ import {assertDataInRange, assertGreaterThan} from '../../util/assert'; import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks'; import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, TVIEW} from '../interfaces/view'; -import {getCheckNoChangesMode, getLView, getSelectedIndex, setSelectedIndex} from '../state'; +import {ActiveElementFlags, executeElementExitFn, getCheckNoChangesMode, getLView, getSelectedIndex, hasActiveElementFlag, setSelectedIndex} from '../state'; +import {resetStylingState} from '../styling_next/state'; + + /** * Advances to an element for later binding instructions. @@ -51,6 +54,10 @@ 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) { @@ -69,6 +76,10 @@ 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 fd093235f6..573fc4711a 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -139,11 +139,11 @@ export function ɵɵelementEnd(): void { } } - if (hasClassInput(tNode) && tNode.classes) { + if (hasClassInput(tNode)) { setDirectiveStylingInput(tNode.classes, lView, tNode.inputs !['class']); } - if (hasStyleInput(tNode) && tNode.styles) { + if (hasStyleInput(tNode)) { setDirectiveStylingInput(tNode.styles, lView, tNode.inputs !['style']); } } @@ -237,11 +237,12 @@ export function ɵɵelementHostAttrs(attrs: TAttributes) { } function setDirectiveStylingInput( - context: TStylingContext | StylingMapArray, lView: LView, stylingInputs: (string | number)[]) { + context: TStylingContext | StylingMapArray | null, 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 = getInitialStylingValue(context) || null; + const value = (context && 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 ea8dc3d701..d3e7fd1d80 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -30,8 +30,9 @@ 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 {getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, incrementActiveDirectiveId, namespaceHTMLInternal, selectView, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state'; +import {ActiveElementFlags, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, hasActiveElementFlag, 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'; @@ -82,16 +83,18 @@ 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++; } @@ -499,6 +502,12 @@ function executeTemplate( } templateFn(rf, context); } finally { + if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { + executeElementExitFn(); + } + if (hasActiveElementFlag(ActiveElementFlags.ResetStylesOnExit)) { + resetStylingState(); + } setSelectedIndex(prevSelectedIndex); } } @@ -1094,14 +1103,10 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod const def = tView.data[i] as DirectiveDef; const directive = viewData[i]; if (def.hostBindings) { - 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. + // 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); } 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 0f7b88f3f9..8f60f5a4d8 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -116,7 +116,6 @@ 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, @@ -129,7 +128,6 @@ 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 0fbf4234ac..0699a1e42a 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 {resetAllStylingState, resetStylingState} from './styling_next/state'; +import {resetStylingState} from './styling_next/state'; /** @@ -129,31 +129,38 @@ export function getLView(): LView { * The reason why this value is `1` instead of `0` is because the `0` * value is reserved for the template. */ -const MIN_DIRECTIVE_ID = 1; - -let activeDirectiveId = MIN_DIRECTIVE_ID; +let activeDirectiveId = 0; /** - * Position depth (with respect from leaf to root) in a directive sub-class inheritance chain. + * Flags used for an active element during change detection. + * + * These flags are used within other instructions to inform cleanup or + * exit operations to run when an element is being processed. + * + * 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). */ -let activeDirectiveSuperClassDepthPosition = 0; +export const enum ActiveElementFlags { + Initial = 0b00, + RunExitFn = 0b01, + ResetStylesOnExit = 0b10, + Size = 2, +} /** - * Total count of how many directives are a part of an inheritance chain. - * - * 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 this value resets back to zero once the next directive is - * visited (when `incrementActiveDirectiveId` or `setActiveHostElement` - * is called). + * Determines whether or not a flag is currently set for the active element. */ -let activeDirectiveSuperClassHeight = 0; +export function hasActiveElementFlag(flag: ActiveElementFlags) { + return (_selectedIndex & flag) === flag; +} + +/** + * Sets a flag is for the active element. + */ +export function setActiveElementFlag(flag: ActiveElementFlags) { + _selectedIndex |= flag; +} /** * Sets the active directive host element and resets the directive id value @@ -163,14 +170,44 @@ let activeDirectiveSuperClassHeight = 0; * the directive/component instance lives */ export function setActiveHostElement(elementIndex: number | null = null) { - if (_selectedIndex !== elementIndex) { + if (getSelectedIndex() !== elementIndex) { + if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { + executeElementExitFn(); + } + if (hasActiveElementFlag(ActiveElementFlags.ResetStylesOnExit)) { + resetStylingState(); + } setSelectedIndex(elementIndex === null ? -1 : elementIndex); - activeDirectiveId = elementIndex === null ? 0 : MIN_DIRECTIVE_ID; - activeDirectiveSuperClassDepthPosition = 0; - activeDirectiveSuperClassHeight = 0; + activeDirectiveId = 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. * @@ -211,71 +248,12 @@ export function getActiveDirectiveId() { * different set of directives). */ export function incrementActiveDirectiveId() { - 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; + // 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; } /** @@ -447,10 +425,10 @@ export function resetComponentState() { elementDepthCount = 0; bindingsEnabled = true; setCurrentStyleSanitizer(null); - resetAllStylingState(); } -let _selectedIndex = -1; +/* tslint:disable */ +let _selectedIndex = -1 << ActiveElementFlags.Size; /** * Gets the most recent index passed to {@link select} @@ -459,7 +437,7 @@ let _selectedIndex = -1; * current `LView` to act on. */ export function getSelectedIndex() { - return _selectedIndex; + return _selectedIndex >> ActiveElementFlags.Size; } /** @@ -467,13 +445,12 @@ 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; - - // we have now jumped to another element - // therefore the state is stale - resetStylingState(); + _selectedIndex = index << ActiveElementFlags.Size; } diff --git a/packages/core/src/render3/styling_next/bindings.ts b/packages/core/src/render3/styling_next/bindings.ts index 1a2d78fc14..cc40299b64 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} from '../../sanitization/bypass'; +import {SafeValue, unwrapSafeValue} 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, 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'; +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'; @@ -35,13 +35,6 @@ import {allowStylingFlush, getBindingValue, getGuardMask, getMapProp, getMapValu * -------- */ -// 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. * @@ -49,26 +42,6 @@ export const DEFAULT_GUARD_MASK_VALUE = 0b1; */ 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). * @@ -79,23 +52,25 @@ let deferredBindingQueue: (TStylingContext | number | string | null | boolean)[] * state each time it's called (which then allows the `TStylingContext` * and the bit mask values to be in sync). */ -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 { +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 { const isMapBased = !prop; - const state = getStylingState(element, stateIsPersisted(context)); - const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++; + const state = getStylingState(element, directiveIndex); + const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++; if (value !== NO_CHANGE) { const updated = updateBindingData( - context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate, false); + context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, 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 << index; + state.classesBitMask |= 1 << countIndex; return true; } } @@ -112,19 +87,20 @@ export function updateClassBinding( * state each time it's called (which then allows the `TStylingContext` * and the bit mask values to be in sync). */ -export function updateStyleBinding( - context: TStylingContext, data: LStylingData, element: RElement, prop: string | null, - bindingIndex: number, +export function updateStyleViaContext( + context: TStylingContext, data: LStylingData, element: RElement, directiveIndex: number, + prop: string | null, bindingIndex: number, value: string | number | SafeValue | null | undefined | StylingMapArray | NO_CHANGE, - sanitizer: StyleSanitizeFn | null, deferRegistration: boolean, forceUpdate: boolean): boolean { + sanitizer: StyleSanitizeFn | null, forceUpdate?: boolean): boolean { const isMapBased = !prop; - const state = getStylingState(element, stateIsPersisted(context)); - const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++; + const state = getStylingState(element, directiveIndex); + const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++; if (value !== NO_CHANGE) { - const sanitizationRequired = isMapBased || + const sanitizationRequired = isMapBased ? + true : (sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false); const updated = updateBindingData( - context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate, + context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate, sanitizationRequired); if (updated || forceUpdate) { // We flip the bit in the bitMask to reflect that the binding @@ -132,7 +108,7 @@ export function updateStyleBinding( // 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 << index; + state.stylesBitMask |= 1 << countIndex; return true; } } @@ -144,8 +120,6 @@ export function updateStyleBinding( * * 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). @@ -153,74 +127,92 @@ export function updateStyleBinding( * @returns whether or not the binding value was updated in the `LStylingData`. */ function updateBindingData( - 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); - } + 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); } const changed = forceUpdate || hasValueChanged(data[bindingIndex], value); if (changed) { - data[bindingIndex] = value; + setValue(data, bindingIndex, value); + const doSetValuesAsStale = (getConfig(context) & TStylingConfig.HasHostBindings) && + !hostBindingsMode && (prop ? !value : true); + if (doSetValuesAsStale) { + renderHostBindingsAsStale(context, data, prop, !prop); + } } return changed; } /** - * Schedules a binding registration to be run at a later point. + * Iterates over all host-binding values for the given `prop` value in the context and sets their + * corresponding binding values to `null`. * - * 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. + * 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. */ -function deferBindingRegistration( - context: TStylingContext, counterIndex: number, prop: string | null, bindingIndex: number, - sanitizationRequired: boolean) { - deferredBindingQueue.unshift(context, counterIndex, prop, bindingIndex, sanitizationRequired); -} +function renderHostBindingsAsStale( + context: TStylingContext, data: LStylingData, prop: string | null, isMapBased: boolean): void { + const valuesCount = getValuesCount(context); -/** - * 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); + 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); + } + } } - 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. * @@ -246,61 +238,80 @@ function flushDeferredBindings() { * value. * * Note that this function is also used for map-based styling bindings. They are treated - * 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). + * much the same as prop-based bindings, but, their property name value is set as `[MAP]`. */ export function registerBinding( - 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; - } - i += TStylingContextIndex.BindingsStartOffset + valuesCount; - } + 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; - 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; + let totalSources = 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. + while (totalSources <= sourceIndex) { + addNewSourceColumn(context); + totalSources++; + } + + 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); + } + addBindingIntoContext(context, i, bindingValue, countId, sourceIndex); + found = true; + break; + } + i += entriesPerRow; + } + + if (!found) { + allocateNewContextEntry(context, context.length, prop, sanitizationRequired); + addBindingIntoContext(context, i, bindingValue, countId, sourceIndex); } - 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) { - // 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`) + context: TStylingContext, index: number, prop: string, sanitizationRequired?: boolean): void { const config = sanitizationRequired ? TStylingContextPropConfigFlags.SanitizationRequired : TStylingContextPropConfigFlags.Default; - context.splice(index, 0, config, DEFAULT_SIZE_VALUE, prop, DEFAULT_BINDING_VALUE); - setGuardMask(context, index, DEFAULT_GUARD_MASK_VALUE); + 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); } /** @@ -316,75 +327,70 @@ 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, 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--; - } - + context: TStylingContext, index: number, bindingValue: number | string | boolean | null, + bitIndex: number, sourceIndex: number) { if (typeof bindingValue === 'number') { - // 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; + 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); } } +/** + * 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 `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. + * 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. * - * 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: + * 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). * - * - `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). + * 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). */ export function flushStyling( renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, @@ -392,53 +398,28 @@ export function flushStyling( element: RElement, directiveIndex: number, styleSanitizer: StyleSanitizeFn | null): void { ngDevMode && ngDevMode.flushStyling++; - const persistState = classesContext ? stateIsPersisted(classesContext) : - (stylesContext ? stateIsPersisted(stylesContext) : false); - const allowFlushClasses = allowStylingFlush(classesContext, directiveIndex); - const allowFlushStyles = allowStylingFlush(stylesContext, directiveIndex); + const state = getStylingState(element, directiveIndex); + const hostBindingsMode = isHostStylingActive(state.sourceIndex); - // 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(); - } - - 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); - - if (classesFlushed && stylesFlushed) { - resetStylingState(); - if (persistState) { - deleteStylingStateFromStorage(element); + if (stylesContext) { + if (!isContextLocked(stylesContext, hostBindingsMode)) { + lockAndFinalizeContext(stylesContext, hostBindingsMode); } - } else if (persistState) { - storeStylingState(element, state); + applyStylingViaContext( + stylesContext, renderer, element, data, state.stylesBitMask, setStyle, styleSanitizer, + hostBindingsMode); } -} -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; + if (classesContext) { + if (!isContextLocked(classesContext, hostBindingsMode)) { + lockAndFinalizeContext(classesContext, hostBindingsMode); } + applyStylingViaContext( + classesContext, renderer, element, data, state.classesBitMask, setClass, null, + hostBindingsMode); } - return allowFlush; -} -function contextHasUpdates(context: TStylingContext | null, bitMask: number) { - return context && bitMask > BIT_MASK_START_VALUE; + resetStylingState(); } /** @@ -462,14 +443,50 @@ function contextHasUpdates(context: TStylingContext | null, bitMask: number) { * 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): void { - if (!isContextLocked(context)) { - const initialValues = getStylingMapArray(context); - if (initialValues) { - updateInitialStylingOnContext(context, initialValues); +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; } - lockContext(context); + } + + if (hasInitialStyling) { + patchConfig(context, TStylingConfig.HasInitialStyling); } } @@ -497,60 +514,74 @@ function lockAndFinalizeContext(context: TStylingContext): void { * algorithm works for map-based styling bindings. * * Note that this function is not designed to be called in isolation (use - * `applyClasses` and `applyStyles` to actually apply styling values). + * the `flushStyling` function so that it can call this function for both + * the styles and classes contexts). */ -export function applyStyling( +export function applyStylingViaContext( context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, bindingData: LStylingData, bitMaskValue: number | boolean, applyStylingFn: ApplyStylingFn, - sanitizer: StyleSanitizeFn | null) { + sanitizer: StyleSanitizeFn | null, hostBindingsMode: boolean): void { const bitMask = normalizeBitMaskValue(bitMaskValue); - const stylingMapsSyncFn = getStylingMapsSyncFn(); - const mapsGuardMask = getGuardMask(context, TStylingContextIndex.MapBindingsPosition); - const applyAllValues = (bitMask & mapsGuardMask) > 0; - const mapsMode = + + 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 = applyAllValues ? StylingMapsSyncMode.ApplyAllValues : StylingMapsSyncMode.TraverseValues; + if (hostBindingsMode) { + mapsMode |= StylingMapsSyncMode.RecurseInnerMaps; + totalBindingsToVisit = valuesCount - 1; + } let i = getPropValuesStartPosition(context); while (i < context.length) { - const valuesCount = getValuesCount(context, i); - const guardMask = getGuardMask(context, i); + const guardMask = getGuardMask(context, i, hostBindingsMode); if (bitMask & guardMask) { let valueApplied = false; const prop = getProp(context, i); - const valuesCountUpToDefault = valuesCount - 1; - const defaultValue = getBindingValue(context, i, valuesCountUpToDefault) as string | null; + const defaultValue = getDefaultValue(context, i); - // 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++) { + // Part 1: Visit the `[styling.prop]` value + for (let j = 0; j < totalBindingsToVisit; j++) { const bindingIndex = getBindingValue(context, i, j) as number; - 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; + 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; } } - // 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 + // Part 3: apply the default value (e.g. `
` => `200px` gets applied) // 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`). @@ -566,10 +597,92 @@ export function applyStyling( // values. For this reason, one more call to the sync function // needs to be issued at the end. if (stylingMapsSyncFn) { - stylingMapsSyncFn(context, renderer, element, bindingData, applyStylingFn, sanitizer, mapsMode); + if (hostBindingsMode) { + mapsMode |= StylingMapsSyncMode.CheckValuesOnly; + } + stylingMapsSyncFn( + context, renderer, element, bindingData, 0, 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; @@ -593,7 +706,7 @@ export function setStylingMapsSyncFn(fn: SyncStylingMapsFn) { /** * Assigns a style value to a style property for the given element. */ -const setStyle: ApplyStylingFn = +export 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 @@ -620,7 +733,7 @@ const setStyle: ApplyStylingFn = /** * Adds/removes the provided className value to the provided element. */ -const setClass: ApplyStylingFn = +export const setClass: ApplyStylingFn = (renderer: Renderer3 | null, native: RElement, className: string, value: any) => { if (className !== '') { // the reason why this may be `null` is either because @@ -666,33 +779,3 @@ 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 0ba7585067..f967d27e76 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, TVIEW} from '../interfaces/view'; -import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getCurrentStyleSanitizer, getLView, getPreviousOrParentTNode, getSelectedIndex, setCurrentStyleSanitizer} from '../state'; +import {BINDING_INDEX, LView, RENDERER} from '../interfaces/view'; +import {ActiveElementFlags, getActiveDirectiveId, getCurrentStyleSanitizer, getLView, getSelectedIndex, setActiveElementFlag, setCurrentStyleSanitizer, setElementExitFn} from '../state'; import {NO_CHANGE} from '../tokens'; import {renderStringify} from '../util/misc_utils'; import {getNativeByTNode, getTNode} from '../util/view_utils'; -import {flushStyling, updateClassBinding, updateStyleBinding} from './bindings'; +import {applyStylingMapDirectly, applyStylingValueDirectly, flushStyling, setClass, setStyle, updateClassViaContext, updateStyleViaContext} from './bindings'; import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from './interfaces'; -import {activateStylingMapFeature, addItemToStylingMap, normalizeIntoStylingMap, stylingMapToString} from './map_based_bindings'; +import {activateStylingMapFeature} from './map_based_bindings'; import {attachStylingDebugObject} from './styling_debug'; -import {allocTStylingContext, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isStylingContext, updateLastDirectiveIndex as _updateLastDirectiveIndex} from './util'; +import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isHostStylingActive, isStylingContext, normalizeIntoStylingMap, setValue, stylingMapToString} from './util'; @@ -34,34 +34,13 @@ import {allocTStylingContext, concatString, forceClassesAsString, forceStylesAsS * -------- */ -/** - * 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 - * `select(n)` is executed or the hostBindings/template function exits) + * `advance(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. @@ -92,7 +71,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. + * within a host binding function. * * @codeGenApi */ @@ -101,9 +80,15 @@ 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) { + suffix?: string | null | undefined): void { const lView = getLView(); // if a value is interpolated then it may render a `NO_CHANGE` value. @@ -112,9 +97,8 @@ export function stylePropInternal( // are stored inside of the lView. const bindingIndex = lView[BINDING_INDEX]++; - const updated = _stylingProp( - elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false, - deferStylingUpdate()); + const updated = + stylingProp(elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false); if (ngDevMode) { ngDevMode.styleProp++; if (updated) { @@ -134,7 +118,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. + * is called within a host binding function. * * @codeGenApi */ @@ -147,8 +131,7 @@ 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, deferStylingUpdate()); + const updated = stylingProp(getSelectedIndex(), bindingIndex, className, value, true); if (ngDevMode) { ngDevMode.classProp++; if (updated) { @@ -159,28 +142,57 @@ 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, defer: boolean): boolean { + isClassBased: boolean): boolean { + let updated = false; + const lView = getLView(); const tNode = getTNode(elementIndex, lView); const native = getNativeByTNode(tNode, lView) as RElement; - let valueHasChanged = false; - if (isClassBased) { - valueHasChanged = updateClassBinding( - getClassesContext(tNode), lView, native, prop, bindingIndex, - value as string | boolean | null, defer, false); + 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); } else { - const sanitizer = getCurrentStyleSanitizer(); - valueHasChanged = updateStyleBinding( - getStylesContext(tNode), lView, native, prop, bindingIndex, - value as string | SafeValue | null, sanitizer, defer, false); + // 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(); } - return valueHasChanged; + return updated; } /** @@ -207,7 +219,6 @@ 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 @@ -218,12 +229,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 (!directiveIndex && hasStyleInput(tNode) && styles !== NO_CHANGE) { + if (!isHostStyling() && hasStyleInput(tNode) && styles !== NO_CHANGE) { updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false); styles = NO_CHANGE; } - const updated = _stylingMap(index, context, bindingIndex, styles, false, deferStylingUpdate()); + const updated = _stylingMap(index, context, bindingIndex, styles, false); if (ngDevMode) { ngDevMode.styleMap++; if (updated) { @@ -254,12 +265,17 @@ 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) { + elementIndex: number, classes: {[className: string]: any} | NO_CHANGE | string | null): void { 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 @@ -270,13 +286,12 @@ 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 (!directiveIndex && hasClassInput(tNode) && classes !== NO_CHANGE) { + if (!isHostStyling() && hasClassInput(tNode) && classes !== NO_CHANGE) { updateDirectiveInputValue(context, lView, tNode, bindingIndex, classes, true); classes = NO_CHANGE; } - const updated = - _stylingMap(elementIndex, context, bindingIndex, classes, true, deferStylingUpdate()); + const updated = _stylingMap(elementIndex, context, bindingIndex, classes, true); if (ngDevMode) { ngDevMode.classMap++; if (updated) { @@ -293,27 +308,52 @@ export function classMapInternal( */ function _stylingMap( elementIndex: number, context: TStylingContext, bindingIndex: number, - value: {[key: string]: any} | string | null, isClassBased: boolean, defer: boolean) { - activateStylingMapFeature(); - const lView = getLView(); + value: {[key: string]: any} | string | null, isClassBased: boolean): boolean { + let updated = false; + const lView = getLView(); + const directiveIndex = getActiveDirectiveId(); const tNode = getTNode(elementIndex, lView); const native = getNativeByTNode(tNode, lView) as RElement; - const oldValue = lView[bindingIndex]; + const oldValue = lView[bindingIndex] as StylingMapArray | null; + const hostBindingsMode = isHostStyling(); + const sanitizer = getCurrentStyleSanitizer(); + const valueHasChanged = hasValueChanged(oldValue, value); const stylingMapArr = value === NO_CHANGE ? NO_CHANGE : normalizeIntoStylingMap(oldValue, value, !isClassBased); - if (isClassBased) { - updateClassBinding( - context, lView, native, null, bindingIndex, stylingMapArr, defer, valueHasChanged); + + // 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); } else { - const sanitizer = getCurrentStyleSanitizer(); - updateStyleBinding( - context, lView, native, null, bindingIndex, stylingMapArr, sanitizer, defer, - valueHasChanged); + // 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(); } - return valueHasChanged; + markStylingStateAsDirty(); + return updated; } /** @@ -338,13 +378,15 @@ 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)) { - const inputs = tNode.inputs ![isClassBased ? 'class' : 'style'] !; + if (newValue || isContextLocked(context, false)) { + const inputName = isClassBased ? 'class' : 'style'; + const inputs = tNode.inputs ![inputName] !; const initialValue = getInitialStylingValue(context); const value = normalizeStylingDirectiveInputValue(initialValue, newValue, isClassBased); setInputsForProperty(lView, inputs, value); } - lView[bindingIndex] = newValue; + setValue(lView, bindingIndex, newValue); + setElementExitFn(applyStyling); } } @@ -362,7 +404,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 > 0) { + if (initialValue.length) { if (isClassBased) { value = concatString(initialValue, forceClassesAsString(bindingValue)); } else { @@ -377,24 +419,21 @@ function normalizeStylingDirectiveInputValue( /** * Flushes all styling code to the element. * - * 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 + * 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. */ -export function ɵɵstylingApply() { +function applyStyling(): void { 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, getClassesContext(tNode), getStylesContext(tNode), native, directiveIndex, - sanitizer); + renderer, lView, classesContext, stylesContext, native, getActiveDirectiveId(), sanitizer); setCurrentStyleSanitizer(null); } @@ -417,12 +456,12 @@ export function registerInitialStylingOnTNode( if (typeof attr == 'number') { mode = attr; } else if (mode == AttributeMarker.Classes) { - classes = classes || ['']; + classes = classes || allocStylingMapArray(); addItemToStylingMap(classes, attr, true); hasAdditionalInitialStyling = true; } else if (mode == AttributeMarker.Styles) { const value = attrs[++i] as string | null; - styles = styles || ['']; + styles = styles || allocStylingMapArray(); addItemToStylingMap(styles, attr, value); hasAdditionalInitialStyling = true; } @@ -450,33 +489,6 @@ 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); } @@ -488,10 +500,10 @@ function getClassesContext(tNode: TNode): TStylingContext { /** * Returns/instantiates a styling context from/to a `tNode` instance. */ -function getContext(tNode: TNode, isClassBased: boolean) { +function getContext(tNode: TNode, isClassBased: boolean): TStylingContext { let context = isClassBased ? tNode.classes : tNode.styles; if (!isStylingContext(context)) { - context = allocTStylingContext(context); + context = allocTStylingContext(context as StylingMapArray | null); if (ngDevMode) { attachStylingDebugObject(context as TStylingContext); } @@ -526,18 +538,14 @@ function resolveStylePropValue( return resolvedValue; } -/** - * 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 deferStylingUpdate(): boolean { - return getActiveDirectiveSuperClassHeight() > 0; +function markStylingStateAsDirty(): void { + setActiveElementFlag(ActiveElementFlags.ResetStylesOnExit); +} + +/** + * Whether or not the style/class binding being applied was executed within a host bindings + * function. + */ +function isHostStyling(): boolean { + return isHostStylingActive(getActiveDirectiveId()); } diff --git a/packages/core/src/render3/styling_next/interfaces.ts b/packages/core/src/render3/styling_next/interfaces.ts index e98b08ae4d..b2597e19d8 100644 --- a/packages/core/src/render3/styling_next/interfaces.ts +++ b/packages/core/src/render3/styling_next/interfaces.ts @@ -26,8 +26,7 @@ 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. It is used each time there are one or more - * styling bindings present for an element. + * a single manifest * * The styling context is stored on a `TNode` on and there are * two instances of it: one for classes and another for styles. @@ -37,6 +36,10 @@ 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 @@ -62,36 +65,48 @@ 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.stylesContext = [ - * [], // initial values array - * 0, // the context config value + * // ... + * //
+ * 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`) * - * 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`) + * 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 * - * 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, + * 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 * ]; * - * tNode.classesContext = [ - * [], // initial values array - * 0, // the context config value + * 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`) * - * 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, + * 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 * ]; * ``` * @@ -100,19 +115,25 @@ import {LView} from '../interfaces/view'; * * ```typescript * context = [ - * CONFIG, // the styling context config value * //... - * guardMask, - * totalEntries, + * configValue, + * templateGuardMask, + * hostBindingsGuardMask, * propName, - * bindingIndices..., + * ...bindingIndices..., * defaultValue + * //... * ]; * ``` * * Below is a breakdown of each value: * - * - **guardMask**: + * - **configValue**: + * Property-specific configuration values. The only config setting + * that is implemented right now is whether or not to sanitize the + * value. + * + * - **templateGuardMask**: * 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 @@ -136,18 +157,23 @@ 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). * - * - **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. + * - **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`. * - * 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. + * ``` + *
// binding index = 31 (counter index = 1) + * ``` * * - **propName**: * The CSS property name or class name (e.g `width` or `active`). @@ -165,15 +191,20 @@ 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 (this is triggered by the - * `stylingApply()` instruction for the active element). + * 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). * * # How Styles/Classes are Rendered * Each time a styling instruction (e.g. `[class.name]`, `[style.prop]`, @@ -190,15 +221,24 @@ import {LView} from '../interfaces/view'; * function updateStyleProp(prop: string, value: string) { * const lView = getLView(); * const bindingIndex = BINDING_INDEX++; - * const indexForStyle = localStylesCounter++; + * + * // update the local counter value + * const indexForStyle = stylingState.stylesCount++; * if (lView[bindingIndex] !== value) { * lView[bindingIndex] = value; - * localBitMaskForStyles |= 1 << indexForStyle; + * + * // tell the local state that we have updated a style value + * // by updating the bit mask + * stylingState.bitMaskForStyles |= 1 << indexForStyle; * } * } * ``` * - * ## The Apply Algorithm + * 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) * 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. @@ -283,94 +323,147 @@ 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 a - * `TStylingContext` value. + * A series of flags used to configure the config value present within an instance of + * `TStylingContext`. */ -export const enum TStylingConfigFlags { +export const enum TStylingConfig { /** - * The initial state of the styling context config + * The initial state of the styling context config. */ - Initial = 0b0, + Initial = 0b0000000, /** - * A flag which marks the context as being locked. + * Whether or not there are prop-based bindings present. * - * 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). + * Examples include: + * 1. `
` + * 2. `
` + * 3. `@HostBinding('style.prop') x` + * 4. `@HostBinding('class.prop') x` */ - Locked = 0b1, + HasPropBindings = 0b0000001, /** - * Whether or not to store the state between updates in a global storage map. + * Whether or not there are map-based bindings present. * - * 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. + * Examples include: + * 1. `
` + * 2. `
` + * 3. `@HostBinding('style') x` + * 4. `@HostBinding('class') x` */ - PersistStateValues = 0b10, + 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, /** A Mask of all the configurations */ - Mask = 0b11, + Mask = 0b11111111, /** Total amount of configuration bits used */ - TotalBits = 2, + TotalBits = 8, } /** - * An index of position and offset values used to natigate the `TStylingContext`. + * An index of position and offset values used to navigate the `TStylingContext`. */ export const enum TStylingContextIndex { - 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, + ConfigPosition = 0, + TotalSourcesPosition = 1, + InitialStylingValuePosition = 2, + ValuesStartPosition = 3, // each tuple entry in the context - // (mask, count, prop, ...bindings||default-value) - ConfigAndGuardOffset = 0, - ValuesCountOffset = 1, - PropOffset = 2, - BindingsStartOffset = 3, - MinTupleLength = 4, + // (config, templateBitGuard, hostBindingBitGuard, prop, ...bindings||default-value) + ConfigOffset = 0, + TemplateBitGuardOffset = 1, + HostBindingsBitGuardOffset = 2, + PropOffset = 3, + BindingsStartOffset = 4 } /** @@ -387,8 +480,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: string|null, bindingIndex?: number|null): void; + (renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string, value: any, + bindingIndex?: number|null): void; } /** @@ -419,12 +512,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 { - /** 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 location of the raw key/value map instance used last to populate the array entries */ + RawValuePosition = 0, + /** The size of each property/value entry */ TupleSize = 2, @@ -458,8 +551,9 @@ export const enum StylingMapArrayIndex { */ export interface SyncStylingMapsFn { (context: TStylingContext, renderer: Renderer3|ProceduralRenderer3|null, element: RElement, - data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn|null, - mode: StylingMapsSyncMode, targetProp?: string|null, defaultValue?: string|null): boolean; + data: LStylingData, sourceIndex: number, applyStylingFn: ApplyStylingFn, + sanitizer: StyleSanitizeFn|null, mode: StylingMapsSyncMode, targetProp?: string|null, + defaultValue?: boolean|string|null): boolean; } /** @@ -477,4 +571,10 @@ 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 259ca917ac..44f0230f8f 100644 --- a/packages/core/src/render3/styling_next/map_based_bindings.ts +++ b/packages/core/src/render3/styling_next/map_based_bindings.ts @@ -5,12 +5,13 @@ * 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 {concatString, getBindingValue, getMapProp, getMapValue, getValuesCount, hyphenate, isStylingValueDefined, setMapValue} from './util'; +import {getBindingValue, getMapProp, getMapValue, getValue, getValuesCount, isStylingValueDefined} from './util'; @@ -23,6 +24,13 @@ import {concatString, getBindingValue, getMapProp, getMapValue, getValuesCount, * -------- */ +/** + * 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. * @@ -53,7 +61,7 @@ import {concatString, getBindingValue, getMapProp, getMapValue, getValuesCount, * value is marked as dirty. * * Styling values are applied once CD exits the element (which happens when - * the `select(n)` instruction is called or the template function exits). When + * the `advance(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. @@ -105,14 +113,14 @@ import {concatString, getBindingValue, getMapProp, getMapValue, getValuesCount, */ export const syncStylingMap: SyncStylingMapsFn = (context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, - data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn | null, - mode: StylingMapsSyncMode, targetProp?: string | null, - defaultValue?: string | null): boolean => { + data: LStylingData, sourceIndex: number, applyStylingFn: ApplyStylingFn, + sanitizer: StyleSanitizeFn | null, mode: StylingMapsSyncMode, targetProp?: string | null, + defaultValue?: string | boolean | 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, TStylingContextIndex.MapBindingsPosition); + const totalMaps = getValuesCount(context); if (totalMaps) { let runTheSyncAlgorithm = true; const loopUntilEnd = !targetProp; @@ -121,7 +129,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)) { + if (loopUntilEnd && (mode & StylingMapsSyncMode.ApplyAllValues) === 0) { runTheSyncAlgorithm = false; targetPropValueWasApplied = true; } @@ -129,7 +137,7 @@ export const syncStylingMap: SyncStylingMapsFn = if (runTheSyncAlgorithm) { targetPropValueWasApplied = innerSyncStylingMap( context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp || null, - 0, defaultValue || null); + sourceIndex, defaultValue || null); } if (loopUntilEnd) { @@ -153,83 +161,104 @@ 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 | null): boolean { + 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; + } + let targetPropValueWasApplied = false; - const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition); - if (currentMapIndex < totalMaps) { - const bindingIndex = getBindingValue( - context, TStylingContextIndex.MapBindingsPosition, currentMapIndex) as number; - const stylingMapArr = data[bindingIndex] as StylingMapArray; - + if (currentMapIndex <= mapsLimit) { 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); + const bindingIndex = getBindingValue( + context, TStylingContextIndex.ValuesStartPosition, currentMapIndex) as number; + const stylingMapArr = getValue(data, bindingIndex); - // 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); + 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); - if (iteratedTooFar) { - if (!targetPropValueWasApplied) { - targetPropValueWasApplied = valueApplied; + // 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 = recurseInnerMaps ? + innerSyncStylingMap( + context, renderer, element, data, applyStylingFn, sanitizer, innerMode, innerProp, + currentMapIndex + 1, defaultValue) : + false; + + if (iteratedTooFar) { + if (!targetPropValueWasApplied) { + targetPropValueWasApplied = valueApplied; + } + break; } - break; + + 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; } + setCurrentSyncCursor(currentMapIndex, cursor); - 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; + // 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); } - - 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( + } else if (recurseInnerMaps) { + targetPropValueWasApplied = 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. * @@ -276,8 +305,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: number, isTargetPropMatched: boolean) { - let doApplyValue = (mode & StylingMapsSyncMode.ApplyAllValues) > 0; +function isValueAllowedToBeApplied(mode: StylingMapsSyncMode, isTargetPropMatched: boolean) { + let doApplyValue = (mode & StylingMapsSyncMode.ApplyAllValues) !== 0; if (!doApplyValue) { if (mode & StylingMapsSyncMode.ApplyTargetProp) { doApplyValue = isTargetPropMatched; @@ -320,126 +349,3 @@ 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 41f559196a..8cf06b3ea9 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,79 +23,92 @@ * 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; } -export const STYLING_INDEX_START_VALUE = 1; -export const BIT_MASK_START_VALUE = 0; +// 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 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, - }; +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++; } - return _stylingState !; + return _state; } +/** + * Clears the styling state so that it can be used by another element's styling code. + */ export function resetStylingState() { - _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(); + _state.element = null; } diff --git a/packages/core/src/render3/styling_next/styling_debug.ts b/packages/core/src/render3/styling_next/styling_debug.ts index 04ec2d90c2..652ad0be3f 100644 --- a/packages/core/src/render3/styling_next/styling_debug.ts +++ b/packages/core/src/render3/styling_next/styling_debug.ts @@ -7,14 +7,13 @@ */ 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 {applyStyling} from './bindings'; -import {ApplyStylingFn, LStylingData, TStylingContext, TStylingContextIndex} from './interfaces'; +import {applyStylingViaContext} from './bindings'; +import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from './interfaces'; import {activateStylingMapFeature} from './map_based_bindings'; -import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased, isSanitizationRequired} from './util'; +import {allowDirectStyling as _allowDirectStyling, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isMapBased, isSanitizationRequired} from './util'; @@ -53,20 +52,32 @@ 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: {[key: string]: LStylingSummary}; + summary: {[propertyName: string]: LStylingSummary}; /** * A key/value map of all styling properties and their - * runtime values. + * runtime values */ - values: {[key: string]: string | number | null | boolean}; + values: {[propertyName: string]: string | number | null | boolean}; /** - * Overrides the sanitizer used to process styles. + * Overrides the sanitizer used to process styles */ overrideSanitizer(sanitizer: StyleSanitizeFn|null): void; } @@ -83,9 +94,15 @@ export interface TStylingTupleSummary { /** * The bit guard mask that is used to compare and protect against - * styling changes when and styling bindings update + * styling changes when any template style/class bindings update */ - guardMask: number; + 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; /** * Whether or not the entry requires sanitization @@ -93,18 +110,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); @@ -121,7 +138,8 @@ export function attachStylingDebugObject(context: TStylingContext) { class TStylingContextDebug { constructor(public readonly context: TStylingContext) {} - get isLocked() { return isContextLocked(this.context); } + get isTemplateLocked() { return isContextLocked(this.context, true); } + get isHostBindingsLocked() { return isContextLocked(this.context, false); } /** * Returns a detailed summary of each styling entry in the context. @@ -130,30 +148,36 @@ class TStylingContextDebug { */ get entries(): {[prop: string]: TStylingTupleSummary} { const context = this.context; + const totalColumns = getValuesCount(context); const entries: {[prop: string]: TStylingTupleSummary} = {}; - const start = TStylingContextIndex.MapBindingsPosition; + const start = getPropValuesStartPosition(context); let i = start; while (i < context.length) { - 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 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 sources: (number | string | null)[] = []; - for (let j = 0; j < valuesCount; j++) { - sources.push(context[bindingsStartPosition + j] as number | string | null); + 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); } - - entries[prop] = {prop, guardMask, sanitizationRequired, valuesCount, defaultValue, sources}; } - i += TStylingContextIndex.BindingsStartOffset + valuesCount; + entries[prop] = { + prop, + templateBitMask, + hostBindingsBitMask, + sanitizationRequired, + valuesCount: sources.length, defaultValue, sources, + }; + + i += TStylingContextIndex.BindingsStartOffset + totalColumns; } return entries; } @@ -191,6 +215,29 @@ 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. */ @@ -205,16 +252,23 @@ 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 = getValuesCount(this.context, TStylingContextIndex.MapBindingsPosition) > 0; + const hasMaps = hasConfig(this.context, TStylingConfig.HasMapBindings); 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()); - applyStyling(this.context, null, mockElement, this._data, true, mapFn, sanitizer); + + // 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); } } diff --git a/packages/core/src/render3/styling_next/util.ts b/packages/core/src/render3/styling_next/util.ts index 5410c8840d..5a26befaed 100644 --- a/packages/core/src/render3/styling_next/util.ts +++ b/packages/core/src/render3/styling_next/util.ts @@ -5,12 +5,35 @@ * 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'; -const MAP_BASED_ENTRY_PROP_NAME = '--MAP--'; -const TEMPLATE_DIRECTIVE_INDEX = 0; +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; /** * Creates a new instance of the `TStylingContext`. @@ -21,84 +44,83 @@ const TEMPLATE_DIRECTIVE_INDEX = 0; * `TStylingContext` with the initial values (see `interfaces.ts` for more info). */ export function allocTStylingContext(initialStyling?: StylingMapArray | null): TStylingContext { - // 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; + initialStyling = initialStyling || allocStylingMapArray(); return [ - initialStyling || [''], // empty initial-styling map value - TStylingConfigFlags.Initial, - TEMPLATE_DIRECTIVE_INDEX, - mapBasedConfig, - 0, - MAP_BASED_ENTRY_PROP_NAME, + 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 ]; } -/** - * 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 allocStylingMapArray(): StylingMapArray { + return ['']; } -function getConfig(context: TStylingContext) { +export function getConfig(context: TStylingContext) { return context[TStylingContextIndex.ConfigPosition]; } -export function setConfig(context: TStylingContext, value: number) { +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 { context[TStylingContextIndex.ConfigPosition] = value; } -export function getProp(context: TStylingContext, index: number) { +export function patchConfig(context: TStylingContext, flag: TStylingConfig): void { + context[TStylingContextIndex.ConfigPosition] |= flag; +} + +export function getProp(context: TStylingContext, index: number): string { return context[index + TStylingContextIndex.PropOffset] as string; } function getPropConfig(context: TStylingContext, index: number): number { - return (context[index + TStylingContextIndex.ConfigAndGuardOffset] as number) & + return (context[index + TStylingContextIndex.ConfigOffset] as number) & TStylingContextPropConfigFlags.Mask; } -export function isSanitizationRequired(context: TStylingContext, index: number) { - return (getPropConfig(context, index) & TStylingContextPropConfigFlags.SanitizationRequired) > 0; +export function isSanitizationRequired(context: TStylingContext, index: number): boolean { + return (getPropConfig(context, index) & TStylingContextPropConfigFlags.SanitizationRequired) !== + 0; } -export function getGuardMask(context: TStylingContext, index: number) { - const configGuardValue = context[index + TStylingContextIndex.ConfigAndGuardOffset] as number; - return configGuardValue >> TStylingContextPropConfigFlags.TotalBits; +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 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 setGuardMask( + context: TStylingContext, index: number, maskValue: number, isHostBinding: boolean) { + const position = index + (isHostBinding ? TStylingContextIndex.HostBindingsBitGuardOffset : + TStylingContextIndex.TemplateBitGuardOffset); + context[position] = maskValue; } -export function getValuesCount(context: TStylingContext, index: number) { - return context[index + TStylingContextIndex.ValuesCountOffset] as number; +export function getValuesCount(context: TStylingContext): number { + return getTotalSources(context) + 1; +} + +export function getTotalSources(context: TStylingContext): number { + return context[TStylingContextIndex.TotalSourcesPosition]; } export function getBindingValue(context: TStylingContext, index: number, offset: number) { @@ -106,39 +128,44 @@ export function getBindingValue(context: TStylingContext, index: number, offset: } export function getDefaultValue(context: TStylingContext, index: number): string|boolean|null { - const valuesCount = getValuesCount(context, index); - return context[index + TStylingContextIndex.BindingsStartOffset + valuesCount - 1] as string | + return context[index + TStylingContextIndex.BindingsStartOffset + getTotalSources(context)] as + string | boolean | null; } -/** - * 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 setDefaultValue( + context: TStylingContext, index: number, value: string | boolean | null) { + return context[index + TStylingContextIndex.BindingsStartOffset + getTotalSources(context)] = + value; } -export function lockContext(context: TStylingContext) { - setConfig(context, getConfig(context) | TStylingConfigFlags.Locked); +export function setValue(data: LStylingData, bindingIndex: number, value: any) { + data[bindingIndex] = value; } -export function isContextLocked(context: TStylingContext): boolean { - return (getConfig(context) & TStylingConfigFlags.Locked) > 0; +export function getValue(data: LStylingData, bindingIndex: number): T|null { + return bindingIndex > 0 ? data[bindingIndex] as T : null; } -export function stateIsPersisted(context: TStylingContext): boolean { - return (getConfig(context) & TStylingConfigFlags.PersistStateValues) > 0; +export function lockContext(context: TStylingContext, hostBindingsMode: boolean): void { + patchConfig(context, getLockedConfig(hostBindingsMode)); } -export function markContextToPersistState(context: TStylingContext) { - setConfig(context, getConfig(context) | TStylingConfigFlags.PersistStateValues); +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 getPropValuesStartPosition(context: TStylingContext) { - return TStylingContextIndex.MapBindingsBindingsStartPosition + - context[TStylingContextIndex.MapBindingsValuesCountPosition]; + let startPosition = TStylingContextIndex.ValuesStartPosition; + if (hasConfig(context, TStylingConfig.HasMapBindings)) { + startPosition += TStylingContextIndex.BindingsStartOffset + getValuesCount(context); + } + return startPosition; } export function isMapBased(prop: string) { @@ -197,17 +224,23 @@ export function getStylingMapArray(value: TStylingContext | StylingMapArray | nu StylingMapArray|null { return isStylingContext(value) ? (value as TStylingContext)[TStylingContextIndex.InitialStylingValuePosition] : - value; + value as StylingMapArray; } 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.MapBindingsBindingsStartPosition && + 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'); +} + export function getInitialStylingValue(context: TStylingContext | StylingMapArray | null): string { const map = getStylingMapArray(context); return map && (map[StylingMapArrayIndex.RawValuePosition] as string | null) || ''; @@ -225,6 +258,13 @@ 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; @@ -253,3 +293,130 @@ 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 fbfc88f44c..612923c650 100644 --- a/packages/core/src/sanitization/bypass.ts +++ b/packages/core/src/sanitization/bypass.ts @@ -61,9 +61,7 @@ export interface SafeResourceUrl extends SafeValue {} abstract class SafeValueImpl implements SafeValue { - constructor(public changingThisBreaksApplicationSecurity: string) { - // empty - } + constructor(public changingThisBreaksApplicationSecurity: string) {} abstract getTypeName(): string; @@ -89,10 +87,10 @@ class SafeResourceUrlImpl extends SafeValueImpl implements SafeResourceUrl { getTypeName() { return BypassType.ResourceUrl; } } -export function unwrapSafeValue(value: SafeValue): string { +export function unwrapSafeValue(value: string | 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 41c4e3fad0..d078be0483 100644 --- a/packages/core/src/util/ng_dev_mode.ts +++ b/packages/core/src/util/ng_dev_mode.ts @@ -59,8 +59,6 @@ declare global { flushStyling: number; classesApplied: number; stylesApplied: number; - stylingWritePersistedState: number; - stylingReadPersistedState: number; } } @@ -101,8 +99,6 @@ 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 79aea9599d..392b996974 100644 --- a/packages/core/test/acceptance/styling_next_spec.ts +++ b/packages/core/test/acceptance/styling_next_spec.ts @@ -15,6 +15,8 @@ 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', () => { @@ -124,6 +126,74 @@ 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', () => { @@ -240,6 +310,7 @@ describe('new styling integration', () => { fixture.componentInstance.w3 = null; fixture.detectChanges(); + expect(styles.values).toEqual({ 'width': '200px', }); @@ -309,16 +380,22 @@ describe('new styling integration', () => { .it('should apply map-based style and class entries', () => { @Component({template: '
'}) class Cmp { - public c !: {[key: string]: any}; - updateClasses(prop: string) { - this.c = {...this.c || {}}; - this.c[prop] = true; + 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 s !: {[key: string]: any}; + public s: {[key: string]: any}|null = null; updateStyles(prop: string, value: string|number|null) { - this.s = {...this.s || {}}; - this.s[prop] = value; + const s = this.s || (this.s = {}); + Object.assign(s, {[prop]: value}); + } + + reset() { + this.s = null; + this.c = null; } } @@ -332,22 +409,47 @@ describe('new styling integration', () => { const element = fixture.nativeElement.querySelector('div'); const node = getDebugNode(element) !; - const styles = node.styles !; - const classes = node.classes !; + let styles = node.styles !; + let classes = node.classes !; - const stylesSummary = styles.summary; - const widthSummary = stylesSummary['width']; + let stylesSummary = styles.summary; + let widthSummary = stylesSummary['width']; expect(widthSummary.prop).toEqual('width'); expect(widthSummary.value).toEqual('100px'); - const heightSummary = stylesSummary['height']; + let heightSummary = stylesSummary['height']; expect(heightSummary.prop).toEqual('height'); expect(heightSummary.value).toEqual('200px'); - const classesSummary = classes.summary; - const abcSummary = classesSummary['abc']; + let classesSummary = classes.summary; + let abcSummary = classesSummary['abc']; expect(abcSummary.prop).toEqual('abc'); - expect(abcSummary.value as any).toEqual(true); + 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(); }); onlyInIvy('ivy resolves styling across directives, components and templates in unison') @@ -385,6 +487,7 @@ describe('new styling integration', () => { const node = getDebugNode(element) !; const styles = node.styles !; + expect(styles.values).toEqual({ 'width': '555px', 'color': 'red', @@ -509,7 +612,8 @@ describe('new styling integration', () => { resetStylingCounters(); fixture.detectChanges(); - assertStyleCounters(1, 0); + // the width is applied both in TEMPLATE and in HOST_BINDINGS mode + assertStyleCounters(2, 0); assertStyle(element, 'width', '999px'); assertStyle(element, 'height', '123px'); @@ -517,8 +621,8 @@ describe('new styling integration', () => { resetStylingCounters(); fixture.detectChanges(); - // both are applied because the map was altered - assertStyleCounters(2, 0); + // the width is only applied once + assertStyleCounters(1, 0); assertStyle(element, 'width', '0px'); assertStyle(element, 'height', '123px'); @@ -526,8 +630,8 @@ describe('new styling integration', () => { resetStylingCounters(); fixture.detectChanges(); - // all three are applied because the map was altered - assertStyleCounters(3, 0); + // only the width and color have changed + assertStyleCounters(2, 0); assertStyle(element, 'width', '1000px'); assertStyle(element, 'height', '123px'); assertStyle(element, 'color', 'red'); @@ -536,7 +640,9 @@ describe('new styling integration', () => { resetStylingCounters(); fixture.detectChanges(); - assertStyleCounters(1, 0); + // height gets applied twice and all other + // values get applied + assertStyleCounters(4, 0); assertStyle(element, 'width', '1000px'); assertStyle(element, 'height', '1000px'); assertStyle(element, 'color', 'red'); @@ -545,8 +651,7 @@ describe('new styling integration', () => { resetStylingCounters(); fixture.detectChanges(); - // all four are applied because the map was altered - assertStyleCounters(4, 0); + assertStyleCounters(5, 0); assertStyle(element, 'width', '2000px'); assertStyle(element, 'height', '1000px'); assertStyle(element, 'color', 'blue'); @@ -557,62 +662,13 @@ describe('new styling integration', () => { fixture.detectChanges(); // all four are applied because the map was altered - assertStyleCounters(3, 1); + assertStyleCounters(4, 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({ @@ -910,7 +966,6 @@ 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'); @@ -937,10 +992,10 @@ describe('new styling integration', () => { @Component({ template: ` -
-
-
- ` +
+
+
+ ` }) class Cmp { w = 100; @@ -1092,6 +1147,27 @@ describe('new styling integration', () => { expect(div.style.height).toEqual('200px'); expect(div.style.opacity).toEqual('1'); }); + + it('should allow [ngStyle] and [ngClass] to be used together', () => { + @Component({ + template: ` +
+ ` + }) + class Cmp { + c: any = 'foo bar'; + s: any = {width: '200px'}; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(div.style.width).toEqual('200px'); + expect(div.classList.contains('foo')).toBeTruthy(); + expect(div.classList.contains('bar')).toBeTruthy(); + }); }); function assertStyleCounters(countForSet: number, countForRemove: number) { diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index 3b2ea66ad6..acfb5eab31 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.ts @@ -628,7 +628,6 @@ 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 f0bd05a1f1..f3a00560ea 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -155,6 +155,9 @@ { "name": "_currentNamespace" }, + { + "name": "_elementExitFn" + }, { "name": "_global" }, @@ -165,7 +168,7 @@ "name": "_selectedIndex" }, { - "name": "_stateStorage" + "name": "_state" }, { "name": "addComponentLogic" @@ -176,6 +179,9 @@ { "name": "addToViewTree" }, + { + "name": "allocStylingMapArray" + }, { "name": "appendChild" }, @@ -254,6 +260,9 @@ { "name": "executeContentQueries" }, + { + "name": "executeElementExitFn" + }, { "name": "executeInitAndCheckHooks" }, @@ -395,6 +404,9 @@ { "name": "getStylingMapArray" }, + { + "name": "hasActiveElementFlag" + }, { "name": "hasClassInput" }, @@ -572,9 +584,6 @@ { "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 ffd2633f81..441806495d 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -134,6 +134,9 @@ { "name": "__window" }, + { + "name": "_elementExitFn" + }, { "name": "_global" }, @@ -144,7 +147,7 @@ "name": "_selectedIndex" }, { - "name": "_stateStorage" + "name": "_state" }, { "name": "addToViewTree" @@ -209,6 +212,9 @@ { "name": "executeCheckHooks" }, + { + "name": "executeElementExitFn" + }, { "name": "executeInitAndCheckHooks" }, @@ -314,6 +320,9 @@ { "name": "getSelectedIndex" }, + { + "name": "hasActiveElementFlag" + }, { "name": "hasParentInjector" }, @@ -419,9 +428,6 @@ { "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 48769eb646..e617d0c535 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -41,6 +41,9 @@ { "name": "DECLARATION_VIEW" }, + { + "name": "DEFAULT_BINDING_INDEX" + }, { "name": "DEFAULT_BINDING_VALUE" }, @@ -48,7 +51,7 @@ "name": "DEFAULT_GUARD_MASK_VALUE" }, { - "name": "DEFAULT_SIZE_VALUE" + "name": "DEFAULT_TOTAL_SOURCES" }, { "name": "DefaultIterableDiffer" @@ -89,6 +92,9 @@ { "name": "HOST" }, + { + "name": "INDEX_START_VALUE" + }, { "name": "INJECTOR" }, @@ -108,7 +114,7 @@ "name": "MAP_BASED_ENTRY_PROP_NAME" }, { - "name": "MIN_DIRECTIVE_ID" + "name": "MAP_DIRTY_VALUE" }, { "name": "MONKEY_PATCH_KEY_NAME" @@ -212,9 +218,6 @@ { "name": "STYLING_INDEX_FOR_MAP_BINDING" }, - { - "name": "STYLING_INDEX_START_VALUE" - }, { "name": "SWITCH_ELEMENT_REF_FACTORY" }, @@ -224,6 +227,9 @@ { "name": "SWITCH_VIEW_CONTAINER_REF_FACTORY" }, + { + "name": "SafeValueImpl" + }, { "name": "SkipSelf" }, @@ -395,6 +401,9 @@ { "name": "_devMode" }, + { + "name": "_elementExitFn" + }, { "name": "_global" }, @@ -405,16 +414,7 @@ "name": "_selectedIndex" }, { - "name": "_stateStorage" - }, - { - "name": "_stylingElement" - }, - { - "name": "_stylingProp" - }, - { - "name": "_stylingState" + "name": "_state" }, { "name": "_symbolIterator" @@ -422,12 +422,6 @@ { "name": "activeDirectiveId" }, - { - "name": "activeDirectiveSuperClassDepthPosition" - }, - { - "name": "activeDirectiveSuperClassHeight" - }, { "name": "addBindingIntoContext" }, @@ -437,6 +431,9 @@ { "name": "addItemToStylingMap" }, + { + "name": "addNewSourceColumn" + }, { "name": "addRemoveViewFromContainer" }, @@ -446,6 +443,9 @@ { "name": "addToViewTree" }, + { + "name": "allocStylingMapArray" + }, { "name": "allocTStylingContext" }, @@ -453,7 +453,7 @@ "name": "allocateNewContextEntry" }, { - "name": "allowStylingFlush" + "name": "allowDirectStyling" }, { "name": "appendChild" @@ -470,6 +470,15 @@ { "name": "applyStyling" }, + { + "name": "applyStylingValue" + }, + { + "name": "applyStylingValueDirectly" + }, + { + "name": "applyStylingViaContext" + }, { "name": "applyToElementOrContainer" }, @@ -524,9 +533,6 @@ { "name": "containerInternal" }, - { - "name": "contextHasUpdates" - }, { "name": "contextLView" }, @@ -584,18 +590,6 @@ { "name": "defaultScheduler" }, - { - "name": "deferBindingRegistration" - }, - { - "name": "deferStylingUpdate" - }, - { - "name": "deferredBindingQueue" - }, - { - "name": "deleteStylingStateFromStorage" - }, { "name": "destroyLView" }, @@ -629,6 +623,9 @@ { "name": "executeContentQueries" }, + { + "name": "executeElementExitFn" + }, { "name": "executeInitAndCheckHooks" }, @@ -666,10 +663,10 @@ "name": "findExistingListener" }, { - "name": "findViaComponent" + "name": "findInitialStylingValue" }, { - "name": "flushDeferredBindings" + "name": "findViaComponent" }, { "name": "flushStyling" @@ -689,15 +686,6 @@ { "name": "getActiveDirectiveId" }, - { - "name": "getActiveDirectiveStylingIndex" - }, - { - "name": "getActiveDirectiveSuperClassDepth" - }, - { - "name": "getActiveDirectiveSuperClassHeight" - }, { "name": "getBeforeNodeForView" }, @@ -746,6 +734,9 @@ { "name": "getDebugContext" }, + { + "name": "getDefaultValue" + }, { "name": "getDirectiveDef" }, @@ -785,6 +776,9 @@ { "name": "getLViewParent" }, + { + "name": "getLockedConfig" + }, { "name": "getMapProp" }, @@ -893,21 +887,36 @@ { "name": "getTViewCleanup" }, + { + "name": "getTotalSources" + }, { "name": "getTypeName" }, { "name": "getTypeNameForDebugging" }, + { + "name": "getValue" + }, { "name": "getValuesCount" }, { "name": "handleError" }, + { + "name": "hasActiveElementFlag" + }, { "name": "hasClassInput" }, + { + "name": "hasConfig" + }, + { + "name": "hasDirectives" + }, { "name": "hasParentInjector" }, @@ -936,7 +945,7 @@ "name": "initNodeFlags" }, { - "name": "initializeInputAndOutputAliases" + "name": "initializeTNodeInputs" }, { "name": "injectElementRef" @@ -1010,6 +1019,12 @@ { "name": "isForwardRef" }, + { + "name": "isHostStyling" + }, + { + "name": "isHostStylingActive" + }, { "name": "isJsObject" }, @@ -1085,24 +1100,21 @@ { "name": "markAsComponentHost" }, - { - "name": "markContextToPersistState" - }, { "name": "markDirty" }, { "name": "markDirtyIfOnPush" }, + { + "name": "markStylingStateAsDirty" + }, { "name": "markViewDirty" }, { "name": "matchTemplateAttribute" }, - { - "name": "maybeApplyStyling" - }, { "name": "namespaceHTMLInternal" }, @@ -1139,6 +1151,9 @@ { "name": "normalizeBitMaskValue" }, + { + "name": "patchConfig" + }, { "name": "postProcessBaseDirective" }, @@ -1202,6 +1217,9 @@ { "name": "renderDetachView" }, + { + "name": "renderHostBindingsAsStale" + }, { "name": "renderInitialStyling" }, @@ -1214,9 +1232,6 @@ { "name": "renderView" }, - { - "name": "resetAllStylingState" - }, { "name": "resetComponentState" }, @@ -1250,6 +1265,9 @@ { "name": "selectView" }, + { + "name": "setActiveElementFlag" + }, { "name": "setActiveHostElement" }, @@ -1262,9 +1280,6 @@ { "name": "setClass" }, - { - "name": "setConfig" - }, { "name": "setCurrentDirectiveDef" }, @@ -1274,9 +1289,15 @@ { "name": "setCurrentStyleSanitizer" }, + { + "name": "setDefaultValue" + }, { "name": "setDirectiveStylingInput" }, + { + "name": "setElementExitFn" + }, { "name": "setGuardMask" }, @@ -1298,6 +1319,9 @@ { "name": "setIsNotParent" }, + { + "name": "setMapAsDirty" + }, { "name": "setMapValue" }, @@ -1316,18 +1340,15 @@ { "name": "setUpAttributes" }, + { + "name": "setValue" + }, { "name": "shouldSearchParent" }, - { - "name": "stateIsPersisted" - }, { "name": "storeCleanupFn" }, - { - "name": "storeStylingState" - }, { "name": "stringify" }, @@ -1337,6 +1358,9 @@ { "name": "stylingMapToString" }, + { + "name": "stylingProp" + }, { "name": "syncViewWithBlueprint" }, @@ -1358,26 +1382,23 @@ { "name": "unwrapRNode" }, + { + "name": "unwrapSafeValue" + }, { "name": "updateBindingData" }, { - "name": "updateClassBinding" + "name": "updateClassViaContext" }, { "name": "updateInitialStylingOnContext" }, - { - "name": "updateLastDirectiveIndex" - }, - { - "name": "updateLastDirectiveIndex" - }, { "name": "updateRawValueOnContext" }, { - "name": "updateStyleBinding" + "name": "updateStyleViaContext" }, { "name": "viewAttachedToChangeDetector" @@ -1436,12 +1457,6 @@ { "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 27da6ea68e..d275c8cd7a 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, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index'; +import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer, ɵɵ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,11 +20,7 @@ import {NgForOf} from './common_with_def'; import {ComponentFixture, TemplateFixture} from './render_util'; describe('instructions', () => { - function createAnchor() { - ɵɵelementStart(0, 'a'); - ɵɵstyling(); - ɵɵelementEnd(); - } + function createAnchor() { ɵɵelement(0, 'a'); } function createDiv(initialClasses?: string[] | null, initialStyles?: string[] | null) { const attrs: any[] = []; @@ -34,9 +30,7 @@ describe('instructions', () => { if (initialStyles) { attrs.push(AttributeMarker.Styles, ...initialStyles); } - ɵɵelementStart(0, 'div', attrs); - ɵɵstyling(); - ɵɵelementEnd(); + ɵɵelement(0, 'div', attrs); } function createScript() { ɵɵelement(0, 'script'); } @@ -156,7 +150,6 @@ describe('instructions', () => { t.update(() => { ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer); ɵɵstyleProp('background-image', 'url("http://server")'); - ɵɵstylingApply(); }); // nothing is set because sanitizer suppresses it. expect(t.html).toEqual('
'); @@ -164,7 +157,6 @@ 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")'); @@ -173,17 +165,12 @@ describe('instructions', () => { describe('styleMap', () => { function createDivWithStyle() { - ɵɵelementStart(0, 'div', [AttributeMarker.Styles, 'height', '10px']); - ɵɵstyling(); - ɵɵelementEnd(); + ɵɵelement(0, 'div', [AttributeMarker.Styles, 'height', '10px']); } it('should add style', () => { const fixture = new TemplateFixture(createDivWithStyle, () => {}, 1); - fixture.update(() => { - ɵɵstyleMap({'background-color': 'red'}); - ɵɵstylingApply(); - }); + fixture.update(() => { ɵɵstyleMap({'background-color': 'red'}); }); expect(fixture.html).toEqual('
'); }); @@ -205,7 +192,6 @@ describe('instructions', () => { 'filter': 'filter', 'width': 'width' }); - ɵɵstylingApply(); }); const props = detectedValues.sort(); @@ -216,18 +202,11 @@ describe('instructions', () => { }); describe('elementClass', () => { - function createDivWithStyling() { - ɵɵelementStart(0, 'div'); - ɵɵstyling(); - ɵɵelementEnd(); - } + function createDivWithStyling() { ɵɵelement(0, 'div'); } it('should add class', () => { const fixture = new TemplateFixture(createDivWithStyling, () => {}, 1); - fixture.update(() => { - ɵɵclassMap('multiple classes'); - ɵɵstylingApply(); - }); + fixture.update(() => { ɵɵclassMap('multiple classes'); }); expect(fixture.html).toEqual('
'); }); }); diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 87106a41d8..b13462bbe9 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, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all'; +import {ɵɵallocHostVars, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵprojection, ɵɵprojectionDef, ɵɵ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,12 +630,7 @@ describe('element discovery', () => { vars: 0, template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'section'); - ɵɵstyling(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵstylingApply(); + ɵɵelement(0, 'section'); } } }); 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 a6ad65dbe4..cc104fb6fb 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 {ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element'; +import {ɵɵelement, ɵɵ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, ɵɵstyling, ɵɵstylingApply} from '../../../../src/render3/styling_next/instructions'; +import {ɵɵclassMap, ɵɵstyleMap} from '../../../../src/render3/styling_next/instructions'; import {createBenchmark} from '../micro_bench'; import {setupRootViewWithEmbeddedViews} from '../setup'; @@ -31,79 +31,49 @@ import {setupRootViewWithEmbeddedViews} from '../setup'; function testTemplate(rf: RenderFlags, ctx: any) { if (rf & 1) { ɵɵelementStart(0, '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(); + ɵɵ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'); ɵɵ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 2f01d6498d..ef2e8e9115 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 {ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element'; +import {ɵɵelement, ɵɵ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, ɵɵstyling, ɵɵstylingApply} from '../../../../src/render3/styling_next/instructions'; +import {ɵɵclassProp, ɵɵstyleProp} from '../../../../src/render3/styling_next/instructions'; import {createBenchmark} from '../micro_bench'; import {setupRootViewWithEmbeddedViews} from '../setup'; @@ -32,89 +32,50 @@ import {setupRootViewWithEmbeddedViews} from '../setup'; function testTemplate(rf: RenderFlags, ctx: any) { if (rf & 1) { ɵɵelementStart(0, 'div', [AttributeMarker.Classes, 'list']); - ɵɵ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( + ɵɵ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( 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(); } } @@ -134,4 +95,4 @@ while (refreshTime()) { console.profileEnd(); // report results -styleAndClassBindingBenchmark.report(); \ No newline at end of file +styleAndClassBindingBenchmark.report(); diff --git a/packages/core/test/render3/perf/style_binding/index.ts b/packages/core/test/render3/perf/style_binding/index.ts index 2105e3c7ed..37b7b1bf5e 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 {ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element'; +import {ɵɵelement, ɵɵ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, ɵɵstyling, ɵɵstylingApply} from '../../../../src/render3/styling_next/instructions'; +import {ɵɵstyleProp} from '../../../../src/render3/styling_next/instructions'; import {createBenchmark} from '../micro_bench'; import {setupRootViewWithEmbeddedViews} from '../setup'; @@ -31,69 +31,39 @@ import {setupRootViewWithEmbeddedViews} from '../setup'; function testTemplate(rf: RenderFlags, ctx: any) { if (rf & 1) { ɵɵelementStart(0, 'div'); - ɵɵ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(); + ɵɵ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'); ɵɵ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 550adad99b..99161b8d84 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/map_based_bindings'; +import {normalizeIntoStylingMap as createMap} from '../../../src/render3/styling_next/util'; 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 db23feb0ee..3d26c61f9e 100644 --- a/packages/core/test/render3/styling_next/styling_context_spec.ts +++ b/packages/core/test/render3/styling_next/styling_context_spec.ts @@ -5,8 +5,10 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {DEFAULT_GUARD_MASK_VALUE, registerBinding} from '@angular/core/src/render3/styling_next/bindings'; +import {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', () => { @@ -15,33 +17,36 @@ describe('styling context', () => { const context = debug.context; expect(debug.entries).toEqual({}); - registerBinding(context, 1, 'width', '100px'); + registerBinding(context, 1, 0, 'width', '100px'); expect(debug.entries['width']).toEqual({ prop: 'width', valuesCount: 1, sanitizationRequired: false, - guardMask: buildGuardMask(), + templateBitMask: buildGuardMask(), + hostBindingsBitMask: buildGuardMask(), defaultValue: '100px', sources: ['100px'], }); - registerBinding(context, 2, 'width', 20); + registerBinding(context, 2, 0, 'width', 20); expect(debug.entries['width']).toEqual({ prop: 'width', sanitizationRequired: false, valuesCount: 2, - guardMask: buildGuardMask(2), + templateBitMask: buildGuardMask(2), + hostBindingsBitMask: buildGuardMask(), defaultValue: '100px', sources: [20, '100px'], }); - registerBinding(context, 3, 'height', 10); - registerBinding(context, 4, 'height', 15); + registerBinding(context, 3, 0, 'height', 10); + registerBinding(context, 4, 1, 'height', 15); expect(debug.entries['height']).toEqual({ prop: 'height', valuesCount: 3, sanitizationRequired: false, - guardMask: buildGuardMask(3, 4), + templateBitMask: buildGuardMask(3), + hostBindingsBitMask: buildGuardMask(4), defaultValue: null, sources: [10, 15, null], }); @@ -52,13 +57,14 @@ describe('styling context', () => { const context = debug.context; expect(debug.entries).toEqual({}); - registerBinding(context, 1, 'width', 123); - registerBinding(context, 1, 'width', 123); + registerBinding(context, 1, 0, 'width', 123); + registerBinding(context, 1, 0, 'width', 123); expect(debug.entries['width']).toEqual({ prop: 'width', valuesCount: 2, sanitizationRequired: false, - guardMask: buildGuardMask(1), + templateBitMask: buildGuardMask(1), + hostBindingsBitMask: buildGuardMask(), defaultValue: null, sources: [123, null], }); @@ -68,33 +74,36 @@ describe('styling context', () => { const debug = makeContextWithDebug(); const context = debug.context; - registerBinding(context, 1, 'width', null); + registerBinding(context, 1, 0, 'width', null); const x = debug.entries['width']; expect(debug.entries['width']).toEqual({ prop: 'width', valuesCount: 1, sanitizationRequired: false, - guardMask: buildGuardMask(), + templateBitMask: buildGuardMask(), + hostBindingsBitMask: buildGuardMask(), defaultValue: null, sources: [null] }); - registerBinding(context, 1, 'width', '100px'); + registerBinding(context, 1, 0, 'width', '100px'); expect(debug.entries['width']).toEqual({ prop: 'width', valuesCount: 1, sanitizationRequired: false, - guardMask: buildGuardMask(), + templateBitMask: buildGuardMask(), + hostBindingsBitMask: buildGuardMask(), defaultValue: '100px', sources: ['100px'] }); - registerBinding(context, 1, 'width', '200px'); + registerBinding(context, 1, 0, 'width', '200px'); expect(debug.entries['width']).toEqual({ prop: 'width', valuesCount: 1, sanitizationRequired: false, - guardMask: buildGuardMask(), + templateBitMask: buildGuardMask(), + hostBindingsBitMask: 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 138def5d09..256e5c09c9 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, 'width', null); + registerBinding(context, 0, 0, 'width', null); expect(d.summary).toEqual({ width: { prop: 'width', @@ -27,7 +27,7 @@ describe('styling debugging tools', () => { }, }); - registerBinding(context, 0, 'width', '100px'); + registerBinding(context, 0, 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, 'width', someBindingIndex1); + registerBinding(context, 0, 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, 'width', someBindingIndex2); + registerBinding(context, 0, 1, '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 35d5cccb3a..70919a41bc 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -1076,10 +1076,6 @@ 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;