refactor(ivy): make styling instructions use the new styling algorithm (#30742)

This commit is the final patch of the ivy styling algorithm refactor.
This patch swaps functionality from the old styling mechanism to the
new refactored code by changing the instruction code the compiler
generates and by pointing the runtime instruction code to the new
styling algorithm.

PR Close #30742
This commit is contained in:
Matias Niemelä 2019-05-28 10:31:01 -07:00 committed by Kara Erickson
parent f14693b9a4
commit 9c954ebc62
57 changed files with 2287 additions and 2612 deletions

View File

@ -4,10 +4,10 @@
"uncompressed": {
"runtime-es5": 3042,
"runtime-es2015": 3048,
"main-es5": 511036,
"main-es2015": 450486,
"polyfills-es5": 130136,
"polyfills-es2015": 52931
"main-es5": 511052,
"main-es2015": 450562,
"polyfills-es5": 129161,
"polyfills-es2015": 53295
}
}
},
@ -16,10 +16,10 @@
"uncompressed": {
"runtime-es5": 3042,
"runtime-es2015": 3048,
"main-es5": 511036,
"main-es2015": 450486,
"polyfills-es5": 130136,
"polyfills-es2015": 52931
"main-es5": 499085,
"main-es2015": 438296,
"polyfills-es5": 129161,
"polyfills-es2015": 53295
}
}
},
@ -28,10 +28,10 @@
"uncompressed": {
"runtime-es5": 2932,
"runtime-es2015": 2938,
"main-es5": 565073,
"main-es2015": 583108,
"polyfills-es5": 130136,
"polyfills-es2015": 52931
"main-es5": 555102,
"main-es2015": 572938,
"polyfills-es5": 129161,
"polyfills-es2015": 53295
}
}
}

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime": 1440,
"main": 14847,
"main": 14912,
"polyfills": 43567
}
}
@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime": 1440,
"main": 149248,
"main": 147528,
"polyfills": 43567
}
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ɵRenderFlags, ɵrenderComponent as renderComponent, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵselect, ɵɵstyleProp, ɵɵstyling, ɵɵtext, ɵɵtextInterpolate1} from '@angular/core';
import {ɵRenderFlags, ɵrenderComponent as renderComponent, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵselect, ɵɵstyleProp, ɵɵstyling, ɵɵstylingApply, ɵɵtext, ɵɵtextInterpolate1} from '@angular/core';
import {bindAction, profile} from '../../util';
import {createDom, destroyDom, detectChanges} from '../render3/tree';
@ -32,13 +32,12 @@ export class TreeFunction {
});
}
const c1 = ['background-color'];
export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) {
if (rf & ɵRenderFlags.Create) {
ɵɵelementStart(0, 'tree');
{
ɵɵelementStart(1, 'span');
ɵɵstyling(null, c1);
ɵɵstyling();
{ ɵɵtext(2); }
ɵɵelementEnd();
ɵɵcontainer(3);
@ -48,8 +47,8 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) {
}
if (rf & ɵRenderFlags.Update) {
ɵɵselect(1);
ɵɵstyleProp(0, ctx.depth % 2 ? '' : 'grey');
ɵɵstyling();
ɵɵstyleProp('background-color', ctx.depth % 2 ? '' : 'grey');
ɵɵstylingApply();
ɵɵselect(2);
ɵɵtextInterpolate1(' ', ctx.value, ' ');
ɵɵcontainerRefreshStart(3);

View File

@ -478,24 +478,21 @@ describe('compiler compliance', () => {
const factory =
'factory: function MyComponent_Factory(t) { return new (t || MyComponent)(); }';
const template = `
const $e0_classBindings$ = ["error"];
const $e0_styleBindings$ = ["background-color"];
MyComponent.ngComponentDef = i0.ɵɵdefineComponent({type:MyComponent,selectors:[["my-component"]],
factory: function MyComponent_Factory(t){
return new (t || MyComponent)();
},
consts: 1,
vars: 0,
vars: 2,
template: function MyComponent_Template(rf,ctx){
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling($e0_classBindings$, $e0_styleBindings$);
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵstyleProp(0, ctx.color);
$r3$.ɵɵclassProp(0, ctx.error);
$r3$.ɵɵstyleProp("background-color", ctx.color);
$r3$.ɵɵclassProp("error", ctx.error);
$r3$.ɵɵstylingApply();
}
},

View File

@ -884,6 +884,7 @@ describe('compiler compliance: bindings', () => {
factory: function HostAttributeDir_Factory(t) { return new (t || HostAttributeDir)(); },
hostBindings: function HostAttributeDir_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(2);
$r3$.ɵɵelementHostAttrs($c1$);
}

View File

@ -6,10 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AttributeMarker, ViewEncapsulation} from '@angular/compiler/src/core';
import {CompilerStylingMode, compilerSetStylingMode} from '@angular/compiler/src/render3/view/styling_state';
import {AttributeMarker} from '@angular/compiler/src/core';
import {setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
describe('compiler compliance: styling', () => {
@ -385,10 +383,11 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling(null, null, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($ctx$.myStyleExp);
$r3$.ɵɵstylingApply();
}
@ -446,7 +445,7 @@ describe('compiler compliance: styling', () => {
const template = `
consts: 1,
vars: 1,
vars: 2,
template: function MyComponentWithInterpolation_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
@ -460,7 +459,7 @@ describe('compiler compliance: styling', () => {
}
consts: 1,
vars: 2,
vars: 3,
template: function MyComponentWithMuchosInterpolation_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
@ -474,7 +473,7 @@ describe('compiler compliance: styling', () => {
}
consts: 1,
vars: 0,
vars: 1,
template: function MyComponentWithoutInterpolation_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
@ -521,7 +520,6 @@ describe('compiler compliance: styling', () => {
const template = `
const $_c0$ = [${AttributeMarker.Styles}, "opacity", "1"];
const $_c1$ = ["width", "height"];
MyComponent.ngComponentDef = $r3$.ɵɵdefineComponent({
type: MyComponent,
@ -530,17 +528,18 @@ describe('compiler compliance: styling', () => {
return new (t || MyComponent)();
},
consts: 1,
vars: 1,
vars: 4,
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div", $_c0$);
$r3$.ɵɵstyling(null, $_c1$, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($ctx$.myStyleExp);
$r3$.ɵɵstyleProp(0, $ctx$.myWidth);
$r3$.ɵɵstyleProp(1, $ctx$.myHeight);
$r3$.ɵɵstyleProp("width", $ctx$.myWidth);
$r3$.ɵɵstyleProp("height", $ctx$.myHeight);
$r3$.ɵɵstylingApply();
$r3$.ɵɵattribute("style", "border-width: 10px", $r3$.ɵɵsanitizeStyle);
}
@ -575,13 +574,6 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $_c0$ = ["background-image"];
export class MyComponent {
constructor() {
this.myImage = 'url(foo.jpg)';
}
}
MyComponent.ngComponentDef = $r3$.ɵɵdefineComponent({
type: MyComponent,
selectors: [["my-component"]],
@ -589,15 +581,16 @@ describe('compiler compliance: styling', () => {
return new (t || MyComponent)();
},
consts: 1,
vars: 0,
vars: 1,
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling(null, _c0, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵstyleProp(0, ctx.myImage);
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleProp("background-image", ctx.myImage);
$r3$.ɵɵstylingApply();
}
},
@ -629,16 +622,14 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $e0_styles$ = ["font-size"];
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling(null, _c0);
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵstyleProp(0, 12, "px");
$r3$.ɵɵstyleProp("font-size", 12, "px");
$r3$.ɵɵstylingApply();
}
}
@ -646,7 +637,6 @@ describe('compiler compliance: styling', () => {
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should not create instructions for empty style bindings', () => {
@ -742,7 +732,6 @@ describe('compiler compliance: styling', () => {
const template = `
const $e0_attrs$ = [${AttributeMarker.Classes}, "grape"];
const $e0_bindings$ = ["apple", "orange"];
MyComponent.ngComponentDef = $r3$.ɵɵdefineComponent({
type: MyComponent,
@ -751,17 +740,17 @@ describe('compiler compliance: styling', () => {
return new (t || MyComponent)();
},
consts: 1,
vars: 1,
vars: 4,
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div", $e0_attrs$);
$r3$.ɵɵstyling($e0_bindings$);
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵclassMap($ctx$.myClassExp);
$r3$.ɵɵclassProp(0, $ctx$.yesToApple);
$r3$.ɵɵclassProp(1, $ctx$.yesToOrange);
$r3$.ɵɵclassProp("apple", $ctx$.yesToApple);
$r3$.ɵɵclassProp("orange", $ctx$.yesToOrange);
$r3$.ɵɵstylingApply();
$r3$.ɵɵattribute("class", "banana");
}
@ -874,10 +863,11 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling(null, null, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($ctx$.myStyleExp);
$r3$.ɵɵclassMap($ctx$.myClassExp);
$r3$.ɵɵstylingApply();
@ -915,14 +905,15 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling(null, null, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyling();
$r3$.ɵɵpipe(1, "stylePipe");
$r3$.ɵɵpipe(2, "classPipe");
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind1(1, 0, $ctx$.myStyleExp));
$r3$.ɵɵclassMap($r3$.ɵɵpipeBind1(2, 2, $ctx$.myClassExp));
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind1(1, 2, $ctx$.myStyleExp));
$r3$.ɵɵclassMap($r3$.ɵɵpipeBind1(2, 4, $ctx$.myClassExp));
$r3$.ɵɵstylingApply();
}
}
@ -963,13 +954,10 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $e0_classBindings$ = ["foo"];
const $e0_styleBindings$ = ["bar", "baz"];
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling($e0_classBindings$, $e0_styleBindings$, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyling();
$r3$.ɵɵpipe(1, "pipe");
$r3$.ɵɵpipe(2, "pipe");
$r3$.ɵɵpipe(3, "pipe");
@ -978,11 +966,12 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 1, $ctx$.myStyleExp, 1000));
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 6, $ctx$.myStyleExp, 1000));
$r3$.ɵɵclassMap($e2_styling$);
$r3$.ɵɵstyleProp(0, $r3$.ɵɵpipeBind2(2, 4, $ctx$.barExp, 3000));
$r3$.ɵɵstyleProp(1, $r3$.ɵɵpipeBind2(3, 7, $ctx$.bazExp, 4000));
$r3$.ɵɵclassProp(0, $r3$.ɵɵpipeBind2(4, 10, $ctx$.fooExp, 2000));
$r3$.ɵɵstyleProp("bar", $r3$.ɵɵpipeBind2(2, 9, $ctx$.barExp, 3000));
$r3$.ɵɵstyleProp("baz", $r3$.ɵɵpipeBind2(3, 12, $ctx$.bazExp, 4000));
$r3$.ɵɵclassProp("foo", $r3$.ɵɵpipeBind2(4, 15, $ctx$.fooExp, 2000));
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(5);
$r3$.ɵɵtextInterpolate1(" ", $ctx$.item, "");
@ -1027,16 +1016,16 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 2) {
$r3$.ɵɵstyleProp(0, $ctx$.w1);
$r3$.ɵɵstyleProp("width", $ctx$.w1);
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(1);
$r3$.ɵɵstyleProp(0, $ctx$.h1);
$r3$.ɵɵstyleProp("height", $ctx$.h1);
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(2);
$r3$.ɵɵclassProp(0, $ctx$.a1);
$r3$.ɵɵclassProp("active", $ctx$.a1);
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(3);
$r3$.ɵɵclassProp(0, $ctx$.r1);
$r3$.ɵɵclassProp("removed", $ctx$.r1);
$r3$.ɵɵstylingApply();
}
}
@ -1083,20 +1072,18 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $e0_attrs$ = [${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"];
const $e0_classBindings$ = ["foo"];
const $e0_styleBindings$ = ["color"];
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(4);
$r3$.ɵɵelementHostAttrs($e0_attrs$);
$r3$.ɵɵstyling($e0_classBindings$, $e0_styleBindings$, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyle);
$r3$.ɵɵclassMap(ctx.myClass);
$r3$.ɵɵstyleProp(0, ctx.myColorProp);
$r3$.ɵɵclassProp(0, ctx.myFooClass);
$r3$.ɵɵstyleProp("color", ctx.myColorProp);
$r3$.ɵɵclassProp("foo", ctx.myFooClass);
$r3$.ɵɵstylingApply();
}
},
@ -1146,20 +1133,19 @@ describe('compiler compliance: styling', () => {
};
const template = `
const _c0 = ["bar", "foo"];
const _c1 = ["height", "width"];
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵstyling(_c0, _c1, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵallocHostVars(6);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyle);
$r3$.ɵɵclassMap(ctx.myClasses);
$r3$.ɵɵstyleProp(0, ctx.myHeightProp, "pt");
$r3$.ɵɵstyleProp(1, ctx.myWidthProp);
$r3$.ɵɵclassProp(0, ctx.myBarClass);
$r3$.ɵɵclassProp(1, ctx.myFooClass);
$r3$.ɵɵstyleProp("height", ctx.myHeightProp, "pt");
$r3$.ɵɵstyleProp("width", ctx.myWidthProp);
$r3$.ɵɵclassProp("bar", ctx.myBarClass);
$r3$.ɵɵclassProp("foo", ctx.myFooClass);
$r3$.ɵɵstylingApply();
}
},
@ -1209,38 +1195,35 @@ describe('compiler compliance: styling', () => {
};
const template = `
const _c2 = ["bar"];
const _c3 = ["height"];
function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵstyling(_c2, _c3, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyling();
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyleExp);
$r3$.ɵɵclassMap(ctx.myClassExp);
$r3$.ɵɵstyleProp(0, ctx.myHeightExp, null, true);
$r3$.ɵɵclassProp(0, ctx.myBarClassExp, true);
$r3$.ɵɵstyleProp("height", ctx.myHeightExp);
$r3$.ɵɵclassProp("bar", ctx.myBarClassExp);
$r3$.ɵɵstylingApply();
}
},
`;
const hostBindings = `
const _c0 = ["foo"];
const _c1 = ["width"];
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵstyling(_c0, _c1, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵallocHostVars(4);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyleExp);
$r3$.ɵɵclassMap(ctx.myClassExp);
$r3$.ɵɵstyleProp(0, ctx.myWidthExp, null, true);
$r3$.ɵɵclassProp(0, ctx.myFooClassExp, true);
$r3$.ɵɵstyleProp("width", ctx.myWidthExp);
$r3$.ɵɵclassProp("foo", ctx.myFooClassExp);
$r3$.ɵɵstylingApply();
}
},
@ -1296,13 +1279,9 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $widthDir_classes$ = ["foo"];
const $widthDir_styles$ = ["width"];
const $heightDir_classes$ = ["bar"];
const $heightDir_styles$ = ["height"];
function ClassDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(1);
$r3$.ɵɵstyling();
}
if (rf & 2) {
@ -1313,22 +1292,24 @@ describe('compiler compliance: styling', () => {
function WidthDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵstyling($widthDir_classes$, $widthDir_styles$);
$r3$.ɵɵallocHostVars(2);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵstyleProp(0, ctx.myWidth);
$r3$.ɵɵclassProp(0, ctx.myFooClass);
$r3$.ɵɵstyleProp("width", ctx.myWidth);
$r3$.ɵɵclassProp("foo", ctx.myFooClass);
$r3$.ɵɵstylingApply();
}
}
function HeightDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵstyling($heightDir_classes$, $heightDir_styles$);
$r3$.ɵɵallocHostVars(2);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵstyleProp(0, ctx.myHeight);
$r3$.ɵɵclassProp(0, ctx.myBarClass);
$r3$.ɵɵstyleProp("height", ctx.myHeight);
$r3$.ɵɵclassProp("bar", ctx.myBarClass);
$r3$.ɵɵstylingApply();
}
}
@ -1472,34 +1453,34 @@ describe('compiler compliance: styling', () => {
const template = `
if (rf & 2) {
$r3$.ɵɵstylePropInterpolateV(0, ["a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i", ctx.nine, "j"]);
$r3$.ɵɵstylePropInterpolateV("color", ["a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i", ctx.nine, "j"]);
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(1);
$r3$.ɵɵstylePropInterpolate8(0, "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i");
$r3$.ɵɵstylePropInterpolate8("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i");
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(2);
$r3$.ɵɵstylePropInterpolate7(0, "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h");
$r3$.ɵɵstylePropInterpolate7("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h");
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(3);
$r3$.ɵɵstylePropInterpolate6(0, "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g");
$r3$.ɵɵstylePropInterpolate6("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g");
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(4);
$r3$.ɵɵstylePropInterpolate5(0, "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f");
$r3$.ɵɵstylePropInterpolate5("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f");
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(5);
$r3$.ɵɵstylePropInterpolate4(0, "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e");
$r3$.ɵɵstylePropInterpolate4("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e");
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(6);
$r3$.ɵɵstylePropInterpolate3(0, "a", ctx.one, "b", ctx.two, "c", ctx.three, "d");
$r3$.ɵɵstylePropInterpolate3("color", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d");
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(7);
$r3$.ɵɵstylePropInterpolate2(0, "a", ctx.one, "b", ctx.two, "c");
$r3$.ɵɵstylePropInterpolate2("color", "a", ctx.one, "b", ctx.two, "c");
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(8);
$r3$.ɵɵstylePropInterpolate1(0, "a", ctx.one, "b");
$r3$.ɵɵstylePropInterpolate1("color", "a", ctx.one, "b");
$r3$.ɵɵstylingApply();
$r3$.ɵɵselect(9);
$r3$.ɵɵstyleProp(0, ctx.one);
$r3$.ɵɵstyleProp("color", ctx.one);
$r3$.ɵɵstylingApply();
}
@ -1530,7 +1511,7 @@ describe('compiler compliance: styling', () => {
const template = `
if (rf & 2) {
$r3$.ɵɵstylePropInterpolate2(0, "a", ctx.one, "b", ctx.two, "c", "px");
$r3$.ɵɵstylePropInterpolate2("width", "a", ctx.one, "b", ctx.two, "c", "px");
$r3$.ɵɵstylingApply();
}
@ -1561,7 +1542,7 @@ describe('compiler compliance: styling', () => {
const template = `
if (rf & 2) {
$r3$.ɵɵstylePropInterpolate2(0, "a", ctx.one, "b", ctx.two, "c", null, true);
$r3$.ɵɵstylePropInterpolate2("width", "a", ctx.one, "b", ctx.two, "c");
$r3$.ɵɵstylingApply();
}
@ -1616,12 +1597,13 @@ describe('compiler compliance: styling', () => {
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(2);
$r3$.ɵɵallocHostVars(4);
$r3$.ɵɵelementHostAttrs($_c0$);
$r3$.ɵɵstyling(null, null, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title);
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.myStyle);
$r3$.ɵɵclassMap(ctx.myClass);
$r3$.ɵɵstylingApply();
@ -1658,18 +1640,15 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $_c0$ = ["foo"];
const $_c1$ = ["width"];
hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(2);
$r3$.ɵɵstyling($_c0$, $_c1$);
$r3$.ɵɵallocHostVars(4);
$r3$.ɵɵstyling();
}
if (rf & 2) {
$r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title);
$r3$.ɵɵstyleProp(0, ctx.myWidth);
$r3$.ɵɵclassProp(0, ctx.myFooClass);
$r3$.ɵɵstyleProp("width", ctx.myWidth);
$r3$.ɵɵclassProp("foo", ctx.myFooClass);
$r3$.ɵɵstylingApply();
}
}
@ -1680,10 +1659,6 @@ describe('compiler compliance: styling', () => {
});
describe('new styling refactor', () => {
beforeEach(() => { compilerSetStylingMode(CompilerStylingMode.UseNew); });
afterEach(() => { compilerSetStylingMode(CompilerStylingMode.UseOld); });
it('should generate a `styleSanitizer` instruction when one or more sanitizable style properties are statically detected',
() => {
const files = {
@ -1709,7 +1684,7 @@ describe('compiler compliance: styling', () => {
if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleProp(0, ctx.bgExp);
$r3$.ɵɵstyleProp("background-image", ctx.bgExp);
$r3$.ɵɵstylingApply();
}
@ -1782,7 +1757,7 @@ describe('compiler compliance: styling', () => {
if (rf & 2) {
$r3$.ɵɵclassMap(ctx.mapExp);
$r3$.ɵɵclassProp(0, ctx.nameExp);
$r3$.ɵɵclassProp("name", ctx.nameExp);
$r3$.ɵɵstylingApply();
}
@ -1792,5 +1767,67 @@ describe('compiler compliance: styling', () => {
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should generate the correct amount of host bindings when styling is present', () => {
const files = {
app: {
'spec.ts': `
import {Component, Directive, NgModule} from '@angular/core';
@Directive({
selector: '[my-dir]',
host: {
'[title]': 'title',
'[class.foo]': 'foo',
'[@anim]': \`{
value: _animValue,
params: {
param1: _animParam1,
param2: _animParam2
}
}\`
}
})
export class MyDir {
title = '';
foo = true;
_animValue = null;
_animParam1 = null;
_animParam2 = null;
}
@Component({
selector: 'my-app',
template: \`
<div my-dir></div>
\`
})
export class MyAppComp {}
@NgModule({declarations: [MyAppComp, MyDir]})
export class MyModule {}
`
}
};
const template = `
hostBindings: function MyDir_HostBindings(rf, ctx, elIndex) {
$r3$.ɵɵallocHostVars(9);
if (rf & 2) {
$r3$.ɵɵhostProperty("title", ctx.title);
$r3$.ɵɵupdateSyntheticHostBinding("@anim",
$r3$.ɵɵpureFunction2(6, _c1, ctx._animValue,
$r3$.ɵɵpureFunction2(3, _c0, ctx._animParam1, ctx._animParam2)));
$r3$.ɵɵclassProp("foo", ctx.foo);
$r3$.ɵɵstylingApply();
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});
});

View File

@ -1825,16 +1825,16 @@ runInEachFileSystem(os => {
const hostBindingsFn = `
hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
i0.ɵɵallocHostVars(2);
i0.ɵɵallocHostVars(3);
i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); });
i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody);
i0.ɵɵlistener("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); });
i0.ɵɵstyling(_c0);
i0.ɵɵstyling();
}
if (rf & 2) {
i0.ɵɵhostProperty("prop", ctx.bar);
i0.ɵɵattribute("hello", ctx.foo);
i0.ɵɵclassProp(0, ctx.someClass);
i0.ɵɵclassProp("someclass", ctx.someClass);
i0.ɵɵstylingApply();
}
}

View File

