diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json index 415c04e78a..4ba1f0b7e6 100755 --- a/aio/scripts/_payload-limits.json +++ b/aio/scripts/_payload-limits.json @@ -4,10 +4,10 @@ "uncompressed": { "runtime-es5": 3042, "runtime-es2015": 3048, - "main-es5": 511036, - "main-es2015": 450486, - "polyfills-es5": 130136, - "polyfills-es2015": 52931 + "main-es5": 511052, + "main-es2015": 450562, + "polyfills-es5": 129161, + "polyfills-es2015": 53295 } } }, @@ -16,10 +16,10 @@ "uncompressed": { "runtime-es5": 3042, "runtime-es2015": 3048, - "main-es5": 511036, - "main-es2015": 450486, - "polyfills-es5": 130136, - "polyfills-es2015": 52931 + "main-es5": 499085, + "main-es2015": 438296, + "polyfills-es5": 129161, + "polyfills-es2015": 53295 } } }, @@ -28,10 +28,10 @@ "uncompressed": { "runtime-es5": 2932, "runtime-es2015": 2938, - "main-es5": 565073, - "main-es2015": 583108, - "polyfills-es5": 130136, - "polyfills-es2015": 52931 + "main-es5": 555102, + "main-es2015": 572938, + "polyfills-es5": 129161, + "polyfills-es2015": 53295 } } } diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 7a79c70b08..6d4990abfc 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime": 1440, - "main": 14847, + "main": 14912, "polyfills": 43567 } } @@ -21,7 +21,7 @@ "master": { "uncompressed": { "runtime": 1440, - "main": 149248, + "main": 147528, "polyfills": 43567 } } diff --git a/modules/benchmarks/src/tree/render3_function/index.ts b/modules/benchmarks/src/tree/render3_function/index.ts index 9b2d11ab2c..1401374bf0 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, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵselect, ɵɵstyleProp, ɵɵstyling, ɵɵtext, ɵɵtextInterpolate1} from '@angular/core'; +import {ɵRenderFlags, ɵrenderComponent as renderComponent, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵselect, ɵɵstyleProp, ɵɵstyling, ɵɵstylingApply, ɵɵtext, ɵɵtextInterpolate1} from '@angular/core'; import {bindAction, profile} from '../../util'; import {createDom, destroyDom, detectChanges} from '../render3/tree'; @@ -32,13 +32,12 @@ export class TreeFunction { }); } -const c1 = ['background-color']; export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) { if (rf & ɵRenderFlags.Create) { ɵɵelementStart(0, 'tree'); { ɵɵelementStart(1, 'span'); - ɵɵstyling(null, c1); + ɵɵstyling(); { ɵɵtext(2); } ɵɵelementEnd(); ɵɵcontainer(3); @@ -48,8 +47,8 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) { } if (rf & ɵRenderFlags.Update) { ɵɵselect(1); - ɵɵstyleProp(0, ctx.depth % 2 ? '' : 'grey'); - ɵɵstyling(); + ɵɵstyleProp('background-color', ctx.depth % 2 ? '' : 'grey'); + ɵɵstylingApply(); ɵɵselect(2); ɵɵtextInterpolate1(' ', ctx.value, ' '); ɵɵcontainerRefreshStart(3); 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 50f048ebed..fc3581ad51 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -478,24 +478,21 @@ describe('compiler compliance', () => { const factory = 'factory: function MyComponent_Factory(t) { return new (t || MyComponent)(); }'; const template = ` - const $e0_classBindings$ = ["error"]; - const $e0_styleBindings$ = ["background-color"]; - … MyComponent.ngComponentDef = i0.ɵɵdefineComponent({type:MyComponent,selectors:[["my-component"]], factory: function MyComponent_Factory(t){ return new (t || MyComponent)(); }, consts: 1, - vars: 0, + vars: 2, template: function MyComponent_Template(rf,ctx){ if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling($e0_classBindings$, $e0_styleBindings$); + $r3$.ɵɵstyling(); $r3$.ɵɵelementEnd(); } if (rf & 2) { - $r3$.ɵɵstyleProp(0, ctx.color); - $r3$.ɵɵclassProp(0, ctx.error); + $r3$.ɵɵstyleProp("background-color", ctx.color); + $r3$.ɵɵclassProp("error", ctx.error); $r3$.ɵɵstylingApply(); } }, diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts index fc89d8e9f6..09d5b2b081 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts @@ -884,6 +884,7 @@ describe('compiler compliance: bindings', () => { factory: function HostAttributeDir_Factory(t) { return new (t || HostAttributeDir)(); }, hostBindings: function HostAttributeDir_HostBindings(rf, ctx, elIndex) { if (rf & 1) { + $r3$.ɵɵallocHostVars(2); $r3$.ɵɵelementHostAttrs($c1$); … } 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 478e3b9811..01bc7c9619 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -6,10 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {AttributeMarker, ViewEncapsulation} from '@angular/compiler/src/core'; -import {CompilerStylingMode, compilerSetStylingMode} from '@angular/compiler/src/render3/view/styling_state'; +import {AttributeMarker} from '@angular/compiler/src/core'; import {setup} from '@angular/compiler/test/aot/test_util'; - import {compile, expectEmit} from './mock_compile'; describe('compiler compliance: styling', () => { @@ -385,10 +383,11 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(null, null, $r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyling(); $r3$.ɵɵelementEnd(); } if (rf & 2) { + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap($ctx$.myStyleExp); $r3$.ɵɵstylingApply(); } @@ -446,7 +445,7 @@ describe('compiler compliance: styling', () => { const template = ` … consts: 1, - vars: 1, + vars: 2, template: function MyComponentWithInterpolation_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); @@ -460,7 +459,7 @@ describe('compiler compliance: styling', () => { } … consts: 1, - vars: 2, + vars: 3, template: function MyComponentWithMuchosInterpolation_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); @@ -474,7 +473,7 @@ describe('compiler compliance: styling', () => { } … consts: 1, - vars: 0, + vars: 1, template: function MyComponentWithoutInterpolation_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); @@ -521,7 +520,6 @@ describe('compiler compliance: styling', () => { const template = ` const $_c0$ = [${AttributeMarker.Styles}, "opacity", "1"]; - const $_c1$ = ["width", "height"]; … MyComponent.ngComponentDef = $r3$.ɵɵdefineComponent({ type: MyComponent, @@ -530,17 +528,18 @@ describe('compiler compliance: styling', () => { return new (t || MyComponent)(); }, consts: 1, - vars: 1, + vars: 4, template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div", $_c0$); - $r3$.ɵɵstyling(null, $_c1$, $r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyling(); $r3$.ɵɵelementEnd(); } if (rf & 2) { + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap($ctx$.myStyleExp); - $r3$.ɵɵstyleProp(0, $ctx$.myWidth); - $r3$.ɵɵstyleProp(1, $ctx$.myHeight); + $r3$.ɵɵstyleProp("width", $ctx$.myWidth); + $r3$.ɵɵstyleProp("height", $ctx$.myHeight); $r3$.ɵɵstylingApply(); $r3$.ɵɵattribute("style", "border-width: 10px", $r3$.ɵɵsanitizeStyle); } @@ -575,13 +574,6 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $_c0$ = ["background-image"]; - export class MyComponent { - constructor() { - this.myImage = 'url(foo.jpg)'; - } - } - MyComponent.ngComponentDef = $r3$.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], @@ -589,15 +581,16 @@ describe('compiler compliance: styling', () => { return new (t || MyComponent)(); }, consts: 1, - vars: 0, + vars: 1, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(null, _c0, $r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyling(); $r3$.ɵɵelementEnd(); } if (rf & 2) { - $r3$.ɵɵstyleProp(0, ctx.myImage); + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyleProp("background-image", ctx.myImage); $r3$.ɵɵstylingApply(); } }, @@ -629,16 +622,14 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $e0_styles$ = ["font-size"]; - … template: function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(null, _c0); + $r3$.ɵɵstyling(); $r3$.ɵɵelementEnd(); } if (rf & 2) { - $r3$.ɵɵstyleProp(0, 12, "px"); + $r3$.ɵɵstyleProp("font-size", 12, "px"); $r3$.ɵɵstylingApply(); } } @@ -646,7 +637,6 @@ describe('compiler compliance: styling', () => { const result = compile(files, angularFiles); expectEmit(result.source, template, 'Incorrect template'); - }); it('should not create instructions for empty style bindings', () => { @@ -742,7 +732,6 @@ describe('compiler compliance: styling', () => { const template = ` const $e0_attrs$ = [${AttributeMarker.Classes}, "grape"]; - const $e0_bindings$ = ["apple", "orange"]; … MyComponent.ngComponentDef = $r3$.ɵɵdefineComponent({ type: MyComponent, @@ -751,17 +740,17 @@ describe('compiler compliance: styling', () => { return new (t || MyComponent)(); }, consts: 1, - vars: 1, + vars: 4, template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div", $e0_attrs$); - $r3$.ɵɵstyling($e0_bindings$); + $r3$.ɵɵstyling(); $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵclassMap($ctx$.myClassExp); - $r3$.ɵɵclassProp(0, $ctx$.yesToApple); - $r3$.ɵɵclassProp(1, $ctx$.yesToOrange); + $r3$.ɵɵclassProp("apple", $ctx$.yesToApple); + $r3$.ɵɵclassProp("orange", $ctx$.yesToOrange); $r3$.ɵɵstylingApply(); $r3$.ɵɵattribute("class", "banana"); } @@ -874,10 +863,11 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(null, null, $r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyling(); $r3$.ɵɵelementEnd(); } if (rf & 2) { + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap($ctx$.myStyleExp); $r3$.ɵɵclassMap($ctx$.myClassExp); $r3$.ɵɵstylingApply(); @@ -915,14 +905,15 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(null, null, $r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyling(); $r3$.ɵɵpipe(1, "stylePipe"); $r3$.ɵɵpipe(2, "classPipe"); $r3$.ɵɵelementEnd(); } if (rf & 2) { - $r3$.ɵɵstyleMap($r3$.ɵɵpipeBind1(1, 0, $ctx$.myStyleExp)); - $r3$.ɵɵclassMap($r3$.ɵɵpipeBind1(2, 2, $ctx$.myClassExp)); + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyleMap($r3$.ɵɵpipeBind1(1, 2, $ctx$.myStyleExp)); + $r3$.ɵɵclassMap($r3$.ɵɵpipeBind1(2, 4, $ctx$.myClassExp)); $r3$.ɵɵstylingApply(); } } @@ -963,13 +954,10 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $e0_classBindings$ = ["foo"]; - const $e0_styleBindings$ = ["bar", "baz"]; - … template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling($e0_classBindings$, $e0_styleBindings$, $r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyling(); $r3$.ɵɵpipe(1, "pipe"); $r3$.ɵɵpipe(2, "pipe"); $r3$.ɵɵpipe(3, "pipe"); @@ -978,11 +966,12 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵelementEnd(); } if (rf & 2) { - $r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 1, $ctx$.myStyleExp, 1000)); + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 6, $ctx$.myStyleExp, 1000)); $r3$.ɵɵclassMap($e2_styling$); - $r3$.ɵɵstyleProp(0, $r3$.ɵɵpipeBind2(2, 4, $ctx$.barExp, 3000)); - $r3$.ɵɵstyleProp(1, $r3$.ɵɵpipeBind2(3, 7, $ctx$.bazExp, 4000)); - $r3$.ɵɵclassProp(0, $r3$.ɵɵpipeBind2(4, 10, $ctx$.fooExp, 2000)); + $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$.ɵɵselect(5); $r3$.ɵɵtextInterpolate1(" ", $ctx$.item, ""); @@ -1027,16 +1016,16 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, $ctx$) { … if (rf & 2) { - $r3$.ɵɵstyleProp(0, $ctx$.w1); + $r3$.ɵɵstyleProp("width", $ctx$.w1); $r3$.ɵɵstylingApply(); $r3$.ɵɵselect(1); - $r3$.ɵɵstyleProp(0, $ctx$.h1); + $r3$.ɵɵstyleProp("height", $ctx$.h1); $r3$.ɵɵstylingApply(); $r3$.ɵɵselect(2); - $r3$.ɵɵclassProp(0, $ctx$.a1); + $r3$.ɵɵclassProp("active", $ctx$.a1); $r3$.ɵɵstylingApply(); $r3$.ɵɵselect(3); - $r3$.ɵɵclassProp(0, $ctx$.r1); + $r3$.ɵɵclassProp("removed", $ctx$.r1); $r3$.ɵɵstylingApply(); } } @@ -1083,20 +1072,18 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $e0_attrs$ = [${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"]; - const $e0_classBindings$ = ["foo"]; - const $e0_styleBindings$ = ["color"]; - … hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { if (rf & 1) { + $r3$.ɵɵallocHostVars(4); $r3$.ɵɵelementHostAttrs($e0_attrs$); - $r3$.ɵɵstyling($e0_classBindings$, $e0_styleBindings$, $r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyling(); } if (rf & 2) { + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap(ctx.myStyle); $r3$.ɵɵclassMap(ctx.myClass); - $r3$.ɵɵstyleProp(0, ctx.myColorProp); - $r3$.ɵɵclassProp(0, ctx.myFooClass); + $r3$.ɵɵstyleProp("color", ctx.myColorProp); + $r3$.ɵɵclassProp("foo", ctx.myFooClass); $r3$.ɵɵstylingApply(); } }, @@ -1146,20 +1133,19 @@ describe('compiler compliance: styling', () => { }; const template = ` - const _c0 = ["bar", "foo"]; - const _c1 = ["height", "width"]; - … hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - $r3$.ɵɵstyling(_c0, _c1, $r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵallocHostVars(6); + $r3$.ɵɵstyling(); } if (rf & 2) { + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap(ctx.myStyle); $r3$.ɵɵclassMap(ctx.myClasses); - $r3$.ɵɵstyleProp(0, ctx.myHeightProp, "pt"); - $r3$.ɵɵstyleProp(1, ctx.myWidthProp); - $r3$.ɵɵclassProp(0, ctx.myBarClass); - $r3$.ɵɵclassProp(1, ctx.myFooClass); + $r3$.ɵɵstyleProp("height", ctx.myHeightProp, "pt"); + $r3$.ɵɵstyleProp("width", ctx.myWidthProp); + $r3$.ɵɵclassProp("bar", ctx.myBarClass); + $r3$.ɵɵclassProp("foo", ctx.myFooClass); $r3$.ɵɵstylingApply(); } }, @@ -1209,38 +1195,35 @@ describe('compiler compliance: styling', () => { }; const template = ` - const _c2 = ["bar"]; - const _c3 = ["height"]; - … function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵstyling(_c2, _c3, $r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyling(); $r3$.ɵɵelementEnd(); } if (rf & 2) { + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap(ctx.myStyleExp); $r3$.ɵɵclassMap(ctx.myClassExp); - $r3$.ɵɵstyleProp(0, ctx.myHeightExp, null, true); - $r3$.ɵɵclassProp(0, ctx.myBarClassExp, true); + $r3$.ɵɵstyleProp("height", ctx.myHeightExp); + $r3$.ɵɵclassProp("bar", ctx.myBarClassExp); $r3$.ɵɵstylingApply(); } }, `; const hostBindings = ` - const _c0 = ["foo"]; - const _c1 = ["width"]; - … hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - $r3$.ɵɵstyling(_c0, _c1, $r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵallocHostVars(4); + $r3$.ɵɵstyling(); } if (rf & 2) { + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap(ctx.myStyleExp); $r3$.ɵɵclassMap(ctx.myClassExp); - $r3$.ɵɵstyleProp(0, ctx.myWidthExp, null, true); - $r3$.ɵɵclassProp(0, ctx.myFooClassExp, true); + $r3$.ɵɵstyleProp("width", ctx.myWidthExp); + $r3$.ɵɵclassProp("foo", ctx.myFooClassExp); $r3$.ɵɵstylingApply(); } }, @@ -1296,13 +1279,9 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $widthDir_classes$ = ["foo"]; - const $widthDir_styles$ = ["width"]; - const $heightDir_classes$ = ["bar"]; - const $heightDir_styles$ = ["height"]; - … function ClassDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { + $r3$.ɵɵallocHostVars(1); $r3$.ɵɵstyling(); } if (rf & 2) { @@ -1313,22 +1292,24 @@ describe('compiler compliance: styling', () => { … function WidthDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - $r3$.ɵɵstyling($widthDir_classes$, $widthDir_styles$); + $r3$.ɵɵallocHostVars(2); + $r3$.ɵɵstyling(); } if (rf & 2) { - $r3$.ɵɵstyleProp(0, ctx.myWidth); - $r3$.ɵɵclassProp(0, ctx.myFooClass); + $r3$.ɵɵstyleProp("width", ctx.myWidth); + $r3$.ɵɵclassProp("foo", ctx.myFooClass); $r3$.ɵɵstylingApply(); } } … function HeightDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - $r3$.ɵɵstyling($heightDir_classes$, $heightDir_styles$); + $r3$.ɵɵallocHostVars(2); + $r3$.ɵɵstyling(); } if (rf & 2) { - $r3$.ɵɵstyleProp(0, ctx.myHeight); - $r3$.ɵɵclassProp(0, ctx.myBarClass); + $r3$.ɵɵstyleProp("height", ctx.myHeight); + $r3$.ɵɵclassProp("bar", ctx.myBarClass); $r3$.ɵɵstylingApply(); } } @@ -1472,34 +1453,34 @@ describe('compiler compliance: styling', () => { const template = ` … if (rf & 2) { - $r3$.ɵɵstylePropInterpolateV(0, ["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$.ɵɵ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$.ɵɵselect(1); - $r3$.ɵɵstylePropInterpolate8(0, "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$.ɵɵ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$.ɵɵselect(2); - $r3$.ɵɵstylePropInterpolate7(0, "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h"); + $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$.ɵɵselect(3); - $r3$.ɵɵstylePropInterpolate6(0, "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g"); + $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$.ɵɵselect(4); - $r3$.ɵɵstylePropInterpolate5(0, "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f"); + $r3$.ɵɵstylePropInterpolate5("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f"); $r3$.ɵɵstylingApply(); $r3$.ɵɵselect(5); - $r3$.ɵɵstylePropInterpolate4(0, "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e"); + $r3$.ɵɵstylePropInterpolate4("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e"); $r3$.ɵɵstylingApply(); $r3$.ɵɵselect(6); - $r3$.ɵɵstylePropInterpolate3(0, "a", ctx.one, "b", ctx.two, "c", ctx.three, "d"); + $r3$.ɵɵstylePropInterpolate3("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d"); $r3$.ɵɵstylingApply(); $r3$.ɵɵselect(7); - $r3$.ɵɵstylePropInterpolate2(0, "a", ctx.one, "b", ctx.two, "c"); + $r3$.ɵɵstylePropInterpolate2("color", "a", ctx.one, "b", ctx.two, "c"); $r3$.ɵɵstylingApply(); $r3$.ɵɵselect(8); - $r3$.ɵɵstylePropInterpolate1(0, "a", ctx.one, "b"); + $r3$.ɵɵstylePropInterpolate1("color", "a", ctx.one, "b"); $r3$.ɵɵstylingApply(); $r3$.ɵɵselect(9); - $r3$.ɵɵstyleProp(0, ctx.one); + $r3$.ɵɵstyleProp("color", ctx.one); $r3$.ɵɵstylingApply(); } … @@ -1530,7 +1511,7 @@ describe('compiler compliance: styling', () => { const template = ` … if (rf & 2) { - $r3$.ɵɵstylePropInterpolate2(0, "a", ctx.one, "b", ctx.two, "c", "px"); + $r3$.ɵɵstylePropInterpolate2("width", "a", ctx.one, "b", ctx.two, "c", "px"); $r3$.ɵɵstylingApply(); } … @@ -1561,7 +1542,7 @@ describe('compiler compliance: styling', () => { const template = ` … if (rf & 2) { - $r3$.ɵɵstylePropInterpolate2(0, "a", ctx.one, "b", ctx.two, "c", null, true); + $r3$.ɵɵstylePropInterpolate2("width", "a", ctx.one, "b", ctx.two, "c"); $r3$.ɵɵstylingApply(); } … @@ -1616,12 +1597,13 @@ describe('compiler compliance: styling', () => { … hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - $r3$.ɵɵallocHostVars(2); + $r3$.ɵɵallocHostVars(4); $r3$.ɵɵelementHostAttrs($_c0$); - $r3$.ɵɵstyling(null, null, $r3$.ɵɵdefaultStyleSanitizer); + $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(); @@ -1658,18 +1640,15 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $_c0$ = ["foo"]; - const $_c1$ = ["width"]; - … hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - $r3$.ɵɵallocHostVars(2); - $r3$.ɵɵstyling($_c0$, $_c1$); + $r3$.ɵɵallocHostVars(4); + $r3$.ɵɵstyling(); } if (rf & 2) { $r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title); - $r3$.ɵɵstyleProp(0, ctx.myWidth); - $r3$.ɵɵclassProp(0, ctx.myFooClass); + $r3$.ɵɵstyleProp("width", ctx.myWidth); + $r3$.ɵɵclassProp("foo", ctx.myFooClass); $r3$.ɵɵstylingApply(); } } @@ -1680,10 +1659,6 @@ describe('compiler compliance: styling', () => { }); describe('new styling refactor', () => { - beforeEach(() => { compilerSetStylingMode(CompilerStylingMode.UseNew); }); - - afterEach(() => { compilerSetStylingMode(CompilerStylingMode.UseOld); }); - it('should generate a `styleSanitizer` instruction when one or more sanitizable style properties are statically detected', () => { const files = { @@ -1709,7 +1684,7 @@ describe('compiler compliance: styling', () => { … if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); - $r3$.ɵɵstyleProp(0, ctx.bgExp); + $r3$.ɵɵstyleProp("background-image", ctx.bgExp); $r3$.ɵɵstylingApply(); } … @@ -1782,7 +1757,7 @@ describe('compiler compliance: styling', () => { … if (rf & 2) { $r3$.ɵɵclassMap(ctx.mapExp); - $r3$.ɵɵclassProp(0, ctx.nameExp); + $r3$.ɵɵclassProp("name", ctx.nameExp); $r3$.ɵɵstylingApply(); } … @@ -1792,5 +1767,67 @@ describe('compiler compliance: styling', () => { const result = compile(files, angularFiles); expectEmit(result.source, template, 'Incorrect template'); }); + + it('should generate the correct amount of host bindings when styling is present', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Directive, NgModule} from '@angular/core'; + + @Directive({ + selector: '[my-dir]', + host: { + '[title]': 'title', + '[class.foo]': 'foo', + '[@anim]': \`{ + value: _animValue, + params: { + param1: _animParam1, + param2: _animParam2 + } + }\` + } + }) + export class MyDir { + title = ''; + foo = true; + _animValue = null; + _animParam1 = null; + _animParam2 = null; + } + + @Component({ + selector: 'my-app', + template: \` +
+ \` + }) + export class MyAppComp {} + + @NgModule({declarations: [MyAppComp, MyDir]}) + export class MyModule {} + ` + } + }; + + const template = ` + hostBindings: function MyDir_HostBindings(rf, ctx, elIndex) { + … + $r3$.ɵɵallocHostVars(9); + … + if (rf & 2) { + $r3$.ɵɵhostProperty("title", ctx.title); + $r3$.ɵɵupdateSyntheticHostBinding("@anim", + $r3$.ɵɵpureFunction2(6, _c1, ctx._animValue, + $r3$.ɵɵpureFunction2(3, _c0, ctx._animParam1, ctx._animParam2))); + $r3$.ɵɵclassProp("foo", ctx.foo); + $r3$.ɵɵstylingApply(); + } + } + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 6bd962b9d6..a89bd1c796 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -1825,16 +1825,16 @@ runInEachFileSystem(os => { const hostBindingsFn = ` hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - i0.ɵɵallocHostVars(2); + i0.ɵɵallocHostVars(3); 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(_c0); + i0.ɵɵstyling(); } if (rf & 2) { i0.ɵɵhostProperty("prop", ctx.bar); i0.ɵɵattribute("hello", ctx.foo); - i0.ɵɵclassProp(0, ctx.someClass); + i0.ɵɵclassProp("someclass", ctx.someClass); i0.ɵɵstylingApply(); } } diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 401e3331f6..7b6fc0deb2 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -586,14 +586,8 @@ function createHostBindingsFunction( hostBindingsMetadata: R3HostMetadata, typeSourceSpan: ParseSourceSpan, bindingParser: BindingParser, constantPool: ConstantPool, selector: string, name?: string): o.Expression|null { - // Initialize hostVarsCount to number of bound host properties (interpolations illegal), - // except 'style' and 'class' properties, since they should *not* allocate host var slots - const hostVarsCount = Object.keys(hostBindingsMetadata.properties) - .filter(name => { - const prefix = getStylingPrefix(name); - return prefix !== 'style' && prefix !== 'class'; - }) - .length; + // Initialize hostVarsCount to number of bound host properties (interpolations illegal) + const hostVarsCount = Object.keys(hostBindingsMetadata.properties).length; const elVarExp = o.variable('elIndex'); const bindingContext = o.variable(CONTEXT_NAME); const styleBuilder = new StylingBuilder(elVarExp, bindingContext); @@ -733,7 +727,10 @@ function createHostBindingsFunction( // the update block of a component/directive templateFn/hostBindingsFn so that the bindings // are evaluated and updated for the element. styleBuilder.buildUpdateLevelInstructions(getValueConverter()).forEach(instruction => { - totalHostVarsCount += instruction.allocateBindingSlots; + // we subtract a value of `1` here because the binding slot was already + // allocated at the top of this method when all the input bindings were + // counted. + totalHostVarsCount += Math.max(instruction.allocateBindingSlots - 1, 0); updateStatements.push(createStylingStmt(instruction, bindingContext, bindingFn)); }); } diff --git a/packages/compiler/src/render3/view/styling_builder.ts b/packages/compiler/src/render3/view/styling_builder.ts index 26133a1eed..b7d06f64d4 100644 --- a/packages/compiler/src/render3/view/styling_builder.ts +++ b/packages/compiler/src/render3/view/styling_builder.ts @@ -15,8 +15,7 @@ import {error} from '../../util'; import * as t from '../r3_ast'; import {Identifiers as R3} from '../r3_identifiers'; -import {parse as parseStyle} from './style_parser'; -import {compilerIsNewStylingInUse} from './styling_state'; +import {hyphenate, parse as parseStyle} from './style_parser'; import {ValueConverter} from './template'; import {getInterpolationArgsLength} from './util'; @@ -69,7 +68,7 @@ interface BoundStylingEntry { * classMap(...) * styleProp(...) * classProp(...) - * stylingApp(...) + * stylingApply(...) * } * * The creation/update methods within the builder class produce these instructions. @@ -171,6 +170,7 @@ export class StylingBuilder { if (isEmptyExpression(value)) { return null; } + name = normalizePropName(name); const {property, hasOverrideFlag, unit: bindingUnit} = parseProperty(name); const entry: BoundStylingEntry = { name: property, @@ -296,46 +296,7 @@ export class StylingBuilder { sourceSpan, allocateBindingSlots: 0, reference: R3.styling, - params: () => { - // a string array of every style-based binding - const styleBindingProps = - this._singleStyleInputs ? this._singleStyleInputs.map(i => o.literal(i.name)) : []; - // a string array of every class-based binding - const classBindingNames = - this._singleClassInputs ? this._singleClassInputs.map(i => o.literal(i.name)) : []; - - // to salvage space in the AOT generated code, there is no point in passing - // in `null` into a param if any follow-up params are not used. Therefore, - // only when a trailing param is used then it will be filled with nulls in between - // (otherwise a shorter amount of params will be filled). The code below helps - // determine how many params are required in the expression code. - // - // min params => styling() - // max params => styling(classBindings, styleBindings, sanitizer) - // - const params: o.Expression[] = []; - let expectedNumberOfArgs = 0; - if (this._useDefaultSanitizer) { - expectedNumberOfArgs = 3; - } else if (styleBindingProps.length) { - expectedNumberOfArgs = 2; - } else if (classBindingNames.length) { - expectedNumberOfArgs = 1; - } - - addParam( - params, classBindingNames.length > 0, - getConstantLiteralFromArray(constantPool, classBindingNames), 1, - expectedNumberOfArgs); - addParam( - params, styleBindingProps.length > 0, - getConstantLiteralFromArray(constantPool, styleBindingProps), 2, - expectedNumberOfArgs); - addParam( - params, this._useDefaultSanitizer, o.importExpr(R3.defaultStyleSanitizer), 3, - expectedNumberOfArgs); - return params; - } + params: () => [], }; } return null; @@ -370,12 +331,8 @@ export class StylingBuilder { private _buildMapBasedInstruction( valueConverter: ValueConverter, isClassBased: boolean, stylingInput: BoundStylingEntry): StylingInstruction { - let totalBindingSlotsRequired = 0; - if (compilerIsNewStylingInUse()) { - // the old implementation does not reserve slot values for - // binding entries. The new one does. - totalBindingSlotsRequired++; - } + // each styling binding value is stored in the LView + let totalBindingSlotsRequired = 1; // these values must be outside of the update block so that they can // be evaluated (the AST visit call) during creation time so that any @@ -408,8 +365,11 @@ export class StylingBuilder { StylingInstruction[] { let totalBindingSlotsRequired = 0; return inputs.map(input => { - const bindingIndex: number = mapIndex.get(input.name !) !; const value = input.value.visit(valueConverter); + + // each styling binding value is stored in the LView + let totalBindingSlotsRequired = 1; + if (value instanceof Interpolation) { totalBindingSlotsRequired += value.expressions.length; @@ -417,35 +377,25 @@ export class StylingBuilder { reference = getInterpolationExpressionFn(value); } } - if (compilerIsNewStylingInUse()) { - // the old implementation does not reserve slot values for - // binding entries. The new one does. - totalBindingSlotsRequired++; - } + return { sourceSpan: input.sourceSpan, supportsInterpolation: !!getInterpolationExpressionFn, allocateBindingSlots: totalBindingSlotsRequired, reference, params: (convertFn: (value: any) => o.Expression | o.Expression[]) => { - // min params => stylingProp(elmIndex, bindingIndex, value) - // max params => stylingProp(elmIndex, bindingIndex, value, overrideFlag) - const params: o.Expression[] = [o.literal(bindingIndex)]; + // params => stylingProp(propName, value) + const params: o.Expression[] = []; + params.push(o.literal(input.name)); + const convertResult = convertFn(value); if (Array.isArray(convertResult)) { params.push(...convertResult); } else { params.push(convertResult); } - if (allowUnits) { - if (input.unit) { - params.push(o.literal(input.unit)); - } else if (input.hasOverrideFlag) { - params.push(o.NULL_EXPR); - } - } - if (input.hasOverrideFlag) { - params.push(o.literal(true)); + if (allowUnits && input.unit) { + params.push(o.literal(input.unit)); } return params; @@ -496,7 +446,7 @@ export class StylingBuilder { buildUpdateLevelInstructions(valueConverter: ValueConverter) { const instructions: StylingInstruction[] = []; if (this.hasBindings) { - if (compilerIsNewStylingInUse() && this._useDefaultSanitizer) { + if (this._useDefaultSanitizer) { instructions.push(this._buildSanitizerFn()); } const styleMapInstruction = this.buildStyleMapInstruction(valueConverter); @@ -630,3 +580,7 @@ function getStylePropInterpolationExpression(interpolation: Interpolation) { return R3.stylePropInterpolateV; } } + +function normalizePropName(prop: string): string { + return hyphenate(prop); +} diff --git a/packages/compiler/src/render3/view/styling_state.ts b/packages/compiler/src/render3/view/styling_state.ts deleted file mode 100644 index c63ab6d8e6..0000000000 --- a/packages/compiler/src/render3/view/styling_state.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** -* @license -* Copyright Google Inc. All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ - -/** - * A temporary enum of states that inform the core whether or not - * to defer all styling instruction calls to the old or new - * styling implementation. - */ -export const enum CompilerStylingMode { - UseOld = 0, - UseBothOldAndNew = 1, - UseNew = 2, -} - -let _stylingMode = 0; - -/** - * Temporary function used to inform the existing styling algorithm - * code to delegate all styling instruction calls to the new refactored - * styling code. - */ -export function compilerSetStylingMode(mode: CompilerStylingMode) { - _stylingMode = mode; -} - -export function compilerIsNewStylingInUse() { - return _stylingMode > CompilerStylingMode.UseOld; -} - -export function compilerAllowOldStyling() { - return _stylingMode < CompilerStylingMode.UseNew; -} diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 128a9bc29d..8bf4c36253 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -122,6 +122,7 @@ export { ɵɵelementContainer, ɵɵstyling, ɵɵstyleMap, + ɵɵstyleSanitizer, ɵɵclassMap, ɵɵclassMapInterpolate1, ɵɵclassMapInterpolate2, diff --git a/packages/core/src/debug/debug_node.ts b/packages/core/src/debug/debug_node.ts index 09580ffbcd..f599614088 100644 --- a/packages/core/src/debug/debug_node.ts +++ b/packages/core/src/debug/debug_node.ts @@ -10,11 +10,12 @@ import {Injector} from '../di'; import {getViewComponent} from '../render3/global_utils_api'; import {CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from '../render3/interfaces/container'; import {TElementNode, TNode, TNodeFlags, TNodeType} from '../render3/interfaces/node'; -import {StylingIndex} from '../render3/interfaces/styling'; import {isComponent, isLContainer} from '../render3/interfaces/type_checks'; import {LView, PARENT, TData, TVIEW, T_HOST} from '../render3/interfaces/view'; -import {getProp, getValue, isClassBasedValue} from '../render3/styling/class_and_style_bindings'; -import {getStylingContextFromLView} from '../render3/styling/util'; +import {TStylingContext} from '../render3/styling_next/interfaces'; +import {stylingMapToStringMap} from '../render3/styling_next/map_based_bindings'; +import {NodeStylingDebug} from '../render3/styling_next/styling_debug'; +import {isStylingContext} from '../render3/styling_next/util'; import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, getLocalRefs, isBrowserEvents, loadLContext, loadLContextFromNode} from '../render3/util/discovery_utils'; import {INTERPOLATION_DELIMITER, isPropMetadataString, renderStringify} from '../render3/util/misc_utils'; import {findComponentView} from '../render3/util/view_traversal_utils'; @@ -328,63 +329,12 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme return attributes; } - get classes(): {[key: string]: boolean;} { - const classes: {[key: string]: boolean;} = {}; - const element = this.nativeElement; - if (element) { - const lContext = loadLContextFromNode(element); - const stylingContext = getStylingContextFromLView(lContext.nodeIndex, lContext.lView); - if (stylingContext) { - for (let i = StylingIndex.SingleStylesStartPosition; i < stylingContext.length; - i += StylingIndex.Size) { - if (isClassBasedValue(stylingContext, i)) { - const className = getProp(stylingContext, i); - const value = getValue(stylingContext, i); - if (typeof value == 'boolean') { - // we want to ignore `null` since those don't overwrite the values. - classes[className] = value; - } - } - } - } else { - // Fallback, just read DOM. - const eClasses = element.classList; - for (let i = 0; i < eClasses.length; i++) { - classes[eClasses[i]] = true; - } - } - } - return classes; + get styles(): {[key: string]: string | null;} { + return _getStylingDebugInfo(this.nativeElement, false); } - get styles(): {[key: string]: string | null;} { - const styles: {[key: string]: string | null;} = {}; - const element = this.nativeElement; - if (element) { - const lContext = loadLContextFromNode(element); - const stylingContext = getStylingContextFromLView(lContext.nodeIndex, lContext.lView); - if (stylingContext) { - for (let i = StylingIndex.SingleStylesStartPosition; i < stylingContext.length; - i += StylingIndex.Size) { - if (!isClassBasedValue(stylingContext, i)) { - const styleName = getProp(stylingContext, i); - const value = getValue(stylingContext, i) as string | null; - if (value !== null) { - // we want to ignore `null` since those don't overwrite the values. - styles[styleName] = value; - } - } - } - } else { - // Fallback, just read DOM. - const eStyles = (element as HTMLElement).style; - for (let i = 0; i < eStyles.length; i++) { - const name = eStyles.item(i); - styles[name] = eStyles.getPropertyValue(name); - } - } - } - return styles; + get classes(): {[key: string]: boolean;} { + return _getStylingDebugInfo(this.nativeElement, true); } get childNodes(): DebugNode[] { @@ -435,6 +385,25 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme } } +function _getStylingDebugInfo(element: any, isClassBased: boolean) { + if (element) { + const context = loadLContextFromNode(element); + const lView = context.lView; + const tData = lView[TVIEW].data; + const tNode = tData[context.nodeIndex] as TNode; + if (isClassBased) { + return isStylingContext(tNode.classes) ? + new NodeStylingDebug(tNode.classes as TStylingContext, lView, true).values : + stylingMapToStringMap(tNode.classes); + } else { + return isStylingContext(tNode.styles) ? + new NodeStylingDebug(tNode.styles as TStylingContext, lView, false).values : + stylingMapToStringMap(tNode.styles); + } + } + return {}; +} + /** * Walk the TNode tree to find matches for the predicate. * diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 35922dfd9f..9cd49a3045 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -17,7 +17,7 @@ import {assertComponentType} from './assert'; import {getComponentDef} from './definition'; import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di'; import {registerPostOrderHooks, registerPreOrderHooks} from './hooks'; -import {CLEAN_PROMISE, addToViewTree, createLView, createTView, getOrCreateTNode, getOrCreateTView, initNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions/shared'; +import {CLEAN_PROMISE, addToViewTree, createLView, createTView, getOrCreateTNode, getOrCreateTView, initNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews, renderInitialStyling} from './instructions/shared'; import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition'; import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node'; import {PlayerHandler} from './interfaces/player'; @@ -230,10 +230,10 @@ export function createRootComponent( setActiveHostElement(null); } - if (rootTNode.stylingTemplate) { + if (rootTNode.classes !== null || rootTNode.styles !== null) { const native = componentView[HOST] !as RElement; - renderInitialClasses(native, rootTNode.stylingTemplate, componentView[RENDERER]); - renderInitialStyles(native, rootTNode.stylingTemplate, componentView[RENDERER]); + const renderer = componentView[RENDERER]; + renderInitialStyling(renderer, native, rootTNode); } return component; diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index ea28825677..0c08142c5a 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -22,6 +22,7 @@ import {isComponent, isComponentDef} from './interfaces/type_checks'; import {DECLARATION_VIEW, INJECTOR, LView, TData, TVIEW, TView, T_HOST} from './interfaces/view'; import {assertNodeOfPossibleTypes} from './node_assert'; import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state'; +import {getInitialStylingValue} from './styling_next/util'; import {isNameOnlyAttributeMarker} from './util/attrs_utils'; import {getParentInjectorIndex, getParentInjectorView, hasParentInjector} from './util/injector_utils'; import {stringifyForError} from './util/misc_utils'; @@ -269,6 +270,13 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str ngDevMode && assertNodeOfPossibleTypes( tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer); ngDevMode && assertDefined(tNode, 'expecting tNode'); + if (attrNameToInject === 'class') { + return getInitialStylingValue(tNode.classes); + } + if (attrNameToInject === 'style') { + return getInitialStylingValue(tNode.styles); + } + const attrs = tNode.attrs; if (attrs) { const attrsLength = attrs.length; @@ -289,22 +297,8 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str } else if (typeof value === 'number') { // Skip to the first value of the marked attribute. i++; - if (value === AttributeMarker.Classes && attrNameToInject === 'class') { - let accumulatedClasses = ''; - while (i < attrsLength && typeof attrs[i] === 'string') { - accumulatedClasses += ' ' + attrs[i++]; - } - return accumulatedClasses.trim(); - } else if (value === AttributeMarker.Styles && attrNameToInject === 'style') { - let accumulatedStyles = ''; - while (i < attrsLength && typeof attrs[i] === 'string') { - accumulatedStyles += `${attrs[i++]}: ${attrs[i++]}; `; - } - return accumulatedStyles.trim(); - } else { - while (i < attrsLength && typeof attrs[i] === 'string') { - i++; - } + while (i < attrsLength && typeof attrs[i] === 'string') { + i++; } } else if (value === attrNameToInject) { return attrs[i + 1] as string; diff --git a/packages/core/src/render3/instructions/all.ts b/packages/core/src/render3/instructions/all.ts index 27a23863ba..985f738ec2 100644 --- a/packages/core/src/render3/instructions/all.ts +++ b/packages/core/src/render3/instructions/all.ts @@ -43,8 +43,7 @@ export * from './projection'; export * from './property'; export * from './property_interpolation'; export * from './select'; -export * from './styling'; -export {styleSanitizer as ɵɵstyleSanitizer} from '../styling_next/instructions'; +export {ɵɵstyling, ɵɵstyleSanitizer, ɵɵstyleMap, ɵɵclassMap, ɵɵstyleProp, ɵɵclassProp, ɵɵstylingApply} from '../styling_next/instructions'; export * from './text'; export * from './text_interpolation'; export * from './class_map_interpolation'; diff --git a/packages/core/src/render3/instructions/class_map_interpolation.ts b/packages/core/src/render3/instructions/class_map_interpolation.ts index 69bf5318eb..ae968fb34c 100644 --- a/packages/core/src/render3/instructions/class_map_interpolation.ts +++ b/packages/core/src/render3/instructions/class_map_interpolation.ts @@ -7,10 +7,9 @@ */ import {getLView, getSelectedIndex} from '../state'; -import {NO_CHANGE} from '../tokens'; - +import {classMapInternal} from '../styling_next/instructions'; import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation'; -import {classMapInternal, getActiveDirectiveStylingIndex} from './styling'; + /** @@ -37,10 +36,7 @@ import {classMapInternal, getActiveDirectiveStylingIndex} from './styling'; export function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation1(lView, prefix, v0, suffix); - if (interpolatedValue !== NO_CHANGE) { - classMapInternal( - lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue); - } + classMapInternal(getSelectedIndex(), interpolatedValue); } /** @@ -70,10 +66,7 @@ export function ɵɵclassMapInterpolate2( prefix: string, v0: any, i0: string, v1: any, suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix); - if (interpolatedValue !== NO_CHANGE) { - classMapInternal( - lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue); - } + classMapInternal(getSelectedIndex(), interpolatedValue); } /** @@ -106,10 +99,7 @@ export function ɵɵclassMapInterpolate3( prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix); - if (interpolatedValue !== NO_CHANGE) { - classMapInternal( - lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue); - } + classMapInternal(getSelectedIndex(), interpolatedValue); } /** @@ -145,10 +135,7 @@ export function ɵɵclassMapInterpolate4( suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix); - if (interpolatedValue !== NO_CHANGE) { - classMapInternal( - lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue); - } + classMapInternal(getSelectedIndex(), interpolatedValue); } /** @@ -187,10 +174,7 @@ export function ɵɵclassMapInterpolate5( const lView = getLView(); const interpolatedValue = interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix); - if (interpolatedValue !== NO_CHANGE) { - classMapInternal( - lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue); - } + classMapInternal(getSelectedIndex(), interpolatedValue); } /** @@ -231,10 +215,7 @@ export function ɵɵclassMapInterpolate6( const lView = getLView(); const interpolatedValue = interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix); - if (interpolatedValue !== NO_CHANGE) { - classMapInternal( - lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue); - } + classMapInternal(getSelectedIndex(), interpolatedValue); } /** @@ -277,10 +258,7 @@ export function ɵɵclassMapInterpolate7( const lView = getLView(); const interpolatedValue = interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix); - if (interpolatedValue !== NO_CHANGE) { - classMapInternal( - lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue); - } + classMapInternal(getSelectedIndex(), interpolatedValue); } /** @@ -326,10 +304,7 @@ export function ɵɵclassMapInterpolate8( const lView = getLView(); const interpolatedValue = interpolation8( lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix); - if (interpolatedValue !== NO_CHANGE) { - classMapInternal( - lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue); - } + classMapInternal(getSelectedIndex(), interpolatedValue); } /** @@ -358,8 +333,5 @@ export function ɵɵclassMapInterpolate8( export function ɵɵclassMapInterpolateV(values: any[]): void { const lView = getLView(); const interpolatedValue = interpolationV(lView, values); - if (interpolatedValue !== NO_CHANGE) { - classMapInternal( - lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue); - } + classMapInternal(getSelectedIndex(), interpolatedValue); } diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 21736e7d3a..e6d4c6c6c1 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -9,23 +9,20 @@ import {assertDataInRange, assertDefined, assertEqual} from '../../util/assert'; import {assertHasParent} from '../assert'; import {attachPatchData} from '../context_discovery'; import {registerPostOrderHooks} from '../hooks'; -import {TAttributes, TNodeFlags, TNodeType} from '../interfaces/node'; +import {PropertyAliases, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; import {RElement} from '../interfaces/renderer'; -import {StylingContext} from '../interfaces/styling'; -import {BINDING_INDEX, HEADER_OFFSET, QUERIES, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; +import {BINDING_INDEX, HEADER_OFFSET, LView, QUERIES, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; import {applyOnCreateInstructions} from '../node_util'; import {decreaseElementDepthCount, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state'; -import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles} from '../styling/class_and_style_bindings'; -import {getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util'; -import {registerInitialStylingIntoContext} from '../styling_next/instructions'; -import {runtimeIsNewStylingInUse} from '../styling_next/state'; -import {attrsStylingIndexOf, setUpAttributes} from '../util/attrs_utils'; +import {registerInitialStylingOnTNode} from '../styling_next/instructions'; +import {StylingMapArray, TStylingContext} from '../styling_next/interfaces'; +import {getInitialStylingValue, hasClassInput, hasStyleInput} from '../styling_next/util'; +import {setUpAttributes} from '../util/attrs_utils'; import {getNativeByTNode, getTNode} from '../util/view_utils'; -import {createDirectivesAndLocals, elementCreate, executeContentQueries, getOrCreateTNode, initializeTNodeInputs, setInputsForProperty, setNodeStylingTemplate} from './shared'; -import {getActiveDirectiveStylingIndex} from './styling'; +import {createDirectivesAndLocals, elementCreate, executeContentQueries, getOrCreateTNode, initializeTNodeInputs, renderInitialStyling, setInputsForProperty} from './shared'; @@ -58,32 +55,16 @@ export function ɵɵelementStart( const renderer = lView[RENDERER]; const tNode = getOrCreateTNode(tView, lView[T_HOST], index, TNodeType.Element, name, attrs || null); - let initialStylesIndex = 0; - let initialClassesIndex = 0; - let lastAttrIndex = -1; - if (attrs) { - lastAttrIndex = setUpAttributes(native, attrs); - - // it's important to only prepare styling-related datastructures once for a given - // tNode and not each time an element is created. Also, the styling code is designed - // to be patched and constructed at various points, but only up until the styling - // template is first allocated (which happens when the very first style/class binding - // value is evaluated). When the template is allocated (when it turns into a context) - // then the styling template is locked and cannot be further extended (it can only be - // instantiated into a context per element) - setNodeStylingTemplate(tView, tNode, attrs, lastAttrIndex); - - const stylingTemplate = tNode.stylingTemplate; - if (stylingTemplate) { - // the initial style/class values are rendered immediately after having been - // initialized into the context so the element styling is ready when directives - // are initialized (since they may read style/class values in their constructor) - initialStylesIndex = renderInitialStyles(native, stylingTemplate, renderer); - initialClassesIndex = renderInitialClasses(native, stylingTemplate, renderer); + if (attrs != null) { + const lastAttrIndex = setUpAttributes(native, attrs); + if (tView.firstTemplatePass) { + registerInitialStylingOnTNode(tNode, attrs, lastAttrIndex); } } + renderInitialStyling(renderer, native, tNode); + appendChild(native, tNode, lView); createDirectivesAndLocals(tView, lView, tNode, localRefs); @@ -104,22 +85,12 @@ export function ɵɵelementStart( if (inputData && inputData.hasOwnProperty('class')) { tNode.flags |= TNodeFlags.hasClassInput; } + if (inputData && inputData.hasOwnProperty('style')) { tNode.flags |= TNodeFlags.hasStyleInput; } } - // we render the styling again below in case any directives have set any `style` and/or - // `class` host attribute values... - if (tNode.stylingTemplate) { - renderInitialClasses(native, tNode.stylingTemplate, renderer, initialClassesIndex); - renderInitialStyles(native, tNode.stylingTemplate, renderer, initialStylesIndex); - } - - if (runtimeIsNewStylingInUse() && lastAttrIndex >= 0) { - registerInitialStylingIntoContext(tNode, attrs as TAttributes, lastAttrIndex); - } - const currentQueries = lView[QUERIES]; if (currentQueries) { currentQueries.addNode(tNode); @@ -144,36 +115,29 @@ export function ɵɵelementEnd(): void { setPreviousOrParentTNode(previousOrParentTNode, false); } + const tNode = previousOrParentTNode; + // this is required for all host-level styling-related instructions to run // in the correct order - previousOrParentTNode.onElementCreationFns && applyOnCreateInstructions(previousOrParentTNode); + tNode.onElementCreationFns && applyOnCreateInstructions(tNode); - ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Element); + ngDevMode && assertNodeType(tNode, TNodeType.Element); const lView = getLView(); const currentQueries = lView[QUERIES]; // Go back up to parent queries only if queries have been cloned on this element. - if (currentQueries && previousOrParentTNode.index === currentQueries.nodeIndex) { + if (currentQueries && tNode.index === currentQueries.nodeIndex) { lView[QUERIES] = currentQueries.parent; } - registerPostOrderHooks(lView[TVIEW], previousOrParentTNode); + registerPostOrderHooks(lView[TVIEW], tNode); decreaseElementDepthCount(); - // this is fired at the end of elementEnd because ALL of the stylingBindings code - // (for directives and the template) have now executed which means the styling - // context can be instantiated properly. - let stylingContext: StylingContext|null = null; - if (hasClassInput(previousOrParentTNode)) { - stylingContext = getStylingContextFromLView(previousOrParentTNode.index, lView); - setInputsForProperty( - lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext)); + if (hasClassInput(tNode) && tNode.classes) { + setDirectiveStylingInput(tNode.classes, lView, tNode.inputs !['class']); } - if (hasStyleInput(previousOrParentTNode)) { - stylingContext = - stylingContext || getStylingContextFromLView(previousOrParentTNode.index, lView); - setInputsForProperty( - lView, previousOrParentTNode.inputs !['style'] !, - getInitialStyleStringValue(stylingContext)); + + if (hasStyleInput(tNode) && tNode.styles) { + setDirectiveStylingInput(tNode.styles, lView, tNode.inputs !['style']); } } @@ -237,6 +201,7 @@ export function ɵɵelement( export function ɵɵelementHostAttrs(attrs: TAttributes) { const hostElementIndex = getSelectedIndex(); const lView = getLView(); + const tView = lView[TVIEW]; const tNode = getTNode(hostElementIndex, lView); // non-element nodes (e.g. ``) are not rendered as actual @@ -245,16 +210,34 @@ export function ɵɵelementHostAttrs(attrs: TAttributes) { if (tNode.type === TNodeType.Element) { const native = getNativeByTNode(tNode, lView) as RElement; const lastAttrIndex = setUpAttributes(native, attrs); - const stylingAttrsStartIndex = attrsStylingIndexOf(attrs, lastAttrIndex); - if (stylingAttrsStartIndex >= 0) { - const directiveStylingIndex = getActiveDirectiveStylingIndex(); - if (tNode.stylingTemplate) { - patchContextWithStaticAttrs( - tNode.stylingTemplate, attrs, stylingAttrsStartIndex, directiveStylingIndex); - } else { - tNode.stylingTemplate = - initializeStaticContext(attrs, stylingAttrsStartIndex, directiveStylingIndex); + if (tView.firstTemplatePass) { + const stylingNeedsToBeRendered = registerInitialStylingOnTNode(tNode, attrs, lastAttrIndex); + + // this is only called during the first template pass in the + // event that this current directive assigned initial style/class + // host attribute values to the element. Because initial styling + // values are applied before directives are first rendered (within + // `createElement`) this means that initial styling for any directives + // still needs to be applied. Note that this will only happen during + // the first template pass and not each time a directive applies its + // attribute values to the element. + if (stylingNeedsToBeRendered) { + const renderer = lView[RENDERER]; + renderInitialStyling(renderer, native, tNode); } } } } + +function setDirectiveStylingInput( + context: TStylingContext | StylingMapArray, lView: LView, stylingInputs: (string | number)[]) { + // older versions of Angular treat the input as `null` in the + // event that the value does not exist at all. For this reason + // we can't have a styling value be an empty string. + const value = 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 + // be (Jira Issue = FW-1467). + setInputsForProperty(lView, stylingInputs, value); +} diff --git a/packages/core/src/render3/instructions/element_container.ts b/packages/core/src/render3/instructions/element_container.ts index 47f08c8763..3476cffd7b 100644 --- a/packages/core/src/render3/instructions/element_container.ts +++ b/packages/core/src/render3/instructions/element_container.ts @@ -15,8 +15,10 @@ import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; import {applyOnCreateInstructions} from '../node_util'; import {getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from '../state'; +import {registerInitialStylingOnTNode} from '../styling_next/instructions'; + +import {createDirectivesAndLocals, executeContentQueries, getOrCreateTNode} from './shared'; -import {createDirectivesAndLocals, executeContentQueries, getOrCreateTNode, setNodeStylingTemplate} from './shared'; /** @@ -52,10 +54,10 @@ export function ɵɵelementContainerStart( tView, lView[T_HOST], index, TNodeType.ElementContainer, tagName, attrs || null); - if (attrs) { + if (attrs && tView.firstTemplatePass) { // While ng-container doesn't necessarily support styling, we use the style context to identify // and execute directives on the ng-container. - setNodeStylingTemplate(tView, tNode, attrs, 0); + registerInitialStylingOnTNode(tNode, attrs as TAttributes, 0); } appendChild(native, tNode, lView); diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index b39230184f..cd98fcf226 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -18,11 +18,10 @@ import {SelectorFlags} from '../interfaces/projection'; import {LQueries} from '../interfaces/query'; import {RComment, RElement, RNode} from '../interfaces/renderer'; import {StylingContext} from '../interfaces/styling'; -import {isStylingContext} from '../interfaces/type_checks'; import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, HookData, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TData, TVIEW, TView as ITView, TView, T_HOST} from '../interfaces/view'; import {TStylingContext} from '../styling_next/interfaces'; -import {runtimeIsNewStylingInUse} from '../styling_next/state'; import {DebugStyling as DebugNewStyling, NodeStylingDebug} from '../styling_next/styling_debug'; +import {isStylingContext} from '../styling_next/util'; import {attachDebugObject} from '../util/debug_utils'; import {getTNode, unwrapRNode} from '../util/view_utils'; @@ -132,8 +131,8 @@ export const TNodeConstructor = class TNode implements ITNode { public stylingTemplate: StylingContext|null, // public projection: number|(ITNode|RNode[])[]|null, // public onElementCreationFns: Function[]|null, // - public newStyles: TStylingContext|null, // - public newClasses: TStylingContext|null, // + public styles: TStylingContext|null, // + public classes: TStylingContext|null, // ) {} get type_(): string { @@ -331,16 +330,14 @@ export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|nul while (tNodeCursor) { const rawValue = lView[tNode.index]; const native = unwrapRNode(rawValue); - const componentLViewDebug = - isStylingContext(rawValue) ? null : toDebug(readLViewValue(rawValue)); - - let styles: DebugNewStyling|null = null; - let classes: DebugNewStyling|null = null; - if (runtimeIsNewStylingInUse()) { - styles = tNode.newStyles ? new NodeStylingDebug(tNode.newStyles, lView, false) : null; - classes = tNode.newClasses ? new NodeStylingDebug(tNode.newClasses, lView, true) : null; - } + const componentLViewDebug = toDebug(readLViewValue(rawValue)); + const styles = isStylingContext(tNode.styles) ? + new NodeStylingDebug(tNode.styles as any as TStylingContext, lView) : + null; + const classes = isStylingContext(tNode.classes) ? + new NodeStylingDebug(tNode.classes as any as TStylingContext, lView, true) : + null; debugNodes.push({ html: toHtml(native), native: native as any, styles, classes, diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 5c58bc5f55..b130ce64ae 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -34,8 +34,9 @@ import {isNodeMatchingSelectorList} from '../node_selector_matcher'; import {enterView, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, incrementActiveDirectiveId, isCreationMode, leaveView, namespaceHTMLInternal, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode, setSelectedIndex} from '../state'; import {initializeStaticContext as initializeStaticStylingContext} from '../styling/class_and_style_bindings'; import {ANIMATION_PROP_PREFIX, isAnimationProp} from '../styling/util'; +import {renderStylingMap} from '../styling_next/bindings'; +import {getInitialStylingValue, getStylingMapArray} from '../styling_next/util'; import {NO_CHANGE} from '../tokens'; -import {attrsStylingIndexOf} from '../util/attrs_utils'; import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils'; import {getLViewParent, getRootContext} from '../util/view_traversal_utils'; import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, readPatchedLView, resetPreOrderHookFlags, unwrapRNode, viewAttachedToChangeDetector} from '../util/view_utils'; @@ -490,25 +491,6 @@ function getRenderFlags(view: LView): RenderFlags { //// Element ////////////////////////// -/** - * Appropriately sets `stylingTemplate` on a TNode - * - * Does not apply styles to DOM nodes - * - * @param tNode The node whose `stylingTemplate` to set - * @param attrs The attribute array source to set the attributes from - * @param attrsStartIndex Optional start index to start processing the `attrs` from - */ -export function setNodeStylingTemplate( - tView: TView, tNode: TNode, attrs: TAttributes, attrsStartIndex: number) { - if (tView.firstTemplatePass && !tNode.stylingTemplate) { - const stylingAttrsStartIndex = attrsStylingIndexOf(attrs, attrsStartIndex); - if (stylingAttrsStartIndex >= 0) { - tNode.stylingTemplate = initializeStaticStylingContext(attrs, stylingAttrsStartIndex); - } - } -} - export function executeContentQueries(tView: TView, tNode: TNode, lView: LView) { if (isContentQueryHost(tNode)) { const start = tNode.directiveStart; @@ -756,66 +738,61 @@ export function createTNode( adjustedIndex: number, tagName: string | null, attrs: TAttributes | null): TNode { ngDevMode && ngDevMode.tNode++; let injectorIndex = tParent ? tParent.injectorIndex : -1; - return ngDevMode ? - new TNodeConstructor( - tView, // tView_: TView - type, // type: TNodeType - adjustedIndex, // index: number - injectorIndex, // injectorIndex: number - -1, // directiveStart: number - -1, // directiveEnd: number - -1, // propertyMetadataStartIndex: number - -1, // propertyMetadataEndIndex: number - 0, // flags: TNodeFlags - 0, // providerIndexes: TNodeProviderIndexes - tagName, // tagName: string|null - attrs, // attrs: (string|AttributeMarker|(string|SelectorFlags)[])[]|null - null, // localNames: (string|number)[]|null - undefined, // initialInputs: (string[]|null)[]|null|undefined - undefined, // inputs: PropertyAliases|null|undefined - undefined, // outputs: PropertyAliases|null|undefined - null, // tViews: ITView|ITView[]|null - null, // next: ITNode|null - null, // projectionNext: ITNode|null - null, // child: ITNode|null - tParent, // parent: TElementNode|TContainerNode|null - null, // stylingTemplate: StylingContext|null - null, // projection: number|(ITNode|RNode[])[]|null - null, // onElementCreationFns: Function[]|null - // TODO (matsko): rename this to `styles` once the old styling impl is gone - null, // newStyles: TStylingContext|null - // TODO (matsko): rename this to `classes` once the old styling impl is gone - null, // newClasses: TStylingContext|null - ) : - { - type: type, - index: adjustedIndex, - injectorIndex: injectorIndex, - directiveStart: -1, - directiveEnd: -1, - propertyMetadataStartIndex: -1, - propertyMetadataEndIndex: -1, - flags: 0, - providerIndexes: 0, - tagName: tagName, - attrs: attrs, - localNames: null, - initialInputs: undefined, - inputs: undefined, - outputs: undefined, - tViews: null, - next: null, - projectionNext: null, - child: null, - parent: tParent, - stylingTemplate: null, - projection: null, - onElementCreationFns: null, - // TODO (matsko): rename this to `styles` once the old styling impl is gone - newStyles: null, - // TODO (matsko): rename this to `classes` once the old styling impl is gone - newClasses: null, - }; + return ngDevMode ? new TNodeConstructor( + tView, // tView_: TView + type, // type: TNodeType + adjustedIndex, // index: number + injectorIndex, // injectorIndex: number + -1, // directiveStart: number + -1, // directiveEnd: number + -1, // propertyMetadataStartIndex: number + -1, // propertyMetadataEndIndex: number + 0, // flags: TNodeFlags + 0, // providerIndexes: TNodeProviderIndexes + tagName, // tagName: string|null + attrs, // attrs: (string|AttributeMarker|(string|SelectorFlags)[])[]|null + null, // localNames: (string|number)[]|null + undefined, // initialInputs: (string[]|null)[]|null|undefined + undefined, // inputs: PropertyAliases|null|undefined + undefined, // outputs: PropertyAliases|null|undefined + null, // tViews: ITView|ITView[]|null + null, // next: ITNode|null + null, // projectionNext: ITNode|null + null, // child: ITNode|null + tParent, // parent: TElementNode|TContainerNode|null + null, // stylingTemplate: StylingContext|null + null, // projection: number|(ITNode|RNode[])[]|null + null, // onElementCreationFns: Function[]|null + null, // newStyles: TStylingContext|null + null, // newClasses: TStylingContext|null + ) : + { + type: type, + index: adjustedIndex, + injectorIndex: injectorIndex, + directiveStart: -1, + directiveEnd: -1, + propertyMetadataStartIndex: -1, + propertyMetadataEndIndex: -1, + flags: 0, + providerIndexes: 0, + tagName: tagName, + attrs: attrs, + localNames: null, + initialInputs: undefined, + inputs: undefined, + outputs: undefined, + tViews: null, + next: null, + projectionNext: null, + child: null, + parent: tParent, + stylingTemplate: null, + projection: null, + onElementCreationFns: null, + styles: null, + classes: null, + }; } @@ -1899,3 +1876,17 @@ export function textBindingInternal(lView: LView, index: number, value: string): const renderer = lView[RENDERER]; isProceduralRenderer(renderer) ? renderer.setValue(element, value) : element.textContent = value; } + +/** + * Renders all initial styling (class and style values) on to the element from the tNode. + * + * All initial styling data (i.e. any values extracted from the `style` or `class` attributes + * on an element) are collected into the `tNode.styles` and `tNode.classes` data structures. + * These values are populated during the creation phase of an element and are then later + * applied once the element is instantiated. This function applies each of the static + * style and class entries to the element. + */ +export function renderInitialStyling(renderer: Renderer3, native: RElement, tNode: TNode) { + renderStylingMap(renderer, native, tNode.classes, true); + renderStylingMap(renderer, native, tNode.styles, false); +} diff --git a/packages/core/src/render3/instructions/style_prop_interpolation.ts b/packages/core/src/render3/instructions/style_prop_interpolation.ts index bbfb93b024..76e0c6af6f 100644 --- a/packages/core/src/render3/instructions/style_prop_interpolation.ts +++ b/packages/core/src/render3/instructions/style_prop_interpolation.ts @@ -6,12 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ import {getLView, getSelectedIndex} from '../state'; -import {NO_CHANGE} from '../tokens'; - +import {stylePropInternal} from '../styling_next/instructions'; import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation'; import {TsickleIssue1009} from './shared'; -import {getActiveDirectiveStylingIndex, stylePropInternal} from './styling'; - /** @@ -37,20 +34,15 @@ import {getActiveDirectiveStylingIndex, stylePropInternal} from './styling'; * @param v0 Value checked for change. * @param suffix Static value used for concatenation only. * @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`. - * @param forceOverride Whether or not to update the styling value immediately. * @returns itself, so that it may be chained. * @codeGenApi */ export function ɵɵstylePropInterpolate1( - styleIndex: number, prefix: string, v0: any, suffix: string, valueSuffix?: string | null, - forceOverride?: boolean): TsickleIssue1009 { + prop: string, prefix: string, v0: any, suffix: string, + valueSuffix?: string | null): TsickleIssue1009 { const lView = getLView(); const interpolatedValue = interpolation1(lView, prefix, v0, suffix); - if (interpolatedValue !== NO_CHANGE) { - stylePropInternal( - lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(), - interpolatedValue as string, valueSuffix, forceOverride); - } + stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); return ɵɵstylePropInterpolate1; } @@ -79,20 +71,15 @@ export function ɵɵstylePropInterpolate1( * @param v1 Value checked for change. * @param suffix Static value used for concatenation only. * @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`. - * @param forceOverride Whether or not to update the styling value immediately. * @returns itself, so that it may be chained. * @codeGenApi */ export function ɵɵstylePropInterpolate2( - styleIndex: number, prefix: string, v0: any, i0: string, v1: any, suffix: string, - valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009 { + prop: string, prefix: string, v0: any, i0: string, v1: any, suffix: string, + valueSuffix?: string | null): TsickleIssue1009 { const lView = getLView(); const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix); - if (interpolatedValue !== NO_CHANGE) { - stylePropInternal( - lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(), - interpolatedValue as string, valueSuffix, forceOverride); - } + stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); return ɵɵstylePropInterpolate2; } @@ -123,20 +110,15 @@ export function ɵɵstylePropInterpolate2( * @param v2 Value checked for change. * @param suffix Static value used for concatenation only. * @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`. - * @param forceOverride Whether or not to update the styling value immediately. * @returns itself, so that it may be chained. * @codeGenApi */ export function ɵɵstylePropInterpolate3( - styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, - suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009 { + prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string, + valueSuffix?: string | null): TsickleIssue1009 { const lView = getLView(); const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix); - if (interpolatedValue !== NO_CHANGE) { - stylePropInternal( - lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(), - interpolatedValue as string, valueSuffix, forceOverride); - } + stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); return ɵɵstylePropInterpolate3; } @@ -169,21 +151,15 @@ export function ɵɵstylePropInterpolate3( * @param v3 Value checked for change. * @param suffix Static value used for concatenation only. * @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`. - * @param forceOverride Whether or not to update the styling value immediately. * @returns itself, so that it may be chained. * @codeGenApi */ export function ɵɵstylePropInterpolate4( - styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, - i2: string, v3: any, suffix: string, valueSuffix?: string | null, - forceOverride?: boolean): TsickleIssue1009 { + prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, + v3: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009 { const lView = getLView(); const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix); - if (interpolatedValue !== NO_CHANGE) { - stylePropInternal( - lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(), - interpolatedValue as string, valueSuffix, forceOverride); - } + stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); return ɵɵstylePropInterpolate4; } @@ -218,22 +194,16 @@ export function ɵɵstylePropInterpolate4( * @param v4 Value checked for change. * @param suffix Static value used for concatenation only. * @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`. - * @param forceOverride Whether or not to update the styling value immediately. * @returns itself, so that it may be chained. * @codeGenApi */ export function ɵɵstylePropInterpolate5( - styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, - i2: string, v3: any, i3: string, v4: any, suffix: string, valueSuffix?: string | null, - forceOverride?: boolean): TsickleIssue1009 { + prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, + v3: any, i3: string, v4: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009 { const lView = getLView(); const interpolatedValue = interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix); - if (interpolatedValue !== NO_CHANGE) { - stylePropInternal( - lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(), - interpolatedValue as string, valueSuffix, forceOverride); - } + stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); return ɵɵstylePropInterpolate5; } @@ -270,22 +240,17 @@ export function ɵɵstylePropInterpolate5( * @param v5 Value checked for change. * @param suffix Static value used for concatenation only. * @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`. - * @param forceOverride Whether or not to update the styling value immediately. * @returns itself, so that it may be chained. * @codeGenApi */ export function ɵɵstylePropInterpolate6( - styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, - i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, suffix: string, - valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009 { + prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, + v3: any, i3: string, v4: any, i4: string, v5: any, suffix: string, + valueSuffix?: string | null): TsickleIssue1009 { const lView = getLView(); const interpolatedValue = interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix); - if (interpolatedValue !== NO_CHANGE) { - stylePropInternal( - lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(), - interpolatedValue as string, valueSuffix, forceOverride); - } + stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); return ɵɵstylePropInterpolate6; } @@ -325,22 +290,17 @@ export function ɵɵstylePropInterpolate6( * @param v6 Value checked for change. * @param suffix Static value used for concatenation only. * @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`. - * @param forceOverride Whether or not to update the styling value immediately. * @returns itself, so that it may be chained. * @codeGenApi */ export function ɵɵstylePropInterpolate7( - styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, - i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, - suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009 { + prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, + v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string, + valueSuffix?: string | null): TsickleIssue1009 { const lView = getLView(); const interpolatedValue = interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix); - if (interpolatedValue !== NO_CHANGE) { - stylePropInternal( - lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(), - interpolatedValue as string, valueSuffix, forceOverride); - } + stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); return ɵɵstylePropInterpolate7; } @@ -382,23 +342,17 @@ export function ɵɵstylePropInterpolate7( * @param v7 Value checked for change. * @param suffix Static value used for concatenation only. * @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`. - * @param forceOverride Whether or not to update the styling value immediately. * @returns itself, so that it may be chained. * @codeGenApi */ export function ɵɵstylePropInterpolate8( - styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, - i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, - v7: any, suffix: string, valueSuffix?: string | null, - forceOverride?: boolean): TsickleIssue1009 { + prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, + v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any, + suffix: string, valueSuffix?: string | null): TsickleIssue1009 { const lView = getLView(); const interpolatedValue = interpolation8( lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix); - if (interpolatedValue !== NO_CHANGE) { - stylePropInternal( - lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(), - interpolatedValue as string, valueSuffix, forceOverride); - } + stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); return ɵɵstylePropInterpolate8; } @@ -429,19 +383,13 @@ export function ɵɵstylePropInterpolate8( * a string prefix and ending with a string suffix. * (e.g. `['prefix', value0, '-', value1, '-', value2, ..., value99, 'suffix']`) * @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`. - * @param forceOverride Whether or not to update the styling value immediately. * @returns itself, so that it may be chained. * @codeGenApi */ export function ɵɵstylePropInterpolateV( - styleIndex: number, values: any[], valueSuffix?: string | null, - forceOverride?: boolean): TsickleIssue1009 { + prop: string, values: any[], valueSuffix?: string | null): TsickleIssue1009 { const lView = getLView(); - const interpolated = interpolationV(lView, values); - if (interpolated !== NO_CHANGE) { - stylePropInternal( - lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(), - interpolated as string, valueSuffix, forceOverride); - } + const interpolatedValue = interpolationV(lView, values); + stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); return ɵɵstylePropInterpolateV; } diff --git a/packages/core/src/render3/instructions/styling.ts b/packages/core/src/render3/instructions/styling.ts index aa2256201a..87b026c96c 100644 --- a/packages/core/src/render3/instructions/styling.ts +++ b/packages/core/src/render3/instructions/styling.ts @@ -16,10 +16,8 @@ import {ParamsOf, enqueueHostInstruction, registerHostDirective} from '../stylin import {BoundPlayerFactory} from '../styling/player_factory'; import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from '../styling/shared'; import {getCachedStylingContext, setCachedStylingContext} from '../styling/state'; -import {allocateOrUpdateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util'; -import {classMap as newClassMap, classProp as newClassProp, styleMap as newStyleMap, styleProp as newStyleProp, stylingApply as newStylingApply, stylingInit as newStylingInit} from '../styling_next/instructions'; -import {runtimeAllowOldStyling, runtimeIsNewStylingInUse} from '../styling_next/state'; -import {getBindingNameFromIndex} from '../styling_next/util'; +import {allocateOrUpdateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContextFromLView} from '../styling/util'; +import {hasClassInput, hasStyleInput} from '../styling_next/util'; import {NO_CHANGE} from '../tokens'; import {renderStringify} from '../util/misc_utils'; import {getRootContext} from '../util/view_traversal_utils'; @@ -76,13 +74,6 @@ export function ɵɵstyling( const directiveStylingIndex = getActiveDirectiveStylingIndex(); if (directiveStylingIndex) { - // this is temporary hack to get the existing styling instructions to - // play ball with the new refactored implementation. - // TODO (matsko): remove this once the old implementation is not needed. - if (runtimeIsNewStylingInUse()) { - newStylingInit(); - } - // despite the binding being applied in a queue (below), the allocation // of the directive into the context happens right away. The reason for // this is to retain the ordering of the directives (which is important @@ -165,15 +156,6 @@ export function stylePropInternal( updatestyleProp( stylingContext, styleIndex, valueToAdd, DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride); } - - if (runtimeIsNewStylingInUse()) { - const prop = getBindingNameFromIndex(stylingContext, styleIndex, directiveStylingIndex, false); - - // the reason why we cast the value as `boolean` is - // because the new styling refactor does not yet support - // sanitization or animation players. - newStyleProp(prop, value as string | number, suffix); - } } function resolveStylePropValue( @@ -232,15 +214,6 @@ export function ɵɵclassProp( updateclassProp( stylingContext, classIndex, input, DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride); } - - if (runtimeIsNewStylingInUse()) { - const prop = getBindingNameFromIndex(stylingContext, classIndex, directiveStylingIndex, true); - - // the reason why we cast the value as `boolean` is - // because the new styling refactor does not yet support - // sanitization or animation players. - newClassProp(prop, input as boolean); - } } @@ -292,10 +265,6 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu } updateStyleMap(stylingContext, styles); } - - if (runtimeIsNewStylingInUse()) { - newStyleMap(styles); - } } @@ -342,10 +311,6 @@ export function classMapInternal( } updateClassMap(stylingContext, classes); } - - if (runtimeIsNewStylingInUse()) { - newClassMap(classes); - } } /** @@ -371,13 +336,11 @@ export function ɵɵstylingApply(): void { const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0; const stylingContext = getStylingContext(index, lView); - if (runtimeAllowOldStyling()) { - const totalPlayersQueued = renderStyling( - stylingContext, renderer, lView, isFirstRender, null, null, directiveStylingIndex); - if (totalPlayersQueued > 0) { - const rootContext = getRootContext(lView); - scheduleTick(rootContext, RootContextFlags.FlushPlayers); - } + const totalPlayersQueued = renderStyling( + stylingContext, renderer, lView, isFirstRender, null, null, directiveStylingIndex); + if (totalPlayersQueued > 0) { + const rootContext = getRootContext(lView); + scheduleTick(rootContext, RootContextFlags.FlushPlayers); } // because select(n) may not run between every instruction, the cached styling @@ -388,10 +351,6 @@ export function ɵɵstylingApply(): void { // cleared because there is no code in Angular that applies more styling code after a // styling flush has occurred. Note that this will be fixed once FW-1254 lands. setCachedStylingContext(null); - - if (runtimeIsNewStylingInUse()) { - newStylingApply(); - } } export function getActiveDirectiveStylingIndex() { diff --git a/packages/core/src/render3/interfaces/container.ts b/packages/core/src/render3/interfaces/container.ts index 209aefc0d3..a385de073c 100644 --- a/packages/core/src/render3/interfaces/container.ts +++ b/packages/core/src/render3/interfaces/container.ts @@ -11,7 +11,6 @@ import {ViewRef} from '../../linker/view_ref'; import {TNode} from './node'; import {LQueries} from './query'; import {RComment, RElement} from './renderer'; -import {StylingContext} from './styling'; import {HOST, LView, NEXT, PARENT, QUERIES, T_HOST} from './view'; @@ -54,11 +53,8 @@ export interface LContainer extends Array { * * The host could be an LView if this container is on a component node. * In that case, the component LView is its HOST. - * - * It could also be a styling context if this is a node with a style/class - * binding. */ - readonly[HOST]: RElement|RComment|StylingContext|LView; + readonly[HOST]: RElement|RComment|LView; /** * This is a type field which allows us to differentiate `LContainer` from `StylingContext` in an diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 9ceb418289..024f549bbb 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -5,13 +5,15 @@ * 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 {TStylingContext} from '../styling_next/interfaces'; +import {StylingMapArray, TStylingContext} from '../styling_next/interfaces'; + import {CssSelector} from './projection'; import {RNode} from './renderer'; import {StylingContext} from './styling'; import {LView, TView} from './view'; + /** * TNodeType corresponds to the {@link TNode} `type` property. */ @@ -457,10 +459,46 @@ export interface TNode { * with functions each time the creation block is called. */ onElementCreationFns: Function[]|null; - // TODO (matsko): rename this to `styles` once the old styling impl is gone - newStyles: TStylingContext|null; - // TODO (matsko): rename this to `classes` once the old styling impl is gone - newClasses: TStylingContext|null; + + /** + * A collection of all style bindings and/or static style values for an element. + * + * This field will be populated if and when: + * + * - There are one or more initial styles on an element (e.g. `
`) + * - There are one or more style bindings on an element (e.g. `
`) + * + * If and when there are only initial styles (no bindings) then an instance of `StylingMapArray` + * will be used here. Otherwise an instance of `TStylingContext` will be created when there + * are one or more style bindings on an element. + * + * During element creation this value is likely to be populated with an instance of + * `StylingMapArray` and only when the bindings are evaluated (which happens during + * update mode) then it will be converted to a `TStylingContext` if any style bindings + * are encountered. If and when this happens then the existing `StylingMapArray` value + * will be placed into the initial styling slot in the newly created `TStylingContext`. + */ + styles: StylingMapArray|TStylingContext|null; + + /** + * A collection of all class bindings and/or static class values for an element. + * + * This field will be populated if and when: + * + * - There are one or more initial classes on an element (e.g. `
`) + * - There are one or more class bindings on an element (e.g. `
`) + * + * If and when there are only initial classes (no bindings) then an instance of `StylingMapArray` + * will be used here. Otherwise an instance of `TStylingContext` will be created when there + * are one or more class bindings on an element. + * + * During element creation this value is likely to be populated with an instance of + * `StylingMapArray` and only when the bindings are evaluated (which happens during + * update mode) then it will be converted to a `TStylingContext` if any class bindings + * are encountered. If and when this happens then the existing `StylingMapArray` value + * will be placed into the initial styling slot in the newly created `TStylingContext`. + */ + classes: StylingMapArray|TStylingContext|null; } /** Static data for an element */ diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 3dbb82718a..6ae7fb7851 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -20,7 +20,6 @@ import {TElementNode, TNode, TViewNode} from './node'; import {PlayerHandler} from './player'; import {LQueries} from './query'; import {RElement, Renderer3, RendererFactory3} from './renderer'; -import {StylingContext} from './styling'; @@ -71,13 +70,9 @@ export interface OpaqueViewState { export interface LView extends Array { /** * The host node for this LView instance, if this is a component view. - * * If this is an embedded view, HOST will be null. - * - * If the component uses host bindings for styling that the `RElement` will be wrapped with - * `StylingContext`. */ - [HOST]: RElement|StylingContext|null; + [HOST]: RElement|null; /** * The static data for this view. We need a reference to this so we can easily walk up the diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 9336edc656..c239229513 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -17,7 +17,6 @@ import {NodeInjectorFactory} from './interfaces/injector'; import {TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode, TViewNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection'; import {ProceduralRenderer3, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; -import {StylingContext} from './interfaces/styling'; import {isLContainer, isLView, isRootView} from './interfaces/type_checks'; import {CHILD_HEAD, CLEANUP, FLAGS, HOST, HookData, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, T_HOST, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; @@ -70,7 +69,7 @@ const enum WalkTNodeTreeAction { */ function executeActionOnElementOrContainer( action: WalkTNodeTreeAction, renderer: Renderer3, parent: RElement | null, - lNodeToHandle: RNode | LContainer | LView | StylingContext, beforeNode?: RNode | null) { + lNodeToHandle: RNode | LContainer | LView, beforeNode?: RNode | null) { ngDevMode && assertDefined(lNodeToHandle, '\'lNodeToHandle\' is undefined'); let lContainer: LContainer|undefined; let isComponent = false; diff --git a/packages/core/src/render3/node_selector_matcher.ts b/packages/core/src/render3/node_selector_matcher.ts index 33c88df692..594eb9c9b6 100644 --- a/packages/core/src/render3/node_selector_matcher.ts +++ b/packages/core/src/render3/node_selector_matcher.ts @@ -12,7 +12,7 @@ import {assertDefined, assertNotEqual} from '../util/assert'; import {AttributeMarker, TAttributes, TNode, TNodeType, unusedValueExportToPlacateAjd as unused1} from './interfaces/node'; import {CssSelector, CssSelectorList, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection'; -import {getInitialClassNameValue} from './styling/class_and_style_bindings'; +import {getInitialStylingValue} from './styling_next/util'; import {isNameOnlyAttributeMarker} from './util/attrs_utils'; const unusedValueToPlacateAjd = unused1 + unused2; @@ -103,8 +103,9 @@ export function isNodeMatchingSelector( // special case for matching against classes when a tNode has been instantiated with // class and style values as separate attribute values (e.g. ['title', CLASS, 'foo']) - if ((mode & SelectorFlags.CLASS) && tNode.stylingTemplate) { - if (!isCssClassMatching(readClassValueFromTNode(tNode), selectorAttrValue as string)) { + if ((mode & SelectorFlags.CLASS) && tNode.classes) { + if (!isCssClassMatching( + getInitialStylingValue(tNode.classes), selectorAttrValue as string)) { if (isPositive(mode)) return false; skipToNextSelector = true; } @@ -152,16 +153,6 @@ function isPositive(mode: SelectorFlags): boolean { return (mode & SelectorFlags.NOT) === 0; } -function readClassValueFromTNode(tNode: TNode): string { - // comparing against CSS class values is complex because the compiler doesn't place them as - // regular attributes when an element is created. Instead, the classes (and styles for - // that matter) are placed in a special styling context that is used for resolving all - // class/style values across static attributes, [style]/[class] and [style.prop]/[class.name] - // bindings. Therefore if and when the styling context exists then the class values are to be - // extracted by the context helper code below... - return tNode.stylingTemplate ? getInitialClassNameValue(tNode.stylingTemplate) : ''; -} - /** * Examines the attribute's definition array for a node to find the index of the * attribute that matches the given `name`. diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index 57e8b54b3d..37a49da87c 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {assertDefined, assertGreaterThan} from '../util/assert'; +import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; +import {assertDefined} from '../util/assert'; import {assertLViewOrUndefined} from './assert'; import {executeHooks} from './hooks'; @@ -14,6 +15,7 @@ import {ComponentDef, DirectiveDef} from './interfaces/definition'; import {TElementNode, TNode, TViewNode} from './interfaces/node'; import {BINDING_INDEX, CONTEXT, DECLARATION_VIEW, FLAGS, InitPhaseState, LView, LViewFlags, OpaqueViewState, TVIEW} from './interfaces/view'; import {setCachedStylingContext} from './styling/state'; +import {resetAllStylingState, resetStylingState} from './styling_next/state'; import {resetPreOrderHookFlags} from './util/view_utils'; @@ -458,6 +460,8 @@ export function resetComponentState() { previousOrParentTNode = null !; elementDepthCount = 0; bindingsEnabled = true; + setCurrentStyleSanitizer(null); + resetAllStylingState(); } /** @@ -513,6 +517,10 @@ export function setSelectedIndex(index: number) { // remove the styling context from the cache // because we are now on a different element setCachedStylingContext(null); + + // we have now jumped to another element + // therefore the state is stale + resetStylingState(); } @@ -557,3 +565,12 @@ export function namespaceHTMLInternal() { export function getNamespace(): string|null { return _currentNamespace; } + +let _currentSanitizer: StyleSanitizeFn|null; +export function setCurrentStyleSanitizer(sanitizer: StyleSanitizeFn | null) { + _currentSanitizer = sanitizer; +} + +export function getCurrentStyleSanitizer() { + return _currentSanitizer; +} diff --git a/packages/core/src/render3/styling/class_and_style_bindings.ts b/packages/core/src/render3/styling/class_and_style_bindings.ts index 23881b4caa..ce18612c0a 100644 --- a/packages/core/src/render3/styling/class_and_style_bindings.ts +++ b/packages/core/src/render3/styling/class_and_style_bindings.ts @@ -686,7 +686,7 @@ function patchStylingMapIntoContext( // directive values if their identity has not changed. If a previous directive set this // value as dirty (because its own shape changed) then this means that the object has been // offset to a different area in the context. Because its value has been offset then it - // can't write to a region that it wrote to before (which may have been apart of another + // can't write to a region that it wrote to before (which may have been a part of another // directive) and therefore its shape changes too. let valuesEntryShapeChange = existingCachedValueIsDirty || ((!existingCachedValue && cacheValue) ? true : false); @@ -822,7 +822,7 @@ function patchStylingMapIntoContext( } // STEP 3: - // Remove (nullify) any existing entries in the context that were not apart of the + // Remove (nullify) any existing entries in the context that were not a part of the // map input value that was passed into this algorithm for this directive. while (ctxIndex < ctxEnd) { valuesEntryShapeChange = true; // some values are missing @@ -914,7 +914,13 @@ function updateSingleStylingValue( const currDirective = getDirectiveIndexFromEntry(context, singleIndex); const value: string|boolean|null = (input instanceof BoundPlayerFactory) ? input.value : input; - ngDevMode && ngDevMode.stylingProp++; + if (ngDevMode) { + if (isClassBased) { + ngDevMode.classProp++; + } else { + ngDevMode.styleProp++; + } + } if (hasValueChanged(currFlag, currValue, value) && (forceOverride || allowValueChange(currValue, value, currDirective, directiveIndex))) { @@ -973,7 +979,13 @@ function updateSingleStylingValue( setContextPlayersDirty(context, true); } - ngDevMode && ngDevMode.stylingPropCacheMiss++; + if (ngDevMode) { + if (isClassBased) { + ngDevMode.classPropCacheMiss++; + } else { + ngDevMode.stylePropCacheMiss++; + } + } } } @@ -1002,7 +1014,7 @@ export function renderStyling( isFirstRender: boolean, classesStore?: BindingStore | null, stylesStore?: BindingStore | null, directiveIndex: number = 0): number { let totalPlayersQueued = 0; - ngDevMode && ngDevMode.stylingApply++; + ngDevMode && ngDevMode.flushStyling++; // this prevents multiple attempts to render style/class values on // the same element... @@ -1017,8 +1029,6 @@ export function renderStyling( flushHostInstructionsQueue(context); if (isContextDirty(context)) { - ngDevMode && ngDevMode.stylingApplyCacheMiss++; - // this is here to prevent things like ... // or if there are any host style or class bindings present in a directive set on // a container node @@ -1807,7 +1817,7 @@ function isMultiValueCacheHit( * - The dirty flag will be set to true * * If the `dirtyFutureValues` param is provided then it will update all future entries (binding - * entries that exist as apart of other directives) to be dirty as well. This will force the + * entries that exist as a part of other directives) to be dirty as well. This will force the * styling algorithm to reapply those values once change detection checks them (which will in * turn cause the styling context to update itself and the correct styling values will be * rendered on screen). diff --git a/packages/core/src/render3/styling/util.ts b/packages/core/src/render3/styling/util.ts index d5afb98a0b..94a6adad87 100644 --- a/packages/core/src/render3/styling/util.ts +++ b/packages/core/src/render3/styling/util.ts @@ -156,14 +156,6 @@ export function isAnimationProp(name: string): boolean { return name[0] === ANIMATION_PROP_PREFIX; } -export function hasClassInput(tNode: TNode) { - return (tNode.flags & TNodeFlags.hasClassInput) !== 0; -} - -export function hasStyleInput(tNode: TNode) { - return (tNode.flags & TNodeFlags.hasStyleInput) !== 0; -} - export function forceClassesAsString(classes: string | {[key: string]: any} | null | undefined): string { if (classes && typeof classes !== 'string') { @@ -226,7 +218,7 @@ export function getPlayersInternal(playerContext: PlayerContext): Player[] { const players: Player[] = []; const nonFactoryPlayersStart = playerContext[PlayerIndex.NonBuilderPlayersStart]; - // add all factory-based players (which are apart of [style] and [class] bindings) + // add all factory-based players (which are a part of [style] and [class] bindings) for (let i = PlayerIndex.PlayerBuildersStartPosition + PlayerIndex.PlayerOffsetPosition; i < nonFactoryPlayersStart; i += PlayerIndex.PlayerAndPlayerBuildersTupleSize) { const player = playerContext[i] as Player | null; @@ -235,7 +227,7 @@ export function getPlayersInternal(playerContext: PlayerContext): Player[] { } } - // add all custom players (not apart of [style] and [class] bindings) + // add all custom players (not a part of [style] and [class] bindings) for (let i = nonFactoryPlayersStart; i < playerContext.length; i++) { players.push(playerContext[i] as Player); } diff --git a/packages/core/src/render3/styling_next/bindings.ts b/packages/core/src/render3/styling_next/bindings.ts index 728a64f2d3..5f1838dfa3 100644 --- a/packages/core/src/render3/styling_next/bindings.ts +++ b/packages/core/src/render3/styling_next/bindings.ts @@ -8,8 +8,10 @@ import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer'; import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer'; -import {ApplyStylingFn, LStylingData, LStylingMap, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces'; -import {allowStylingFlush, getBindingValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasValueChanged, isContextLocked, isSanitizationRequired, isStylingValueDefined, lockContext, setGuardMask} from './util'; +import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces'; +import {BIT_MASK_START_VALUE, deleteStylingStateFromStorage, getStylingState, resetStylingState, storeStylingState} from './state'; +import {allowStylingFlush, getBindingValue, getGuardMask, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getValuesCount, hasValueChanged, isContextLocked, isSanitizationRequired, isStylingValueDefined, lockContext, setGuardMask, stateIsPersisted} from './util'; + /** @@ -31,25 +33,38 @@ import {allowStylingFlush, getBindingValue, getGuardMask, getProp, getPropValues * -------- */ -const DEFAULT_BINDING_VALUE = null; -const DEFAULT_SIZE_VALUE = 1; - // The first bit value reflects a map-based binding value's bit. // The reason why it's always activated for every entry in the map // is so that if any map-binding values update then all other prop // based bindings will pass the guard check automatically without // any extra code or flags. export const DEFAULT_GUARD_MASK_VALUE = 0b1; -const STYLING_INDEX_FOR_MAP_BINDING = 0; -const STYLING_INDEX_START_VALUE = 1; -// the values below are global to all styling code below. Each value -// will either increment or mutate each time a styling instruction is -// executed. Do not modify the values below. -let currentStyleIndex = STYLING_INDEX_START_VALUE; -let currentClassIndex = STYLING_INDEX_START_VALUE; -let stylesBitMask = 0; -let classesBitMask = 0; +/** + * The guard/update mask bit index location for map-based bindings. + * + * All map-based bindings (i.e. `[style]` and `[class]` ) + */ +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)[] = []; /** @@ -63,16 +78,24 @@ let deferredBindingQueue: (TStylingContext | number | string | null | boolean)[] * and the bit mask values to be in sync). */ export function updateClassBinding( - context: TStylingContext, data: LStylingData, prop: string | null, bindingIndex: number, - value: boolean | string | null | undefined | LStylingMap, deferRegistration: boolean, - forceUpdate: boolean): void { + context: TStylingContext, data: LStylingData, element: RElement, prop: string | null, + bindingIndex: number, value: boolean | string | null | undefined | StylingMapArray, + deferRegistration: boolean, forceUpdate: boolean): boolean { const isMapBased = !prop; - const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : currentClassIndex++; + const state = getStylingState(element, stateIsPersisted(context)); + const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++; const updated = updateBindingData( context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate, false); if (updated || forceUpdate) { - classesBitMask |= 1 << index; + // 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; + return true; } + return false; } /** @@ -86,11 +109,12 @@ export function updateClassBinding( * and the bit mask values to be in sync). */ export function updateStyleBinding( - context: TStylingContext, data: LStylingData, prop: string | null, bindingIndex: number, - value: String | string | number | null | undefined | LStylingMap, - sanitizer: StyleSanitizeFn | null, deferRegistration: boolean, forceUpdate: boolean): void { + context: TStylingContext, data: LStylingData, element: RElement, prop: string | null, + bindingIndex: number, value: String | string | number | null | undefined | StylingMapArray, + sanitizer: StyleSanitizeFn | null, deferRegistration: boolean, forceUpdate: boolean): boolean { const isMapBased = !prop; - const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : currentStyleIndex++; + const state = getStylingState(element, stateIsPersisted(context)); + const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++; const sanitizationRequired = isMapBased ? true : (sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false); @@ -98,8 +122,15 @@ export function updateStyleBinding( context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate, sanitizationRequired); if (updated || forceUpdate) { - stylesBitMask |= 1 << index; + // 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 property need to be + // applied again because on or more of the bindings for the CSS + // property have changed. + state.stylesBitMask |= 1 << index; + return true; } + return false; } /** @@ -118,7 +149,7 @@ export function updateStyleBinding( function updateBindingData( context: TStylingContext, data: LStylingData, counterIndex: number, prop: string | null, bindingIndex: number, - value: string | String | number | boolean | null | undefined | LStylingMap, + value: string | String | number | boolean | null | undefined | StylingMapArray, deferRegistration: boolean, forceUpdate: boolean, sanitizationRequired: boolean): boolean { if (!isContextLocked(context)) { if (deferRegistration) { @@ -215,9 +246,10 @@ function flushDeferredBindings() { */ export function registerBinding( context: TStylingContext, countId: number, prop: string | null, - bindingValue: number | null | string | boolean, sanitizationRequired?: boolean) { - // prop-based bindings (e.g `
`) + 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) { @@ -238,6 +270,7 @@ export function registerBinding( if (!found) { allocateNewContextEntry(context, context.length, prop, sanitizationRequired); addBindingIntoContext(context, false, i, bindingValue, countId); + registered = true; } } else { // map-based bindings (e.g `
`) @@ -245,7 +278,9 @@ export function registerBinding( // since it is already there when the context is first created. addBindingIntoContext( context, true, TStylingContextIndex.MapBindingsPosition, bindingValue, countId); + registered = true; } + return registered; } function allocateNewContextEntry( @@ -284,7 +319,8 @@ function addBindingIntoContext( bindingValue: number | string | boolean | null, countId: number) { const valuesCount = getValuesCount(context, index); - let lastValueIndex = index + TStylingContextIndex.BindingsStartOffset + valuesCount; + 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 @@ -293,6 +329,20 @@ function addBindingIntoContext( } 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)++; @@ -301,57 +351,120 @@ function addBindingIntoContext( // to be included into the existing mask value. const guardMask = getGuardMask(context, index) | (1 << countId); setGuardMask(context, index, guardMask); - } else if (typeof bindingValue === 'string' && context[lastValueIndex] == null) { + } else if (bindingValue !== null && context[lastValueIndex] == null) { context[lastValueIndex] = bindingValue; } } /** - * Applies all class entries in the provided context to the provided element and resets - * any counter and/or bitMask values associated with class bindings. + * Applies all pending style and class bindings to the provided element. * - * @returns whether or not the classes were flushed to the 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. + * + * 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: + * + * - `flushStyling` is called from the very last directive that has styling for + * the element (see `allowStylingFlush()`). + * - one or more bindings for classes or styles has updated (this is checked by + * examining the classes or styles bit mask). + * + * If the style and class values are successfully applied to the element then + * the temporary state values for the element will be cleared. Otherwise, if + * this did not occur then the styling state is persisted (see `state.ts` for + * more information on how this works). */ -export function applyClasses( - renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, context: TStylingContext, - element: RElement, directiveIndex: number): boolean { - let classesFlushed = false; - if (allowStylingFlush(context, directiveIndex)) { - const isFirstPass = !isContextLocked(context); - isFirstPass && lockContext(context); - if (classesBitMask) { - // there is no way to sanitize a class value therefore `sanitizer=null` - applyStyling(context, renderer, element, data, classesBitMask, setClass, null); - classesBitMask = 0; - classesFlushed = true; - } - currentClassIndex = STYLING_INDEX_START_VALUE; +export function flushStyling( + renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, + classesContext: TStylingContext | null, stylesContext: TStylingContext | null, + 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); + + // 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(); } - return classesFlushed; + + 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); + } + } else if (persistState) { + storeStylingState(element, state); + } +} + +function maybeApplyStyling( + renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, data: LStylingData, + context: TStylingContext | null, allowFlush: boolean, bitMask: number, + styleSetter: ApplyStylingFn, styleSanitizer: any | null): boolean { + if (allowFlush && context) { + lockAndFinalizeContext(context); + if (contextHasUpdates(context, bitMask)) { + ngDevMode && (styleSanitizer ? ngDevMode.stylesApplied++ : ngDevMode.classesApplied++); + applyStyling(context !, renderer, element, data, bitMask, styleSetter, styleSanitizer); + return true; + } + } + return allowFlush; +} + +function contextHasUpdates(context: TStylingContext | null, bitMask: number) { + return context && bitMask > BIT_MASK_START_VALUE; } /** - * Applies all style entries in the provided context to the provided element and resets - * any counter and/or bitMask values associated with style bindings. + * Locks the context (so no more bindings can be added) and also copies over initial class/style + * values into their binding areas. * - * @returns whether or not the styles were flushed to the element. + * There are two main actions that take place in this function: + * + * - Locking the context: + * Locking the context is required so that the style/class instructions know NOT to + * register a binding again after the first update pass has run. If a locking bit was + * not used then it would need to scan over the context each time an instruction is run + * (which is expensive). + * + * - Patching initial values: + * Directives and component host bindings may include static class/style values which are + * bound to the host element. When this happens, the styling context will need to be informed + * so it can use these static styling values as defaults when a matching binding is falsy. + * These initial styling values are read from the initial styling values slot within the + * provided `TStylingContext` (which is an instance of a `StylingMapArray`). This inner map will + * 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. */ -export function applyStyles( - renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, context: TStylingContext, - element: RElement, directiveIndex: number, sanitizer: StyleSanitizeFn | null): boolean { - let stylesFlushed = false; - if (allowStylingFlush(context, directiveIndex)) { - const isFirstPass = !isContextLocked(context); - isFirstPass && lockContext(context); - if (stylesBitMask) { - applyStyling(context, renderer, element, data, stylesBitMask, setStyle, sanitizer); - stylesBitMask = 0; - stylesFlushed = true; +function lockAndFinalizeContext(context: TStylingContext): void { + if (!isContextLocked(context)) { + const initialValues = getStylingMapArray(context); + if (initialValues) { + updateInitialStylingOnContext(context, initialValues); } - currentStyleIndex = STYLING_INDEX_START_VALUE; - return true; + lockContext(context); } - return stylesFlushed; } /** @@ -367,7 +480,7 @@ export function applyStyles( * * If there are any map-based entries present (which are applied to the * element via the `[style]` and `[class]` bindings) then those entries - * will be applied as well. However, the code for that is not apart of + * will be applied as well. However, the code for that is not a part of * this function. Instead, each time a property is visited, then the * code below will call an external function called `stylingMapsSyncFn` * and, if present, it will keep the application of styling values in @@ -384,8 +497,6 @@ export function applyStyling( context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, bindingData: LStylingData, bitMaskValue: number | boolean, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn | null) { - deferredBindingQueue.length && flushDeferredBindings(); - const bitMask = normalizeBitMaskValue(bitMaskValue); const stylingMapsSyncFn = getStylingMapsSyncFn(); const mapsGuardMask = getGuardMask(context, TStylingContextIndex.MapBindingsPosition); @@ -477,7 +588,12 @@ export function setStylingMapsSyncFn(fn: SyncStylingMapsFn) { * Assigns a style value to a style property for the given element. */ const setStyle: ApplyStylingFn = - (renderer: Renderer3 | null, native: any, prop: string, value: string | null) => { + (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 + // environment that doesn't have styling. In either + // case it's safe not to apply styling to the element. + const nativeStyle = native.style; if (value) { // opacity, z-index and flexbox all have number values // and these need to be converted into strings so that @@ -486,12 +602,12 @@ const setStyle: ApplyStylingFn = ngDevMode && ngDevMode.rendererSetStyle++; renderer && isProceduralRenderer(renderer) ? renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) : - native.style.setProperty(prop, value); + (nativeStyle && nativeStyle.setProperty(prop, value)); } else { ngDevMode && ngDevMode.rendererRemoveStyle++; renderer && isProceduralRenderer(renderer) ? renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) : - native.style.removeProperty(prop); + (nativeStyle && nativeStyle.removeProperty(prop)); } }; @@ -499,16 +615,78 @@ const setStyle: ApplyStylingFn = * Adds/removes the provided className value to the provided element. */ const setClass: ApplyStylingFn = - (renderer: Renderer3 | null, native: any, className: string, value: any) => { + (renderer: Renderer3 | null, native: RElement, className: string, value: any) => { if (className !== '') { + // the reason why this may be `null` is either because + // it's a container element or it's a part of a test + // environment that doesn't have styling. In either + // case it's safe not to apply styling to the element. + const classList = native.classList; if (value) { ngDevMode && ngDevMode.rendererAddClass++; renderer && isProceduralRenderer(renderer) ? renderer.addClass(native, className) : - native.classList.add(className); + (classList && classList.add(className)); } else { ngDevMode && ngDevMode.rendererRemoveClass++; renderer && isProceduralRenderer(renderer) ? renderer.removeClass(native, className) : - native.classList.remove(className); + (classList && classList.remove(className)); } } }; + +/** + * Iterates over all provided styling entries and renders them on the element. + * + * This function is used alongside a `StylingMapArray` entry. This entry is not + * the same as the `TStylingContext` and is only really used when an element contains + * initial styling values (e.g. `
`), but no style/class bindings + * are present. If and when that happens then this function will be called to render all + * initial styling values on an element. + */ +export function renderStylingMap( + renderer: Renderer3, element: RElement, stylingValues: TStylingContext | StylingMapArray | null, + isClassBased: boolean): void { + const stylingMapArr = getStylingMapArray(stylingValues); + if (stylingMapArr) { + for (let i = StylingMapArrayIndex.ValuesStartPosition; i < stylingMapArr.length; + i += StylingMapArrayIndex.TupleSize) { + const prop = getMapProp(stylingMapArr, i); + const value = getMapValue(stylingMapArr, i); + if (isClassBased) { + setClass(renderer, element, prop, value, null); + } else { + setStyle(renderer, element, prop, value, null); + } + } + } +} + +/** + * 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 99c40b7b7a..7851c90891 100644 --- a/packages/core/src/render3/styling_next/instructions.ts +++ b/packages/core/src/render3/styling_next/instructions.ts @@ -5,25 +5,22 @@ * 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 {Sanitizer} from '../../sanitization/security'; import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; -import {LContainer} from '../interfaces/container'; +import {setInputsForProperty} from '../instructions/shared'; import {AttributeMarker, TAttributes, TNode, TNodeType} from '../interfaces/node'; import {RElement} from '../interfaces/renderer'; -import {StylingContext as OldStylingContext, StylingIndex as OldStylingIndex} from '../interfaces/styling'; -import {isStylingContext as isOldStylingContext} from '../interfaces/type_checks'; -import {BINDING_INDEX, HEADER_OFFSET, HOST, LView, RENDERER} from '../interfaces/view'; -import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getLView, getSelectedIndex} from '../state'; +import {BINDING_INDEX, LView, RENDERER, TVIEW} from '../interfaces/view'; +import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getCurrentStyleSanitizer, getLView, getPreviousOrParentTNode, getSelectedIndex, setCurrentStyleSanitizer} from '../state'; +import {forceClassesAsString, forceStylesAsString} from '../styling/util'; import {NO_CHANGE} from '../tokens'; import {renderStringify} from '../util/misc_utils'; -import {getTNode} from '../util/view_utils'; +import {getNativeByTNode, getTNode} from '../util/view_utils'; -import {applyClasses, applyStyles, registerBinding, updateClassBinding, updateStyleBinding} from './bindings'; -import {TStylingContext} from './interfaces'; -import {activeStylingMapFeature, normalizeIntoStylingMap} from './map_based_bindings'; -import {setCurrentStyleSanitizer} from './state'; +import {flushStyling, updateClassBinding, updateStyleBinding} from './bindings'; +import {StylingMapArrayIndex, TStylingContext} from './interfaces'; +import {activateStylingMapFeature, addItemToStylingMap, normalizeIntoStylingMap, stylingMapToString} from './map_based_bindings'; import {attachStylingDebugObject} from './styling_debug'; -import {allocTStylingContext, getCurrentOrLViewSanitizer, hasValueChanged, updateContextDirectiveIndex} from './util'; +import {allocTStylingContext, concatString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isStylingContext, updateLastDirectiveIndex as _updateLastDirectiveIndex} from './util'; @@ -48,12 +45,24 @@ import {allocTStylingContext, getCurrentOrLViewSanitizer, hasValueChanged, updat * 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 stylingInit() { +export function ɵɵstyling() { const lView = getLView(); - const index = getSelectedIndex(); - const tNode = getTNode(index, lView); - updateLastDirectiveIndex(tNode, getActiveDirectiveStylingIndex()); + const tView = lView[TVIEW]; + if (tView.firstTemplatePass) { + const tNode = getPreviousOrParentTNode(); + const directiveStylingIndex = getActiveDirectiveStylingIndex(); + + // temporary workaround until `select(n)` is fully compatible + if (directiveStylingIndex) { + const fns = tNode.onElementCreationFns = tNode.onElementCreationFns || []; + fns.push(() => updateLastDirectiveIndex(tNode, directiveStylingIndex)); + } else { + updateLastDirectiveIndex(tNode, directiveStylingIndex); + } + } } /** @@ -72,59 +81,220 @@ export function stylingInit() { * * @codeGenApi */ -export function styleSanitizer(sanitizer: Sanitizer | StyleSanitizeFn | null): void { +export function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void { setCurrentStyleSanitizer(sanitizer); } /** - * Mirror implementation of the `styleProp()` instruction (found in `instructions/styling.ts`). + * Update a style binding on an element with the provided value. + * + * If the style value is falsy then it will be removed from the element + * (or assigned a different value depending if there are any styles placed + * on the element with `styleMap` or any static styles that are + * present from when the element was created with `styling`). + * + * Note that the styling element is updated as part of `stylingApply`. + * + * @param prop A valid CSS property. + * @param value New value to write (`null` or an empty string to remove). + * @param suffix Optional suffix. Used with scalar values to add unit such as `px`. + * Note that when a suffix is provided then the underlying sanitizer will + * be ignored. + * + * Note that this will apply the provided style value to the host element if this function is called + * within a host binding. + * + * @codeGenApi */ -export function styleProp( +export function ɵɵstyleProp( prop: string, value: string | number | String | null, suffix?: string | null): void { - _stylingProp(prop, resolveStylePropValue(value, suffix), false); + stylePropInternal(getSelectedIndex(), prop, value, suffix); +} + +export function stylePropInternal( + elementIndex: number, prop: string, value: string | number | String | null, + suffix?: string | null | undefined) { + const lView = getLView(); + + // 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 + // still needs to be incremented because all styling binding values + // are stored inside of the lView. + const bindingIndex = lView[BINDING_INDEX]++; + + const updated = _stylingProp( + elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false, + deferStylingUpdate()); + if (ngDevMode) { + ngDevMode.styleProp++; + if (updated) { + ngDevMode.stylePropCacheMiss++; + } + } } /** - * Mirror implementation of the `classProp()` instruction (found in `instructions/styling.ts`). + * Update a class binding on an element with the provided value. + * + * This instruction is meant to handle the `[class.foo]="exp"` case and, + * therefore, the class binding itself must already be allocated using + * `styling` within the creation block. + * + * @param prop A valid CSS class (only one). + * @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. + * + * @codeGenApi */ -export function classProp(className: string, value: boolean | null): void { - _stylingProp(className, value, true); +export function ɵɵclassProp(className: string, value: boolean | null): void { + const lView = getLView(); + + // 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 + // still needs to be incremented because all styling binding values + // are stored inside of the lView. + const bindingIndex = lView[BINDING_INDEX]++; + + const updated = + _stylingProp(getSelectedIndex(), bindingIndex, className, value, true, deferStylingUpdate()); + if (ngDevMode) { + ngDevMode.classProp++; + if (updated) { + ngDevMode.classPropCacheMiss++; + } + } } /** * Shared function used to update a prop-based styling binding for an element. */ function _stylingProp( - prop: string, value: boolean | number | String | string | null, isClassBased: boolean) { + elementIndex: number, bindingIndex: number, prop: string, + value: boolean | number | String | string | null | undefined | NO_CHANGE, isClassBased: boolean, + defer: boolean): boolean { + const lView = getLView(); + const tNode = getTNode(elementIndex, lView); + const native = getNativeByTNode(tNode, lView) as RElement; + + let updated = false; + if (value !== NO_CHANGE) { + if (isClassBased) { + updated = updateClassBinding( + getClassesContext(tNode), lView, native, prop, bindingIndex, + value as string | boolean | null, defer, false); + } else { + const sanitizer = getCurrentStyleSanitizer(); + updated = updateStyleBinding( + getStylesContext(tNode), lView, native, prop, bindingIndex, value as string | null, + sanitizer, defer, false); + } + } + + return updated; +} + +/** + * Update style bindings using an object literal on an element. + * + * This instruction is meant to apply styling via the `[style]="exp"` template bindings. + * When styles are applied to the element they will then be updated with respect to + * any styles/classes set via `styleProp`. If any styles are set to falsy + * then they will be removed from the element. + * + * Note that the styling instruction will not be applied until `stylingApply` is called. + * + * @param styles A key/value style map of the styles that will be applied to the given element. + * Any missing styles (that have already been applied to the element beforehand) will be + * removed (unset) from the element's styling. + * + * Note that this will apply the provided styleMap value to the host element if this function + * is called within a host binding. + * + * @codeGenApi + */ +export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | null): void { const index = getSelectedIndex(); const lView = getLView(); - const bindingIndex = lView[BINDING_INDEX]++; const tNode = getTNode(index, lView); - const defer = getActiveDirectiveSuperClassHeight() > 0; - if (isClassBased) { - updateClassBinding( - getClassesContext(tNode), lView, prop, bindingIndex, value as string | boolean | null, - defer, false); - } else { - const sanitizer = getCurrentOrLViewSanitizer(lView); - updateStyleBinding( - getStylesContext(tNode), lView, prop, bindingIndex, value as string | null, sanitizer, - defer, false); + 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 + // still needs to be incremented because all styling binding values + // are stored inside of the lView. + const bindingIndex = lView[BINDING_INDEX]++; + + // 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) { + updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false); + styles = NO_CHANGE; + } + + const updated = _stylingMap(index, context, bindingIndex, styles, false, deferStylingUpdate()); + if (ngDevMode) { + ngDevMode.styleMap++; + if (updated) { + ngDevMode.styleMapCacheMiss++; + } } } /** - * Mirror implementation of the `styleMap()` instruction (found in `instructions/styling.ts`). + * Update class bindings using an object literal or class-string on an element. + * + * This instruction is meant to apply styling via the `[class]="exp"` template bindings. + * When classes are applied to the element they will then be updated with + * respect to any styles/classes set via `classProp`. If any + * classes are set to falsy then they will be removed from the element. + * + * Note that the styling instruction will not be applied until `stylingApply` is called. + * Note that this will the provided classMap value to the host element if this function is called + * within a host binding. + * + * @param classes A key/value map or string of CSS classes that will be added to the + * given element. Any missing classes (that have already been applied to the element + * beforehand) will be removed (unset) from the element's list of CSS classes. + * + * @codeGenApi */ -export function styleMap(styles: {[styleName: string]: any} | NO_CHANGE | null): void { - _stylingMap(styles, false); +export function ɵɵclassMap(classes: {[className: string]: any} | NO_CHANGE | string | null): void { + classMapInternal(getSelectedIndex(), classes); } -/** - * Mirror implementation of the `classMap()` instruction (found in `instructions/styling.ts`). - */ -export function classMap(classes: {[className: string]: any} | NO_CHANGE | string | null): void { - _stylingMap(classes, true); +export function classMapInternal( + elementIndex: number, classes: {[className: string]: any} | NO_CHANGE | string | null) { + const lView = getLView(); + const tNode = getTNode(elementIndex, lView); + const context = getClassesContext(tNode); + const directiveIndex = getActiveDirectiveStylingIndex(); + + // if a value is interpolated then it may render a `NO_CHANGE` value. + // in this case we do not need to do anything, but the binding index + // still needs to be incremented because all styling binding values + // are stored inside of the lView. + const bindingIndex = lView[BINDING_INDEX]++; + + // 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) { + updateDirectiveInputValue(context, lView, tNode, bindingIndex, classes, true); + classes = NO_CHANGE; + } + + const updated = + _stylingMap(elementIndex, context, bindingIndex, classes, true, deferStylingUpdate()); + if (ngDevMode) { + ngDevMode.classMap++; + if (updated) { + ngDevMode.classMapCacheMiss++; + } + } } /** @@ -133,28 +303,89 @@ export function classMap(classes: {[className: string]: any} | NO_CHANGE | strin * When this function is called it will activate support for `[style]` and * `[class]` bindings in Angular. */ -function _stylingMap(value: {[key: string]: any} | string | null, isClassBased: boolean) { - activeStylingMapFeature(); - const index = getSelectedIndex(); +function _stylingMap( + elementIndex: number, context: TStylingContext, bindingIndex: number, + value: {[key: string]: any} | string | null, isClassBased: boolean, defer: boolean) { + activateStylingMapFeature(); const lView = getLView(); - const bindingIndex = lView[BINDING_INDEX]++; + let valueHasChanged = false; if (value !== NO_CHANGE) { - const tNode = getTNode(index, lView); - const defer = getActiveDirectiveSuperClassHeight() > 0; + const tNode = getTNode(elementIndex, lView); + const native = getNativeByTNode(tNode, lView) as RElement; const oldValue = lView[bindingIndex]; - const valueHasChanged = hasValueChanged(oldValue, value); - const lStylingMap = normalizeIntoStylingMap(oldValue, value); + valueHasChanged = hasValueChanged(oldValue, value); + const stylingMapArr = normalizeIntoStylingMap(oldValue, value, !isClassBased); if (isClassBased) { updateClassBinding( - getClassesContext(tNode), lView, null, bindingIndex, lStylingMap, defer, valueHasChanged); + context, lView, native, null, bindingIndex, stylingMapArr, defer, valueHasChanged); } else { - const sanitizer = getCurrentOrLViewSanitizer(lView); + const sanitizer = getCurrentStyleSanitizer(); updateStyleBinding( - getStylesContext(tNode), lView, null, bindingIndex, lStylingMap, sanitizer, defer, + context, lView, native, null, bindingIndex, stylingMapArr, sanitizer, defer, valueHasChanged); } } + + return valueHasChanged; +} + +/** + * Writes a value to a directive's `style` or `class` input binding (if it has changed). + * + * If a directive has a `@Input` binding that is set on `style` or `class` then that value + * will take priority over the underlying style/class styling bindings. This value will + * be updated for the binding each time during change detection. + * + * When this occurs this function will attempt to write the value to the input binding + * depending on the following situations: + * + * - If `oldValue !== newValue` + * - If `newValue` is `null` (but this is skipped if it is during the first update pass-- + * which is when the context is not locked yet) + */ +function updateDirectiveInputValue( + context: TStylingContext, lView: LView, tNode: TNode, bindingIndex: number, newValue: any, + isClassBased: boolean): void { + const oldValue = lView[bindingIndex]; + if (oldValue !== newValue) { + // 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'] !; + const initialValue = getInitialStylingValue(context); + const value = normalizeStylingDirectiveInputValue(initialValue, newValue, isClassBased); + setInputsForProperty(lView, inputs, value); + } + lView[bindingIndex] = newValue; + } +} + +/** + * Returns the appropriate directive input value for `style` or `class`. + * + * Earlier versions of Angular expect a binding value to be passed into directive code + * exactly as it is unless there is a static value present (in which case both values + * will be stringified and concatenated). + */ +function normalizeStylingDirectiveInputValue( + initialValue: string, bindingValue: string | {[key: string]: any} | null, + isClassBased: boolean) { + let value = bindingValue; + + // 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 (isClassBased) { + value = concatString(initialValue, forceClassesAsString(bindingValue)); + } else { + value = concatString( + initialValue, forceStylesAsString(bindingValue as{[key: string]: any} | null | undefined), + ';'); + } + } + return value; } /** @@ -169,53 +400,22 @@ function _stylingMap(value: {[key: string]: any} | string | null, isClassBased: * host binding instruction code at the right time), this means that a * styling apply function is still needed. * - * This function is a mirror implementation of the `stylingApply()` - * instruction (found in `instructions/styling.ts`). + * @codeGenApi */ -export function stylingApply() { - const index = getSelectedIndex(); +export function ɵɵstylingApply() { + const elementIndex = getSelectedIndex(); const lView = getLView(); - const tNode = getTNode(index, lView); + const tNode = getTNode(elementIndex, lView); const renderer = getRenderer(tNode, lView); - const native = getNativeFromLView(index, lView); + const native = getNativeByTNode(tNode, lView) as RElement; const directiveIndex = getActiveDirectiveStylingIndex(); - applyClasses(renderer, lView, getClassesContext(tNode), native, directiveIndex); - - const sanitizer = getCurrentOrLViewSanitizer(lView); - applyStyles(renderer, lView, getStylesContext(tNode), native, directiveIndex, sanitizer); - + const sanitizer = getCurrentStyleSanitizer(); + flushStyling( + renderer, lView, getClassesContext(tNode), getStylesContext(tNode), native, directiveIndex, + sanitizer); setCurrentStyleSanitizer(null); } -/** - * 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/`). - * - * The purpose of this function is to traverse through the LView data - * for a specific element index and return the native node. Because the - * current implementation relies on there being a styling context array, - * the code below will need to loop through these array values until it - * gets a native element node. - * - * Note that this code is temporary and will disappear once the new - * styling refactor lands in its entirety. - */ -function getNativeFromLView(index: number, viewData: LView): RElement { - let storageIndex = index + HEADER_OFFSET; - let slotValue: LContainer|LView|OldStylingContext|RElement = viewData[storageIndex]; - let wrapper: LContainer|LView|OldStylingContext = viewData; - while (Array.isArray(slotValue)) { - wrapper = slotValue; - slotValue = slotValue[HOST] as LView | OldStylingContext | RElement; - } - if (isOldStylingContext(wrapper)) { - return wrapper[OldStylingIndex.ElementPosition] as RElement; - } else { - return slotValue; - } -} - function getRenderer(tNode: TNode, lView: LView) { return tNode.type === TNodeType.Element ? lView[RENDERER] : null; } @@ -224,28 +424,41 @@ function getRenderer(tNode: TNode, lView: LView) { * Searches and assigns provided all static style/class entries (found in the `attrs` value) * and registers them in their respective styling contexts. */ -export function registerInitialStylingIntoContext( - tNode: TNode, attrs: TAttributes, startIndex: number) { - let classesContext !: TStylingContext; - let stylesContext !: TStylingContext; +export function registerInitialStylingOnTNode( + tNode: TNode, attrs: TAttributes, startIndex: number): boolean { + let hasAdditionalInitialStyling = false; + let styles = getStylingMapArray(tNode.styles); + let classes = getStylingMapArray(tNode.classes); let mode = -1; for (let i = startIndex; i < attrs.length; i++) { - const attr = attrs[i]; + const attr = attrs[i] as string; if (typeof attr == 'number') { mode = attr; } else if (mode == AttributeMarker.Classes) { - classesContext = classesContext || getClassesContext(tNode); - registerBinding(classesContext, -1, attr as string, true, false); + classes = classes || ['']; + addItemToStylingMap(classes, attr, true); + hasAdditionalInitialStyling = true; } else if (mode == AttributeMarker.Styles) { - stylesContext = stylesContext || getStylesContext(tNode); - registerBinding(stylesContext, -1, attr as string, attrs[++i] as string, false); + const value = attrs[++i] as string | null; + styles = styles || ['']; + addItemToStylingMap(styles, attr, value); + hasAdditionalInitialStyling = true; } } + + if (classes && classes.length > StylingMapArrayIndex.ValuesStartPosition) { + classes[StylingMapArrayIndex.RawValuePosition] = stylingMapToString(classes, true); + tNode.classes = classes; + } + + if (styles && styles.length > StylingMapArrayIndex.ValuesStartPosition) { + styles[StylingMapArrayIndex.RawValuePosition] = stylingMapToString(styles, false); + tNode.styles = styles; + } + + return hasAdditionalInitialStyling; } -/** - * Mirror implementation of the same function found in `instructions/styling.ts`. - */ 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 @@ -269,8 +482,8 @@ export function getActiveDirectiveStylingIndex(): number { * removed once `select(n)` is fixed). */ function updateLastDirectiveIndex(tNode: TNode, directiveIndex: number) { - updateContextDirectiveIndex(getClassesContext(tNode), directiveIndex); - updateContextDirectiveIndex(getStylesContext(tNode), directiveIndex); + _updateLastDirectiveIndex(getClassesContext(tNode), directiveIndex); + _updateLastDirectiveIndex(getStylesContext(tNode), directiveIndex); } function getStylesContext(tNode: TNode): TStylingContext { @@ -285,23 +498,26 @@ function getClassesContext(tNode: TNode): TStylingContext { * Returns/instantiates a styling context from/to a `tNode` instance. */ function getContext(tNode: TNode, isClassBased: boolean) { - let context = isClassBased ? tNode.newClasses : tNode.newStyles; - if (!context) { - context = allocTStylingContext(); + let context = isClassBased ? tNode.classes : tNode.styles; + if (!isStylingContext(context)) { + context = allocTStylingContext(context); if (ngDevMode) { - attachStylingDebugObject(context); + attachStylingDebugObject(context as TStylingContext); } if (isClassBased) { - tNode.newClasses = context; + tNode.classes = context; } else { - tNode.newStyles = context; + tNode.styles = context; } } - return context; + return context as TStylingContext; } function resolveStylePropValue( - value: string | number | String | null, suffix: string | null | undefined) { + value: string | number | String | null | NO_CHANGE, suffix: string | null | undefined): string| + null|undefined|NO_CHANGE { + if (value === NO_CHANGE) return value; + let resolvedValue: string|null = null; if (value !== null) { if (suffix) { @@ -318,3 +534,19 @@ 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; +} diff --git a/packages/core/src/render3/styling_next/interfaces.ts b/packages/core/src/render3/styling_next/interfaces.ts index 3d99d9cbd5..671bc91bf0 100644 --- a/packages/core/src/render3/styling_next/interfaces.ts +++ b/packages/core/src/render3/styling_next/interfaces.ts @@ -37,17 +37,37 @@ import {LView} from '../interfaces/view'; * tNode.classes = [ ... a context only for classes ... ]; * ``` * + * `tNode.styles` and `tNode.classes` can be an instance of the following: + * + * ```typescript + * tNode.styles = null; // no static styling or styling bindings active + * tNode.styles = StylingMapArray; // only static values present (e.g. `
`) + * tNode.styles = TStylingContext; // one or more styling bindings present (e.g. `
`) + * ``` + * + * Both `tNode.styles` and `tNode.classes` are instantiated when anything + * styling-related is active on an element. They are first created from + * from the any of the element-level instructions (e.g. `element`, + * `elementStart`, `elementHostAttrs`). When any static style/class + * values are encountered they are registered on the `tNode.styles` + * and `tNode.classes` data-structures. By default (when any static + * values are encountered) the `tNode.styles` or `tNode.classes` values + * are instances of a `StylingMapArray`. Only when style/class bindings + * are detected then that styling map is converted into an instance of + * `TStylingContext`. + * * Due to the fact the the `TStylingContext` is stored on a `TNode` * this means that all data within the context is static. Instead of * storing actual styling binding values, the lView binding index values * are stored within the context. (static nature means it is more compact.) - * * ```typescript * //
// lView binding index = 22 * tNode.stylesContext = [ + * [], // initial values array * 0, // the context config value * * 0b001, // guard mask for width @@ -64,6 +84,7 @@ import {LView} from '../interfaces/view'; * ]; * * tNode.classesContext = [ + * [], // initial values array * 0, // the context config value * * 0b001, // guard mask for active @@ -260,7 +281,11 @@ import {LView} from '../interfaces/view'; * If sanitization returns an empty value then that empty value will be applied * to the element. */ -export interface TStylingContext extends Array { +export interface TStylingContext extends + Array { + /** Initial value position for static styles */ + [TStylingContextIndex.InitialStylingValuePosition]: StylingMapArray|null; + /** Configuration data for the context */ [TStylingContextIndex.ConfigPosition]: TStylingConfigFlags; @@ -268,7 +293,7 @@ export interface TStylingContext extends Array { - [LStylingMapIndex.RawValuePosition]: {}|string|null; +export interface StylingMapArray extends Array<{}|string|number|null> { + [StylingMapArrayIndex.RawValuePosition]: {}|string|null; } /** - * An index of position and offset points for any data stored within a `LStylingMap` instance. + * An index of position and offset points for any data stored within a `StylingMapArray` instance. */ -export const enum LStylingMapIndex { +export const enum StylingMapArrayIndex { /** The location of the raw key/value map instance used last to populate the array entries */ RawValuePosition = 0, @@ -394,7 +439,7 @@ export const enum LStylingMapIndex { * Used to apply/traverse across all map-based styling entries up to the provided `targetProp` * value. * - * When called, each of the map-based `LStylingMap` entries (which are stored in + * When called, each of the map-based `StylingMapArray` entries (which are stored in * the provided `LStylingData` array) will be iterated over. Depending on the provided * `mode` value, each prop/value entry may be applied or skipped over. * 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 d24f67eec4..259ca917ac 100644 --- a/packages/core/src/render3/styling_next/map_based_bindings.ts +++ b/packages/core/src/render3/styling_next/map_based_bindings.ts @@ -9,8 +9,9 @@ import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanit import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer'; import {setStylingMapsSyncFn} from './bindings'; -import {ApplyStylingFn, LStylingData, LStylingMap, LStylingMapIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from './interfaces'; -import {getBindingValue, getValuesCount, isStylingValueDefined} from './util'; +import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from './interfaces'; +import {concatString, getBindingValue, getMapProp, getMapValue, getValuesCount, hyphenate, isStylingValueDefined, setMapValue} from './util'; + /** @@ -45,8 +46,8 @@ import {getBindingValue, getValuesCount, isStylingValueDefined} from './util'; * * # The Algorithm * Whenever a map-based binding updates (which is when the identity of the - * map-value changes) then the map is iterated over and a `LStylingMap` array - * is produced. The `LStylingMap` instance is stored in the binding location + * map-value changes) then the map is iterated over and a `StylingMapArray` array + * is produced. The `StylingMapArray` instance is stored in the binding location * where the `BINDING_INDEX` is situated when the `styleMap()` or `classMap()` * instruction were called. Once the binding changes, then the internal `bitMask` * value is marked as dirty. @@ -154,19 +155,18 @@ function innerSyncStylingMap( mode: StylingMapsSyncMode, targetProp: string | null, currentMapIndex: number, defaultValue: string | null): boolean { let targetPropValueWasApplied = false; - const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition); if (currentMapIndex < totalMaps) { const bindingIndex = getBindingValue( context, TStylingContextIndex.MapBindingsPosition, currentMapIndex) as number; - const lStylingMap = data[bindingIndex] as LStylingMap; + const stylingMapArr = data[bindingIndex] as StylingMapArray; let cursor = getCurrentSyncCursor(currentMapIndex); - while (cursor < lStylingMap.length) { - const prop = getMapProp(lStylingMap, cursor); + while (cursor < stylingMapArr.length) { + const prop = getMapProp(stylingMapArr, cursor); const iteratedTooFar = targetProp && prop > targetProp; const isTargetPropMatched = !iteratedTooFar && prop === targetProp; - const value = getMapValue(lStylingMap, cursor); + const value = getMapValue(stylingMapArr, cursor); const valueIsDefined = isStylingValueDefined(value); // the recursive code is designed to keep applying until @@ -183,6 +183,9 @@ function innerSyncStylingMap( currentMapIndex + 1, defaultValue); if (iteratedTooFar) { + if (!targetPropValueWasApplied) { + targetPropValueWasApplied = valueApplied; + } break; } @@ -198,11 +201,24 @@ function innerSyncStylingMap( } targetPropValueWasApplied = valueApplied && isTargetPropMatched; - cursor += LStylingMapIndex.TupleSize; + cursor += StylingMapArrayIndex.TupleSize; } setCurrentSyncCursor(currentMapIndex, cursor); + + // this is a fallback case in the event that the styling map is `null` for this + // binding but there are other map-based bindings that need to be evaluated + // afterwards. If the `prop` value is falsy then the intention is to cycle + // through all of the properties in the remaining maps as well. If the current + // styling map is too short then there are no values to iterate over. In either + // case the follow-up maps need to be iterated over. + if (stylingMapArr.length === StylingMapArrayIndex.ValuesStartPosition || !targetProp) { + return innerSyncStylingMap( + context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp, + currentMapIndex + 1, defaultValue); + } } + return targetPropValueWasApplied; } @@ -210,7 +226,7 @@ function innerSyncStylingMap( /** * Enables support for map-based styling bindings (e.g. `[style]` and `[class]` bindings). */ -export function activeStylingMapFeature() { +export function activateStylingMapFeature() { setStylingMapsSyncFn(syncStylingMap); } @@ -220,19 +236,21 @@ export function activeStylingMapFeature() { * If an inner map is iterated on then this is done so for one * of two reasons: * - * - The target property was detected and the inner map - * must now "catch up" (pointer-wise) up to where the current - * map's cursor is situated. + * - value is being applied: + * if the value is being applied from this current styling + * map then there is no need to apply it in a deeper map. * - * - The target property was not detected in the current map - * and must be found in an inner map. This can only be allowed - * if the current map iteration is not set to skip the target - * property. + * - value is being not applied: + * apply the value if it is found in a deeper map. + * + * When these reasons are encountered the flags will for the + * inner map mode will be configured. */ function resolveInnerMapMode( currentMode: number, valueIsDefined: boolean, isExactMatch: boolean): number { let innerMode = currentMode; - if (!valueIsDefined && isExactMatch && !(currentMode & StylingMapsSyncMode.SkipTargetProp)) { + if (!valueIsDefined && !(currentMode & StylingMapsSyncMode.SkipTargetProp) && + (isExactMatch || (currentMode & StylingMapsSyncMode.ApplyAllValues))) { // case 1: set the mode to apply the targeted prop value if it // ends up being encountered in another map value innerMode |= StylingMapsSyncMode.ApplyTargetProp; @@ -243,6 +261,7 @@ function resolveInnerMapMode( innerMode |= StylingMapsSyncMode.SkipTargetProp; innerMode &= ~StylingMapsSyncMode.ApplyTargetProp; } + return innerMode; } @@ -281,7 +300,7 @@ const MAP_CURSORS: number[] = []; */ function resetSyncCursors() { for (let i = 0; i < MAP_CURSORS.length; i++) { - MAP_CURSORS[i] = LStylingMapIndex.ValuesStartPosition; + MAP_CURSORS[i] = StylingMapArrayIndex.ValuesStartPosition; } } @@ -290,7 +309,7 @@ function resetSyncCursors() { */ function getCurrentSyncCursor(mapIndex: number) { if (mapIndex >= MAP_CURSORS.length) { - MAP_CURSORS.push(LStylingMapIndex.ValuesStartPosition); + MAP_CURSORS.push(StylingMapArrayIndex.ValuesStartPosition); } return MAP_CURSORS[mapIndex]; } @@ -303,33 +322,34 @@ function setCurrentSyncCursor(mapIndex: number, indexValue: number) { } /** - * Used to convert a {key:value} map into a `LStylingMap` array. + * Used to convert a {key:value} map into a `StylingMapArray` array. * - * This function will either generate a new `LStylingMap` instance + * This function will either generate a new `StylingMapArray` instance * or it will patch the provided `newValues` map value into an - * existing `LStylingMap` value (this only happens if `bindingValue` - * is an instance of `LStylingMap`). + * existing `StylingMapArray` value (this only happens if `bindingValue` + * is an instance of `StylingMapArray`). * - * If a new key/value map is provided with an old `LStylingMap` + * 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 | LStylingMap, - newValues: {[key: string]: any} | string | null | undefined): LStylingMap { - const lStylingMap: LStylingMap = Array.isArray(bindingValue) ? bindingValue : [null]; - lStylingMap[LStylingMapIndex.RawValuePosition] = newValues || null; + 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 = LStylingMapIndex.ValuesStartPosition; j < lStylingMap.length; - j += LStylingMapIndex.TupleSize) { - setMapValue(lStylingMap, j, null); + for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length; + j += StylingMapArrayIndex.TupleSize) { + setMapValue(stylingMapArr, j, null); } let props: string[]|null = null; @@ -346,36 +366,80 @@ export function normalizeIntoStylingMap( } if (props) { - outer: for (let i = 0; i < props.length; i++) { + 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]; - for (let j = LStylingMapIndex.ValuesStartPosition; j < lStylingMap.length; - j += LStylingMapIndex.TupleSize) { - const propAtIndex = getMapProp(lStylingMap, j); - if (prop <= propAtIndex) { - if (propAtIndex === prop) { - setMapValue(lStylingMap, j, value); - } else { - lStylingMap.splice(j, 0, prop, value); - } - continue outer; - } - } - lStylingMap.push(prop, value); + addItemToStylingMap(stylingMapArr, newProp, value, true); } } - return lStylingMap; + return stylingMapArr; } -export function getMapProp(map: LStylingMap, index: number): string { - return map[index + LStylingMapIndex.PropOffset] as string; +/** + * 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; } -export function setMapValue(map: LStylingMap, index: number, value: string | null): void { - map[index + LStylingMapIndex.ValueOffset] = value; +/** + * 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; } -export function getMapValue(map: LStylingMap, index: number): string|null { - return map[index + LStylingMapIndex.ValueOffset] as string | null; +/** + * 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 50e42c6807..41f559196a 100644 --- a/packages/core/src/render3/styling_next/state.ts +++ b/packages/core/src/render3/styling_next/state.ts @@ -5,57 +5,97 @@ * 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 {Sanitizer} from '../../sanitization/security'; -import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; /** * -------- * - * This file contains temporary code to incorporate the new styling refactor - * code to work alongside the existing instruction set. + * // TODO(matsko): add updateMask info * - * This file will be removed once `select(n)` is fully functional (once - * it is able to evaluate host bindings in sync element-by-element - * with template code). + * This file contains all state-based logic for styling in Angular. + * + * Styling in Angular is evaluated with a series of styling-specific + * template instructions which are called one after another each time + * change detection occurs in Angular. + * + * Styling makes use of various temporary, state-based variables between + * instructions so that it can better cache and optimize its values. + * These values are usually populated and cleared when an element is + * 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`. * * -------- */ -/** - * A temporary enum of states that inform the core whether or not - * to defer all styling instruction calls to the old or new - * styling implementation. - */ -export const enum RuntimeStylingMode { - UseOld = 0, - UseBothOldAndNew = 1, - UseNew = 2, -} +let _stylingState: StylingState|null = null; +const _stateStorage = new Map(); -let _stylingMode = 0; +// 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; /** - * Temporary function used to inform the existing styling algorithm - * code to delegate all styling instruction calls to the new refactored - * styling code. + * Used as a state reference for update values between style/class binding instructions. */ -export function runtimeSetStylingMode(mode: RuntimeStylingMode) { - _stylingMode = mode; +export interface StylingState { + classesBitMask: number; + classesIndex: number; + stylesBitMask: number; + stylesIndex: number; } -export function runtimeIsNewStylingInUse() { - return _stylingMode > RuntimeStylingMode.UseOld; +export const STYLING_INDEX_START_VALUE = 1; +export const BIT_MASK_START_VALUE = 0; + +export function getStylingState(element: any, readFromMap?: boolean): StylingState { + if (!_stylingElement || element !== _stylingElement) { + _stylingElement = element; + if (readFromMap) { + _stylingState = _stateStorage.get(element) || null; + ngDevMode && ngDevMode.stylingReadPersistedState++; + } + _stylingState = _stylingState || { + classesBitMask: BIT_MASK_START_VALUE, + classesIndex: STYLING_INDEX_START_VALUE, + stylesBitMask: BIT_MASK_START_VALUE, + stylesIndex: STYLING_INDEX_START_VALUE, + }; + } + return _stylingState !; } -export function runtimeAllowOldStyling() { - return _stylingMode < RuntimeStylingMode.UseNew; +export function resetStylingState() { + _stylingState = null; + _stylingElement = null; } -let _currentSanitizer: Sanitizer|StyleSanitizeFn|null; -export function setCurrentStyleSanitizer(sanitizer: Sanitizer | StyleSanitizeFn | null) { - _currentSanitizer = sanitizer; +export function storeStylingState(element: any, state: StylingState) { + ngDevMode && ngDevMode.stylingWritePersistedState++; + _stateStorage.set(element, state); } -export function getCurrentStyleSanitizer() { - return _currentSanitizer; +export function deleteStylingStateFromStorage(element: any) { + _stateStorage.delete(element); +} + +export function resetAllStylingState() { + resetStylingState(); + _stateStorage.clear(); } diff --git a/packages/core/src/render3/styling_next/styling_debug.ts b/packages/core/src/render3/styling_next/styling_debug.ts index 926ba8044b..04ec2d90c2 100644 --- a/packages/core/src/render3/styling_next/styling_debug.ts +++ b/packages/core/src/render3/styling_next/styling_debug.ts @@ -5,17 +5,16 @@ * 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 {Sanitizer} from '../../sanitization/security'; import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {RElement} from '../interfaces/renderer'; -import {LView, SANITIZER} from '../interfaces/view'; +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 {activeStylingMapFeature} from './map_based_bindings'; -import {getCurrentStyleSanitizer} from './state'; -import {getCurrentOrLViewSanitizer, getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased, isSanitizationRequired} from './util'; +import {activateStylingMapFeature} from './map_based_bindings'; +import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased, isSanitizationRequired} from './util'; @@ -79,7 +78,7 @@ export interface TStylingTupleSummary { /** The property (style or class property) that this tuple represents */ prop: string; - /** The total amount of styling entries apart of this tuple */ + /** The total amount of styling entries a part of this tuple */ valuesCount: number; /** @@ -208,15 +207,14 @@ export class NodeStylingDebug implements DebugStyling { const mockElement = {} as any; const hasMaps = getValuesCount(this.context, TStylingContextIndex.MapBindingsPosition) > 0; if (hasMaps) { - activeStylingMapFeature(); + activateStylingMapFeature(); } const mapFn: ApplyStylingFn = (renderer: any, element: RElement, prop: string, value: string | null, bindingIndex?: number | null) => { fn(prop, value, bindingIndex || null); }; - const sanitizer = this._isClassBased ? null : (this._sanitizer || - getCurrentOrLViewSanitizer(this._data as LView)); + const sanitizer = this._isClassBased ? null : (this._sanitizer || getCurrentStyleSanitizer()); applyStyling(this.context, null, mockElement, this._data, true, mapFn, sanitizer); } } diff --git a/packages/core/src/render3/styling_next/util.ts b/packages/core/src/render3/styling_next/util.ts index 9708910c7e..1eeccdedbf 100644 --- a/packages/core/src/render3/styling_next/util.ts +++ b/packages/core/src/render3/styling_next/util.ts @@ -5,50 +5,62 @@ * 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 {Sanitizer, SecurityContext} from '../../sanitization/security'; -import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer'; -import {StylingContext} from '../interfaces/styling'; -import {LView, SANITIZER} from '../interfaces/view'; -import {getProp as getOldProp, getSinglePropIndexValue as getOldSinglePropIndexValue} from '../styling/class_and_style_bindings'; +import {TNode, TNodeFlags} from '../interfaces/node'; +import {isDifferent} from '../util/misc_utils'; -import {LStylingMap, LStylingMapIndex, TStylingConfigFlags, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces'; -import {getCurrentStyleSanitizer, setCurrentStyleSanitizer} from './state'; +import {StylingMapArray, StylingMapArrayIndex, TStylingConfigFlags, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces'; const MAP_BASED_ENTRY_PROP_NAME = '--MAP--'; /** * Creates a new instance of the `TStylingContext`. * - * This function will also pre-fill the context with data - * for map-based bindings. + * The `TStylingContext` is used as a manifest of all style or all class bindings on + * an element. Because it is a T-level data-structure, it is only created once per + * tNode for styles and for classes. This function allocates a new instance of a + * `TStylingContext` with the initial values (see `interfaces.ts` for more info). */ -export function allocTStylingContext(): TStylingContext { +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; - return [TStylingConfigFlags.Initial, 0, mapBasedConfig, 0, MAP_BASED_ENTRY_PROP_NAME]; + const context: TStylingContext = [ + initialStyling || null, + TStylingConfigFlags.Initial, + // the LastDirectiveIndex value in the context is used to track which directive is the last + // to call `stylingApply()`. The `-1` value implies that no directive has been set yet. + -1, + mapBasedConfig, + 0, + MAP_BASED_ENTRY_PROP_NAME, + ]; + return context; } /** - * Temporary function that allows for a string-based property name to be - * obtained from an index-based property identifier. + * Sets the provided directive as the last directive index in the provided `TStylingContext`. * - * This function will be removed once the new styling refactor code (which - * lives inside of `render3/styling_next/`) replaces the existing styling - * implementation. + * 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 getBindingNameFromIndex( - stylingContext: StylingContext, offset: number, directiveIndex: number, isClassBased: boolean) { - const singleIndex = - getOldSinglePropIndexValue(stylingContext, directiveIndex, offset, isClassBased); - return getOldProp(stylingContext, singleIndex); -} - -export function updateContextDirectiveIndex(context: TStylingContext, index: number) { - context[TStylingContextIndex.MaxDirectiveIndexPosition] = index; +export function updateLastDirectiveIndex( + context: TStylingContext, lastDirectiveIndex: number): void { + const currentValue = context[TStylingContextIndex.LastDirectiveIndexPosition]; + if (lastDirectiveIndex !== currentValue) { + context[TStylingContextIndex.LastDirectiveIndexPosition] = lastDirectiveIndex; + if (currentValue === 0 && lastDirectiveIndex > 0) { + markContextToPersistState(context); + } + } } function getConfig(context: TStylingContext) { @@ -101,8 +113,9 @@ export function getDefaultValue(context: TStylingContext, index: number): string * 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, index: number) { - return index === context[TStylingContextIndex.MaxDirectiveIndexPosition]; +export function allowStylingFlush(context: TStylingContext | null, index: number) { + return (context && index === context[TStylingContextIndex.LastDirectiveIndexPosition]) ? true : + false; } export function lockContext(context: TStylingContext) { @@ -113,6 +126,14 @@ export function isContextLocked(context: TStylingContext): boolean { return (getConfig(context) & TStylingConfigFlags.Locked) > 0; } +export function stateIsPersisted(context: TStylingContext): boolean { + return (getConfig(context) & TStylingConfigFlags.PersistStateValues) > 0; +} + +export function markContextToPersistState(context: TStylingContext) { + setConfig(context, getConfig(context) | TStylingConfigFlags.PersistStateValues); +} + export function getPropValuesStartPosition(context: TStylingContext) { return TStylingContextIndex.MapBindingsBindingsStartPosition + context[TStylingContextIndex.MapBindingsValuesCountPosition]; @@ -123,11 +144,20 @@ export function isMapBased(prop: string) { } export function hasValueChanged( - a: LStylingMap | number | String | string | null | boolean | undefined | {}, - b: LStylingMap | number | String | string | null | boolean | undefined | {}): boolean { - const compareValueA = Array.isArray(a) ? a[LStylingMapIndex.RawValuePosition] : a; - const compareValueB = Array.isArray(b) ? b[LStylingMapIndex.RawValuePosition] : b; - return compareValueA !== compareValueB; + a: StylingMapArray | number | String | string | null | boolean | undefined | {}, + b: StylingMapArray | number | String | string | null | boolean | undefined | {}): boolean { + let compareValueA = Array.isArray(a) ? a[StylingMapArrayIndex.RawValuePosition] : a; + let compareValueB = Array.isArray(b) ? b[StylingMapArrayIndex.RawValuePosition] : b; + + // these are special cases for String based values (which are created as artifacts + // when sanitization is bypassed on a particular value) + if (compareValueA instanceof String) { + compareValueA = compareValueA.toString(); + } + if (compareValueB instanceof String) { + compareValueB = compareValueB.toString(); + } + return isDifferent(compareValueA, compareValueB); } /** @@ -142,35 +172,59 @@ export function isStylingValueDefined(value: any) { return value != null && value !== ''; } -/** - * Returns the current style sanitizer function for the given view. - * - * The default style sanitizer (which lives inside of `LView`) will - * be returned depending on whether the `styleSanitizer` instruction - * was called or not prior to any styling instructions running. - */ -export function getCurrentOrLViewSanitizer(lView: LView): StyleSanitizeFn|null { - const sanitizer: StyleSanitizeFn|null = (getCurrentStyleSanitizer() || lView[SANITIZER]) as any; - if (sanitizer && typeof sanitizer !== 'function') { - setCurrentStyleSanitizer(sanitizer); - return sanitizeUsingSanitizerObject; - } - return sanitizer; +export function concatString(a: string, b: string, separator = ' '): string { + return a + ((b.length && a.length) ? separator : '') + b; +} + +export function hyphenate(value: string): string { + return value.replace(/[a-z][A-Z]/g, v => v.charAt(0) + '-' + v.charAt(1)).toLowerCase(); } /** - * Style sanitization function that internally uses a `Sanitizer` instance to handle style - * sanitization. + * Returns an instance of `StylingMapArray`. + * + * This function is designed to find an instance of `StylingMapArray` in case it is stored + * inside of an instance of `TStylingContext`. When a styling context is created it + * will copy over an initial styling values from the tNode (which are stored as a + * `StylingMapArray` on the `tNode.classes` or `tNode.styles` values). */ -const sanitizeUsingSanitizerObject: StyleSanitizeFn = - (prop: string, value: string | null, mode?: StyleSanitizeMode) => { - const sanitizer = getCurrentStyleSanitizer() as Sanitizer; - if (sanitizer) { - if (mode !== undefined && mode & StyleSanitizeMode.SanitizeOnly) { - return sanitizer.sanitize(SecurityContext.STYLE, value); - } else { - return true; - } - } - return value; - }; +export function getStylingMapArray(value: TStylingContext | StylingMapArray | null): + StylingMapArray|null { + return isStylingContext(value) ? + (value as TStylingContext)[TStylingContextIndex.InitialStylingValuePosition] : + value; +} + +export function isStylingContext(value: TStylingContext | StylingMapArray | null): boolean { + // the StylingMapArray is in the format of [initial, prop, string, prop, string] + // and this is the defining value to distinguish between arrays + return Array.isArray(value) && + value.length >= TStylingContextIndex.MapBindingsBindingsStartPosition && + typeof value[1] !== 'string'; +} + +export function getInitialStylingValue(context: TStylingContext | StylingMapArray | null): string { + const map = getStylingMapArray(context); + return map && (map[StylingMapArrayIndex.RawValuePosition] as string | null) || ''; +} + +export function hasClassInput(tNode: TNode) { + return (tNode.flags & TNodeFlags.hasClassInput) !== 0; +} + +export function hasStyleInput(tNode: TNode) { + return (tNode.flags & TNodeFlags.hasStyleInput) !== 0; +} + +export function getMapProp(map: StylingMapArray, index: number): string { + return map[index + StylingMapArrayIndex.PropOffset] as string; +} + +export function setMapValue( + map: StylingMapArray, index: number, value: string | boolean | null): void { + map[index + StylingMapArrayIndex.ValueOffset] = value; +} + +export function getMapValue(map: StylingMapArray, index: number): string|null { + return map[index + StylingMapArrayIndex.ValueOffset] as string | null; +} diff --git a/packages/core/src/render3/util/view_utils.ts b/packages/core/src/render3/util/view_utils.ts index 67d37015f1..32343ca21f 100644 --- a/packages/core/src/render3/util/view_utils.ts +++ b/packages/core/src/render3/util/view_utils.ts @@ -40,9 +40,9 @@ import {FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, PARENT, PREORDER_HOOK_FLA /** * Returns `RNode`. - * @param value wrapped value of `RNode`, `LView`, `LContainer`, `StylingContext` + * @param value wrapped value of `RNode`, `LView`, `LContainer` */ -export function unwrapRNode(value: RNode | LView | LContainer | StylingContext): RNode { +export function unwrapRNode(value: RNode | LView | LContainer): RNode { while (Array.isArray(value)) { value = value[HOST] as any; } @@ -51,9 +51,9 @@ export function unwrapRNode(value: RNode | LView | LContainer | StylingContext): /** * Returns `LView` or `null` if not found. - * @param value wrapped value of `RNode`, `LView`, `LContainer`, `StylingContext` + * @param value wrapped value of `RNode`, `LView`, `LContainer` */ -export function unwrapLView(value: RNode | LView | LContainer | StylingContext): LView|null { +export function unwrapLView(value: RNode | LView | LContainer): LView|null { while (Array.isArray(value)) { // This check is same as `isLView()` but we don't call at as we don't want to call // `Array.isArray()` twice and give JITer more work for inlining. @@ -65,10 +65,9 @@ export function unwrapLView(value: RNode | LView | LContainer | StylingContext): /** * Returns `LContainer` or `null` if not found. - * @param value wrapped value of `RNode`, `LView`, `LContainer`, `StylingContext` + * @param value wrapped value of `RNode`, `LView`, `LContainer` */ -export function unwrapLContainer(value: RNode | LView | LContainer | StylingContext): LContainer| - null { +export function unwrapLContainer(value: RNode | LView | LContainer): LContainer|null { while (Array.isArray(value)) { // This check is same as `isLContainer()` but we don't call at as we don't want to call // `Array.isArray()` twice and give JITer more work for inlining. diff --git a/packages/core/src/util/ng_dev_mode.ts b/packages/core/src/util/ng_dev_mode.ts index f0424c44b3..505987072b 100644 --- a/packages/core/src/util/ng_dev_mode.ts +++ b/packages/core/src/util/ng_dev_mode.ts @@ -38,10 +38,15 @@ declare global { styleMapCacheMiss: number; classMap: number; classMapCacheMiss: number; - stylingProp: number; - stylingPropCacheMiss: number; - stylingApply: number; - stylingApplyCacheMiss: number; + styleProp: number; + stylePropCacheMiss: number; + classProp: number; + classPropCacheMiss: number; + flushStyling: number; + classesApplied: number; + stylesApplied: number; + stylingWritePersistedState: number; + stylingReadPersistedState: number; } } @@ -75,10 +80,15 @@ export function ngDevModeResetPerfCounters(): NgDevModePerfCounters { styleMapCacheMiss: 0, classMap: 0, classMapCacheMiss: 0, - stylingProp: 0, - stylingPropCacheMiss: 0, - stylingApply: 0, - stylingApplyCacheMiss: 0, + styleProp: 0, + stylePropCacheMiss: 0, + classProp: 0, + classPropCacheMiss: 0, + 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/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index dc4d4d68d0..351253ed86 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -1634,7 +1634,8 @@ describe('di', () => { expect(directive.otherAttr).toBe('value'); expect(directive.className).toBe('hello there'); - expect(directive.inlineStyles).toBe('margin: 1px; color: red;'); + expect(directive.inlineStyles).toMatch(/color:\s*red/); + expect(directive.inlineStyles).toMatch(/margin:\s*1px/); }); it('should not inject attributes with namespace', () => { diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts index d90726b5b9..1988f9d778 100644 --- a/packages/core/test/acceptance/i18n_spec.ts +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -1087,7 +1087,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) .toEqual( - `
traduction: un email
`); + `
traduction: un email
`); directiveInstances.forEach(instance => instance.klass = 'bar'); fixture.componentRef.instance.exp1 = 2; @@ -1095,7 +1095,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) .toEqual( - `
traduction: 2 emails
`); + `
traduction: 2 emails
`); }); it('should handle i18n attribute with directive inputs', () => { diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts index b4986714db..7b9356f3e1 100644 --- a/packages/core/test/acceptance/integration_spec.ts +++ b/packages/core/test/acceptance/integration_spec.ts @@ -1043,10 +1043,10 @@ describe('acceptance integration tests', () => { @Directive({selector: '[DirWithStyle]'}) class DirWithStyleDirective { - public stylesVal: string = ''; + public stylesVal: any = ''; @Input() - set style(value: string) { this.stylesVal = value; } + set style(value: any) { this.stylesVal = value; } } it('should delegate initial classes to a [class] input binding if present on a directive on the same element', @@ -1061,8 +1061,10 @@ describe('acceptance integration tests', () => { const fixture = TestBed.createComponent(App); fixture.detectChanges(); - expect(fixture.componentInstance.mockClassDirective.classesVal) - .toEqual('apple orange banana'); + // the initial values always get sorted in non VE code + // but there is no sorting guarantee within VE code + expect(fixture.componentInstance.mockClassDirective.classesVal.split(/\s+/).sort()) + .toEqual(['apple', 'banana', 'orange']); }); it('should delegate initial styles to a [style] input binding if present on a directive on the same element', @@ -1118,7 +1120,7 @@ describe('acceptance integration tests', () => { fixture.detectChanges(); expect(fixture.componentInstance.mockStyleDirective.stylesVal) - .toEqual('width:200px;height:500px'); + .toEqual({'width': '200px', 'height': '500px'}); }); onlyInIvy('Style binding merging works differently in Ivy') diff --git a/packages/core/test/acceptance/styling_next_spec.ts b/packages/core/test/acceptance/styling_next_spec.ts index 4c5b50cd1b..5125a8e984 100644 --- a/packages/core/test/acceptance/styling_next_spec.ts +++ b/packages/core/test/acceptance/styling_next_spec.ts @@ -5,13 +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 {CompilerStylingMode, compilerSetStylingMode} from '@angular/compiler/src/render3/view/styling_state'; import {Component, Directive, HostBinding, Input, ViewChild} from '@angular/core'; import {SecurityContext} from '@angular/core/src/core'; -import {getLContext} from '@angular/core/src/render3/context_discovery'; import {DebugNode, LViewDebug, toDebug} from '@angular/core/src/render3/instructions/lview_debug'; -import {SANITIZER} from '@angular/core/src/render3/interfaces/view'; -import {RuntimeStylingMode, runtimeSetStylingMode, setCurrentStyleSanitizer} from '@angular/core/src/render3/styling_next/state'; +import {getCheckNoChangesMode} from '@angular/core/src/render3/state'; import {loadLContextFromNode} from '@angular/core/src/render3/util/discovery_utils'; import {ngDevModeResetPerfCounters as resetStylingCounters} from '@angular/core/src/util/ng_dev_mode'; import {TestBed} from '@angular/core/testing'; @@ -20,16 +17,6 @@ import {expect} from '@angular/platform-browser/testing/src/matchers'; import {onlyInIvy} from '@angular/private/testing'; describe('new styling integration', () => { - beforeEach(() => { - runtimeSetStylingMode(RuntimeStylingMode.UseNew); - compilerSetStylingMode(CompilerStylingMode.UseNew); - }); - - afterEach(() => { - runtimeSetStylingMode(RuntimeStylingMode.UseOld); - compilerSetStylingMode(CompilerStylingMode.UseOld); - }); - 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', () => { @@ -579,6 +566,55 @@ describe('new styling integration', () => { 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({ @@ -686,38 +722,6 @@ describe('new styling integration', () => { }); }); - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should pick up and use the sanitizer present in the lView', () => { - @Component({ - template: ` -
- ` - }) - class Cmp { - w = '100px'; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - const lView = getLContext(element) !.lView; - lView[SANITIZER] = new MockSanitizer(value => { return `${value}-safe`; }); - - comp.w = '200px'; - fixture.detectChanges(); - - const node = getDebugNode(element) !; - const styles = node.styles !; - expect(styles.values['width']).toEqual('200px-safe'); - - // this is here so that it won't get picked up accidentally in another test - lView[SANITIZER] = null; - setCurrentStyleSanitizer(null); - }); - it('should be able to bind a SafeValue to clip-path', () => { @Component({template: '
'}) class Cmp { @@ -737,6 +741,321 @@ describe('new styling integration', () => { // that we use to run tests doesn't support `clip-path` in `CSSStyleDeclaration`. expect(html).toMatch(/style=["|']clip-path:\s*url\(.*#test.*\)/); }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should evaluate follow-up [style] maps even if a former map is null', () => { + @Directive({selector: '[dir-with-styling]'}) + class DirWithStyleMap { + @HostBinding('style') public styleMap: any = {color: 'red'}; + } + + @Directive({selector: '[dir-with-styling-part2]'}) + class DirWithStyleMapPart2 { + @HostBinding('style') public styleMap: any = {width: '200px'}; + } + + @Component({ + template: ` +
+ ` + }) + class Cmp { + map: any = null; + + @ViewChild('div', {read: DirWithStyleMap, static: true}) + dir1 !: DirWithStyleMap; + + @ViewChild('div', {read: DirWithStyleMapPart2, static: true}) + dir2 !: DirWithStyleMapPart2; + } + + TestBed.configureTestingModule( + {declarations: [Cmp, DirWithStyleMap, DirWithStyleMapPart2]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + const node = getDebugNode(element) !; + const styles = node.styles !; + + const values = styles.values; + const props = Object.keys(values).sort(); + expect(props).toEqual(['color', 'width']); + + expect(values['width']).toEqual('200px'); + expect(values['color']).toEqual('red'); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should evaluate initial style/class values on a list of elements that changes', () => { + @Component({ + template: ` +
+ {{ item }} +
+ ` + }) + class Cmp { + items = [1, 2, 3]; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + function getItemElements(): HTMLElement[] { + return [].slice.call(fixture.nativeElement.querySelectorAll('div')); + } + + function getItemClasses(): string[] { + return getItemElements().map(e => e.className).sort().join(' ').split(' '); + } + + expect(getItemElements().length).toEqual(3); + expect(getItemClasses()).toEqual([ + 'initial-class', + 'item-1', + 'initial-class', + 'item-2', + 'initial-class', + 'item-3', + ]); + + comp.items = [2, 4, 6, 8]; + fixture.detectChanges(); + + expect(getItemElements().length).toEqual(4); + expect(getItemClasses()).toEqual([ + 'initial-class', + 'item-2', + 'initial-class', + 'item-4', + 'initial-class', + 'item-6', + 'initial-class', + 'item-8', + ]); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should create and update multiple class bindings across multiple elements in a template', + () => { + @Component({ + template: ` +
header
+
+ {{ item }} +
+
footer
+ ` + }) + class Cmp { + items = [1, 2, 3]; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + function getItemElements(): HTMLElement[] { + return [].slice.call(fixture.nativeElement.querySelectorAll('div')); + } + + function getItemClasses(): string[] { + return getItemElements().map(e => e.className).sort().join(' ').split(' '); + } + + const header = fixture.nativeElement.querySelector('header'); + expect(header.classList.contains('header')); + + const footer = fixture.nativeElement.querySelector('footer'); + expect(footer.classList.contains('footer')); + + expect(getItemElements().length).toEqual(3); + expect(getItemClasses()).toEqual([ + 'item', + 'item-1', + 'item', + 'item-2', + 'item', + 'item-3', + ]); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should understand multiple directives which contain initial classes', () => { + @Directive({selector: 'dir-one'}) + class DirOne { + @HostBinding('class') public className = 'dir-one'; + } + + @Directive({selector: 'dir-two'}) + class DirTwo { + @HostBinding('class') public className = 'dir-two'; + } + + @Component({ + template: ` + +
+ + ` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + const dirOne = fixture.nativeElement.querySelector('dir-one'); + const div = fixture.nativeElement.querySelector('div'); + const dirTwo = fixture.nativeElement.querySelector('dir-two'); + + expect(dirOne.classList.contains('dir-one')).toBeTruthy(); + expect(dirTwo.classList.contains('dir-two')).toBeTruthy(); + expect(div.classList.contains('initial')).toBeTruthy(); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should evaluate styling across the template directives when there are multiple elements/sources of styling', + () => { + @Directive({selector: '[one]'}) + class DirOne { + @HostBinding('class') public className = 'dir-one'; + } + + @Directive({selector: '[two]'}) + class DirTwo { + @HostBinding('class') public className = 'dir-two'; + } + + @Component({ + template: ` +
+
+
+ ` + }) + class Cmp { + w = 100; + h = 200; + c = 'red'; + } + + TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const divA = fixture.nativeElement.querySelector('.a'); + const divB = fixture.nativeElement.querySelector('.b'); + const divC = fixture.nativeElement.querySelector('.c'); + + expect(divA.style.width).toEqual('100px'); + expect(divB.style.height).toEqual('200px'); + expect(divC.style.color).toEqual('red'); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should evaluate styling across the template and directives within embedded views', + () => { + @Directive({selector: '[some-dir-with-styling]'}) + class SomeDirWithStyling { + @HostBinding('style') + public styles = { + width: '200px', + height: '500px', + }; + } + + @Component({ + template: ` +
+ {{ item }} +
+
+

+ ` + }) + class Cmp { + items: any[] = []; + c = 'red'; + w = 100; + h = 100; + } + + TestBed.configureTestingModule({declarations: [Cmp, SomeDirWithStyling]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + comp.items = [1, 2, 3, 4]; + fixture.detectChanges(); + + const items = fixture.nativeElement.querySelectorAll('.item'); + expect(items.length).toEqual(4); + const [a, b, c, d] = items; + expect(a.style.height).toEqual('0px'); + expect(b.style.height).toEqual('100px'); + expect(c.style.height).toEqual('200px'); + expect(d.style.height).toEqual('300px'); + + const section = fixture.nativeElement.querySelector('section'); + const p = fixture.nativeElement.querySelector('p'); + + expect(section.style['width']).toEqual('100px'); + expect(p.style['height']).toEqual('100px'); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should flush bindings even if any styling hasn\'t changed in a previous directive', + () => { + @Directive({selector: '[one]'}) + class DirOne { + @HostBinding('style.width') w = '100px'; + @HostBinding('style.opacity') o = '0.5'; + } + + @Directive({selector: '[two]'}) + class DirTwo { + @HostBinding('style.height') h = '200px'; + @HostBinding('style.color') c = 'red'; + } + + @Component({template: '
'}) + class Cmp { + @ViewChild('target', {read: DirOne, static: true}) one !: DirOne; + @ViewChild('target', {read: DirTwo, static: true}) two !: DirTwo; + } + + TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(div.style.opacity).toEqual('0.5'); + expect(div.style.color).toEqual('red'); + expect(div.style.width).toEqual('100px'); + expect(div.style.height).toEqual('200px'); + + comp.two.h = '300px'; + fixture.detectChanges(); + expect(div.style.opacity).toEqual('0.5'); + expect(div.style.color).toEqual('red'); + expect(div.style.width).toEqual('100px'); + expect(div.style.height).toEqual('300px'); + }); }); function assertStyleCounters(countForSet: number, countForRemove: number) { @@ -760,8 +1079,3 @@ function getDebugNode(element: Node): DebugNode|null { } return null; } - -class MockSanitizer { - constructor(private _interceptorFn: ((value: any) => any)) {} - sanitize(context: SecurityContext, value: any): string|null { return this._interceptorFn(value); } -} diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index 6fd6d69156..40b3664634 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.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 {Component, Directive, ElementRef} from '@angular/core'; +import {Component, Directive, ElementRef, Input} from '@angular/core'; import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {TestBed} from '@angular/core/testing'; import {DomSanitizer, SafeStyle} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {onlyInIvy} from '@angular/private/testing'; +import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; describe('styling', () => { beforeEach(ngDevModeResetPerfCounters); @@ -153,10 +153,8 @@ describe('styling', () => { expect(div.style.backgroundImage).toBe('url("#test")'); onlyInIvy('perf counters').expectPerfCounters({ - stylingApply: 2, - stylingApplyCacheMiss: 1, - stylingProp: 2, - stylingPropCacheMiss: 1, + styleProp: 2, + stylePropCacheMiss: 1, tNode: 3, }); }); @@ -373,4 +371,187 @@ describe('styling', () => { expect(div.style.width).toBe('2667px'); }); + it('should not write to a `class` input binding in the event that there is no static class value', + () => { + let capturedClassBindingCount = 0; + let capturedClassBindingValue: string|null|undefined = undefined; + let capturedMyClassBindingCount = 0; + let capturedMyClassBindingValue: string|null|undefined = undefined; + + @Component({template: '
'}) + class Cmp { + c: any = null; + x = 'foo'; + } + + @Directive({selector: '[my-class-dir]'}) + class MyClassDir { + @Input('class') + set classVal(v: string) { + capturedClassBindingCount++; + capturedClassBindingValue = v; + } + + @Input('my-class-dir') + set myClassVal(v: string) { + capturedMyClassBindingCount++; + capturedMyClassBindingValue = v; + } + } + + TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + expect(capturedClassBindingCount).toEqual(1); + expect(capturedClassBindingValue as any).toEqual(null); + expect(capturedMyClassBindingCount).toEqual(1); + expect(capturedMyClassBindingValue !).toEqual('foo'); + + fixture.componentInstance.c = 'dynamic-value'; + fixture.detectChanges(); + + expect(capturedClassBindingCount).toEqual(2); + expect(capturedClassBindingValue !).toEqual('dynamic-value'); + expect(capturedMyClassBindingCount).toEqual(1); + expect(capturedMyClassBindingValue !).toEqual('foo'); + + fixture.componentInstance.c = null; + fixture.detectChanges(); + + expect(capturedClassBindingCount).toEqual(3); + expect(capturedClassBindingValue as any).toEqual(null); + expect(capturedMyClassBindingCount).toEqual(1); + expect(capturedMyClassBindingValue !).toEqual('foo'); + + fixture.componentInstance.c = ''; + fixture.detectChanges(); + + expect(capturedClassBindingCount).toEqual(4); + expect(capturedClassBindingValue as any).toEqual(''); + expect(capturedMyClassBindingCount).toEqual(1); + expect(capturedMyClassBindingValue !).toEqual('foo'); + }); + + it('should write to [class] binding during `update` mode if there is an instantiation-level value', + () => { + let capturedClassBindingCount = 0; + let capturedClassBindingValue: string|null|undefined = undefined; + + @Component({template: '
'}) + class Cmp { + c: any = 'bar'; + } + + @Directive({selector: '[my-class-dir]'}) + class MyClassDir { + @Input('class') + set classVal(v: string) { + capturedClassBindingCount++; + capturedClassBindingValue = v; + } + } + + // 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 + // be (Jira Issue = FW-1467). + let totalWrites = ivyEnabled ? 1 : 0; + + TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]}); + const fixture = TestBed.createComponent(Cmp); + expect(capturedClassBindingCount).toEqual(totalWrites++); + fixture.detectChanges(); + + expect(capturedClassBindingCount).toEqual(totalWrites++); + expect(capturedClassBindingValue as any).toEqual('bar'); + + fixture.componentInstance.c = 'dynamic-bar'; + fixture.detectChanges(); + + expect(capturedClassBindingCount).toEqual(totalWrites++); + expect(capturedClassBindingValue !).toEqual('dynamic-bar'); + }); + + it('should write to a `class` input binding if there is a static class value', () => { + let capturedClassBindingCount = 0; + let capturedClassBindingValue: string|null = null; + let capturedMyClassBindingCount = 0; + let capturedMyClassBindingValue: string|null = null; + + @Component({template: '
'}) + class Cmp { + x = 'foo'; + } + + @Directive({selector: '[my-class-dir]'}) + class MyClassDir { + @Input('class') + set classVal(v: string) { + capturedClassBindingCount++; + capturedClassBindingValue = v; + } + + @Input('my-class-dir') + set myClassVal(v: string) { + capturedMyClassBindingCount++; + capturedMyClassBindingValue = v; + } + } + + TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + expect(capturedClassBindingValue !).toEqual('static-val'); + expect(capturedClassBindingCount).toEqual(1); + expect(capturedMyClassBindingValue !).toEqual('foo'); + expect(capturedMyClassBindingCount).toEqual(1); + }); + + onlyInIvy('only ivy persists static class/style attrs with their binding counterparts') + .it('should write to a `class` input binding if there is a static class value and there is a binding value', + () => { + let capturedClassBindingCount = 0; + let capturedClassBindingValue: string|null = null; + let capturedMyClassBindingCount = 0; + let capturedMyClassBindingValue: string|null = null; + + @Component({template: '
'}) + class Cmp { + c: any = null; + x: any = 'foo'; + } + + @Directive({selector: '[my-class-dir]'}) + class MyClassDir { + @Input('class') + set classVal(v: string) { + capturedClassBindingCount++; + capturedClassBindingValue = v; + } + + @Input('my-class-dir') + set myClassVal(v: string) { + capturedMyClassBindingCount++; + capturedMyClassBindingValue = v; + } + } + + TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + expect(capturedClassBindingCount).toEqual(1); + expect(capturedClassBindingValue !).toEqual('static-val'); + expect(capturedMyClassBindingCount).toEqual(1); + expect(capturedMyClassBindingValue !).toEqual('foo'); + + fixture.componentInstance.c = 'dynamic-val'; + fixture.detectChanges(); + + expect(capturedClassBindingCount).toEqual(2); + expect(capturedClassBindingValue !).toEqual('static-val dynamic-val'); + expect(capturedMyClassBindingCount).toEqual(1); + expect(capturedMyClassBindingValue !).toEqual('foo'); + }); }); diff --git a/packages/core/test/bundling/animation_world/index.ts b/packages/core/test/bundling/animation_world/index.ts index 61ca860d1f..38cd84b9b1 100644 --- a/packages/core/test/bundling/animation_world/index.ts +++ b/packages/core/test/bundling/animation_world/index.ts @@ -66,10 +66,6 @@ class BoxWithOverriddenStylesComponent { @Component({ selector: 'animation-world', template: ` -
any)[]} = {}; - constructor(private _element: HTMLElement, private _animationName: string, time: string) { - this._animationStyle = `${time} ${_animationName}`; - } - private _start() { - (this._element as any).style.animation = this._animationStyle; - const animationFn = (event: AnimationEvent) => { - if (event.animationName == this._animationName) { - this._element.removeEventListener('animationend', animationFn); - this.finish(); - } - }; - this._element.addEventListener('animationend', animationFn); - } - addEventListener(state: PlayState|string, cb: () => any): void { - const key = state.toString(); - const arr = this._listeners[key] = (this._listeners[key] || []); - arr.push(cb); - } - play(): void { - if (this.state <= PlayState.Pending) { - this._start(); - } - if (this.state != PlayState.Running) { - setAnimationPlayState(this._element, 'running'); - this.state = PlayState.Running; - this._emit(this.state); - } - } - pause(): void { - if (this.state != PlayState.Paused) { - setAnimationPlayState(this._element, 'paused'); - this.state = PlayState.Paused; - this._emit(this.state); - } - } - finish(): void { - if (this.state < PlayState.Finished) { - this._element.style.animation = ''; - this.state = PlayState.Finished; - this._emit(this.state); - } - } - destroy(): void { - if (this.state < PlayState.Destroyed) { - this.finish(); - this.state = PlayState.Destroyed; - this._emit(this.state); - } - } - capture(): any {} - private _emit(state: PlayState) { - const arr = this._listeners[state.toString()] || []; - arr.forEach(cb => cb()); - } -} - -function setAnimationPlayState(element: HTMLElement, state: string) { - element.style.animationPlayState = state; -} - -class AnimationDebugger implements PlayerHandler { - private _players: Player[] = []; - - flushPlayers() { - this._players.forEach(player => { - if (!player.parent) { - player.play(); - } - }); - this._players.length = 0; - } - - queuePlayer(player: Player): void { this._players.push(player); } -} - -const playerHandler = new AnimationDebugger(); -renderComponent(AnimationWorldComponent, {playerHandler}); - -function animateStyleFactory(keyframes: any[], duration: number, easing: string) { - const limit = keyframes.length - 1; - const finalKeyframe = keyframes[limit]; - return bindPlayerFactory( - (element: HTMLElement, type: number, values: {[key: string]: any}, - isFirstRender: boolean) => { - const kf = keyframes.slice(0, limit); - kf.push(values); - return new WebAnimationsPlayer(element, keyframes, duration, easing); - }, - finalKeyframe); -} - -class WebAnimationsPlayer implements Player { - state = PlayState.Pending; - parent: Player|null = null; - private _listeners: {[stateName: string]: (() => any)[]} = {}; - constructor( - private _element: HTMLElement, private _keyframes: {[key: string]: any}[], - private _duration: number, private _easing: string) {} - private _start() { - const player = this._element.animate( - this._keyframes as any[], {duration: this._duration, easing: this._easing, fill: 'both'}); - player.addEventListener('finish', e => { this.finish(); }); - } - addEventListener(state: PlayState|string, cb: () => any): void { - const key = state.toString(); - const arr = this._listeners[key] = (this._listeners[key] || []); - arr.push(cb); - } - play(): void { - if (this.state <= PlayState.Pending) { - this._start(); - } - if (this.state != PlayState.Running) { - this.state = PlayState.Running; - this._emit(this.state); - } - } - pause(): void { - if (this.state != PlayState.Paused) { - this.state = PlayState.Paused; - this._emit(this.state); - } - } - finish(): void { - if (this.state < PlayState.Finished) { - this._element.style.animation = ''; - this.state = PlayState.Finished; - this._emit(this.state); - } - } - destroy(): void { - if (this.state < PlayState.Destroyed) { - this.finish(); - this.state = PlayState.Destroyed; - this._emit(this.state); - } - } - capture(): any {} - private _emit(state: PlayState) { - const arr = this._listeners[state.toString()] || []; - arr.forEach(cb => cb()); - } -} +renderComponent(AnimationWorldComponent); 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 c50407aa3b..955ef5aa0a 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -32,18 +32,6 @@ { "name": "DECLARATION_VIEW" }, - { - "name": "DEFAULT_BINDING_VALUE" - }, - { - "name": "DEFAULT_GUARD_MASK_VALUE" - }, - { - "name": "DEFAULT_SIZE_VALUE" - }, - { - "name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX" - }, { "name": "DepComponent" }, @@ -71,9 +59,6 @@ { "name": "INJECTOR_BLOOM_PARENT_SIZE" }, - { - "name": "MAP_BASED_ENTRY_PROP_NAME" - }, { "name": "MONKEY_PATCH_KEY_NAME" }, @@ -186,35 +171,17 @@ "name": "_selectedIndex" }, { - "name": "_stylingMode" - }, - { - "name": "addBindingIntoContext" + "name": "_stateStorage" }, { "name": "addComponentLogic" }, { - "name": "addOrUpdateStaticStyle" + "name": "addItemToStylingMap" }, { "name": "addToViewTree" }, - { - "name": "allocStylingContext" - }, - { - "name": "allocTStylingContext" - }, - { - "name": "allocateNewContextEntry" - }, - { - "name": "allocateOrUpdateDirectiveIntoContext" - }, - { - "name": "allowValueChange" - }, { "name": "appendChild" }, @@ -224,9 +191,6 @@ { "name": "attachPatchData" }, - { - "name": "attrsStylingIndexOf" - }, { "name": "baseResolveDirective" }, @@ -252,10 +216,10 @@ "name": "componentRefresh" }, { - "name": "createDirectivesAndLocals" + "name": "concatString" }, { - "name": "createEmptyStylingContext" + "name": "createDirectivesAndLocals" }, { "name": "createLView" @@ -347,9 +311,6 @@ { "name": "getCheckNoChangesMode" }, - { - "name": "getClassesContext" - }, { "name": "getClosureSafeProperty" }, @@ -362,18 +323,12 @@ { "name": "getContainerRenderParent" }, - { - "name": "getContext" - }, { "name": "getDirectiveDef" }, { "name": "getElementDepthCount" }, - { - "name": "getGuardMask" - }, { "name": "getHighestElementOrICUContainer" }, @@ -381,10 +336,7 @@ "name": "getHostNative" }, { - "name": "getInitialClassNameValue" - }, - { - "name": "getInitialStyleStringValue" + "name": "getInitialStylingValue" }, { "name": "getInjectorIndex" @@ -401,6 +353,12 @@ { "name": "getLViewParent" }, + { + "name": "getMapProp" + }, + { + "name": "getMapValue" + }, { "name": "getNameOnlyMarkerIndex" }, @@ -446,15 +404,6 @@ { "name": "getPreviousOrParentTNode" }, - { - "name": "getProp" - }, - { - "name": "getPropConfig" - }, - { - "name": "getPropValuesStartPosition" - }, { "name": "getRenderFlags" }, @@ -471,16 +420,7 @@ "name": "getSelectedIndex" }, { - "name": "getStylesContext" - }, - { - "name": "getStylingContextFromLView" - }, - { - "name": "getTNode" - }, - { - "name": "getValuesCount" + "name": "getStylingMapArray" }, { "name": "hasClassInput" @@ -506,9 +446,6 @@ { "name": "initNodeFlags" }, - { - "name": "initializeStaticContext" - }, { "name": "initializeTNodeInputs" }, @@ -575,6 +512,9 @@ { "name": "isStylingContext" }, + { + "name": "isStylingValueDefined" + }, { "name": "leaveView" }, @@ -605,12 +545,6 @@ { "name": "noSideEffects" }, - { - "name": "patchContextWithStaticAttrs" - }, - { - "name": "patchInitialStylingValue" - }, { "name": "postProcessBaseDirective" }, @@ -620,9 +554,6 @@ { "name": "queueComponentIndexForCheck" }, - { - "name": "readClassValueFromTNode" - }, { "name": "readPatchedData" }, @@ -642,10 +573,7 @@ "name": "refreshDynamicEmbeddedViews" }, { - "name": "registerBinding" - }, - { - "name": "registerInitialStylingIntoContext" + "name": "registerInitialStylingOnTNode" }, { "name": "registerPostOrderHooks" @@ -663,14 +591,17 @@ "name": "renderEmbeddedTemplate" }, { - "name": "renderInitialClasses" - }, - { - "name": "renderInitialStyles" + "name": "renderInitialStyling" }, { "name": "renderStringify" }, + { + "name": "renderStylingMap" + }, + { + "name": "resetAllStylingState" + }, { "name": "resetComponentState" }, @@ -678,10 +609,10 @@ "name": "resetPreOrderHookFlags" }, { - "name": "resolveDirectives" + "name": "resetStylingState" }, { - "name": "runtimeIsNewStylingInUse" + "name": "resolveDirectives" }, { "name": "saveNameToExportMap" @@ -698,9 +629,6 @@ { "name": "setBindingRoot" }, - { - "name": "setCachedStylingContext" - }, { "name": "setClass" }, @@ -711,7 +639,10 @@ "name": "setCurrentQueryIndex" }, { - "name": "setGuardMask" + "name": "setCurrentStyleSanitizer" + }, + { + "name": "setDirectiveStylingInput" }, { "name": "setHostBindings" @@ -732,7 +663,7 @@ "name": "setIsNotParent" }, { - "name": "setNodeStylingTemplate" + "name": "setMapValue" }, { "name": "setPreviousOrParentTNode" @@ -752,6 +683,9 @@ { "name": "stringifyForError" }, + { + "name": "stylingMapToString" + }, { "name": "syncViewWithBlueprint" }, 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 9f40289a54..fc4279505c 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -149,6 +149,9 @@ { "name": "_selectedIndex" }, + { + "name": "_stateStorage" + }, { "name": "addToViewTree" }, @@ -284,6 +287,12 @@ { "name": "getLViewParent" }, + { + "name": "getMapProp" + }, + { + "name": "getMapValue" + }, { "name": "getNativeAnchorNode" }, @@ -338,6 +347,9 @@ { "name": "getSelectedIndex" }, + { + "name": "getStylingMapArray" + }, { "name": "hasParentInjector" }, @@ -383,6 +395,9 @@ { "name": "isRootView" }, + { + "name": "isStylingContext" + }, { "name": "leaveView" }, @@ -444,20 +459,26 @@ "name": "renderEmbeddedTemplate" }, { - "name": "renderInitialClasses" - }, - { - "name": "renderInitialStyles" + "name": "renderInitialStyling" }, { "name": "renderStringify" }, + { + "name": "renderStylingMap" + }, + { + "name": "resetAllStylingState" + }, { "name": "resetComponentState" }, { "name": "resetPreOrderHookFlags" }, + { + "name": "resetStylingState" + }, { "name": "selectInternal" }, @@ -467,9 +488,6 @@ { "name": "setBindingRoot" }, - { - "name": "setCachedStylingContext" - }, { "name": "setClass" }, @@ -479,6 +497,9 @@ { "name": "setCurrentQueryIndex" }, + { + "name": "setCurrentStyleSanitizer" + }, { "name": "setHostBindings" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 15078290fe..37c9e928de 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -12,10 +12,10 @@ "name": "BINDING_INDEX" }, { - "name": "BLOOM_MASK" + "name": "BIT_MASK_START_VALUE" }, { - "name": "BoundPlayerFactory" + "name": "BLOOM_MASK" }, { "name": "CHILD_HEAD" @@ -38,12 +38,6 @@ { "name": "ChangeDetectionStrategy" }, - { - "name": "ClassAndStylePlayerBuilder" - }, - { - "name": "CorePlayerHandler" - }, { "name": "DECLARATION_VIEW" }, @@ -56,9 +50,6 @@ { "name": "DEFAULT_SIZE_VALUE" }, - { - "name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX" - }, { "name": "DefaultIterableDiffer" }, @@ -230,9 +221,6 @@ { "name": "SWITCH_VIEW_CONTAINER_REF_FACTORY" }, - { - "name": "SecurityContext" - }, { "name": "SkipSelf" }, @@ -374,9 +362,6 @@ { "name": "_c21" }, - { - "name": "_c22" - }, { "name": "_c3" }, @@ -417,11 +402,17 @@ "name": "_selectedIndex" }, { - "name": "_stylingMode" + "name": "_stateStorage" + }, + { + "name": "_stylingElement" }, { "name": "_stylingProp" }, + { + "name": "_stylingState" + }, { "name": "_symbolIterator" }, @@ -441,10 +432,7 @@ "name": "addComponentLogic" }, { - "name": "addOrUpdateStaticStyle" - }, - { - "name": "addPlayerInternal" + "name": "addItemToStylingMap" }, { "name": "addRemoveViewFromContainer" @@ -455,42 +443,21 @@ { "name": "addToViewTree" }, - { - "name": "allocPlayerContext" - }, - { - "name": "allocStylingContext" - }, { "name": "allocTStylingContext" }, { "name": "allocateNewContextEntry" }, - { - "name": "allocateOrUpdateDirectiveIntoContext" - }, - { - "name": "allowFlush" - }, { "name": "allowStylingFlush" }, - { - "name": "allowValueChange" - }, { "name": "appendChild" }, - { - "name": "applyClasses" - }, { "name": "applyOnCreateInstructions" }, - { - "name": "applyStyles" - }, { "name": "applyStyling" }, @@ -503,9 +470,6 @@ { "name": "attachPatchData" }, - { - "name": "attrsStylingIndexOf" - }, { "name": "baseResolveDirective" }, @@ -524,9 +488,6 @@ { "name": "bloomHashBitOrFactory" }, - { - "name": "booleanOrNull" - }, { "name": "cacheMatchingLocalNames" }, @@ -545,12 +506,6 @@ { "name": "checkView" }, - { - "name": "classProp" - }, - { - "name": "classesBitMask" - }, { "name": "cleanUpView" }, @@ -560,9 +515,15 @@ { "name": "componentRefresh" }, + { + "name": "concatString" + }, { "name": "containerInternal" }, + { + "name": "contextHasUpdates" + }, { "name": "contextLView" }, @@ -578,9 +539,6 @@ { "name": "createEmbeddedViewAndNode" }, - { - "name": "createEmptyStylingContext" - }, { "name": "createLContainer" }, @@ -617,12 +575,6 @@ { "name": "createViewBlueprint" }, - { - "name": "currentClassIndex" - }, - { - "name": "currentStyleIndex" - }, { "name": "decreaseElementDepthCount" }, @@ -635,9 +587,15 @@ { "name": "deferBindingRegistration" }, + { + "name": "deferStylingUpdate" + }, { "name": "deferredBindingQueue" }, + { + "name": "deleteStylingStateFromStorage" + }, { "name": "destroyLView" }, @@ -653,9 +611,6 @@ { "name": "diPublicInInjector" }, - { - "name": "directiveOwnerPointers" - }, { "name": "domRendererFactory3" }, @@ -665,9 +620,6 @@ { "name": "elementPropertyInternal" }, - { - "name": "enqueueHostInstruction" - }, { "name": "enterView" }, @@ -731,12 +683,6 @@ { "name": "findExistingListener" }, - { - "name": "findNextInsertionIndex" - }, - { - "name": "findOrPatchDirectiveIntoRegistry" - }, { "name": "findViaComponent" }, @@ -744,7 +690,7 @@ "name": "flushDeferredBindings" }, { - "name": "flushQueue" + "name": "flushStyling" }, { "name": "forwardRef" @@ -764,9 +710,6 @@ { "name": "getActiveDirectiveStylingIndex" }, - { - "name": "getActiveDirectiveStylingIndex" - }, { "name": "getActiveDirectiveSuperClassDepth" }, @@ -776,18 +719,12 @@ { "name": "getBeforeNodeForView" }, - { - "name": "getBindingNameFromIndex" - }, { "name": "getBindingValue" }, { "name": "getBindingsEnabled" }, - { - "name": "getCachedStylingContext" - }, { "name": "getCheckNoChangesMode" }, @@ -821,9 +758,6 @@ { "name": "getContextLView" }, - { - "name": "getCurrentOrLViewSanitizer" - }, { "name": "getCurrentStyleSanitizer" }, @@ -833,9 +767,6 @@ { "name": "getDirectiveDef" }, - { - "name": "getDirectiveIndexFromEntry" - }, { "name": "getElementDepthCount" }, @@ -852,19 +783,7 @@ "name": "getHostNative" }, { - "name": "getInitialClassNameValue" - }, - { - "name": "getInitialIndex" - }, - { - "name": "getInitialStyleStringValue" - }, - { - "name": "getInitialStylingValuesIndexOf" - }, - { - "name": "getInitialValue" + "name": "getInitialStylingValue" }, { "name": "getInjectableDef" @@ -885,13 +804,10 @@ "name": "getLViewParent" }, { - "name": "getMatchingBindingIndex" + "name": "getMapProp" }, { - "name": "getMultiOrSingleIndex" - }, - { - "name": "getMultiStylesStartIndex" + "name": "getMapValue" }, { "name": "getNameOnlyMarkerIndex" @@ -911,9 +827,6 @@ { "name": "getNativeByTNodeOrNull" }, - { - "name": "getNativeFromLView" - }, { "name": "getNodeInjectable" }, @@ -953,18 +866,6 @@ { "name": "getPipeDef" }, - { - "name": "getPlayerBuilder" - }, - { - "name": "getPlayerBuilderIndex" - }, - { - "name": "getPlayerContext" - }, - { - "name": "getPointers" - }, { "name": "getPreviousIndex" }, @@ -974,9 +875,6 @@ { "name": "getProp" }, - { - "name": "getProp" - }, { "name": "getPropConfig" }, @@ -1001,24 +899,18 @@ { "name": "getSelectedIndex" }, - { - "name": "getSinglePropIndexValue" - }, - { - "name": "getStyleSanitizer" - }, { "name": "getStylesContext" }, { - "name": "getStylingContext" - }, - { - "name": "getStylingContextFromLView" + "name": "getStylingMapArray" }, { "name": "getStylingMapsSyncFn" }, + { + "name": "getStylingState" + }, { "name": "getSymbolIterator" }, @@ -1034,9 +926,6 @@ { "name": "getTypeNameForDebugging" }, - { - "name": "getValue" - }, { "name": "getValuesCount" }, @@ -1052,9 +941,6 @@ { "name": "hasParentInjector" }, - { - "name": "hasPlayerBuilderChanged" - }, { "name": "hasStyleInput" }, @@ -1064,15 +950,6 @@ { "name": "hasValueChanged" }, - { - "name": "hasValueChanged" - }, - { - "name": "hyphenate" - }, - { - "name": "hyphenateEntries" - }, { "name": "includeViewProviders" }, @@ -1085,12 +962,6 @@ { "name": "initNodeFlags" }, - { - "name": "initStyling" - }, - { - "name": "initializeStaticContext" - }, { "name": "initializeTNodeInputs" }, @@ -1145,9 +1016,6 @@ { "name": "isContentQueryHost" }, - { - "name": "isContextDirty" - }, { "name": "isContextLocked" }, @@ -1163,9 +1031,6 @@ { "name": "isDifferent" }, - { - "name": "isDirty" - }, { "name": "isFactory" }, @@ -1226,6 +1091,9 @@ { "name": "locateHostElement" }, + { + "name": "lockAndFinalizeContext" + }, { "name": "lockContext" }, @@ -1238,6 +1106,9 @@ { "name": "makeParamDecorator" }, + { + "name": "markContextToPersistState" + }, { "name": "markDirty" }, @@ -1250,6 +1121,9 @@ { "name": "matchTemplateAttribute" }, + { + "name": "maybeApplyStyling" + }, { "name": "namespaceHTMLInternal" }, @@ -1286,30 +1160,15 @@ { "name": "normalizeBitMaskValue" }, - { - "name": "patchContextWithStaticAttrs" - }, - { - "name": "patchInitialStylingValue" - }, - { - "name": "pointers" - }, { "name": "postProcessBaseDirective" }, { "name": "postProcessDirective" }, - { - "name": "prepareInitialFlag" - }, { "name": "queueComponentIndexForCheck" }, - { - "name": "readClassValueFromTNode" - }, { "name": "readPatchedData" }, @@ -1332,13 +1191,7 @@ "name": "registerBinding" }, { - "name": "registerHostDirective" - }, - { - "name": "registerInitialStylingIntoContext" - }, - { - "name": "registerMultiMapEntry" + "name": "registerInitialStylingOnTNode" }, { "name": "registerPostOrderHooks" @@ -1365,16 +1218,16 @@ "name": "renderEmbeddedTemplate" }, { - "name": "renderInitialClasses" - }, - { - "name": "renderInitialStyles" + "name": "renderInitialStyling" }, { "name": "renderStringify" }, { - "name": "renderStyling" + "name": "renderStylingMap" + }, + { + "name": "resetAllStylingState" }, { "name": "resetComponentState" @@ -1382,21 +1235,15 @@ { "name": "resetPreOrderHookFlags" }, + { + "name": "resetStylingState" + }, { "name": "resolveDirectives" }, { "name": "resolveForwardRef" }, - { - "name": "runtimeAllowOldStyling" - }, - { - "name": "runtimeIsNewStylingInUse" - }, - { - "name": "sanitizeUsingSanitizerObject" - }, { "name": "saveNameToExportMap" }, @@ -1421,27 +1268,15 @@ { "name": "setBindingRoot" }, - { - "name": "setCachedStylingContext" - }, { "name": "setCheckNoChangesMode" }, { "name": "setClass" }, - { - "name": "setClass" - }, { "name": "setConfig" }, - { - "name": "setContextDirty" - }, - { - "name": "setContextPlayersDirty" - }, { "name": "setCurrentDirectiveDef" }, @@ -1452,10 +1287,7 @@ "name": "setCurrentStyleSanitizer" }, { - "name": "setDirty" - }, - { - "name": "setFlag" + "name": "setDirectiveStylingInput" }, { "name": "setGuardMask" @@ -1479,32 +1311,17 @@ "name": "setIsNotParent" }, { - "name": "setNodeStylingTemplate" - }, - { - "name": "setPlayerBuilder" - }, - { - "name": "setPlayerBuilderIndex" + "name": "setMapValue" }, { "name": "setPreviousOrParentTNode" }, - { - "name": "setProp" - }, - { - "name": "setSanitizeFlag" - }, { "name": "setSelectedIndex" }, { "name": "setStyle" }, - { - "name": "setStyle" - }, { "name": "setTNodeAndViewData" }, @@ -1512,10 +1329,10 @@ "name": "setUpAttributes" }, { - "name": "setValue" + "name": "shouldSearchParent" }, { - "name": "shouldSearchParent" + "name": "stateIsPersisted" }, { "name": "storeBindingMetadata" @@ -1523,6 +1340,9 @@ { "name": "storeCleanupFn" }, + { + "name": "storeStylingState" + }, { "name": "stringify" }, @@ -1530,16 +1350,7 @@ "name": "stringifyForError" }, { - "name": "stylesBitMask" - }, - { - "name": "stylingApply" - }, - { - "name": "stylingContext" - }, - { - "name": "stylingInit" + "name": "stylingMapToString" }, { "name": "syncViewWithBlueprint" @@ -1566,26 +1377,17 @@ "name": "updateClassBinding" }, { - "name": "updateClassProp" - }, - { - "name": "updateContextDirectiveIndex" - }, - { - "name": "updateContextWithBindings" + "name": "updateInitialStylingOnContext" }, { "name": "updateLastDirectiveIndex" }, { - "name": "updateSingleStylingValue" + "name": "updateLastDirectiveIndex" }, { "name": "updateStyleBinding" }, - { - "name": "valueExists" - }, { "name": "viewAttachedToChangeDetector" }, diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index 11ee5c876c..aed1eaf4eb 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -7,13 +7,13 @@ */ import {NgForOfContext} from '@angular/common'; + import {ɵɵdefineComponent} from '../../src/render3/definition'; -import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index'; +import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index'; import {AttributeMarker} from '../../src/render3/interfaces/node'; import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl} from '../../src/sanitization/bypass'; import {ɵɵdefaultStyleSanitizer, ɵɵsanitizeHtml, ɵɵsanitizeResourceUrl, ɵɵsanitizeScript, ɵɵsanitizeStyle, ɵɵsanitizeUrl} from '../../src/sanitization/sanitization'; import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; -import {StyleSanitizeFn} from '../../src/sanitization/style_sanitizer'; import {NgForOf} from './common_with_def'; import {ComponentFixture, TemplateFixture} from './render_util'; @@ -25,10 +25,7 @@ describe('instructions', () => { ɵɵelementEnd(); } - function createDiv( - initialClasses?: string[] | null, classBindingNames?: string[] | null, - initialStyles?: string[] | null, styleBindingNames?: string[] | null, - styleSanitizer?: StyleSanitizeFn) { + function createDiv(initialClasses?: string[] | null, initialStyles?: string[] | null) { const attrs: any[] = []; if (initialClasses) { attrs.push(AttributeMarker.Classes, ...initialClasses); @@ -37,7 +34,7 @@ describe('instructions', () => { attrs.push(AttributeMarker.Styles, ...initialStyles); } ɵɵelementStart(0, 'div', attrs); - ɵɵstyling(classBindingNames || null, styleBindingNames || null, styleSanitizer); + ɵɵstyling(); ɵɵelementEnd(); } @@ -56,16 +53,10 @@ describe('instructions', () => { it('should update bindings when value changes with the correct perf counters', () => { const t = new TemplateFixture(createAnchor, () => {}, 1, 1); - t.update(() => { - ɵɵselect(0); - ɵɵproperty('title', 'Hello'); - }); + t.update(() => { ɵɵproperty('title', 'Hello'); }); expect(t.html).toEqual(''); - t.update(() => { - ɵɵselect(0); - ɵɵproperty('title', 'World'); - }); + t.update(() => { ɵɵproperty('title', 'World'); }); expect(t.html).toEqual(''); expect(ngDevMode).toHaveProperties({ firstTemplatePass: 1, @@ -78,10 +69,7 @@ describe('instructions', () => { it('should not update bindings when value does not change, with the correct perf counters', () => { - const idempotentUpdate = () => { - ɵɵselect(0); - ɵɵproperty('title', 'Hello'); - }; + const idempotentUpdate = () => { ɵɵproperty('title', 'Hello'); }; const t = new TemplateFixture(createAnchor, idempotentUpdate, 1, 1); t.update(); @@ -121,14 +109,10 @@ describe('instructions', () => { it('should use sanitizer function', () => { const t = new TemplateFixture(createDiv, () => {}, 1, 1); - t.update(() => { - ɵɵselect(0); - ɵɵattribute('title', 'javascript:true', ɵɵsanitizeUrl); - }); + t.update(() => { ɵɵattribute('title', 'javascript:true', ɵɵsanitizeUrl); }); expect(t.html).toEqual('
'); t.update(() => { - ɵɵselect(0); ɵɵattribute('title', bypassSanitizationTrustUrl('javascript:true'), ɵɵsanitizeUrl); }); expect(t.html).toEqual('
'); @@ -167,18 +151,17 @@ describe('instructions', () => { describe('styleProp', () => { it('should automatically sanitize unless a bypass operation is applied', () => { - const t = new TemplateFixture(() => { - return createDiv(null, null, null, ['background-image'], ɵɵdefaultStyleSanitizer); - }, () => {}, 1); + const t = new TemplateFixture(() => { return createDiv(); }, () => {}, 1); t.update(() => { - ɵɵstyleProp(0, 'url("http://server")'); + ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer); + ɵɵstyleProp('background-image', 'url("http://server")'); ɵɵstylingApply(); }); // nothing is set because sanitizer suppresses it. expect(t.html).toEqual('
'); t.update(() => { - ɵɵstyleProp(0, bypassSanitizationTrustStyle('url("http://server2")')); + ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('url("http://server2")')); ɵɵstylingApply(); }); expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image')) @@ -187,13 +170,11 @@ describe('instructions', () => { it('should not re-apply the style value even if it is a newly bypassed again', () => { const sanitizerInterceptor = new MockSanitizerInterceptor(); - const t = createTemplateFixtureWithSanitizer( - () => createDiv( - null, null, null, ['background-image'], sanitizerInterceptor.getStyleSanitizer()), - 1, sanitizerInterceptor); + const t = createTemplateFixtureWithSanitizer(() => createDiv(), 1, sanitizerInterceptor); t.update(() => { - ɵɵstyleProp(0, bypassSanitizationTrustStyle('apple')); + ɵɵstyleSanitizer(sanitizerInterceptor.getStyleSanitizer()); + ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('apple')); ɵɵstylingApply(); }); @@ -201,7 +182,8 @@ describe('instructions', () => { sanitizerInterceptor.lastValue = null; t.update(() => { - ɵɵstyleProp(0, bypassSanitizationTrustStyle('apple')); + ɵɵstyleSanitizer(sanitizerInterceptor.getStyleSanitizer()); + ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('apple')); ɵɵstylingApply(); }); expect(sanitizerInterceptor.lastValue).toEqual(null); @@ -211,7 +193,7 @@ describe('instructions', () => { describe('styleMap', () => { function createDivWithStyle() { ɵɵelementStart(0, 'div', [AttributeMarker.Styles, 'height', '10px']); - ɵɵstyling([], ['height']); + ɵɵstyling(); ɵɵelementEnd(); } @@ -228,11 +210,11 @@ describe('instructions', () => { const detectedValues: string[] = []; const sanitizerInterceptor = new MockSanitizerInterceptor(value => { detectedValues.push(value); }); - const fixture = createTemplateFixtureWithSanitizer( - () => createDiv(null, null, null, null, sanitizerInterceptor.getStyleSanitizer()), 1, - sanitizerInterceptor); + const fixture = + createTemplateFixtureWithSanitizer(() => createDiv(), 1, sanitizerInterceptor); fixture.update(() => { + ɵɵstyleSanitizer(sanitizerInterceptor.getStyleSanitizer()); ɵɵstyleMap({ 'background-image': 'background-image', 'background': 'background', @@ -265,7 +247,7 @@ describe('instructions', () => { ɵɵclassMap('multiple classes'); ɵɵstylingApply(); }); - expect(fixture.html).toEqual('
'); + expect(fixture.html).toEqual('
'); }); }); @@ -320,7 +302,6 @@ describe('instructions', () => { ɵɵtemplate(0, ToDoAppComponent_NgForOf_Template_0, 2, 1, 'ul', _c0); } if (rf & RenderFlags.Update) { - ɵɵselect(0); ɵɵproperty('ngForOf', ctx.rows); } }, @@ -343,10 +324,7 @@ describe('instructions', () => { const inputValue = 'http://foo'; const outputValue = 'http://foo-sanitized'; - t.update(() => { - ɵɵselect(0); - ɵɵattribute('href', inputValue, ɵɵsanitizeUrl); - }); + t.update(() => { ɵɵattribute('href', inputValue, ɵɵsanitizeUrl); }); expect(t.html).toEqual(``); expect(s.lastSanitizedValue).toEqual(outputValue); }); @@ -357,10 +335,7 @@ describe('instructions', () => { const inputValue = s.bypassSecurityTrustUrl('http://foo'); const outputValue = 'http://foo'; - t.update(() => { - ɵɵselect(0); - ɵɵattribute('href', inputValue, ɵɵsanitizeUrl); - }); + t.update(() => { ɵɵattribute('href', inputValue, ɵɵsanitizeUrl); }); expect(t.html).toEqual(``); expect(s.lastSanitizedValue).toBeFalsy(); }); @@ -371,10 +346,7 @@ describe('instructions', () => { const inputValue = bypassSanitizationTrustUrl('http://foo'); const outputValue = 'http://foo-ivy'; - t.update(() => { - ɵɵselect(0); - ɵɵattribute('href', inputValue, ɵɵsanitizeUrl); - }); + t.update(() => { ɵɵattribute('href', inputValue, ɵɵsanitizeUrl); }); expect(t.html).toEqual(``); expect(s.lastSanitizedValue).toBeFalsy(); }); @@ -385,10 +357,7 @@ describe('instructions', () => { const inputValue = 'color:red'; const outputValue = 'color:blue'; - t.update(() => { - ɵɵselect(0); - ɵɵattribute('style', inputValue, ɵɵsanitizeStyle); - }); + t.update(() => { ɵɵattribute('style', inputValue, ɵɵsanitizeStyle); }); expect(stripStyleWsCharacters(t.html)).toEqual(`
`); expect(s.lastSanitizedValue).toEqual(outputValue); }); @@ -399,10 +368,7 @@ describe('instructions', () => { const inputValue = s.bypassSecurityTrustStyle('color:maroon'); const outputValue = 'color:maroon'; - t.update(() => { - ɵɵselect(0); - ɵɵattribute('style', inputValue, ɵɵsanitizeStyle); - }); + t.update(() => { ɵɵattribute('style', inputValue, ɵɵsanitizeStyle); }); expect(stripStyleWsCharacters(t.html)).toEqual(`
`); expect(s.lastSanitizedValue).toBeFalsy(); }); @@ -413,10 +379,7 @@ describe('instructions', () => { const inputValue = bypassSanitizationTrustStyle('font-family:foo'); const outputValue = 'font-family:foo-ivy'; - t.update(() => { - ɵɵselect(0); - ɵɵattribute('style', inputValue, ɵɵsanitizeStyle); - }); + t.update(() => { ɵɵattribute('style', inputValue, ɵɵsanitizeStyle); }); expect(stripStyleWsCharacters(t.html)).toEqual(`
`); expect(s.lastSanitizedValue).toBeFalsy(); }); @@ -427,10 +390,7 @@ describe('instructions', () => { const inputValue = 'http://resource'; const outputValue = 'http://resource-sanitized'; - t.update(() => { - ɵɵselect(0); - ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl); - }); + t.update(() => { ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl); }); expect(t.html).toEqual(``); expect(s.lastSanitizedValue).toEqual(outputValue); }); @@ -441,10 +401,7 @@ describe('instructions', () => { const inputValue = s.bypassSecurityTrustResourceUrl('file://all-my-secrets.pdf'); const outputValue = 'file://all-my-secrets.pdf'; - t.update(() => { - ɵɵselect(0); - ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl); - }); + t.update(() => { ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl); }); expect(t.html).toEqual(``); expect(s.lastSanitizedValue).toBeFalsy(); }); @@ -455,10 +412,7 @@ describe('instructions', () => { const inputValue = bypassSanitizationTrustResourceUrl('file://all-my-secrets.pdf'); const outputValue = 'file://all-my-secrets.pdf-ivy'; - t.update(() => { - ɵɵselect(0); - ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl); - }); + t.update(() => { ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl); }); expect(t.html).toEqual(``); expect(s.lastSanitizedValue).toBeFalsy(); }); @@ -469,10 +423,7 @@ describe('instructions', () => { const inputValue = 'fn();'; const outputValue = 'fn(); //sanitized'; - t.update(() => { - ɵɵselect(0); - ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript); - }); + t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript); }); expect(t.html).toEqual(``); expect(s.lastSanitizedValue).toEqual(outputValue); }); @@ -483,10 +434,7 @@ describe('instructions', () => { const inputValue = s.bypassSecurityTrustScript('alert("bar")'); const outputValue = 'alert("bar")'; - t.update(() => { - ɵɵselect(0); - ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript); - }); + t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript); }); expect(t.html).toEqual(``); expect(s.lastSanitizedValue).toBeFalsy(); }); @@ -497,10 +445,7 @@ describe('instructions', () => { const inputValue = bypassSanitizationTrustScript('alert("bar")'); const outputValue = 'alert("bar")-ivy'; - t.update(() => { - ɵɵselect(0); - ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript); - }); + t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript); }); expect(t.html).toEqual(``); expect(s.lastSanitizedValue).toBeFalsy(); }); @@ -511,10 +456,7 @@ describe('instructions', () => { const inputValue = '
'; const outputValue = '
'; - t.update(() => { - ɵɵselect(0); - ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml); - }); + t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml); }); expect(t.html).toEqual(`
${outputValue}
`); expect(s.lastSanitizedValue).toEqual(outputValue); }); @@ -525,10 +467,7 @@ describe('instructions', () => { const inputValue = s.bypassSecurityTrustHtml('
'); const outputValue = '
'; - t.update(() => { - ɵɵselect(0); - ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml); - }); + t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml); }); expect(t.html).toEqual(`
${outputValue}
`); expect(s.lastSanitizedValue).toBeFalsy(); }); @@ -539,10 +478,7 @@ describe('instructions', () => { const inputValue = bypassSanitizationTrustHtml('
'); const outputValue = '
-ivy'; - t.update(() => { - ɵɵselect(0); - ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml); - }); + t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml); }); expect(t.html).toEqual(`
${outputValue}
`); expect(s.lastSanitizedValue).toBeFalsy(); }); diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 87e02ba27f..c87f1eca92 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -633,7 +633,7 @@ describe('element discovery', () => { template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { ɵɵelementStart(0, 'section'); - ɵɵstyling(['class-foo']); + ɵɵstyling(); ɵɵelementEnd(); } if (rf & RenderFlags.Update) { @@ -651,8 +651,7 @@ describe('element discovery', () => { expect(Array.isArray(result1)).toBeTruthy(); const elementResult = result1[HEADER_OFFSET]; // first element - expect(Array.isArray(elementResult)).toBeTruthy(); - expect(elementResult[StylingIndex.ElementPosition]).toBe(section); + expect(elementResult).toBe(section); const context = getLContext(section) !; const result2 = section[MONKEY_PATCH_KEY_NAME]; diff --git a/packages/core/test/render3/node_selector_matcher_spec.ts b/packages/core/test/render3/node_selector_matcher_spec.ts index 204e596a9f..dbd6e3cdf3 100644 --- a/packages/core/test/render3/node_selector_matcher_spec.ts +++ b/packages/core/test/render3/node_selector_matcher_spec.ts @@ -337,11 +337,11 @@ describe('css selector matching', () => { //
(with attrs but without styling context) tNode.attrs = ['class', 'abc']; - tNode.stylingTemplate = null; + tNode.classes = null; expect(isMatching('div', tNode, selector)).toBeTruthy(); //
(with styling context but without attrs) - tNode.stylingTemplate = initializeStaticContext([AttributeMarker.Classes, 'abc'], 0); + tNode.classes = ['abc', 'abc', true]; tNode.attrs = null; expect(isMatching('div', tNode, selector)).toBeTruthy(); }); diff --git a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts index 1c12ea5e6b..6dac9b8bdd 100644 --- a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts +++ b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts @@ -378,122 +378,6 @@ describe('style and class based bindings', () => { }); }); - describe('instructions', () => { - it('should handle a combination of initial, multi and singular style values (in that order)', - () => { - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span', [ - AttributeMarker.Styles, - 'width', - '200px', - 'height', - '100px', - 'opacity', - '0.5', - ]); - ɵɵstyling(null, ['width']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵstyleMap(ctx.myStyles); - ɵɵstyleProp(0, ctx.myWidth); - ɵɵstylingApply(); - } - } - - expect(renderToHtml( - Template, {myStyles: {width: '200px', height: '200px'}, myWidth: '300px'}, 1)) - .toEqual(''); - - expect( - renderToHtml(Template, {myStyles: {width: '200px', height: null}, myWidth: null}, 1)) - .toEqual(''); - }); - - it('should support styles on SVG elements', () => { - // - // - // - class Comp { - diameter: number = 100; - - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵnamespaceSVG(); - ɵɵelementStart(0, 'svg'); - ɵɵstyling(null, ['width', 'height']); - ɵɵelementStart(1, 'circle', ['stroke', 'green', 'fill', 'yellow']); - ɵɵelementEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵstyleProp(0, ctx.diameter, 'px'); - ɵɵstyleProp(1, ctx.diameter, 'px'); - ɵɵstylingApply(); - } - } - }); - } - - const fixture = new ComponentFixture(Comp); - fixture.update(); - - const target = fixture.hostElement.querySelector('svg') !as any; - expect(target.style.width).toEqual('100px'); - expect(target.style.height).toEqual('100px'); - - expect(fixture.html) - .toEqual( - ''); - }); - - it('should support binding to camelCased and hyphenated style properties', () => { - //
- class Comp { - borderWidth: string = '3px'; - borderColor: string = 'red'; - - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - ɵɵstyling(null, ['borderWidth', 'border-color']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵstyleProp(0, ctx.borderWidth); - ɵɵstyleProp(1, ctx.borderColor); - ɵɵstylingApply(); - } - } - }); - } - - const fixture = new ComponentFixture(Comp); - fixture.update(); - - const target = fixture.hostElement.querySelector('div') !as any; - - expect(target.style.borderWidth).toEqual('3px'); - expect(target.style.borderColor).toEqual('red'); - expect(fixture.html).toContain('border-width: 3px'); - expect(fixture.html).toContain('border-color: red'); - }); - - }); - describe('dynamic styling properties within a styling context', () => { it('should initialize a context with a series of styling bindings as well as single property offsets', () => { @@ -2595,438 +2479,7 @@ describe('style and class based bindings', () => { ]); }); - it('should destroy an existing player that was queued before it is flushed once the binding updates', - () => { - const context = createStylingContext(null, ['width']); - const handler = new CorePlayerHandler(); - const lView = createMockViewData(handler, context); - - const players: MockPlayer[] = []; - const buildFn = - (element: HTMLElement, type: BindingType, value: any, firstRender: boolean, - oldPlayer: Player | null) => { - const player = new MockPlayer(value); - players.push(player); - return player; - }; - - expect(context[StylingIndex.PlayerContext]).toEqual(null); - - let mapFactory = bindPlayerFactory(buildFn, {width: '200px'}); - updateStyleAndClassMaps(context, null, mapFactory); - renderStyles(context, false, undefined, lView); - - expect(players.length).toEqual(1); - const p1 = players.pop() !; - expect(p1.state).toEqual(PlayState.Pending); - - mapFactory = bindPlayerFactory(buildFn, {width: '100px'}); - updateStyleAndClassMaps(context, null, mapFactory); - renderStyles(context, false, undefined, lView); - - expect(players.length).toEqual(1); - const p2 = players.pop() !; - expect(p1.state).toEqual(PlayState.Destroyed); - expect(p2.state).toEqual(PlayState.Pending); - }); - - it('should nullify style map and style property factories if any follow up expressions not use them', - () => { - const context = createStylingContext(null, ['color'], null, ['foo']); - const handler = new CorePlayerHandler(); - const lView = createMockViewData(handler, context); - - const stylePlayers: Player[] = []; - const buildStyleFn = (element: HTMLElement, type: BindingType, value: any) => { - const player = new MockPlayer(); - stylePlayers.push(player); - return player; - }; - - const classPlayers: Player[] = []; - const buildClassFn = (element: HTMLElement, type: BindingType, value: any) => { - const player = new MockPlayer(); - classPlayers.push(player); - return player; - }; - - assertContext(context, [ - element, // - masterConfig(18, false), // - [2, null], // - [null, null, 'color', null, 0], // - [null, null, 'foo', false, 0], // - [1, 1, 1, 1, 10, 14], // - [1, 0, 22, null, 1], // - [1, 0, 18, null, 1], // - null, // - null, // - - // #10 - cleanStyle(3, 18), - 'color', - null, - 0, - - // #14 - cleanClass(3, 22), - 'foo', - null, - 0, - - // #18 - cleanStyle(3, 10), - 'color', - null, - 0, - - // #22 - cleanClass(3, 14), - 'foo', - null, - 0, - ]); - - const cachedClassMap = {map: true}; - const cachedStyleMap = {opacity: '1'}; - const styleMapWithPlayerFactory = bindPlayerFactory(buildStyleFn, cachedStyleMap); - const classMapWithPlayerFactory = bindPlayerFactory(buildClassFn, cachedClassMap); - const styleMapPlayerBuilder = makePlayerBuilder(styleMapWithPlayerFactory, false); - const classMapPlayerBuilder = makePlayerBuilder(classMapWithPlayerFactory, true); - updateStyleAndClassMaps(context, classMapWithPlayerFactory, styleMapWithPlayerFactory); - - const colorWithPlayerFactory = bindPlayerFactory(buildStyleFn, 'red'); - const fooWithPlayerFactory = bindPlayerFactory(buildClassFn, true); - const colorPlayerBuilder = makePlayerBuilder(colorWithPlayerFactory, false); - const fooPlayerBuilder = makePlayerBuilder(fooWithPlayerFactory, true); - updateStyleProp(context, 0, colorWithPlayerFactory as any); - updateClassProp(context, 0, fooWithPlayerFactory as any); - renderStyles(context, false, undefined, lView); - - const p1 = classPlayers.shift(); - const p2 = stylePlayers.shift(); - const p3 = stylePlayers.shift(); - const p4 = classPlayers.shift(); - - let playerContext = context[StylingIndex.PlayerContext] !; - expect(playerContext).toEqual([ - 9, classMapPlayerBuilder, p1, styleMapPlayerBuilder, p2, colorPlayerBuilder, p3, - fooPlayerBuilder, p4 - ] as PlayerContext); - - assertContext(context, [ - element, // - masterConfig(18, false), // - [2, null], // - [null, null, 'color', null, 0], // - [null, null, 'foo', false, 0], // - [1, 1, 1, 1, 10, 14], // - [1, 0, 26, classMapWithPlayerFactory, 1], // - [1, 0, 18, styleMapWithPlayerFactory, 1], // - null, // - playerContext, - - // #10 - cleanStyle(3, 22), - 'color', - 'red', - directiveOwnerPointers(0, 5), - - // #14 - cleanClass(3, 30), - 'foo', - true, - directiveOwnerPointers(0, 7), - - // #18 - cleanStyle(0, 0), - 'opacity', - '1', - directiveOwnerPointers(0, 3), - - // #22 - cleanStyle(3, 10), - 'color', - null, - 0, - - // #26 - cleanClass(0, 0), - 'map', - true, - directiveOwnerPointers(0, 1), - - // #30 - cleanClass(3, 14), - 'foo', - null, - 0, - ]); - - updateStyleAndClassMaps(context, cachedClassMap, cachedStyleMap); - - const colorWithoutPlayerFactory = 'blue'; - const fooWithoutPlayerFactory = false; - updateStyleProp(context, 0, colorWithoutPlayerFactory); - updateClassProp(context, 0, fooWithoutPlayerFactory); - renderStyles(context, false, undefined, lView); - - playerContext = context[StylingIndex.PlayerContext] !; - expect(playerContext).toEqual([ - 9, null, null, null, null, null, null, null, null - ] as PlayerContext); - - assertContext(context, [ - element, // - masterConfig(18, false), // - [2, null], // - [null, null, 'color', null, 0], // - [null, null, 'foo', false, 0], // - [1, 1, 1, 1, 10, 14], // - [1, 0, 26, cachedClassMap, 1], // - [1, 0, 18, cachedStyleMap, 1], // - null, // - playerContext, - - // #10 - cleanStyle(3, 22), - 'color', - 'blue', - 0, - - // #14 - cleanClass(3, 30), - 'foo', - false, - 0, - - // #18 - cleanStyle(0, 0), - 'opacity', - '1', - 0, - - // #22 - cleanStyle(3, 10), - 'color', - null, - 0, - - // #26 - cleanClass(0, 0), - 'map', - true, - 0, - - // #30 - cleanClass(3, 14), - 'foo', - null, - 0, - ]); - }); - - it('should not call a factory if no style and/or class values have been updated', () => { - const context = createStylingContext([]); - const handler = new CorePlayerHandler(); - const lView = createMockViewData(handler, context); - - let styleCalls = 0; - const buildStyleFn = (element: HTMLElement, type: BindingType, value: any) => { - styleCalls++; - return new MockPlayer(); - }; - - let classCalls = 0; - const buildClassFn = (element: HTMLElement, type: BindingType, value: any) => { - classCalls++; - return new MockPlayer(); - }; - - let styleFactory = bindPlayerFactory(buildStyleFn, {opacity: '1'}) as BoundPlayerFactory; - let classFactory = bindPlayerFactory(buildClassFn, 'bar') as BoundPlayerFactory; - updateStyleAndClassMaps(context, classFactory, styleFactory); - expect(styleCalls).toEqual(0); - expect(classCalls).toEqual(0); - - renderStyles(context, false, undefined, lView); - expect(styleCalls).toEqual(1); - expect(classCalls).toEqual(1); - - renderStyles(context, false, undefined, lView); - expect(styleCalls).toEqual(1); - expect(classCalls).toEqual(1); - - styleFactory = bindPlayerFactory(buildStyleFn, {opacity: '0.5'}) as BoundPlayerFactory; - updateStyleAndClassMaps(context, classFactory, styleFactory); - renderStyles(context, false, undefined, lView); - expect(styleCalls).toEqual(2); - expect(classCalls).toEqual(1); - - classFactory = bindPlayerFactory(buildClassFn, 'foo') as BoundPlayerFactory; - updateStyleAndClassMaps(context, classFactory, styleFactory); - renderStyles(context, false, undefined, lView); - expect(styleCalls).toEqual(2); - expect(classCalls).toEqual(2); - - updateStyleAndClassMaps(context, 'foo', {opacity: '0.5'}); - renderStyles(context, false, undefined, lView); - expect(styleCalls).toEqual(2); - expect(classCalls).toEqual(2); - }); - - it('should invoke a single prop player over a multi style player when present and delegate back if not', - () => { - const context = createStylingContext(null, ['color']); - const handler = new CorePlayerHandler(); - const lView = createMockViewData(handler, context); - - let propPlayer: Player|null = null; - const propBuildFn = (element: HTMLElement, type: BindingType, value: any) => { - return propPlayer = new MockPlayer(); - }; - - let styleMapPlayer: Player|null = null; - const mapBuildFn = (element: HTMLElement, type: BindingType, value: any) => { - return styleMapPlayer = new MockPlayer(); - }; - - const mapFactory = bindPlayerFactory(mapBuildFn, {color: 'black'}); - updateStyleAndClassMaps(context, null, mapFactory); - updateStyleProp(context, 0, 'green'); - renderStyles(context, false, undefined, lView); - - expect(propPlayer).toBeFalsy(); - expect(styleMapPlayer).toBeFalsy(); - - const propFactory = bindPlayerFactory(propBuildFn, 'orange'); - updateStyleProp(context, 0, propFactory as any); - renderStyles(context, false, undefined, lView); - - expect(propPlayer).toBeTruthy(); - expect(styleMapPlayer).toBeFalsy(); - - propPlayer = styleMapPlayer = null; - - updateStyleProp(context, 0, null); - renderStyles(context, false, undefined, lView); - - expect(propPlayer).toBeFalsy(); - expect(styleMapPlayer).toBeTruthy(); - - propPlayer = styleMapPlayer = null; - - updateStyleAndClassMaps(context, null, null); - renderStyles(context, false, undefined, lView); - - expect(propPlayer).toBeFalsy(); - expect(styleMapPlayer).toBeFalsy(); - }); - - it('should return the old player for styles when a follow-up player is instantiated', () => { - const context = createStylingContext([]); - const handler = new CorePlayerHandler(); - const lView = createMockViewData(handler, context); - - let previousPlayer: MockPlayer|null = null; - let currentPlayer: MockPlayer|null = null; - const buildFn = - (element: HTMLElement, type: BindingType, value: any, firstRender: boolean, - existingPlayer: Player | null) => { - previousPlayer = existingPlayer as MockPlayer | null; - return currentPlayer = new MockPlayer(value); - }; - - let factory = bindPlayerFactory<{[key: string]: any}>(buildFn, {width: '200px'}); - updateStyleAndClassMaps(context, null, factory); - renderStyles(context, false, undefined, lView); - - expect(previousPlayer).toEqual(null); - expect(currentPlayer !.value).toEqual({width: '200px'}); - - factory = bindPlayerFactory(buildFn, {height: '200px'}); - - updateStyleAndClassMaps(context, null, factory); - renderStyles(context, false, undefined, lView); - - expect(previousPlayer !.value).toEqual({width: '200px'}); - expect(currentPlayer !.value).toEqual({width: null, height: '200px'}); - }); - - it('should return the old player for classes when a follow-up player is instantiated', () => { - const context = createStylingContext(); - const handler = new CorePlayerHandler(); - const lView = createMockViewData(handler, context); - - let currentPlayer: MockPlayer|null = null; - let previousPlayer: MockPlayer|null = null; - const buildFn = - (element: HTMLElement, type: BindingType, value: any, firstRender: boolean, - existingPlayer: Player | null) => { - previousPlayer = existingPlayer as MockPlayer | null; - return currentPlayer = new MockPlayer(value); - }; - - let factory = bindPlayerFactory(buildFn, {foo: true}); - updateStyleAndClassMaps(context, null, factory); - renderStyles(context, false, undefined, lView); - - expect(currentPlayer).toBeTruthy(); - expect(previousPlayer).toBeFalsy(); - expect(currentPlayer !.value).toEqual({foo: true}); - - previousPlayer = currentPlayer = null; - - factory = bindPlayerFactory(buildFn, {bar: true}); - updateStyleAndClassMaps(context, null, factory); - renderStyles(context, false, undefined, lView); - - expect(currentPlayer).toBeTruthy(); - expect(previousPlayer).toBeTruthy(); - expect(currentPlayer !.value).toEqual({foo: null, bar: true}); - expect(previousPlayer !.value).toEqual({foo: true}); - }); - - it('should sanitize styles before they are passed into the player', () => { - const sanitizer = (function(prop: string, value: string, mode: StyleSanitizeMode): any { - let allow = true; - if (mode & StyleSanitizeMode.ValidateProperty) { - allow = prop === 'width' || prop === 'height'; - } - - if (mode & StyleSanitizeMode.SanitizeOnly) { - return allow ? `${value}-safe!` : value; - } else { - return allow; - } - }) as StyleSanitizeFn; - - const context = createStylingContext(null, null, null, null, sanitizer); - const handler = new CorePlayerHandler(); - const lView = createMockViewData(handler, context); - - let values: {[key: string]: any}|null = null; - const buildFn = - (element: HTMLElement, type: BindingType, value: any, isFirstRender: boolean) => { - values = value; - return new MockPlayer(); - }; - - let factory = bindPlayerFactory<{[key: string]: any}>( - buildFn, {width: '200px', height: '100px', opacity: '1'}); - updateStyleAndClassMaps(context, null, factory); - renderStyles(context, false, undefined, lView); - - expect(values !).toEqual({width: '200px-safe!', height: '100px-safe!', opacity: '1'}); - - factory = bindPlayerFactory(buildFn, {width: 'auto'}); - updateStyleAndClassMaps(context, null, factory); - renderStyles(context, false, undefined, lView); - - expect(values !).toEqual({width: 'auto-safe!', height: null, opacity: null}); - }); - - it('should automatically destroy existing players when the follow-up binding is not apart of a factory', + it('should automatically destroy existing players when the follow-up binding is not a part of a factory', () => { const context = createStylingContext(null, ['width'], null, ['foo', 'bar']); const handler = new CorePlayerHandler(); @@ -3079,244 +2532,6 @@ describe('style and class based bindings', () => { expect(p4.state).toEqual(PlayState.Destroyed); expect(p5.state).toEqual(PlayState.Running); }); - - it('should list all [style] and [class] players alongside custom players in the context', - () => { - const players: Player[] = []; - const styleBuildFn = (element: HTMLElement, type: BindingType, value: any) => { - const player = new MockPlayer(); - players.push(player); - return player; - }; - - const classBuildFn = (element: HTMLElement, type: BindingType, value: any) => { - const player = new MockPlayer(); - players.push(player); - return player; - }; - - const styleMapFactory = bindPlayerFactory(styleBuildFn, {height: '200px'}); - const classMapFactory = bindPlayerFactory(classBuildFn, {bar: true}); - const widthFactory = bindPlayerFactory(styleBuildFn, '100px'); - const fooFactory = bindPlayerFactory(classBuildFn, true); - - class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - ɵɵstyling(['foo'], ['width']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵstyleMap(styleMapFactory); - ɵɵclassMap(classMapFactory); - ɵɵstyleProp(0, widthFactory); - ɵɵclassProp(0, fooFactory); - ɵɵstylingApply(); - } - } - }); - } - - const fixture = new ComponentFixture(Comp); - fixture.update(); - - const target = fixture.hostElement.querySelector('div') !as any; - const elementContext = getLContext(target) !; - const context = elementContext.lView[elementContext.nodeIndex] as StylingContext; - - expect(players.length).toEqual(4); - const [p1, p2, p3, p4] = players; - - const playerContext = context[StylingIndex.PlayerContext]; - expect(playerContext).toContain(p1); - expect(playerContext).toContain(p2); - expect(playerContext).toContain(p3); - expect(playerContext).toContain(p4); - - expect(getPlayers(target)).toEqual([p1, p2, p3, p4]); - - const p5 = new MockPlayer(); - const p6 = new MockPlayer(); - addPlayer(target, p5); - addPlayer(target, p6); - - expect(getPlayers(target)).toEqual([p1, p2, p3, p4, p5, p6]); - p3.destroy(); - p5.destroy(); - - expect(getPlayers(target)).toEqual([p1, p2, p4, p6]); - }); - - it('should build a player and signal that the first render is active', () => { - const firstRenderCaptures: any[] = []; - const otherRenderCaptures: any[] = []; - const buildFn = - (element: HTMLElement, type: BindingType, value: any, isFirstRender: boolean) => { - if (isFirstRender) { - firstRenderCaptures.push({type, value}); - } else { - otherRenderCaptures.push({type, value}); - } - return new MockPlayer(); - }; - - let styleMapFactory = - bindPlayerFactory(buildFn, {height: '200px'}) as BoundPlayerFactory; - let classMapFactory = bindPlayerFactory(buildFn, {bar: true}) as BoundPlayerFactory; - let widthFactory = bindPlayerFactory(buildFn, '100px') as BoundPlayerFactory; - let fooFactory = bindPlayerFactory(buildFn, true) as BoundPlayerFactory; - - class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - ɵɵstyling(['foo'], ['width']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵstyleMap(styleMapFactory); - ɵɵclassMap(classMapFactory); - ɵɵstyleProp(0, widthFactory); - ɵɵclassProp(0, fooFactory); - ɵɵstylingApply(); - } - } - }); - } - - const fixture = new ComponentFixture(Comp); - - expect(firstRenderCaptures.length).toEqual(4); - expect(firstRenderCaptures[0]).toEqual({type: BindingType.Class, value: {bar: true}}); - expect(firstRenderCaptures[1]).toEqual({type: BindingType.Style, value: {height: '200px'}}); - expect(firstRenderCaptures[2]).toEqual({type: BindingType.Style, value: {width: '100px'}}); - expect(firstRenderCaptures[3]).toEqual({type: BindingType.Class, value: {foo: true}}); - expect(otherRenderCaptures.length).toEqual(0); - - firstRenderCaptures.length = 0; - styleMapFactory.value = {height: '100px'}; - classMapFactory.value = {bar: false}; - widthFactory.value = '50px'; - fooFactory.value = false; - - styleMapFactory = bindPlayerFactory(buildFn, {height: '100px'}) as BoundPlayerFactory; - classMapFactory = bindPlayerFactory(buildFn, {bar: false}) as BoundPlayerFactory; - widthFactory = bindPlayerFactory(buildFn, '50px') as BoundPlayerFactory; - fooFactory = bindPlayerFactory(buildFn, false) as BoundPlayerFactory; - - fixture.update(); - - expect(firstRenderCaptures.length).toEqual(0); - expect(otherRenderCaptures.length).toEqual(4); - expect(otherRenderCaptures[0]).toEqual({type: BindingType.Class, value: {bar: false}}); - expect(otherRenderCaptures[1]).toEqual({type: BindingType.Style, value: {height: '100px'}}); - expect(otherRenderCaptures[2]).toEqual({type: BindingType.Style, value: {width: '50px'}}); - expect(otherRenderCaptures[3]).toEqual({type: BindingType.Class, value: {foo: false}}); - }); - - it('should render styling players on both template and directive host bindings', () => { - const players: MockPlayer[] = []; - const styleBuildFn = (element: HTMLElement, type: BindingType, value: any) => { - const player = new MockPlayer(); - player.data = value; - players.push(player); - return player; - }; - - const classBuildFn = (element: HTMLElement, type: BindingType, value: any) => { - const player = new MockPlayer(); - player.data = value; - players.push(player); - return player; - }; - - const widthFactory1 = bindPlayerFactory(styleBuildFn, '100px'); - const widthFactory2 = bindPlayerFactory(styleBuildFn, '200px'); - const fooFactory1 = bindPlayerFactory(classBuildFn, true); - const fooFactory2 = bindPlayerFactory(classBuildFn, true); - - class MyDir { - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDir, - selectors: [['', 'my-dir', '']], - factory: () => new MyDir(), - hostBindings: function(rf: RenderFlags, ctx: MyDir, elementIndex: number) { - if (rf & RenderFlags.Create) { - ɵɵstyling(['foo'], ['width']); - } - if (rf & RenderFlags.Update) { - ɵɵstyleProp(0, ctx.widthFactory); - ɵɵclassProp(0, ctx.fooFactory); - ɵɵstylingApply(); - } - } - }); - - widthFactory = widthFactory2; - fooFactory = fooFactory2; - } - - class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - directives: [MyDir], - factory: () => new Comp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['my-dir', '']); - ɵɵstyling(['foo'], ['width']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵstyleProp(0, ctx.widthFactory); - ɵɵclassProp(0, ctx.fooFactory); - ɵɵstylingApply(); - } - } - }); - - widthFactory: any = widthFactory1; - fooFactory: any = fooFactory1; - } - - const fixture = new ComponentFixture(Comp); - const component = fixture.component; - fixture.update(); - - expect(players.length).toEqual(2); - const [p1, p2] = players; - players.length = 0; - - expect(p1.data).toEqual({width: '100px'}); - expect(p2.data).toEqual({foo: true}); - - component.fooFactory = null; - component.widthFactory = null; - - fixture.update(); - - expect(players.length).toEqual(2); - const [p3, p4] = players; - - expect(p3.data).toEqual({width: '200px'}); - expect(p4.data).toEqual({foo: true}); - }); }); }); diff --git a/packages/core/test/render3/styling/players_spec.ts b/packages/core/test/render3/styling/players_spec.ts index c8010f1bbe..ed602b7786 100644 --- a/packages/core/test/render3/styling/players_spec.ts +++ b/packages/core/test/render3/styling/players_spec.ts @@ -258,7 +258,7 @@ class CompWithStyling { template: (rf: RenderFlags, ctx: CompWithStyling) => { if (rf & RenderFlags.Create) { ɵɵelementStart(0, 'div'); - ɵɵstyling(['fooClass']); + ɵɵstyling(); ɵɵelementEnd(); } if (rf & RenderFlags.Update) { 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 a6ebd1e439..550adad99b 100644 --- a/packages/core/test/render3/styling_next/map_based_bindings_spec.ts +++ b/packages/core/test/render3/styling_next/map_based_bindings_spec.ts @@ -8,8 +8,8 @@ import {normalizeIntoStylingMap as createMap} from '../../../src/render3/styling_next/map_based_bindings'; describe('map-based bindings', () => { - describe('LStylingMap construction', () => { - it('should create a new LStylingMap instance from a given value', () => { + describe('StylingMapArray construction', () => { + it('should create a new StylingMapArray instance from a given value', () => { createAndAssertValues(null, []); createAndAssertValues(undefined, []); createAndAssertValues({}, []); @@ -30,7 +30,7 @@ describe('map-based bindings', () => { expect(map2).toEqual([value2, 'maybe', true, 'no', true, 'yes', true]); }); - it('should patch an existing LStylingMap entry with new values and retain the alphabetical order', + it('should patch an existing StylingMapArray entry with new values and retain the alphabetical order', () => { const value1 = {color: 'red'}; const map1 = createMap(null, value1); @@ -52,7 +52,7 @@ describe('map-based bindings', () => { expect(map4).toEqual([value4, 'everyonesClass', true, 'myClass', true, 'yourClass', true]); }); - it('should nullify old values that are not apart of the new set of values', () => { + it('should nullify old values that are not a part of the new set of values', () => { const value1 = {color: 'red', fontSize: '20px'}; const map1 = createMap(null, value1); expect(map1).toEqual([value1, 'color', 'red', 'fontSize', '20px']); @@ -70,6 +70,12 @@ describe('map-based bindings', () => { const map4 = createMap(map3, value4); expect(map4).toEqual([value4, 'apple', true, 'banana', true, 'orange', null]); }); + + it('should hyphenate property names ', () => { + const value1 = {fontSize: '50px', paddingTopLeft: '20px'}; + const map1 = createMap(null, value1, true); + expect(map1).toEqual([value1, 'font-size', '50px', 'padding-top-left', '20px']); + }); }); }); 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 e84137ba6b..db23feb0ee 100644 --- a/packages/core/test/render3/styling_next/styling_context_spec.ts +++ b/packages/core/test/render3/styling_next/styling_context_spec.ts @@ -47,6 +47,23 @@ describe('styling context', () => { }); }); + it('should only register the same binding index once per property', () => { + const debug = makeContextWithDebug(); + const context = debug.context; + expect(debug.entries).toEqual({}); + + registerBinding(context, 1, 'width', 123); + registerBinding(context, 1, 'width', 123); + expect(debug.entries['width']).toEqual({ + prop: 'width', + valuesCount: 2, + sanitizationRequired: false, + guardMask: buildGuardMask(1), + defaultValue: null, + sources: [123, null], + }); + }); + it('should overwrite a default value for an entry only if it is non-null', () => { const debug = makeContextWithDebug(); const context = debug.context; diff --git a/packages/core/test/render3/view_utils_spec.ts b/packages/core/test/render3/view_utils_spec.ts index 73cd3c0b29..81cb6f41e8 100644 --- a/packages/core/test/render3/view_utils_spec.ts +++ b/packages/core/test/render3/view_utils_spec.ts @@ -20,11 +20,6 @@ describe('view_utils', () => { const lContainer = createLContainer(lView, lView, div, tNode, true); const styleContext = createEmptyStylingContext(lContainer, null, null, null); - expect(unwrapRNode(styleContext)).toBe(div); - expect(unwrapStylingContext(styleContext)).toBe(styleContext); - expect(unwrapLContainer(styleContext)).toBe(lContainer); - expect(unwrapLView(styleContext)).toBe(lView); - expect(isLView(lView)).toBe(true); expect(isLView(lContainer)).toBe(false); expect(isLView(styleContext)).toBe(false); diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 898fab9134..70fc2e1784 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -715,8 +715,8 @@ export interface ɵɵBaseDef { } export declare function ɵɵclassMap(classes: { - [styleName: string]: any; -} | string | null): void; + [className: string]: any; +} | NO_CHANGE | string | null): void; export declare function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void; @@ -736,7 +736,7 @@ export declare function ɵɵclassMapInterpolate8(prefix: string, v0: any, i0: st export declare function ɵɵclassMapInterpolateV(values: any[]): void; -export declare function ɵɵclassProp(classIndex: number, value: boolean | PlayerFactory, forceOverride?: boolean): void; +export declare function ɵɵclassProp(className: string, value: boolean | null): void; export declare type ɵɵComponentDefWithMeta