refactor(ivy): remove styling state storage and introduce direct style writing (#32591)

This patch is a final major refactor in styling Angular.

This PR includes three main fixes:

All temporary state taht is persisted between template style/class application
and style/class application in host bindings is now removed.
Removes the styling() and stylingApply() instructions.
Introduces a "direct apply" mode that is used apply prop-based
style/class in the event that there are no map-based bindings as
well as property collisions.

PR Close #32259

PR Close #32591
This commit is contained in:
Matias Niemelä 2019-09-09 13:14:26 -07:00 committed by Andrew Kushnir
parent e6ed4a21e4
commit 4f41473048
46 changed files with 1756 additions and 1614 deletions

View File

@ -16,8 +16,8 @@
"uncompressed": {
"runtime-es5": 3042,
"runtime-es2015": 3048,
"main-es5": 493330,
"main-es2015": 435296,
"main-es5": 492593,
"main-es2015": 438296,
"polyfills-es5": 131024,
"polyfills-es2015": 52433
}
@ -28,7 +28,7 @@
"uncompressed": {
"runtime-es5": 2932,
"runtime-es2015": 2938,
"main-es5": 550854,
"main-es5": 552068,
"main-es2015": 493320,
"polyfills-es5": 131024,
"polyfills-es2015": 52433

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime": 1440,
"main": 13264,
"main": 13604,
"polyfills": 45340
}
}

View File

@ -36,7 +36,7 @@ export class TableComponent {
@NgModule({imports: [BrowserModule], bootstrap: [TableComponent], declarations: [TableComponent]})
export class AppModule {
constructor(sanitizer: DomSanitizer) {
trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('');
trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('white');
trustedGreyColor = sanitizer.bypassSecurityTrustStyle('grey');
}
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ɵRenderFlags, ɵrenderComponent as renderComponent, ɵɵadvance, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵstyleProp, ɵɵstyling, ɵɵstylingApply, ɵɵtext, ɵɵtextInterpolate1} from '@angular/core';
import {ɵRenderFlags, ɵrenderComponent as renderComponent, ɵɵadvance, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵstyleProp, ɵɵtext, ɵɵtextInterpolate1} from '@angular/core';
import {bindAction, profile} from '../../util';
import {createDom, destroyDom, detectChanges} from '../render3/tree';
@ -39,7 +39,6 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) {
ɵɵelementStart(0, 'tree');
{
ɵɵelementStart(1, 'span');
ɵɵstyling();
{ ɵɵtext(2); }
ɵɵelementEnd();
ɵɵcontainer(3);
@ -50,7 +49,6 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) {
if (rf & ɵRenderFlags.Update) {
ɵɵadvance(1);
ɵɵstyleProp('background-color', ctx.depth % 2 ? '' : 'grey');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵtextInterpolate1(' ', ctx.value, ' ');
ɵɵcontainerRefreshStart(3);

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, DoCheck, Input, ɵRenderFlags, ɵɵallocHostVars, ɵɵclassMap, ɵɵdefineDirective, ɵɵstyling, ɵɵstylingApply} from '@angular/core';
import {Directive, DoCheck, Input, ɵRenderFlags, ɵɵallocHostVars, ɵɵclassMap, ɵɵdefineDirective} from '@angular/core';
import {NgClassImpl, NgClassImplProvider} from './ng_class_impl';
@ -35,11 +35,9 @@ export const ngClassDirectiveDef__POST_R3__ = ɵɵdefineDirective({
hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) {
if (rf & ɵRenderFlags.Create) {
ɵɵallocHostVars(1);
ɵɵstyling();
}
if (rf & ɵRenderFlags.Update) {
ɵɵclassMap(ctx.getValue());
ɵɵstylingApply();
}
}
});

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, DoCheck, Input, ɵRenderFlags, ɵɵdefineDirective, ɵɵstyleMap, ɵɵstyling, ɵɵstylingApply} from '@angular/core';
import {Directive, DoCheck, Input, ɵRenderFlags, ɵɵallocHostVars, ɵɵdefineDirective, ɵɵstyleMap} from '@angular/core';
import {NgStyleImpl, NgStyleImplProvider} from './ng_style_impl';
@ -35,11 +35,10 @@ export const ngStyleDirectiveDef__POST_R3__ = ɵɵdefineDirective({
selectors: null as any,
hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) {
if (rf & ɵRenderFlags.Create) {
ɵɵstyling();
ɵɵallocHostVars(1);
}
if (rf & ɵRenderFlags.Update) {
ɵɵstyleMap(ctx.getValue());
ɵɵstylingApply();
}
}
});

View File