@ -586,14 +586,8 @@ function createHostBindingsFunction(
hostBindingsMetadata: R3HostMetadata, typeSourceSpan: ParseSourceSpan,
bindingParser: BindingParser, constantPool: ConstantPool, selector: string,
name?: string): o.Expression|null {
// Initialize hostVarsCount to number of bound host properties (interpolations illegal),
// except 'style' and 'class' properties, since they should *not* allocate host var slots
const hostVarsCount = Object.keys(hostBindingsMetadata.properties)
.filter(name => {
const prefix = getStylingPrefix(name);
return prefix !== 'style' && prefix !== 'class';
})
.length;
// Initialize hostVarsCount to number of bound host properties (interpolations illegal)
const hostVarsCount = Object.keys(hostBindingsMetadata.properties).length;
const elVarExp = o.variable('elIndex');
const bindingContext = o.variable(CONTEXT_NAME);
const styleBuilder = new StylingBuilder(elVarExp, bindingContext);
@ -733,7 +727,10 @@ function createHostBindingsFunction(
// the update block of a component/directive templateFn/hostBindingsFn so that the bindings
// are evaluated and updated for the element.
styleBuilder.buildUpdateLevelInstructions(getValueConverter()).forEach(instruction => {
totalHostVarsCount += instruction.allocateBindingSlots;
// we subtract a value of `1` here because the binding slot was already
// allocated at the top of this method when all the input bindings were
// counted.
totalHostVarsCount += Math.max(instruction.allocateBindingSlots - 1, 0);
updateStatements.push(createStylingStmt(instruction, bindingContext, bindingFn));
});
}

View File

@ -15,8 +15,7 @@ import {error} from '../../util';
import * as t from '../r3_ast';
import {Identifiers as R3} from '../r3_identifiers';
import {parse as parseStyle} from './style_parser';
import {compilerIsNewStylingInUse} from './styling_state';
import {hyphenate, parse as parseStyle} from './style_parser';
import {ValueConverter} from './template';
import {getInterpolationArgsLength} from './util';
@ -69,7 +68,7 @@ interface BoundStylingEntry {
* classMap(...)
* styleProp(...)
* classProp(...)
* stylingApp(...)
* stylingApply(...)
* }
*
* The creation/update methods within the builder class produce these instructions.
@ -171,6 +170,7 @@ export class StylingBuilder {
if (isEmptyExpression(value)) {
return null;
}
name = normalizePropName(name);
const {property, hasOverrideFlag, unit: bindingUnit} = parseProperty(name);
const entry: BoundStylingEntry = {
name: property,
@ -296,46 +296,7 @@ export class StylingBuilder {
sourceSpan,
allocateBindingSlots: 0,
reference: R3.styling,
params: () => {
// a string array of every style-based binding
const styleBindingProps =
this._singleStyleInputs ? this._singleStyleInputs.map(i => o.literal(i.name)) : [];
// a string array of every class-based binding
const classBindingNames =
this._singleClassInputs ? this._singleClassInputs.map(i => o.literal(i.name)) : [];
// to salvage space in the AOT generated code, there is no point in passing
// in `null` into a param if any follow-up params are not used. Therefore,
// only when a trailing param is used then it will be filled with nulls in between
// (otherwise a shorter amount of params will be filled). The code below helps
// determine how many params are required in the expression code.
//
// min params => styling()
// max params => styling(classBindings, styleBindings, sanitizer)
//
const params: o.Expression[] = [];
let expectedNumberOfArgs = 0;
if (this._useDefaultSanitizer) {
expectedNumberOfArgs = 3;
} else if (styleBindingProps.length) {
expectedNumberOfArgs = 2;
} else if (classBindingNames.length) {
expectedNumberOfArgs = 1;
}
addParam(
params, classBindingNames.length > 0,
getConstantLiteralFromArray(constantPool, classBindingNames), 1,
expectedNumberOfArgs);
addParam(
params, styleBindingProps.length > 0,
getConstantLiteralFromArray(constantPool, styleBindingProps), 2,
expectedNumberOfArgs);
addParam(
params, this._useDefaultSanitizer, o.importExpr(R3.defaultStyleSanitizer), 3,
expectedNumberOfArgs);
return params;
}
params: () => [],
};
}
return null;
@ -370,12 +331,8 @@ export class StylingBuilder {
private _buildMapBasedInstruction(
valueConverter: ValueConverter, isClassBased: boolean,
stylingInput: BoundStylingEntry): StylingInstruction {
let totalBindingSlotsRequired = 0;
if (compilerIsNewStylingInUse()) {
// the old implementation does not reserve slot values for
// binding entries. The new one does.
totalBindingSlotsRequired++;
}
// each styling binding value is stored in the LView
let totalBindingSlotsRequired = 1;
// these values must be outside of the update block so that they can
// be evaluated (the AST visit call) during creation time so that any
@ -408,8 +365,11 @@ export class StylingBuilder {
StylingInstruction[] {
let totalBindingSlotsRequired = 0;
return inputs.map(input => {
const bindingIndex: number = mapIndex.get(input.name !) !;
const value = input.value.visit(valueConverter);
// each styling binding value is stored in the LView
let totalBindingSlotsRequired = 1;
if (value instanceof Interpolation) {
totalBindingSlotsRequired += value.expressions.length;
@ -417,35 +377,25 @@ export class StylingBuilder {
reference = getInterpolationExpressionFn(value);
}
}
if (compilerIsNewStylingInUse()) {
// the old implementation does not reserve slot values for
// binding entries. The new one does.
totalBindingSlotsRequired++;
}
return {
sourceSpan: input.sourceSpan,
supportsInterpolation: !!getInterpolationExpressionFn,
allocateBindingSlots: totalBindingSlotsRequired, reference,
params: (convertFn: (value: any) => o.Expression | o.Expression[]) => {
// min params => stylingProp(elmIndex, bindingIndex, value)
// max params => stylingProp(elmIndex, bindingIndex, value, overrideFlag)
const params: o.Expression[] = [o.literal(bindingIndex)];
// params => stylingProp(propName, value)
const params: o.Expression[] = [];
params.push(o.literal(input.name));
const convertResult = convertFn(value);
if (Array.isArray(convertResult)) {
params.push(...convertResult);
} else {
params.push(convertResult);
}
if (allowUnits) {
if (input.unit) {
params.push(o.literal(input.unit));
} else if (input.hasOverrideFlag) {
params.push(o.NULL_EXPR);
}
}
if (input.hasOverrideFlag) {
params.push(o.literal(true));
if (allowUnits && input.unit) {
params.push(o.literal(input.unit));
}
return params;
@ -496,7 +446,7 @@ export class StylingBuilder {
buildUpdateLevelInstructions(valueConverter: ValueConverter) {
const instructions: StylingInstruction[] = [];
if (this.hasBindings) {
if (compilerIsNewStylingInUse() && this._useDefaultSanitizer) {
if (this._useDefaultSanitizer) {
instructions.push(this._buildSanitizerFn());
}
const styleMapInstruction = this.buildStyleMapInstruction(valueConverter);
@ -630,3 +580,7 @@ function getStylePropInterpolationExpression(interpolation: Interpolation) {
return R3.stylePropInterpolateV;
}
}
function normalizePropName(prop: string): string {
return hyphenate(prop);
}

View File

@ -1,37 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* A temporary enum of states that inform the core whether or not
* to defer all styling instruction calls to the old or new
* styling implementation.
*/
export const enum CompilerStylingMode {
UseOld = 0,
UseBothOldAndNew = 1,
UseNew = 2,
}
let _stylingMode = 0;
/**
* Temporary function used to inform the existing styling algorithm
* code to delegate all styling instruction calls to the new refactored
* styling code.
*/
export function compilerSetStylingMode(mode: CompilerStylingMode) {
_stylingMode = mode;
}
export function compilerIsNewStylingInUse() {
return _stylingMode > CompilerStylingMode.UseOld;
}
export function compilerAllowOldStyling() {
return _stylingMode < CompilerStylingMode.UseNew;
}

View File

@ -122,6 +122,7 @@ export {
ɵɵelementContainer,
ɵɵstyling,
ɵɵstyleMap,
ɵɵstyleSanitizer,
ɵɵclassMap,
ɵɵclassMapInterpolate1,
ɵɵclassMapInterpolate2,

View File

@ -10,11 +10,12 @@ import {Injector} from '../di';
import {getViewComponent} from '../render3/global_utils_api';
import {CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from '../render3/interfaces/container';
import {TElementNode, TNode, TNodeFlags, TNodeType} from '../render3/interfaces/node';
import {StylingIndex} from '../render3/interfaces/styling';
import {isComponent, isLContainer} from '../render3/interfaces/type_checks';
import {LView, PARENT, TData, TVIEW, T_HOST} from '../render3/interfaces/view';
import {getProp, getValue, isClassBasedValue} from '../render3/styling/class_and_style_bindings';
import {getStylingContextFromLView} from '../render3/styling/util';
import {TStylingContext} from '../render3/styling_next/interfaces';
import {stylingMapToStringMap} from '../render3/styling_next/map_based_bindings';
import {NodeStylingDebug} from '../render3/styling_next/styling_debug';
import {isStylingContext} from '../render3/styling_next/util';
import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, getLocalRefs, isBrowserEvents, loadLContext, loadLContextFromNode} from '../render3/util/discovery_utils';
import {INTERPOLATION_DELIMITER, isPropMetadataString, renderStringify} from '../render3/util/misc_utils';
import {findComponentView} from '../render3/util/view_traversal_utils';
@ -328,63 +329,12 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
return attributes;
}
get classes(): {[key: string]: boolean;} {
const classes: {[key: string]: boolean;} = {};
const element = this.nativeElement;
if (element) {
const lContext = loadLContextFromNode(element);
const stylingContext = getStylingContextFromLView(lContext.nodeIndex, lContext.lView);
if (stylingContext) {
for (let i = StylingIndex.SingleStylesStartPosition; i < stylingContext.length;
i += StylingIndex.Size) {
if (isClassBasedValue(stylingContext, i)) {
const className = getProp(stylingContext, i);
const value = getValue(stylingContext, i);
if (typeof value == 'boolean') {
// we want to ignore `null` since those don't overwrite the values.
classes[className] = value;
}
}
}
} else {
// Fallback, just read DOM.
const eClasses = element.classList;
for (let i = 0; i < eClasses.length; i++) {
classes[eClasses[i]] = true;
}
}
}
return classes;
get styles(): {[key: string]: string | null;} {
return _getStylingDebugInfo(this.nativeElement, false);
}
get styles(): {[key: string]: string | null;} {
const styles: {[key: string]: string | null;} = {};
const element = this.nativeElement;
if (element) {
const lContext = loadLContextFromNode(element);
const stylingContext = getStylingContextFromLView(lContext.nodeIndex, lContext.lView);
if (stylingContext) {
for (let i = StylingIndex.SingleStylesStartPosition; i < stylingContext.length;
i += StylingIndex.Size) {
if (!isClassBasedValue(stylingContext, i)) {
const styleName = getProp(stylingContext, i);
const value = getValue(stylingContext, i) as string | null;
if (value !== null) {
// we want to ignore `null` since those don't overwrite the values.
styles[styleName] = value;
}
}
}
} else {
// Fallback, just read DOM.
const eStyles = (element as HTMLElement).style;
for (let i = 0; i < eStyles.length; i++) {
const name = eStyles.item(i);
styles[name] = eStyles.getPropertyValue(name);
}
}
}
return styles;
get classes(): {[key: string]: boolean;} {
return _getStylingDebugInfo(this.nativeElement, true);
}
get childNodes(): DebugNode[] {
@ -435,6 +385,25 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
}
}
function _getStylingDebugInfo(element: any, isClassBased: boolean) {
if (element) {
const context = loadLContextFromNode(element);
const lView = context.lView;
const tData = lView[TVIEW].data;
const tNode = tData[context.nodeIndex] as TNode;
if (isClassBased) {
return isStylingContext(tNode.classes) ?
new NodeStylingDebug(tNode.classes as TStylingContext, lView, true).values :
stylingMapToStringMap(tNode.classes);
} else {
return isStylingContext(tNode.styles) ?
new NodeStylingDebug(tNode.styles as TStylingContext, lView, false).values :
stylingMapToStringMap(tNode.styles);
}
}
return {};
}
/**
* Walk the TNode tree to find matches for the predicate.
*

View File

@ -17,7 +17,7 @@ import {assertComponentType} from './assert';
import {getComponentDef} from './definition';
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
import {registerPostOrderHooks, registerPreOrderHooks} from './hooks';
import {CLEAN_PROMISE, addToViewTree, createLView, createTView, getOrCreateTNode, getOrCreateTView, initNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions/shared';
import {CLEAN_PROMISE, addToViewTree, createLView, createTView, getOrCreateTNode, getOrCreateTView, initNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews, renderInitialStyling} from './instructions/shared';
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node';
import {PlayerHandler} from './interfaces/player';
@ -230,10 +230,10 @@ export function createRootComponent<T>(
setActiveHostElement(null);
}
if (rootTNode.stylingTemplate) {
if (rootTNode.classes !== null || rootTNode.styles !== null) {
const native = componentView[HOST] !as RElement;
renderInitialClasses(native, rootTNode.stylingTemplate, componentView[RENDERER]);
renderInitialStyles(native, rootTNode.stylingTemplate, componentView[RENDERER]);
const renderer = componentView[RENDERER];
renderInitialStyling(renderer, native, rootTNode);
}
return component;

View File

@ -22,6 +22,7 @@ import {isComponent, isComponentDef} from './interfaces/type_checks';
import {DECLARATION_VIEW, INJECTOR, LView, TData, TVIEW, TView, T_HOST} from './interfaces/view';
import {assertNodeOfPossibleTypes} from './node_assert';
import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state';
import {getInitialStylingValue} from './styling_next/util';
import {isNameOnlyAttributeMarker} from './util/attrs_utils';
import {getParentInjectorIndex, getParentInjectorView, hasParentInjector} from './util/injector_utils';
import {stringifyForError} from './util/misc_utils';
@ -269,6 +270,13 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str
ngDevMode && assertNodeOfPossibleTypes(
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
ngDevMode && assertDefined(tNode, 'expecting tNode');
if (attrNameToInject === 'class') {
return getInitialStylingValue(tNode.classes);
}
if (attrNameToInject === 'style') {
return getInitialStylingValue(tNode.styles);
}
const attrs = tNode.attrs;
if (attrs) {
const attrsLength = attrs.length;
@ -289,22 +297,8 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str
} else if (typeof value === 'number') {
// Skip to the first value of the marked attribute.
i++;
if (value === AttributeMarker.Classes && attrNameToInject === 'class') {
let accumulatedClasses = '';
while (i < attrsLength && typeof attrs[i] === 'string') {
accumulatedClasses += ' ' + attrs[i++];
}
return accumulatedClasses.trim();
} else if (value === AttributeMarker.Styles && attrNameToInject === 'style') {
let accumulatedStyles = '';
while (i < attrsLength && typeof attrs[i] === 'string') {
accumulatedStyles += `${attrs[i++]}: ${attrs[i++]}; `;
}
return accumulatedStyles.trim();
} else {
while (i < attrsLength && typeof attrs[i] === 'string') {
i++;
}
while (i < attrsLength && typeof attrs[i] === 'string') {
i++;
}
} else if (value === attrNameToInject) {
return attrs[i + 1] as string;

View File

@ -43,8 +43,7 @@ export * from './projection';
export * from './property';
export * from './property_interpolation';
export * from './select';
export * from './styling';
export {styleSanitizer as ɵɵstyleSanitizer} from '../styling_next/instructions';
export {ɵɵstyling, ɵɵstyleSanitizer, ɵɵstyleMap, ɵɵclassMap, ɵɵstyleProp, ɵɵclassProp, ɵɵstylingApply} from '../styling_next/instructions';
export * from './text';
export * from './text_interpolation';
export * from './class_map_interpolation';

View File

@ -7,10 +7,9 @@
*/
import {getLView, getSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {classMapInternal} from '../styling_next/instructions';
import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation';
import {classMapInternal, getActiveDirectiveStylingIndex} from './styling';
/**
@ -37,10 +36,7 @@ import {classMapInternal, getActiveDirectiveStylingIndex} from './styling';
export function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void {
const lView = getLView();
const interpolatedValue = interpolation1(lView, prefix, v0, suffix);
if (interpolatedValue !== NO_CHANGE) {
classMapInternal(
lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue);
}
classMapInternal(getSelectedIndex(), interpolatedValue);
}
/**
@ -70,10 +66,7 @@ export function ɵɵclassMapInterpolate2(
prefix: string, v0: any, i0: string, v1: any, suffix: string): void {
const lView = getLView();
const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix);
if (interpolatedValue !== NO_CHANGE) {
classMapInternal(
lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue);
}
classMapInternal(getSelectedIndex(), interpolatedValue);
}
/**
@ -106,10 +99,7 @@ export function ɵɵclassMapInterpolate3(
prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): void {
const lView = getLView();
const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix);
if (interpolatedValue !== NO_CHANGE) {
classMapInternal(
lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue);
}
classMapInternal(getSelectedIndex(), interpolatedValue);
}
/**
@ -145,10 +135,7 @@ export function ɵɵclassMapInterpolate4(
suffix: string): void {
const lView = getLView();
const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix);
if (interpolatedValue !== NO_CHANGE) {
classMapInternal(
lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue);
}
classMapInternal(getSelectedIndex(), interpolatedValue);
}
/**
@ -187,10 +174,7 @@ export function ɵɵclassMapInterpolate5(
const lView = getLView();
const interpolatedValue =
interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix);
if (interpolatedValue !== NO_CHANGE) {
classMapInternal(
lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue);
}
classMapInternal(getSelectedIndex(), interpolatedValue);
}
/**
@ -231,10 +215,7 @@ export function ɵɵclassMapInterpolate6(
const lView = getLView();
const interpolatedValue =
interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix);
if (interpolatedValue !== NO_CHANGE) {
classMapInternal(
lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue);
}
classMapInternal(getSelectedIndex(), interpolatedValue);
}
/**
@ -277,10 +258,7 @@ export function ɵɵclassMapInterpolate7(
const lView = getLView();
const interpolatedValue =
interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix);
if (interpolatedValue !== NO_CHANGE) {
classMapInternal(
lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue);
}
classMapInternal(getSelectedIndex(), interpolatedValue);
}
/**
@ -326,10 +304,7 @@ export function ɵɵclassMapInterpolate8(
const lView = getLView();
const interpolatedValue = interpolation8(
lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix);
if (interpolatedValue !== NO_CHANGE) {
classMapInternal(
lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue);
}
classMapInternal(getSelectedIndex(), interpolatedValue);
}
/**
@ -358,8 +333,5 @@ export function ɵɵclassMapInterpolate8(
export function ɵɵclassMapInterpolateV(values: any[]): void {
const lView = getLView();
const interpolatedValue = interpolationV(lView, values);
if (interpolatedValue !== NO_CHANGE) {
classMapInternal(
lView, getSelectedIndex(), getActiveDirectiveStylingIndex(), interpolatedValue);
}
classMapInternal(getSelectedIndex(), interpolatedValue);
}

View File

@ -9,23 +9,20 @@ import {assertDataInRange, assertDefined, assertEqual} from '../../util/assert';
import {assertHasParent} from '../assert';
import {attachPatchData} from '../context_discovery';
import {registerPostOrderHooks} from '../hooks';
import {TAttributes, TNodeFlags, TNodeType} from '../interfaces/node';
import {PropertyAliases, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {StylingContext} from '../interfaces/styling';
import {BINDING_INDEX, HEADER_OFFSET, QUERIES, RENDERER, TVIEW, T_HOST} from '../interfaces/view';
import {BINDING_INDEX, HEADER_OFFSET, LView, QUERIES, RENDERER, TVIEW, T_HOST} from '../interfaces/view';
import {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation';
import {applyOnCreateInstructions} from '../node_util';
import {decreaseElementDepthCount, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state';
import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles} from '../styling/class_and_style_bindings';
import {getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util';
import {registerInitialStylingIntoContext} from '../styling_next/instructions';
import {runtimeIsNewStylingInUse} from '../styling_next/state';
import {attrsStylingIndexOf, setUpAttributes} from '../util/attrs_utils';
import {registerInitialStylingOnTNode} from '../styling_next/instructions';
import {StylingMapArray, TStylingContext} from '../styling_next/interfaces';
import {getInitialStylingValue, hasClassInput, hasStyleInput} from '../styling_next/util';
import {setUpAttributes} from '../util/attrs_utils';
import {getNativeByTNode, getTNode} from '../util/view_utils';
import {createDirectivesAndLocals, elementCreate, executeContentQueries, getOrCreateTNode, initializeTNodeInputs, setInputsForProperty, setNodeStylingTemplate} from './shared';
import {getActiveDirectiveStylingIndex} from './styling';
import {createDirectivesAndLocals, elementCreate, executeContentQueries, getOrCreateTNode, initializeTNodeInputs, renderInitialStyling, setInputsForProperty} from './shared';
@ -58,32 +55,16 @@ export function ɵɵelementStart(
const renderer = lView[RENDERER];
const tNode =
getOrCreateTNode(tView, lView[T_HOST], index, TNodeType.Element, name, attrs || null);
let initialStylesIndex = 0;
let initialClassesIndex = 0;
let lastAttrIndex = -1;
if (attrs) {
lastAttrIndex = setUpAttributes(native, attrs);
// it's important to only prepare styling-related datastructures once for a given
// tNode and not each time an element is created. Also, the styling code is designed
// to be patched and constructed at various points, but only up until the styling
// template is first allocated (which happens when the very first style/class binding
// value is evaluated). When the template is allocated (when it turns into a context)
// then the styling template is locked and cannot be further extended (it can only be
// instantiated into a context per element)
setNodeStylingTemplate(tView, tNode, attrs, lastAttrIndex);
const stylingTemplate = tNode.stylingTemplate;
if (stylingTemplate) {
// the initial style/class values are rendered immediately after having been
// initialized into the context so the element styling is ready when directives
// are initialized (since they may read style/class values in their constructor)
initialStylesIndex = renderInitialStyles(native, stylingTemplate, renderer);
initialClassesIndex = renderInitialClasses(native, stylingTemplate, renderer);
if (attrs != null) {
const lastAttrIndex = setUpAttributes(native, attrs);
if (tView.firstTemplatePass) {
registerInitialStylingOnTNode(tNode, attrs, lastAttrIndex);
}
}
renderInitialStyling(renderer, native, tNode);
appendChild(native, tNode, lView);
createDirectivesAndLocals(tView, lView, tNode, localRefs);
@ -104,22 +85,12 @@ export function ɵɵelementStart(
if (inputData && inputData.hasOwnProperty('class')) {
tNode.flags |= TNodeFlags.hasClassInput;
}
if (inputData && inputData.hasOwnProperty('style')) {
tNode.flags |= TNodeFlags.hasStyleInput;
}
}
// we render the styling again below in case any directives have set any `style` and/or
// `class` host attribute values...
if (tNode.stylingTemplate) {
renderInitialClasses(native, tNode.stylingTemplate, renderer, initialClassesIndex);
renderInitialStyles(native, tNode.stylingTemplate, renderer, initialStylesIndex);
}
if (runtimeIsNewStylingInUse() && lastAttrIndex >= 0) {
registerInitialStylingIntoContext(tNode, attrs as TAttributes, lastAttrIndex);
}
const currentQueries = lView[QUERIES];
if (currentQueries) {
currentQueries.addNode(tNode);
@ -144,36 +115,29 @@ export function ɵɵelementEnd(): void {
setPreviousOrParentTNode(previousOrParentTNode, false);
}
const tNode = previousOrParentTNode;
// this is required for all host-level styling-related instructions to run
// in the correct order
previousOrParentTNode.onElementCreationFns && applyOnCreateInstructions(previousOrParentTNode);
tNode.onElementCreationFns && applyOnCreateInstructions(tNode);
ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Element);
ngDevMode && assertNodeType(tNode, TNodeType.Element);
const lView = getLView();
const currentQueries = lView[QUERIES];
// Go back up to parent queries only if queries have been cloned on this element.
if (currentQueries && previousOrParentTNode.index === currentQueries.nodeIndex) {
if (currentQueries && tNode.index === currentQueries.nodeIndex) {
lView[QUERIES] = currentQueries.parent;
}
registerPostOrderHooks(lView[TVIEW], previousOrParentTNode);
registerPostOrderHooks(lView[TVIEW], tNode);
decreaseElementDepthCount();
// this is fired at the end of elementEnd because ALL of the stylingBindings code
// (for directives and the template) have now executed which means the styling
// context can be instantiated properly.
let stylingContext: StylingContext|null = null;
if (hasClassInput(previousOrParentTNode)) {
stylingContext = getStylingContextFromLView(previousOrParentTNode.index, lView);
setInputsForProperty(
lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext));
if (hasClassInput(tNode) && tNode.classes) {
setDirectiveStylingInput(tNode.classes, lView, tNode.inputs !['class']);
}
if (hasStyleInput(previousOrParentTNode)) {
stylingContext =
stylingContext || getStylingContextFromLView(previousOrParentTNode.index, lView);
setInputsForProperty(
lView, previousOrParentTNode.inputs !['style'] !,
getInitialStyleStringValue(stylingContext));
if (hasStyleInput(tNode) && tNode.styles) {
setDirectiveStylingInput(tNode.styles, lView, tNode.inputs !['style']);
}
}
@ -237,6 +201,7 @@ export function ɵɵelement(
export function ɵɵelementHostAttrs(attrs: TAttributes) {
const hostElementIndex = getSelectedIndex();
const lView = getLView();
const tView = lView[TVIEW];
const tNode = getTNode(hostElementIndex, lView);
// non-element nodes (e.g. `<ng-container>`) are not rendered as actual
@ -245,16 +210,34 @@ export function ɵɵelementHostAttrs(attrs: TAttributes) {
if (tNode.type === TNodeType.Element) {
const native = getNativeByTNode(tNode, lView) as RElement;
const lastAttrIndex = setUpAttributes(native, attrs);
const stylingAttrsStartIndex = attrsStylingIndexOf(attrs, lastAttrIndex);
if (stylingAttrsStartIndex >= 0) {
const directiveStylingIndex = getActiveDirectiveStylingIndex();
if (tNode.stylingTemplate) {
patchContextWithStaticAttrs(
tNode.stylingTemplate, attrs, stylingAttrsStartIndex, directiveStylingIndex);
} else {
tNode.stylingTemplate =
initializeStaticContext(attrs, stylingAttrsStartIndex, directiveStylingIndex);
if (tView.firstTemplatePass) {
const stylingNeedsToBeRendered = registerInitialStylingOnTNode(tNode, attrs, lastAttrIndex);
// this is only called during the first template pass in the
// event that this current directive assigned initial style/class
// host attribute values to the element. Because initial styling
// values are applied before directives are first rendered (within
// `createElement`) this means that initial styling for any directives
// still needs to be applied. Note that this will only happen during
// the first template pass and not each time a directive applies its
// attribute values to the element.
if (stylingNeedsToBeRendered) {
const renderer = lView[RENDERER];
renderInitialStyling(renderer, native, tNode);
}
}
}
}
function setDirectiveStylingInput(
context: TStylingContext | StylingMapArray, lView: LView, stylingInputs: (string | number)[]) {
// older versions of Angular treat the input as `null` in the
// event that the value does not exist at all. For this reason
// we can't have a styling value be an empty string.
const value = getInitialStylingValue(context) || null;
// Ivy does an extra `[class]` write with a falsy value since the value
// is applied during creation mode. This is a deviation from VE and should
// be (Jira Issue = FW-1467).
setInputsForProperty(lView, stylingInputs, value);
}

View File

@ -15,8 +15,10 @@ import {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation';
import {applyOnCreateInstructions} from '../node_util';
import {getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from '../state';
import {registerInitialStylingOnTNode} from '../styling_next/instructions';
import {createDirectivesAndLocals, executeContentQueries, getOrCreateTNode} from './shared';
import {createDirectivesAndLocals, executeContentQueries, getOrCreateTNode, setNodeStylingTemplate} from './shared';
/**
@ -52,10 +54,10 @@ export function ɵɵelementContainerStart(
tView, lView[T_HOST], index, TNodeType.ElementContainer, tagName, attrs || null);
if (attrs) {
if (attrs && tView.firstTemplatePass) {
// While ng-container doesn't necessarily support styling, we use the style context to identify
// and execute directives on the ng-container.
setNodeStylingTemplate(tView, tNode, attrs, 0);
registerInitialStylingOnTNode(tNode, attrs as TAttributes, 0);
}
appendChild(native, tNode, lView);

View File

@ -18,11 +18,10 @@ import {SelectorFlags} from '../interfaces/projection';
import {LQueries} from '../interfaces/query';
import {RComment, RElement, RNode} from '../interfaces/renderer';
import {StylingContext} from '../interfaces/styling';
import {isStylingContext} from '../interfaces/type_checks';
import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, HookData, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TData, TVIEW, TView as ITView, TView, T_HOST} from '../interfaces/view';
import {TStylingContext} from '../styling_next/interfaces';
import {runtimeIsNewStylingInUse} from '../styling_next/state';
import {DebugStyling as DebugNewStyling, NodeStylingDebug} from '../styling_next/styling_debug';
import {isStylingContext} from '../styling_next/util';
import {attachDebugObject} from '../util/debug_utils';
import {getTNode, unwrapRNode} from '../util/view_utils';
@ -132,8 +131,8 @@ export const TNodeConstructor = class TNode implements ITNode {
public stylingTemplate: StylingContext|null, //
public projection: number|(ITNode|RNode[])[]|null, //
public onElementCreationFns: Function[]|null, //
public newStyles: TStylingContext|null, //
public newClasses: TStylingContext|null, //
public styles: TStylingContext|null, //
public classes: TStylingContext|null, //
) {}
get type_(): string {
@ -331,16 +330,14 @@ export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|nul
while (tNodeCursor) {
const rawValue = lView[tNode.index];
const native = unwrapRNode(rawValue);
const componentLViewDebug =
isStylingContext(rawValue) ? null : toDebug(readLViewValue(rawValue));
let styles: DebugNewStyling|null = null;
let classes: DebugNewStyling|null = null;
if (runtimeIsNewStylingInUse()) {
styles = tNode.newStyles ? new NodeStylingDebug(tNode.newStyles, lView, false) : null;
classes = tNode.newClasses ? new NodeStylingDebug(tNode.newClasses, lView, true) : null;
}
const componentLViewDebug = toDebug(readLViewValue(rawValue));
const styles = isStylingContext(tNode.styles) ?
new NodeStylingDebug(tNode.styles as any as TStylingContext, lView) :
null;
const classes = isStylingContext(tNode.classes) ?
new NodeStylingDebug(tNode.classes as any as TStylingContext, lView, true) :
null;
debugNodes.push({
html: toHtml(native),
native: native as any, styles, classes,

View File

@ -34,8 +34,9 @@ import {isNodeMatchingSelectorList} from '../node_selector_matcher';
import {enterView, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, incrementActiveDirectiveId, isCreationMode, leaveView, namespaceHTMLInternal, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode, setSelectedIndex} from '../state';
import {initializeStaticContext as initializeStaticStylingContext} from '../styling/class_and_style_bindings';
import {ANIMATION_PROP_PREFIX, isAnimationProp} from '../styling/util';
import {renderStylingMap} from '../styling_next/bindings';
import {getInitialStylingValue, getStylingMapArray} from '../styling_next/util';
import {NO_CHANGE} from '../tokens';
import {attrsStylingIndexOf} from '../util/attrs_utils';
import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils';
import {getLViewParent, getRootContext} from '../util/view_traversal_utils';
import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, readPatchedLView, resetPreOrderHookFlags, unwrapRNode, viewAttachedToChangeDetector} from '../util/view_utils';
@ -490,25 +491,6 @@ function getRenderFlags(view: LView): RenderFlags {
//// Element
//////////////////////////
/**
* Appropriately sets `stylingTemplate` on a TNode
*
* Does not apply styles to DOM nodes
*
* @param tNode The node whose `stylingTemplate` to set
* @param attrs The attribute array source to set the attributes from
* @param attrsStartIndex Optional start index to start processing the `attrs` from
*/
export function setNodeStylingTemplate(
tView: TView, tNode: TNode, attrs: TAttributes, attrsStartIndex: number) {
if (tView.firstTemplatePass && !tNode.stylingTemplate) {
const stylingAttrsStartIndex = attrsStylingIndexOf(attrs, attrsStartIndex);
if (stylingAttrsStartIndex >= 0) {
tNode.stylingTemplate = initializeStaticStylingContext(attrs, stylingAttrsStartIndex);
}
}
}
export function executeContentQueries(tView: TView, tNode: TNode, lView: LView) {
if (isContentQueryHost(tNode)) {
const start = tNode.directiveStart;
@ -756,66 +738,61 @@ export function createTNode(
adjustedIndex: number, tagName: string | null, attrs: TAttributes | null): TNode {
ngDevMode && ngDevMode.tNode++;
let injectorIndex = tParent ? tParent.injectorIndex : -1;
return ngDevMode ?
new TNodeConstructor(
tView, // tView_: TView
type, // type: TNodeType
adjustedIndex, // index: number
injectorIndex, // injectorIndex: number
-1, // directiveStart: number
-1, // directiveEnd: number
-1, // propertyMetadataStartIndex: number
-1, // propertyMetadataEndIndex: number
0, // flags: TNodeFlags
0, // providerIndexes: TNodeProviderIndexes
tagName, // tagName: string|null
attrs, // attrs: (string|AttributeMarker|(string|SelectorFlags)[])[]|null
null, // localNames: (string|number)[]|null
undefined, // initialInputs: (string[]|null)[]|null|undefined
undefined, // inputs: PropertyAliases|null|undefined
undefined, // outputs: PropertyAliases|null|undefined
null, // tViews: ITView|ITView[]|null
null, // next: ITNode|null
null, // projectionNext: ITNode|null
null, // child: ITNode|null
tParent, // parent: TElementNode|TContainerNode|null
null, // stylingTemplate: StylingContext|null
null, // projection: number|(ITNode|RNode[])[]|null
null, // onElementCreationFns: Function[]|null
// TODO (matsko): rename this to `styles` once the old styling impl is gone
null, // newStyles: TStylingContext|null
// TODO (matsko): rename this to `classes` once the old styling impl is gone
null, // newClasses: TStylingContext|null
) :
{
type: type,
index: adjustedIndex,
injectorIndex: injectorIndex,
directiveStart: -1,
directiveEnd: -1,
propertyMetadataStartIndex: -1,
propertyMetadataEndIndex: -1,
flags: 0,
providerIndexes: 0,
tagName: tagName,
attrs: attrs,
localNames: null,
initialInputs: undefined,
inputs: undefined,
outputs: undefined,
tViews: null,
next: null,
projectionNext: null,
child: null,
parent: tParent,
stylingTemplate: null,
projection: null,
onElementCreationFns: null,
// TODO (matsko): rename this to `styles` once the old styling impl is gone
newStyles: null,
// TODO (matsko): rename this to `classes` once the old styling impl is gone
newClasses: null,
};
return ngDevMode ? new TNodeConstructor(
tView, // tView_: TView
type, // type: TNodeType
adjustedIndex, // index: number
injectorIndex, // injectorIndex: number
-1, // directiveStart: number
-1, // directiveEnd: number
-1, // propertyMetadataStartIndex: number
-1, // propertyMetadataEndIndex: number
0, // flags: TNodeFlags
0, // providerIndexes: TNodeProviderIndexes
tagName, // tagName: string|null
attrs, // attrs: (string|AttributeMarker|(string|SelectorFlags)[])[]|null
null, // localNames: (string|number)[]|null
undefined, // initialInputs: (string[]|null)[]|null|undefined
undefined, // inputs: PropertyAliases|null|undefined
undefined, // outputs: PropertyAliases|null|undefined
null, // tViews: ITView|ITView[]|null
null, // next: ITNode|null
null, // projectionNext: ITNode|null
null, // child: ITNode|null
tParent, // parent: TElementNode|TContainerNode|null
null, // stylingTemplate: StylingContext|null
null, // projection: number|(ITNode|RNode[])[]|null
null, // onElementCreationFns: Function[]|null
null, // newStyles: TStylingContext|null
null, // newClasses: TStylingContext|null
) :
{
type: type,
index: adjustedIndex,
injectorIndex: injectorIndex,
directiveStart: -1,
directiveEnd: -1,
propertyMetadataStartIndex: -1,
propertyMetadataEndIndex: -1,
flags: 0,
providerIndexes: 0,
tagName: tagName,
attrs: attrs,
localNames: null,
initialInputs: undefined,
inputs: undefined,
outputs: undefined,
tViews: null,
next: null,
projectionNext: null,
child: null,
parent: tParent,
stylingTemplate: null,
projection: null,
onElementCreationFns: null,
styles: null,
classes: null,
};
}
@ -1899,3 +1876,17 @@ export function textBindingInternal(lView: LView, index: number, value: string):
const renderer = lView[RENDERER];
isProceduralRenderer(renderer) ? renderer.setValue(element, value) : element.textContent = value;
}
/**
* Renders all initial styling (class and style values) on to the element from the tNode.
*
* All initial styling data (i.e. any values extracted from the `style` or `class` attributes
* on an element) are collected into the `tNode.styles` and `tNode.classes` data structures.
* These values are populated during the creation phase of an element and are then later
* applied once the element is instantiated. This function applies each of the static
* style and class entries to the element.
*/
export function renderInitialStyling(renderer: Renderer3, native: RElement, tNode: TNode) {
renderStylingMap(renderer, native, tNode.classes, true);
renderStylingMap(renderer, native, tNode.styles, false);
}

View File

@ -6,12 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {getLView, getSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {stylePropInternal} from '../styling_next/instructions';
import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation';
import {TsickleIssue1009} from './shared';
import {getActiveDirectiveStylingIndex, stylePropInternal} from './styling';
/**
@ -37,20 +34,15 @@ import {getActiveDirectiveStylingIndex, stylePropInternal} from './styling';
* @param v0 Value checked for change.
* @param suffix Static value used for concatenation only.
* @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param forceOverride Whether or not to update the styling value immediately.
* @returns itself, so that it may be chained.
* @codeGenApi
*/
export function ɵɵstylePropInterpolate1(
styleIndex: number, prefix: string, v0: any, suffix: string, valueSuffix?: string | null,
forceOverride?: boolean): TsickleIssue1009 {
prop: string, prefix: string, v0: any, suffix: string,
valueSuffix?: string | null): TsickleIssue1009 {
const lView = getLView();
const interpolatedValue = interpolation1(lView, prefix, v0, suffix);
if (interpolatedValue !== NO_CHANGE) {
stylePropInternal(
lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(),
interpolatedValue as string, valueSuffix, forceOverride);
}
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
return ɵɵstylePropInterpolate1;
}
@ -79,20 +71,15 @@ export function ɵɵstylePropInterpolate1(
* @param v1 Value checked for change.
* @param suffix Static value used for concatenation only.
* @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param forceOverride Whether or not to update the styling value immediately.
* @returns itself, so that it may be chained.
* @codeGenApi
*/
export function ɵɵstylePropInterpolate2(
styleIndex: number, prefix: string, v0: any, i0: string, v1: any, suffix: string,
valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009 {
prop: string, prefix: string, v0: any, i0: string, v1: any, suffix: string,
valueSuffix?: string | null): TsickleIssue1009 {
const lView = getLView();
const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix);
if (interpolatedValue !== NO_CHANGE) {
stylePropInternal(
lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(),
interpolatedValue as string, valueSuffix, forceOverride);
}
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
return ɵɵstylePropInterpolate2;
}
@ -123,20 +110,15 @@ export function ɵɵstylePropInterpolate2(
* @param v2 Value checked for change.
* @param suffix Static value used for concatenation only.
* @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param forceOverride Whether or not to update the styling value immediately.
* @returns itself, so that it may be chained.
* @codeGenApi
*/
export function ɵɵstylePropInterpolate3(
styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any,
suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009 {
prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string,
valueSuffix?: string | null): TsickleIssue1009 {
const lView = getLView();
const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix);
if (interpolatedValue !== NO_CHANGE) {
stylePropInternal(
lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(),
interpolatedValue as string, valueSuffix, forceOverride);
}
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
return ɵɵstylePropInterpolate3;
}
@ -169,21 +151,15 @@ export function ɵɵstylePropInterpolate3(
* @param v3 Value checked for change.
* @param suffix Static value used for concatenation only.
* @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param forceOverride Whether or not to update the styling value immediately.
* @returns itself, so that it may be chained.
* @codeGenApi
*/
export function ɵɵstylePropInterpolate4(
styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any,
i2: string, v3: any, suffix: string, valueSuffix?: string | null,
forceOverride?: boolean): TsickleIssue1009 {
prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string,
v3: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009 {
const lView = getLView();
const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix);
if (interpolatedValue !== NO_CHANGE) {
stylePropInternal(
lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(),
interpolatedValue as string, valueSuffix, forceOverride);
}
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
return ɵɵstylePropInterpolate4;
}
@ -218,22 +194,16 @@ export function ɵɵstylePropInterpolate4(
* @param v4 Value checked for change.
* @param suffix Static value used for concatenation only.
* @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param forceOverride Whether or not to update the styling value immediately.
* @returns itself, so that it may be chained.
* @codeGenApi
*/
export function ɵɵstylePropInterpolate5(
styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any,
i2: string, v3: any, i3: string, v4: any, suffix: string, valueSuffix?: string | null,
forceOverride?: boolean): TsickleIssue1009 {
prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string,
v3: any, i3: string, v4: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009 {
const lView = getLView();
const interpolatedValue =
interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix);
if (interpolatedValue !== NO_CHANGE) {
stylePropInternal(
lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(),
interpolatedValue as string, valueSuffix, forceOverride);
}
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
return ɵɵstylePropInterpolate5;
}
@ -270,22 +240,17 @@ export function ɵɵstylePropInterpolate5(
* @param v5 Value checked for change.
* @param suffix Static value used for concatenation only.
* @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param forceOverride Whether or not to update the styling value immediately.
* @returns itself, so that it may be chained.
* @codeGenApi
*/
export function ɵɵstylePropInterpolate6(
styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any,
i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, suffix: string,
valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009 {
prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string,
v3: any, i3: string, v4: any, i4: string, v5: any, suffix: string,
valueSuffix?: string | null): TsickleIssue1009 {
const lView = getLView();
const interpolatedValue =
interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix);
if (interpolatedValue !== NO_CHANGE) {
stylePropInternal(
lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(),
interpolatedValue as string, valueSuffix, forceOverride);
}
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
return ɵɵstylePropInterpolate6;
}
@ -325,22 +290,17 @@ export function ɵɵstylePropInterpolate6(
* @param v6 Value checked for change.
* @param suffix Static value used for concatenation only.
* @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param forceOverride Whether or not to update the styling value immediately.
* @returns itself, so that it may be chained.
* @codeGenApi
*/
export function ɵɵstylePropInterpolate7(
styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any,
i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any,
suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009 {
prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string,
v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string,
valueSuffix?: string | null): TsickleIssue1009 {
const lView = getLView();
const interpolatedValue =
interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix);
if (interpolatedValue !== NO_CHANGE) {
stylePropInternal(
lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(),
interpolatedValue as string, valueSuffix, forceOverride);
}
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
return ɵɵstylePropInterpolate7;
}
@ -382,23 +342,17 @@ export function ɵɵstylePropInterpolate7(
* @param v7 Value checked for change.
* @param suffix Static value used for concatenation only.
* @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param forceOverride Whether or not to update the styling value immediately.
* @returns itself, so that it may be chained.
* @codeGenApi
*/
export function ɵɵstylePropInterpolate8(
styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any,
i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string,
v7: any, suffix: string, valueSuffix?: string | null,
forceOverride?: boolean): TsickleIssue1009 {
prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string,
v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any,
suffix: string, valueSuffix?: string | null): TsickleIssue1009 {
const lView = getLView();
const interpolatedValue = interpolation8(
lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix);
if (interpolatedValue !== NO_CHANGE) {
stylePropInternal(
lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(),
interpolatedValue as string, valueSuffix, forceOverride);
}
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
return ɵɵstylePropInterpolate8;
}
@ -429,19 +383,13 @@ export function ɵɵstylePropInterpolate8(
* a string prefix and ending with a string suffix.
* (e.g. `['prefix', value0, '-', value1, '-', value2, ..., value99, 'suffix']`)
* @param valueSuffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param forceOverride Whether or not to update the styling value immediately.
* @returns itself, so that it may be chained.
* @codeGenApi
*/
export function ɵɵstylePropInterpolateV(
styleIndex: number, values: any[], valueSuffix?: string | null,
forceOverride?: boolean): TsickleIssue1009 {
prop: string, values: any[], valueSuffix?: string | null): TsickleIssue1009 {
const lView = getLView();
const interpolated = interpolationV(lView, values);
if (interpolated !== NO_CHANGE) {
stylePropInternal(
lView, getSelectedIndex(), styleIndex, getActiveDirectiveStylingIndex(),
interpolated as string, valueSuffix, forceOverride);
}
const interpolatedValue = interpolationV(lView, values);
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
return ɵɵstylePropInterpolateV;
}

View File

@ -16,10 +16,8 @@ import {ParamsOf, enqueueHostInstruction, registerHostDirective} from '../stylin
import {BoundPlayerFactory} from '../styling/player_factory';
import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from '../styling/shared';
import {getCachedStylingContext, setCachedStylingContext} from '../styling/state';
import {allocateOrUpdateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util';
import {classMap as newClassMap, classProp as newClassProp, styleMap as newStyleMap, styleProp as newStyleProp, stylingApply as newStylingApply, stylingInit as newStylingInit} from '../styling_next/instructions';
import {runtimeAllowOldStyling, runtimeIsNewStylingInUse} from '../styling_next/state';
import {getBindingNameFromIndex} from '../styling_next/util';
import {allocateOrUpdateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContextFromLView} from '../styling/util';
import {hasClassInput, hasStyleInput} from '../styling_next/util';
import {NO_CHANGE} from '../tokens';
import {renderStringify} from '../util/misc_utils';
import {getRootContext} from '../util/view_traversal_utils';
@ -76,13 +74,6 @@ export function ɵɵstyling(
const directiveStylingIndex = getActiveDirectiveStylingIndex();
if (directiveStylingIndex) {
// this is temporary hack to get the existing styling instructions to
// play ball with the new refactored implementation.
// TODO (matsko): remove this once the old implementation is not needed.
if (runtimeIsNewStylingInUse()) {
newStylingInit();
}
// despite the binding being applied in a queue (below), the allocation
// of the directive into the context happens right away. The reason for
// this is to retain the ordering of the directives (which is important
@ -165,15 +156,6 @@ export function stylePropInternal(
updatestyleProp(
stylingContext, styleIndex, valueToAdd, DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride);
}
if (runtimeIsNewStylingInUse()) {
const prop = getBindingNameFromIndex(stylingContext, styleIndex, directiveStylingIndex, false);
// the reason why we cast the value as `boolean` is
// because the new styling refactor does not yet support
// sanitization or animation players.
newStyleProp(prop, value as string | number, suffix);
}
}
function resolveStylePropValue(
@ -232,15 +214,6 @@ export function ɵɵclassProp(
updateclassProp(
stylingContext, classIndex, input, DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride);
}
if (runtimeIsNewStylingInUse()) {
const prop = getBindingNameFromIndex(stylingContext, classIndex, directiveStylingIndex, true);
// the reason why we cast the value as `boolean` is
// because the new styling refactor does not yet support
// sanitization or animation players.
newClassProp(prop, input as boolean);
}
}
@ -292,10 +265,6 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu
}
updateStyleMap(stylingContext, styles);
}
if (runtimeIsNewStylingInUse()) {
newStyleMap(styles);
}
}
@ -342,10 +311,6 @@ export function classMapInternal(
}
updateClassMap(stylingContext, classes);
}
if (runtimeIsNewStylingInUse()) {
newClassMap(classes);
}
}
/**
@ -371,13 +336,11 @@ export function ɵɵstylingApply(): void {
const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0;
const stylingContext = getStylingContext(index, lView);
if (runtimeAllowOldStyling()) {
const totalPlayersQueued = renderStyling(
stylingContext, renderer, lView, isFirstRender, null, null, directiveStylingIndex);
if (totalPlayersQueued > 0) {
const rootContext = getRootContext(lView);
scheduleTick(rootContext, RootContextFlags.FlushPlayers);
}
const totalPlayersQueued = renderStyling(
stylingContext, renderer, lView, isFirstRender, null, null, directiveStylingIndex);
if (totalPlayersQueued > 0) {
const rootContext = getRootContext(lView);
scheduleTick(rootContext, RootContextFlags.FlushPlayers);
}
// because select(n) may not run between every instruction, the cached styling
@ -388,10 +351,6 @@ export function ɵɵstylingApply(): void {
// cleared because there is no code in Angular that applies more styling code after a
// styling flush has occurred. Note that this will be fixed once FW-1254 lands.
setCachedStylingContext(null);
if (runtimeIsNewStylingInUse()) {
newStylingApply();
}
}
export function getActiveDirectiveStylingIndex() {

View File

@ -11,7 +11,6 @@ import {ViewRef} from '../../linker/view_ref';
import {TNode} from './node';
import {LQueries} from './query';
import {RComment, RElement} from './renderer';
import {StylingContext} from './styling';
import {HOST, LView, NEXT, PARENT, QUERIES, T_HOST} from './view';
@ -54,11 +53,8 @@ export interface LContainer extends Array<any> {
*
* The host could be an LView if this container is on a component node.
* In that case, the component LView is its HOST.
*
* It could also be a styling context if this is a node with a style/class
* binding.
*/
readonly[HOST]: RElement|RComment|StylingContext|LView;
readonly[HOST]: RElement|RComment|LView;
/**
* This is a type field which allows us to differentiate `LContainer` from `StylingContext` in an

View File

@ -5,13 +5,15 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {TStylingContext} from '../styling_next/interfaces';
import {StylingMapArray, TStylingContext} from '../styling_next/interfaces';
import {CssSelector} from './projection';
import {RNode} from './renderer';
import {StylingContext} from './styling';
import {LView, TView} from './view';
/**
* TNodeType corresponds to the {@link TNode} `type` property.
*/
@ -457,10 +459,46 @@ export interface TNode {
* with functions each time the creation block is called.
*/
onElementCreationFns: Function[]|null;
// TODO (matsko): rename this to `styles` once the old styling impl is gone
newStyles: TStylingContext|null;
// TODO (matsko): rename this to `classes` once the old styling impl is gone
newClasses: TStylingContext|null;
/**
* A collection of all style bindings and/or static style values for an element.
*
* This field will be populated if and when:
*
* - There are one or more initial styles on an element (e.g. `<div style="width:200px">`)
* - There are one or more style bindings on an element (e.g. `<div [style.width]="w">`)
*
* If and when there are only initial styles (no bindings) then an instance of `StylingMapArray`
* will be used here. Otherwise an instance of `TStylingContext` will be created when there
* are one or more style bindings on an element.
*
* During element creation this value is likely to be populated with an instance of
* `StylingMapArray` and only when the bindings are evaluated (which happens during
* update mode) then it will be converted to a `TStylingContext` if any style bindings
* are encountered. If and when this happens then the existing `StylingMapArray` value
* will be placed into the initial styling slot in the newly created `TStylingContext`.
*/
styles: StylingMapArray|TStylingContext|null;
/**
* A collection of all class bindings and/or static class values for an element.
*
* This field will be populated if and when:
*
* - There are one or more initial classes on an element (e.g. `<div class="one two three">`)
* - There are one or more class bindings on an element (e.g. `<div [class.foo]="f">`)
*
* If and when there are only initial classes (no bindings) then an instance of `StylingMapArray`
* will be used here. Otherwise an instance of `TStylingContext` will be created when there
* are one or more class bindings on an element.
*
* During element creation this value is likely to be populated with an instance of
* `StylingMapArray` and only when the bindings are evaluated (which happens during
* update mode) then it will be converted to a `TStylingContext` if any class bindings
* are encountered. If and when this happens then the existing `StylingMapArray` value
* will be placed into the initial styling slot in the newly created `TStylingContext`.
*/
classes: StylingMapArray|TStylingContext|null;
}
/** Static data for an element */

View File

@ -20,7 +20,6 @@ import {TElementNode, TNode, TViewNode} from './node';
import {PlayerHandler} from './player';
import {LQueries} from './query';
import {RElement, Renderer3, RendererFactory3} from './renderer';
import {StylingContext} from './styling';
@ -71,13 +70,9 @@ export interface OpaqueViewState {
export interface LView extends Array<any> {
/**
* The host node for this LView instance, if this is a component view.
*
* If this is an embedded view, HOST will be null.
*
* If the component uses host bindings for styling that the `RElement` will be wrapped with
* `StylingContext`.
*/
[HOST]: RElement|StylingContext|null;
[HOST]: RElement|null;
/**
* The static data for this view. We need a reference to this so we can easily walk up the

View File

@ -17,7 +17,6 @@ import {NodeInjectorFactory} from './interfaces/injector';
import {TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode, TViewNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node';
import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection';
import {ProceduralRenderer3, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer';
import {StylingContext} from './interfaces/styling';
import {isLContainer, isLView, isRootView} from './interfaces/type_checks';
import {CHILD_HEAD, CLEANUP, FLAGS, HOST, HookData, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, T_HOST, unusedValueExportToPlacateAjd as unused5} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
@ -70,7 +69,7 @@ const enum WalkTNodeTreeAction {
*/
function executeActionOnElementOrContainer(
action: WalkTNodeTreeAction, renderer: Renderer3, parent: RElement | null,
lNodeToHandle: RNode | LContainer | LView | StylingContext, beforeNode?: RNode | null) {
lNodeToHandle: RNode | LContainer | LView, beforeNode?: RNode | null) {
ngDevMode && assertDefined(lNodeToHandle, '\'lNodeToHandle\' is undefined');
let lContainer: LContainer|undefined;
let isComponent = false;

View File

@ -12,7 +12,7 @@ import {assertDefined, assertNotEqual} from '../util/assert';
import {AttributeMarker, TAttributes, TNode, TNodeType, unusedValueExportToPlacateAjd as unused1} from './interfaces/node';
import {CssSelector, CssSelectorList, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection';
import {getInitialClassNameValue} from './styling/class_and_style_bindings';
import {getInitialStylingValue} from './styling_next/util';
import {isNameOnlyAttributeMarker} from './util/attrs_utils';
const unusedValueToPlacateAjd = unused1 + unused2;
@ -103,8 +103,9 @@ export function isNodeMatchingSelector(
// special case for matching against classes when a tNode has been instantiated with
// class and style values as separate attribute values (e.g. ['title', CLASS, 'foo'])
if ((mode & SelectorFlags.CLASS) && tNode.stylingTemplate) {
if (!isCssClassMatching(readClassValueFromTNode(tNode), selectorAttrValue as string)) {
if ((mode & SelectorFlags.CLASS) && tNode.classes) {
if (!isCssClassMatching(
getInitialStylingValue(tNode.classes), selectorAttrValue as string)) {
if (isPositive(mode)) return false;
skipToNextSelector = true;
}
@ -152,16 +153,6 @@ function isPositive(mode: SelectorFlags): boolean {
return (mode & SelectorFlags.NOT) === 0;
}
function readClassValueFromTNode(tNode: TNode): string {
// comparing against CSS class values is complex because the compiler doesn't place them as
// regular attributes when an element is created. Instead, the classes (and styles for
// that matter) are placed in a special styling context that is used for resolving all
// class/style values across static attributes, [style]/[class] and [style.prop]/[class.name]
// bindings. Therefore if and when the styling context exists then the class values are to be
// extracted by the context helper code below...
return tNode.stylingTemplate ? getInitialClassNameValue(tNode.stylingTemplate) : '';
}
/**
* Examines the attribute's definition array for a node to find the index of the
* attribute that matches the given `name`.

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertDefined, assertGreaterThan} from '../util/assert';
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {assertDefined} from '../util/assert';
import {assertLViewOrUndefined} from './assert';
import {executeHooks} from './hooks';
@ -14,6 +15,7 @@ import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {TElementNode, TNode, TViewNode} from './interfaces/node';
import {BINDING_INDEX, CONTEXT, DECLARATION_VIEW, FLAGS, InitPhaseState, LView, LViewFlags, OpaqueViewState, TVIEW} from './interfaces/view';
import {setCachedStylingContext} from './styling/state';
import {resetAllStylingState, resetStylingState} from './styling_next/state';
import {resetPreOrderHookFlags} from './util/view_utils';
@ -458,6 +460,8 @@ export function resetComponentState() {
previousOrParentTNode = null !;
elementDepthCount = 0;
bindingsEnabled = true;
setCurrentStyleSanitizer(null);
resetAllStylingState();
}
/**
@ -513,6 +517,10 @@ export function setSelectedIndex(index: number) {
// remove the styling context from the cache
// because we are now on a different element
setCachedStylingContext(null);
// we have now jumped to another element
// therefore the state is stale
resetStylingState();
}
@ -557,3 +565,12 @@ export function namespaceHTMLInternal() {
export function getNamespace(): string|null {
return _currentNamespace;
}
let _currentSanitizer: StyleSanitizeFn|null;
export function setCurrentStyleSanitizer(sanitizer: StyleSanitizeFn | null) {
_currentSanitizer = sanitizer;
}
export function getCurrentStyleSanitizer() {
return _currentSanitizer;
}

View File

@ -686,7 +686,7 @@ function patchStylingMapIntoContext(
// directive values if their identity has not changed. If a previous directive set this
// value as dirty (because its own shape changed) then this means that the object has been
// offset to a different area in the context. Because its value has been offset then it
// can't write to a region that it wrote to before (which may have been apart of another
// can't write to a region that it wrote to before (which may have been a part of another
// directive) and therefore its shape changes too.
let valuesEntryShapeChange =
existingCachedValueIsDirty || ((!existingCachedValue && cacheValue) ? true : false);
@ -822,7 +822,7 @@ function patchStylingMapIntoContext(
}
// STEP 3:
// Remove (nullify) any existing entries in the context that were not apart of the
// Remove (nullify) any existing entries in the context that were not a part of the
// map input value that was passed into this algorithm for this directive.
while (ctxIndex < ctxEnd) {
valuesEntryShapeChange = true; // some values are missing
@ -914,7 +914,13 @@ function updateSingleStylingValue(
const currDirective = getDirectiveIndexFromEntry(context, singleIndex);
const value: string|boolean|null = (input instanceof BoundPlayerFactory) ? input.value : input;
ngDevMode && ngDevMode.stylingProp++;
if (ngDevMode) {
if (isClassBased) {
ngDevMode.classProp++;
} else {
ngDevMode.styleProp++;
}
}
if (hasValueChanged(currFlag, currValue, value) &&
(forceOverride || allowValueChange(currValue, value, currDirective, directiveIndex))) {
@ -973,7 +979,13 @@ function updateSingleStylingValue(
setContextPlayersDirty(context, true);
}
ngDevMode && ngDevMode.stylingPropCacheMiss++;
if (ngDevMode) {
if (isClassBased) {
ngDevMode.classPropCacheMiss++;
} else {
ngDevMode.stylePropCacheMiss++;
}
}
}
}
@ -1002,7 +1014,7 @@ export function renderStyling(
isFirstRender: boolean, classesStore?: BindingStore | null, stylesStore?: BindingStore | null,
directiveIndex: number = 0): number {
let totalPlayersQueued = 0;
ngDevMode && ngDevMode.stylingApply++;
ngDevMode && ngDevMode.flushStyling++;
// this prevents multiple attempts to render style/class values on
// the same element...
@ -1017,8 +1029,6 @@ export function renderStyling(
flushHostInstructionsQueue(context);
if (isContextDirty(context)) {
ngDevMode && ngDevMode.stylingApplyCacheMiss++;
// this is here to prevent things like <ng-container [style] [class]>...</ng-container>
// or if there are any host style or class bindings present in a directive set on
// a container node
@ -1807,7 +1817,7 @@ function isMultiValueCacheHit(
* - The dirty flag will be set to true
*
* If the `dirtyFutureValues` param is provided then it will update all future entries (binding
* entries that exist as apart of other directives) to be dirty as well. This will force the
* entries that exist as a part of other directives) to be dirty as well. This will force the
* styling algorithm to reapply those values once change detection checks them (which will in
* turn cause the styling context to update itself and the correct styling values will be
* rendered on screen).

View File

@ -156,14 +156,6 @@ export function isAnimationProp(name: string): boolean {
return name[0] === ANIMATION_PROP_PREFIX;
}
export function hasClassInput(tNode: TNode) {
return (tNode.flags & TNodeFlags.hasClassInput) !== 0;
}
export function hasStyleInput(tNode: TNode) {
return (tNode.flags & TNodeFlags.hasStyleInput) !== 0;
}
export function forceClassesAsString(classes: string | {[key: string]: any} | null | undefined):
string {
if (classes && typeof classes !== 'string') {
@ -226,7 +218,7 @@ export function getPlayersInternal(playerContext: PlayerContext): Player[] {
const players: Player[] = [];
const nonFactoryPlayersStart = playerContext[PlayerIndex.NonBuilderPlayersStart];
// add all factory-based players (which are apart of [style] and [class] bindings)
// add all factory-based players (which are a part of [style] and [class] bindings)
for (let i = PlayerIndex.PlayerBuildersStartPosition + PlayerIndex.PlayerOffsetPosition;
i < nonFactoryPlayersStart; i += PlayerIndex.PlayerAndPlayerBuildersTupleSize) {
const player = playerContext[i] as Player | null;
@ -235,7 +227,7 @@ export function getPlayersInternal(playerContext: PlayerContext): Player[] {
}
}
// add all custom players (not apart of [style] and [class] bindings)
// add all custom players (not a part of [style] and [class] bindings)
for (let i = nonFactoryPlayersStart; i < playerContext.length; i++) {
players.push(playerContext[i] as Player);
}

View File

@ -8,8 +8,10 @@
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, LStylingMap, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
import {allowStylingFlush, getBindingValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasValueChanged, isContextLocked, isSanitizationRequired, isStylingValueDefined, lockContext, setGuardMask} from './util';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
import {BIT_MASK_START_VALUE, deleteStylingStateFromStorage, getStylingState, resetStylingState, storeStylingState} from './state';
import {allowStylingFlush, getBindingValue, getGuardMask, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getValuesCount, hasValueChanged, isContextLocked, isSanitizationRequired, isStylingValueDefined, lockContext, setGuardMask, stateIsPersisted} from './util';
/**
@ -31,25 +33,38 @@ import {allowStylingFlush, getBindingValue, getGuardMask, getProp, getPropValues
* --------
*/
const DEFAULT_BINDING_VALUE = null;
const DEFAULT_SIZE_VALUE = 1;
// The first bit value reflects a map-based binding value's bit.
// The reason why it's always activated for every entry in the map
// is so that if any map-binding values update then all other prop
// based bindings will pass the guard check automatically without
// any extra code or flags.
export const DEFAULT_GUARD_MASK_VALUE = 0b1;
const STYLING_INDEX_FOR_MAP_BINDING = 0;
const STYLING_INDEX_START_VALUE = 1;
// the values below are global to all styling code below. Each value
// will either increment or mutate each time a styling instruction is
// executed. Do not modify the values below.
let currentStyleIndex = STYLING_INDEX_START_VALUE;
let currentClassIndex = STYLING_INDEX_START_VALUE;
let stylesBitMask = 0;
let classesBitMask = 0;
/**
* The guard/update mask bit index location for map-based bindings.
*
* All map-based bindings (i.e. `[style]` and `[class]` )
*/
const STYLING_INDEX_FOR_MAP_BINDING = 0;
/**
* Default fallback value for a styling binding.
*
* A value of `null` is used here which signals to the styling algorithm that
* the styling value is not present. This way if there are no other values
* detected then it will be removed once the style/class property is dirty and
* diffed within the styling algorithm present in `flushStyling`.
*/
const DEFAULT_BINDING_VALUE = null;
/**
* Default size count value for a new entry in a context.
*
* A value of `1` is used here because each entry in the context has a default
* property.
*/
const DEFAULT_SIZE_VALUE = 1;
let deferredBindingQueue: (TStylingContext | number | string | null | boolean)[] = [];
/**
@ -63,16 +78,24 @@ let deferredBindingQueue: (TStylingContext | number | string | null | boolean)[]
* and the bit mask values to be in sync).
*/
export function updateClassBinding(
context: TStylingContext, data: LStylingData, prop: string | null, bindingIndex: number,
value: boolean | string | null | undefined | LStylingMap, deferRegistration: boolean,
forceUpdate: boolean): void {
context: TStylingContext, data: LStylingData, element: RElement, prop: string | null,
bindingIndex: number, value: boolean | string | null | undefined | StylingMapArray,
deferRegistration: boolean, forceUpdate: boolean): boolean {
const isMapBased = !prop;
const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : currentClassIndex++;
const state = getStylingState(element, stateIsPersisted(context));
const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++;
const updated = updateBindingData(
context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate, false);
if (updated || forceUpdate) {
classesBitMask |= 1 << index;
// We flip the bit in the bitMask to reflect that the binding
// at the `index` slot has changed. This identifies to the flushing
// phase that the bindings for this particular CSS class need to be
// applied again because on or more of the bindings for the CSS
// class have changed.
state.classesBitMask |= 1 << index;
return true;
}
return false;
}
/**
@ -86,11 +109,12 @@ export function updateClassBinding(
* and the bit mask values to be in sync).
*/
export function updateStyleBinding(
context: TStylingContext, data: LStylingData, prop: string | null, bindingIndex: number,
value: String | string | number | null | undefined | LStylingMap,
sanitizer: StyleSanitizeFn | null, deferRegistration: boolean, forceUpdate: boolean): void {
context: TStylingContext, data: LStylingData, element: RElement, prop: string | null,
bindingIndex: number, value: String | string | number | null | undefined | StylingMapArray,
sanitizer: StyleSanitizeFn | null, deferRegistration: boolean, forceUpdate: boolean): boolean {
const isMapBased = !prop;
const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : currentStyleIndex++;
const state = getStylingState(element, stateIsPersisted(context));
const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++;
const sanitizationRequired = isMapBased ?
true :
(sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false);
@ -98,8 +122,15 @@ export function updateStyleBinding(
context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate,
sanitizationRequired);
if (updated || forceUpdate) {
stylesBitMask |= 1 << index;
// We flip the bit in the bitMask to reflect that the binding
// at the `index` slot has changed. This identifies to the flushing
// phase that the bindings for this particular property need to be
// applied again because on or more of the bindings for the CSS
// property have changed.
state.stylesBitMask |= 1 << index;
return true;
}
return false;
}
/**
@ -118,7 +149,7 @@ export function updateStyleBinding(
function updateBindingData(
context: TStylingContext, data: LStylingData, counterIndex: number, prop: string | null,
bindingIndex: number,
value: string | String | number | boolean | null | undefined | LStylingMap,
value: string | String | number | boolean | null | undefined | StylingMapArray,
deferRegistration: boolean, forceUpdate: boolean, sanitizationRequired: boolean): boolean {
if (!isContextLocked(context)) {
if (deferRegistration) {
@ -215,9 +246,10 @@ function flushDeferredBindings() {
*/
export function registerBinding(
context: TStylingContext, countId: number, prop: string | null,
bindingValue: number | null | string | boolean, sanitizationRequired?: boolean) {
// prop-based bindings (e.g `<div [style.width]="w" [class.foo]="f">`)
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) {
@ -238,6 +270,7 @@ export function registerBinding(
if (!found) {
allocateNewContextEntry(context, context.length, prop, sanitizationRequired);
addBindingIntoContext(context, false, i, bindingValue, countId);
registered = true;
}
} else {
// map-based bindings (e.g `<div [style]="s" [class]="{className:true}">`)
@ -245,7 +278,9 @@ export function registerBinding(
// since it is already there when the context is first created.
addBindingIntoContext(
context, true, TStylingContextIndex.MapBindingsPosition, bindingValue, countId);
registered = true;
}
return registered;
}
function allocateNewContextEntry(
@ -284,7 +319,8 @@ function addBindingIntoContext(
bindingValue: number | string | boolean | null, countId: number) {
const valuesCount = getValuesCount(context, index);
let lastValueIndex = index + TStylingContextIndex.BindingsStartOffset + valuesCount;
const firstValueIndex = index + TStylingContextIndex.BindingsStartOffset;
let lastValueIndex = firstValueIndex + valuesCount;
if (!isMapBased) {
// prop-based values all have default values, but map-based entries do not.
// we want to access the index for the default value in this case and not just
@ -293,6 +329,20 @@ function addBindingIntoContext(
}
if (typeof bindingValue === 'number') {
// the loop here will check to see if the binding already exists
// for the property in the context. Why? The reason for this is
// because the styling context is not "locked" until the first
// flush has occurred. This means that if a repeated element
// registers its styling bindings then it will register each
// binding more than once (since its duplicated). This check
// will prevent that from happening. Note that this only happens
// when a binding is first encountered and not each time it is
// updated.
for (let i = firstValueIndex; i <= lastValueIndex; i++) {
const indexAtPosition = context[i];
if (indexAtPosition === bindingValue) return;
}
context.splice(lastValueIndex, 0, bindingValue);
(context[index + TStylingContextIndex.ValuesCountOffset] as number)++;
@ -301,57 +351,120 @@ function addBindingIntoContext(
// to be included into the existing mask value.
const guardMask = getGuardMask(context, index) | (1 << countId);
setGuardMask(context, index, guardMask);
} else if (typeof bindingValue === 'string' && context[lastValueIndex] == null) {
} else if (bindingValue !== null && context[lastValueIndex] == null) {
context[lastValueIndex] = bindingValue;
}
}
/**
* Applies all class entries in the provided context to the provided element and resets
* any counter and/or bitMask values associated with class bindings.
* Applies all pending style and class bindings to the provided element.
*
* @returns whether or not the classes were flushed to the element.
* This function will attempt to flush styling via the provided `classesContext`
* and `stylesContext` context values. This function is designed to be run from
* the `stylingApply()` instruction (which is run at the very end of styling
* change detection) and will rely on any state values that are set from when
* any styling bindings update.
*
* This function may be called multiple times on the same element because it can
* be called from the template code as well as from host bindings. In order for
* styling to be successfully flushed to the element (which will only happen once
* despite this being called multiple times), the following criteria must be met:
*
* - `flushStyling` is called from the very last directive that has styling for
* the element (see `allowStylingFlush()`).
* - one or more bindings for classes or styles has updated (this is checked by
* examining the classes or styles bit mask).
*
* If the style and class values are successfully applied to the element then
* the temporary state values for the element will be cleared. Otherwise, if
* this did not occur then the styling state is persisted (see `state.ts` for
* more information on how this works).
*/
export function applyClasses(
renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, context: TStylingContext,
element: RElement, directiveIndex: number): boolean {
let classesFlushed = false;
if (allowStylingFlush(context, directiveIndex)) {
const isFirstPass = !isContextLocked(context);
isFirstPass && lockContext(context);
if (classesBitMask) {
// there is no way to sanitize a class value therefore `sanitizer=null`
applyStyling(context, renderer, element, data, classesBitMask, setClass, null);
classesBitMask = 0;
classesFlushed = true;
}
currentClassIndex = STYLING_INDEX_START_VALUE;
export function flushStyling(
renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData,
classesContext: TStylingContext | null, stylesContext: TStylingContext | null,
element: RElement, directiveIndex: number, styleSanitizer: StyleSanitizeFn | null): void {
ngDevMode && ngDevMode.flushStyling++;
const persistState = classesContext ? stateIsPersisted(classesContext) :
(stylesContext ? stateIsPersisted(stylesContext) : false);
const allowFlushClasses = allowStylingFlush(classesContext, directiveIndex);
const allowFlushStyles = allowStylingFlush(stylesContext, directiveIndex);
// deferred bindings are bindings which are scheduled to register with
// the context at a later point. These bindings can only registered when
// the context will be 100% flushed to the element.
if (deferredBindingQueue.length && (allowFlushClasses || allowFlushStyles)) {
flushDeferredBindings();
}
return classesFlushed;
const state = getStylingState(element, persistState);
const classesFlushed = maybeApplyStyling(
renderer, element, data, classesContext, allowFlushClasses, state.classesBitMask, setClass,
null);
const stylesFlushed = maybeApplyStyling(
renderer, element, data, stylesContext, allowFlushStyles, state.stylesBitMask, setStyle,
styleSanitizer);
if (classesFlushed && stylesFlushed) {
resetStylingState();
if (persistState) {
deleteStylingStateFromStorage(element);
}
} else if (persistState) {
storeStylingState(element, state);
}
}
function maybeApplyStyling(
renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, data: LStylingData,
context: TStylingContext | null, allowFlush: boolean, bitMask: number,
styleSetter: ApplyStylingFn, styleSanitizer: any | null): boolean {
if (allowFlush && context) {
lockAndFinalizeContext(context);
if (contextHasUpdates(context, bitMask)) {
ngDevMode && (styleSanitizer ? ngDevMode.stylesApplied++ : ngDevMode.classesApplied++);
applyStyling(context !, renderer, element, data, bitMask, styleSetter, styleSanitizer);
return true;
}
}
return allowFlush;
}
function contextHasUpdates(context: TStylingContext | null, bitMask: number) {
return context && bitMask > BIT_MASK_START_VALUE;
}
/**
* Applies all style entries in the provided context to the provided element and resets
* any counter and/or bitMask values associated with style bindings.
* Locks the context (so no more bindings can be added) and also copies over initial class/style
* values into their binding areas.
*
* @returns whether or not the styles were flushed to the element.
* There are two main actions that take place in this function:
*
* - Locking the context:
* Locking the context is required so that the style/class instructions know NOT to
* register a binding again after the first update pass has run. If a locking bit was
* not used then it would need to scan over the context each time an instruction is run
* (which is expensive).
*
* - Patching initial values:
* Directives and component host bindings may include static class/style values which are
* bound to the host element. When this happens, the styling context will need to be informed
* so it can use these static styling values as defaults when a matching binding is falsy.
* These initial styling values are read from the initial styling values slot within the
* provided `TStylingContext` (which is an instance of a `StylingMapArray`). This inner map will
* be updated each time a host binding applies its static styling values (via `elementHostAttrs`)
* so these values are only read at this point because this is the very last point before the
* first style/class values are flushed to the element.
*/
export function applyStyles(
renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, context: TStylingContext,
element: RElement, directiveIndex: number, sanitizer: StyleSanitizeFn | null): boolean {
let stylesFlushed = false;
if (allowStylingFlush(context, directiveIndex)) {
const isFirstPass = !isContextLocked(context);
isFirstPass && lockContext(context);
if (stylesBitMask) {
applyStyling(context, renderer, element, data, stylesBitMask, setStyle, sanitizer);
stylesBitMask = 0;
stylesFlushed = true;
function lockAndFinalizeContext(context: TStylingContext): void {
if (!isContextLocked(context)) {
const initialValues = getStylingMapArray(context);
if (initialValues) {
updateInitialStylingOnContext(context, initialValues);
}
currentStyleIndex = STYLING_INDEX_START_VALUE;
return true;
lockContext(context);
}
return stylesFlushed;
}
/**
@ -367,7 +480,7 @@ export function applyStyles(
*
* If there are any map-based entries present (which are applied to the
* element via the `[style]` and `[class]` bindings) then those entries
* will be applied as well. However, the code for that is not apart of
* will be applied as well. However, the code for that is not a part of
* this function. Instead, each time a property is visited, then the
* code below will call an external function called `stylingMapsSyncFn`
* and, if present, it will keep the application of styling values in
@ -384,8 +497,6 @@ export function applyStyling(
context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
bindingData: LStylingData, bitMaskValue: number | boolean, applyStylingFn: ApplyStylingFn,
sanitizer: StyleSanitizeFn | null) {
deferredBindingQueue.length && flushDeferredBindings();
const bitMask = normalizeBitMaskValue(bitMaskValue);
const stylingMapsSyncFn = getStylingMapsSyncFn();
const mapsGuardMask = getGuardMask(context, TStylingContextIndex.MapBindingsPosition);
@ -477,7 +588,12 @@ export function setStylingMapsSyncFn(fn: SyncStylingMapsFn) {
* Assigns a style value to a style property for the given element.
*/
const setStyle: ApplyStylingFn =
(renderer: Renderer3 | null, native: any, prop: string, value: string | null) => {
(renderer: Renderer3 | null, native: RElement, prop: string, value: string | null) => {
// the reason why this may be `null` is either because
// it's a container element or it's a part of a test
// environment that doesn't have styling. In either
// case it's safe not to apply styling to the element.
const nativeStyle = native.style;
if (value) {
// opacity, z-index and flexbox all have number values
// and these need to be converted into strings so that
@ -486,12 +602,12 @@ const setStyle: ApplyStylingFn =
ngDevMode && ngDevMode.rendererSetStyle++;
renderer && isProceduralRenderer(renderer) ?
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) :
native.style.setProperty(prop, value);
(nativeStyle && nativeStyle.setProperty(prop, value));
} else {
ngDevMode && ngDevMode.rendererRemoveStyle++;
renderer && isProceduralRenderer(renderer) ?
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) :
native.style.removeProperty(prop);
(nativeStyle && nativeStyle.removeProperty(prop));
}
};
@ -499,16 +615,78 @@ const setStyle: ApplyStylingFn =
* Adds/removes the provided className value to the provided element.
*/
const setClass: ApplyStylingFn =
(renderer: Renderer3 | null, native: any, className: string, value: any) => {
(renderer: Renderer3 | null, native: RElement, className: string, value: any) => {
if (className !== '') {
// the reason why this may be `null` is either because
// it's a container element or it's a part of a test
// environment that doesn't have styling. In either
// case it's safe not to apply styling to the element.
const classList = native.classList;
if (value) {
ngDevMode && ngDevMode.rendererAddClass++;
renderer && isProceduralRenderer(renderer) ? renderer.addClass(native, className) :
native.classList.add(className);
(classList && classList.add(className));
} else {
ngDevMode && ngDevMode.rendererRemoveClass++;
renderer && isProceduralRenderer(renderer) ? renderer.removeClass(native, className) :
native.classList.remove(className);
(classList && classList.remove(className));
}
}
};
/**
* Iterates over all provided styling entries and renders them on the element.
*
* This function is used alongside a `StylingMapArray` entry. This entry is not
* the same as the `TStylingContext` and is only really used when an element contains
* initial styling values (e.g. `<div style="width:200px">`), but no style/class bindings
* are present. If and when that happens then this function will be called to render all
* initial styling values on an element.
*/
export function renderStylingMap(
renderer: Renderer3, element: RElement, stylingValues: TStylingContext | StylingMapArray | null,
isClassBased: boolean): void {
const stylingMapArr = getStylingMapArray(stylingValues);
if (stylingMapArr) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < stylingMapArr.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(stylingMapArr, i);
const value = getMapValue(stylingMapArr, i);
if (isClassBased) {
setClass(renderer, element, prop, value, null);
} else {
setStyle(renderer, element, prop, value, null);
}
}
}
}
/**
* Registers all initial styling entries into the provided context.
*
* This function will iterate over all entries in the provided `initialStyling` ar}ray and register
* them as default (initial) values in the provided context. Initial styling values in a context are
* the default values that are to be applied unless overwritten by a binding.
*
* The reason why this function exists and isn't a part of the context construction is because
* host binding is evaluated at a later stage after the element is created. This means that
* if a directive or component contains any initial styling code (i.e. `<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

@ -5,25 +5,22 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Sanitizer} from '../../sanitization/security';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {LContainer} from '../interfaces/container';
import {setInputsForProperty} from '../instructions/shared';
import {AttributeMarker, TAttributes, TNode, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {StylingContext as OldStylingContext, StylingIndex as OldStylingIndex} from '../interfaces/styling';
import {isStylingContext as isOldStylingContext} from '../interfaces/type_checks';
import {BINDING_INDEX, HEADER_OFFSET, HOST, LView, RENDERER} from '../interfaces/view';
import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getLView, getSelectedIndex} from '../state';
import {BINDING_INDEX, LView, RENDERER, TVIEW} from '../interfaces/view';
import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getCurrentStyleSanitizer, getLView, getPreviousOrParentTNode, getSelectedIndex, setCurrentStyleSanitizer} from '../state';
import {forceClassesAsString, forceStylesAsString} from '../styling/util';
import {NO_CHANGE} from '../tokens';
import {renderStringify} from '../util/misc_utils';
import {getTNode} from '../util/view_utils';
import {getNativeByTNode, getTNode} from '../util/view_utils';
import {applyClasses, applyStyles, registerBinding, updateClassBinding, updateStyleBinding} from './bindings';
import {TStylingContext} from './interfaces';
import {activeStylingMapFeature, normalizeIntoStylingMap} from './map_based_bindings';
import {setCurrentStyleSanitizer} from './state';
import {flushStyling, updateClassBinding, updateStyleBinding} from './bindings';
import {StylingMapArrayIndex, TStylingContext} from './interfaces';
import {activateStylingMapFeature, addItemToStylingMap, normalizeIntoStylingMap, stylingMapToString} from './map_based_bindings';
import {attachStylingDebugObject} from './styling_debug';
import {allocTStylingContext, getCurrentOrLViewSanitizer, hasValueChanged, updateContextDirectiveIndex} from './util';
import {allocTStylingContext, concatString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isStylingContext, updateLastDirectiveIndex as _updateLastDirectiveIndex} from './util';
@ -48,12 +45,24 @@ import {allocTStylingContext, getCurrentOrLViewSanitizer, hasValueChanged, updat
* central idea here is that the directive index values are bound
* into the context. The directive index is temporary and is only
* required until the `select(n)` instruction is fully functional.
*
* @codeGenApi
*/
export function stylingInit() {
export function ɵɵstyling() {
const lView = getLView();
const index = getSelectedIndex();
const tNode = getTNode(index, lView);
updateLastDirectiveIndex(tNode, getActiveDirectiveStylingIndex());
const tView = lView[TVIEW];
if (tView.firstTemplatePass) {
const tNode = getPreviousOrParentTNode();
const directiveStylingIndex = getActiveDirectiveStylingIndex();
// temporary workaround until `select(n)` is fully compatible
if (directiveStylingIndex) {
const fns = tNode.onElementCreationFns = tNode.onElementCreationFns || [];
fns.push(() => updateLastDirectiveIndex(tNode, directiveStylingIndex));
} else {
updateLastDirectiveIndex(tNode, directiveStylingIndex);
}
}
}
/**
@ -72,59 +81,220 @@ export function stylingInit() {
*
* @codeGenApi
*/
export function styleSanitizer(sanitizer: Sanitizer | StyleSanitizeFn | null): void {
export function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void {
setCurrentStyleSanitizer(sanitizer);
}
/**
* Mirror implementation of the `styleProp()` instruction (found in `instructions/styling.ts`).
* Update a style binding on an element with the provided value.
*
* If the style value is falsy then it will be removed from the element
* (or assigned a different value depending if there are any styles placed
* on the element with `styleMap` or any static styles that are
* present from when the element was created with `styling`).
*
* Note that the styling element is updated as part of `stylingApply`.
*
* @param prop A valid CSS property.
* @param value New value to write (`null` or an empty string to remove).
* @param suffix Optional suffix. Used with scalar values to add unit such as `px`.
* Note that when a suffix is provided then the underlying sanitizer will
* be ignored.
*
* Note that this will apply the provided style value to the host element if this function is called
* within a host binding.
*
* @codeGenApi
*/
export function styleProp(
export function ɵɵstyleProp(
prop: string, value: string | number | String | null, suffix?: string | null): void {
_stylingProp(prop, resolveStylePropValue(value, suffix), false);
stylePropInternal(getSelectedIndex(), prop, value, suffix);
}
export function stylePropInternal(
elementIndex: number, prop: string, value: string | number | String | null,
suffix?: string | null | undefined) {
const lView = getLView();
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
const updated = _stylingProp(
elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false,
deferStylingUpdate());
if (ngDevMode) {
ngDevMode.styleProp++;
if (updated) {
ngDevMode.stylePropCacheMiss++;
}
}
}
/**
* Mirror implementation of the `classProp()` instruction (found in `instructions/styling.ts`).
* Update a class binding on an element with the provided value.
*
* This instruction is meant to handle the `[class.foo]="exp"` case and,
* therefore, the class binding itself must already be allocated using
* `styling` within the creation block.
*
* @param prop A valid CSS class (only one).
* @param value A true/false value which will turn the class on or off.
*
* Note that this will apply the provided class value to the host element if this function
* is called within a host binding.
*
* @codeGenApi
*/
export function classProp(className: string, value: boolean | null): void {
_stylingProp(className, value, true);
export function ɵɵclassProp(className: string, value: boolean | null): void {
const lView = getLView();
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
const updated =
_stylingProp(getSelectedIndex(), bindingIndex, className, value, true, deferStylingUpdate());
if (ngDevMode) {
ngDevMode.classProp++;
if (updated) {
ngDevMode.classPropCacheMiss++;
}
}
}
/**
* Shared function used to update a prop-based styling binding for an element.
*/
function _stylingProp(
prop: string, value: boolean | number | String | string | null, isClassBased: boolean) {
elementIndex: number, bindingIndex: number, prop: string,
value: boolean | number | String | string | null | undefined | NO_CHANGE, isClassBased: boolean,
defer: boolean): boolean {
const lView = getLView();
const tNode = getTNode(elementIndex, lView);
const native = getNativeByTNode(tNode, lView) as RElement;
let updated = false;
if (value !== NO_CHANGE) {
if (isClassBased) {
updated = updateClassBinding(
getClassesContext(tNode), lView, native, prop, bindingIndex,
value as string | boolean | null, defer, false);
} else {
const sanitizer = getCurrentStyleSanitizer();
updated = updateStyleBinding(
getStylesContext(tNode), lView, native, prop, bindingIndex, value as string | null,
sanitizer, defer, false);
}
}
return updated;
}
/**
* Update style bindings using an object literal on an element.
*
* This instruction is meant to apply styling via the `[style]="exp"` template bindings.
* When styles are applied to the element they will then be updated with respect to
* any styles/classes set via `styleProp`. If any styles are set to falsy
* then they will be removed from the element.
*
* Note that the styling instruction will not be applied until `stylingApply` is called.
*
* @param styles A key/value style map of the styles that will be applied to the given element.
* Any missing styles (that have already been applied to the element beforehand) will be
* removed (unset) from the element's styling.
*
* Note that this will apply the provided styleMap value to the host element if this function
* is called within a host binding.
*
* @codeGenApi
*/
export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | null): void {
const index = getSelectedIndex();
const lView = getLView();
const bindingIndex = lView[BINDING_INDEX]++;
const tNode = getTNode(index, lView);
const defer = getActiveDirectiveSuperClassHeight() > 0;
if (isClassBased) {
updateClassBinding(
getClassesContext(tNode), lView, prop, bindingIndex, value as string | boolean | null,
defer, false);
} else {
const sanitizer = getCurrentOrLViewSanitizer(lView);
updateStyleBinding(
getStylesContext(tNode), lView, prop, bindingIndex, value as string | null, sanitizer,
defer, false);
const context = getStylesContext(tNode);
const directiveIndex = getActiveDirectiveStylingIndex();
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
// inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function)
if (!directiveIndex && hasStyleInput(tNode) && styles !== NO_CHANGE) {
updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false);
styles = NO_CHANGE;
}
const updated = _stylingMap(index, context, bindingIndex, styles, false, deferStylingUpdate());
if (ngDevMode) {
ngDevMode.styleMap++;
if (updated) {
ngDevMode.styleMapCacheMiss++;
}
}
}
/**
* Mirror implementation of the `styleMap()` instruction (found in `instructions/styling.ts`).
* Update class bindings using an object literal or class-string on an element.
*
* This instruction is meant to apply styling via the `[class]="exp"` template bindings.
* When classes are applied to the element they will then be updated with
* respect to any styles/classes set via `classProp`. If any
* classes are set to falsy then they will be removed from the element.
*
* Note that the styling instruction will not be applied until `stylingApply` is called.
* Note that this will the provided classMap value to the host element if this function is called
* within a host binding.
*
* @param classes A key/value map or string of CSS classes that will be added to the
* given element. Any missing classes (that have already been applied to the element
* beforehand) will be removed (unset) from the element's list of CSS classes.
*
* @codeGenApi
*/
export function styleMap(styles: {[styleName: string]: any} | NO_CHANGE | null): void {
_stylingMap(styles, false);
export function ɵɵclassMap(classes: {[className: string]: any} | NO_CHANGE | string | null): void {
classMapInternal(getSelectedIndex(), classes);
}
/**
* Mirror implementation of the `classMap()` instruction (found in `instructions/styling.ts`).
*/
export function classMap(classes: {[className: string]: any} | NO_CHANGE | string | null): void {
_stylingMap(classes, true);
export function classMapInternal(
elementIndex: number, classes: {[className: string]: any} | NO_CHANGE | string | null) {
const lView = getLView();
const tNode = getTNode(elementIndex, lView);
const context = getClassesContext(tNode);
const directiveIndex = getActiveDirectiveStylingIndex();
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
// inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function)
if (!directiveIndex && hasClassInput(tNode) && classes !== NO_CHANGE) {
updateDirectiveInputValue(context, lView, tNode, bindingIndex, classes, true);
classes = NO_CHANGE;
}
const updated =
_stylingMap(elementIndex, context, bindingIndex, classes, true, deferStylingUpdate());
if (ngDevMode) {
ngDevMode.classMap++;
if (updated) {
ngDevMode.classMapCacheMiss++;
}
}
}
/**
@ -133,28 +303,89 @@ export function classMap(classes: {[className: string]: any} | NO_CHANGE | strin
* When this function is called it will activate support for `[style]` and
* `[class]` bindings in Angular.
*/
function _stylingMap(value: {[key: string]: any} | string | null, isClassBased: boolean) {
activeStylingMapFeature();
const index = getSelectedIndex();
function _stylingMap(
elementIndex: number, context: TStylingContext, bindingIndex: number,
value: {[key: string]: any} | string | null, isClassBased: boolean, defer: boolean) {
activateStylingMapFeature();
const lView = getLView();
const bindingIndex = lView[BINDING_INDEX]++;
let valueHasChanged = false;
if (value !== NO_CHANGE) {
const tNode = getTNode(index, lView);
const defer = getActiveDirectiveSuperClassHeight() > 0;
const tNode = getTNode(elementIndex, lView);
const native = getNativeByTNode(tNode, lView) as RElement;
const oldValue = lView[bindingIndex];
const valueHasChanged = hasValueChanged(oldValue, value);
const lStylingMap = normalizeIntoStylingMap(oldValue, value);
valueHasChanged = hasValueChanged(oldValue, value);
const stylingMapArr = normalizeIntoStylingMap(oldValue, value, !isClassBased);
if (isClassBased) {
updateClassBinding(
getClassesContext(tNode), lView, null, bindingIndex, lStylingMap, defer, valueHasChanged);
context, lView, native, null, bindingIndex, stylingMapArr, defer, valueHasChanged);
} else {
const sanitizer = getCurrentOrLViewSanitizer(lView);
const sanitizer = getCurrentStyleSanitizer();
updateStyleBinding(
getStylesContext(tNode), lView, null, bindingIndex, lStylingMap, sanitizer, defer,
context, lView, native, null, bindingIndex, stylingMapArr, sanitizer, defer,
valueHasChanged);
}
}
return valueHasChanged;
}
/**
* Writes a value to a directive's `style` or `class` input binding (if it has changed).
*
* If a directive has a `@Input` binding that is set on `style` or `class` then that value
* will take priority over the underlying style/class styling bindings. This value will
* be updated for the binding each time during change detection.
*
* When this occurs this function will attempt to write the value to the input binding
* depending on the following situations:
*
* - If `oldValue !== newValue`
* - If `newValue` is `null` (but this is skipped if it is during the first update pass--
* which is when the context is not locked yet)
*/
function updateDirectiveInputValue(
context: TStylingContext, lView: LView, tNode: TNode, bindingIndex: number, newValue: any,
isClassBased: boolean): void {
const oldValue = lView[bindingIndex];
if (oldValue !== newValue) {
// even if the value has changed we may not want to emit it to the
// directive input(s) in the event that it is falsy during the
// first update pass.
if (newValue || isContextLocked(context)) {
const inputs = tNode.inputs ![isClassBased ? 'class' : 'style'] !;
const initialValue = getInitialStylingValue(context);
const value = normalizeStylingDirectiveInputValue(initialValue, newValue, isClassBased);
setInputsForProperty(lView, inputs, value);
}
lView[bindingIndex] = newValue;
}
}
/**
* Returns the appropriate directive input value for `style` or `class`.
*
* Earlier versions of Angular expect a binding value to be passed into directive code
* exactly as it is unless there is a static value present (in which case both values
* will be stringified and concatenated).
*/
function normalizeStylingDirectiveInputValue(
initialValue: string, bindingValue: string | {[key: string]: any} | null,
isClassBased: boolean) {
let value = bindingValue;
// we only concat values if there is an initial value, otherwise we return the value as is.
// Note that this is to satisfy backwards-compatibility in Angular.
if (initialValue.length > 0) {
if (isClassBased) {
value = concatString(initialValue, forceClassesAsString(bindingValue));
} else {
value = concatString(
initialValue, forceStylesAsString(bindingValue as{[key: string]: any} | null | undefined),
';');
}
}
return value;
}
/**
@ -169,53 +400,22 @@ function _stylingMap(value: {[key: string]: any} | string | null, isClassBased:
* host binding instruction code at the right time), this means that a
* styling apply function is still needed.
*
* This function is a mirror implementation of the `stylingApply()`
* instruction (found in `instructions/styling.ts`).
* @codeGenApi
*/
export function stylingApply() {
const index = getSelectedIndex();
export function ɵɵstylingApply() {
const elementIndex = getSelectedIndex();
const lView = getLView();
const tNode = getTNode(index, lView);
const tNode = getTNode(elementIndex, lView);
const renderer = getRenderer(tNode, lView);
const native = getNativeFromLView(index, lView);
const native = getNativeByTNode(tNode, lView) as RElement;
const directiveIndex = getActiveDirectiveStylingIndex();
applyClasses(renderer, lView, getClassesContext(tNode), native, directiveIndex);
const sanitizer = getCurrentOrLViewSanitizer(lView);
applyStyles(renderer, lView, getStylesContext(tNode), native, directiveIndex, sanitizer);
const sanitizer = getCurrentStyleSanitizer();
flushStyling(
renderer, lView, getClassesContext(tNode), getStylesContext(tNode), native, directiveIndex,
sanitizer);
setCurrentStyleSanitizer(null);
}
/**
* Temporary function to bridge styling functionality between this new
* refactor (which is here inside of `styling_next/`) and the old
* implementation (which lives inside of `styling/`).
*
* The purpose of this function is to traverse through the LView data
* for a specific element index and return the native node. Because the
* current implementation relies on there being a styling context array,
* the code below will need to loop through these array values until it
* gets a native element node.
*
* Note that this code is temporary and will disappear once the new
* styling refactor lands in its entirety.
*/
function getNativeFromLView(index: number, viewData: LView): RElement {
let storageIndex = index + HEADER_OFFSET;
let slotValue: LContainer|LView|OldStylingContext|RElement = viewData[storageIndex];
let wrapper: LContainer|LView|OldStylingContext = viewData;
while (Array.isArray(slotValue)) {
wrapper = slotValue;
slotValue = slotValue[HOST] as LView | OldStylingContext | RElement;
}
if (isOldStylingContext(wrapper)) {
return wrapper[OldStylingIndex.ElementPosition] as RElement;
} else {
return slotValue;
}
}
function getRenderer(tNode: TNode, lView: LView) {
return tNode.type === TNodeType.Element ? lView[RENDERER] : null;
}
@ -224,28 +424,41 @@ function getRenderer(tNode: TNode, lView: LView) {
* Searches and assigns provided all static style/class entries (found in the `attrs` value)
* and registers them in their respective styling contexts.
*/
export function registerInitialStylingIntoContext(
tNode: TNode, attrs: TAttributes, startIndex: number) {
let classesContext !: TStylingContext;
let stylesContext !: TStylingContext;
export function registerInitialStylingOnTNode(
tNode: TNode, attrs: TAttributes, startIndex: number): boolean {
let hasAdditionalInitialStyling = false;
let styles = getStylingMapArray(tNode.styles);
let classes = getStylingMapArray(tNode.classes);
let mode = -1;
for (let i = startIndex; i < attrs.length; i++) {
const attr = attrs[i];
const attr = attrs[i] as string;
if (typeof attr == 'number') {
mode = attr;
} else if (mode == AttributeMarker.Classes) {
classesContext = classesContext || getClassesContext(tNode);
registerBinding(classesContext, -1, attr as string, true, false);
classes = classes || [''];
addItemToStylingMap(classes, attr, true);
hasAdditionalInitialStyling = true;
} else if (mode == AttributeMarker.Styles) {
stylesContext = stylesContext || getStylesContext(tNode);
registerBinding(stylesContext, -1, attr as string, attrs[++i] as string, false);
const value = attrs[++i] as string | null;
styles = styles || [''];
addItemToStylingMap(styles, attr, value);
hasAdditionalInitialStyling = true;
}
}
if (classes && classes.length > StylingMapArrayIndex.ValuesStartPosition) {
classes[StylingMapArrayIndex.RawValuePosition] = stylingMapToString(classes, true);
tNode.classes = classes;
}
if (styles && styles.length > StylingMapArrayIndex.ValuesStartPosition) {
styles[StylingMapArrayIndex.RawValuePosition] = stylingMapToString(styles, false);
tNode.styles = styles;
}
return hasAdditionalInitialStyling;
}
/**
* Mirror implementation of the same function found in `instructions/styling.ts`.
*/
export function getActiveDirectiveStylingIndex(): number {
// whenever a directive's hostBindings function is called a uniqueId value
// is assigned. Normally this is enough to help distinguish one directive
@ -269,8 +482,8 @@ export function getActiveDirectiveStylingIndex(): number {
* removed once `select(n)` is fixed).
*/
function updateLastDirectiveIndex(tNode: TNode, directiveIndex: number) {
updateContextDirectiveIndex(getClassesContext(tNode), directiveIndex);
updateContextDirectiveIndex(getStylesContext(tNode), directiveIndex);
_updateLastDirectiveIndex(getClassesContext(tNode), directiveIndex);
_updateLastDirectiveIndex(getStylesContext(tNode), directiveIndex);
}
function getStylesContext(tNode: TNode): TStylingContext {
@ -285,23 +498,26 @@ function getClassesContext(tNode: TNode): TStylingContext {
* Returns/instantiates a styling context from/to a `tNode` instance.
*/
function getContext(tNode: TNode, isClassBased: boolean) {
let context = isClassBased ? tNode.newClasses : tNode.newStyles;
if (!context) {
context = allocTStylingContext();
let context = isClassBased ? tNode.classes : tNode.styles;
if (!isStylingContext(context)) {
context = allocTStylingContext(context);
if (ngDevMode) {
attachStylingDebugObject(context);
attachStylingDebugObject(context as TStylingContext);
}
if (isClassBased) {
tNode.newClasses = context;
tNode.classes = context;
} else {
tNode.newStyles = context;
tNode.styles = context;
}
}
return context;
return context as TStylingContext;
}
function resolveStylePropValue(
value: string | number | String | null, suffix: string | null | undefined) {
value: string | number | String | null | NO_CHANGE, suffix: string | null | undefined): string|
null|undefined|NO_CHANGE {
if (value === NO_CHANGE) return value;
let resolvedValue: string|null = null;
if (value !== null) {
if (suffix) {
@ -318,3 +534,19 @@ function resolveStylePropValue(
}
return resolvedValue;
}
/**
* Whether or not a style/class binding update should be applied later.
*
* This function will decide whether a binding should be applied immediately
* or later (just before the styles/classes are flushed to the element). The
* reason why this feature exists is because of super/sub directive inheritance.
* Angular will evaluate host bindings on the super directive first and the sub
* directive, but the styling bindings on the sub directive are of higher priority
* than the super directive. For this reason all styling bindings that take place
* in this circumstance will need to be deferred until later so that they can be
* applied together and in a different order (the algorithm handles that part).
*/
function deferStylingUpdate(): boolean {
return getActiveDirectiveSuperClassHeight() > 0;
}

View File

@ -37,17 +37,37 @@ import {LView} from '../interfaces/view';
* tNode.classes = [ ... a context only for classes ... ];
* ```
*
* `tNode.styles` and `tNode.classes` can be an instance of the following:
*
* ```typescript
* tNode.styles = null; // no static styling or styling bindings active
* tNode.styles = StylingMapArray; // only static values present (e.g. `<div style="width:200">`)
* tNode.styles = TStylingContext; // one or more styling bindings present (e.g. `<div
* [style.width]>`)
* ```
*
* Both `tNode.styles` and `tNode.classes` are instantiated when anything
* styling-related is active on an element. They are first created from
* from the any of the element-level instructions (e.g. `element`,
* `elementStart`, `elementHostAttrs`). When any static style/class
* values are encountered they are registered on the `tNode.styles`
* and `tNode.classes` data-structures. By default (when any static
* values are encountered) the `tNode.styles` or `tNode.classes` values
* are instances of a `StylingMapArray`. Only when style/class bindings
* are detected then that styling map is converted into an instance of
* `TStylingContext`.
*
* Due to the fact the the `TStylingContext` is stored on a `TNode`
* this means that all data within the context is static. Instead of
* storing actual styling binding values, the lView binding index values
* are stored within the context. (static nature means it is more compact.)
*
* ```typescript
* // <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
*
* 0b001, // guard mask for width
@ -64,6 +84,7 @@ import {LView} from '../interfaces/view';
* ];
*
* tNode.classesContext = [
* [], // initial values array
* 0, // the context config value
*
* 0b001, // guard mask for active
@ -260,7 +281,11 @@ import {LView} from '../interfaces/view';
* If sanitization returns an empty value then that empty value will be applied
* to the element.
*/
export interface TStylingContext extends Array<number|string|number|boolean|null|LStylingMap> {
export interface TStylingContext extends
Array<number|string|number|boolean|null|StylingMapArray|{}> {
/** Initial value position for static styles */
[TStylingContextIndex.InitialStylingValuePosition]: StylingMapArray|null;
/** Configuration data for the context */
[TStylingContextIndex.ConfigPosition]: TStylingConfigFlags;
@ -268,7 +293,7 @@ export interface TStylingContext extends Array<number|string|number|boolean|null
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.MaxDirectiveIndexPosition]: number;
[TStylingContextIndex.LastDirectiveIndexPosition]: number;
/** The bit guard value for all map-based bindings on an element */
[TStylingContextIndex.MapBindingsBitGuardPosition]: number;
@ -303,22 +328,41 @@ export const enum TStylingConfigFlags {
* bindings can be added to it).
*/
Locked = 0b1,
/**
* Whether or not to store the state between updates in a global storage map.
*
* 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.
*/
PersistStateValues = 0b10,
/** A Mask of all the configurations */
Mask = 0b11,
/** Total amount of configuration bits used */
TotalBits = 2,
}
/**
* An index of position and offset values used to natigate the `TStylingContext`.
*/
export const enum TStylingContextIndex {
ConfigPosition = 0,
MaxDirectiveIndexPosition = 1,
InitialStylingValuePosition = 0,
ConfigPosition = 1,
LastDirectiveIndexPosition = 2,
// index/offset values for map-based entries (i.e. `[style]`
// and `[class] bindings).
MapBindingsPosition = 2,
MapBindingsBitGuardPosition = 2,
MapBindingsValuesCountPosition = 3,
MapBindingsPropPosition = 4,
MapBindingsBindingsStartPosition = 5,
// and `[class]` bindings).
MapBindingsPosition = 3,
MapBindingsBitGuardPosition = 3,
MapBindingsValuesCountPosition = 4,
MapBindingsPropPosition = 5,
MapBindingsBindingsStartPosition = 6,
// each tuple entry in the context
// (mask, count, prop, ...bindings||default-value)
@ -326,6 +370,7 @@ export const enum TStylingContextIndex {
ValuesCountOffset = 1,
PropOffset = 2,
BindingsStartOffset = 3,
MinTupleLength = 4,
}
/**
@ -366,14 +411,14 @@ export type LStylingData = LView | (string | number | boolean | null)[];
* of the key/value array that was used to populate the property/
* value entries that take place in the remainder of the array.
*/
export interface LStylingMap extends Array<{}|string|number|null> {
[LStylingMapIndex.RawValuePosition]: {}|string|null;
export interface StylingMapArray extends Array<{}|string|number|null> {
[StylingMapArrayIndex.RawValuePosition]: {}|string|null;
}
/**
* An index of position and offset points for any data stored within a `LStylingMap` instance.
* An index of position and offset points for any data stored within a `StylingMapArray` instance.
*/
export const enum LStylingMapIndex {
export const enum StylingMapArrayIndex {
/** The location of the raw key/value map instance used last to populate the array entries */
RawValuePosition = 0,
@ -394,7 +439,7 @@ export const enum LStylingMapIndex {
* Used to apply/traverse across all map-based styling entries up to the provided `targetProp`
* value.
*
* When called, each of the map-based `LStylingMap` entries (which are stored in
* When called, each of the map-based `StylingMapArray` entries (which are stored in
* the provided `LStylingData` array) will be iterated over. Depending on the provided
* `mode` value, each prop/value entry may be applied or skipped over.
*

View File

@ -9,8 +9,9 @@ import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanit
import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer';
import {setStylingMapsSyncFn} from './bindings';
import {ApplyStylingFn, LStylingData, LStylingMap, LStylingMapIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from './interfaces';
import {getBindingValue, getValuesCount, isStylingValueDefined} from './util';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from './interfaces';
import {concatString, getBindingValue, getMapProp, getMapValue, getValuesCount, hyphenate, isStylingValueDefined, setMapValue} from './util';
/**
@ -45,8 +46,8 @@ import {getBindingValue, getValuesCount, isStylingValueDefined} from './util';
*
* # The Algorithm
* Whenever a map-based binding updates (which is when the identity of the
* map-value changes) then the map is iterated over and a `LStylingMap` array
* is produced. The `LStylingMap` instance is stored in the binding location
* map-value changes) then the map is iterated over and a `StylingMapArray` array
* is produced. The `StylingMapArray` instance is stored in the binding location
* where the `BINDING_INDEX` is situated when the `styleMap()` or `classMap()`
* instruction were called. Once the binding changes, then the internal `bitMask`
* value is marked as dirty.
@ -154,19 +155,18 @@ function innerSyncStylingMap(
mode: StylingMapsSyncMode, targetProp: string | null, currentMapIndex: number,
defaultValue: string | null): boolean {
let targetPropValueWasApplied = false;
const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition);
if (currentMapIndex < totalMaps) {
const bindingIndex = getBindingValue(
context, TStylingContextIndex.MapBindingsPosition, currentMapIndex) as number;
const lStylingMap = data[bindingIndex] as LStylingMap;
const stylingMapArr = data[bindingIndex] as StylingMapArray;
let cursor = getCurrentSyncCursor(currentMapIndex);
while (cursor < lStylingMap.length) {
const prop = getMapProp(lStylingMap, cursor);
while (cursor < stylingMapArr.length) {
const prop = getMapProp(stylingMapArr, cursor);
const iteratedTooFar = targetProp && prop > targetProp;
const isTargetPropMatched = !iteratedTooFar && prop === targetProp;
const value = getMapValue(lStylingMap, cursor);
const value = getMapValue(stylingMapArr, cursor);
const valueIsDefined = isStylingValueDefined(value);
// the recursive code is designed to keep applying until
@ -183,6 +183,9 @@ function innerSyncStylingMap(
currentMapIndex + 1, defaultValue);
if (iteratedTooFar) {
if (!targetPropValueWasApplied) {
targetPropValueWasApplied = valueApplied;
}
break;
}
@ -198,11 +201,24 @@ function innerSyncStylingMap(
}
targetPropValueWasApplied = valueApplied && isTargetPropMatched;
cursor += LStylingMapIndex.TupleSize;
cursor += StylingMapArrayIndex.TupleSize;
}
setCurrentSyncCursor(currentMapIndex, cursor);
// this is a fallback case in the event that the styling map is `null` for this
// binding but there are other map-based bindings that need to be evaluated
// afterwards. If the `prop` value is falsy then the intention is to cycle
// through all of the properties in the remaining maps as well. If the current
// styling map is too short then there are no values to iterate over. In either
// case the follow-up maps need to be iterated over.
if (stylingMapArr.length === StylingMapArrayIndex.ValuesStartPosition || !targetProp) {
return innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp,
currentMapIndex + 1, defaultValue);
}
}
return targetPropValueWasApplied;
}
@ -210,7 +226,7 @@ function innerSyncStylingMap(
/**
* Enables support for map-based styling bindings (e.g. `[style]` and `[class]` bindings).
*/
export function activeStylingMapFeature() {
export function activateStylingMapFeature() {
setStylingMapsSyncFn(syncStylingMap);
}
@ -220,19 +236,21 @@ export function activeStylingMapFeature() {
* If an inner map is iterated on then this is done so for one
* of two reasons:
*
* - The target property was detected and the inner map
* must now "catch up" (pointer-wise) up to where the current
* map's cursor is situated.
* - value is being applied:
* if the value is being applied from this current styling
* map then there is no need to apply it in a deeper map.
*
* - The target property was not detected in the current map
* and must be found in an inner map. This can only be allowed
* if the current map iteration is not set to skip the target
* property.
* - value is being not applied:
* apply the value if it is found in a deeper map.
*
* When these reasons are encountered the flags will for the
* inner map mode will be configured.
*/
function resolveInnerMapMode(
currentMode: number, valueIsDefined: boolean, isExactMatch: boolean): number {
let innerMode = currentMode;
if (!valueIsDefined && isExactMatch && !(currentMode & StylingMapsSyncMode.SkipTargetProp)) {
if (!valueIsDefined && !(currentMode & StylingMapsSyncMode.SkipTargetProp) &&
(isExactMatch || (currentMode & StylingMapsSyncMode.ApplyAllValues))) {
// case 1: set the mode to apply the targeted prop value if it
// ends up being encountered in another map value
innerMode |= StylingMapsSyncMode.ApplyTargetProp;
@ -243,6 +261,7 @@ function resolveInnerMapMode(
innerMode |= StylingMapsSyncMode.SkipTargetProp;
innerMode &= ~StylingMapsSyncMode.ApplyTargetProp;
}
return innerMode;
}
@ -281,7 +300,7 @@ const MAP_CURSORS: number[] = [];
*/
function resetSyncCursors() {
for (let i = 0; i < MAP_CURSORS.length; i++) {
MAP_CURSORS[i] = LStylingMapIndex.ValuesStartPosition;
MAP_CURSORS[i] = StylingMapArrayIndex.ValuesStartPosition;
}
}
@ -290,7 +309,7 @@ function resetSyncCursors() {
*/
function getCurrentSyncCursor(mapIndex: number) {
if (mapIndex >= MAP_CURSORS.length) {
MAP_CURSORS.push(LStylingMapIndex.ValuesStartPosition);
MAP_CURSORS.push(StylingMapArrayIndex.ValuesStartPosition);
}
return MAP_CURSORS[mapIndex];
}
@ -303,33 +322,34 @@ function setCurrentSyncCursor(mapIndex: number, indexValue: number) {
}
/**
* Used to convert a {key:value} map into a `LStylingMap` array.
* Used to convert a {key:value} map into a `StylingMapArray` array.
*
* This function will either generate a new `LStylingMap` instance
* This function will either generate a new `StylingMapArray` instance
* or it will patch the provided `newValues` map value into an
* existing `LStylingMap` value (this only happens if `bindingValue`
* is an instance of `LStylingMap`).
* existing `StylingMapArray` value (this only happens if `bindingValue`
* is an instance of `StylingMapArray`).
*
* If a new key/value map is provided with an old `LStylingMap`
* If a new key/value map is provided with an old `StylingMapArray`
* value then all properties will be overwritten with their new
* values or with `null`. This means that the array will never
* shrink in size (but it will also not be created and thrown
* away whenever the {key:value} map entries change).
*/
export function normalizeIntoStylingMap(
bindingValue: null | LStylingMap,
newValues: {[key: string]: any} | string | null | undefined): LStylingMap {
const lStylingMap: LStylingMap = Array.isArray(bindingValue) ? bindingValue : [null];
lStylingMap[LStylingMapIndex.RawValuePosition] = newValues || null;
bindingValue: null | StylingMapArray,
newValues: {[key: string]: any} | string | null | undefined,
normalizeProps?: boolean): StylingMapArray {
const stylingMapArr: StylingMapArray = Array.isArray(bindingValue) ? bindingValue : [null];
stylingMapArr[StylingMapArrayIndex.RawValuePosition] = newValues || null;
// because the new values may not include all the properties
// that the old ones had, all values are set to `null` before
// the new values are applied. This way, when flushed, the
// styling algorithm knows exactly what style/class values
// to remove from the element (since they are `null`).
for (let j = LStylingMapIndex.ValuesStartPosition; j < lStylingMap.length;
j += LStylingMapIndex.TupleSize) {
setMapValue(lStylingMap, j, null);
for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length;
j += StylingMapArrayIndex.TupleSize) {
setMapValue(stylingMapArr, j, null);
}
let props: string[]|null = null;
@ -346,36 +366,80 @@ export function normalizeIntoStylingMap(
}
if (props) {
outer: for (let i = 0; i < props.length; i++) {
for (let i = 0; i < props.length; i++) {
const prop = props[i] as string;
const newProp = normalizeProps ? hyphenate(prop) : prop;
const value = allValuesTrue ? true : map ![prop];
for (let j = LStylingMapIndex.ValuesStartPosition; j < lStylingMap.length;
j += LStylingMapIndex.TupleSize) {
const propAtIndex = getMapProp(lStylingMap, j);
if (prop <= propAtIndex) {
if (propAtIndex === prop) {
setMapValue(lStylingMap, j, value);
} else {
lStylingMap.splice(j, 0, prop, value);
}
continue outer;
}
}
lStylingMap.push(prop, value);
addItemToStylingMap(stylingMapArr, newProp, value, true);
}
}
return lStylingMap;
return stylingMapArr;
}
export function getMapProp(map: LStylingMap, index: number): string {
return map[index + LStylingMapIndex.PropOffset] as string;
/**
* Inserts the provided item into the provided styling array at the right spot.
*
* The `StylingMapArray` type is a sorted key/value array of entries. This means
* that when a new entry is inserted it must be placed at the right spot in the
* array. This function figures out exactly where to place it.
*/
export function addItemToStylingMap(
stylingMapArr: StylingMapArray, prop: string, value: string | boolean | null,
allowOverwrite?: boolean) {
for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length;
j += StylingMapArrayIndex.TupleSize) {
const propAtIndex = getMapProp(stylingMapArr, j);
if (prop <= propAtIndex) {
let applied = false;
if (propAtIndex === prop) {
const valueAtIndex = stylingMapArr[j];
if (allowOverwrite || !isStylingValueDefined(valueAtIndex)) {
applied = true;
setMapValue(stylingMapArr, j, value);
}
} else {
applied = true;
stylingMapArr.splice(j, 0, prop, value);
}
return applied;
}
}
stylingMapArr.push(prop, value);
return true;
}
export function setMapValue(map: LStylingMap, index: number, value: string | null): void {
map[index + LStylingMapIndex.ValueOffset] = value;
/**
* Converts the provided styling map array into a string.
*
* Classes => `one two three`
* Styles => `prop:value; prop2:value2`
*/
export function stylingMapToString(map: StylingMapArray, isClassBased: boolean): string {
let str = '';
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i) as string;
const attrValue = concatString(prop, isClassBased ? '' : value, ':');
str = concatString(str, attrValue, isClassBased ? ' ' : '; ');
}
return str;
}
export function getMapValue(map: LStylingMap, index: number): string|null {
return map[index + LStylingMapIndex.ValueOffset] as string | null;
/**
* Converts the provided styling map array into a key value map.
*/
export function stylingMapToStringMap(map: StylingMapArray | null): {[key: string]: any} {
let stringMap: {[key: string]: any} = {};
if (map) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i) as string;
stringMap[prop] = value;
}
}
return stringMap;
}

View File

@ -5,57 +5,97 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Sanitizer} from '../../sanitization/security';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
/**
* --------
*
* This file contains temporary code to incorporate the new styling refactor
* code to work alongside the existing instruction set.
* // TODO(matsko): add updateMask info
*
* This file will be removed once `select(n)` is fully functional (once
* it is able to evaluate host bindings in sync element-by-element
* with template code).
* This file contains all state-based logic for styling in Angular.
*
* Styling in Angular is evaluated with a series of styling-specific
* template instructions which are called one after another each time
* change detection occurs in Angular.
*
* Styling makes use of various temporary, state-based variables between
* instructions so that it can better cache and optimize its values.
* These values are usually populated and cleared when an element is
* exited in change detection (once all the instructions are run for
* that element).
*
* There are, however, situations where the state-based values
* need to be stored and used at a later point. This ONLY occurs when
* there are template-level as well as host-binding-level styling
* instructions on the same element. The example below shows exactly
* what could be:
*
* ```html
* <!-- 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`.
*
* --------
*/
/**
* A temporary enum of states that inform the core whether or not
* to defer all styling instruction calls to the old or new
* styling implementation.
*/
export const enum RuntimeStylingMode {
UseOld = 0,
UseBothOldAndNew = 1,
UseNew = 2,
}
let _stylingState: StylingState|null = null;
const _stateStorage = new Map<any, StylingState>();
let _stylingMode = 0;
// this value is not used outside this file and is only here
// as a caching check for when the element changes.
let _stylingElement: any = null;
/**
* Temporary function used to inform the existing styling algorithm
* code to delegate all styling instruction calls to the new refactored
* styling code.
* Used as a state reference for update values between style/class binding instructions.
*/
export function runtimeSetStylingMode(mode: RuntimeStylingMode) {
_stylingMode = mode;
export interface StylingState {
classesBitMask: number;
classesIndex: number;
stylesBitMask: number;
stylesIndex: number;
}
export function runtimeIsNewStylingInUse() {
return _stylingMode > RuntimeStylingMode.UseOld;
export const STYLING_INDEX_START_VALUE = 1;
export const BIT_MASK_START_VALUE = 0;
export function getStylingState(element: any, readFromMap?: boolean): StylingState {
if (!_stylingElement || element !== _stylingElement) {
_stylingElement = element;
if (readFromMap) {
_stylingState = _stateStorage.get(element) || null;
ngDevMode && ngDevMode.stylingReadPersistedState++;
}
_stylingState = _stylingState || {
classesBitMask: BIT_MASK_START_VALUE,
classesIndex: STYLING_INDEX_START_VALUE,
stylesBitMask: BIT_MASK_START_VALUE,
stylesIndex: STYLING_INDEX_START_VALUE,
};
}
return _stylingState !;
}
export function runtimeAllowOldStyling() {
return _stylingMode < RuntimeStylingMode.UseNew;
export function resetStylingState() {
_stylingState = null;
_stylingElement = null;
}
let _currentSanitizer: Sanitizer|StyleSanitizeFn|null;
export function setCurrentStyleSanitizer(sanitizer: Sanitizer | StyleSanitizeFn | null) {
_currentSanitizer = sanitizer;
export function storeStylingState(element: any, state: StylingState) {
ngDevMode && ngDevMode.stylingWritePersistedState++;
_stateStorage.set(element, state);
}
export function getCurrentStyleSanitizer() {
return _currentSanitizer;
export function deleteStylingStateFromStorage(element: any) {
_stateStorage.delete(element);
}
export function resetAllStylingState() {
resetStylingState();
_stateStorage.clear();
}

View File

@ -5,17 +5,16 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Sanitizer} from '../../sanitization/security';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {RElement} from '../interfaces/renderer';
import {LView, SANITIZER} from '../interfaces/view';
import {LView} from '../interfaces/view';
import {getCurrentStyleSanitizer} from '../state';
import {attachDebugObject} from '../util/debug_utils';
import {applyStyling} from './bindings';
import {ApplyStylingFn, LStylingData, TStylingContext, TStylingContextIndex} from './interfaces';
import {activeStylingMapFeature} from './map_based_bindings';
import {getCurrentStyleSanitizer} from './state';
import {getCurrentOrLViewSanitizer, getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased, isSanitizationRequired} from './util';
import {activateStylingMapFeature} from './map_based_bindings';
import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased, isSanitizationRequired} from './util';
@ -79,7 +78,7 @@ export interface TStylingTupleSummary {
/** The property (style or class property) that this tuple represents */
prop: string;
/** The total amount of styling entries apart of this tuple */
/** The total amount of styling entries a part of this tuple */
valuesCount: number;
/**
@ -208,15 +207,14 @@ export class NodeStylingDebug implements DebugStyling {
const mockElement = {} as any;
const hasMaps = getValuesCount(this.context, TStylingContextIndex.MapBindingsPosition) > 0;
if (hasMaps) {
activeStylingMapFeature();
activateStylingMapFeature();
}
const mapFn: ApplyStylingFn =
(renderer: any, element: RElement, prop: string, value: string | null,
bindingIndex?: number | null) => { fn(prop, value, bindingIndex || null); };
const sanitizer = this._isClassBased ? null : (this._sanitizer ||
getCurrentOrLViewSanitizer(this._data as LView));
const sanitizer = this._isClassBased ? null : (this._sanitizer || getCurrentStyleSanitizer());
applyStyling(this.context, null, mockElement, this._data, true, mapFn, sanitizer);
}
}

View File

@ -5,50 +5,62 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Sanitizer, SecurityContext} from '../../sanitization/security';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {StylingContext} from '../interfaces/styling';
import {LView, SANITIZER} from '../interfaces/view';
import {getProp as getOldProp, getSinglePropIndexValue as getOldSinglePropIndexValue} from '../styling/class_and_style_bindings';
import {TNode, TNodeFlags} from '../interfaces/node';
import {isDifferent} from '../util/misc_utils';
import {LStylingMap, LStylingMapIndex, TStylingConfigFlags, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
import {getCurrentStyleSanitizer, setCurrentStyleSanitizer} from './state';
import {StylingMapArray, StylingMapArrayIndex, TStylingConfigFlags, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
const MAP_BASED_ENTRY_PROP_NAME = '--MAP--';
/**
* Creates a new instance of the `TStylingContext`.
*
* This function will also pre-fill the context with data
* for map-based bindings.
* The `TStylingContext` is used as a manifest of all style or all class bindings on
* an element. Because it is a T-level data-structure, it is only created once per
* tNode for styles and for classes. This function allocates a new instance of a
* `TStylingContext` with the initial values (see `interfaces.ts` for more info).
*/
export function allocTStylingContext(): TStylingContext {
export function allocTStylingContext(initialStyling?: StylingMapArray | null): TStylingContext {
// because map-based bindings deal with a dynamic set of values, there
// is no way to know ahead of time whether or not sanitization is required.
// For this reason the configuration will always mark sanitization as active
// (this means that when map-based values are applied then sanitization will
// be checked against each property).
const mapBasedConfig = TStylingContextPropConfigFlags.SanitizationRequired;
return [TStylingConfigFlags.Initial, 0, mapBasedConfig, 0, MAP_BASED_ENTRY_PROP_NAME];
const context: TStylingContext = [
initialStyling || null,
TStylingConfigFlags.Initial,
// the LastDirectiveIndex value in the context is used to track which directive is the last
// to call `stylingApply()`. The `-1` value implies that no directive has been set yet.
-1,
mapBasedConfig,
0,
MAP_BASED_ENTRY_PROP_NAME,
];
return context;
}
/**
* Temporary function that allows for a string-based property name to be
* obtained from an index-based property identifier.
* Sets the provided directive as the last directive index in the provided `TStylingContext`.
*
* This function will be removed once the new styling refactor code (which
* lives inside of `render3/styling_next/`) replaces the existing styling
* implementation.
* Styling in Angular can be applied from the template as well as multiple sources of
* host bindings. This means that each binding function (the template function or the
* hostBindings functions) will generate styling instructions as well as a styling
* apply function (i.e. `stylingApply()`). Because host bindings functions and the
* template function are independent from one another this means that the styling apply
* function will be called multiple times. By tracking the last directive index (which
* is what happens in this function) the styling algorithm knows exactly when to flush
* styling (which is when the last styling apply function is executed).
*/
export function getBindingNameFromIndex(
stylingContext: StylingContext, offset: number, directiveIndex: number, isClassBased: boolean) {
const singleIndex =
getOldSinglePropIndexValue(stylingContext, directiveIndex, offset, isClassBased);
return getOldProp(stylingContext, singleIndex);
}
export function updateContextDirectiveIndex(context: TStylingContext, index: number) {
context[TStylingContextIndex.MaxDirectiveIndexPosition] = index;
export function updateLastDirectiveIndex(
context: TStylingContext, lastDirectiveIndex: number): void {
const currentValue = context[TStylingContextIndex.LastDirectiveIndexPosition];
if (lastDirectiveIndex !== currentValue) {
context[TStylingContextIndex.LastDirectiveIndexPosition] = lastDirectiveIndex;
if (currentValue === 0 && lastDirectiveIndex > 0) {
markContextToPersistState(context);
}
}
}
function getConfig(context: TStylingContext) {
@ -101,8 +113,9 @@ export function getDefaultValue(context: TStylingContext, index: number): string
* Temporary function which determines whether or not a context is
* allowed to be flushed based on the provided directive index.
*/
export function allowStylingFlush(context: TStylingContext, index: number) {
return index === context[TStylingContextIndex.MaxDirectiveIndexPosition];
export function allowStylingFlush(context: TStylingContext | null, index: number) {
return (context && index === context[TStylingContextIndex.LastDirectiveIndexPosition]) ? true :
false;
}
export function lockContext(context: TStylingContext) {
@ -113,6 +126,14 @@ export function isContextLocked(context: TStylingContext): boolean {
return (getConfig(context) & TStylingConfigFlags.Locked) > 0;
}
export function stateIsPersisted(context: TStylingContext): boolean {
return (getConfig(context) & TStylingConfigFlags.PersistStateValues) > 0;
}
export function markContextToPersistState(context: TStylingContext) {
setConfig(context, getConfig(context) | TStylingConfigFlags.PersistStateValues);
}
export function getPropValuesStartPosition(context: TStylingContext) {
return TStylingContextIndex.MapBindingsBindingsStartPosition +
context[TStylingContextIndex.MapBindingsValuesCountPosition];
@ -123,11 +144,20 @@ export function isMapBased(prop: string) {
}
export function hasValueChanged(
a: LStylingMap | number | String | string | null | boolean | undefined | {},
b: LStylingMap | number | String | string | null | boolean | undefined | {}): boolean {
const compareValueA = Array.isArray(a) ? a[LStylingMapIndex.RawValuePosition] : a;
const compareValueB = Array.isArray(b) ? b[LStylingMapIndex.RawValuePosition] : b;
return compareValueA !== compareValueB;
a: StylingMapArray | number | String | string | null | boolean | undefined | {},
b: StylingMapArray | number | String | string | null | boolean | undefined | {}): boolean {
let compareValueA = Array.isArray(a) ? a[StylingMapArrayIndex.RawValuePosition] : a;
let compareValueB = Array.isArray(b) ? b[StylingMapArrayIndex.RawValuePosition] : b;
// these are special cases for String based values (which are created as artifacts
// when sanitization is bypassed on a particular value)
if (compareValueA instanceof String) {
compareValueA = compareValueA.toString();
}
if (compareValueB instanceof String) {
compareValueB = compareValueB.toString();
}
return isDifferent(compareValueA, compareValueB);
}
/**
@ -142,35 +172,59 @@ export function isStylingValueDefined(value: any) {
return value != null && value !== '';
}
/**
* Returns the current style sanitizer function for the given view.
*
* The default style sanitizer (which lives inside of `LView`) will
* be returned depending on whether the `styleSanitizer` instruction
* was called or not prior to any styling instructions running.
*/
export function getCurrentOrLViewSanitizer(lView: LView): StyleSanitizeFn|null {
const sanitizer: StyleSanitizeFn|null = (getCurrentStyleSanitizer() || lView[SANITIZER]) as any;
if (sanitizer && typeof sanitizer !== 'function') {
setCurrentStyleSanitizer(sanitizer);
return sanitizeUsingSanitizerObject;
}
return sanitizer;
export function concatString(a: string, b: string, separator = ' '): string {
return a + ((b.length && a.length) ? separator : '') + b;
}
export function hyphenate(value: string): string {
return value.replace(/[a-z][A-Z]/g, v => v.charAt(0) + '-' + v.charAt(1)).toLowerCase();
}
/**
* Style sanitization function that internally uses a `Sanitizer` instance to handle style
* sanitization.
* Returns an instance of `StylingMapArray`.
*
* This function is designed to find an instance of `StylingMapArray` in case it is stored
* inside of an instance of `TStylingContext`. When a styling context is created it
* will copy over an initial styling values from the tNode (which are stored as a
* `StylingMapArray` on the `tNode.classes` or `tNode.styles` values).
*/
const sanitizeUsingSanitizerObject: StyleSanitizeFn =
(prop: string, value: string | null, mode?: StyleSanitizeMode) => {
const sanitizer = getCurrentStyleSanitizer() as Sanitizer;
if (sanitizer) {
if (mode !== undefined && mode & StyleSanitizeMode.SanitizeOnly) {
return sanitizer.sanitize(SecurityContext.STYLE, value);
} else {
return true;
}
}
return value;
};
export function getStylingMapArray(value: TStylingContext | StylingMapArray | null):
StylingMapArray|null {
return isStylingContext(value) ?
(value as TStylingContext)[TStylingContextIndex.InitialStylingValuePosition] :
value;
}
export function isStylingContext(value: TStylingContext | StylingMapArray | null): boolean {
// the StylingMapArray is in the format of [initial, prop, string, prop, string]
// and this is the defining value to distinguish between arrays
return Array.isArray(value) &&
value.length >= TStylingContextIndex.MapBindingsBindingsStartPosition &&
typeof value[1] !== 'string';
}
export function getInitialStylingValue(context: TStylingContext | StylingMapArray | null): string {
const map = getStylingMapArray(context);
return map && (map[StylingMapArrayIndex.RawValuePosition] as string | null) || '';
}
export function hasClassInput(tNode: TNode) {
return (tNode.flags & TNodeFlags.hasClassInput) !== 0;
}
export function hasStyleInput(tNode: TNode) {
return (tNode.flags & TNodeFlags.hasStyleInput) !== 0;
}
export function getMapProp(map: StylingMapArray, index: number): string {
return map[index + StylingMapArrayIndex.PropOffset] as string;
}
export function setMapValue(
map: StylingMapArray, index: number, value: string | boolean | null): void {
map[index + StylingMapArrayIndex.ValueOffset] = value;
}
export function getMapValue(map: StylingMapArray, index: number): string|null {
return map[index + StylingMapArrayIndex.ValueOffset] as string | null;
}

View File

@ -40,9 +40,9 @@ import {FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, PARENT, PREORDER_HOOK_FLA
/**
* Returns `RNode`.
* @param value wrapped value of `RNode`, `LView`, `LContainer`, `StylingContext`
* @param value wrapped value of `RNode`, `LView`, `LContainer`
*/
export function unwrapRNode(value: RNode | LView | LContainer | StylingContext): RNode {
export function unwrapRNode(value: RNode | LView | LContainer): RNode {
while (Array.isArray(value)) {
value = value[HOST] as any;
}
@ -51,9 +51,9 @@ export function unwrapRNode(value: RNode | LView | LContainer | StylingContext):
/**
* Returns `LView` or `null` if not found.
* @param value wrapped value of `RNode`, `LView`, `LContainer`, `StylingContext`
* @param value wrapped value of `RNode`, `LView`, `LContainer`
*/
export function unwrapLView(value: RNode | LView | LContainer | StylingContext): LView|null {
export function unwrapLView(value: RNode | LView | LContainer): LView|null {
while (Array.isArray(value)) {
// This check is same as `isLView()` but we don't call at as we don't want to call
// `Array.isArray()` twice and give JITer more work for inlining.
@ -65,10 +65,9 @@ export function unwrapLView(value: RNode | LView | LContainer | StylingContext):
/**
* Returns `LContainer` or `null` if not found.
* @param value wrapped value of `RNode`, `LView`, `LContainer`, `StylingContext`
* @param value wrapped value of `RNode`, `LView`, `LContainer`
*/
export function unwrapLContainer(value: RNode | LView | LContainer | StylingContext): LContainer|
null {
export function unwrapLContainer(value: RNode | LView | LContainer): LContainer|null {
while (Array.isArray(value)) {
// This check is same as `isLContainer()` but we don't call at as we don't want to call
// `Array.isArray()` twice and give JITer more work for inlining.

View File

@ -38,10 +38,15 @@ declare global {
styleMapCacheMiss: number;
classMap: number;
classMapCacheMiss: number;
stylingProp: number;
stylingPropCacheMiss: number;
stylingApply: number;
stylingApplyCacheMiss: number;
styleProp: number;
stylePropCacheMiss: number;
classProp: number;
classPropCacheMiss: number;
flushStyling: number;
classesApplied: number;
stylesApplied: number;
stylingWritePersistedState: number;
stylingReadPersistedState: number;
}
}
@ -75,10 +80,15 @@ export function ngDevModeResetPerfCounters(): NgDevModePerfCounters {
styleMapCacheMiss: 0,
classMap: 0,
classMapCacheMiss: 0,
stylingProp: 0,
stylingPropCacheMiss: 0,
stylingApply: 0,
stylingApplyCacheMiss: 0,
styleProp: 0,
stylePropCacheMiss: 0,
classProp: 0,
classPropCacheMiss: 0,
flushStyling: 0,
classesApplied: 0,
stylesApplied: 0,
stylingWritePersistedState: 0,
stylingReadPersistedState: 0,
};
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.

View File

@ -1634,7 +1634,8 @@ describe('di', () => {
expect(directive.otherAttr).toBe('value');
expect(directive.className).toBe('hello there');
expect(directive.inlineStyles).toBe('margin: 1px; color: red;');
expect(directive.inlineStyles).toMatch(/color:\s*red/);
expect(directive.inlineStyles).toMatch(/margin:\s*1px/);
});
it('should not inject attributes with namespace', () => {

View File

@ -1087,7 +1087,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`<div test="" title="début 2 milieu 1 fin" class="foo"> traduction: un <i>email</i><!--ICU 19--></div><div test="" class="foo"></div>`);
`<div test="" title="début 2 milieu 1 fin" class="foo"> traduction: un <i>email</i><!--ICU 20--></div><div test="" class="foo"></div>`);
directiveInstances.forEach(instance => instance.klass = 'bar');
fixture.componentRef.instance.exp1 = 2;
@ -1095,7 +1095,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`<div test="" title="début 3 milieu 2 fin" class="bar"> traduction: 2 emails<!--ICU 19--></div><div test="" class="bar"></div>`);
`<div test="" title="début 3 milieu 2 fin" class="bar"> traduction: 2 emails<!--ICU 20--></div><div test="" class="bar"></div>`);
});
it('should handle i18n attribute with directive inputs', () => {

View File

@ -1043,10 +1043,10 @@ describe('acceptance integration tests', () => {
@Directive({selector: '[DirWithStyle]'})
class DirWithStyleDirective {
public stylesVal: string = '';
public stylesVal: any = '';
@Input()
set style(value: string) { this.stylesVal = value; }
set style(value: any) { this.stylesVal = value; }
}
it('should delegate initial classes to a [class] input binding if present on a directive on the same element',
@ -1061,8 +1061,10 @@ describe('acceptance integration tests', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.mockClassDirective.classesVal)
.toEqual('apple orange banana');
// the initial values always get sorted in non VE code
// but there is no sorting guarantee within VE code
expect(fixture.componentInstance.mockClassDirective.classesVal.split(/\s+/).sort())
.toEqual(['apple', 'banana', 'orange']);
});
it('should delegate initial styles to a [style] input binding if present on a directive on the same element',
@ -1118,7 +1120,7 @@ describe('acceptance integration tests', () => {
fixture.detectChanges();
expect(fixture.componentInstance.mockStyleDirective.stylesVal)
.toEqual('width:200px;height:500px');
.toEqual({'width': '200px', 'height': '500px'});
});
onlyInIvy('Style binding merging works differently in Ivy')

View File

@ -5,13 +5,10 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CompilerStylingMode, compilerSetStylingMode} from '@angular/compiler/src/render3/view/styling_state';
import {Component, Directive, HostBinding, Input, ViewChild} from '@angular/core';
import {SecurityContext} from '@angular/core/src/core';
import {getLContext} from '@angular/core/src/render3/context_discovery';
import {DebugNode, LViewDebug, toDebug} from '@angular/core/src/render3/instructions/lview_debug';
import {SANITIZER} from '@angular/core/src/render3/interfaces/view';
import {RuntimeStylingMode, runtimeSetStylingMode, setCurrentStyleSanitizer} from '@angular/core/src/render3/styling_next/state';
import {getCheckNoChangesMode} from '@angular/core/src/render3/state';
import {loadLContextFromNode} from '@angular/core/src/render3/util/discovery_utils';
import {ngDevModeResetPerfCounters as resetStylingCounters} from '@angular/core/src/util/ng_dev_mode';
import {TestBed} from '@angular/core/testing';
@ -20,16 +17,6 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
describe('new styling integration', () => {
beforeEach(() => {
runtimeSetStylingMode(RuntimeStylingMode.UseNew);
compilerSetStylingMode(CompilerStylingMode.UseNew);
});
afterEach(() => {
runtimeSetStylingMode(RuntimeStylingMode.UseOld);
compilerSetStylingMode(CompilerStylingMode.UseOld);
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should apply single property styles/classes to the element and default to any static styling values',
() => {
@ -579,6 +566,55 @@ describe('new styling integration', () => {
assertStyle(element, 'opacity', '');
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should only persist state values in a local map if template AND host styling is used together',
() => {
@Directive({selector: '[dir-that-sets-styling]'})
class Dir {
@HostBinding('style.width') w = '100px';
}
@Component({
template: `
<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({
@ -686,38 +722,6 @@ describe('new styling integration', () => {
});
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should pick up and use the sanitizer present in the lView', () => {
@Component({
template: `
<div [style.width]="w"></div>
`
})
class Cmp {
w = '100px';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
const lView = getLContext(element) !.lView;
lView[SANITIZER] = new MockSanitizer(value => { return `${value}-safe`; });
comp.w = '200px';
fixture.detectChanges();
const node = getDebugNode(element) !;
const styles = node.styles !;
expect(styles.values['width']).toEqual('200px-safe');
// this is here so that it won't get picked up accidentally in another test
lView[SANITIZER] = null;
setCurrentStyleSanitizer(null);
});
it('should be able to bind a SafeValue to clip-path', () => {
@Component({template: '<div [style.clip-path]="path"></div>'})
class Cmp {
@ -737,6 +741,321 @@ describe('new styling integration', () => {
// that we use to run tests doesn't support `clip-path` in `CSSStyleDeclaration`.
expect(html).toMatch(/style=["|']clip-path:\s*url\(.*#test.*\)/);
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should evaluate follow-up [style] maps even if a former map is null', () => {
@Directive({selector: '[dir-with-styling]'})
class DirWithStyleMap {
@HostBinding('style') public styleMap: any = {color: 'red'};
}
@Directive({selector: '[dir-with-styling-part2]'})
class DirWithStyleMapPart2 {
@HostBinding('style') public styleMap: any = {width: '200px'};
}
@Component({
template: `
<div #div
[style]="map"
dir-with-styling
dir-with-styling-part2></div>
`
})
class Cmp {
map: any = null;
@ViewChild('div', {read: DirWithStyleMap, static: true})
dir1 !: DirWithStyleMap;
@ViewChild('div', {read: DirWithStyleMapPart2, static: true})
dir2 !: DirWithStyleMapPart2;
}
TestBed.configureTestingModule(
{declarations: [Cmp, DirWithStyleMap, DirWithStyleMapPart2]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
const node = getDebugNode(element) !;
const styles = node.styles !;
const values = styles.values;
const props = Object.keys(values).sort();
expect(props).toEqual(['color', 'width']);
expect(values['width']).toEqual('200px');
expect(values['color']).toEqual('red');
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should evaluate initial style/class values on a list of elements that changes', () => {
@Component({
template: `
<div *ngFor="let item of items"
class="initial-class item-{{ item }}">
{{ item }}
</div>
`
})
class Cmp {
items = [1, 2, 3];
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
function getItemElements(): HTMLElement[] {
return [].slice.call(fixture.nativeElement.querySelectorAll('div'));
}
function getItemClasses(): string[] {
return getItemElements().map(e => e.className).sort().join(' ').split(' ');
}
expect(getItemElements().length).toEqual(3);
expect(getItemClasses()).toEqual([
'initial-class',
'item-1',
'initial-class',
'item-2',
'initial-class',
'item-3',
]);
comp.items = [2, 4, 6, 8];
fixture.detectChanges();
expect(getItemElements().length).toEqual(4);
expect(getItemClasses()).toEqual([
'initial-class',
'item-2',
'initial-class',
'item-4',
'initial-class',
'item-6',
'initial-class',
'item-8',
]);
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should create and update multiple class bindings across multiple elements in a template',
() => {
@Component({
template: `
<header class="header">header</header>
<div *ngFor="let item of items" class="item item-{{ item }}">
{{ item }}
</div>
<footer class="footer">footer</footer>
`
})
class Cmp {
items = [1, 2, 3];
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
function getItemElements(): HTMLElement[] {
return [].slice.call(fixture.nativeElement.querySelectorAll('div'));
}
function getItemClasses(): string[] {
return getItemElements().map(e => e.className).sort().join(' ').split(' ');
}
const header = fixture.nativeElement.querySelector('header');
expect(header.classList.contains('header'));
const footer = fixture.nativeElement.querySelector('footer');
expect(footer.classList.contains('footer'));
expect(getItemElements().length).toEqual(3);
expect(getItemClasses()).toEqual([
'item',
'item-1',
'item',
'item-2',
'item',
'item-3',
]);
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should understand multiple directives which contain initial classes', () => {
@Directive({selector: 'dir-one'})
class DirOne {
@HostBinding('class') public className = 'dir-one';
}
@Directive({selector: 'dir-two'})
class DirTwo {
@HostBinding('class') public className = 'dir-two';
}
@Component({
template: `
<dir-one></dir-one>
<div class="initial"></div>
<dir-two></dir-two>
`
})
class Cmp {
}
TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const dirOne = fixture.nativeElement.querySelector('dir-one');
const div = fixture.nativeElement.querySelector('div');
const dirTwo = fixture.nativeElement.querySelector('dir-two');
expect(dirOne.classList.contains('dir-one')).toBeTruthy();
expect(dirTwo.classList.contains('dir-two')).toBeTruthy();
expect(div.classList.contains('initial')).toBeTruthy();
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should evaluate styling across the template directives when there are multiple elements/sources of styling',
() => {
@Directive({selector: '[one]'})
class DirOne {
@HostBinding('class') public className = 'dir-one';
}
@Directive({selector: '[two]'})
class DirTwo {
@HostBinding('class') public className = 'dir-two';
}
@Component({
template: `
<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;
h = 200;
c = 'red';
}
TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const divA = fixture.nativeElement.querySelector('.a');
const divB = fixture.nativeElement.querySelector('.b');
const divC = fixture.nativeElement.querySelector('.c');
expect(divA.style.width).toEqual('100px');
expect(divB.style.height).toEqual('200px');
expect(divC.style.color).toEqual('red');
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should evaluate styling across the template and directives within embedded views',
() => {
@Directive({selector: '[some-dir-with-styling]'})
class SomeDirWithStyling {
@HostBinding('style')
public styles = {
width: '200px',
height: '500px',
};
}
@Component({
template: `
<div
class="item"
*ngFor="let item of items; let i = index"
[style.color]="c"
[style.height.px]="h * i"
some-dir-with-styling>
{{ item }}
</div>
<section [style.width.px]="w"></section>
<p [style.height.px]="h"></p>
`
})
class Cmp {
items: any[] = [];
c = 'red';
w = 100;
h = 100;
}
TestBed.configureTestingModule({declarations: [Cmp, SomeDirWithStyling]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
comp.items = [1, 2, 3, 4];
fixture.detectChanges();
const items = fixture.nativeElement.querySelectorAll('.item');
expect(items.length).toEqual(4);
const [a, b, c, d] = items;
expect(a.style.height).toEqual('0px');
expect(b.style.height).toEqual('100px');
expect(c.style.height).toEqual('200px');
expect(d.style.height).toEqual('300px');
const section = fixture.nativeElement.querySelector('section');
const p = fixture.nativeElement.querySelector('p');
expect(section.style['width']).toEqual('100px');
expect(p.style['height']).toEqual('100px');
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should flush bindings even if any styling hasn\'t changed in a previous directive',
() => {
@Directive({selector: '[one]'})
class DirOne {
@HostBinding('style.width') w = '100px';
@HostBinding('style.opacity') o = '0.5';
}
@Directive({selector: '[two]'})
class DirTwo {
@HostBinding('style.height') h = '200px';
@HostBinding('style.color') c = 'red';
}
@Component({template: '<div #target one two></div>'})
class Cmp {
@ViewChild('target', {read: DirOne, static: true}) one !: DirOne;
@ViewChild('target', {read: DirTwo, static: true}) two !: DirTwo;
}
TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.style.opacity).toEqual('0.5');
expect(div.style.color).toEqual('red');
expect(div.style.width).toEqual('100px');
expect(div.style.height).toEqual('200px');
comp.two.h = '300px';
fixture.detectChanges();
expect(div.style.opacity).toEqual('0.5');
expect(div.style.color).toEqual('red');
expect(div.style.width).toEqual('100px');
expect(div.style.height).toEqual('300px');
});
});
function assertStyleCounters(countForSet: number, countForRemove: number) {
@ -760,8 +1079,3 @@ function getDebugNode(element: Node): DebugNode|null {
}
return null;
}
class MockSanitizer {
constructor(private _interceptorFn: ((value: any) => any)) {}
sanitize(context: SecurityContext, value: any): string|null { return this._interceptorFn(value); }
}

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 {Component, Directive, ElementRef} from '@angular/core';
import {Component, Directive, ElementRef, Input} from '@angular/core';
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
import {TestBed} from '@angular/core/testing';
import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
describe('styling', () => {
beforeEach(ngDevModeResetPerfCounters);
@ -153,10 +153,8 @@ describe('styling', () => {
expect(div.style.backgroundImage).toBe('url("#test")');
onlyInIvy('perf counters').expectPerfCounters({
stylingApply: 2,
stylingApplyCacheMiss: 1,
stylingProp: 2,
stylingPropCacheMiss: 1,
styleProp: 2,
stylePropCacheMiss: 1,
tNode: 3,
});
});
@ -373,4 +371,187 @@ describe('styling', () => {
expect(div.style.width).toBe('2667px');
});
it('should not write to a `class` input binding in the event that there is no static class value',
() => {
let capturedClassBindingCount = 0;
let capturedClassBindingValue: string|null|undefined = undefined;
let capturedMyClassBindingCount = 0;
let capturedMyClassBindingValue: string|null|undefined = undefined;
@Component({template: '<div [class]="c" [my-class-dir]="x"></div>'})
class Cmp {
c: any = null;
x = 'foo';
}
@Directive({selector: '[my-class-dir]'})
class MyClassDir {
@Input('class')
set classVal(v: string) {
capturedClassBindingCount++;
capturedClassBindingValue = v;
}
@Input('my-class-dir')
set myClassVal(v: string) {
capturedMyClassBindingCount++;
capturedMyClassBindingValue = v;
}
}
TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
expect(capturedClassBindingCount).toEqual(1);
expect(capturedClassBindingValue as any).toEqual(null);
expect(capturedMyClassBindingCount).toEqual(1);
expect(capturedMyClassBindingValue !).toEqual('foo');
fixture.componentInstance.c = 'dynamic-value';
fixture.detectChanges();
expect(capturedClassBindingCount).toEqual(2);
expect(capturedClassBindingValue !).toEqual('dynamic-value');
expect(capturedMyClassBindingCount).toEqual(1);
expect(capturedMyClassBindingValue !).toEqual('foo');
fixture.componentInstance.c = null;
fixture.detectChanges();
expect(capturedClassBindingCount).toEqual(3);
expect(capturedClassBindingValue as any).toEqual(null);
expect(capturedMyClassBindingCount).toEqual(1);
expect(capturedMyClassBindingValue !).toEqual('foo');
fixture.componentInstance.c = '';
fixture.detectChanges();
expect(capturedClassBindingCount).toEqual(4);
expect(capturedClassBindingValue as any).toEqual('');
expect(capturedMyClassBindingCount).toEqual(1);
expect(capturedMyClassBindingValue !).toEqual('foo');
});
it('should write to [class] binding during `update` mode if there is an instantiation-level value',
() => {
let capturedClassBindingCount = 0;
let capturedClassBindingValue: string|null|undefined = undefined;
@Component({template: '<div [class]="c" my-class-dir></div>'})
class Cmp {
c: any = 'bar';
}
@Directive({selector: '[my-class-dir]'})
class MyClassDir {
@Input('class')
set classVal(v: string) {
capturedClassBindingCount++;
capturedClassBindingValue = v;
}
}
// Ivy does an extra `[class]` write with a falsy value since the value
// is applied during creation mode. This is a deviation from VE and should
// be (Jira Issue = FW-1467).
let totalWrites = ivyEnabled ? 1 : 0;
TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]});
const fixture = TestBed.createComponent(Cmp);
expect(capturedClassBindingCount).toEqual(totalWrites++);
fixture.detectChanges();
expect(capturedClassBindingCount).toEqual(totalWrites++);
expect(capturedClassBindingValue as any).toEqual('bar');
fixture.componentInstance.c = 'dynamic-bar';
fixture.detectChanges();
expect(capturedClassBindingCount).toEqual(totalWrites++);
expect(capturedClassBindingValue !).toEqual('dynamic-bar');
});
it('should write to a `class` input binding if there is a static class value', () => {
let capturedClassBindingCount = 0;
let capturedClassBindingValue: string|null = null;
let capturedMyClassBindingCount = 0;
let capturedMyClassBindingValue: string|null = null;
@Component({template: '<div class="static-val" [my-class-dir]="x"></div>'})
class Cmp {
x = 'foo';
}
@Directive({selector: '[my-class-dir]'})
class MyClassDir {
@Input('class')
set classVal(v: string) {
capturedClassBindingCount++;
capturedClassBindingValue = v;
}
@Input('my-class-dir')
set myClassVal(v: string) {
capturedMyClassBindingCount++;
capturedMyClassBindingValue = v;
}
}
TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
expect(capturedClassBindingValue !).toEqual('static-val');
expect(capturedClassBindingCount).toEqual(1);
expect(capturedMyClassBindingValue !).toEqual('foo');
expect(capturedMyClassBindingCount).toEqual(1);
});
onlyInIvy('only ivy persists static class/style attrs with their binding counterparts')
.it('should write to a `class` input binding if there is a static class value and there is a binding value',
() => {
let capturedClassBindingCount = 0;
let capturedClassBindingValue: string|null = null;
let capturedMyClassBindingCount = 0;
let capturedMyClassBindingValue: string|null = null;
@Component({template: '<div class="static-val" [class]="c" [my-class-dir]="x"></div>'})
class Cmp {
c: any = null;
x: any = 'foo';
}
@Directive({selector: '[my-class-dir]'})
class MyClassDir {
@Input('class')
set classVal(v: string) {
capturedClassBindingCount++;
capturedClassBindingValue = v;
}
@Input('my-class-dir')
set myClassVal(v: string) {
capturedMyClassBindingCount++;
capturedMyClassBindingValue = v;
}
}
TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
expect(capturedClassBindingCount).toEqual(1);
expect(capturedClassBindingValue !).toEqual('static-val');
expect(capturedMyClassBindingCount).toEqual(1);
expect(capturedMyClassBindingValue !).toEqual('foo');
fixture.componentInstance.c = 'dynamic-val';
fixture.detectChanges();
expect(capturedClassBindingCount).toEqual(2);
expect(capturedClassBindingValue !).toEqual('static-val dynamic-val');
expect(capturedMyClassBindingCount).toEqual(1);
expect(capturedMyClassBindingValue !).toEqual('foo');
});
});

View File

@ -66,10 +66,6 @@ class BoxWithOverriddenStylesComponent {
@Component({
selector: 'animation-world',
template: `
<nav>
<button (click)="animateWithCustomPlayer()">Animate List (custom player)</button>
<button (click)="animateWithStyles()">Populate List (style bindings)</button>
</nav>
<div class="list">
<div
#makeColorGrey="makeColorGrey"
@ -115,21 +111,6 @@ class AnimationWorldComponent {
makeColorGrey.toggle();
markDirty(this);
}
animateWithStyles() {
this.styles = animateStyleFactory([{opacity: 0}, {opacity: 1}], 300, 'ease-out');
markDirty(this);
}
animateWithCustomPlayer() {
const elements = this._hostElement.querySelectorAll('div.record') as any as HTMLElement[];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const delay = i * 100;
const player = buildAnimationPlayer(element, 'fadeInOut', `500ms ease-out ${delay}ms both`);
addPlayer(element, player);
}
}
}
@NgModule({
@ -139,156 +120,4 @@ class AnimationWorldComponent {
class AnimationWorldModule {
}
function buildAnimationPlayer(element: HTMLElement, animationName: string, time: string): Player {
return new SimpleKeyframePlayer(element, animationName, time);
}
class SimpleKeyframePlayer implements Player {
state = PlayState.Pending;
parent: Player|null = null;
private _animationStyle: string = '';
private _listeners: {[stateName: string]: (() => any)[]} = {};
constructor(private _element: HTMLElement, private _animationName: string, time: string) {
this._animationStyle = `${time} ${_animationName}`;
}
private _start() {
(this._element as any).style.animation = this._animationStyle;
const animationFn = (event: AnimationEvent) => {
if (event.animationName == this._animationName) {
this._element.removeEventListener('animationend', animationFn);
this.finish();
}
};
this._element.addEventListener('animationend', animationFn);
}
addEventListener(state: PlayState|string, cb: () => any): void {
const key = state.toString();
const arr = this._listeners[key] = (this._listeners[key] || []);
arr.push(cb);
}
play(): void {
if (this.state <= PlayState.Pending) {
this._start();
}
if (this.state != PlayState.Running) {
setAnimationPlayState(this._element, 'running');
this.state = PlayState.Running;
this._emit(this.state);
}
}
pause(): void {
if (this.state != PlayState.Paused) {
setAnimationPlayState(this._element, 'paused');
this.state = PlayState.Paused;
this._emit(this.state);
}
}
finish(): void {
if (this.state < PlayState.Finished) {
this._element.style.animation = '';
this.state = PlayState.Finished;
this._emit(this.state);
}
}
destroy(): void {
if (this.state < PlayState.Destroyed) {
this.finish();
this.state = PlayState.Destroyed;
this._emit(this.state);
}
}
capture(): any {}
private _emit(state: PlayState) {
const arr = this._listeners[state.toString()] || [];
arr.forEach(cb => cb());
}
}
function setAnimationPlayState(element: HTMLElement, state: string) {
element.style.animationPlayState = state;
}
class AnimationDebugger implements PlayerHandler {
private _players: Player[] = [];
flushPlayers() {
this._players.forEach(player => {
if (!player.parent) {
player.play();
}
});
this._players.length = 0;
}
queuePlayer(player: Player): void { this._players.push(player); }
}
const playerHandler = new AnimationDebugger();
renderComponent(AnimationWorldComponent, {playerHandler});
function animateStyleFactory(keyframes: any[], duration: number, easing: string) {
const limit = keyframes.length - 1;
const finalKeyframe = keyframes[limit];
return bindPlayerFactory(
(element: HTMLElement, type: number, values: {[key: string]: any},
isFirstRender: boolean) => {
const kf = keyframes.slice(0, limit);
kf.push(values);
return new WebAnimationsPlayer(element, keyframes, duration, easing);
},
finalKeyframe);
}
class WebAnimationsPlayer implements Player {
state = PlayState.Pending;
parent: Player|null = null;
private _listeners: {[stateName: string]: (() => any)[]} = {};
constructor(
private _element: HTMLElement, private _keyframes: {[key: string]: any}[],
private _duration: number, private _easing: string) {}
private _start() {
const player = this._element.animate(
this._keyframes as any[], {duration: this._duration, easing: this._easing, fill: 'both'});
player.addEventListener('finish', e => { this.finish(); });
}
addEventListener(state: PlayState|string, cb: () => any): void {
const key = state.toString();
const arr = this._listeners[key] = (this._listeners[key] || []);
arr.push(cb);
}
play(): void {
if (this.state <= PlayState.Pending) {
this._start();
}
if (this.state != PlayState.Running) {
this.state = PlayState.Running;
this._emit(this.state);
}
}
pause(): void {
if (this.state != PlayState.Paused) {
this.state = PlayState.Paused;
this._emit(this.state);
}
}
finish(): void {
if (this.state < PlayState.Finished) {
this._element.style.animation = '';
this.state = PlayState.Finished;
this._emit(this.state);
}
}
destroy(): void {
if (this.state < PlayState.Destroyed) {
this.finish();
this.state = PlayState.Destroyed;
this._emit(this.state);
}
}
capture(): any {}
private _emit(state: PlayState) {
const arr = this._listeners[state.toString()] || [];
arr.forEach(cb => cb());
}
}
renderComponent(AnimationWorldComponent);

View File

@ -32,18 +32,6 @@
{
"name": "DECLARATION_VIEW"
},
{
"name": "DEFAULT_BINDING_VALUE"
},
{
"name": "DEFAULT_GUARD_MASK_VALUE"
},
{
"name": "DEFAULT_SIZE_VALUE"
},
{
"name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX"
},
{
"name": "DepComponent"
},
@ -71,9 +59,6 @@
{
"name": "INJECTOR_BLOOM_PARENT_SIZE"
},
{
"name": "MAP_BASED_ENTRY_PROP_NAME"
},
{
"name": "MONKEY_PATCH_KEY_NAME"
},
@ -186,35 +171,17 @@
"name": "_selectedIndex"
},
{
"name": "_stylingMode"
},
{
"name": "addBindingIntoContext"
"name": "_stateStorage"
},
{
"name": "addComponentLogic"
},
{
"name": "addOrUpdateStaticStyle"
"name": "addItemToStylingMap"
},
{
"name": "addToViewTree"
},
{
"name": "allocStylingContext"
},
{
"name": "allocTStylingContext"
},
{
"name": "allocateNewContextEntry"
},
{
"name": "allocateOrUpdateDirectiveIntoContext"
},
{
"name": "allowValueChange"
},
{
"name": "appendChild"
},
@ -224,9 +191,6 @@
{
"name": "attachPatchData"
},
{
"name": "attrsStylingIndexOf"
},
{
"name": "baseResolveDirective"
},
@ -252,10 +216,10 @@
"name": "componentRefresh"
},
{
"name": "createDirectivesAndLocals"
"name": "concatString"
},
{
"name": "createEmptyStylingContext"
"name": "createDirectivesAndLocals"
},
{
"name": "createLView"
@ -347,9 +311,6 @@
{
"name": "getCheckNoChangesMode"
},
{
"name": "getClassesContext"
},
{
"name": "getClosureSafeProperty"
},
@ -362,18 +323,12 @@
{
"name": "getContainerRenderParent"
},
{
"name": "getContext"
},
{
"name": "getDirectiveDef"
},
{
"name": "getElementDepthCount"
},
{
"name": "getGuardMask"
},
{
"name": "getHighestElementOrICUContainer"
},
@ -381,10 +336,7 @@
"name": "getHostNative"
},
{
"name": "getInitialClassNameValue"
},
{
"name": "getInitialStyleStringValue"
"name": "getInitialStylingValue"
},
{
"name": "getInjectorIndex"
@ -401,6 +353,12 @@
{
"name": "getLViewParent"
},
{
"name": "getMapProp"
},
{
"name": "getMapValue"
},
{
"name": "getNameOnlyMarkerIndex"
},
@ -446,15 +404,6 @@
{
"name": "getPreviousOrParentTNode"
},
{
"name": "getProp"
},
{
"name": "getPropConfig"
},
{
"name": "getPropValuesStartPosition"
},
{
"name": "getRenderFlags"
},
@ -471,16 +420,7 @@
"name": "getSelectedIndex"
},
{
"name": "getStylesContext"
},
{
"name": "getStylingContextFromLView"
},
{
"name": "getTNode"
},
{
"name": "getValuesCount"
"name": "getStylingMapArray"
},
{
"name": "hasClassInput"
@ -506,9 +446,6 @@
{
"name": "initNodeFlags"
},
{
"name": "initializeStaticContext"
},
{
"name": "initializeTNodeInputs"
},
@ -575,6 +512,9 @@
{
"name": "isStylingContext"
},
{
"name": "isStylingValueDefined"
},
{
"name": "leaveView"
},
@ -605,12 +545,6 @@
{
"name": "noSideEffects"
},
{
"name": "patchContextWithStaticAttrs"
},
{
"name": "patchInitialStylingValue"
},
{
"name": "postProcessBaseDirective"
},
@ -620,9 +554,6 @@
{
"name": "queueComponentIndexForCheck"
},
{
"name": "readClassValueFromTNode"
},
{
"name": "readPatchedData"
},
@ -642,10 +573,7 @@
"name": "refreshDynamicEmbeddedViews"
},
{
"name": "registerBinding"
},
{
"name": "registerInitialStylingIntoContext"
"name": "registerInitialStylingOnTNode"
},
{
"name": "registerPostOrderHooks"
@ -663,14 +591,17 @@
"name": "renderEmbeddedTemplate"
},
{
"name": "renderInitialClasses"
},
{
"name": "renderInitialStyles"
"name": "renderInitialStyling"
},
{
"name": "renderStringify"
},
{
"name": "renderStylingMap"
},
{
"name": "resetAllStylingState"
},
{
"name": "resetComponentState"
},
@ -678,10 +609,10 @@
"name": "resetPreOrderHookFlags"
},
{
"name": "resolveDirectives"
"name": "resetStylingState"
},
{
"name": "runtimeIsNewStylingInUse"
"name": "resolveDirectives"
},
{
"name": "saveNameToExportMap"
@ -698,9 +629,6 @@
{
"name": "setBindingRoot"
},
{
"name": "setCachedStylingContext"
},
{
"name": "setClass"
},
@ -711,7 +639,10 @@
"name": "setCurrentQueryIndex"
},
{
"name": "setGuardMask"
"name": "setCurrentStyleSanitizer"
},
{
"name": "setDirectiveStylingInput"
},
{
"name": "setHostBindings"
@ -732,7 +663,7 @@
"name": "setIsNotParent"
},
{
"name": "setNodeStylingTemplate"
"name": "setMapValue"
},
{
"name": "setPreviousOrParentTNode"
@ -752,6 +683,9 @@
{
"name": "stringifyForError"
},
{
"name": "stylingMapToString"
},
{
"name": "syncViewWithBlueprint"
},

View File

@ -149,6 +149,9 @@
{
"name": "_selectedIndex"
},
{
"name": "_stateStorage"
},
{
"name": "addToViewTree"
},
@ -284,6 +287,12 @@
{
"name": "getLViewParent"
},
{
"name": "getMapProp"
},
{
"name": "getMapValue"
},
{
"name": "getNativeAnchorNode"
},
@ -338,6 +347,9 @@
{
"name": "getSelectedIndex"
},
{
"name": "getStylingMapArray"
},
{
"name": "hasParentInjector"
},
@ -383,6 +395,9 @@
{
"name": "isRootView"
},
{
"name": "isStylingContext"
},
{
"name": "leaveView"
},
@ -444,20 +459,26 @@
"name": "renderEmbeddedTemplate"
},
{
"name": "renderInitialClasses"
},
{
"name": "renderInitialStyles"
"name": "renderInitialStyling"
},
{
"name": "renderStringify"
},
{
"name": "renderStylingMap"
},
{
"name": "resetAllStylingState"
},
{
"name": "resetComponentState"
},
{
"name": "resetPreOrderHookFlags"
},
{
"name": "resetStylingState"
},
{
"name": "selectInternal"
},
@ -467,9 +488,6 @@
{
"name": "setBindingRoot"
},
{
"name": "setCachedStylingContext"
},
{
"name": "setClass"
},
@ -479,6 +497,9 @@
{
"name": "setCurrentQueryIndex"
},
{
"name": "setCurrentStyleSanitizer"
},
{
"name": "setHostBindings"
},

View File

@ -12,10 +12,10 @@
"name": "BINDING_INDEX"
},
{
"name": "BLOOM_MASK"
"name": "BIT_MASK_START_VALUE"
},
{
"name": "BoundPlayerFactory"
"name": "BLOOM_MASK"
},
{
"name": "CHILD_HEAD"
@ -38,12 +38,6 @@
{
"name": "ChangeDetectionStrategy"
},
{
"name": "ClassAndStylePlayerBuilder"
},
{
"name": "CorePlayerHandler"
},
{
"name": "DECLARATION_VIEW"
},
@ -56,9 +50,6 @@
{
"name": "DEFAULT_SIZE_VALUE"
},
{
"name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX"
},
{
"name": "DefaultIterableDiffer"
},
@ -230,9 +221,6 @@
{
"name": "SWITCH_VIEW_CONTAINER_REF_FACTORY"
},
{
"name": "SecurityContext"
},
{
"name": "SkipSelf"
},
@ -374,9 +362,6 @@
{
"name": "_c21"
},
{
"name": "_c22"
},
{
"name": "_c3"
},
@ -417,11 +402,17 @@
"name": "_selectedIndex"
},
{
"name": "_stylingMode"
"name": "_stateStorage"
},
{
"name": "_stylingElement"
},
{
"name": "_stylingProp"
},
{
"name": "_stylingState"
},
{
"name": "_symbolIterator"
},
@ -441,10 +432,7 @@
"name": "addComponentLogic"
},
{
"name": "addOrUpdateStaticStyle"
},
{
"name": "addPlayerInternal"
"name": "addItemToStylingMap"
},
{
"name": "addRemoveViewFromContainer"
@ -455,42 +443,21 @@
{
"name": "addToViewTree"
},
{
"name": "allocPlayerContext"
},
{
"name": "allocStylingContext"
},
{
"name": "allocTStylingContext"
},
{
"name": "allocateNewContextEntry"
},
{
"name": "allocateOrUpdateDirectiveIntoContext"
},
{
"name": "allowFlush"
},
{
"name": "allowStylingFlush"
},
{
"name": "allowValueChange"
},
{
"name": "appendChild"
},
{
"name": "applyClasses"
},
{
"name": "applyOnCreateInstructions"
},
{
"name": "applyStyles"
},
{
"name": "applyStyling"
},
@ -503,9 +470,6 @@
{
"name": "attachPatchData"
},
{
"name": "attrsStylingIndexOf"
},
{
"name": "baseResolveDirective"
},
@ -524,9 +488,6 @@
{
"name": "bloomHashBitOrFactory"
},
{
"name": "booleanOrNull"
},
{
"name": "cacheMatchingLocalNames"
},
@ -545,12 +506,6 @@
{
"name": "checkView"
},
{
"name": "classProp"
},
{
"name": "classesBitMask"
},
{
"name": "cleanUpView"
},
@ -560,9 +515,15 @@
{
"name": "componentRefresh"
},
{
"name": "concatString"
},
{
"name": "containerInternal"
},
{
"name": "contextHasUpdates"
},
{
"name": "contextLView"
},
@ -578,9 +539,6 @@
{
"name": "createEmbeddedViewAndNode"
},
{
"name": "createEmptyStylingContext"
},
{
"name": "createLContainer"
},
@ -617,12 +575,6 @@
{
"name": "createViewBlueprint"
},
{
"name": "currentClassIndex"
},
{
"name": "currentStyleIndex"
},
{
"name": "decreaseElementDepthCount"
},
@ -635,9 +587,15 @@
{
"name": "deferBindingRegistration"
},
{
"name": "deferStylingUpdate"
},
{
"name": "deferredBindingQueue"
},
{
"name": "deleteStylingStateFromStorage"
},
{
"name": "destroyLView"
},
@ -653,9 +611,6 @@
{
"name": "diPublicInInjector"
},
{
"name": "directiveOwnerPointers"
},
{
"name": "domRendererFactory3"
},
@ -665,9 +620,6 @@
{
"name": "elementPropertyInternal"
},
{
"name": "enqueueHostInstruction"
},
{
"name": "enterView"
},
@ -731,12 +683,6 @@
{
"name": "findExistingListener"
},
{
"name": "findNextInsertionIndex"
},
{
"name": "findOrPatchDirectiveIntoRegistry"
},
{
"name": "findViaComponent"
},
@ -744,7 +690,7 @@
"name": "flushDeferredBindings"
},
{
"name": "flushQueue"
"name": "flushStyling"
},
{
"name": "forwardRef"
@ -764,9 +710,6 @@
{
"name": "getActiveDirectiveStylingIndex"
},
{
"name": "getActiveDirectiveStylingIndex"
},
{
"name": "getActiveDirectiveSuperClassDepth"
},
@ -776,18 +719,12 @@
{
"name": "getBeforeNodeForView"
},
{
"name": "getBindingNameFromIndex"
},
{
"name": "getBindingValue"
},
{
"name": "getBindingsEnabled"
},
{
"name": "getCachedStylingContext"
},
{
"name": "getCheckNoChangesMode"
},
@ -821,9 +758,6 @@
{
"name": "getContextLView"
},
{
"name": "getCurrentOrLViewSanitizer"
},
{
"name": "getCurrentStyleSanitizer"
},
@ -833,9 +767,6 @@
{
"name": "getDirectiveDef"
},
{
"name": "getDirectiveIndexFromEntry"
},
{
"name": "getElementDepthCount"
},
@ -852,19 +783,7 @@
"name": "getHostNative"
},
{
"name": "getInitialClassNameValue"
},
{
"name": "getInitialIndex"
},
{
"name": "getInitialStyleStringValue"
},
{
"name": "getInitialStylingValuesIndexOf"
},
{
"name": "getInitialValue"
"name": "getInitialStylingValue"
},
{
"name": "getInjectableDef"
@ -885,13 +804,10 @@
"name": "getLViewParent"
},
{
"name": "getMatchingBindingIndex"
"name": "getMapProp"
},
{
"name": "getMultiOrSingleIndex"
},
{
"name": "getMultiStylesStartIndex"
"name": "getMapValue"
},
{
"name": "getNameOnlyMarkerIndex"
@ -911,9 +827,6 @@
{
"name": "getNativeByTNodeOrNull"
},
{
"name": "getNativeFromLView"
},
{
"name": "getNodeInjectable"
},
@ -953,18 +866,6 @@
{
"name": "getPipeDef"
},
{
"name": "getPlayerBuilder"
},
{
"name": "getPlayerBuilderIndex"
},
{
"name": "getPlayerContext"
},
{
"name": "getPointers"
},
{
"name": "getPreviousIndex"
},
@ -974,9 +875,6 @@
{
"name": "getProp"
},
{
"name": "getProp"
},
{
"name": "getPropConfig"
},
@ -1001,24 +899,18 @@
{
"name": "getSelectedIndex"
},
{
"name": "getSinglePropIndexValue"
},
{
"name": "getStyleSanitizer"
},
{
"name": "getStylesContext"
},
{
"name": "getStylingContext"
},
{
"name": "getStylingContextFromLView"
"name": "getStylingMapArray"
},
{
"name": "getStylingMapsSyncFn"
},
{
"name": "getStylingState"
},
{
"name": "getSymbolIterator"
},
@ -1034,9 +926,6 @@
{
"name": "getTypeNameForDebugging"
},
{
"name": "getValue"
},
{
"name": "getValuesCount"
},
@ -1052,9 +941,6 @@
{
"name": "hasParentInjector"
},
{
"name": "hasPlayerBuilderChanged"
},
{
"name": "hasStyleInput"
},
@ -1064,15 +950,6 @@
{
"name": "hasValueChanged"
},
{
"name": "hasValueChanged"
},
{
"name": "hyphenate"
},
{
"name": "hyphenateEntries"
},
{
"name": "includeViewProviders"
},
@ -1085,12 +962,6 @@
{
"name": "initNodeFlags"
},
{
"name": "initStyling"
},
{
"name": "initializeStaticContext"
},
{
"name": "initializeTNodeInputs"
},
@ -1145,9 +1016,6 @@
{
"name": "isContentQueryHost"
},
{
"name": "isContextDirty"
},
{
"name": "isContextLocked"
},
@ -1163,9 +1031,6 @@
{
"name": "isDifferent"
},
{
"name": "isDirty"
},
{
"name": "isFactory"
},
@ -1226,6 +1091,9 @@
{
"name": "locateHostElement"
},
{
"name": "lockAndFinalizeContext"
},
{
"name": "lockContext"
},
@ -1238,6 +1106,9 @@
{
"name": "makeParamDecorator"
},
{
"name": "markContextToPersistState"
},
{
"name": "markDirty"
},
@ -1250,6 +1121,9 @@
{
"name": "matchTemplateAttribute"
},
{
"name": "maybeApplyStyling"
},
{
"name": "namespaceHTMLInternal"
},
@ -1286,30 +1160,15 @@
{
"name": "normalizeBitMaskValue"
},
{
"name": "patchContextWithStaticAttrs"
},
{
"name": "patchInitialStylingValue"
},
{
"name": "pointers"
},
{
"name": "postProcessBaseDirective"
},
{
"name": "postProcessDirective"
},
{
"name": "prepareInitialFlag"
},
{
"name": "queueComponentIndexForCheck"
},
{
"name": "readClassValueFromTNode"
},
{
"name": "readPatchedData"
},
@ -1332,13 +1191,7 @@
"name": "registerBinding"
},
{
"name": "registerHostDirective"
},
{
"name": "registerInitialStylingIntoContext"
},
{
"name": "registerMultiMapEntry"
"name": "registerInitialStylingOnTNode"
},
{
"name": "registerPostOrderHooks"
@ -1365,16 +1218,16 @@
"name": "renderEmbeddedTemplate"
},
{
"name": "renderInitialClasses"
},
{
"name": "renderInitialStyles"
"name": "renderInitialStyling"
},
{
"name": "renderStringify"
},
{
"name": "renderStyling"
"name": "renderStylingMap"
},
{
"name": "resetAllStylingState"
},
{
"name": "resetComponentState"
@ -1382,21 +1235,15 @@
{
"name": "resetPreOrderHookFlags"
},
{
"name": "resetStylingState"
},
{
"name": "resolveDirectives"
},
{
"name": "resolveForwardRef"
},
{
"name": "runtimeAllowOldStyling"
},
{
"name": "runtimeIsNewStylingInUse"
},
{
"name": "sanitizeUsingSanitizerObject"
},
{
"name": "saveNameToExportMap"
},
@ -1421,27 +1268,15 @@
{
"name": "setBindingRoot"
},
{
"name": "setCachedStylingContext"
},
{
"name": "setCheckNoChangesMode"
},
{
"name": "setClass"
},
{
"name": "setClass"
},
{
"name": "setConfig"
},
{
"name": "setContextDirty"
},
{
"name": "setContextPlayersDirty"
},
{
"name": "setCurrentDirectiveDef"
},
@ -1452,10 +1287,7 @@
"name": "setCurrentStyleSanitizer"
},
{
"name": "setDirty"
},
{
"name": "setFlag"
"name": "setDirectiveStylingInput"
},
{
"name": "setGuardMask"
@ -1479,32 +1311,17 @@
"name": "setIsNotParent"
},
{
"name": "setNodeStylingTemplate"
},
{
"name": "setPlayerBuilder"
},
{
"name": "setPlayerBuilderIndex"
"name": "setMapValue"
},
{
"name": "setPreviousOrParentTNode"
},
{
"name": "setProp"
},
{
"name": "setSanitizeFlag"
},
{
"name": "setSelectedIndex"
},
{
"name": "setStyle"
},
{
"name": "setStyle"
},
{
"name": "setTNodeAndViewData"
},
@ -1512,10 +1329,10 @@
"name": "setUpAttributes"
},
{
"name": "setValue"
"name": "shouldSearchParent"
},
{
"name": "shouldSearchParent"
"name": "stateIsPersisted"
},
{
"name": "storeBindingMetadata"
@ -1523,6 +1340,9 @@
{
"name": "storeCleanupFn"
},
{
"name": "storeStylingState"
},
{
"name": "stringify"
},
@ -1530,16 +1350,7 @@
"name": "stringifyForError"
},
{
"name": "stylesBitMask"
},
{
"name": "stylingApply"
},
{
"name": "stylingContext"
},
{
"name": "stylingInit"
"name": "stylingMapToString"
},
{
"name": "syncViewWithBlueprint"
@ -1566,26 +1377,17 @@
"name": "updateClassBinding"
},
{
"name": "updateClassProp"
},
{
"name": "updateContextDirectiveIndex"
},
{
"name": "updateContextWithBindings"
"name": "updateInitialStylingOnContext"
},
{
"name": "updateLastDirectiveIndex"
},
{
"name": "updateSingleStylingValue"
"name": "updateLastDirectiveIndex"
},
{
"name": "updateStyleBinding"
},
{
"name": "valueExists"
},
{
"name": "viewAttachedToChangeDetector"
},

View File

@ -7,13 +7,13 @@
*/
import {NgForOfContext} from '@angular/common';
import {ɵɵdefineComponent} from '../../src/render3/definition';
import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
import {AttributeMarker} from '../../src/render3/interfaces/node';
import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl} from '../../src/sanitization/bypass';
import {ɵɵdefaultStyleSanitizer, ɵɵsanitizeHtml, ɵɵsanitizeResourceUrl, ɵɵsanitizeScript, ɵɵsanitizeStyle, ɵɵsanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
import {StyleSanitizeFn} from '../../src/sanitization/style_sanitizer';
import {NgForOf} from './common_with_def';
import {ComponentFixture, TemplateFixture} from './render_util';
@ -25,10 +25,7 @@ describe('instructions', () => {
ɵɵelementEnd();
}
function createDiv(
initialClasses?: string[] | null, classBindingNames?: string[] | null,
initialStyles?: string[] | null, styleBindingNames?: string[] | null,
styleSanitizer?: StyleSanitizeFn) {
function createDiv(initialClasses?: string[] | null, initialStyles?: string[] | null) {
const attrs: any[] = [];
if (initialClasses) {
attrs.push(AttributeMarker.Classes, ...initialClasses);
@ -37,7 +34,7 @@ describe('instructions', () => {
attrs.push(AttributeMarker.Styles, ...initialStyles);
}
ɵɵelementStart(0, 'div', attrs);
ɵɵstyling(classBindingNames || null, styleBindingNames || null, styleSanitizer);
ɵɵstyling();
ɵɵelementEnd();
}
@ -56,16 +53,10 @@ describe('instructions', () => {
it('should update bindings when value changes with the correct perf counters', () => {
const t = new TemplateFixture(createAnchor, () => {}, 1, 1);
t.update(() => {
ɵɵselect(0);
ɵɵproperty('title', 'Hello');
});
t.update(() => { ɵɵproperty('title', 'Hello'); });
expect(t.html).toEqual('<a title="Hello"></a>');
t.update(() => {
ɵɵselect(0);
ɵɵproperty('title', 'World');
});
t.update(() => { ɵɵproperty('title', 'World'); });
expect(t.html).toEqual('<a title="World"></a>');
expect(ngDevMode).toHaveProperties({
firstTemplatePass: 1,
@ -78,10 +69,7 @@ describe('instructions', () => {
it('should not update bindings when value does not change, with the correct perf counters',
() => {
const idempotentUpdate = () => {
ɵɵselect(0);
ɵɵproperty('title', 'Hello');
};
const idempotentUpdate = () => { ɵɵproperty('title', 'Hello'); };
const t = new TemplateFixture(createAnchor, idempotentUpdate, 1, 1);
t.update();
@ -121,14 +109,10 @@ describe('instructions', () => {
it('should use sanitizer function', () => {
const t = new TemplateFixture(createDiv, () => {}, 1, 1);
t.update(() => {
ɵɵselect(0);
ɵɵattribute('title', 'javascript:true', ɵɵsanitizeUrl);
});
t.update(() => { ɵɵattribute('title', 'javascript:true', ɵɵsanitizeUrl); });
expect(t.html).toEqual('<div title="unsafe:javascript:true"></div>');
t.update(() => {
ɵɵselect(0);
ɵɵattribute('title', bypassSanitizationTrustUrl('javascript:true'), ɵɵsanitizeUrl);
});
expect(t.html).toEqual('<div title="javascript:true"></div>');
@ -167,18 +151,17 @@ describe('instructions', () => {
describe('styleProp', () => {
it('should automatically sanitize unless a bypass operation is applied', () => {
const t = new TemplateFixture(() => {
return createDiv(null, null, null, ['background-image'], ɵɵdefaultStyleSanitizer);
}, () => {}, 1);
const t = new TemplateFixture(() => { return createDiv(); }, () => {}, 1);
t.update(() => {
ɵɵstyleProp(0, 'url("http://server")');
ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer);
ɵɵstyleProp('background-image', 'url("http://server")');
ɵɵstylingApply();
});
// nothing is set because sanitizer suppresses it.
expect(t.html).toEqual('<div></div>');
t.update(() => {
ɵɵstyleProp(0, bypassSanitizationTrustStyle('url("http://server2")'));
ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('url("http://server2")'));
ɵɵstylingApply();
});
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
@ -187,13 +170,11 @@ describe('instructions', () => {
it('should not re-apply the style value even if it is a newly bypassed again', () => {
const sanitizerInterceptor = new MockSanitizerInterceptor();
const t = createTemplateFixtureWithSanitizer(
() => createDiv(
null, null, null, ['background-image'], sanitizerInterceptor.getStyleSanitizer()),
1, sanitizerInterceptor);
const t = createTemplateFixtureWithSanitizer(() => createDiv(), 1, sanitizerInterceptor);
t.update(() => {
ɵɵstyleProp(0, bypassSanitizationTrustStyle('apple'));
ɵɵstyleSanitizer(sanitizerInterceptor.getStyleSanitizer());
ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('apple'));
ɵɵstylingApply();
});
@ -201,7 +182,8 @@ describe('instructions', () => {
sanitizerInterceptor.lastValue = null;
t.update(() => {
ɵɵstyleProp(0, bypassSanitizationTrustStyle('apple'));
ɵɵstyleSanitizer(sanitizerInterceptor.getStyleSanitizer());
ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('apple'));
ɵɵstylingApply();
});
expect(sanitizerInterceptor.lastValue).toEqual(null);
@ -211,7 +193,7 @@ describe('instructions', () => {
describe('styleMap', () => {
function createDivWithStyle() {
ɵɵelementStart(0, 'div', [AttributeMarker.Styles, 'height', '10px']);
ɵɵstyling([], ['height']);
ɵɵstyling();
ɵɵelementEnd();
}
@ -228,11 +210,11 @@ describe('instructions', () => {
const detectedValues: string[] = [];
const sanitizerInterceptor =
new MockSanitizerInterceptor(value => { detectedValues.push(value); });
const fixture = createTemplateFixtureWithSanitizer(
() => createDiv(null, null, null, null, sanitizerInterceptor.getStyleSanitizer()), 1,
sanitizerInterceptor);
const fixture =
createTemplateFixtureWithSanitizer(() => createDiv(), 1, sanitizerInterceptor);
fixture.update(() => {
ɵɵstyleSanitizer(sanitizerInterceptor.getStyleSanitizer());
ɵɵstyleMap({
'background-image': 'background-image',
'background': 'background',
@ -265,7 +247,7 @@ describe('instructions', () => {
ɵɵclassMap('multiple classes');
ɵɵstylingApply();
});
expect(fixture.html).toEqual('<div class="multiple classes"></div>');
expect(fixture.html).toEqual('<div class="classes multiple"></div>');
});
});
@ -320,7 +302,6 @@ describe('instructions', () => {
ɵɵtemplate(0, ToDoAppComponent_NgForOf_Template_0, 2, 1, 'ul', _c0);
}
if (rf & RenderFlags.Update) {
ɵɵselect(0);
ɵɵproperty('ngForOf', ctx.rows);
}
},
@ -343,10 +324,7 @@ describe('instructions', () => {
const inputValue = 'http://foo';
const outputValue = 'http://foo-sanitized';
t.update(() => {
ɵɵselect(0);
ɵɵattribute('href', inputValue, ɵɵsanitizeUrl);
});
t.update(() => { ɵɵattribute('href', inputValue, ɵɵsanitizeUrl); });
expect(t.html).toEqual(`<a href="${outputValue}"></a>`);
expect(s.lastSanitizedValue).toEqual(outputValue);
});
@ -357,10 +335,7 @@ describe('instructions', () => {
const inputValue = s.bypassSecurityTrustUrl('http://foo');
const outputValue = 'http://foo';
t.update(() => {
ɵɵselect(0);
ɵɵattribute('href', inputValue, ɵɵsanitizeUrl);
});
t.update(() => { ɵɵattribute('href', inputValue, ɵɵsanitizeUrl); });
expect(t.html).toEqual(`<a href="${outputValue}"></a>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
@ -371,10 +346,7 @@ describe('instructions', () => {
const inputValue = bypassSanitizationTrustUrl('http://foo');
const outputValue = 'http://foo-ivy';
t.update(() => {
ɵɵselect(0);
ɵɵattribute('href', inputValue, ɵɵsanitizeUrl);
});
t.update(() => { ɵɵattribute('href', inputValue, ɵɵsanitizeUrl); });
expect(t.html).toEqual(`<a href="${outputValue}"></a>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
@ -385,10 +357,7 @@ describe('instructions', () => {
const inputValue = 'color:red';
const outputValue = 'color:blue';
t.update(() => {
ɵɵselect(0);
ɵɵattribute('style', inputValue, ɵɵsanitizeStyle);
});
t.update(() => { ɵɵattribute('style', inputValue, ɵɵsanitizeStyle); });
expect(stripStyleWsCharacters(t.html)).toEqual(`<div style="${outputValue}"></div>`);
expect(s.lastSanitizedValue).toEqual(outputValue);
});
@ -399,10 +368,7 @@ describe('instructions', () => {
const inputValue = s.bypassSecurityTrustStyle('color:maroon');
const outputValue = 'color:maroon';
t.update(() => {
ɵɵselect(0);
ɵɵattribute('style', inputValue, ɵɵsanitizeStyle);
});
t.update(() => { ɵɵattribute('style', inputValue, ɵɵsanitizeStyle); });
expect(stripStyleWsCharacters(t.html)).toEqual(`<div style="${outputValue}"></div>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
@ -413,10 +379,7 @@ describe('instructions', () => {
const inputValue = bypassSanitizationTrustStyle('font-family:foo');
const outputValue = 'font-family:foo-ivy';
t.update(() => {
ɵɵselect(0);
ɵɵattribute('style', inputValue, ɵɵsanitizeStyle);
});
t.update(() => { ɵɵattribute('style', inputValue, ɵɵsanitizeStyle); });
expect(stripStyleWsCharacters(t.html)).toEqual(`<div style="${outputValue}"></div>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
@ -427,10 +390,7 @@ describe('instructions', () => {
const inputValue = 'http://resource';
const outputValue = 'http://resource-sanitized';
t.update(() => {
ɵɵselect(0);
ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl);
});
t.update(() => { ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl); });
expect(t.html).toEqual(`<script src="${outputValue}"></script>`);
expect(s.lastSanitizedValue).toEqual(outputValue);
});
@ -441,10 +401,7 @@ describe('instructions', () => {
const inputValue = s.bypassSecurityTrustResourceUrl('file://all-my-secrets.pdf');
const outputValue = 'file://all-my-secrets.pdf';
t.update(() => {
ɵɵselect(0);
ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl);
});
t.update(() => { ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl); });
expect(t.html).toEqual(`<script src="${outputValue}"></script>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
@ -455,10 +412,7 @@ describe('instructions', () => {
const inputValue = bypassSanitizationTrustResourceUrl('file://all-my-secrets.pdf');
const outputValue = 'file://all-my-secrets.pdf-ivy';
t.update(() => {
ɵɵselect(0);
ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl);
});
t.update(() => { ɵɵattribute('src', inputValue, ɵɵsanitizeResourceUrl); });
expect(t.html).toEqual(`<script src="${outputValue}"></script>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
@ -469,10 +423,7 @@ describe('instructions', () => {
const inputValue = 'fn();';
const outputValue = 'fn(); //sanitized';
t.update(() => {
ɵɵselect(0);
ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript);
});
t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript); });
expect(t.html).toEqual(`<script>${outputValue}</script>`);
expect(s.lastSanitizedValue).toEqual(outputValue);
});
@ -483,10 +434,7 @@ describe('instructions', () => {
const inputValue = s.bypassSecurityTrustScript('alert("bar")');
const outputValue = 'alert("bar")';
t.update(() => {
ɵɵselect(0);
ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript);
});
t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript); });
expect(t.html).toEqual(`<script>${outputValue}</script>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
@ -497,10 +445,7 @@ describe('instructions', () => {
const inputValue = bypassSanitizationTrustScript('alert("bar")');
const outputValue = 'alert("bar")-ivy';
t.update(() => {
ɵɵselect(0);
ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript);
});
t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript); });
expect(t.html).toEqual(`<script>${outputValue}</script>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
@ -511,10 +456,7 @@ describe('instructions', () => {
const inputValue = '<header></header>';
const outputValue = '<header></header> <!--sanitized-->';
t.update(() => {
ɵɵselect(0);
ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml);
});
t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml); });
expect(t.html).toEqual(`<div>${outputValue}</div>`);
expect(s.lastSanitizedValue).toEqual(outputValue);
});
@ -525,10 +467,7 @@ describe('instructions', () => {
const inputValue = s.bypassSecurityTrustHtml('<div onclick="alert(123)"></div>');
const outputValue = '<div onclick="alert(123)"></div>';
t.update(() => {
ɵɵselect(0);
ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml);
});
t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml); });
expect(t.html).toEqual(`<div>${outputValue}</div>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
@ -539,10 +478,7 @@ describe('instructions', () => {
const inputValue = bypassSanitizationTrustHtml('<div onclick="alert(123)"></div>');
const outputValue = '<div onclick="alert(123)"></div>-ivy';
t.update(() => {
ɵɵselect(0);
ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml);
});
t.update(() => { ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeHtml); });
expect(t.html).toEqual(`<div>${outputValue}</div>`);
expect(s.lastSanitizedValue).toBeFalsy();
});

View File

@ -633,7 +633,7 @@ describe('element discovery', () => {
template: (rf: RenderFlags, ctx: StructuredComp) => {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'section');
ɵɵstyling(['class-foo']);
ɵɵstyling();
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
@ -651,8 +651,7 @@ describe('element discovery', () => {
expect(Array.isArray(result1)).toBeTruthy();
const elementResult = result1[HEADER_OFFSET]; // first element
expect(Array.isArray(elementResult)).toBeTruthy();
expect(elementResult[StylingIndex.ElementPosition]).toBe(section);
expect(elementResult).toBe(section);
const context = getLContext(section) !;
const result2 = section[MONKEY_PATCH_KEY_NAME];

View File

@ -337,11 +337,11 @@ describe('css selector matching', () => {
// <div class="abc"> (with attrs but without styling context)
tNode.attrs = ['class', 'abc'];
tNode.stylingTemplate = null;
tNode.classes = null;
expect(isMatching('div', tNode, selector)).toBeTruthy();
// <div class="abc"> (with styling context but without attrs)
tNode.stylingTemplate = initializeStaticContext([AttributeMarker.Classes, 'abc'], 0);
tNode.classes = ['abc', 'abc', true];
tNode.attrs = null;
expect(isMatching('div', tNode, selector)).toBeTruthy();
});

View File

@ -378,122 +378,6 @@ describe('style and class based bindings', () => {
});
});
describe('instructions', () => {
it('should handle a combination of initial, multi and singular style values (in that order)',
() => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'span', [
AttributeMarker.Styles,
'width',
'200px',
'height',
'100px',
'opacity',
'0.5',
]);
ɵɵstyling(null, ['width']);
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵstyleMap(ctx.myStyles);
ɵɵstyleProp(0, ctx.myWidth);
ɵɵstylingApply();
}
}
expect(renderToHtml(
Template, {myStyles: {width: '200px', height: '200px'}, myWidth: '300px'}, 1))
.toEqual('<span style="height: 200px; opacity: 0.5; width: 300px;"></span>');
expect(
renderToHtml(Template, {myStyles: {width: '200px', height: null}, myWidth: null}, 1))
.toEqual('<span style="height: 100px; opacity: 0.5; width: 200px;"></span>');
});
it('should support styles on SVG elements', () => {
// <svg [style.width.px]="diameter" [style.height.px]="diameter">
// <circle stroke="green" fill="yellow" />
// </svg>
class Comp {
diameter: number = 100;
static ngComponentDef = ɵɵdefineComponent({
type: Comp,
selectors: [['comp']],
factory: () => new Comp(),
consts: 2,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
ɵɵnamespaceSVG();
ɵɵelementStart(0, 'svg');
ɵɵstyling(null, ['width', 'height']);
ɵɵelementStart(1, 'circle', ['stroke', 'green', 'fill', 'yellow']);
ɵɵelementEnd();
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵstyleProp(0, ctx.diameter, 'px');
ɵɵstyleProp(1, ctx.diameter, 'px');
ɵɵstylingApply();
}
}
});
}
const fixture = new ComponentFixture(Comp);
fixture.update();
const target = fixture.hostElement.querySelector('svg') !as any;
expect(target.style.width).toEqual('100px');
expect(target.style.height).toEqual('100px');
expect(fixture.html)
.toEqual(
'<svg style="height: 100px; width: 100px;"><circle fill="yellow" stroke="green"></circle></svg>');
});
it('should support binding to camelCased and hyphenated style properties', () => {
// <div [style.borderWidth]="border-width" [style.border-color]="borderColor"></div>
class Comp {
borderWidth: string = '3px';
borderColor: string = 'red';
static ngComponentDef = ɵɵdefineComponent({
type: Comp,
selectors: [['comp']],
factory: () => new Comp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
ɵɵstyling(null, ['borderWidth', 'border-color']);
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵstyleProp(0, ctx.borderWidth);
ɵɵstyleProp(1, ctx.borderColor);
ɵɵstylingApply();
}
}
});
}
const fixture = new ComponentFixture(Comp);
fixture.update();
const target = fixture.hostElement.querySelector('div') !as any;
expect(target.style.borderWidth).toEqual('3px');
expect(target.style.borderColor).toEqual('red');
expect(fixture.html).toContain('border-width: 3px');
expect(fixture.html).toContain('border-color: red');
});
});
describe('dynamic styling properties within a styling context', () => {
it('should initialize a context with a series of styling bindings as well as single property offsets',
() => {
@ -2595,438 +2479,7 @@ describe('style and class based bindings', () => {
]);
});
it('should destroy an existing player that was queued before it is flushed once the binding updates',
() => {
const context = createStylingContext(null, ['width']);
const handler = new CorePlayerHandler();
const lView = createMockViewData(handler, context);
const players: MockPlayer[] = [];
const buildFn =
(element: HTMLElement, type: BindingType, value: any, firstRender: boolean,
oldPlayer: Player | null) => {
const player = new MockPlayer(value);
players.push(player);
return player;
};
expect(context[StylingIndex.PlayerContext]).toEqual(null);
let mapFactory = bindPlayerFactory(buildFn, {width: '200px'});
updateStyleAndClassMaps(context, null, mapFactory);
renderStyles(context, false, undefined, lView);
expect(players.length).toEqual(1);
const p1 = players.pop() !;
expect(p1.state).toEqual(PlayState.Pending);
mapFactory = bindPlayerFactory(buildFn, {width: '100px'});
updateStyleAndClassMaps(context, null, mapFactory);
renderStyles(context, false, undefined, lView);
expect(players.length).toEqual(1);
const p2 = players.pop() !;
expect(p1.state).toEqual(PlayState.Destroyed);
expect(p2.state).toEqual(PlayState.Pending);
});
it('should nullify style map and style property factories if any follow up expressions not use them',
() => {
const context = createStylingContext(null, ['color'], null, ['foo']);
const handler = new CorePlayerHandler();
const lView = createMockViewData(handler, context);
const stylePlayers: Player[] = [];
const buildStyleFn = (element: HTMLElement, type: BindingType, value: any) => {
const player = new MockPlayer();
stylePlayers.push(player);
return player;
};
const classPlayers: Player[] = [];
const buildClassFn = (element: HTMLElement, type: BindingType, value: any) => {
const player = new MockPlayer();
classPlayers.push(player);
return player;
};
assertContext(context, [
element, //
masterConfig(18, false), //
[2, null], //
[null, null, 'color', null, 0], //
[null, null, 'foo', false, 0], //
[1, 1, 1, 1, 10, 14], //
[1, 0, 22, null, 1], //
[1, 0, 18, null, 1], //
null, //
null, //
// #10
cleanStyle(3, 18),
'color',
null,
0,
// #14
cleanClass(3, 22),
'foo',
null,
0,
// #18
cleanStyle(3, 10),
'color',
null,
0,
// #22
cleanClass(3, 14),
'foo',
null,
0,
]);
const cachedClassMap = {map: true};
const cachedStyleMap = {opacity: '1'};
const styleMapWithPlayerFactory = bindPlayerFactory(buildStyleFn, cachedStyleMap);
const classMapWithPlayerFactory = bindPlayerFactory(buildClassFn, cachedClassMap);
const styleMapPlayerBuilder = makePlayerBuilder(styleMapWithPlayerFactory, false);
const classMapPlayerBuilder = makePlayerBuilder(classMapWithPlayerFactory, true);
updateStyleAndClassMaps(context, classMapWithPlayerFactory, styleMapWithPlayerFactory);
const colorWithPlayerFactory = bindPlayerFactory(buildStyleFn, 'red');
const fooWithPlayerFactory = bindPlayerFactory(buildClassFn, true);
const colorPlayerBuilder = makePlayerBuilder(colorWithPlayerFactory, false);
const fooPlayerBuilder = makePlayerBuilder(fooWithPlayerFactory, true);
updateStyleProp(context, 0, colorWithPlayerFactory as any);
updateClassProp(context, 0, fooWithPlayerFactory as any);
renderStyles(context, false, undefined, lView);
const p1 = classPlayers.shift();
const p2 = stylePlayers.shift();
const p3 = stylePlayers.shift();
const p4 = classPlayers.shift();
let playerContext = context[StylingIndex.PlayerContext] !;
expect(playerContext).toEqual([
9, classMapPlayerBuilder, p1, styleMapPlayerBuilder, p2, colorPlayerBuilder, p3,
fooPlayerBuilder, p4
] as PlayerContext);
assertContext(context, [
element, //
masterConfig(18, false), //
[2, null], //
[null, null, 'color', null, 0], //
[null, null, 'foo', false, 0], //
[1, 1, 1, 1, 10, 14], //
[1, 0, 26, classMapWithPlayerFactory, 1], //
[1, 0, 18, styleMapWithPlayerFactory, 1], //
null, //
playerContext,
// #10
cleanStyle(3, 22),
'color',
'red',
directiveOwnerPointers(0, 5),
// #14
cleanClass(3, 30),
'foo',
true,
directiveOwnerPointers(0, 7),
// #18
cleanStyle(0, 0),
'opacity',
'1',
directiveOwnerPointers(0, 3),
// #22
cleanStyle(3, 10),
'color',
null,
0,
// #26
cleanClass(0, 0),
'map',
true,
directiveOwnerPointers(0, 1),
// #30
cleanClass(3, 14),
'foo',
null,
0,
]);
updateStyleAndClassMaps(context, cachedClassMap, cachedStyleMap);
const colorWithoutPlayerFactory = 'blue';
const fooWithoutPlayerFactory = false;
updateStyleProp(context, 0, colorWithoutPlayerFactory);
updateClassProp(context, 0, fooWithoutPlayerFactory);
renderStyles(context, false, undefined, lView);
playerContext = context[StylingIndex.PlayerContext] !;
expect(playerContext).toEqual([
9, null, null, null, null, null, null, null, null
] as PlayerContext);
assertContext(context, [
element, //
masterConfig(18, false), //
[2, null], //
[null, null, 'color', null, 0], //
[null, null, 'foo', false, 0], //
[1, 1, 1, 1, 10, 14], //
[1, 0, 26, cachedClassMap, 1], //
[1, 0, 18, cachedStyleMap, 1], //
null, //
playerContext,
// #10
cleanStyle(3, 22),
'color',
'blue',
0,
// #14
cleanClass(3, 30),
'foo',
false,
0,
// #18
cleanStyle(0, 0),
'opacity',
'1',
0,
// #22
cleanStyle(3, 10),
'color',
null,
0,
// #26
cleanClass(0, 0),
'map',
true,
0,
// #30
cleanClass(3, 14),
'foo',
null,
0,
]);
});
it('should not call a factory if no style and/or class values have been updated', () => {
const context = createStylingContext([]);
const handler = new CorePlayerHandler();
const lView = createMockViewData(handler, context);
let styleCalls = 0;
const buildStyleFn = (element: HTMLElement, type: BindingType, value: any) => {
styleCalls++;
return new MockPlayer();
};
let classCalls = 0;
const buildClassFn = (element: HTMLElement, type: BindingType, value: any) => {
classCalls++;
return new MockPlayer();
};
let styleFactory = bindPlayerFactory(buildStyleFn, {opacity: '1'}) as BoundPlayerFactory<any>;
let classFactory = bindPlayerFactory(buildClassFn, 'bar') as BoundPlayerFactory<any>;
updateStyleAndClassMaps(context, classFactory, styleFactory);
expect(styleCalls).toEqual(0);
expect(classCalls).toEqual(0);
renderStyles(context, false, undefined, lView);
expect(styleCalls).toEqual(1);
expect(classCalls).toEqual(1);
renderStyles(context, false, undefined, lView);
expect(styleCalls).toEqual(1);
expect(classCalls).toEqual(1);
styleFactory = bindPlayerFactory(buildStyleFn, {opacity: '0.5'}) as BoundPlayerFactory<any>;
updateStyleAndClassMaps(context, classFactory, styleFactory);
renderStyles(context, false, undefined, lView);
expect(styleCalls).toEqual(2);
expect(classCalls).toEqual(1);
classFactory = bindPlayerFactory(buildClassFn, 'foo') as BoundPlayerFactory<any>;
updateStyleAndClassMaps(context, classFactory, styleFactory);
renderStyles(context, false, undefined, lView);
expect(styleCalls).toEqual(2);
expect(classCalls).toEqual(2);
updateStyleAndClassMaps(context, 'foo', {opacity: '0.5'});
renderStyles(context, false, undefined, lView);
expect(styleCalls).toEqual(2);
expect(classCalls).toEqual(2);
});
it('should invoke a single prop player over a multi style player when present and delegate back if not',
() => {
const context = createStylingContext(null, ['color']);
const handler = new CorePlayerHandler();
const lView = createMockViewData(handler, context);
let propPlayer: Player|null = null;
const propBuildFn = (element: HTMLElement, type: BindingType, value: any) => {
return propPlayer = new MockPlayer();
};
let styleMapPlayer: Player|null = null;
const mapBuildFn = (element: HTMLElement, type: BindingType, value: any) => {
return styleMapPlayer = new MockPlayer();
};
const mapFactory = bindPlayerFactory(mapBuildFn, {color: 'black'});
updateStyleAndClassMaps(context, null, mapFactory);
updateStyleProp(context, 0, 'green');
renderStyles(context, false, undefined, lView);
expect(propPlayer).toBeFalsy();
expect(styleMapPlayer).toBeFalsy();
const propFactory = bindPlayerFactory(propBuildFn, 'orange');
updateStyleProp(context, 0, propFactory as any);
renderStyles(context, false, undefined, lView);
expect(propPlayer).toBeTruthy();
expect(styleMapPlayer).toBeFalsy();
propPlayer = styleMapPlayer = null;
updateStyleProp(context, 0, null);
renderStyles(context, false, undefined, lView);
expect(propPlayer).toBeFalsy();
expect(styleMapPlayer).toBeTruthy();
propPlayer = styleMapPlayer = null;
updateStyleAndClassMaps(context, null, null);
renderStyles(context, false, undefined, lView);
expect(propPlayer).toBeFalsy();
expect(styleMapPlayer).toBeFalsy();
});
it('should return the old player for styles when a follow-up player is instantiated', () => {
const context = createStylingContext([]);
const handler = new CorePlayerHandler();
const lView = createMockViewData(handler, context);
let previousPlayer: MockPlayer|null = null;
let currentPlayer: MockPlayer|null = null;
const buildFn =
(element: HTMLElement, type: BindingType, value: any, firstRender: boolean,
existingPlayer: Player | null) => {
previousPlayer = existingPlayer as MockPlayer | null;
return currentPlayer = new MockPlayer(value);
};
let factory = bindPlayerFactory<{[key: string]: any}>(buildFn, {width: '200px'});
updateStyleAndClassMaps(context, null, factory);
renderStyles(context, false, undefined, lView);
expect(previousPlayer).toEqual(null);
expect(currentPlayer !.value).toEqual({width: '200px'});
factory = bindPlayerFactory(buildFn, {height: '200px'});
updateStyleAndClassMaps(context, null, factory);
renderStyles(context, false, undefined, lView);
expect(previousPlayer !.value).toEqual({width: '200px'});
expect(currentPlayer !.value).toEqual({width: null, height: '200px'});
});
it('should return the old player for classes when a follow-up player is instantiated', () => {
const context = createStylingContext();
const handler = new CorePlayerHandler();
const lView = createMockViewData(handler, context);
let currentPlayer: MockPlayer|null = null;
let previousPlayer: MockPlayer|null = null;
const buildFn =
(element: HTMLElement, type: BindingType, value: any, firstRender: boolean,
existingPlayer: Player | null) => {
previousPlayer = existingPlayer as MockPlayer | null;
return currentPlayer = new MockPlayer(value);
};
let factory = bindPlayerFactory<any>(buildFn, {foo: true});
updateStyleAndClassMaps(context, null, factory);
renderStyles(context, false, undefined, lView);
expect(currentPlayer).toBeTruthy();
expect(previousPlayer).toBeFalsy();
expect(currentPlayer !.value).toEqual({foo: true});
previousPlayer = currentPlayer = null;
factory = bindPlayerFactory(buildFn, {bar: true});
updateStyleAndClassMaps(context, null, factory);
renderStyles(context, false, undefined, lView);
expect(currentPlayer).toBeTruthy();
expect(previousPlayer).toBeTruthy();
expect(currentPlayer !.value).toEqual({foo: null, bar: true});
expect(previousPlayer !.value).toEqual({foo: true});
});
it('should sanitize styles before they are passed into the player', () => {
const sanitizer = (function(prop: string, value: string, mode: StyleSanitizeMode): any {
let allow = true;
if (mode & StyleSanitizeMode.ValidateProperty) {
allow = prop === 'width' || prop === 'height';
}
if (mode & StyleSanitizeMode.SanitizeOnly) {
return allow ? `${value}-safe!` : value;
} else {
return allow;
}
}) as StyleSanitizeFn;
const context = createStylingContext(null, null, null, null, sanitizer);
const handler = new CorePlayerHandler();
const lView = createMockViewData(handler, context);
let values: {[key: string]: any}|null = null;
const buildFn =
(element: HTMLElement, type: BindingType, value: any, isFirstRender: boolean) => {
values = value;
return new MockPlayer();
};
let factory = bindPlayerFactory<{[key: string]: any}>(
buildFn, {width: '200px', height: '100px', opacity: '1'});
updateStyleAndClassMaps(context, null, factory);
renderStyles(context, false, undefined, lView);
expect(values !).toEqual({width: '200px-safe!', height: '100px-safe!', opacity: '1'});
factory = bindPlayerFactory(buildFn, {width: 'auto'});
updateStyleAndClassMaps(context, null, factory);
renderStyles(context, false, undefined, lView);
expect(values !).toEqual({width: 'auto-safe!', height: null, opacity: null});
});
it('should automatically destroy existing players when the follow-up binding is not apart of a factory',
it('should automatically destroy existing players when the follow-up binding is not a part of a factory',
() => {
const context = createStylingContext(null, ['width'], null, ['foo', 'bar']);
const handler = new CorePlayerHandler();
@ -3079,244 +2532,6 @@ describe('style and class based bindings', () => {
expect(p4.state).toEqual(PlayState.Destroyed);
expect(p5.state).toEqual(PlayState.Running);
});
it('should list all [style] and [class] players alongside custom players in the context',
() => {
const players: Player[] = [];
const styleBuildFn = (element: HTMLElement, type: BindingType, value: any) => {
const player = new MockPlayer();
players.push(player);
return player;
};
const classBuildFn = (element: HTMLElement, type: BindingType, value: any) => {
const player = new MockPlayer();
players.push(player);
return player;
};
const styleMapFactory = bindPlayerFactory(styleBuildFn, {height: '200px'});
const classMapFactory = bindPlayerFactory(classBuildFn, {bar: true});
const widthFactory = bindPlayerFactory(styleBuildFn, '100px');
const fooFactory = bindPlayerFactory(classBuildFn, true);
class Comp {
static ngComponentDef = ɵɵdefineComponent({
type: Comp,
selectors: [['comp']],
factory: () => new Comp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
ɵɵstyling(['foo'], ['width']);
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵstyleMap(styleMapFactory);
ɵɵclassMap(classMapFactory);
ɵɵstyleProp(0, widthFactory);
ɵɵclassProp(0, fooFactory);
ɵɵstylingApply();
}
}
});
}
const fixture = new ComponentFixture(Comp);
fixture.update();
const target = fixture.hostElement.querySelector('div') !as any;
const elementContext = getLContext(target) !;
const context = elementContext.lView[elementContext.nodeIndex] as StylingContext;
expect(players.length).toEqual(4);
const [p1, p2, p3, p4] = players;
const playerContext = context[StylingIndex.PlayerContext];
expect(playerContext).toContain(p1);
expect(playerContext).toContain(p2);
expect(playerContext).toContain(p3);
expect(playerContext).toContain(p4);
expect(getPlayers(target)).toEqual([p1, p2, p3, p4]);
const p5 = new MockPlayer();
const p6 = new MockPlayer();
addPlayer(target, p5);
addPlayer(target, p6);
expect(getPlayers(target)).toEqual([p1, p2, p3, p4, p5, p6]);
p3.destroy();
p5.destroy();
expect(getPlayers(target)).toEqual([p1, p2, p4, p6]);
});
it('should build a player and signal that the first render is active', () => {
const firstRenderCaptures: any[] = [];
const otherRenderCaptures: any[] = [];
const buildFn =
(element: HTMLElement, type: BindingType, value: any, isFirstRender: boolean) => {
if (isFirstRender) {
firstRenderCaptures.push({type, value});
} else {
otherRenderCaptures.push({type, value});
}
return new MockPlayer();
};
let styleMapFactory =
bindPlayerFactory(buildFn, {height: '200px'}) as BoundPlayerFactory<any>;
let classMapFactory = bindPlayerFactory(buildFn, {bar: true}) as BoundPlayerFactory<any>;
let widthFactory = bindPlayerFactory(buildFn, '100px') as BoundPlayerFactory<any>;
let fooFactory = bindPlayerFactory(buildFn, true) as BoundPlayerFactory<any>;
class Comp {
static ngComponentDef = ɵɵdefineComponent({
type: Comp,
selectors: [['comp']],
factory: () => new Comp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
ɵɵstyling(['foo'], ['width']);
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵstyleMap(styleMapFactory);
ɵɵclassMap(classMapFactory);
ɵɵstyleProp(0, widthFactory);
ɵɵclassProp(0, fooFactory);
ɵɵstylingApply();
}
}
});
}
const fixture = new ComponentFixture(Comp);
expect(firstRenderCaptures.length).toEqual(4);
expect(firstRenderCaptures[0]).toEqual({type: BindingType.Class, value: {bar: true}});
expect(firstRenderCaptures[1]).toEqual({type: BindingType.Style, value: {height: '200px'}});
expect(firstRenderCaptures[2]).toEqual({type: BindingType.Style, value: {width: '100px'}});
expect(firstRenderCaptures[3]).toEqual({type: BindingType.Class, value: {foo: true}});
expect(otherRenderCaptures.length).toEqual(0);
firstRenderCaptures.length = 0;
styleMapFactory.value = {height: '100px'};
classMapFactory.value = {bar: false};
widthFactory.value = '50px';
fooFactory.value = false;
styleMapFactory = bindPlayerFactory(buildFn, {height: '100px'}) as BoundPlayerFactory<any>;
classMapFactory = bindPlayerFactory(buildFn, {bar: false}) as BoundPlayerFactory<any>;
widthFactory = bindPlayerFactory(buildFn, '50px') as BoundPlayerFactory<any>;
fooFactory = bindPlayerFactory(buildFn, false) as BoundPlayerFactory<any>;
fixture.update();
expect(firstRenderCaptures.length).toEqual(0);
expect(otherRenderCaptures.length).toEqual(4);
expect(otherRenderCaptures[0]).toEqual({type: BindingType.Class, value: {bar: false}});
expect(otherRenderCaptures[1]).toEqual({type: BindingType.Style, value: {height: '100px'}});
expect(otherRenderCaptures[2]).toEqual({type: BindingType.Style, value: {width: '50px'}});
expect(otherRenderCaptures[3]).toEqual({type: BindingType.Class, value: {foo: false}});
});
it('should render styling players on both template and directive host bindings', () => {
const players: MockPlayer[] = [];
const styleBuildFn = (element: HTMLElement, type: BindingType, value: any) => {
const player = new MockPlayer();
player.data = value;
players.push(player);
return player;
};
const classBuildFn = (element: HTMLElement, type: BindingType, value: any) => {
const player = new MockPlayer();
player.data = value;
players.push(player);
return player;
};
const widthFactory1 = bindPlayerFactory(styleBuildFn, '100px');
const widthFactory2 = bindPlayerFactory(styleBuildFn, '200px');
const fooFactory1 = bindPlayerFactory(classBuildFn, true);
const fooFactory2 = bindPlayerFactory(classBuildFn, true);
class MyDir {
static ngDirectiveDef = ɵɵdefineDirective({
type: MyDir,
selectors: [['', 'my-dir', '']],
factory: () => new MyDir(),
hostBindings: function(rf: RenderFlags, ctx: MyDir, elementIndex: number) {
if (rf & RenderFlags.Create) {
ɵɵstyling(['foo'], ['width']);
}
if (rf & RenderFlags.Update) {
ɵɵstyleProp(0, ctx.widthFactory);
ɵɵclassProp(0, ctx.fooFactory);
ɵɵstylingApply();
}
}
});
widthFactory = widthFactory2;
fooFactory = fooFactory2;
}
class Comp {
static ngComponentDef = ɵɵdefineComponent({
type: Comp,
selectors: [['comp']],
directives: [MyDir],
factory: () => new Comp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div', ['my-dir', '']);
ɵɵstyling(['foo'], ['width']);
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵstyleProp(0, ctx.widthFactory);
ɵɵclassProp(0, ctx.fooFactory);
ɵɵstylingApply();
}
}
});
widthFactory: any = widthFactory1;
fooFactory: any = fooFactory1;
}
const fixture = new ComponentFixture(Comp);
const component = fixture.component;
fixture.update();
expect(players.length).toEqual(2);
const [p1, p2] = players;
players.length = 0;
expect(p1.data).toEqual({width: '100px'});
expect(p2.data).toEqual({foo: true});
component.fooFactory = null;
component.widthFactory = null;
fixture.update();
expect(players.length).toEqual(2);
const [p3, p4] = players;
expect(p3.data).toEqual({width: '200px'});
expect(p4.data).toEqual({foo: true});
});
});
});

View File

@ -258,7 +258,7 @@ class CompWithStyling {
template: (rf: RenderFlags, ctx: CompWithStyling) => {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
ɵɵstyling(['fooClass']);
ɵɵstyling();
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {

View File

@ -8,8 +8,8 @@
import {normalizeIntoStylingMap as createMap} from '../../../src/render3/styling_next/map_based_bindings';
describe('map-based bindings', () => {
describe('LStylingMap construction', () => {
it('should create a new LStylingMap instance from a given value', () => {
describe('StylingMapArray construction', () => {
it('should create a new StylingMapArray instance from a given value', () => {
createAndAssertValues(null, []);
createAndAssertValues(undefined, []);
createAndAssertValues({}, []);
@ -30,7 +30,7 @@ describe('map-based bindings', () => {
expect(map2).toEqual([value2, 'maybe', true, 'no', true, 'yes', true]);
});
it('should patch an existing LStylingMap entry with new values and retain the alphabetical order',
it('should patch an existing StylingMapArray entry with new values and retain the alphabetical order',
() => {
const value1 = {color: 'red'};
const map1 = createMap(null, value1);
@ -52,7 +52,7 @@ describe('map-based bindings', () => {
expect(map4).toEqual([value4, 'everyonesClass', true, 'myClass', true, 'yourClass', true]);
});
it('should nullify old values that are not apart of the new set of values', () => {
it('should nullify old values that are not a part of the new set of values', () => {
const value1 = {color: 'red', fontSize: '20px'};
const map1 = createMap(null, value1);
expect(map1).toEqual([value1, 'color', 'red', 'fontSize', '20px']);
@ -70,6 +70,12 @@ describe('map-based bindings', () => {
const map4 = createMap(map3, value4);
expect(map4).toEqual([value4, 'apple', true, 'banana', true, 'orange', null]);
});
it('should hyphenate property names ', () => {
const value1 = {fontSize: '50px', paddingTopLeft: '20px'};
const map1 = createMap(null, value1, true);
expect(map1).toEqual([value1, 'font-size', '50px', 'padding-top-left', '20px']);
});
});
});

View File

@ -47,6 +47,23 @@ describe('styling context', () => {
});
});
it('should only register the same binding index once per property', () => {
const debug = makeContextWithDebug();
const context = debug.context;
expect(debug.entries).toEqual({});
registerBinding(context, 1, 'width', 123);
registerBinding(context, 1, 'width', 123);
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 2,
sanitizationRequired: false,
guardMask: buildGuardMask(1),
defaultValue: null,
sources: [123, null],
});
});
it('should overwrite a default value for an entry only if it is non-null', () => {
const debug = makeContextWithDebug();
const context = debug.context;

View File

@ -20,11 +20,6 @@ describe('view_utils', () => {
const lContainer = createLContainer(lView, lView, div, tNode, true);
const styleContext = createEmptyStylingContext(lContainer, null, null, null);
expect(unwrapRNode(styleContext)).toBe(div);
expect(unwrapStylingContext(styleContext)).toBe(styleContext);
expect(unwrapLContainer(styleContext)).toBe(lContainer);
expect(unwrapLView(styleContext)).toBe(lView);
expect(isLView(lView)).toBe(true);
expect(isLView(lContainer)).toBe(false);
expect(isLView(styleContext)).toBe(false);

View File

@ -715,8 +715,8 @@ export interface ɵɵBaseDef<T> {
}
export declare function ɵɵclassMap(classes: {
[styleName: string]: any;
} | string | null): void;
[className: string]: any;
} | NO_CHANGE | string | null): void;
export declare function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void;
@ -736,7 +736,7 @@ export declare function ɵɵclassMapInterpolate8(prefix: string, v0: any, i0: st
export declare function ɵɵclassMapInterpolateV(values: any[]): void;
export declare function ɵɵclassProp(classIndex: number, value: boolean | PlayerFactory, forceOverride?: boolean): void;
export declare function ɵɵclassProp(className: string, value: boolean | null): void;
export declare type ɵɵComponentDefWithMeta<T, Selector extends String, ExportAs extends string[], InputMap extends {
[key: string]: string;
@ -1059,27 +1059,29 @@ export declare function ɵɵstyleMap(styles: {
[styleName: string]: any;
} | NO_CHANGE | null): void;
export declare function ɵɵstyleProp(styleIndex: number, value: string | number | String | PlayerFactory | null, suffix?: string | null, forceOverride?: boolean): void;
export declare function ɵɵstyleProp(prop: string, value: string | number | String | null, suffix?: string | null): void;
export declare function ɵɵstylePropInterpolate1(styleIndex: number, prefix: string, v0: any, suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate1(prop: string, prefix: string, v0: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate2(styleIndex: number, prefix: string, v0: any, i0: string, v1: any, suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate2(prop: string, prefix: string, v0: any, i0: string, v1: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate3(styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate3(prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate4(styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate4(prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate5(styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate5(prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate6(styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate6(prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate7(styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate7(prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate8(styleIndex: number, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any, suffix: string, valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009;
export declare function ɵɵstylePropInterpolate8(prop: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any, suffix: string, valueSuffix?: string | null): TsickleIssue1009;
export declare function ɵɵstylePropInterpolateV(styleIndex: number, values: any[], valueSuffix?: string | null, forceOverride?: boolean): TsickleIssue1009;
export declare function ɵɵstylePropInterpolateV(prop: string, values: any[], valueSuffix?: string | null): TsickleIssue1009;
export declare function ɵɵstyling(classBindingNames?: string[] | null, styleBindingNames?: string[] | null, styleSanitizer?: StyleSanitizeFn | null): void;
export declare function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void;
export declare function ɵɵstyling(): void;
export declare function ɵɵstylingApply(): void;