@ -483,14 +483,11 @@ describe('compiler compliance', () => {
vars: 2,
template: function MyComponent_Template(rf,ctx){
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵstyleProp("background-color", ctx.color);
$r3$.ɵɵclassProp("error", ctx.error);
$r3$.ɵɵstylingApply();
}
},
encapsulation: 2

View File

@ -376,14 +376,11 @@ describe('compiler compliance: styling', () => {
const template = `
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($ctx$.myStyleExp);
$r3$.ɵɵstylingApply();
}
}
`;
@ -442,13 +439,10 @@ describe('compiler compliance: styling', () => {
vars: 2,
template: function MyComponentWithInterpolation_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵclassMapInterpolate1("foo foo-", $ctx$.fooId, "");
$r3$.ɵɵstylingApply();
}
}
@ -456,13 +450,10 @@ describe('compiler compliance: styling', () => {
vars: 3,
template: function MyComponentWithMuchosInterpolation_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵclassMapInterpolate2("foo foo-", $ctx$.fooId, "-", $ctx$.fooUsername, "");
$r3$.ɵɵstylingApply();
}
}
@ -470,13 +461,10 @@ describe('compiler compliance: styling', () => {
vars: 1,
template: function MyComponentWithoutInterpolation_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵclassMap($ctx$.exp);
$r3$.ɵɵstylingApply();
}
}
`;
@ -522,16 +510,13 @@ describe('compiler compliance: styling', () => {
vars: 4,
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div", $_c0$);
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div", $_c0$);
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($ctx$.myStyleExp);
$r3$.ɵɵstyleProp("width", $ctx$.myWidth);
$r3$.ɵɵstyleProp("height", $ctx$.myHeight);
$r3$.ɵɵstylingApply();
$r3$.ɵɵattribute("style", "border-width: 10px", $r3$.ɵɵsanitizeStyle);
}
},
@ -572,14 +557,11 @@ describe('compiler compliance: styling', () => {
vars: 1,
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleProp("background-image", ctx.myImage);
$r3$.ɵɵstylingApply();
}
},
encapsulation: 2
@ -612,13 +594,10 @@ describe('compiler compliance: styling', () => {
const template = `
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵstyleProp("font-size", 12, "px");
$r3$.ɵɵstylingApply();
}
}
`;
@ -676,13 +655,10 @@ describe('compiler compliance: styling', () => {
const template = `
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵclassMap($ctx$.myClassExp);
$r3$.ɵɵstylingApply();
}
}
`;
@ -728,15 +704,12 @@ describe('compiler compliance: styling', () => {
vars: 4,
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div", $e0_attrs$);
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div", $e0_attrs$);
}
if (rf & 2) {
$r3$.ɵɵclassMap($ctx$.myClassExp);
$r3$.ɵɵclassProp("apple", $ctx$.yesToApple);
$r3$.ɵɵclassProp("orange", $ctx$.yesToOrange);
$r3$.ɵɵstylingApply();
$r3$.ɵɵattribute("class", "banana");
}
},
@ -844,15 +817,12 @@ describe('compiler compliance: styling', () => {
const template = `
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($ctx$.myStyleExp);
$r3$.ɵɵclassMap($ctx$.myClassExp);
$r3$.ɵɵstylingApply();
}
}
`;
@ -887,7 +857,6 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵpipe(1, "stylePipe");
$r3$.ɵɵpipe(2, "classPipe");
$r3$.ɵɵelementEnd();
@ -896,7 +865,6 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind1(1, 2, $ctx$.myStyleExp));
$r3$.ɵɵclassMap($r3$.ɵɵpipeBind1(2, 4, $ctx$.myClassExp));
$r3$.ɵɵstylingApply();
}
}
`;
@ -939,7 +907,6 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵpipe(1, "pipe");
$r3$.ɵɵpipe(2, "pipe");
$r3$.ɵɵpipe(3, "pipe");
@ -954,9 +921,8 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵstyleProp("bar", $r3$.ɵɵpipeBind2(2, 9, $ctx$.barExp, 3000));
$r3$.ɵɵstyleProp("baz", $r3$.ɵɵpipeBind2(3, 12, $ctx$.bazExp, 4000));
$r3$.ɵɵclassProp("foo", $r3$.ɵɵpipeBind2(4, 15, $ctx$.fooExp, 2000));
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(5);
$r3$.ɵɵtextInterpolate1(" ", $ctx$.item, "");
$r3$.ɵɵtextInterpolate1(" ", $ctx$.item, "");
}
}
`;
@ -999,16 +965,12 @@ describe('compiler compliance: styling', () => {
if (rf & 2) {
$r3$.ɵɵstyleProp("width", $ctx$.w1);
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵstyleProp("height", $ctx$.h1);
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassProp("active", $ctx$.a1);
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassProp("removed", $ctx$.r1);
$r3$.ɵɵstylingApply();
}
}
`;
@ -1058,7 +1020,6 @@ describe('compiler compliance: styling', () => {
if (rf & 1) {
$r3$.ɵɵallocHostVars(4);
$r3$.ɵɵelementHostAttrs($e0_attrs$);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
@ -1066,7 +1027,6 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵclassMap(ctx.myClass);
$r3$.ɵɵstyleProp("color", ctx.myColorProp);
$r3$.ɵɵclassProp("foo", ctx.myFooClass);
$r3$.ɵɵstylingApply();
}
},
consts: 0,
@ -1118,7 +1078,6 @@ describe('compiler compliance: styling', () => {
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(6);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
@ -1128,7 +1087,6 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵstyleProp("width", ctx.myWidthProp);
$r3$.ɵɵclassProp("bar", ctx.myBarClass);
$r3$.ɵɵclassProp("foo", ctx.myFooClass);
$r3$.ɵɵstylingApply();
}
},
consts: 0,
@ -1179,9 +1137,7 @@ describe('compiler compliance: styling', () => {
const template = `
function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelement(0, "div");
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
@ -1189,7 +1145,6 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵclassMap(ctx.myClassExp);
$r3$.ɵɵstyleProp("height", ctx.myHeightExp);
$r3$.ɵɵclassProp("bar", ctx.myBarClassExp);
$r3$.ɵɵstylingApply();
}
},
`;
@ -1198,7 +1153,6 @@ describe('compiler compliance: styling', () => {
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(4);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
@ -1206,7 +1160,6 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵclassMap(ctx.myClassExp);
$r3$.ɵɵstyleProp("width", ctx.myWidthExp);
$r3$.ɵɵclassProp("foo", ctx.myFooClassExp);
$r3$.ɵɵstylingApply();
}
},
`;
@ -1266,35 +1219,29 @@ describe('compiler compliance: styling', () => {
function ClassDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(1);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵclassMap(ctx.myClassMap);
$r3$.ɵɵstylingApply();
}
}
function WidthDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(2);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵstyleProp("width", ctx.myWidth);
$r3$.ɵɵclassProp("foo", ctx.myFooClass);
$r3$.ɵɵstylingApply();
}
}
function HeightDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(2);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵstyleProp("height", ctx.myHeight);
$r3$.ɵɵclassProp("bar", ctx.myBarClass);
$r3$.ɵɵstylingApply();
}
}
@ -1336,34 +1283,24 @@ describe('compiler compliance: styling', () => {
if (rf & 2) {
$r3$.ɵɵclassMapInterpolateV(["a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i", ctx.nine, "j"]);
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate8("a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate7("a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate6("a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate5("a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate4("a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate3("a", ctx.one, "b", ctx.two, "c", ctx.three, "d");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate2("a", ctx.one, "b", ctx.two, "c");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMapInterpolate1("a", ctx.one, "b");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵclassMap(ctx.one);
$r3$.ɵɵstylingApply();
}
`;
@ -1438,34 +1375,24 @@ describe('compiler compliance: styling', () => {
if (rf & 2) {
$r3$.ɵɵstylePropInterpolateV("color", ["a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i", ctx.nine, "j"]);
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵstylePropInterpolate8("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵstylePropInterpolate7("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵstylePropInterpolate6("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵstylePropInterpolate5("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵstylePropInterpolate4("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵstylePropInterpolate3("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵstylePropInterpolate2("color", "a", ctx.one, "b", ctx.two, "c");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵstylePropInterpolate1("color", "a", ctx.one, "b");
$r3$.ɵɵstylingApply();
$r3$.ɵɵadvance(1);
$r3$.ɵɵstyleProp("color", ctx.one);
$r3$.ɵɵstylingApply();
}
`;
@ -1496,7 +1423,6 @@ describe('compiler compliance: styling', () => {
if (rf & 2) {
$r3$.ɵɵstylePropInterpolate2("width", "a", ctx.one, "b", ctx.two, "c", "px");
$r3$.ɵɵstylingApply();
}
`;
@ -1527,7 +1453,6 @@ describe('compiler compliance: styling', () => {
if (rf & 2) {
$r3$.ɵɵstylePropInterpolate2("width", "a", ctx.one, "b", ctx.two, "c");
$r3$.ɵɵstylingApply();
}
`;
@ -1583,14 +1508,12 @@ describe('compiler compliance: styling', () => {
if (rf & 1) {
$r3$.ɵɵallocHostVars(4);
$r3$.ɵɵelementHostAttrs($_c0$);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title);
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyle);
$r3$.ɵɵclassMap(ctx.myClass);
$r3$.ɵɵstylingApply();
}
}
`;
@ -1627,13 +1550,11 @@ describe('compiler compliance: styling', () => {
hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(4);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title);
$r3$.ɵɵstyleProp("width", ctx.myWidth);
$r3$.ɵɵclassProp("foo", ctx.myFooClass);
$r3$.ɵɵstylingApply();
}
}
`;
@ -1669,7 +1590,6 @@ describe('compiler compliance: styling', () => {
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleProp("background-image", ctx.bgExp);
$r3$.ɵɵstylingApply();
}
}
@ -1705,7 +1625,6 @@ describe('compiler compliance: styling', () => {
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.mapExp);
$r3$.ɵɵstylingApply();
}
}
@ -1742,7 +1661,6 @@ describe('compiler compliance: styling', () => {
if (rf & 2) {
$r3$.ɵɵclassMap(ctx.mapExp);
$r3$.ɵɵclassProp("name", ctx.nameExp);
$r3$.ɵɵstylingApply();
}
}
@ -1805,7 +1723,6 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵpureFunction2(6, _c1, ctx._animValue,
$r3$.ɵɵpureFunction2(3, _c0, ctx._animParam1, ctx._animParam2)));
$r3$.ɵɵclassProp("foo", ctx.foo);
$r3$.ɵɵstylingApply();
}
}
`;

View File

@ -1867,13 +1867,11 @@ runInEachFileSystem(os => {
i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); });
i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody);
i0.ɵɵlistener("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); });
i0.ɵɵstyling();
}
if (rf & 2) {
i0.ɵɵhostProperty("prop", ctx.bar);
i0.ɵɵattribute("hello", ctx.foo);
i0.ɵɵclassProp("someclass", ctx.someClass);
i0.ɵɵstylingApply();
}
}
`;

View File

@ -69,8 +69,6 @@ export class Identifiers {
static elementContainer: o.ExternalReference = {name: 'ɵɵelementContainer', moduleName: CORE};
static styling: o.ExternalReference = {name: 'ɵɵstyling', moduleName: CORE};
static styleMap: o.ExternalReference = {name: 'ɵɵstyleMap', moduleName: CORE};
static classMap: o.ExternalReference = {name: 'ɵɵclassMap', moduleName: CORE};
@ -115,8 +113,6 @@ export class Identifiers {
static stylePropInterpolateV:
o.ExternalReference = {name: 'ɵɵstylePropInterpolateV', moduleName: CORE};
static stylingApply: o.ExternalReference = {name: 'ɵɵstylingApply', moduleName: CORE};
static styleSanitizer: o.ExternalReference = {name: 'ɵɵstyleSanitizer', moduleName: CORE};
static elementHostAttrs: o.ExternalReference = {name: 'ɵɵelementHostAttrs', moduleName: CORE};

View File

@ -713,16 +713,6 @@ function createHostBindingsFunction(
}
if (styleBuilder.hasBindings) {
// singular style/class bindings (things like `[style.prop]` and `[class.name]`)
// MUST be registered on a given element within the component/directive
// templateFn/hostBindingsFn functions. The instruction below will figure out
// what all the bindings are and then generate the statements required to register
// those bindings to the element via `styling`.
const stylingInstruction = styleBuilder.buildStylingInstruction(null, constantPool);
if (stylingInstruction) {
createStatements.push(createStylingStmt(stylingInstruction, bindingContext, bindingFn));
}
// finally each binding that was registered in the statement above will need to be added to
// the update block of a component/directive templateFn/hostBindingsFn so that the bindings
// are evaluated and updated for the element.

View File

@ -7,7 +7,7 @@
*/
import {ConstantPool} from '../../constant_pool';
import {AttributeMarker} from '../../core';
import {AST, BindingType, Interpolation} from '../../expression_parser/ast';
import {AST, ASTWithSource, BindingPipe, BindingType, Interpolation} from '../../expression_parser/ast';
import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util';
import {isEmptyExpression} from '../../template_parser/template_parser';
@ -68,7 +68,6 @@ interface BoundStylingEntry {
* classMap(...)
* styleProp(...)
* classProp(...)
* stylingApply(...)
* }
*
* The creation/update methods within the builder class produce these instructions.
@ -81,6 +80,7 @@ export class StylingBuilder {
* (i.e. `[style]`, `[class]`, `[style.prop]` or `[class.name]`)
*/
public hasBindings = false;
public hasBindingsWithPipes = false;
/** the input for [class] (if it exists) */
private _classMapInput: BoundStylingEntry|null = null;
@ -187,6 +187,7 @@ export class StylingBuilder {
}
this._lastStylingInput = entry;
this._firstStylingInput = this._firstStylingInput || entry;
this._checkForPipes(value);
this.hasBindings = true;
return entry;
}
@ -207,10 +208,17 @@ export class StylingBuilder {
}
this._lastStylingInput = entry;
this._firstStylingInput = this._firstStylingInput || entry;
this._checkForPipes(value);
this.hasBindings = true;
return entry;
}
private _checkForPipes(value: AST) {
if ((value instanceof ASTWithSource) && (value.ast instanceof BindingPipe)) {
this.hasBindingsWithPipes = true;
}
}
/**
* Registers the element's static style string value to the builder.
*
@ -284,25 +292,6 @@ export class StylingBuilder {
return null;
}
/**
* Builds an instruction with all the expressions and parameters for `styling`.
*
* The instruction generation code below is used for producing the AOT statement code which is
* responsible for registering style/class bindings to an element.
*/
buildStylingInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
StylingInstruction|null {
if (this.hasBindings) {
return {
sourceSpan,
allocateBindingSlots: 0,
reference: R3.styling,
params: () => [],
};
}
return null;
}
/**
* Builds an instruction with all the expressions and parameters for `classMap`.
*
@ -422,15 +411,6 @@ export class StylingBuilder {
return [];
}
private _buildApplyFn(): StylingInstruction {
return {
sourceSpan: this._lastStylingInput ? this._lastStylingInput.sourceSpan : null,
reference: R3.stylingApply,
allocateBindingSlots: 0,
params: () => { return []; }
};
}
private _buildSanitizerFn(): StylingInstruction {
return {
sourceSpan: this._firstStylingInput ? this._firstStylingInput.sourceSpan : null,
@ -460,7 +440,6 @@ export class StylingBuilder {
}
instructions.push(...this._buildStyleInputs(valueConverter));
instructions.push(...this._buildClassInputs(valueConverter));
instructions.push(this._buildApplyFn());
}
return instructions;
}

View File

@ -623,11 +623,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const hasChildren = (!isI18nRootElement && this.i18n) ? !hasTextChildrenOnly(element.children) :
element.children.length > 0;
const createSelfClosingInstruction = !stylingBuilder.hasBindings &&
const createSelfClosingInstruction = !stylingBuilder.hasBindingsWithPipes &&
element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren;
const createSelfClosingI18nInstruction = !createSelfClosingInstruction &&
!stylingBuilder.hasBindings && hasTextChildrenOnly(element.children);
const createSelfClosingI18nInstruction =
!createSelfClosingInstruction && hasTextChildrenOnly(element.children);
if (createSelfClosingInstruction) {
this.creationInstruction(
@ -681,16 +680,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}
}
// The style bindings code is placed into two distinct blocks within the template function AOT
// code: creation and update. The creation code contains the `styling` instructions
// which will apply the collected binding values to the element. `styling` is
// designed to run inside of `elementStart` and `elementEnd`. The update instructions
// (things like `styleProp`, `classProp`, etc..) are applied later on in this
// file
this.processStylingInstruction(
elementIndex,
stylingBuilder.buildStylingInstruction(element.sourceSpan, this.constantPool), true);
// Generate Listeners (outputs)
element.outputs.forEach((outputAst: t.BoundEvent) => {
this.creationInstruction(

View File

@ -120,7 +120,6 @@ export {
ɵɵelementContainerStart,
ɵɵelementContainerEnd,
ɵɵelementContainer,
ɵɵstyling,
ɵɵstyleMap,
ɵɵstyleSanitizer,
ɵɵclassMap,
@ -143,7 +142,6 @@ export {
ɵɵstylePropInterpolate7,
ɵɵstylePropInterpolate8,
ɵɵstylePropInterpolateV,
ɵɵstylingApply,
ɵɵclassProp,
ɵɵelementHostAttrs,

View File

@ -12,10 +12,9 @@ import {CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from '../render3/interfaces
import {TElementNode, TNode, TNodeFlags, TNodeType} from '../render3/interfaces/node';
import {isComponentHost, isLContainer} from '../render3/interfaces/type_checks';
import {LView, PARENT, TData, TVIEW, T_HOST} from '../render3/interfaces/view';
import {TStylingContext} from '../render3/styling_next/interfaces';
import {stylingMapToStringMap} from '../render3/styling_next/map_based_bindings';
import {StylingMapArray, TStylingContext} from '../render3/styling_next/interfaces';
import {NodeStylingDebug} from '../render3/styling_next/styling_debug';
import {isStylingContext} from '../render3/styling_next/util';
import {isStylingContext, stylingMapToStringMap} from '../render3/styling_next/util';
import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, getLocalRefs, isBrowserEvents, loadLContext} from '../render3/util/discovery_utils';
import {INTERPOLATION_DELIMITER, renderStringify} from '../render3/util/misc_utils';
import {findComponentView} from '../render3/util/view_traversal_utils';
@ -431,11 +430,11 @@ function _getStylingDebugInfo(element: any, isClassBased: boolean) {
if (isClassBased) {
return isStylingContext(tNode.classes) ?
new NodeStylingDebug(tNode.classes as TStylingContext, lView, true).values :
stylingMapToStringMap(tNode.classes);
stylingMapToStringMap(tNode.classes as StylingMapArray | null);
} else {
return isStylingContext(tNode.styles) ?
new NodeStylingDebug(tNode.styles as TStylingContext, lView, false).values :
stylingMapToStringMap(tNode.styles);
stylingMapToStringMap(tNode.styles as StylingMapArray | null);
}
}

View File

@ -23,7 +23,7 @@ import {TElementNode, TNode, TNodeType} from './interfaces/node';
import {PlayerHandler} from './interfaces/player';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, LView, LViewFlags, RootContext, RootContextFlags, TVIEW} from './interfaces/view';
import {getPreviousOrParentTNode, resetComponentState, selectView, setActiveHostElement} from './state';
import {getPreviousOrParentTNode, incrementActiveDirectiveId, resetComponentState, selectView, setActiveHostElement} from './state';
import {publishDefaultGlobalUtils} from './util/global_utils';
import {defaultScheduler, stringifyForError} from './util/misc_utils';
import {getRootContext} from './util/view_traversal_utils';
@ -217,6 +217,7 @@ export function createRootComponent<T>(
if (tView.firstTemplatePass && componentDef.hostBindings) {
const elementIndex = rootTNode.index - HEADER_OFFSET;
setActiveHostElement(elementIndex);
incrementActiveDirectiveId();
const expando = tView.expandoInstructions !;
invokeHostBindingsInCreationMode(

View File

@ -11,7 +11,6 @@ import {fillProperties} from '../../util/property';
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {ComponentDef, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, HostBindingsFunction, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {isComponentDef} from '../interfaces/type_checks';
import {adjustActiveDirectiveSuperClassDepthPosition} from '../state';
import {ɵɵNgOnChangesFeature} from './ng_onchanges_feature';
@ -177,24 +176,8 @@ function inheritHostBindings(
// to ensure we don't inherit it twice.
if (superHostBindings !== prevHostBindings) {
if (prevHostBindings) {
// because inheritance is unknown during compile time, the runtime code
// needs to be informed of the super-class depth so that instruction code
// can distinguish one host bindings function from another. The reason why
// relying on the directive uniqueId exclusively is not enough is because the
// uniqueId value and the directive instance stay the same between hostBindings
// calls throughout the directive inheritance chain. This means that without
// a super-class depth value, there is no way to know whether a parent or
// sub-class host bindings function is currently being executed.
definition.hostBindings = (rf: RenderFlags, ctx: any, elementIndex: number) => {
// The reason why we increment first and then decrement is so that parent
// hostBindings calls have a higher id value compared to sub-class hostBindings
// calls (this way the leaf directive is always at a super-class depth of 0).
adjustActiveDirectiveSuperClassDepthPosition(1);
try {
superHostBindings(rf, ctx, elementIndex);
} finally {
adjustActiveDirectiveSuperClassDepthPosition(-1);
}
superHostBindings(rf, ctx, elementIndex);
prevHostBindings(rf, ctx, elementIndex);
};
} else {

View File

@ -114,8 +114,6 @@ export {
ɵɵstylePropInterpolateV,
ɵɵstyleSanitizer,
ɵɵstyling,
ɵɵstylingApply,
ɵɵtemplate,
ɵɵtext,

View File

@ -8,7 +8,10 @@
import {assertDataInRange, assertGreaterThan} from '../../util/assert';
import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks';
import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, TVIEW} from '../interfaces/view';
import {getCheckNoChangesMode, getLView, getSelectedIndex, setSelectedIndex} from '../state';
import {ActiveElementFlags, executeElementExitFn, getCheckNoChangesMode, getLView, getSelectedIndex, hasActiveElementFlag, setSelectedIndex} from '../state';
import {resetStylingState} from '../styling_next/state';
/**
* Advances to an element for later binding instructions.
@ -51,6 +54,10 @@ export function selectIndexInternal(lView: LView, index: number, checkNoChangesM
ngDevMode && assertGreaterThan(index, -1, 'Invalid index');
ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET);
if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) {
executeElementExitFn();
}
// Flush the initial hooks for elements in the view that have been added up to this point.
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
if (!checkNoChangesMode) {
@ -69,6 +76,10 @@ export function selectIndexInternal(lView: LView, index: number, checkNoChangesM
}
}
if (hasActiveElementFlag(ActiveElementFlags.ResetStylesOnExit)) {
resetStylingState();
}
// We must set the selected index *after* running the hooks, because hooks may have side-effects
// that cause other template functions to run, thus updating the selected index, which is global
// state. If we run `setSelectedIndex` *before* we run the hooks, in some cases the selected index

View File

@ -139,11 +139,11 @@ export function ɵɵelementEnd(): void {
}
}
if (hasClassInput(tNode) && tNode.classes) {
if (hasClassInput(tNode)) {
setDirectiveStylingInput(tNode.classes, lView, tNode.inputs !['class']);
}
if (hasStyleInput(tNode) && tNode.styles) {
if (hasStyleInput(tNode)) {
setDirectiveStylingInput(tNode.styles, lView, tNode.inputs !['style']);
}
}
@ -237,11 +237,12 @@ export function ɵɵelementHostAttrs(attrs: TAttributes) {
}
function setDirectiveStylingInput(
context: TStylingContext | StylingMapArray, lView: LView, stylingInputs: (string | number)[]) {
context: TStylingContext | StylingMapArray | null, lView: LView,
stylingInputs: (string | number)[]) {
// older versions of Angular treat the input as `null` in the
// event that the value does not exist at all. For this reason
// we can't have a styling value be an empty string.
const value = getInitialStylingValue(context) || null;
const value = (context && getInitialStylingValue(context)) || null;
// Ivy does an extra `[class]` write with a falsy value since the value
// is applied during creation mode. This is a deviation from VE and should

View File

@ -30,8 +30,9 @@ import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRoo
import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TData, TVIEW, TView, T_HOST} from '../interfaces/view';
import {assertNodeOfPossibleTypes} from '../node_assert';
import {isNodeMatchingSelectorList} from '../node_selector_matcher';
import {getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, incrementActiveDirectiveId, namespaceHTMLInternal, selectView, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state';
import {ActiveElementFlags, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, hasActiveElementFlag, incrementActiveDirectiveId, namespaceHTMLInternal, selectView, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state';
import {renderStylingMap} from '../styling_next/bindings';
import {resetStylingState} from '../styling_next/state';
import {NO_CHANGE} from '../tokens';
import {ANIMATION_PROP_PREFIX, isAnimationProp} from '../util/attrs_utils';
import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils';
@ -82,16 +83,18 @@ export function setHostBindings(tView: TView, viewData: LView): void {
} else {
// If it's not a number, it's a host binding function that needs to be executed.
if (instruction !== null) {
viewData[BINDING_INDEX] = bindingRootIndex;
const hostCtx = unwrapRNode(viewData[currentDirectiveIndex]);
instruction(RenderFlags.Update, hostCtx, currentElementIndex);
// Each directive gets a uniqueId value that is the same for both
// create and update calls when the hostBindings function is called. The
// directive uniqueId is not set anywhere--it is just incremented between
// each hostBindings call and is useful for helping instruction code
// uniquely determine which directive is currently active when executed.
// It is important that this be called first before the actual instructions
// are run because this way the first directive ID value is not zero.
incrementActiveDirectiveId();
viewData[BINDING_INDEX] = bindingRootIndex;
const hostCtx = unwrapRNode(viewData[currentDirectiveIndex]);
instruction(RenderFlags.Update, hostCtx, currentElementIndex);
}
currentDirectiveIndex++;
}
@ -499,6 +502,12 @@ function executeTemplate<T>(
}
templateFn(rf, context);
} finally {
if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) {
executeElementExitFn();
}
if (hasActiveElementFlag(ActiveElementFlags.ResetStylesOnExit)) {
resetStylingState();
}
setSelectedIndex(prevSelectedIndex);
}
}
@ -1094,14 +1103,10 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
const def = tView.data[i] as DirectiveDef<any>;
const directive = viewData[i];
if (def.hostBindings) {
invokeHostBindingsInCreationMode(def, expando, directive, tNode, firstTemplatePass);
// Each directive gets a uniqueId value that is the same for both
// create and update calls when the hostBindings function is called. The
// directive uniqueId is not set anywhere--it is just incremented between
// each hostBindings call and is useful for helping instruction code
// uniquely determine which directive is currently active when executed.
// It is important that this be called first before the actual instructions
// are run because this way the first directive ID value is not zero.
incrementActiveDirectiveId();
invokeHostBindingsInCreationMode(def, expando, directive, tNode, firstTemplatePass);
} else if (firstTemplatePass) {
expando.push(null);
}

View File

@ -116,7 +116,6 @@ export const angularCoreEnv: {[name: string]: Function} =
'ɵɵclassMapInterpolate7': r3.ɵɵclassMapInterpolate7,
'ɵɵclassMapInterpolate8': r3.ɵɵclassMapInterpolate8,
'ɵɵclassMapInterpolateV': r3.ɵɵclassMapInterpolateV,
'ɵɵstyling': r3.ɵɵstyling,
'ɵɵstyleMap': r3.ɵɵstyleMap,
'ɵɵstyleProp': r3.ɵɵstyleProp,
'ɵɵstylePropInterpolate1': r3.ɵɵstylePropInterpolate1,
@ -129,7 +128,6 @@ export const angularCoreEnv: {[name: string]: Function} =
'ɵɵstylePropInterpolate8': r3.ɵɵstylePropInterpolate8,
'ɵɵstylePropInterpolateV': r3.ɵɵstylePropInterpolateV,
'ɵɵstyleSanitizer': r3.ɵɵstyleSanitizer,
'ɵɵstylingApply': r3.ɵɵstylingApply,
'ɵɵclassProp': r3.ɵɵclassProp,
'ɵɵselect': r3.ɵɵselect,
'ɵɵadvance': r3.ɵɵadvance,

View File

@ -13,7 +13,7 @@ import {assertLViewOrUndefined} from './assert';
import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {TElementNode, TNode, TViewNode} from './interfaces/node';
import {CONTEXT, DECLARATION_VIEW, LView, OpaqueViewState, TVIEW} from './interfaces/view';
import {resetAllStylingState, resetStylingState} from './styling_next/state';
import {resetStylingState} from './styling_next/state';
/**
@ -129,31 +129,38 @@ export function getLView(): LView {
* The reason why this value is `1` instead of `0` is because the `0`
* value is reserved for the template.
*/
const MIN_DIRECTIVE_ID = 1;
let activeDirectiveId = MIN_DIRECTIVE_ID;
let activeDirectiveId = 0;
/**
* Position depth (with respect from leaf to root) in a directive sub-class inheritance chain.
* Flags used for an active element during change detection.
*
* These flags are used within other instructions to inform cleanup or
* exit operations to run when an element is being processed.
*
* Note that these flags are reset each time an element changes (whether it
* happens when `advance()` is run or when change detection exits out of a template
* function or when all host bindings are processed for an element).
*/
let activeDirectiveSuperClassDepthPosition = 0;
export const enum ActiveElementFlags {
Initial = 0b00,
RunExitFn = 0b01,
ResetStylesOnExit = 0b10,
Size = 2,
}
/**
* Total count of how many directives are a part of an inheritance chain.
*
* When directives are sub-classed (extended) from one to another, Angular
* needs to keep track of exactly how many were encountered so it can accurately
* generate the next directive id (once the next directive id is visited).
* Normally the next directive id just a single incremented value from the
* previous one, however, if the previous directive is a part of an inheritance
* chain (a series of sub-classed directives) then the incremented value must
* also take into account the total amount of sub-classed values.
*
* Note that this value resets back to zero once the next directive is
* visited (when `incrementActiveDirectiveId` or `setActiveHostElement`
* is called).
* Determines whether or not a flag is currently set for the active element.
*/
let activeDirectiveSuperClassHeight = 0;
export function hasActiveElementFlag(flag: ActiveElementFlags) {
return (_selectedIndex & flag) === flag;
}
/**
* Sets a flag is for the active element.
*/
export function setActiveElementFlag(flag: ActiveElementFlags) {
_selectedIndex |= flag;
}
/**
* Sets the active directive host element and resets the directive id value
@ -163,14 +170,44 @@ let activeDirectiveSuperClassHeight = 0;
* the directive/component instance lives
*/
export function setActiveHostElement(elementIndex: number | null = null) {
if (_selectedIndex !== elementIndex) {
if (getSelectedIndex() !== elementIndex) {
if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) {
executeElementExitFn();
}
if (hasActiveElementFlag(ActiveElementFlags.ResetStylesOnExit)) {
resetStylingState();
}
setSelectedIndex(elementIndex === null ? -1 : elementIndex);
activeDirectiveId = elementIndex === null ? 0 : MIN_DIRECTIVE_ID;
activeDirectiveSuperClassDepthPosition = 0;
activeDirectiveSuperClassHeight = 0;
activeDirectiveId = 0;
}
}
let _elementExitFn: Function|null = null;
export function executeElementExitFn() {
_elementExitFn !();
// TODO (matsko|misko): remove this unassignment once the state management of
// global variables are better managed.
_selectedIndex &= ~ActiveElementFlags.RunExitFn;
}
/**
* Queues a function to be run once the element is "exited" in CD.
*
* Change detection will focus on an element either when the `advance()`
* instruction is called or when the template or host bindings instruction
* code is invoked. The element is then "exited" when the next element is
* selected or when change detection for the template or host bindings is
* complete. When this occurs (the element change operation) then an exit
* function will be invoked if it has been set. This function can be used
* to assign that exit function.
*
* @param fn
*/
export function setElementExitFn(fn: Function): void {
setActiveElementFlag(ActiveElementFlags.RunExitFn);
_elementExitFn = fn;
}
/**
* Returns the current id value of the current directive.
*
@ -211,71 +248,12 @@ export function getActiveDirectiveId() {
* different set of directives).
*/
export function incrementActiveDirectiveId() {
activeDirectiveId += 1 + activeDirectiveSuperClassHeight;
// because we are dealing with a new directive this
// means we have exited out of the inheritance chain
activeDirectiveSuperClassDepthPosition = 0;
activeDirectiveSuperClassHeight = 0;
}
/**
* Set the current super class (reverse inheritance) position depth for a directive.
*
* For example we have two directives: Child and Other (but Child is a sub-class of Parent)
* <div child-dir other-dir></div>
*
* // increment
* parentInstance->hostBindings() (depth = 1)
* // decrement
* childInstance->hostBindings() (depth = 0)
* otherInstance->hostBindings() (depth = 0 b/c it's a different directive)
*
* Note that this is only active when `hostBinding` functions are being processed.
*/
export function adjustActiveDirectiveSuperClassDepthPosition(delta: number) {
activeDirectiveSuperClassDepthPosition += delta;
// we keep track of the height value so that when the next directive is visited
// then Angular knows to generate a new directive id value which has taken into
// account how many sub-class directives were a part of the previous directive.
activeDirectiveSuperClassHeight =
Math.max(activeDirectiveSuperClassHeight, activeDirectiveSuperClassDepthPosition);
}
/**
* Returns he current depth of the super/sub class inheritance chain.
*
* This will return how many inherited directive/component classes
* exist in the current chain.
*
* ```typescript
* @Directive({ selector: '[super-dir]' })
* class SuperDir {}
*
* @Directive({ selector: '[sub-dir]' })
* class SubDir extends SuperDir {}
*
* // if `<div sub-dir>` is used then the super class height is `1`
* // if `<div super-dir>` is used then the super class height is `0`
* ```
*/
export function getActiveDirectiveSuperClassHeight() {
return activeDirectiveSuperClassHeight;
}
/**
* Returns the current super class (reverse inheritance) depth for a directive.
*
* This is designed to help instruction code distinguish different hostBindings
* calls from each other when a directive has extended from another directive.
* Normally using the directive id value is enough, but with the case
* of parent/sub-class directive inheritance more information is required.
*
* Note that this is only active when `hostBinding` functions are being processed.
*/
export function getActiveDirectiveSuperClassDepth() {
return activeDirectiveSuperClassDepthPosition;
// Each directive gets a uniqueId value that is the same for both
// create and update calls when the hostBindings function is called. The
// directive uniqueId is not set anywhere--it is just incremented between
// each hostBindings call and is useful for helping instruction code
// uniquely determine which directive is currently active when executed.
activeDirectiveId += 1;
}
/**
@ -447,10 +425,10 @@ export function resetComponentState() {
elementDepthCount = 0;
bindingsEnabled = true;
setCurrentStyleSanitizer(null);
resetAllStylingState();
}
let _selectedIndex = -1;
/* tslint:disable */
let _selectedIndex = -1 << ActiveElementFlags.Size;
/**
* Gets the most recent index passed to {@link select}
@ -459,7 +437,7 @@ let _selectedIndex = -1;
* current `LView` to act on.
*/
export function getSelectedIndex() {
return _selectedIndex;
return _selectedIndex >> ActiveElementFlags.Size;
}
/**
@ -467,13 +445,12 @@ export function getSelectedIndex() {
*
* Used with {@link property} instruction (and more in the future) to identify the index in the
* current `LView` to act on.
*
* (Note that if an "exit function" was set earlier (via `setElementExitFn()`) then that will be
* run if and when the provided `index` value is different from the current selected index value.)
*/
export function setSelectedIndex(index: number) {
_selectedIndex = index;
// we have now jumped to another element
// therefore the state is stale
resetStylingState();
_selectedIndex = index << ActiveElementFlags.Size;
}

View File

@ -5,14 +5,14 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SafeValue} from '../../sanitization/bypass';
import {SafeValue, unwrapSafeValue} from '../../sanitization/bypass';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {NO_CHANGE} from '../tokens';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
import {BIT_MASK_START_VALUE, deleteStylingStateFromStorage, getStylingState, resetStylingState, storeStylingState} from './state';
import {allowStylingFlush, getBindingValue, getGuardMask, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getValuesCount, hasValueChanged, isContextLocked, isSanitizationRequired, isStylingValueDefined, lockContext, setGuardMask, stateIsPersisted} from './util';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
import {getStylingState, resetStylingState} from './state';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, getBindingValue, getConfig, getDefaultValue, getGuardMask, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from './util';
@ -35,13 +35,6 @@ import {allowStylingFlush, getBindingValue, getGuardMask, getMapProp, getMapValu
* --------
*/
// The first bit value reflects a map-based binding value's bit.
// The reason why it's always activated for every entry in the map
// is so that if any map-binding values update then all other prop
// based bindings will pass the guard check automatically without
// any extra code or flags.
export const DEFAULT_GUARD_MASK_VALUE = 0b1;
/**
* The guard/update mask bit index location for map-based bindings.
*
@ -49,26 +42,6 @@ export const DEFAULT_GUARD_MASK_VALUE = 0b1;
*/
const STYLING_INDEX_FOR_MAP_BINDING = 0;
/**
* Default fallback value for a styling binding.
*
* A value of `null` is used here which signals to the styling algorithm that
* the styling value is not present. This way if there are no other values
* detected then it will be removed once the style/class property is dirty and
* diffed within the styling algorithm present in `flushStyling`.
*/
const DEFAULT_BINDING_VALUE = null;
/**
* Default size count value for a new entry in a context.
*
* A value of `1` is used here because each entry in the context has a default
* property.
*/
const DEFAULT_SIZE_VALUE = 1;
let deferredBindingQueue: (TStylingContext | number | string | null | boolean)[] = [];
/**
* Visits a class-based binding and updates the new value (if changed).
*
@ -79,23 +52,25 @@ let deferredBindingQueue: (TStylingContext | number | string | null | boolean)[]
* state each time it's called (which then allows the `TStylingContext`
* and the bit mask values to be in sync).
*/
export function updateClassBinding(
context: TStylingContext, data: LStylingData, element: RElement, prop: string | null,
bindingIndex: number, value: boolean | string | null | undefined | StylingMapArray | NO_CHANGE,
deferRegistration: boolean, forceUpdate: boolean): boolean {
export function updateClassViaContext(
context: TStylingContext, data: LStylingData, element: RElement, directiveIndex: number,
prop: string | null, bindingIndex: number,
value: boolean | string | null | undefined | StylingMapArray | NO_CHANGE,
forceUpdate?: boolean): boolean {
const isMapBased = !prop;
const state = getStylingState(element, stateIsPersisted(context));
const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++;
const state = getStylingState(element, directiveIndex);
const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++;
if (value !== NO_CHANGE) {
const updated = updateBindingData(
context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate, false);
context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate,
false);
if (updated || forceUpdate) {
// We flip the bit in the bitMask to reflect that the binding
// at the `index` slot has changed. This identifies to the flushing
// phase that the bindings for this particular CSS class need to be
// applied again because on or more of the bindings for the CSS
// class have changed.
state.classesBitMask |= 1 << index;
state.classesBitMask |= 1 << countIndex;
return true;
}
}
@ -112,19 +87,20 @@ export function updateClassBinding(
* state each time it's called (which then allows the `TStylingContext`
* and the bit mask values to be in sync).
*/
export function updateStyleBinding(
context: TStylingContext, data: LStylingData, element: RElement, prop: string | null,
bindingIndex: number,
export function updateStyleViaContext(
context: TStylingContext, data: LStylingData, element: RElement, directiveIndex: number,
prop: string | null, bindingIndex: number,
value: string | number | SafeValue | null | undefined | StylingMapArray | NO_CHANGE,
sanitizer: StyleSanitizeFn | null, deferRegistration: boolean, forceUpdate: boolean): boolean {
sanitizer: StyleSanitizeFn | null, forceUpdate?: boolean): boolean {
const isMapBased = !prop;
const state = getStylingState(element, stateIsPersisted(context));
const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++;
const state = getStylingState(element, directiveIndex);
const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++;
if (value !== NO_CHANGE) {
const sanitizationRequired = isMapBased ||
const sanitizationRequired = isMapBased ?
true :
(sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false);
const updated = updateBindingData(
context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate,
context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate,
sanitizationRequired);
if (updated || forceUpdate) {
// We flip the bit in the bitMask to reflect that the binding
@ -132,7 +108,7 @@ export function updateStyleBinding(
// phase that the bindings for this particular property need to be
// applied again because on or more of the bindings for the CSS
// property have changed.
state.stylesBitMask |= 1 << index;
state.stylesBitMask |= 1 << countIndex;
return true;
}
}
@ -144,8 +120,6 @@ export function updateStyleBinding(
*
* This function is designed to be called from `updateStyleBinding` and `updateClassBinding`.
* If called during the first update pass, the binding will be registered in the context.
* If the binding does get registered and the `deferRegistration` flag is true then the
* binding data will be queued up until the context is later flushed in `applyStyling`.
*
* This function will also update binding slot in the provided `LStylingData` with the
* new binding entry (if it has changed).
@ -153,74 +127,92 @@ export function updateStyleBinding(
* @returns whether or not the binding value was updated in the `LStylingData`.
*/
function updateBindingData(
context: TStylingContext, data: LStylingData, counterIndex: number, prop: string | null,
bindingIndex: number,
value: string | SafeValue | number | boolean | null | undefined | StylingMapArray | NO_CHANGE,
deferRegistration: boolean, forceUpdate: boolean, sanitizationRequired: boolean): boolean {
if (!isContextLocked(context)) {
if (deferRegistration) {
deferBindingRegistration(context, counterIndex, prop, bindingIndex, sanitizationRequired);
} else {
deferredBindingQueue.length && flushDeferredBindings();
// this will only happen during the first update pass of the
// context. The reason why we can't use `tNode.firstTemplatePass`
// here is because its not guaranteed to be true when the first
// update pass is executed (remember that all styling instructions
// are run in the update phase, and, as a result, are no more
// styling instructions that are run in the creation phase).
registerBinding(context, counterIndex, prop, bindingIndex, sanitizationRequired);
}
context: TStylingContext, data: LStylingData, counterIndex: number, sourceIndex: number,
prop: string | null, bindingIndex: number,
value: string | SafeValue | number | boolean | null | undefined | StylingMapArray,
forceUpdate?: boolean, sanitizationRequired?: boolean): boolean {
const hostBindingsMode = isHostStylingActive(sourceIndex);
if (!isContextLocked(context, hostBindingsMode)) {
// this will only happen during the first update pass of the
// context. The reason why we can't use `tNode.firstTemplatePass`
// here is because its not guaranteed to be true when the first
// update pass is executed (remember that all styling instructions
// are run in the update phase, and, as a result, are no more
// styling instructions that are run in the creation phase).
registerBinding(context, counterIndex, sourceIndex, prop, bindingIndex, sanitizationRequired);
patchConfig(
context,
hostBindingsMode ? TStylingConfig.HasHostBindings : TStylingConfig.HasTemplateBindings);
patchConfig(context, prop ? TStylingConfig.HasPropBindings : TStylingConfig.HasMapBindings);
}
const changed = forceUpdate || hasValueChanged(data[bindingIndex], value);
if (changed) {
data[bindingIndex] = value;
setValue(data, bindingIndex, value);
const doSetValuesAsStale = (getConfig(context) & TStylingConfig.HasHostBindings) &&
!hostBindingsMode && (prop ? !value : true);
if (doSetValuesAsStale) {
renderHostBindingsAsStale(context, data, prop, !prop);
}
}
return changed;
}
/**
* Schedules a binding registration to be run at a later point.
* Iterates over all host-binding values for the given `prop` value in the context and sets their
* corresponding binding values to `null`.
*
* The reasoning for this feature is to ensure that styling
* bindings are registered in the correct order for when
* directives/components have a super/sub class inheritance
* chains. Each directive's styling bindings must be
* registered into the context in reverse order. Therefore all
* bindings will be buffered in reverse order and then applied
* after the inheritance chain exits.
* Whenever a template binding changes its value to `null`, all host-binding values should be
* re-applied
* to the element when the host bindings are evaluated. This may not always happen in the event
* that none of the bindings changed within the host bindings code. For this reason this function
* is expected to be called each time a template binding becomes falsy or when a map-based template
* binding changes.
*/
function deferBindingRegistration(
context: TStylingContext, counterIndex: number, prop: string | null, bindingIndex: number,
sanitizationRequired: boolean) {
deferredBindingQueue.unshift(context, counterIndex, prop, bindingIndex, sanitizationRequired);
}
function renderHostBindingsAsStale(
context: TStylingContext, data: LStylingData, prop: string | null, isMapBased: boolean): void {
const valuesCount = getValuesCount(context);
/**
* Flushes the collection of deferred bindings and causes each entry
* to be registered into the context.
*/
function flushDeferredBindings() {
let i = 0;
while (i < deferredBindingQueue.length) {
const context = deferredBindingQueue[i++] as TStylingContext;
const count = deferredBindingQueue[i++] as number;
const prop = deferredBindingQueue[i++] as string;
const bindingIndex = deferredBindingQueue[i++] as number | null;
const sanitizationRequired = deferredBindingQueue[i++] as boolean;
registerBinding(context, count, prop, bindingIndex, sanitizationRequired);
if (hasConfig(context, TStylingConfig.HasPropBindings)) {
const itemsPerRow = TStylingContextIndex.BindingsStartOffset + valuesCount;
let i = TStylingContextIndex.ValuesStartPosition;
while (i < context.length) {
if (getProp(context, i) === prop) {
break;
}
i += itemsPerRow;
}
const bindingsStart = i + TStylingContextIndex.BindingsStartOffset;
const valuesStart = bindingsStart + 1; // the first column is template bindings
const valuesEnd = bindingsStart + valuesCount - 1;
for (let i = valuesStart; i < valuesEnd; i++) {
const bindingIndex = context[i] as number;
if (bindingIndex !== 0) {
setValue(data, bindingIndex, null);
}
}
}
if (hasConfig(context, TStylingConfig.HasMapBindings)) {
const bindingsStart =
TStylingContextIndex.ValuesStartPosition + TStylingContextIndex.BindingsStartOffset;
const valuesStart = bindingsStart + 1; // the first column is template bindings
const valuesEnd = bindingsStart + valuesCount - 1;
for (let i = valuesStart; i < valuesEnd; i++) {
const stylingMap = getValue<StylingMapArray>(data, context[i] as number);
if (stylingMap) {
setMapAsDirty(stylingMap);
}
}
}
deferredBindingQueue.length = 0;
}
/**
* Registers the provided binding (prop + bindingIndex) into the context.
*
* This function is shared between bindings that are assigned immediately
* (via `updateBindingData`) and at a deferred stage. When called, it will
* figure out exactly where to place the binding data in the context.
*
* It is needed because it will either update or insert a styling property
* into the context at the correct spot.
*
@ -246,61 +238,80 @@ function flushDeferredBindings() {
* value.
*
* Note that this function is also used for map-based styling bindings. They are treated
* much the same as prop-based bindings, but, because they do not have a property value
* (since it's a map), all map-based entries are stored in an already populated area of
* the context at the top (which is reserved for map-based entries).
* much the same as prop-based bindings, but, their property name value is set as `[MAP]`.
*/
export function registerBinding(
context: TStylingContext, countId: number, prop: string | null,
bindingValue: number | null | string | boolean, sanitizationRequired?: boolean): boolean {
let registered = false;
if (prop) {
// prop-based bindings (e.g `<div [style.width]="w" [class.foo]="f">`)
let found = false;
let i = getPropValuesStartPosition(context);
while (i < context.length) {
const valuesCount = getValuesCount(context, i);
const p = getProp(context, i);
found = prop <= p;
if (found) {
// all style/class bindings are sorted by property name
if (prop < p) {
allocateNewContextEntry(context, i, prop, sanitizationRequired);
}
addBindingIntoContext(context, false, i, bindingValue, countId);
break;
}
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
}
context: TStylingContext, countId: number, sourceIndex: number, prop: string | null,
bindingValue: number | null | string | boolean, sanitizationRequired?: boolean): void {
let found = false;
prop = prop || MAP_BASED_ENTRY_PROP_NAME;
if (!found) {
allocateNewContextEntry(context, context.length, prop, sanitizationRequired);
addBindingIntoContext(context, false, i, bindingValue, countId);
registered = true;
}
} else {
// map-based bindings (e.g `<div [style]="s" [class]="{className:true}">`)
// there is no need to allocate the map-based binding region into the context
// since it is already there when the context is first created.
addBindingIntoContext(
context, true, TStylingContextIndex.MapBindingsPosition, bindingValue, countId);
registered = true;
let totalSources = getTotalSources(context);
// if a new source is detected then a new column needs to be allocated into
// the styling context. The column is basically a new allocation of binding
// sources that will be available to each property.
while (totalSources <= sourceIndex) {
addNewSourceColumn(context);
totalSources++;
}
const isBindingIndexValue = typeof bindingValue === 'number';
const entriesPerRow = TStylingContextIndex.BindingsStartOffset + getValuesCount(context);
let i = TStylingContextIndex.ValuesStartPosition;
// all style/class bindings are sorted by property name
while (i < context.length) {
const p = getProp(context, i);
if (prop <= p) {
if (prop < p) {
allocateNewContextEntry(context, i, prop, sanitizationRequired);
} else if (isBindingIndexValue) {
patchConfig(context, TStylingConfig.HasCollisions);
}
addBindingIntoContext(context, i, bindingValue, countId, sourceIndex);
found = true;
break;
}
i += entriesPerRow;
}
if (!found) {
allocateNewContextEntry(context, context.length, prop, sanitizationRequired);
addBindingIntoContext(context, i, bindingValue, countId, sourceIndex);
}
return registered;
}
/**
* Inserts a new row into the provided `TStylingContext` and assigns the provided `prop` value as
* the property entry.
*/
function allocateNewContextEntry(
context: TStylingContext, index: number, prop: string, sanitizationRequired?: boolean) {
// 1,2: splice index locations
// 3: each entry gets a config value (guard mask + flags)
// 4. each entry gets a size value (which is always one because there is always a default binding
// value)
// 5. the property that is getting allocated into the context
// 6. the default binding value (usually `null`)
context: TStylingContext, index: number, prop: string, sanitizationRequired?: boolean): void {
const config = sanitizationRequired ? TStylingContextPropConfigFlags.SanitizationRequired :
TStylingContextPropConfigFlags.Default;
context.splice(index, 0, config, DEFAULT_SIZE_VALUE, prop, DEFAULT_BINDING_VALUE);
setGuardMask(context, index, DEFAULT_GUARD_MASK_VALUE);
context.splice(
index, 0,
config, // 1) config value
DEFAULT_GUARD_MASK_VALUE, // 2) template bit mask
DEFAULT_GUARD_MASK_VALUE, // 3) host bindings bit mask
prop, // 4) prop value (e.g. `width`, `myClass`, etc...)
);
index += 4; // the 4 values above
// 5...) default binding index for the template value
// depending on how many sources already exist in the context,
// multiple default index entries may need to be inserted for
// the new value in the context.
const totalBindingsPerEntry = getTotalSources(context);
for (let i = 0; i < totalBindingsPerEntry; i++) {
context.splice(index, 0, DEFAULT_BINDING_INDEX);
index++;
}
// 6) default binding value for the new entry
context.splice(index, 0, DEFAULT_BINDING_VALUE);
}
/**
@ -316,75 +327,70 @@ function allocateNewContextEntry(
*
* - Otherwise the binding value will update the default value for the property
* and this will only happen if the default value is `null`.
*
* Note that this function also handles map-based bindings and will insert them
* at the top of the context.
*/
function addBindingIntoContext(
context: TStylingContext, isMapBased: boolean, index: number,
bindingValue: number | string | boolean | null, countId: number) {
const valuesCount = getValuesCount(context, index);
const firstValueIndex = index + TStylingContextIndex.BindingsStartOffset;
let lastValueIndex = firstValueIndex + valuesCount;
if (!isMapBased) {
// prop-based values all have default values, but map-based entries do not.
// we want to access the index for the default value in this case and not just
// the bindings...
lastValueIndex--;
}
context: TStylingContext, index: number, bindingValue: number | string | boolean | null,
bitIndex: number, sourceIndex: number) {
if (typeof bindingValue === 'number') {
// the loop here will check to see if the binding already exists
// for the property in the context. Why? The reason for this is
// because the styling context is not "locked" until the first
// flush has occurred. This means that if a repeated element
// registers its styling bindings then it will register each
// binding more than once (since its duplicated). This check
// will prevent that from happening. Note that this only happens
// when a binding is first encountered and not each time it is
// updated.
for (let i = firstValueIndex; i <= lastValueIndex; i++) {
const indexAtPosition = context[i];
if (indexAtPosition === bindingValue) return;
}
context.splice(lastValueIndex, 0, bindingValue);
(context[index + TStylingContextIndex.ValuesCountOffset] as number)++;
// now that a new binding index has been added to the property
// the guard mask bit value (at the `countId` position) needs
// to be included into the existing mask value.
const guardMask = getGuardMask(context, index) | (1 << countId);
setGuardMask(context, index, guardMask);
} else if (bindingValue !== null && context[lastValueIndex] == null) {
context[lastValueIndex] = bindingValue;
const hostBindingsMode = isHostStylingActive(sourceIndex);
const cellIndex = index + TStylingContextIndex.BindingsStartOffset + sourceIndex;
context[cellIndex] = bindingValue;
const updatedBitMask = getGuardMask(context, index, hostBindingsMode) | (1 << bitIndex);
setGuardMask(context, index, updatedBitMask, hostBindingsMode);
} else if (bindingValue !== null && getDefaultValue(context, index) === null) {
setDefaultValue(context, index, bindingValue);
}
}
/**
* Registers a new column into the provided `TStylingContext`.
*
* If and when a new source is detected then a new column needs to
* be allocated into the styling context. The column is basically
* a new allocation of binding sources that will be available to each
* property.
*
* Each column that exists in the styling context resembles a styling
* source. A styling source an either be the template or one or more
* components or directives all containing styling host bindings.
*/
function addNewSourceColumn(context: TStylingContext): void {
// we use -1 here because we want to insert right before the last value (the default value)
const insertOffset = TStylingContextIndex.BindingsStartOffset + getValuesCount(context) - 1;
let index = TStylingContextIndex.ValuesStartPosition;
while (index < context.length) {
index += insertOffset;
context.splice(index++, 0, DEFAULT_BINDING_INDEX);
// the value was inserted just before the default value, but the
// next entry in the context starts just after it. Therefore++.
index++;
}
context[TStylingContextIndex.TotalSourcesPosition]++;
}
/**
* Applies all pending style and class bindings to the provided element.
*
* This function will attempt to flush styling via the provided `classesContext`
* and `stylesContext` context values. This function is designed to be run from
* the `stylingApply()` instruction (which is run at the very end of styling
* change detection) and will rely on any state values that are set from when
* any styling bindings update.
* the internal `stylingApply` function (which is scheduled to run at the very
* end of change detection for an element if one or more style/class bindings
* were processed) and will rely on any state values that are set from when
* any of the styling bindings executed.
*
* This function may be called multiple times on the same element because it can
* be called from the template code as well as from host bindings. In order for
* styling to be successfully flushed to the element (which will only happen once
* despite this being called multiple times), the following criteria must be met:
* This function is designed to be called twice: one when change detection has
* processed an element within the template bindings (i.e. just as `advance()`
* is called) and when host bindings have been processed. In both cases the
* styles and classes in both contexts will be applied to the element, but the
* algorithm will selectively decide which bindings to run depending on the
* columns in the context. The provided `directiveIndex` value will help the
* algorithm determine which bindings to apply: either the template bindings or
* the host bindings (see `applyStylingToElement` for more information).
*
* - `flushStyling` is called from the very last directive that has styling for
* the element (see `allowStylingFlush()`).
* - one or more bindings for classes or styles has updated (this is checked by
* examining the classes or styles bit mask).
*
* If the style and class values are successfully applied to the element then
* the temporary state values for the element will be cleared. Otherwise, if
* this did not occur then the styling state is persisted (see `state.ts` for
* more information on how this works).
* Note that once this function is called all temporary styling state data
* (i.e. the `bitMask` and `counter` values for styles and classes will be cleared).
*/
export function flushStyling(
renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData,
@ -392,53 +398,28 @@ export function flushStyling(
element: RElement, directiveIndex: number, styleSanitizer: StyleSanitizeFn | null): void {
ngDevMode && ngDevMode.flushStyling++;
const persistState = classesContext ? stateIsPersisted(classesContext) :
(stylesContext ? stateIsPersisted(stylesContext) : false);
const allowFlushClasses = allowStylingFlush(classesContext, directiveIndex);
const allowFlushStyles = allowStylingFlush(stylesContext, directiveIndex);
const state = getStylingState(element, directiveIndex);
const hostBindingsMode = isHostStylingActive(state.sourceIndex);
// deferred bindings are bindings which are scheduled to register with
// the context at a later point. These bindings can only registered when
// the context will be 100% flushed to the element.
if (deferredBindingQueue.length && (allowFlushClasses || allowFlushStyles)) {
flushDeferredBindings();
}
const state = getStylingState(element, persistState);
const classesFlushed = maybeApplyStyling(
renderer, element, data, classesContext, allowFlushClasses, state.classesBitMask, setClass,
null);
const stylesFlushed = maybeApplyStyling(
renderer, element, data, stylesContext, allowFlushStyles, state.stylesBitMask, setStyle,
styleSanitizer);
if (classesFlushed && stylesFlushed) {
resetStylingState();
if (persistState) {
deleteStylingStateFromStorage(element);
if (stylesContext) {
if (!isContextLocked(stylesContext, hostBindingsMode)) {
lockAndFinalizeContext(stylesContext, hostBindingsMode);
}
} else if (persistState) {
storeStylingState(element, state);
applyStylingViaContext(
stylesContext, renderer, element, data, state.stylesBitMask, setStyle, styleSanitizer,
hostBindingsMode);
}
}
function maybeApplyStyling(
renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, data: LStylingData,
context: TStylingContext | null, allowFlush: boolean, bitMask: number,
styleSetter: ApplyStylingFn, styleSanitizer: any | null): boolean {
if (allowFlush && context) {
lockAndFinalizeContext(context);
if (contextHasUpdates(context, bitMask)) {
ngDevMode && (styleSanitizer ? ngDevMode.stylesApplied++ : ngDevMode.classesApplied++);
applyStyling(context !, renderer, element, data, bitMask, styleSetter, styleSanitizer);
return true;
if (classesContext) {
if (!isContextLocked(classesContext, hostBindingsMode)) {
lockAndFinalizeContext(classesContext, hostBindingsMode);
}
applyStylingViaContext(
classesContext, renderer, element, data, state.classesBitMask, setClass, null,
hostBindingsMode);
}
return allowFlush;
}
function contextHasUpdates(context: TStylingContext | null, bitMask: number) {
return context && bitMask > BIT_MASK_START_VALUE;
resetStylingState();
}
/**
@ -462,14 +443,50 @@ function contextHasUpdates(context: TStylingContext | null, bitMask: number) {
* be updated each time a host binding applies its static styling values (via `elementHostAttrs`)
* so these values are only read at this point because this is the very last point before the
* first style/class values are flushed to the element.
*
* Note that the `TStylingContext` styling context contains two locks: one for template bindings
* and another for host bindings. Either one of these locks will be set when styling is applied
* during the template binding flush and/or during the host bindings flush.
*/
function lockAndFinalizeContext(context: TStylingContext): void {
if (!isContextLocked(context)) {
const initialValues = getStylingMapArray(context);
if (initialValues) {
updateInitialStylingOnContext(context, initialValues);
function lockAndFinalizeContext(context: TStylingContext, hostBindingsMode: boolean): void {
const initialValues = getStylingMapArray(context) !;
updateInitialStylingOnContext(context, initialValues);
lockContext(context, hostBindingsMode);
}
/**
* Registers all initial styling entries into the provided context.
*
* This function will iterate over all entries in the provided `initialStyling` ar}ray and register
* them as default (initial) values in the provided context. Initial styling values in a context are
* the default values that are to be applied unless overwritten by a binding.
*
* The reason why this function exists and isn't a part of the context construction is because
* host binding is evaluated at a later stage after the element is created. This means that
* if a directive or component contains any initial styling code (i.e. `<div class="foo">`)
* then that initial styling data can only be applied once the styling for that element
* is first applied (at the end of the update phase). Once that happens then the context will
* update itself with the complete initial styling for the element.
*/
function updateInitialStylingOnContext(
context: TStylingContext, initialStyling: StylingMapArray): void {
// `-1` is used here because all initial styling data is not a apart
// of a binding (since it's static)
const COUNT_ID_FOR_STYLING = -1;
let hasInitialStyling = false;
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < initialStyling.length;
i += StylingMapArrayIndex.TupleSize) {
const value = getMapValue(initialStyling, i);
if (value) {
const prop = getMapProp(initialStyling, i);
registerBinding(context, COUNT_ID_FOR_STYLING, 0, prop, value, false);
hasInitialStyling = true;
}
lockContext(context);
}
if (hasInitialStyling) {
patchConfig(context, TStylingConfig.HasInitialStyling);
}
}
@ -497,60 +514,74 @@ function lockAndFinalizeContext(context: TStylingContext): void {
* algorithm works for map-based styling bindings.
*
* Note that this function is not designed to be called in isolation (use
* `applyClasses` and `applyStyles` to actually apply styling values).
* the `flushStyling` function so that it can call this function for both
* the styles and classes contexts).
*/
export function applyStyling(
export function applyStylingViaContext(
context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
bindingData: LStylingData, bitMaskValue: number | boolean, applyStylingFn: ApplyStylingFn,
sanitizer: StyleSanitizeFn | null) {
sanitizer: StyleSanitizeFn | null, hostBindingsMode: boolean): void {
const bitMask = normalizeBitMaskValue(bitMaskValue);
const stylingMapsSyncFn = getStylingMapsSyncFn();
const mapsGuardMask = getGuardMask(context, TStylingContextIndex.MapBindingsPosition);
const applyAllValues = (bitMask & mapsGuardMask) > 0;
const mapsMode =
let stylingMapsSyncFn: SyncStylingMapsFn|null = null;
let applyAllValues = false;
if (hasConfig(context, TStylingConfig.HasMapBindings)) {
stylingMapsSyncFn = getStylingMapsSyncFn();
const mapsGuardMask =
getGuardMask(context, TStylingContextIndex.ValuesStartPosition, hostBindingsMode);
applyAllValues = (bitMask & mapsGuardMask) !== 0;
}
const valuesCount = getValuesCount(context);
let totalBindingsToVisit = 1;
let mapsMode =
applyAllValues ? StylingMapsSyncMode.ApplyAllValues : StylingMapsSyncMode.TraverseValues;
if (hostBindingsMode) {
mapsMode |= StylingMapsSyncMode.RecurseInnerMaps;
totalBindingsToVisit = valuesCount - 1;
}
let i = getPropValuesStartPosition(context);
while (i < context.length) {
const valuesCount = getValuesCount(context, i);
const guardMask = getGuardMask(context, i);
const guardMask = getGuardMask(context, i, hostBindingsMode);
if (bitMask & guardMask) {
let valueApplied = false;
const prop = getProp(context, i);
const valuesCountUpToDefault = valuesCount - 1;
const defaultValue = getBindingValue(context, i, valuesCountUpToDefault) as string | null;
const defaultValue = getDefaultValue(context, i);
// case 1: apply prop-based values
// try to apply the binding values and see if a non-null
// value gets set for the styling binding
for (let j = 0; j < valuesCountUpToDefault; j++) {
// Part 1: Visit the `[styling.prop]` value
for (let j = 0; j < totalBindingsToVisit; j++) {
const bindingIndex = getBindingValue(context, i, j) as number;
const value = bindingData[bindingIndex];
if (isStylingValueDefined(value)) {
const finalValue = sanitizer && isSanitizationRequired(context, i) ?
sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) :
value;
applyStylingFn(renderer, element, prop, finalValue, bindingIndex);
valueApplied = true;
break;
if (!valueApplied && bindingIndex !== 0) {
const value = getValue(bindingData, bindingIndex);
if (isStylingValueDefined(value)) {
const checkValueOnly = hostBindingsMode && j === 0;
if (!checkValueOnly) {
const finalValue = sanitizer && isSanitizationRequired(context, i) ?
sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) :
unwrapSafeValue(value);
applyStylingFn(renderer, element, prop, finalValue, bindingIndex);
}
valueApplied = true;
}
}
// Part 2: Visit the `[style]` or `[class]` map-based value
if (stylingMapsSyncFn) {
// determine whether or not to apply the target property or to skip it
let mode = mapsMode | (valueApplied ? StylingMapsSyncMode.SkipTargetProp :
StylingMapsSyncMode.ApplyTargetProp);
if (hostBindingsMode && j === 0) {
mode |= StylingMapsSyncMode.CheckValuesOnly;
}
const valueAppliedWithinMap = stylingMapsSyncFn(
context, renderer, element, bindingData, j, applyStylingFn, sanitizer, mode, prop,
defaultValue);
valueApplied = valueApplied || valueAppliedWithinMap;
}
}
// case 2: apply map-based values
// traverse through each map-based styling binding and update all values up to
// the provided `prop` value. If the property was not applied in the loop above
// then it will be attempted to be applied in the maps sync code below.
if (stylingMapsSyncFn) {
// determine whether or not to apply the target property or to skip it
const mode = mapsMode | (valueApplied ? StylingMapsSyncMode.SkipTargetProp :
StylingMapsSyncMode.ApplyTargetProp);
const valueAppliedWithinMap = stylingMapsSyncFn(
context, renderer, element, bindingData, applyStylingFn, sanitizer, mode, prop,
defaultValue);
valueApplied = valueApplied || valueAppliedWithinMap;
}
// case 3: apply the default value
// Part 3: apply the default value (e.g. `<div style="width:200">` => `200px` gets applied)
// if the value has not yet been applied then a truthy value does not exist in the
// prop-based or map-based bindings code. If and when this happens, just apply the
// default value (even if the default value is `null`).
@ -566,10 +597,92 @@ export function applyStyling(
// values. For this reason, one more call to the sync function
// needs to be issued at the end.
if (stylingMapsSyncFn) {
stylingMapsSyncFn(context, renderer, element, bindingData, applyStylingFn, sanitizer, mapsMode);
if (hostBindingsMode) {
mapsMode |= StylingMapsSyncMode.CheckValuesOnly;
}
stylingMapsSyncFn(
context, renderer, element, bindingData, 0, applyStylingFn, sanitizer, mapsMode);
}
}
/**
* Applies the provided styling map to the element directly (without context resolution).
*
* This function is designed to be run from the styling instructions and will be called
* automatically. This function is intended to be used for performance reasons in the
* event that there is no need to apply styling via context resolution.
*
* See `allowDirectStylingApply`.
*
* @returns whether or not the styling map was applied to the element.
*/
export function applyStylingMapDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, map: StylingMapArray, applyFn: ApplyStylingFn,
sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean): boolean {
if (forceUpdate || hasValueChanged(data[bindingIndex], map)) {
setValue(data, bindingIndex, map);
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i);
applyStylingValue(renderer, context, element, prop, value, applyFn, bindingIndex, sanitizer);
}
return true;
}
return false;
}
/**
* Applies the provided styling prop/value to the element directly (without context resolution).
*
* This function is designed to be run from the styling instructions and will be called
* automatically. This function is intended to be used for performance reasons in the
* event that there is no need to apply styling via context resolution.
*
* See `allowDirectStylingApply`.
*
* @returns whether or not the prop/value styling was applied to the element.
*/
export function applyStylingValueDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, prop: string, value: any, applyFn: ApplyStylingFn,
sanitizer?: StyleSanitizeFn | null): boolean {
if (hasValueChanged(data[bindingIndex], value)) {
setValue(data, bindingIndex, value);
applyStylingValue(renderer, context, element, prop, value, applyFn, bindingIndex, sanitizer);
return true;
}
return false;
}
function applyStylingValue(
renderer: any, context: TStylingContext, element: RElement, prop: string, value: any,
applyFn: ApplyStylingFn, bindingIndex: number, sanitizer?: StyleSanitizeFn | null) {
let valueToApply: string|null = unwrapSafeValue(value);
if (isStylingValueDefined(valueToApply)) {
valueToApply =
sanitizer ? sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) : valueToApply;
} else if (hasConfig(context, TStylingConfig.HasInitialStyling)) {
const initialStyles = getStylingMapArray(context);
if (initialStyles) {
valueToApply = findInitialStylingValue(initialStyles, prop);
}
}
applyFn(renderer, element, prop, valueToApply, bindingIndex);
}
function findInitialStylingValue(map: StylingMapArray, prop: string): string|null {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const p = getMapProp(map, i);
if (p >= prop) {
return p === prop ? getMapValue(map, i) : null;
}
}
return null;
}
function normalizeBitMaskValue(value: number | boolean): number {
// if pass => apply all values (-1 implies that all bits are flipped to true)
if (value === true) return -1;
@ -593,7 +706,7 @@ export function setStylingMapsSyncFn(fn: SyncStylingMapsFn) {
/**
* Assigns a style value to a style property for the given element.
*/
const setStyle: ApplyStylingFn =
export const setStyle: ApplyStylingFn =
(renderer: Renderer3 | null, native: RElement, prop: string, value: string | null) => {
// the reason why this may be `null` is either because
// it's a container element or it's a part of a test
@ -620,7 +733,7 @@ const setStyle: ApplyStylingFn =
/**
* Adds/removes the provided className value to the provided element.
*/
const setClass: ApplyStylingFn =
export const setClass: ApplyStylingFn =
(renderer: Renderer3 | null, native: RElement, className: string, value: any) => {
if (className !== '') {
// the reason why this may be `null` is either because
@ -666,33 +779,3 @@ export function renderStylingMap(
}
}
}
/**
* Registers all initial styling entries into the provided context.
*
* This function will iterate over all entries in the provided `initialStyling` ar}ray and register
* them as default (initial) values in the provided context. Initial styling values in a context are
* the default values that are to be applied unless overwritten by a binding.
*
* The reason why this function exists and isn't a part of the context construction is because
* host binding is evaluated at a later stage after the element is created. This means that
* if a directive or component contains any initial styling code (i.e. `<div class="foo">`)
* 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);
}
}
}

View File

@ -10,17 +10,17 @@ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {setInputsForProperty} from '../instructions/shared';
import {AttributeMarker, TAttributes, TNode, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {BINDING_INDEX, LView, RENDERER, TVIEW} from '../interfaces/view';
import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getCurrentStyleSanitizer, getLView, getPreviousOrParentTNode, getSelectedIndex, setCurrentStyleSanitizer} from '../state';
import {BINDING_INDEX, LView, RENDERER} from '../interfaces/view';
import {ActiveElementFlags, getActiveDirectiveId, getCurrentStyleSanitizer, getLView, getSelectedIndex, setActiveElementFlag, setCurrentStyleSanitizer, setElementExitFn} from '../state';
import {NO_CHANGE} from '../tokens';
import {renderStringify} from '../util/misc_utils';
import {getNativeByTNode, getTNode} from '../util/view_utils';
import {flushStyling, updateClassBinding, updateStyleBinding} from './bindings';
import {applyStylingMapDirectly, applyStylingValueDirectly, flushStyling, setClass, setStyle, updateClassViaContext, updateStyleViaContext} from './bindings';
import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from './interfaces';
import {activateStylingMapFeature, addItemToStylingMap, normalizeIntoStylingMap, stylingMapToString} from './map_based_bindings';
import {activateStylingMapFeature} from './map_based_bindings';
import {attachStylingDebugObject} from './styling_debug';
import {allocTStylingContext, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isStylingContext, updateLastDirectiveIndex as _updateLastDirectiveIndex} from './util';
import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isHostStylingActive, isStylingContext, normalizeIntoStylingMap, setValue, stylingMapToString} from './util';
@ -34,34 +34,13 @@ import {allocTStylingContext, concatString, forceClassesAsString, forceStylesAsS
* --------
*/
/**
* Temporary function to bridge styling functionality between this new
* refactor (which is here inside of `styling_next/`) and the old
* implementation (which lives inside of `styling/`).
*
* This function is executed during the creation block of an element.
* Because the existing styling implementation issues a call to the
* `styling()` instruction, this instruction will also get run. The
* central idea here is that the directive index values are bound
* into the context. The directive index is temporary and is only
* required until the `select(n)` instruction is fully functional.
*
* @codeGenApi
*/
export function ɵɵstyling() {
const tView = getLView()[TVIEW];
if (tView.firstTemplatePass) {
updateLastDirectiveIndex(getPreviousOrParentTNode(), getActiveDirectiveStylingIndex());
}
}
/**
* Sets the current style sanitizer function which will then be used
* within all follow-up prop and map-based style binding instructions
* for the given element.
*
* Note that once styling has been applied to the element (i.e. once
* `select(n)` is executed or the hostBindings/template function exits)
* `advance(n)` is executed or the hostBindings/template function exits)
* then the active `sanitizerFn` will be set to `null`. This means that
* once styling is applied to another element then a another call to
* `styleSanitizer` will need to be made.
@ -92,7 +71,7 @@ export function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void {
* be ignored.
*
* Note that this will apply the provided style value to the host element if this function is called
* within a host binding.
* within a host binding function.
*
* @codeGenApi
*/
@ -101,9 +80,15 @@ export function ɵɵstyleProp(
stylePropInternal(getSelectedIndex(), prop, value, suffix);
}
/**
* Internal function for applying a single style to an element.
*
* The reason why this function has been separated from `ɵɵstyleProp` is because
* it is also called from `ɵɵstylePropInterpolate`.
*/
export function stylePropInternal(
elementIndex: number, prop: string, value: string | number | SafeValue | null,
suffix?: string | null | undefined) {
suffix?: string | null | undefined): void {
const lView = getLView();
// if a value is interpolated then it may render a `NO_CHANGE` value.
@ -112,9 +97,8 @@ export function stylePropInternal(
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
const updated = _stylingProp(
elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false,
deferStylingUpdate());
const updated =
stylingProp(elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false);
if (ngDevMode) {
ngDevMode.styleProp++;
if (updated) {
@ -134,7 +118,7 @@ export function stylePropInternal(
* @param value A true/false value which will turn the class on or off.
*
* Note that this will apply the provided class value to the host element if this function
* is called within a host binding.
* is called within a host binding function.
*
* @codeGenApi
*/
@ -147,8 +131,7 @@ export function ɵɵclassProp(className: string, value: boolean | null): void {
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
const updated =
_stylingProp(getSelectedIndex(), bindingIndex, className, value, true, deferStylingUpdate());
const updated = stylingProp(getSelectedIndex(), bindingIndex, className, value, true);
if (ngDevMode) {
ngDevMode.classProp++;
if (updated) {
@ -159,28 +142,57 @@ export function ɵɵclassProp(className: string, value: boolean | null): void {
/**
* Shared function used to update a prop-based styling binding for an element.
*
* Depending on the state of the `tNode.styles` styles context, the style/prop
* value may be applied directly to the element instead of being processed
* through the context. The reason why this occurs is for performance and fully
* depends on the state of the context (i.e. whether or not there are duplicate
* bindings or whether or not there are map-based bindings and property bindings
* present together).
*/
function _stylingProp(
function stylingProp(
elementIndex: number, bindingIndex: number, prop: string,
value: boolean | number | SafeValue | string | null | undefined | NO_CHANGE,
isClassBased: boolean, defer: boolean): boolean {
isClassBased: boolean): boolean {
let updated = false;
const lView = getLView();
const tNode = getTNode(elementIndex, lView);
const native = getNativeByTNode(tNode, lView) as RElement;
let valueHasChanged = false;
if (isClassBased) {
valueHasChanged = updateClassBinding(
getClassesContext(tNode), lView, native, prop, bindingIndex,
value as string | boolean | null, defer, false);
const hostBindingsMode = isHostStyling();
const context = isClassBased ? getClassesContext(tNode) : getStylesContext(tNode);
const sanitizer = isClassBased ? null : getCurrentStyleSanitizer();
// Direct Apply Case: bypass context resolution and apply the
// style/class value directly to the element
if (allowDirectStyling(context, hostBindingsMode)) {
const renderer = getRenderer(tNode, lView);
updated = applyStylingValueDirectly(
renderer, context, native, lView, bindingIndex, prop, value,
isClassBased ? setClass : setStyle, sanitizer);
} else {
const sanitizer = getCurrentStyleSanitizer();
valueHasChanged = updateStyleBinding(
getStylesContext(tNode), lView, native, prop, bindingIndex,
value as string | SafeValue | null, sanitizer, defer, false);
// Context Resolution (or first update) Case: save the value
// and defer to the context to flush and apply the style/class binding
// value to the element.
const directiveIndex = getActiveDirectiveId();
if (isClassBased) {
updated = updateClassViaContext(
context, lView, native, directiveIndex, prop, bindingIndex,
value as string | boolean | null);
} else {
updated = updateStyleViaContext(
context, lView, native, directiveIndex, prop, bindingIndex,
value as string | SafeValue | null, sanitizer);
}
if (updated) {
setElementExitFn(applyStyling);
}
markStylingStateAsDirty();
}
return valueHasChanged;
return updated;
}
/**
@ -207,7 +219,6 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu
const lView = getLView();
const tNode = getTNode(index, lView);
const context = getStylesContext(tNode);
const directiveIndex = getActiveDirectiveStylingIndex();
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
@ -218,12 +229,12 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu
// inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function)
if (!directiveIndex && hasStyleInput(tNode) && styles !== NO_CHANGE) {
if (!isHostStyling() && hasStyleInput(tNode) && styles !== NO_CHANGE) {
updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false);
styles = NO_CHANGE;
}
const updated = _stylingMap(index, context, bindingIndex, styles, false, deferStylingUpdate());
const updated = _stylingMap(index, context, bindingIndex, styles, false);
if (ngDevMode) {
ngDevMode.styleMap++;
if (updated) {
@ -254,12 +265,17 @@ export function ɵɵclassMap(classes: {[className: string]: any} | NO_CHANGE | s
classMapInternal(getSelectedIndex(), classes);
}
/**
* Internal function for applying a class string or key/value map of classes to an element.
*
* The reason why this function has been separated from `ɵɵclassMap` is because
* it is also called from `ɵɵclassMapInterpolate`.
*/
export function classMapInternal(
elementIndex: number, classes: {[className: string]: any} | NO_CHANGE | string | null) {
elementIndex: number, classes: {[className: string]: any} | NO_CHANGE | string | null): void {
const lView = getLView();
const tNode = getTNode(elementIndex, lView);
const context = getClassesContext(tNode);
const directiveIndex = getActiveDirectiveStylingIndex();
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
@ -270,13 +286,12 @@ export function classMapInternal(
// inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function)
if (!directiveIndex && hasClassInput(tNode) && classes !== NO_CHANGE) {
if (!isHostStyling() && hasClassInput(tNode) && classes !== NO_CHANGE) {
updateDirectiveInputValue(context, lView, tNode, bindingIndex, classes, true);
classes = NO_CHANGE;
}
const updated =
_stylingMap(elementIndex, context, bindingIndex, classes, true, deferStylingUpdate());
const updated = _stylingMap(elementIndex, context, bindingIndex, classes, true);
if (ngDevMode) {
ngDevMode.classMap++;
if (updated) {
@ -293,27 +308,52 @@ export function classMapInternal(
*/
function _stylingMap(
elementIndex: number, context: TStylingContext, bindingIndex: number,
value: {[key: string]: any} | string | null, isClassBased: boolean, defer: boolean) {
activateStylingMapFeature();
const lView = getLView();
value: {[key: string]: any} | string | null, isClassBased: boolean): boolean {
let updated = false;
const lView = getLView();
const directiveIndex = getActiveDirectiveId();
const tNode = getTNode(elementIndex, lView);
const native = getNativeByTNode(tNode, lView) as RElement;
const oldValue = lView[bindingIndex];
const oldValue = lView[bindingIndex] as StylingMapArray | null;
const hostBindingsMode = isHostStyling();
const sanitizer = getCurrentStyleSanitizer();
const valueHasChanged = hasValueChanged(oldValue, value);
const stylingMapArr =
value === NO_CHANGE ? NO_CHANGE : normalizeIntoStylingMap(oldValue, value, !isClassBased);
if (isClassBased) {
updateClassBinding(
context, lView, native, null, bindingIndex, stylingMapArr, defer, valueHasChanged);
// Direct Apply Case: bypass context resolution and apply the
// style/class map values directly to the element
if (allowDirectStyling(context, hostBindingsMode)) {
const renderer = getRenderer(tNode, lView);
updated = applyStylingMapDirectly(
renderer, context, native, lView, bindingIndex, stylingMapArr as StylingMapArray,
isClassBased ? setClass : setStyle, sanitizer, valueHasChanged);
} else {
const sanitizer = getCurrentStyleSanitizer();
updateStyleBinding(
context, lView, native, null, bindingIndex, stylingMapArr, sanitizer, defer,
valueHasChanged);
// Context Resolution (or first update) Case: save the map value
// and defer to the context to flush and apply the style/class binding
// value to the element.
if (isClassBased) {
updateClassViaContext(
context, lView, native, directiveIndex, null, bindingIndex, stylingMapArr,
valueHasChanged);
} else {
updateStyleViaContext(
context, lView, native, directiveIndex, null, bindingIndex, stylingMapArr, sanitizer,
valueHasChanged);
}
if (valueHasChanged) {
updated = true;
setElementExitFn(applyStyling);
}
activateStylingMapFeature();
}
return valueHasChanged;
markStylingStateAsDirty();
return updated;
}
/**
@ -338,13 +378,15 @@ function updateDirectiveInputValue(
// even if the value has changed we may not want to emit it to the
// directive input(s) in the event that it is falsy during the
// first update pass.
if (newValue || isContextLocked(context)) {
const inputs = tNode.inputs ![isClassBased ? 'class' : 'style'] !;
if (newValue || isContextLocked(context, false)) {
const inputName = isClassBased ? 'class' : 'style';
const inputs = tNode.inputs ![inputName] !;
const initialValue = getInitialStylingValue(context);
const value = normalizeStylingDirectiveInputValue(initialValue, newValue, isClassBased);
setInputsForProperty(lView, inputs, value);
}
lView[bindingIndex] = newValue;
setValue(lView, bindingIndex, newValue);
setElementExitFn(applyStyling);
}
}
@ -362,7 +404,7 @@ function normalizeStylingDirectiveInputValue(
// we only concat values if there is an initial value, otherwise we return the value as is.
// Note that this is to satisfy backwards-compatibility in Angular.
if (initialValue.length > 0) {
if (initialValue.length) {
if (isClassBased) {
value = concatString(initialValue, forceClassesAsString(bindingValue));
} else {
@ -377,24 +419,21 @@ function normalizeStylingDirectiveInputValue(
/**
* Flushes all styling code to the element.
*
* This function is designed to be called from the template and hostBindings
* functions and may be called multiple times depending whether multiple
* sources of styling exist. If called multiple times, only the last call
* to `stlyingApply()` will render styling to the element.
*
* @codeGenApi
* This function is designed to be scheduled from any of the four styling instructions
* in this file. When called it will flush all style and class bindings to the element
* via the context resolution algorithm.
*/
export function ɵɵstylingApply() {
function applyStyling(): void {
const elementIndex = getSelectedIndex();
const lView = getLView();
const tNode = getTNode(elementIndex, lView);
const renderer = getRenderer(tNode, lView);
const native = getNativeByTNode(tNode, lView) as RElement;
const directiveIndex = getActiveDirectiveStylingIndex();
const sanitizer = getCurrentStyleSanitizer();
const classesContext = isStylingContext(tNode.classes) ? tNode.classes as TStylingContext : null;
const stylesContext = isStylingContext(tNode.styles) ? tNode.styles as TStylingContext : null;
flushStyling(
renderer, lView, getClassesContext(tNode), getStylesContext(tNode), native, directiveIndex,
sanitizer);
renderer, lView, classesContext, stylesContext, native, getActiveDirectiveId(), sanitizer);
setCurrentStyleSanitizer(null);
}
@ -417,12 +456,12 @@ export function registerInitialStylingOnTNode(
if (typeof attr == 'number') {
mode = attr;
} else if (mode == AttributeMarker.Classes) {
classes = classes || [''];
classes = classes || allocStylingMapArray();
addItemToStylingMap(classes, attr, true);
hasAdditionalInitialStyling = true;
} else if (mode == AttributeMarker.Styles) {
const value = attrs[++i] as string | null;
styles = styles || [''];
styles = styles || allocStylingMapArray();
addItemToStylingMap(styles, attr, value);
hasAdditionalInitialStyling = true;
}
@ -450,33 +489,6 @@ function updateRawValueOnContext(context: TStylingContext | StylingMapArray, val
stylingMapArr[StylingMapArrayIndex.RawValuePosition] = value;
}
export function getActiveDirectiveStylingIndex(): number {
// whenever a directive's hostBindings function is called a uniqueId value
// is assigned. Normally this is enough to help distinguish one directive
// from another for the styling context, but there are situations where a
// sub-class directive could inherit and assign styling in concert with a
// parent directive. To help the styling code distinguish between a parent
// sub-classed directive the inheritance depth is taken into account as well.
return getActiveDirectiveId() + getActiveDirectiveSuperClassDepth();
}
/**
* Temporary function that will update the max directive index value in
* both the classes and styles contexts present on the provided `tNode`.
*
* This code is only used because the `select(n)` code functionality is not
* yet 100% functional. The `select(n)` instruction cannot yet evaluate host
* bindings function code in sync with the associated template function code.
* For this reason the styling algorithm needs to track the last directive index
* value so that it knows exactly when to render styling to the element since
* `stylingApply()` is called multiple times per CD (`stylingApply` will be
* removed once `select(n)` is fixed).
*/
function updateLastDirectiveIndex(tNode: TNode, directiveIndex: number) {
_updateLastDirectiveIndex(getClassesContext(tNode), directiveIndex);
_updateLastDirectiveIndex(getStylesContext(tNode), directiveIndex);
}
function getStylesContext(tNode: TNode): TStylingContext {
return getContext(tNode, false);
}
@ -488,10 +500,10 @@ function getClassesContext(tNode: TNode): TStylingContext {
/**
* Returns/instantiates a styling context from/to a `tNode` instance.
*/
function getContext(tNode: TNode, isClassBased: boolean) {
function getContext(tNode: TNode, isClassBased: boolean): TStylingContext {
let context = isClassBased ? tNode.classes : tNode.styles;
if (!isStylingContext(context)) {
context = allocTStylingContext(context);
context = allocTStylingContext(context as StylingMapArray | null);
if (ngDevMode) {
attachStylingDebugObject(context as TStylingContext);
}
@ -526,18 +538,14 @@ function resolveStylePropValue(
return resolvedValue;
}
/**
* Whether or not a style/class binding update should be applied later.
*
* This function will decide whether a binding should be applied immediately
* or later (just before the styles/classes are flushed to the element). The
* reason why this feature exists is because of super/sub directive inheritance.
* Angular will evaluate host bindings on the super directive first and the sub
* directive, but the styling bindings on the sub directive are of higher priority
* than the super directive. For this reason all styling bindings that take place
* in this circumstance will need to be deferred until later so that they can be
* applied together and in a different order (the algorithm handles that part).
*/
function deferStylingUpdate(): boolean {
return getActiveDirectiveSuperClassHeight() > 0;
function markStylingStateAsDirty(): void {
setActiveElementFlag(ActiveElementFlags.ResetStylesOnExit);
}
/**
* Whether or not the style/class binding being applied was executed within a host bindings
* function.
*/
function isHostStyling(): boolean {
return isHostStylingActive(getActiveDirectiveId());
}

View File

@ -26,8 +26,7 @@ import {LView} from '../interfaces/view';
* The `TStylingContext` unites all template styling bindings (i.e.
* `[class]` and `[style]` bindings) as well as all host-level
* styling bindings (for components and directives) together into
* a single manifest. It is used each time there are one or more
* styling bindings present for an element.
* a single manifest
*
* The styling context is stored on a `TNode` on and there are
* two instances of it: one for classes and another for styles.
@ -37,6 +36,10 @@ import {LView} from '../interfaces/view';
* tNode.classes = [ ... a context only for classes ... ];
* ```
*
* The styling context is created each time there are one or more
* styling bindings (style or class bindings) present for an element,
* but is only created once per `TNode`.
*
* `tNode.styles` and `tNode.classes` can be an instance of the following:
*
* ```typescript
@ -62,36 +65,48 @@ import {LView} from '../interfaces/view';
* storing actual styling binding values, the lView binding index values
* are stored within the context. (static nature means it is more compact.)
*
* The code below shows a breakdown of two instances of `TStylingContext`
* (one for `tNode.styles` and another for `tNode.classes`):
*
* ```typescript
* // <div [class.active]="c" // lView binding index = 20
* // [style.width]="x" // lView binding index = 21
* // [style.height]="y"> // lView binding index = 22
* tNode.stylesContext = [
* [], // initial values array
* 0, // the context config value
* // ...
* // </div>
* tNode.styles = [
* 0, // the context config value (see `TStylingContextConfig`)
* 1, // the total amount of sources present (only `1` b/c there are only template
* bindings)
* [null], // initial values array (an instance of `StylingMapArray`)
*
* 0b001, // guard mask for width
* 2, // total entries for width
* 'width', // the property name
* 21, // the binding location for the "x" binding in the lView
* null,
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b010, // template guard mask for height
* 0, // host bindings guard mask for height
* 'height', // the property name
* 22, // the binding location for the "y" binding in the lView
* null, // the default value for height
*
* 0b010, // guard mask for height
* 2, // total entries for height
* 'height', // the property name
* 22, // the binding location for the "y" binding in the lView
* null,
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b001, // template guard mask for width
* 0, // host bindings guard mask for width
* 'width', // the property name
* 21, // the binding location for the "x" binding in the lView
* null, // the default value for width
* ];
*
* tNode.classesContext = [
* [], // initial values array
* 0, // the context config value
* tNode.classes = [
* 0, // the context config value (see `TStylingContextConfig`)
* 1, // the total amount of sources present (only `1` b/c there are only template
* bindings)
* [null], // initial values array (an instance of `StylingMapArray`)
*
* 0b001, // guard mask for active
* 2, // total entries for active
* 'active', // the property name
* 20, // the binding location for the "c" binding in the lView
* null,
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b001, // template guard mask for width
* 0, // host bindings guard mask for width
* 'active', // the property name
* 20, // the binding location for the "c" binding in the lView
* null, // the default value for the `active` class
* ];
* ```
*
@ -100,19 +115,25 @@ import {LView} from '../interfaces/view';
*
* ```typescript
* context = [
* CONFIG, // the styling context config value
* //...
* guardMask,
* totalEntries,
* configValue,
* templateGuardMask,
* hostBindingsGuardMask,
* propName,
* bindingIndices...,
* ...bindingIndices...,
* defaultValue
* //...
* ];
* ```
*
* Below is a breakdown of each value:
*
* - **guardMask**:
* - **configValue**:
* Property-specific configuration values. The only config setting
* that is implemented right now is whether or not to sanitize the
* value.
*
* - **templateGuardMask**:
* A numeric value where each bit represents a binding index
* location. Each binding index location is assigned based on
* a local counter value that increments each time an instruction
@ -136,18 +157,23 @@ import {LView} from '../interfaces/view';
* efficient way to flip all bits on the mask when a special kind
* of caching scenario occurs or when there are more than 32 bindings).
*
* - **totalEntries**:
* Each property present in the contains various binding sources of
* where the styling data could come from. This includes template
* level bindings, directive/component host bindings as well as the
* default value (or static value) all writing to the same property.
* This value depicts how many binding source entries exist for the
* property.
* - **hostBindingsGuardMask**:
* Another instance of a guard mask that is specific to host bindings.
* This behaves exactly the same way as does the `templateGuardMask`,
* but will not contain any binding information processed in the template.
* The reason why there are two instances of guard masks (one for the
* template and another for host bindings) is because the template bindings
* are processed before host bindings and the state information is not
* carried over into the host bindings code. As soon as host bindings are
* processed for an element the counter and state-based bit mask values are
* set to `0`.
*
* The reason why the totalEntries value is needed is because the
* styling context is dynamic in size and it's not possible
* for the flushing or update algorithms to know when and where
* a property starts and ends without it.
* ```
* <div [style.width]="x" // binding index = 21 (counter index = 0)
* [style.height]="y" // binding index = 22 (counter index = 1)
* dir-that-sets-width // binding index = 30 (counter index = 0)
* dir-that-sets-width> // binding index = 31 (counter index = 1)
* ```
*
* - **propName**:
* The CSS property name or class name (e.g `width` or `active`).
@ -165,15 +191,20 @@ import {LView} from '../interfaces/view';
* - **defaultValue**:
* This is the default that will always be applied to the element if
* and when all other binding sources return a result that is null.
* Usually this value is null but it can also be a static value that
* Usually this value is `null` but it can also be a static value that
* is intercepted when the tNode is first constructured (e.g.
* `<div style="width:200px">` has a default value of `200px` for
* the `width` property).
*
* Each time a new binding is encountered it is registered into the
* context. The context then is continually updated until the first
* styling apply call has been called (this is triggered by the
* `stylingApply()` instruction for the active element).
* styling apply call has been called (which is automatically scheduled
* to be called once an element exits during change detection). Note that
* each entry in the context is stored in alphabetical order.
*
* Once styling has been flushed for the first time for an element the
* context will set as locked (this prevents bindings from being added
* to the context later on).
*
* # How Styles/Classes are Rendered
* Each time a styling instruction (e.g. `[class.name]`, `[style.prop]`,
@ -190,15 +221,24 @@ import {LView} from '../interfaces/view';
* function updateStyleProp(prop: string, value: string) {
* const lView = getLView();
* const bindingIndex = BINDING_INDEX++;
* const indexForStyle = localStylesCounter++;
*
* // update the local counter value
* const indexForStyle = stylingState.stylesCount++;
* if (lView[bindingIndex] !== value) {
* lView[bindingIndex] = value;
* localBitMaskForStyles |= 1 << indexForStyle;
*
* // tell the local state that we have updated a style value
* // by updating the bit mask
* stylingState.bitMaskForStyles |= 1 << indexForStyle;
* }
* }
* ```
*
* ## The Apply Algorithm
* Once all the bindings have updated a `bitMask` value will be populated.
* This `bitMask` value is used in the apply algorithm (which is called
* context resolution).
*
* ## The Apply Algorithm (Context Resolution)
* As explained above, each time a binding updates its value, the resulting
* value is stored in the `lView` array. These styling values have yet to
* be flushed to the element.
@ -283,94 +323,147 @@ import {LView} from '../interfaces/view';
*/
export interface TStylingContext extends
Array<number|string|number|boolean|null|StylingMapArray|{}> {
/** Configuration data for the context */
[TStylingContextIndex.ConfigPosition]: TStylingConfig;
/** The total amount of sources present in the context */
[TStylingContextIndex.TotalSourcesPosition]: number;
/** Initial value position for static styles */
[TStylingContextIndex.InitialStylingValuePosition]: StylingMapArray;
/** Configuration data for the context */
[TStylingContextIndex.ConfigPosition]: TStylingConfigFlags;
/** Temporary value used to track directive index entries until
the old styling code is fully removed. The reason why this
is required is to figure out which directive is last and,
when encountered, trigger a styling flush to happen */
[TStylingContextIndex.LastDirectiveIndexPosition]: number;
/** The bit guard value for all map-based bindings on an element */
[TStylingContextIndex.MapBindingsBitGuardPosition]: number;
/** The total amount of map-based bindings present on an element */
[TStylingContextIndex.MapBindingsValuesCountPosition]: number;
/** The prop value for map-based bindings (there actually isn't a
* value at all, but this is just used in the context to avoid
* having any special code to update the binding information for
* map-based entries). */
[TStylingContextIndex.MapBindingsPropPosition]: string;
}
/**
* A series of flags used to configure the config value present within a
* `TStylingContext` value.
* A series of flags used to configure the config value present within an instance of
* `TStylingContext`.
*/
export const enum TStylingConfigFlags {
export const enum TStylingConfig {
/**
* The initial state of the styling context config
* The initial state of the styling context config.
*/
Initial = 0b0,
Initial = 0b0000000,
/**
* A flag which marks the context as being locked.
* Whether or not there are prop-based bindings present.
*
* The styling context is constructed across an element template
* function as well as any associated hostBindings functions. When
* this occurs, the context itself is open to mutation and only once
* it has been flushed once then it will be locked for good (no extra
* bindings can be added to it).
* Examples include:
* 1. `<div [style.prop]="x">`
* 2. `<div [class.prop]="x">`
* 3. `@HostBinding('style.prop') x`
* 4. `@HostBinding('class.prop') x`
*/
Locked = 0b1,
HasPropBindings = 0b0000001,
/**
* Whether or not to store the state between updates in a global storage map.
* Whether or not there are map-based bindings present.
*
* This flag helps the algorithm avoid storing all state values temporarily in
* a storage map (that lives in `state.ts`). The flag is only flipped to true if
* and when an element contains style/class bindings that exist both on the
* template-level as well as within host bindings on the same element. This is a
* rare case, and a storage map is required so that the state values can be restored
* between the template code running and the host binding code executing.
* Examples include:
* 1. `<div [style]="x">`
* 2. `<div [class]="x">`
* 3. `@HostBinding('style') x`
* 4. `@HostBinding('class') x`
*/
PersistStateValues = 0b10,
HasMapBindings = 0b0000010,
/**
* Whether or not there are map-based and prop-based bindings present.
*
* Examples include:
* 1. `<div [style]="x" [style.prop]="y">`
* 2. `<div [class]="x" [style.prop]="y">`
* 3. `<div [style]="x" dir-that-sets-some-prop>`
* 4. `<div [class]="x" dir-that-sets-some-class>`
*/
HasPropAndMapBindings = 0b0000011,
/**
* Whether or not there are two or more sources for a single property in the context.
*
* Examples include:
* 1. prop + prop: `<div [style.width]="x" dir-that-sets-width>`
* 2. map + prop: `<div [style]="x" [style.prop]>`
* 3. map + map: `<div [style]="x" dir-that-sets-style>`
*/
HasCollisions = 0b0000100,
/**
* Whether or not the context contains initial styling values.
*
* Examples include:
* 1. `<div style="width:200px">`
* 2. `<div class="one two three">`
* 3. `@Directive({ host: { 'style': 'width:200px' } })`
* 4. `@Directive({ host: { 'class': 'one two three' } })`
*/
HasInitialStyling = 0b00001000,
/**
* Whether or not the context contains one or more template bindings.
*
* Examples include:
* 1. `<div [style]="x">`
* 2. `<div [style.width]="x">`
* 3. `<div [class]="x">`
* 4. `<div [class.name]="x">`
*/
HasTemplateBindings = 0b00010000,
/**
* Whether or not the context contains one or more host bindings.
*
* Examples include:
* 1. `@HostBinding('style') x`
* 2. `@HostBinding('style.width') x`
* 3. `@HostBinding('class') x`
* 4. `@HostBinding('class.name') x`
*/
HasHostBindings = 0b00100000,
/**
* Whether or not the template bindings are allowed to be registered in the context.
*
* This flag is after one or more template-based style/class bindings were
* set and processed for an element. Once the bindings are processed then a call
* to stylingApply is issued and the lock will be put into place.
*
* Note that this is only set once.
*/
TemplateBindingsLocked = 0b01000000,
/**
* Whether or not the host bindings are allowed to be registered in the context.
*
* This flag is after one or more host-based style/class bindings were
* set and processed for an element. Once the bindings are processed then a call
* to stylingApply is issued and the lock will be put into place.
*
* Note that this is only set once.
*/
HostBindingsLocked = 0b10000000,
/** A Mask of all the configurations */
Mask = 0b11,
Mask = 0b11111111,
/** Total amount of configuration bits used */
TotalBits = 2,
TotalBits = 8,
}
/**
* An index of position and offset values used to natigate the `TStylingContext`.
* An index of position and offset values used to navigate the `TStylingContext`.
*/
export const enum TStylingContextIndex {
InitialStylingValuePosition = 0,
ConfigPosition = 1,
LastDirectiveIndexPosition = 2,
// index/offset values for map-based entries (i.e. `[style]`
// and `[class]` bindings).
MapBindingsPosition = 3,
MapBindingsBitGuardPosition = 3,
MapBindingsValuesCountPosition = 4,
MapBindingsPropPosition = 5,
MapBindingsBindingsStartPosition = 6,
ConfigPosition = 0,
TotalSourcesPosition = 1,
InitialStylingValuePosition = 2,
ValuesStartPosition = 3,
// each tuple entry in the context
// (mask, count, prop, ...bindings||default-value)
ConfigAndGuardOffset = 0,
ValuesCountOffset = 1,
PropOffset = 2,
BindingsStartOffset = 3,
MinTupleLength = 4,
// (config, templateBitGuard, hostBindingBitGuard, prop, ...bindings||default-value)
ConfigOffset = 0,
TemplateBitGuardOffset = 1,
HostBindingsBitGuardOffset = 2,
PropOffset = 3,
BindingsStartOffset = 4
}
/**
@ -387,8 +480,8 @@ export const enum TStylingContextPropConfigFlags {
* A function used to apply or remove styling from an element for a given property.
*/
export interface ApplyStylingFn {
(renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string,
value: string|null, bindingIndex?: number|null): void;
(renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string, value: any,
bindingIndex?: number|null): void;
}
/**
@ -419,12 +512,12 @@ export interface StylingMapArray extends Array<{}|string|number|null> {
* An index of position and offset points for any data stored within a `StylingMapArray` instance.
*/
export const enum StylingMapArrayIndex {
/** The location of the raw key/value map instance used last to populate the array entries */
RawValuePosition = 0,
/** Where the values start in the array */
ValuesStartPosition = 1,
/** The location of the raw key/value map instance used last to populate the array entries */
RawValuePosition = 0,
/** The size of each property/value entry */
TupleSize = 2,
@ -458,8 +551,9 @@ export const enum StylingMapArrayIndex {
*/
export interface SyncStylingMapsFn {
(context: TStylingContext, renderer: Renderer3|ProceduralRenderer3|null, element: RElement,
data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn|null,
mode: StylingMapsSyncMode, targetProp?: string|null, defaultValue?: string|null): boolean;
data: LStylingData, sourceIndex: number, applyStylingFn: ApplyStylingFn,
sanitizer: StyleSanitizeFn|null, mode: StylingMapsSyncMode, targetProp?: string|null,
defaultValue?: boolean|string|null): boolean;
}
/**
@ -477,4 +571,10 @@ export const enum StylingMapsSyncMode {
/** Skip applying the target prop/value entry */
SkipTargetProp = 0b100,
/** Iterate over inner maps map values in the context */
RecurseInnerMaps = 0b1000,
/** Only check to see if a value was set somewhere in each map */
CheckValuesOnly = 0b10000,
}

View File

@ -5,12 +5,13 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {unwrapSafeValue} from '../../sanitization/bypass';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer';
import {setStylingMapsSyncFn} from './bindings';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from './interfaces';
import {concatString, getBindingValue, getMapProp, getMapValue, getValuesCount, hyphenate, isStylingValueDefined, setMapValue} from './util';
import {getBindingValue, getMapProp, getMapValue, getValue, getValuesCount, isStylingValueDefined} from './util';
@ -23,6 +24,13 @@ import {concatString, getBindingValue, getMapProp, getMapValue, getValuesCount,
* --------
*/
/**
* Enables support for map-based styling bindings (e.g. `[style]` and `[class]` bindings).
*/
export function activateStylingMapFeature() {
setStylingMapsSyncFn(syncStylingMap);
}
/**
* Used to apply styling values presently within any map-based bindings on an element.
*
@ -53,7 +61,7 @@ import {concatString, getBindingValue, getMapProp, getMapValue, getValuesCount,
* value is marked as dirty.
*
* Styling values are applied once CD exits the element (which happens when
* the `select(n)` instruction is called or the template function exits). When
* the `advance(n)` instruction is called or the template function exits). When
* this occurs, all prop-based bindings are applied. If a map-based binding is
* present then a special flushing function (called a sync function) is made
* available and it will be called each time a styling property is flushed.
@ -105,14 +113,14 @@ import {concatString, getBindingValue, getMapProp, getMapValue, getValuesCount,
*/
export const syncStylingMap: SyncStylingMapsFn =
(context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn | null,
mode: StylingMapsSyncMode, targetProp?: string | null,
defaultValue?: string | null): boolean => {
data: LStylingData, sourceIndex: number, applyStylingFn: ApplyStylingFn,
sanitizer: StyleSanitizeFn | null, mode: StylingMapsSyncMode, targetProp?: string | null,
defaultValue?: string | boolean | null): boolean => {
let targetPropValueWasApplied = false;
// once the map-based styling code is activate it is never deactivated. For this reason a
// check to see if the current styling context has any map based bindings is required.
const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition);
const totalMaps = getValuesCount(context);
if (totalMaps) {
let runTheSyncAlgorithm = true;
const loopUntilEnd = !targetProp;
@ -121,7 +129,7 @@ export const syncStylingMap: SyncStylingMapsFn =
// hasn't been flagged to apply values (it only traverses values) then
// there is no point in iterating over the array because nothing will
// be applied to the element.
if (loopUntilEnd && (mode & ~StylingMapsSyncMode.ApplyAllValues)) {
if (loopUntilEnd && (mode & StylingMapsSyncMode.ApplyAllValues) === 0) {
runTheSyncAlgorithm = false;
targetPropValueWasApplied = true;
}
@ -129,7 +137,7 @@ export const syncStylingMap: SyncStylingMapsFn =
if (runTheSyncAlgorithm) {
targetPropValueWasApplied = innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp || null,
0, defaultValue || null);
sourceIndex, defaultValue || null);
}
if (loopUntilEnd) {
@ -153,83 +161,104 @@ function innerSyncStylingMap(
context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn | null,
mode: StylingMapsSyncMode, targetProp: string | null, currentMapIndex: number,
defaultValue: string | null): boolean {
defaultValue: string | boolean | null): boolean {
const totalMaps = getValuesCount(context) - 1; // maps have no default value
const mapsLimit = totalMaps - 1;
const recurseInnerMaps =
currentMapIndex < mapsLimit && (mode & StylingMapsSyncMode.RecurseInnerMaps) !== 0;
const checkValuesOnly = (mode & StylingMapsSyncMode.CheckValuesOnly) !== 0;
if (checkValuesOnly) {
// inner modes do not check values ever (that can only happen
// when sourceIndex === 0)
mode &= ~StylingMapsSyncMode.CheckValuesOnly;
}
let targetPropValueWasApplied = false;
const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition);
if (currentMapIndex < totalMaps) {
const bindingIndex = getBindingValue(
context, TStylingContextIndex.MapBindingsPosition, currentMapIndex) as number;
const stylingMapArr = data[bindingIndex] as StylingMapArray;
if (currentMapIndex <= mapsLimit) {
let cursor = getCurrentSyncCursor(currentMapIndex);
while (cursor < stylingMapArr.length) {
const prop = getMapProp(stylingMapArr, cursor);
const iteratedTooFar = targetProp && prop > targetProp;
const isTargetPropMatched = !iteratedTooFar && prop === targetProp;
const value = getMapValue(stylingMapArr, cursor);
const valueIsDefined = isStylingValueDefined(value);
const bindingIndex = getBindingValue(
context, TStylingContextIndex.ValuesStartPosition, currentMapIndex) as number;
const stylingMapArr = getValue<StylingMapArray>(data, bindingIndex);
// the recursive code is designed to keep applying until
// it reaches or goes past the target prop. If and when
// this happens then it will stop processing values, but
// all other map values must also catch up to the same
// point. This is why a recursive call is still issued
// even if the code has iterated too far.
const innerMode =
iteratedTooFar ? mode : resolveInnerMapMode(mode, valueIsDefined, isTargetPropMatched);
const innerProp = iteratedTooFar ? targetProp : prop;
let valueApplied = innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, sanitizer, innerMode, innerProp,
currentMapIndex + 1, defaultValue);
if (stylingMapArr) {
while (cursor < stylingMapArr.length) {
const prop = getMapProp(stylingMapArr, cursor);
const iteratedTooFar = targetProp && prop > targetProp;
const isTargetPropMatched = !iteratedTooFar && prop === targetProp;
const value = getMapValue(stylingMapArr, cursor);
const valueIsDefined = isStylingValueDefined(value);
if (iteratedTooFar) {
if (!targetPropValueWasApplied) {
targetPropValueWasApplied = valueApplied;
// the recursive code is designed to keep applying until
// it reaches or goes past the target prop. If and when
// this happens then it will stop processing values, but
// all other map values must also catch up to the same
// point. This is why a recursive call is still issued
// even if the code has iterated too far.
const innerMode =
iteratedTooFar ? mode : resolveInnerMapMode(mode, valueIsDefined, isTargetPropMatched);
const innerProp = iteratedTooFar ? targetProp : prop;
let valueApplied = recurseInnerMaps ?
innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, sanitizer, innerMode, innerProp,
currentMapIndex + 1, defaultValue) :
false;
if (iteratedTooFar) {
if (!targetPropValueWasApplied) {
targetPropValueWasApplied = valueApplied;
}
break;
}
break;
if (!valueApplied && isValueAllowedToBeApplied(mode, isTargetPropMatched)) {
valueApplied = true;
if (!checkValuesOnly) {
const useDefault = isTargetPropMatched && !valueIsDefined;
const bindingIndexToApply = isTargetPropMatched ? bindingIndex : null;
let finalValue: any;
if (useDefault) {
finalValue = defaultValue;
} else {
finalValue = sanitizer ?
sanitizer(prop, value, StyleSanitizeMode.ValidateAndSanitize) :
(value ? unwrapSafeValue(value) : null);
}
applyStylingFn(renderer, element, prop, finalValue, bindingIndexToApply);
}
}
targetPropValueWasApplied = valueApplied && isTargetPropMatched;
cursor += StylingMapArrayIndex.TupleSize;
}
setCurrentSyncCursor(currentMapIndex, cursor);
if (!valueApplied && isValueAllowedToBeApplied(mode, isTargetPropMatched)) {
const useDefault = isTargetPropMatched && !valueIsDefined;
const valueToApply = useDefault ? defaultValue : value;
const bindingIndexToApply = useDefault ? bindingIndex : null;
const finalValue = sanitizer ?
sanitizer(prop, valueToApply, StyleSanitizeMode.ValidateAndSanitize) :
valueToApply;
applyStylingFn(renderer, element, prop, finalValue, bindingIndexToApply);
valueApplied = true;
// this is a fallback case in the event that the styling map is `null` for this
// binding but there are other map-based bindings that need to be evaluated
// afterwards. If the `prop` value is falsy then the intention is to cycle
// through all of the properties in the remaining maps as well. If the current
// styling map is too short then there are no values to iterate over. In either
// case the follow-up maps need to be iterated over.
if (recurseInnerMaps &&
(stylingMapArr.length === StylingMapArrayIndex.ValuesStartPosition || !targetProp)) {
targetPropValueWasApplied = innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp,
currentMapIndex + 1, defaultValue);
}
targetPropValueWasApplied = valueApplied && isTargetPropMatched;
cursor += StylingMapArrayIndex.TupleSize;
}
setCurrentSyncCursor(currentMapIndex, cursor);
// this is a fallback case in the event that the styling map is `null` for this
// binding but there are other map-based bindings that need to be evaluated
// afterwards. If the `prop` value is falsy then the intention is to cycle
// through all of the properties in the remaining maps as well. If the current
// styling map is too short then there are no values to iterate over. In either
// case the follow-up maps need to be iterated over.
if (stylingMapArr.length === StylingMapArrayIndex.ValuesStartPosition || !targetProp) {
return innerSyncStylingMap(
} else if (recurseInnerMaps) {
targetPropValueWasApplied = innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp,
currentMapIndex + 1, defaultValue);
}
}
return targetPropValueWasApplied;
}
/**
* Enables support for map-based styling bindings (e.g. `[style]` and `[class]` bindings).
*/
export function activateStylingMapFeature() {
setStylingMapsSyncFn(syncStylingMap);
}
/**
* Used to determine the mode for the inner recursive call.
*
@ -276,8 +305,8 @@ function resolveInnerMapMode(
* - But do not allow if the current prop is set to be skipped.
* 2. Otherwise if the current prop is permitted then allow.
*/
function isValueAllowedToBeApplied(mode: number, isTargetPropMatched: boolean) {
let doApplyValue = (mode & StylingMapsSyncMode.ApplyAllValues) > 0;
function isValueAllowedToBeApplied(mode: StylingMapsSyncMode, isTargetPropMatched: boolean) {
let doApplyValue = (mode & StylingMapsSyncMode.ApplyAllValues) !== 0;
if (!doApplyValue) {
if (mode & StylingMapsSyncMode.ApplyTargetProp) {
doApplyValue = isTargetPropMatched;
@ -320,126 +349,3 @@ function getCurrentSyncCursor(mapIndex: number) {
function setCurrentSyncCursor(mapIndex: number, indexValue: number) {
MAP_CURSORS[mapIndex] = indexValue;
}
/**
* Used to convert a {key:value} map into a `StylingMapArray` array.
*
* This function will either generate a new `StylingMapArray` instance
* or it will patch the provided `newValues` map value into an
* existing `StylingMapArray` value (this only happens if `bindingValue`
* is an instance of `StylingMapArray`).
*
* If a new key/value map is provided with an old `StylingMapArray`
* value then all properties will be overwritten with their new
* values or with `null`. This means that the array will never
* shrink in size (but it will also not be created and thrown
* away whenever the {key:value} map entries change).
*/
export function normalizeIntoStylingMap(
bindingValue: null | StylingMapArray,
newValues: {[key: string]: any} | string | null | undefined,
normalizeProps?: boolean): StylingMapArray {
const stylingMapArr: StylingMapArray = Array.isArray(bindingValue) ? bindingValue : [null];
stylingMapArr[StylingMapArrayIndex.RawValuePosition] = newValues || null;
// because the new values may not include all the properties
// that the old ones had, all values are set to `null` before
// the new values are applied. This way, when flushed, the
// styling algorithm knows exactly what style/class values
// to remove from the element (since they are `null`).
for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length;
j += StylingMapArrayIndex.TupleSize) {
setMapValue(stylingMapArr, j, null);
}
let props: string[]|null = null;
let map: {[key: string]: any}|undefined|null;
let allValuesTrue = false;
if (typeof newValues === 'string') { // [class] bindings allow string values
if (newValues.length) {
props = newValues.split(/\s+/);
allValuesTrue = true;
}
} else {
props = newValues ? Object.keys(newValues) : null;
map = newValues;
}
if (props) {
for (let i = 0; i < props.length; i++) {
const prop = props[i] as string;
const newProp = normalizeProps ? hyphenate(prop) : prop;
const value = allValuesTrue ? true : map ![prop];
addItemToStylingMap(stylingMapArr, newProp, value, true);
}
}
return stylingMapArr;
}
/**
* Inserts the provided item into the provided styling array at the right spot.
*
* The `StylingMapArray` type is a sorted key/value array of entries. This means
* that when a new entry is inserted it must be placed at the right spot in the
* array. This function figures out exactly where to place it.
*/
export function addItemToStylingMap(
stylingMapArr: StylingMapArray, prop: string, value: string | boolean | null,
allowOverwrite?: boolean) {
for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length;
j += StylingMapArrayIndex.TupleSize) {
const propAtIndex = getMapProp(stylingMapArr, j);
if (prop <= propAtIndex) {
let applied = false;
if (propAtIndex === prop) {
const valueAtIndex = stylingMapArr[j];
if (allowOverwrite || !isStylingValueDefined(valueAtIndex)) {
applied = true;
setMapValue(stylingMapArr, j, value);
}
} else {
applied = true;
stylingMapArr.splice(j, 0, prop, value);
}
return applied;
}
}
stylingMapArr.push(prop, value);
return true;
}
/**
* Converts the provided styling map array into a string.
*
* Classes => `one two three`
* Styles => `prop:value; prop2:value2`
*/
export function stylingMapToString(map: StylingMapArray, isClassBased: boolean): string {
let str = '';
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i) as string;
const attrValue = concatString(prop, isClassBased ? '' : value, ':');
str = concatString(str, attrValue, isClassBased ? ' ' : '; ');
}
return str;
}
/**
* Converts the provided styling map array into a key value map.
*/
export function stylingMapToStringMap(map: StylingMapArray | null): {[key: string]: any} {
let stringMap: {[key: string]: any} = {};
if (map) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i) as string;
stringMap[prop] = value;
}
}
return stringMap;
}

View File

@ -5,12 +5,12 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {RElement} from '../interfaces/renderer';
import {TEMPLATE_DIRECTIVE_INDEX} from './util';
/**
* --------
*
* // TODO(matsko): add updateMask info
*
* This file contains all state-based logic for styling in Angular.
*
* Styling in Angular is evaluated with a series of styling-specific
@ -23,79 +23,92 @@
* exited in change detection (once all the instructions are run for
* that element).
*
* There are, however, situations where the state-based values
* need to be stored and used at a later point. This ONLY occurs when
* there are template-level as well as host-binding-level styling
* instructions on the same element. The example below shows exactly
* what could be:
*
* ```html
* <!-- two sources of styling: the template and the directive -->
* <div [style.width]="width" dir-that-sets-height></div>
* ```
*
* If and when this situation occurs, the current styling state is
* stored in a storage map value and then later accessed once the
* host bindings are evaluated. Once styling for the current element
* is over then the map entry will be cleared.
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
let _stylingState: StylingState|null = null;
const _stateStorage = new Map<any, StylingState>();
// this value is not used outside this file and is only here
// as a caching check for when the element changes.
let _stylingElement: any = null;
/**
* Used as a state reference for update values between style/class binding instructions.
*
* In addition to storing the element and bit-mask related values, the state also
* stores the `sourceIndex` value. The `sourceIndex` value is an incremented value
* that identifies what "source" (i.e. the template, a specific directive by index or
* component) is currently applying its styling bindings to the element.
*/
export interface StylingState {
/** The element that is currently being processed */
element: RElement|null;
/** The directive index that is currently active (`0` === template) */
directiveIndex: number;
/** The source (column) index that is currently active (`0` === template) */
sourceIndex: number;
/** The classes update bit mask value that is processed during each class binding */
classesBitMask: number;
/** The classes update bit index value that is processed during each class binding */
classesIndex: number;
/** The styles update bit mask value that is processed during each style binding */
stylesBitMask: number;
/** The styles update bit index value that is processed during each style binding */
stylesIndex: number;
}
export const STYLING_INDEX_START_VALUE = 1;
export const BIT_MASK_START_VALUE = 0;
// these values will get filled in the very first time this is accessed...
const _state: StylingState = {
element: null,
directiveIndex: -1,
sourceIndex: -1,
classesBitMask: -1,
classesIndex: -1,
stylesBitMask: -1,
stylesIndex: -1,
};
export function getStylingState(element: any, readFromMap?: boolean): StylingState {
if (!_stylingElement || element !== _stylingElement) {
_stylingElement = element;
if (readFromMap) {
_stylingState = _stateStorage.get(element) || null;
ngDevMode && ngDevMode.stylingReadPersistedState++;
}
_stylingState = _stylingState || {
classesBitMask: BIT_MASK_START_VALUE,
classesIndex: STYLING_INDEX_START_VALUE,
stylesBitMask: BIT_MASK_START_VALUE,
stylesIndex: STYLING_INDEX_START_VALUE,
};
const BIT_MASK_START_VALUE = 0;
// the `0` start value is reserved for [map]-based entries
const INDEX_START_VALUE = 1;
/**
* Returns (or instantiates) the styling state for the given element.
*
* Styling state is accessed and processed each time a style or class binding
* is evaluated.
*
* If and when the provided `element` doesn't match the current element in the
* state then this means that styling was recently cleared or the element has
* changed in change detection. In both cases the styling state is fully reset.
*
* If and when the provided `directiveIndex` doesn't match the current directive
* index in the state then this means that a new source has introduced itself into
* the styling code (or, in other words, another directive or component has started
* to apply its styling host bindings to the element).
*/
export function getStylingState(element: RElement, directiveIndex: number): StylingState {
if (_state.element !== element) {
_state.element = element;
_state.directiveIndex = directiveIndex;
_state.sourceIndex = directiveIndex === TEMPLATE_DIRECTIVE_INDEX ? 0 : 1;
_state.classesBitMask = BIT_MASK_START_VALUE;
_state.classesIndex = INDEX_START_VALUE;
_state.stylesBitMask = BIT_MASK_START_VALUE;
_state.stylesIndex = INDEX_START_VALUE;
} else if (_state.directiveIndex !== directiveIndex) {
_state.directiveIndex = directiveIndex;
_state.sourceIndex++;
}
return _stylingState !;
return _state;
}
/**
* Clears the styling state so that it can be used by another element's styling code.
*/
export function resetStylingState() {
_stylingState = null;
_stylingElement = null;
}
export function storeStylingState(element: any, state: StylingState) {
ngDevMode && ngDevMode.stylingWritePersistedState++;
_stateStorage.set(element, state);
}
export function deleteStylingStateFromStorage(element: any) {
_stateStorage.delete(element);
}
export function resetAllStylingState() {
resetStylingState();
_stateStorage.clear();
_state.element = null;
}

View File

@ -7,14 +7,13 @@
*/
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {RElement} from '../interfaces/renderer';
import {LView} from '../interfaces/view';
import {getCurrentStyleSanitizer} from '../state';
import {attachDebugObject} from '../util/debug_utils';
import {applyStyling} from './bindings';
import {ApplyStylingFn, LStylingData, TStylingContext, TStylingContextIndex} from './interfaces';
import {applyStylingViaContext} from './bindings';
import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from './interfaces';
import {activateStylingMapFeature} from './map_based_bindings';
import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased, isSanitizationRequired} from './util';
import {allowDirectStyling as _allowDirectStyling, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isMapBased, isSanitizationRequired} from './util';
@ -53,20 +52,32 @@ export interface DebugStyling {
/** The associated TStylingContext instance */
context: TStylingContext;
/** Which configuration flags are active (see `TStylingContextConfig`) */
config: {
hasMapBindings: boolean; //
hasPropBindings: boolean; //
hasCollisions: boolean; //
hasTemplateBindings: boolean; //
hasHostBindings: boolean; //
templateBindingsLocked: boolean; //
hostBindingsLocked: boolean; //
allowDirectStyling: boolean; //
};
/**
* A summarization of each style/class property
* present in the context.
* present in the context
*/
summary: {[key: string]: LStylingSummary};
summary: {[propertyName: string]: LStylingSummary};
/**
* A key/value map of all styling properties and their
* runtime values.
* runtime values
*/
values: {[key: string]: string | number | null | boolean};
values: {[propertyName: string]: string | number | null | boolean};
/**
* Overrides the sanitizer used to process styles.
* Overrides the sanitizer used to process styles
*/
overrideSanitizer(sanitizer: StyleSanitizeFn|null): void;
}
@ -83,9 +94,15 @@ export interface TStylingTupleSummary {
/**
* The bit guard mask that is used to compare and protect against
* styling changes when and styling bindings update
* styling changes when any template style/class bindings update
*/
guardMask: number;
templateBitMask: number;
/**
* The bit guard mask that is used to compare and protect against
* styling changes when any host style/class bindings update
*/
hostBindingsBitMask: number;
/**
* Whether or not the entry requires sanitization
@ -93,18 +110,18 @@ export interface TStylingTupleSummary {
sanitizationRequired: boolean;
/**
* The default value that will be applied if any bindings are falsy.
* The default value that will be applied if any bindings are falsy
*/
defaultValue: string|boolean|null;
/**
* All bindingIndex sources that have been registered for this style.
* All bindingIndex sources that have been registered for this style
*/
sources: (number|null|string)[];
}
/**
* Instantiates and attaches an instance of `TStylingContextDebug` to the provided context.
* Instantiates and attaches an instance of `TStylingContextDebug` to the provided context
*/
export function attachStylingDebugObject(context: TStylingContext) {
const debug = new TStylingContextDebug(context);
@ -121,7 +138,8 @@ export function attachStylingDebugObject(context: TStylingContext) {
class TStylingContextDebug {
constructor(public readonly context: TStylingContext) {}
get isLocked() { return isContextLocked(this.context); }
get isTemplateLocked() { return isContextLocked(this.context, true); }
get isHostBindingsLocked() { return isContextLocked(this.context, false); }
/**
* Returns a detailed summary of each styling entry in the context.
@ -130,30 +148,36 @@ class TStylingContextDebug {
*/
get entries(): {[prop: string]: TStylingTupleSummary} {
const context = this.context;
const totalColumns = getValuesCount(context);
const entries: {[prop: string]: TStylingTupleSummary} = {};
const start = TStylingContextIndex.MapBindingsPosition;
const start = getPropValuesStartPosition(context);
let i = start;
while (i < context.length) {
const valuesCount = getValuesCount(context, i);
// the context may contain placeholder values which are populated ahead of time,
// but contain no actual binding values. In this situation there is no point in
// classifying this as an "entry" since no real data is stored here yet.
if (valuesCount) {
const prop = getProp(context, i);
const guardMask = getGuardMask(context, i);
const defaultValue = getDefaultValue(context, i);
const sanitizationRequired = isSanitizationRequired(context, i);
const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset;
const prop = getProp(context, i);
const templateBitMask = getGuardMask(context, i, false);
const hostBindingsBitMask = getGuardMask(context, i, true);
const defaultValue = getDefaultValue(context, i);
const sanitizationRequired = isSanitizationRequired(context, i);
const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset;
const sources: (number | string | null)[] = [];
for (let j = 0; j < valuesCount; j++) {
sources.push(context[bindingsStartPosition + j] as number | string | null);
const sources: (number | string | null)[] = [];
for (let j = 0; j < totalColumns; j++) {
const bindingIndex = context[bindingsStartPosition + j] as number | string | null;
if (bindingIndex !== 0) {
sources.push(bindingIndex);
}
entries[prop] = {prop, guardMask, sanitizationRequired, valuesCount, defaultValue, sources};
}
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
entries[prop] = {
prop,
templateBitMask,
hostBindingsBitMask,
sanitizationRequired,
valuesCount: sources.length, defaultValue, sources,
};
i += TStylingContextIndex.BindingsStartOffset + totalColumns;
}
return entries;
}
@ -191,6 +215,29 @@ export class NodeStylingDebug implements DebugStyling {
return entries;
}
get config() {
const hasMapBindings = hasConfig(this.context, TStylingConfig.HasMapBindings);
const hasPropBindings = hasConfig(this.context, TStylingConfig.HasPropBindings);
const hasCollisions = hasConfig(this.context, TStylingConfig.HasCollisions);
const hasTemplateBindings = hasConfig(this.context, TStylingConfig.HasTemplateBindings);
const hasHostBindings = hasConfig(this.context, TStylingConfig.HasHostBindings);
const templateBindingsLocked = hasConfig(this.context, TStylingConfig.TemplateBindingsLocked);
const hostBindingsLocked = hasConfig(this.context, TStylingConfig.HostBindingsLocked);
const allowDirectStyling =
_allowDirectStyling(this.context, false) || _allowDirectStyling(this.context, true);
return {
hasMapBindings, //
hasPropBindings, //
hasCollisions, //
hasTemplateBindings, //
hasHostBindings, //
templateBindingsLocked, //
hostBindingsLocked, //
allowDirectStyling, //
};
}
/**
* Returns a key/value map of all the styles/classes that were last applied to the element.
*/
@ -205,16 +252,23 @@ export class NodeStylingDebug implements DebugStyling {
// element is only used when the styling algorithm attempts to
// style the value (and we mock out the stylingApplyFn anyway).
const mockElement = {} as any;
const hasMaps = getValuesCount(this.context, TStylingContextIndex.MapBindingsPosition) > 0;
const hasMaps = hasConfig(this.context, TStylingConfig.HasMapBindings);
if (hasMaps) {
activateStylingMapFeature();
}
const mapFn: ApplyStylingFn =
(renderer: any, element: RElement, prop: string, value: string | null,
bindingIndex?: number | null) => { fn(prop, value, bindingIndex || null); };
bindingIndex?: number | null) => fn(prop, value, bindingIndex || null);
const sanitizer = this._isClassBased ? null : (this._sanitizer || getCurrentStyleSanitizer());
applyStyling(this.context, null, mockElement, this._data, true, mapFn, sanitizer);
// run the template bindings
applyStylingViaContext(
this.context, null, mockElement, this._data, true, mapFn, sanitizer, false);
// and also the host bindings
applyStylingViaContext(
this.context, null, mockElement, this._data, true, mapFn, sanitizer, true);
}
}

View File

@ -5,12 +5,35 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {unwrapSafeValue} from '../../sanitization/bypass';
import {TNode, TNodeFlags} from '../interfaces/node';
import {NO_CHANGE} from '../tokens';
import {StylingMapArray, StylingMapArrayIndex, TStylingConfigFlags, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
const MAP_BASED_ENTRY_PROP_NAME = '--MAP--';
const TEMPLATE_DIRECTIVE_INDEX = 0;
import {LStylingData, StylingMapArray, StylingMapArrayIndex, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
export const MAP_BASED_ENTRY_PROP_NAME = '[MAP]';
export const TEMPLATE_DIRECTIVE_INDEX = 0;
/**
* Default fallback value for a styling binding.
*
* A value of `null` is used here which signals to the styling algorithm that
* the styling value is not present. This way if there are no other values
* detected then it will be removed once the style/class property is dirty and
* diffed within the styling algorithm present in `flushStyling`.
*/
export const DEFAULT_BINDING_VALUE = null;
export const DEFAULT_BINDING_INDEX = 0;
const DEFAULT_TOTAL_SOURCES = 1;
// The first bit value reflects a map-based binding value's bit.
// The reason why it's always activated for every entry in the map
// is so that if any map-binding values update then all other prop
// based bindings will pass the guard check automatically without
// any extra code or flags.
export const DEFAULT_GUARD_MASK_VALUE = 0b1;
/**
* Creates a new instance of the `TStylingContext`.
@ -21,84 +44,83 @@ const TEMPLATE_DIRECTIVE_INDEX = 0;
* `TStylingContext` with the initial values (see `interfaces.ts` for more info).
*/
export function allocTStylingContext(initialStyling?: StylingMapArray | null): TStylingContext {
// because map-based bindings deal with a dynamic set of values, there
// is no way to know ahead of time whether or not sanitization is required.
// For this reason the configuration will always mark sanitization as active
// (this means that when map-based values are applied then sanitization will
// be checked against each property).
const mapBasedConfig = TStylingContextPropConfigFlags.SanitizationRequired;
initialStyling = initialStyling || allocStylingMapArray();
return [
initialStyling || [''], // empty initial-styling map value
TStylingConfigFlags.Initial,
TEMPLATE_DIRECTIVE_INDEX,
mapBasedConfig,
0,
MAP_BASED_ENTRY_PROP_NAME,
TStylingConfig.Initial, // 1) config for the styling context
DEFAULT_TOTAL_SOURCES, // 2) total amount of styling sources (template, directives, etc...)
initialStyling, // 3) initial styling values
];
}
/**
* Sets the provided directive as the last directive index in the provided `TStylingContext`.
*
* Styling in Angular can be applied from the template as well as multiple sources of
* host bindings. This means that each binding function (the template function or the
* hostBindings functions) will generate styling instructions as well as a styling
* apply function (i.e. `stylingApply()`). Because host bindings functions and the
* template function are independent from one another this means that the styling apply
* function will be called multiple times. By tracking the last directive index (which
* is what happens in this function) the styling algorithm knows exactly when to flush
* styling (which is when the last styling apply function is executed).
*/
export function updateLastDirectiveIndex(
context: TStylingContext, lastDirectiveIndex: number): void {
if (lastDirectiveIndex === TEMPLATE_DIRECTIVE_INDEX) {
const currentValue = context[TStylingContextIndex.LastDirectiveIndexPosition];
if (currentValue > TEMPLATE_DIRECTIVE_INDEX) {
// This means that a directive or two contained a host bindings function, but
// now the template function also contains styling. When this combination of sources
// comes up then we need to tell the context to store the state between updates
// (because host bindings evaluation happens after template binding evaluation).
markContextToPersistState(context);
}
} else {
context[TStylingContextIndex.LastDirectiveIndexPosition] = lastDirectiveIndex;
}
export function allocStylingMapArray(): StylingMapArray {
return [''];
}
function getConfig(context: TStylingContext) {
export function getConfig(context: TStylingContext) {
return context[TStylingContextIndex.ConfigPosition];
}
export function setConfig(context: TStylingContext, value: number) {
export function hasConfig(context: TStylingContext, flag: TStylingConfig) {
return (getConfig(context) & flag) !== 0;
}
/**
* Determines whether or not to apply styles/classes directly or via context resolution.
*
* There are three cases that are matched here:
* 1. context is locked for template or host bindings (depending on `hostBindingsMode`)
* 2. There are no collisions (i.e. properties with more than one binding)
* 3. There are only "prop" or "map" bindings present, but not both
*/
export function allowDirectStyling(context: TStylingContext, hostBindingsMode: boolean): boolean {
const config = getConfig(context);
return ((config & getLockedConfig(hostBindingsMode)) !== 0) &&
((config & TStylingConfig.HasCollisions) === 0) &&
((config & TStylingConfig.HasPropAndMapBindings) !== TStylingConfig.HasPropAndMapBindings);
}
export function setConfig(context: TStylingContext, value: TStylingConfig): void {
context[TStylingContextIndex.ConfigPosition] = value;
}
export function getProp(context: TStylingContext, index: number) {
export function patchConfig(context: TStylingContext, flag: TStylingConfig): void {
context[TStylingContextIndex.ConfigPosition] |= flag;
}
export function getProp(context: TStylingContext, index: number): string {
return context[index + TStylingContextIndex.PropOffset] as string;
}
function getPropConfig(context: TStylingContext, index: number): number {
return (context[index + TStylingContextIndex.ConfigAndGuardOffset] as number) &
return (context[index + TStylingContextIndex.ConfigOffset] as number) &
TStylingContextPropConfigFlags.Mask;
}
export function isSanitizationRequired(context: TStylingContext, index: number) {
return (getPropConfig(context, index) & TStylingContextPropConfigFlags.SanitizationRequired) > 0;
export function isSanitizationRequired(context: TStylingContext, index: number): boolean {
return (getPropConfig(context, index) & TStylingContextPropConfigFlags.SanitizationRequired) !==
0;
}
export function getGuardMask(context: TStylingContext, index: number) {
const configGuardValue = context[index + TStylingContextIndex.ConfigAndGuardOffset] as number;
return configGuardValue >> TStylingContextPropConfigFlags.TotalBits;
export function getGuardMask(
context: TStylingContext, index: number, isHostBinding: boolean): number {
const position = index + (isHostBinding ? TStylingContextIndex.HostBindingsBitGuardOffset :
TStylingContextIndex.TemplateBitGuardOffset);
return context[position] as number;
}
export function setGuardMask(context: TStylingContext, index: number, maskValue: number) {
const config = getPropConfig(context, index);
const guardMask = maskValue << TStylingContextPropConfigFlags.TotalBits;
context[index + TStylingContextIndex.ConfigAndGuardOffset] = config | guardMask;
export function setGuardMask(
context: TStylingContext, index: number, maskValue: number, isHostBinding: boolean) {
const position = index + (isHostBinding ? TStylingContextIndex.HostBindingsBitGuardOffset :
TStylingContextIndex.TemplateBitGuardOffset);
context[position] = maskValue;
}
export function getValuesCount(context: TStylingContext, index: number) {
return context[index + TStylingContextIndex.ValuesCountOffset] as number;
export function getValuesCount(context: TStylingContext): number {
return getTotalSources(context) + 1;
}
export function getTotalSources(context: TStylingContext): number {
return context[TStylingContextIndex.TotalSourcesPosition];
}
export function getBindingValue(context: TStylingContext, index: number, offset: number) {
@ -106,39 +128,44 @@ export function getBindingValue(context: TStylingContext, index: number, offset:
}
export function getDefaultValue(context: TStylingContext, index: number): string|boolean|null {
const valuesCount = getValuesCount(context, index);
return context[index + TStylingContextIndex.BindingsStartOffset + valuesCount - 1] as string |
return context[index + TStylingContextIndex.BindingsStartOffset + getTotalSources(context)] as
string |
boolean | null;
}
/**
* Temporary function which determines whether or not a context is
* allowed to be flushed based on the provided directive index.
*/
export function allowStylingFlush(context: TStylingContext | null, index: number) {
return (context && index === context[TStylingContextIndex.LastDirectiveIndexPosition]) ? true :
false;
export function setDefaultValue(
context: TStylingContext, index: number, value: string | boolean | null) {
return context[index + TStylingContextIndex.BindingsStartOffset + getTotalSources(context)] =
value;
}
export function lockContext(context: TStylingContext) {
setConfig(context, getConfig(context) | TStylingConfigFlags.Locked);
export function setValue(data: LStylingData, bindingIndex: number, value: any) {
data[bindingIndex] = value;
}
export function isContextLocked(context: TStylingContext): boolean {
return (getConfig(context) & TStylingConfigFlags.Locked) > 0;
export function getValue<T = any>(data: LStylingData, bindingIndex: number): T|null {
return bindingIndex > 0 ? data[bindingIndex] as T : null;
}
export function stateIsPersisted(context: TStylingContext): boolean {
return (getConfig(context) & TStylingConfigFlags.PersistStateValues) > 0;
export function lockContext(context: TStylingContext, hostBindingsMode: boolean): void {
patchConfig(context, getLockedConfig(hostBindingsMode));
}
export function markContextToPersistState(context: TStylingContext) {
setConfig(context, getConfig(context) | TStylingConfigFlags.PersistStateValues);
export function isContextLocked(context: TStylingContext, hostBindingsMode: boolean): boolean {
return hasConfig(context, getLockedConfig(hostBindingsMode));
}
export function getLockedConfig(hostBindingsMode: boolean) {
return hostBindingsMode ? TStylingConfig.HostBindingsLocked :
TStylingConfig.TemplateBindingsLocked;
}
export function getPropValuesStartPosition(context: TStylingContext) {
return TStylingContextIndex.MapBindingsBindingsStartPosition +
context[TStylingContextIndex.MapBindingsValuesCountPosition];
let startPosition = TStylingContextIndex.ValuesStartPosition;
if (hasConfig(context, TStylingConfig.HasMapBindings)) {
startPosition += TStylingContextIndex.BindingsStartOffset + getValuesCount(context);
}
return startPosition;
}
export function isMapBased(prop: string) {
@ -197,17 +224,23 @@ export function getStylingMapArray(value: TStylingContext | StylingMapArray | nu
StylingMapArray|null {
return isStylingContext(value) ?
(value as TStylingContext)[TStylingContextIndex.InitialStylingValuePosition] :
value;
value as StylingMapArray;
}
export function isStylingContext(value: TStylingContext | StylingMapArray | null): boolean {
// the StylingMapArray is in the format of [initial, prop, string, prop, string]
// and this is the defining value to distinguish between arrays
return Array.isArray(value) &&
value.length >= TStylingContextIndex.MapBindingsBindingsStartPosition &&
return Array.isArray(value) && value.length >= TStylingContextIndex.ValuesStartPosition &&
typeof value[1] !== 'string';
}
export function isStylingMapArray(value: TStylingContext | StylingMapArray | null): boolean {
// the StylingMapArray is in the format of [initial, prop, string, prop, string]
// and this is the defining value to distinguish between arrays
return Array.isArray(value) &&
(typeof(value as StylingMapArray)[StylingMapArrayIndex.ValuesStartPosition] === 'string');
}
export function getInitialStylingValue(context: TStylingContext | StylingMapArray | null): string {
const map = getStylingMapArray(context);
return map && (map[StylingMapArrayIndex.RawValuePosition] as string | null) || '';
@ -225,6 +258,13 @@ export function getMapProp(map: StylingMapArray, index: number): string {
return map[index + StylingMapArrayIndex.PropOffset] as string;
}
const MAP_DIRTY_VALUE =
typeof ngDevMode !== 'undefined' && ngDevMode ? {} : {MAP_DIRTY_VALUE: true};
export function setMapAsDirty(map: StylingMapArray): void {
map[StylingMapArrayIndex.RawValuePosition] = MAP_DIRTY_VALUE;
}
export function setMapValue(
map: StylingMapArray, index: number, value: string | boolean | null): void {
map[index + StylingMapArrayIndex.ValueOffset] = value;
@ -253,3 +293,130 @@ export function forceStylesAsString(styles: {[key: string]: any} | null | undefi
}
return str;
}
export function isHostStylingActive(directiveOrSourceId: number): boolean {
return directiveOrSourceId !== TEMPLATE_DIRECTIVE_INDEX;
}
/**
* Converts the provided styling map array into a string.
*
* Classes => `one two three`
* Styles => `prop:value; prop2:value2`
*/
export function stylingMapToString(map: StylingMapArray, isClassBased: boolean): string {
let str = '';
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i) as string;
const attrValue = concatString(prop, isClassBased ? '' : value, ':');
str = concatString(str, attrValue, isClassBased ? ' ' : '; ');
}
return str;
}
/**
* Converts the provided styling map array into a key value map.
*/
export function stylingMapToStringMap(map: StylingMapArray | null): {[key: string]: any} {
let stringMap: {[key: string]: any} = {};
if (map) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i) as string;
stringMap[prop] = value;
}
}
return stringMap;
}
/**
* Inserts the provided item into the provided styling array at the right spot.
*
* The `StylingMapArray` type is a sorted key/value array of entries. This means
* that when a new entry is inserted it must be placed at the right spot in the
* array. This function figures out exactly where to place it.
*/
export function addItemToStylingMap(
stylingMapArr: StylingMapArray, prop: string, value: string | boolean | null,
allowOverwrite?: boolean) {
for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length;
j += StylingMapArrayIndex.TupleSize) {
const propAtIndex = getMapProp(stylingMapArr, j);
if (prop <= propAtIndex) {
let applied = false;
if (propAtIndex === prop) {
const valueAtIndex = stylingMapArr[j];
if (allowOverwrite || !isStylingValueDefined(valueAtIndex)) {
applied = true;
setMapValue(stylingMapArr, j, value);
}
} else {
applied = true;
stylingMapArr.splice(j, 0, prop, value);
}
return applied;
}
}
stylingMapArr.push(prop, value);
return true;
}
/**
* Used to convert a {key:value} map into a `StylingMapArray` array.
*
* This function will either generate a new `StylingMapArray` instance
* or it will patch the provided `newValues` map value into an
* existing `StylingMapArray` value (this only happens if `bindingValue`
* is an instance of `StylingMapArray`).
*
* If a new key/value map is provided with an old `StylingMapArray`
* value then all properties will be overwritten with their new
* values or with `null`. This means that the array will never
* shrink in size (but it will also not be created and thrown
* away whenever the `{key:value}` map entries change).
*/
export function normalizeIntoStylingMap(
bindingValue: null | StylingMapArray,
newValues: {[key: string]: any} | string | null | undefined,
normalizeProps?: boolean): StylingMapArray {
const stylingMapArr: StylingMapArray = Array.isArray(bindingValue) ? bindingValue : [null];
stylingMapArr[StylingMapArrayIndex.RawValuePosition] = newValues || null;
// because the new values may not include all the properties
// that the old ones had, all values are set to `null` before
// the new values are applied. This way, when flushed, the
// styling algorithm knows exactly what style/class values
// to remove from the element (since they are `null`).
for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length;
j += StylingMapArrayIndex.TupleSize) {
setMapValue(stylingMapArr, j, null);
}
let props: string[]|null = null;
let map: {[key: string]: any}|undefined|null;
let allValuesTrue = false;
if (typeof newValues === 'string') { // [class] bindings allow string values
if (newValues.length) {
props = newValues.split(/\s+/);
allValuesTrue = true;
}
} else {
props = newValues ? Object.keys(newValues) : null;
map = newValues;
}
if (props) {
for (let i = 0; i < props.length; i++) {
const prop = props[i] as string;
const newProp = normalizeProps ? hyphenate(prop) : prop;
const value = allValuesTrue ? true : map ![prop];
addItemToStylingMap(stylingMapArr, newProp, value, true);
}
}
return stylingMapArr;
}

View File

@ -61,9 +61,7 @@ export interface SafeResourceUrl extends SafeValue {}
abstract class SafeValueImpl implements SafeValue {
constructor(public changingThisBreaksApplicationSecurity: string) {
// empty
}
constructor(public changingThisBreaksApplicationSecurity: string) {}
abstract getTypeName(): string;
@ -89,10 +87,10 @@ class SafeResourceUrlImpl extends SafeValueImpl implements SafeResourceUrl {
getTypeName() { return BypassType.ResourceUrl; }
}
export function unwrapSafeValue(value: SafeValue): string {
export function unwrapSafeValue(value: string | SafeValue): string {
return value instanceof SafeValueImpl ?
(value as SafeValueImpl).changingThisBreaksApplicationSecurity :
'';
(value as string);
}

View File

@ -59,8 +59,6 @@ declare global {
flushStyling: number;
classesApplied: number;
stylesApplied: number;
stylingWritePersistedState: number;
stylingReadPersistedState: number;
}
}
@ -101,8 +99,6 @@ export function ngDevModeResetPerfCounters(): NgDevModePerfCounters {
flushStyling: 0,
classesApplied: 0,
stylesApplied: 0,
stylingWritePersistedState: 0,
stylingReadPersistedState: 0,
};
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.

View File

@ -15,6 +15,8 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
describe('new styling integration', () => {
beforeEach(() => resetStylingCounters());
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should apply single property styles/classes to the element and default to any static styling values',
() => {
@ -124,6 +126,74 @@ describe('new styling integration', () => {
expect(element.style.width).toEqual('600px');
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should only run stylingFlush once when there are no collisions between styling properties',
() => {
@Directive({selector: '[dir-with-styling]'})
class DirWithStyling {
@HostBinding('style.font-size') public fontSize = '100px';
}
@Component({selector: 'comp-with-styling'})
class CompWithStyling {
@HostBinding('style.width') public width = '900px';
@HostBinding('style.height') public height = '900px';
}
@Component({
template: `
<comp-with-styling
[style.opacity]="opacity"
dir-with-styling>...</comp-with-styling>
`
})
class Cmp {
opacity: string|null = '0.5';
@ViewChild(CompWithStyling, {static: true})
compWithStyling: CompWithStyling|null = null;
@ViewChild(DirWithStyling, {static: true}) dirWithStyling: DirWithStyling|null = null;
}
TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const component = fixture.componentInstance;
const element = fixture.nativeElement.querySelector('comp-with-styling');
const node = getDebugNode(element) !;
const styles = node.styles !;
const config = styles.config;
expect(config.hasCollisions).toBeFalsy();
expect(config.hasMapBindings).toBeFalsy();
expect(config.hasPropBindings).toBeTruthy();
expect(config.allowDirectStyling).toBeTruthy();
expect(element.style.opacity).toEqual('0.5');
expect(element.style.width).toEqual('900px');
expect(element.style.height).toEqual('900px');
expect(element.style.fontSize).toEqual('100px');
// once for the template flush and again for the host bindings
expect(ngDevMode !.flushStyling).toEqual(2);
resetStylingCounters();
component.opacity = '0.6';
component.compWithStyling !.height = '100px';
component.compWithStyling !.width = '100px';
component.dirWithStyling !.fontSize = '50px';
fixture.detectChanges();
expect(element.style.opacity).toEqual('0.6');
expect(element.style.width).toEqual('100px');
expect(element.style.height).toEqual('100px');
expect(element.style.fontSize).toEqual('50px');
// there is no need to flush styling since the styles are applied directly
expect(ngDevMode !.flushStyling).toEqual(0);
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should combine all styling across the template, directive and component host bindings',
() => {
@ -240,6 +310,7 @@ describe('new styling integration', () => {
fixture.componentInstance.w3 = null;
fixture.detectChanges();
expect(styles.values).toEqual({
'width': '200px',
});
@ -309,16 +380,22 @@ describe('new styling integration', () => {
.it('should apply map-based style and class entries', () => {
@Component({template: '<div [style]="s" [class]="c"></div>'})
class Cmp {
public c !: {[key: string]: any};
updateClasses(prop: string) {
this.c = {...this.c || {}};
this.c[prop] = true;
public c: {[key: string]: any}|null = null;
updateClasses(classes: string) {
const c = this.c || (this.c = {});
Object.keys(this.c).forEach(className => { c[className] = false; });
classes.split(/\s+/).forEach(className => { c[className] = true; });
}
public s !: {[key: string]: any};
public s: {[key: string]: any}|null = null;
updateStyles(prop: string, value: string|number|null) {
this.s = {...this.s || {}};
this.s[prop] = value;
const s = this.s || (this.s = {});
Object.assign(s, {[prop]: value});
}
reset() {
this.s = null;
this.c = null;
}
}
@ -332,22 +409,47 @@ describe('new styling integration', () => {
const element = fixture.nativeElement.querySelector('div');
const node = getDebugNode(element) !;
const styles = node.styles !;
const classes = node.classes !;
let styles = node.styles !;
let classes = node.classes !;
const stylesSummary = styles.summary;
const widthSummary = stylesSummary['width'];
let stylesSummary = styles.summary;
let widthSummary = stylesSummary['width'];
expect(widthSummary.prop).toEqual('width');
expect(widthSummary.value).toEqual('100px');
const heightSummary = stylesSummary['height'];
let heightSummary = stylesSummary['height'];
expect(heightSummary.prop).toEqual('height');
expect(heightSummary.value).toEqual('200px');
const classesSummary = classes.summary;
const abcSummary = classesSummary['abc'];
let classesSummary = classes.summary;
let abcSummary = classesSummary['abc'];
expect(abcSummary.prop).toEqual('abc');
expect(abcSummary.value as any).toEqual(true);
expect(abcSummary.value).toBeTruthy();
comp.reset();
comp.updateStyles('width', '500px');
comp.updateStyles('height', null);
comp.updateClasses('def');
fixture.detectChanges();
styles = node.styles !;
classes = node.classes !;
stylesSummary = styles.summary;
widthSummary = stylesSummary['width'];
expect(widthSummary.value).toEqual('500px');
heightSummary = stylesSummary['height'];
expect(heightSummary.value).toEqual(null);
classesSummary = classes.summary;
abcSummary = classesSummary['abc'];
expect(abcSummary.prop).toEqual('abc');
expect(abcSummary.value).toBeFalsy();
let defSummary = classesSummary['def'];
expect(defSummary.prop).toEqual('def');
expect(defSummary.value).toBeTruthy();
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
@ -385,6 +487,7 @@ describe('new styling integration', () => {
const node = getDebugNode(element) !;
const styles = node.styles !;
expect(styles.values).toEqual({
'width': '555px',
'color': 'red',
@ -509,7 +612,8 @@ describe('new styling integration', () => {
resetStylingCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
// the width is applied both in TEMPLATE and in HOST_BINDINGS mode
assertStyleCounters(2, 0);
assertStyle(element, 'width', '999px');
assertStyle(element, 'height', '123px');
@ -517,8 +621,8 @@ describe('new styling integration', () => {
resetStylingCounters();
fixture.detectChanges();
// both are applied because the map was altered
assertStyleCounters(2, 0);
// the width is only applied once
assertStyleCounters(1, 0);
assertStyle(element, 'width', '0px');
assertStyle(element, 'height', '123px');
@ -526,8 +630,8 @@ describe('new styling integration', () => {
resetStylingCounters();
fixture.detectChanges();
// all three are applied because the map was altered
assertStyleCounters(3, 0);
// only the width and color have changed
assertStyleCounters(2, 0);
assertStyle(element, 'width', '1000px');
assertStyle(element, 'height', '123px');
assertStyle(element, 'color', 'red');
@ -536,7 +640,9 @@ describe('new styling integration', () => {
resetStylingCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
// height gets applied twice and all other
// values get applied
assertStyleCounters(4, 0);
assertStyle(element, 'width', '1000px');
assertStyle(element, 'height', '1000px');
assertStyle(element, 'color', 'red');
@ -545,8 +651,7 @@ describe('new styling integration', () => {
resetStylingCounters();
fixture.detectChanges();
// all four are applied because the map was altered
assertStyleCounters(4, 0);
assertStyleCounters(5, 0);
assertStyle(element, 'width', '2000px');
assertStyle(element, 'height', '1000px');
assertStyle(element, 'color', 'blue');
@ -557,62 +662,13 @@ describe('new styling integration', () => {
fixture.detectChanges();
// all four are applied because the map was altered
assertStyleCounters(3, 1);
assertStyleCounters(4, 1);
assertStyle(element, 'width', '2000px');
assertStyle(element, 'height', '1000px');
assertStyle(element, 'color', 'blue');
assertStyle(element, 'opacity', '');
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should only persist state values in a local map if template AND host styling is used together',
() => {
@Directive({selector: '[dir-that-sets-styling]'})
class Dir {
@HostBinding('style.width') w = '100px';
}
@Component({
template: `
<div #a dir-that-sets-styling></div>
<div #b [style.width]="w"></div>
<div #c dir-that-sets-styling [style.width]="w"></div>
`
})
class Cmp {
w = '200px';
@ViewChild('a', {read: Dir, static: true}) a !: Dir;
@ViewChild('c', {read: Dir, static: true}) c !: Dir;
}
TestBed.configureTestingModule({declarations: [Cmp, Dir]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
resetStylingCounters();
comp.a.w = '999px';
comp.w = '999px';
comp.c.w = '999px';
fixture.detectChanges();
expect(ngDevMode !.stylingWritePersistedState).toEqual(totalUpdates(1));
comp.a.w = '888px';
fixture.detectChanges();
expect(ngDevMode !.stylingWritePersistedState).toEqual(totalUpdates(2));
comp.c.w = '777px';
fixture.detectChanges();
expect(ngDevMode !.stylingWritePersistedState).toEqual(totalUpdates(3));
function totalUpdates(value: number) {
// this is doubled because detectChanges is run twice to
// see to check for checkNoChanges
return value * 2;
}
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should sanitize style values before writing them', () => {
@Component({
@ -910,7 +966,6 @@ describe('new styling integration', () => {
TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const dirOne = fixture.nativeElement.querySelector('dir-one');
@ -937,10 +992,10 @@ describe('new styling integration', () => {
@Component({
template: `
<div class="a" [style.width.px]="w" one></div>
<div class="b" [style.height.px]="h" one two></div>
<div class="c" [style.color]="c" two></div>
`
<div class="a" [style.width.px]="w" one></div>
<div class="b" [style.height.px]="h" one two></div>
<div class="c" [style.color]="c" two></div>
`
})
class Cmp {
w = 100;
@ -1092,6 +1147,27 @@ describe('new styling integration', () => {
expect(div.style.height).toEqual('200px');
expect(div.style.opacity).toEqual('1');
});
it('should allow [ngStyle] and [ngClass] to be used together', () => {
@Component({
template: `
<div [ngClass]="c" [ngStyle]="s"></div>
`
})
class Cmp {
c: any = 'foo bar';
s: any = {width: '200px'};
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.style.width).toEqual('200px');
expect(div.classList.contains('foo')).toBeTruthy();
expect(div.classList.contains('bar')).toBeTruthy();
});
});
function assertStyleCounters(countForSet: number, countForRemove: number) {

View File

@ -628,7 +628,6 @@ describe('styling', () => {
expect(childDir.parent).toBeAnInstanceOf(TestDir);
expect(testDirDiv.classList).not.toContain('with-button');
expect(fixture.debugElement.nativeElement.textContent).toContain('Hello');
});
});

View File

@ -155,6 +155,9 @@
{
"name": "_currentNamespace"
},
{
"name": "_elementExitFn"
},
{
"name": "_global"
},
@ -165,7 +168,7 @@
"name": "_selectedIndex"
},
{
"name": "_stateStorage"
"name": "_state"
},
{
"name": "addComponentLogic"
@ -176,6 +179,9 @@
{
"name": "addToViewTree"
},
{
"name": "allocStylingMapArray"
},
{
"name": "appendChild"
},
@ -254,6 +260,9 @@
{
"name": "executeContentQueries"
},
{
"name": "executeElementExitFn"
},
{
"name": "executeInitAndCheckHooks"
},
@ -395,6 +404,9 @@
{
"name": "getStylingMapArray"
},
{
"name": "hasActiveElementFlag"
},
{
"name": "hasClassInput"
},
@ -572,9 +584,6 @@
{
"name": "renderView"
},
{
"name": "resetAllStylingState"
},
{
"name": "resetComponentState"
},

View File

@ -134,6 +134,9 @@
{
"name": "__window"
},
{
"name": "_elementExitFn"
},
{
"name": "_global"
},
@ -144,7 +147,7 @@
"name": "_selectedIndex"
},
{
"name": "_stateStorage"
"name": "_state"
},
{
"name": "addToViewTree"
@ -209,6 +212,9 @@
{
"name": "executeCheckHooks"
},
{
"name": "executeElementExitFn"
},
{
"name": "executeInitAndCheckHooks"
},
@ -314,6 +320,9 @@
{
"name": "getSelectedIndex"
},
{
"name": "hasActiveElementFlag"
},
{
"name": "hasParentInjector"
},
@ -419,9 +428,6 @@
{
"name": "renderView"
},
{
"name": "resetAllStylingState"
},
{
"name": "resetComponentState"
},

View File

@ -41,6 +41,9 @@
{
"name": "DECLARATION_VIEW"
},
{
"name": "DEFAULT_BINDING_INDEX"
},
{
"name": "DEFAULT_BINDING_VALUE"
},
@ -48,7 +51,7 @@
"name": "DEFAULT_GUARD_MASK_VALUE"
},
{
"name": "DEFAULT_SIZE_VALUE"
"name": "DEFAULT_TOTAL_SOURCES"
},
{
"name": "DefaultIterableDiffer"
@ -89,6 +92,9 @@
{
"name": "HOST"
},
{
"name": "INDEX_START_VALUE"
},
{
"name": "INJECTOR"
},
@ -108,7 +114,7 @@
"name": "MAP_BASED_ENTRY_PROP_NAME"
},
{
"name": "MIN_DIRECTIVE_ID"
"name": "MAP_DIRTY_VALUE"
},
{
"name": "MONKEY_PATCH_KEY_NAME"
@ -212,9 +218,6 @@
{
"name": "STYLING_INDEX_FOR_MAP_BINDING"
},
{
"name": "STYLING_INDEX_START_VALUE"
},
{
"name": "SWITCH_ELEMENT_REF_FACTORY"
},
@ -224,6 +227,9 @@
{
"name": "SWITCH_VIEW_CONTAINER_REF_FACTORY"
},
{
"name": "SafeValueImpl"
},
{
"name": "SkipSelf"
},
@ -395,6 +401,9 @@
{
"name": "_devMode"
},
{
"name": "_elementExitFn"
},
{
"name": "_global"
},
@ -405,16 +414,7 @@
"name": "_selectedIndex"
},
{
"name": "_stateStorage"
},
{
"name": "_stylingElement"
},
{
"name": "_stylingProp"
},
{
"name": "_stylingState"
"name": "_state"
},
{
"name": "_symbolIterator"
@ -422,12 +422,6 @@
{
"name": "activeDirectiveId"
},
{
"name": "activeDirectiveSuperClassDepthPosition"
},
{
"name": "activeDirectiveSuperClassHeight"
},
{
"name": "addBindingIntoContext"
},
@ -437,6 +431,9 @@
{
"name": "addItemToStylingMap"
},
{
"name": "addNewSourceColumn"
},
{
"name": "addRemoveViewFromContainer"
},
@ -446,6 +443,9 @@
{
"name": "addToViewTree"
},
{
"name": "allocStylingMapArray"
},
{
"name": "allocTStylingContext"
},
@ -453,7 +453,7 @@
"name": "allocateNewContextEntry"
},
{
"name": "allowStylingFlush"
"name": "allowDirectStyling"
},
{
"name": "appendChild"
@ -470,6 +470,15 @@
{
"name": "applyStyling"
},
{
"name": "applyStylingValue"
},
{
"name": "applyStylingValueDirectly"
},
{
"name": "applyStylingViaContext"
},
{
"name": "applyToElementOrContainer"
},
@ -524,9 +533,6 @@
{
"name": "containerInternal"
},
{
"name": "contextHasUpdates"
},
{
"name": "contextLView"
},
@ -584,18 +590,6 @@
{
"name": "defaultScheduler"
},
{
"name": "deferBindingRegistration"
},
{
"name": "deferStylingUpdate"
},
{
"name": "deferredBindingQueue"
},
{
"name": "deleteStylingStateFromStorage"
},
{
"name": "destroyLView"
},
@ -629,6 +623,9 @@
{
"name": "executeContentQueries"
},
{
"name": "executeElementExitFn"
},
{
"name": "executeInitAndCheckHooks"
},
@ -666,10 +663,10 @@
"name": "findExistingListener"
},
{
"name": "findViaComponent"
"name": "findInitialStylingValue"
},
{
"name": "flushDeferredBindings"
"name": "findViaComponent"
},
{
"name": "flushStyling"
@ -689,15 +686,6 @@
{
"name": "getActiveDirectiveId"
},
{
"name": "getActiveDirectiveStylingIndex"
},
{
"name": "getActiveDirectiveSuperClassDepth"
},
{
"name": "getActiveDirectiveSuperClassHeight"
},
{
"name": "getBeforeNodeForView"
},
@ -746,6 +734,9 @@
{
"name": "getDebugContext"
},
{
"name": "getDefaultValue"
},
{
"name": "getDirectiveDef"
},
@ -785,6 +776,9 @@
{
"name": "getLViewParent"
},
{
"name": "getLockedConfig"
},
{
"name": "getMapProp"
},
@ -893,21 +887,36 @@
{
"name": "getTViewCleanup"
},
{
"name": "getTotalSources"
},
{
"name": "getTypeName"
},
{
"name": "getTypeNameForDebugging"
},
{
"name": "getValue"
},
{
"name": "getValuesCount"
},
{
"name": "handleError"
},
{
"name": "hasActiveElementFlag"
},
{
"name": "hasClassInput"
},
{
"name": "hasConfig"
},
{
"name": "hasDirectives"
},
{
"name": "hasParentInjector"
},
@ -936,7 +945,7 @@
"name": "initNodeFlags"
},
{
"name": "initializeInputAndOutputAliases"
"name": "initializeTNodeInputs"
},
{
"name": "injectElementRef"
@ -1010,6 +1019,12 @@
{
"name": "isForwardRef"
},
{
"name": "isHostStyling"
},
{
"name": "isHostStylingActive"
},
{
"name": "isJsObject"
},
@ -1085,24 +1100,21 @@
{
"name": "markAsComponentHost"
},
{
"name": "markContextToPersistState"
},
{
"name": "markDirty"
},
{
"name": "markDirtyIfOnPush"
},
{
"name": "markStylingStateAsDirty"
},
{
"name": "markViewDirty"
},
{
"name": "matchTemplateAttribute"
},
{
"name": "maybeApplyStyling"
},
{
"name": "namespaceHTMLInternal"
},
@ -1139,6 +1151,9 @@
{
"name": "normalizeBitMaskValue"
},
{
"name": "patchConfig"
},
{
"name": "postProcessBaseDirective"
},
@ -1202,6 +1217,9 @@
{
"name": "renderDetachView"
},
{
"name": "renderHostBindingsAsStale"
},
{
"name": "renderInitialStyling"
},
@ -1214,9 +1232,6 @@
{
"name": "renderView"
},
{
"name": "resetAllStylingState"
},
{
"name": "resetComponentState"
},
@ -1250,6 +1265,9 @@
{
"name": "selectView"
},
{
"name": "setActiveElementFlag"
},
{
"name": "setActiveHostElement"
},
@ -1262,9 +1280,6 @@
{
"name": "setClass"
},
{
"name": "setConfig"
},
{
"name": "setCurrentDirectiveDef"
},
@ -1274,9 +1289,15 @@
{
"name": "setCurrentStyleSanitizer"
},
{
"name": "setDefaultValue"
},
{
"name": "setDirectiveStylingInput"
},
{
"name": "setElementExitFn"
},
{
"name": "setGuardMask"
},
@ -1298,6 +1319,9 @@
{
"name": "setIsNotParent"
},
{
"name": "setMapAsDirty"
},
{
"name": "setMapValue"
},
@ -1316,18 +1340,15 @@
{
"name": "setUpAttributes"
},
{
"name": "setValue"
},
{
"name": "shouldSearchParent"
},
{
"name": "stateIsPersisted"
},
{
"name": "storeCleanupFn"
},
{
"name": "storeStylingState"
},
{
"name": "stringify"
},
@ -1337,6 +1358,9 @@
{
"name": "stylingMapToString"
},
{
"name": "stylingProp"
},
{
"name": "syncViewWithBlueprint"
},
@ -1358,26 +1382,23 @@
{
"name": "unwrapRNode"
},
{
"name": "unwrapSafeValue"
},
{
"name": "updateBindingData"
},
{
"name": "updateClassBinding"
"name": "updateClassViaContext"
},
{
"name": "updateInitialStylingOnContext"
},
{
"name": "updateLastDirectiveIndex"
},
{
"name": "updateLastDirectiveIndex"
},
{
"name": "updateRawValueOnContext"
},
{
"name": "updateStyleBinding"
"name": "updateStyleViaContext"
},
{
"name": "viewAttachedToChangeDetector"
@ -1436,12 +1457,6 @@
{
"name": "ɵɵrestoreView"
},
{
"name": "ɵɵstyling"
},
{
"name": "ɵɵstylingApply"
},
{
"name": "ɵɵtemplate"
},

View File

@ -9,7 +9,7 @@
import {NgForOfContext} from '@angular/common';
import {ɵɵdefineComponent} from '../../src/render3/definition';
import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
import {AttributeMarker} from '../../src/render3/interfaces/node';
import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, unwrapSafeValue} from '../../src/sanitization/bypass';
import {ɵɵdefaultStyleSanitizer, ɵɵsanitizeHtml, ɵɵsanitizeResourceUrl, ɵɵsanitizeScript, ɵɵsanitizeStyle, ɵɵsanitizeUrl} from '../../src/sanitization/sanitization';
@ -20,11 +20,7 @@ import {NgForOf} from './common_with_def';
import {ComponentFixture, TemplateFixture} from './render_util';
describe('instructions', () => {
function createAnchor() {
ɵɵelementStart(0, 'a');
ɵɵstyling();
ɵɵelementEnd();
}
function createAnchor() { ɵɵelement(0, 'a'); }
function createDiv(initialClasses?: string[] | null, initialStyles?: string[] | null) {
const attrs: any[] = [];
@ -34,9 +30,7 @@ describe('instructions', () => {
if (initialStyles) {
attrs.push(AttributeMarker.Styles, ...initialStyles);
}
ɵɵelementStart(0, 'div', attrs);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelement(0, 'div', attrs);
}
function createScript() { ɵɵelement(0, 'script'); }
@ -156,7 +150,6 @@ describe('instructions', () => {
t.update(() => {
ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer);
ɵɵstyleProp('background-image', 'url("http://server")');
ɵɵstylingApply();
});
// nothing is set because sanitizer suppresses it.
expect(t.html).toEqual('<div></div>');
@ -164,7 +157,6 @@ describe('instructions', () => {
t.update(() => {
ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer);
ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('url("http://server2")'));
ɵɵstylingApply();
});
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('url("http://server2")');
@ -173,17 +165,12 @@ describe('instructions', () => {
describe('styleMap', () => {
function createDivWithStyle() {
ɵɵelementStart(0, 'div', [AttributeMarker.Styles, 'height', '10px']);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelement(0, 'div', [AttributeMarker.Styles, 'height', '10px']);
}
it('should add style', () => {
const fixture = new TemplateFixture(createDivWithStyle, () => {}, 1);
fixture.update(() => {
ɵɵstyleMap({'background-color': 'red'});
ɵɵstylingApply();
});
fixture.update(() => { ɵɵstyleMap({'background-color': 'red'}); });
expect(fixture.html).toEqual('<div style="background-color: red; height: 10px;"></div>');
});
@ -205,7 +192,6 @@ describe('instructions', () => {
'filter': 'filter',
'width': 'width'
});
ɵɵstylingApply();
});
const props = detectedValues.sort();
@ -216,18 +202,11 @@ describe('instructions', () => {
});
describe('elementClass', () => {
function createDivWithStyling() {
ɵɵelementStart(0, 'div');
ɵɵstyling();
ɵɵelementEnd();
}
function createDivWithStyling() { ɵɵelement(0, 'div'); }
it('should add class', () => {
const fixture = new TemplateFixture(createDivWithStyling, () => {}, 1);
fixture.update(() => {
ɵɵclassMap('multiple classes');
ɵɵstylingApply();
});
fixture.update(() => { ɵɵclassMap('multiple classes'); });
expect(fixture.html).toEqual('<div class="classes multiple"></div>');
});
});

View File

@ -9,7 +9,7 @@
import {RendererType2} from '../../src/render/api';
import {getLContext} from '../../src/render3/context_discovery';
import {AttributeMarker, ɵɵadvance, ɵɵattribute, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵhostProperty, ɵɵproperty} from '../../src/render3/index';
import {ɵɵallocHostVars, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵprojection, ɵɵprojectionDef, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all';
import {ɵɵallocHostVars, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all';
import {MONKEY_PATCH_KEY_NAME} from '../../src/render3/interfaces/context';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
@ -630,12 +630,7 @@ describe('element discovery', () => {
vars: 0,
template: (rf: RenderFlags, ctx: StructuredComp) => {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'section');
ɵɵstyling();
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵstylingApply();
ɵɵelement(0, 'section');
}
}
});

View File

@ -6,11 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ɵɵadvance} from '../../../../src/render3/instructions/advance';
import {ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element';
import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element';
import {refreshView} from '../../../../src/render3/instructions/shared';
import {RenderFlags} from '../../../../src/render3/interfaces/definition';
import {TVIEW} from '../../../../src/render3/interfaces/view';
import {ɵɵclassMap, ɵɵstyleMap, ɵɵstyling, ɵɵstylingApply} from '../../../../src/render3/styling_next/instructions';
import {ɵɵclassMap, ɵɵstyleMap} from '../../../../src/render3/styling_next/instructions';
import {createBenchmark} from '../micro_bench';
import {setupRootViewWithEmbeddedViews} from '../setup';
@ -31,79 +31,49 @@ import {setupRootViewWithEmbeddedViews} from '../setup';
function testTemplate(rf: RenderFlags, ctx: any) {
if (rf & 1) {
ɵɵelementStart(0, 'div');
ɵɵelementStart(1, 'div');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(2, 'div');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(3, 'div');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(4, 'div');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(5, 'div');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(6, 'div');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(7, 'div');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(8, 'div');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(9, 'div');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(10, 'div');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelement(1, 'div');
ɵɵelement(2, 'div');
ɵɵelement(3, 'div');
ɵɵelement(4, 'div');
ɵɵelement(5, 'div');
ɵɵelement(6, 'div');
ɵɵelement(7, 'div');
ɵɵelement(8, 'div');
ɵɵelement(9, 'div');
ɵɵelement(10, 'div');
ɵɵelementEnd();
}
if (rf & 2) {
ɵɵadvance(1);
ɵɵstyleMap({width: '0px', height: '0px'});
ɵɵclassMap('one two');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleMap({width: '10px', height: '100px'});
ɵɵclassMap('one two');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleMap({width: '20px', height: '200px'});
ɵɵclassMap('one two');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleMap({width: '30px', height: '300px'});
ɵɵclassMap('one two');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleMap({width: '40px', height: '400px'});
ɵɵclassMap('one two');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleMap({width: '50px', height: '500px'});
ɵɵclassMap('one two');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleMap({width: '60px', height: '600px'});
ɵɵclassMap('one two');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleMap({width: '70px', height: '700px'});
ɵɵclassMap('one two');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleMap({width: '80px', height: '800px'});
ɵɵclassMap('one two');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleMap({width: '90px', height: '900px'});
ɵɵclassMap('one two');
ɵɵstylingApply();
}
}

View File

@ -6,12 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ɵɵadvance} from '../../../../src/render3/instructions/advance';
import {ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element';
import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element';
import {refreshView} from '../../../../src/render3/instructions/shared';
import {RenderFlags} from '../../../../src/render3/interfaces/definition';
import {AttributeMarker} from '../../../../src/render3/interfaces/node';
import {TVIEW} from '../../../../src/render3/interfaces/view';
import {ɵɵclassProp, ɵɵstyleProp, ɵɵstyling, ɵɵstylingApply} from '../../../../src/render3/styling_next/instructions';
import {ɵɵclassProp, ɵɵstyleProp} from '../../../../src/render3/styling_next/instructions';
import {createBenchmark} from '../micro_bench';
import {setupRootViewWithEmbeddedViews} from '../setup';
@ -32,89 +32,50 @@ import {setupRootViewWithEmbeddedViews} from '../setup';
function testTemplate(rf: RenderFlags, ctx: any) {
if (rf & 1) {
ɵɵelementStart(0, 'div', [AttributeMarker.Classes, 'list']);
ɵɵelementStart(
1, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(
2, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(
3, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(
4, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(
5, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(
6, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(
7, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(
8, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(
9, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(
ɵɵelement(1, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵelement(2, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵelement(3, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵelement(4, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵelement(5, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵelement(6, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵelement(7, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵelement(8, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵelement(9, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵelement(
10, 'div', [AttributeMarker.Classes, 'item', AttributeMarker.Styles, 'width', '50px']);
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementEnd();
}
if (rf & 2) {
ɵɵadvance(1);
ɵɵstyleProp('width', '0px');
ɵɵclassProp('scale', true);
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('width', '100px');
ɵɵclassProp('scale', true);
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('width', '200px');
ɵɵclassProp('scale', true);
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('width', '300px');
ɵɵclassProp('scale', true);
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('width', '400px');
ɵɵclassProp('scale', true);
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('width', '500px');
ɵɵclassProp('scale', true);
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('width', '600px');
ɵɵclassProp('scale', true);
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('width', '700px');
ɵɵclassProp('scale', true);
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('width', '800px');
ɵɵclassProp('scale', true);
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('width', '900px');
ɵɵclassProp('scale', true);
ɵɵstylingApply();
}
}

View File

@ -6,11 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ɵɵadvance} from '../../../../src/render3/instructions/advance';
import {ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element';
import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart} from '../../../../src/render3/instructions/element';
import {refreshView} from '../../../../src/render3/instructions/shared';
import {RenderFlags} from '../../../../src/render3/interfaces/definition';
import {TVIEW} from '../../../../src/render3/interfaces/view';
import {ɵɵstyleProp, ɵɵstyling, ɵɵstylingApply} from '../../../../src/render3/styling_next/instructions';
import {ɵɵstyleProp} from '../../../../src/render3/styling_next/instructions';
import {createBenchmark} from '../micro_bench';
import {setupRootViewWithEmbeddedViews} from '../setup';
@ -31,69 +31,39 @@ import {setupRootViewWithEmbeddedViews} from '../setup';
function testTemplate(rf: RenderFlags, ctx: any) {
if (rf & 1) {
ɵɵelementStart(0, 'div');
ɵɵelementStart(1, 'button');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(2, 'button');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(3, 'button');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(4, 'button');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(5, 'button');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(6, 'button');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(7, 'button');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(8, 'button');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(9, 'button');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelementStart(10, 'button');
ɵɵstyling();
ɵɵelementEnd();
ɵɵelement(1, 'button');
ɵɵelement(2, 'button');
ɵɵelement(3, 'button');
ɵɵelement(4, 'button');
ɵɵelement(5, 'button');
ɵɵelement(6, 'button');
ɵɵelement(7, 'button');
ɵɵelement(8, 'button');
ɵɵelement(9, 'button');
ɵɵelement(10, 'button');
ɵɵelementEnd();
}
if (rf & 2) {
ɵɵadvance(1);
ɵɵstyleProp('background-color', 'color1');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('background-color', 'color2');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('background-color', 'color3');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('background-color', 'color4');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('background-color', 'color5');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('background-color', 'color6');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('background-color', 'color7');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('background-color', 'color8');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('background-color', 'color9');
ɵɵstylingApply();
ɵɵadvance(1);
ɵɵstyleProp('background-color', 'color10');
ɵɵstylingApply();
}
}

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {normalizeIntoStylingMap as createMap} from '../../../src/render3/styling_next/map_based_bindings';
import {normalizeIntoStylingMap as createMap} from '../../../src/render3/styling_next/util';
describe('map-based bindings', () => {
describe('StylingMapArray construction', () => {

View File

@ -5,8 +5,10 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {DEFAULT_GUARD_MASK_VALUE, registerBinding} from '@angular/core/src/render3/styling_next/bindings';
import {registerBinding} from '@angular/core/src/render3/styling_next/bindings';
import {attachStylingDebugObject} from '@angular/core/src/render3/styling_next/styling_debug';
import {DEFAULT_GUARD_MASK_VALUE} from '@angular/core/src/render3/styling_next/util';
import {allocTStylingContext} from '../../../src/render3/styling_next/util';
describe('styling context', () => {
@ -15,33 +17,36 @@ describe('styling context', () => {
const context = debug.context;
expect(debug.entries).toEqual({});
registerBinding(context, 1, 'width', '100px');
registerBinding(context, 1, 0, 'width', '100px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
sanitizationRequired: false,
guardMask: buildGuardMask(),
templateBitMask: buildGuardMask(),
hostBindingsBitMask: buildGuardMask(),
defaultValue: '100px',
sources: ['100px'],
});
registerBinding(context, 2, 'width', 20);
registerBinding(context, 2, 0, 'width', 20);
expect(debug.entries['width']).toEqual({
prop: 'width',
sanitizationRequired: false,
valuesCount: 2,
guardMask: buildGuardMask(2),
templateBitMask: buildGuardMask(2),
hostBindingsBitMask: buildGuardMask(),
defaultValue: '100px',
sources: [20, '100px'],
});
registerBinding(context, 3, 'height', 10);
registerBinding(context, 4, 'height', 15);
registerBinding(context, 3, 0, 'height', 10);
registerBinding(context, 4, 1, 'height', 15);
expect(debug.entries['height']).toEqual({
prop: 'height',
valuesCount: 3,
sanitizationRequired: false,
guardMask: buildGuardMask(3, 4),
templateBitMask: buildGuardMask(3),
hostBindingsBitMask: buildGuardMask(4),
defaultValue: null,
sources: [10, 15, null],
});
@ -52,13 +57,14 @@ describe('styling context', () => {
const context = debug.context;
expect(debug.entries).toEqual({});
registerBinding(context, 1, 'width', 123);
registerBinding(context, 1, 'width', 123);
registerBinding(context, 1, 0, 'width', 123);
registerBinding(context, 1, 0, 'width', 123);
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 2,
sanitizationRequired: false,
guardMask: buildGuardMask(1),
templateBitMask: buildGuardMask(1),
hostBindingsBitMask: buildGuardMask(),
defaultValue: null,
sources: [123, null],
});
@ -68,33 +74,36 @@ describe('styling context', () => {
const debug = makeContextWithDebug();
const context = debug.context;
registerBinding(context, 1, 'width', null);
registerBinding(context, 1, 0, 'width', null);
const x = debug.entries['width'];
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
sanitizationRequired: false,
guardMask: buildGuardMask(),
templateBitMask: buildGuardMask(),
hostBindingsBitMask: buildGuardMask(),
defaultValue: null,
sources: [null]
});
registerBinding(context, 1, 'width', '100px');
registerBinding(context, 1, 0, 'width', '100px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
sanitizationRequired: false,
guardMask: buildGuardMask(),
templateBitMask: buildGuardMask(),
hostBindingsBitMask: buildGuardMask(),
defaultValue: '100px',
sources: ['100px']
});
registerBinding(context, 1, 'width', '200px');
registerBinding(context, 1, 0, 'width', '200px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
sanitizationRequired: false,
guardMask: buildGuardMask(),
templateBitMask: buildGuardMask(),
hostBindingsBitMask: buildGuardMask(),
defaultValue: '100px',
sources: ['100px']
});

View File

@ -18,7 +18,7 @@ describe('styling debugging tools', () => {
const data: any[] = [];
const d = new NodeStylingDebug(context, data);
registerBinding(context, 0, 'width', null);
registerBinding(context, 0, 0, 'width', null);
expect(d.summary).toEqual({
width: {
prop: 'width',
@ -27,7 +27,7 @@ describe('styling debugging tools', () => {
},
});
registerBinding(context, 0, 'width', '100px');
registerBinding(context, 0, 0, 'width', '100px');
expect(d.summary).toEqual({
width: {
prop: 'width',
@ -39,7 +39,7 @@ describe('styling debugging tools', () => {
const someBindingIndex1 = 1;
data[someBindingIndex1] = '200px';
registerBinding(context, 0, 'width', someBindingIndex1);
registerBinding(context, 0, 0, 'width', someBindingIndex1);
expect(d.summary).toEqual({
width: {
prop: 'width',
@ -51,7 +51,7 @@ describe('styling debugging tools', () => {
const someBindingIndex2 = 2;
data[someBindingIndex2] = '500px';
registerBinding(context, 0, 'width', someBindingIndex2);
registerBinding(context, 0, 1, 'width', someBindingIndex2);
expect(d.summary).toEqual({
width: {
prop: 'width',

View File

@ -1076,10 +1076,6 @@ export declare function ɵɵstylePropInterpolateV(prop: string, values: any[], v
export declare function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void;
export declare function ɵɵstyling(): void;
export declare function ɵɵstylingApply(): void;
export declare function ɵɵtemplate(index: number, templateFn: ComponentTemplate<any> | null, consts: number, vars: number, tagName?: string | null, attrs?: TAttributes | null, localRefs?: string[] | null, localRefExtractor?: LocalRefExtractor): void;
export declare function ɵɵtemplateRefExtractor(tNode: TNode, currentView: LView): ViewEngine_TemplateRef<unknown> | null;