refactor(ivy): change styling to use programmatic API on updates (#34804)

Previously we would write to class/style as strings `element.className` and `element.style.cssText`. Turns out that approach is good for initial render but not good for updates. Updates using this approach are problematic because we have to check to see if there was an out of bound write to style and than perform reconciliation. This also requires the browser to bring up CSS parser which is expensive.

Another problem with old approach is that we had to queue the DOM writes and flush them twice. Once on element advance instruction and once in `hostBindings`. The double flushing is expensive but it also means that a directive can observe that styles are not yet written (they are written after directive executes.)

The new approach uses `element.classList.add/remove` and `element.style.setProperty/removeProperty` API for updates only (it continues to use `element.className` and `element.style.cssText` for initial render as it is cheaper.) The other change is that the styling changes are applied immediately (no queueing). This means that it is the instruction which computes priority. In some circumstances it may result in intermediate writes which are than overwritten with new value. (This should be rare)

Overall this change deletes most of the previous code and replaces it with new simplified implement. The simplification results in code savings.

PR Close #34804
This commit is contained in:
Miško Hevery
2020-01-15 16:52:54 -08:00
parent 3bbe1d9f30
commit 2e1a16bf95
42 changed files with 1172 additions and 2497 deletions

View File

@ -222,8 +222,8 @@ describe('styling', () => {
return;
@Component({
template: `
<div [style.--my-var]=" 'rgb(255, 0, 0)' ">
<span style="background-color: var(--my-var)">CONTENT</span>
<div [style.--my-var]=" '100px' ">
<span style="width: var(--my-var)">CONTENT</span>
</div>`
})
class Cmp {
@ -234,7 +234,7 @@ describe('styling', () => {
fixture.detectChanges();
const span = fixture.nativeElement.querySelector('span') as HTMLElement;
expect(getComputedStyle(span).getPropertyValue('background-color')).toEqual('rgb(255, 0, 0)');
expect(getComputedStyle(span).getPropertyValue('width')).toEqual('100px');
});
});
@ -1405,7 +1405,7 @@ describe('styling', () => {
expect(element.style.fontSize).toEqual('100px');
// once for the template flush and again for the host bindings
expect(ngDevMode !.flushStyling).toEqual(2);
expect(ngDevMode !.rendererSetStyle).toEqual(4);
ngDevModeResetPerfCounters();
component.opacity = '0.6';
@ -1420,7 +1420,7 @@ describe('styling', () => {
expect(element.style.fontSize).toEqual('50px');
// once for the template flush and again for the host bindings
expect(ngDevMode !.flushStyling).toEqual(2);
expect(ngDevMode !.rendererSetStyle).toEqual(4);
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
@ -1692,7 +1692,7 @@ describe('styling', () => {
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
assertStyleCounters(1, 0);
assertStyleCounters(4, 0);
assertStyle(element, 'width', '111px');
assertStyle(element, 'height', '111px');
@ -1754,11 +1754,11 @@ describe('styling', () => {
assertStyle(element, 'width', '0px');
assertStyle(element, 'height', '123px');
comp.dir.map = {width: '1000px', height: '1000px', color: 'red'};
comp.dir.map = {width: '1000px', height: '1100px', color: 'red'};
ngDevModeResetPerfCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
assertStyleCounters(2, 0);
assertStyle(element, 'width', '1000px');
assertStyle(element, 'height', '123px');
assertStyle(element, 'color', 'red');
@ -1771,16 +1771,16 @@ describe('styling', () => {
// values get applied
assertStyleCounters(1, 0);
assertStyle(element, 'width', '1000px');
assertStyle(element, 'height', '1000px');
assertStyle(element, 'height', '1100px');
assertStyle(element, 'color', 'red');
comp.map = {color: 'blue', width: '2000px', opacity: '0.5'};
ngDevModeResetPerfCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
assertStyleCounters(3, 0);
assertStyle(element, 'width', '2000px');
assertStyle(element, 'height', '1000px');
assertStyle(element, 'height', '1100px');
assertStyle(element, 'color', 'blue');
assertStyle(element, 'opacity', '0.5');
@ -1789,22 +1789,20 @@ describe('styling', () => {
fixture.detectChanges();
// all four are applied because the map was altered
// TODO: temporary dissable as it fails in IE. Re-enabled in #34804
// assertStyleCounters(1, 0);
assertStyleCounters(0, 1);
assertStyle(element, 'width', '2000px');
assertStyle(element, 'height', '1000px');
assertStyle(element, 'height', '1100px');
assertStyle(element, 'color', 'blue');
assertStyle(element, 'opacity', '');
});
onlyInIvy('only ivy has [style] support')
onlyInIvy('only ivy has [style.prop] support')
.it('should sanitize style values before writing them', () => {
@Component({
template: `
<div [style.width]="widthExp"
[style.background-image]="bgImageExp"
[style]="styleMapExp"></div>
`
<div [style.width]="widthExp"
[style.background-image]="bgImageExp"></div>
`
})
class Cmp {
widthExp = '';
@ -1823,23 +1821,55 @@ describe('styling', () => {
fixture.detectChanges();
// for some reasons `background-image: unsafe` is suppressed
expect(getSortedStyle(div)).toEqual('');
// for some reasons `border-image: unsafe` is NOT suppressed
comp.styleMapExp = {'filter': 'url("javascript:border")'};
fixture.detectChanges();
expect(getSortedStyle(div)).not.toContain('javascript');
// Prove that bindings work.
comp.widthExp = '789px';
comp.bgImageExp = bypassSanitizationTrustStyle(comp.bgImageExp) as string;
fixture.detectChanges();
expect(div.style.getPropertyValue('background-image')).toEqual('url("javascript:img")');
expect(div.style.getPropertyValue('width')).toEqual('789px');
});
onlyInIvy('only ivy has [style] support')
.it('should sanitize style values before writing them', () => {
@Component({
template: `
<div [style.width]="widthExp"
[style]="styleMapExp"></div>
`
})
class Cmp {
widthExp = '';
styleMapExp: {[key: string]: any} = {};
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
comp.styleMapExp['background-image'] = 'url("javascript:img")';
fixture.detectChanges();
// for some reasons `background-image: unsafe` is suppressed
expect(getSortedStyle(div)).toEqual('');
// for some reasons `border-image: unsafe` is NOT suppressed
fixture.detectChanges();
expect(getSortedStyle(div)).not.toContain('javascript');
// Prove that bindings work.
comp.widthExp = '789px';
comp.styleMapExp = {
'filter': bypassSanitizationTrustStyle(comp.styleMapExp['filter']) as string
'background-image': bypassSanitizationTrustStyle(comp.styleMapExp['background-image'])
};
fixture.detectChanges();
expect(div.style.getPropertyValue('background-image')).toEqual('url("javascript:img")');
// Some browsers strip `url` on filter so we use `toContain`
expect(div.style.getPropertyValue('filter')).toContain('javascript:border');
expect(div.style.getPropertyValue('width')).toEqual('789px');
});
@ -2887,30 +2917,25 @@ describe('styling', () => {
expect(classList.contains('barFoo')).toBeTruthy();
});
// onlyInIvy('[style] bindings are ivy only')
xit('should convert camelCased style property names to snake-case', () => {
// TODO(misko): Temporarily disabled in this PR renabled in
// https://github.com/angular/angular/pull/34616
// Current implementation uses strings to write to DOM. Because of that it does not convert
// property names from camelCase to dash-case. This is rectified in #34616 because we switch
// from string API to `element.style.setProperty` API.
@Component({template: `<div [style]="myStyles"></div>`})
class MyComp {
myStyles = {};
}
onlyInIvy('[style] bindings are ivy only')
.it('should convert camelCased style property names to snake-case', () => {
@Component({template: `<div [style]="myStyles"></div>`})
class MyComp {
myStyles = {};
}
TestBed.configureTestingModule({
declarations: [MyComp],
});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
TestBed.configureTestingModule({
declarations: [MyComp],
});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div') as HTMLDivElement;
fixture.componentInstance.myStyles = {fontSize: '200px'};
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div') as HTMLDivElement;
fixture.componentInstance.myStyles = {fontSize: '200px'};
fixture.detectChanges();
expect(div.style.getPropertyValue('font-size')).toEqual('200px');
});
expect(div.style.getPropertyValue('font-size')).toEqual('200px');
});
it('should recover from an error thrown in styling bindings', () => {
let raiseWidthError = false;
@ -3202,8 +3227,7 @@ describe('styling', () => {
expect(element.classList.contains('parent-comp-active')).toBeFalsy();
});
// TODO(FW-1360): re-enable this test once the new styling changes are in place.
xit('should not set inputs called class if they are not being used in the template', () => {
it('should not set inputs called class if they are not being used in the template', () => {
const logs: string[] = [];
@Directive({selector: '[test]'})

View File

@ -257,9 +257,6 @@
{
"name": "executeContentQueries"
},
{
"name": "executeElementExitFn"
},
{
"name": "executeInitAndCheckHooks"
},
@ -488,9 +485,6 @@
{
"name": "leaveView"
},
{
"name": "leaveViewProcessExit"
},
{
"name": "locateHostElement"
},
@ -585,7 +579,7 @@
"name": "setBindingIndex"
},
{
"name": "setBindingRoot"
"name": "setBindingRootForHostBindings"
},
{
"name": "setCurrentQueryIndex"

View File

@ -221,9 +221,6 @@
{
"name": "executeCheckHooks"
},
{
"name": "executeElementExitFn"
},
{
"name": "executeInitAndCheckHooks"
},
@ -383,9 +380,6 @@
{
"name": "leaveView"
},
{
"name": "leaveViewProcessExit"
},
{
"name": "locateHostElement"
},
@ -459,7 +453,7 @@
"name": "setBindingIndex"
},
{
"name": "setBindingRoot"
"name": "setBindingRootForHostBindings"
},
{
"name": "setCurrentQueryIndex"

View File

@ -47,6 +47,9 @@
{
"name": "EMPTY_ARRAY"
},
{
"name": "EMPTY_ARRAY"
},
{
"name": "EMPTY_OBJ"
},
@ -74,9 +77,6 @@
{
"name": "HOST"
},
{
"name": "IGNORE_DUE_TO_INPUT_SHADOW"
},
{
"name": "INJECTOR"
},
@ -287,6 +287,9 @@
{
"name": "__window"
},
{
"name": "_arrayIndexOfSorted"
},
{
"name": "_currentInjector"
},
@ -323,9 +326,6 @@
{
"name": "appendChild"
},
{
"name": "appendStyling"
},
{
"name": "applyContainer"
},
@ -335,12 +335,27 @@
{
"name": "applyProjectionRecursive"
},
{
"name": "applyStyling"
},
{
"name": "applyToElementOrContainer"
},
{
"name": "applyView"
},
{
"name": "arrayInsert2"
},
{
"name": "arrayMapGet"
},
{
"name": "arrayMapIndexOf"
},
{
"name": "arrayMapSet"
},
{
"name": "assertTemplate"
},
@ -389,15 +404,9 @@
{
"name": "collectNativeNodes"
},
{
"name": "computeClassChanges"
},
{
"name": "computeStaticStyling"
},
{
"name": "computeStyleChanges"
},
{
"name": "concatStringsWithSpace"
},
@ -518,9 +527,6 @@
{
"name": "executeContentQueries"
},
{
"name": "executeElementExitFn"
},
{
"name": "executeInitAndCheckHooks"
},
@ -554,15 +560,12 @@
{
"name": "findExistingListener"
},
{
"name": "findStylingValue"
},
{
"name": "findViaComponent"
},
{
"name": "flushStyleBinding"
},
{
"name": "flushStylingOnElementExit"
},
{
"name": "forwardRef"
},
@ -584,9 +587,6 @@
{
"name": "getCheckNoChangesMode"
},
{
"name": "getClassBindingChanged"
},
{
"name": "getCleanup"
},
@ -656,9 +656,6 @@
{
"name": "getLastParsedKey"
},
{
"name": "getLastParsedValue"
},
{
"name": "getNameOnlyMarkerIndex"
},
@ -728,9 +725,6 @@
{
"name": "getSelectedIndex"
},
{
"name": "getStyleBindingChanged"
},
{
"name": "getSymbolIterator"
},
@ -740,15 +734,15 @@
{
"name": "getTStylingRangeNext"
},
{
"name": "getTStylingRangeNextDuplicate"
},
{
"name": "getTStylingRangePrev"
},
{
"name": "getTStylingRangePrevDuplicate"
},
{
"name": "getTStylingRangeTail"
},
{
"name": "getTViewCleanup"
},
@ -797,6 +791,9 @@
{
"name": "initializeInputAndOutputAliases"
},
{
"name": "initializeStylingStaticArrayMap"
},
{
"name": "injectElementRef"
},
@ -842,9 +839,6 @@
{
"name": "invokeHostBindingsInCreationMode"
},
{
"name": "isActiveHostElement"
},
{
"name": "isAnimationProp"
},
@ -908,6 +902,9 @@
{
"name": "isRootView"
},
{
"name": "isStylingValuePresent"
},
{
"name": "iterateListLike"
},
@ -917,9 +914,6 @@
{
"name": "leaveView"
},
{
"name": "leaveViewProcessExit"
},
{
"name": "listenerInternal"
},
@ -956,9 +950,6 @@
{
"name": "markDuplicates"
},
{
"name": "markStylingBindingDirty"
},
{
"name": "markViewDirty"
},
@ -1004,15 +995,15 @@
{
"name": "noSideEffects"
},
{
"name": "normalizeAndApplySuffixOrSanitizer"
},
{
"name": "parseClassName"
},
{
"name": "parseClassNameNext"
},
{
"name": "parseKeyValue"
},
{
"name": "parseStyle"
},
@ -1022,24 +1013,12 @@
{
"name": "parserState"
},
{
"name": "processClassToken"
},
{
"name": "processStyleKeyValue"
},
{
"name": "readPatchedData"
},
{
"name": "readPatchedLView"
},
{
"name": "reconcileClassNames"
},
{
"name": "reconcileStyleNames"
},
{
"name": "refreshChildComponents"
},
@ -1070,9 +1049,6 @@
{
"name": "removeListeners"
},
{
"name": "removeStyle"
},
{
"name": "removeView"
},
@ -1131,7 +1107,7 @@
"name": "setBindingIndex"
},
{
"name": "setBindingRoot"
"name": "setBindingRootForHostBindings"
},
{
"name": "setCheckNoChangesMode"
@ -1142,9 +1118,6 @@
{
"name": "setDirectiveInputsWhichShadowsStyling"
},
{
"name": "setElementExitFn"
},
{
"name": "setHostBindingsByExecutingExpandoInstructions"
},
@ -1190,9 +1163,6 @@
{
"name": "shouldSearchParent"
},
{
"name": "splitClassList"
},
{
"name": "storeCleanupFn"
},
@ -1202,9 +1172,6 @@
{
"name": "stringifyForError"
},
{
"name": "styleKeyValue"
},
{
"name": "stylingPropertyFirstUpdatePass"
},
@ -1226,9 +1193,6 @@
{
"name": "toTStylingRange"
},
{
"name": "toggleClass"
},
{
"name": "trackByIdentity"
},
@ -1241,6 +1205,9 @@
{
"name": "unwrapSafeValue"
},
{
"name": "updateStyling"
},
{
"name": "viewAttachedToChangeDetector"
},
@ -1253,12 +1220,6 @@
{
"name": "wrapListener"
},
{
"name": "writeAndReconcileClass"
},
{
"name": "writeAndReconcileStyle"
},
{
"name": "writeDirectClass"
},

View File

@ -17,7 +17,7 @@ import {TNODE} from '../../src/render3/interfaces/injector';
import {TNodeType} from '../../src/render3/interfaces/node';
import {isProceduralRenderer} from '../../src/render3/interfaces/renderer';
import {LViewFlags, TVIEW, TViewType} from '../../src/render3/interfaces/view';
import {enterView, leaveViewProcessExit} from '../../src/render3/state';
import {enterView, leaveView} from '../../src/render3/state';
import {getRendererFactory2} from './imported_renderer2';
import {ComponentFixture, createComponent, createDirective} from './render_util';
@ -237,7 +237,7 @@ describe('di', () => {
const injector = getOrCreateNodeInjectorForNode(parentTNode, contentView);
expect(injector).not.toEqual(-1);
} finally {
leaveViewProcessExit();
leaveView();
}
});
});

View File

@ -11,7 +11,7 @@ import {createTNode, createTView} from '@angular/core/src/render3/instructions/s
import {TNodeType} from '@angular/core/src/render3/interfaces/node';
import {LView, TView, TViewType} from '@angular/core/src/render3/interfaces/view';
import {enterView, leaveView} from '@angular/core/src/render3/state';
import {CLASS_MAP_STYLING_KEY, STYLE_MAP_STYLING_KEY, insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list';
import {insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list';
describe('lView_debug', () => {
@ -98,13 +98,13 @@ describe('lView_debug', () => {
}
]);
insertTStylingBinding(tView.data, tNode, STYLE_MAP_STYLING_KEY, 6, true, true);
insertTStylingBinding(tView.data, tNode, CLASS_MAP_STYLING_KEY, 8, true, false);
insertTStylingBinding(tView.data, tNode, null, 6, true, true);
insertTStylingBinding(tView.data, tNode, null, 8, true, false);
expect(tNode.styleBindings_).toEqual([
null, {
index: 8,
key: CLASS_MAP_STYLING_KEY,
key: null,
isTemplate: false,
prevDuplicate: false,
nextDuplicate: true,
@ -124,7 +124,7 @@ describe('lView_debug', () => {
expect(tNode.classBindings_).toEqual([
'STATIC', {
index: 6,
key: STYLE_MAP_STYLING_KEY,
key: null,
isTemplate: false,
prevDuplicate: true,
nextDuplicate: true,

View File

@ -0,0 +1,53 @@
/**
* @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
*/
import {createLView, createTNode, createTView} from '@angular/core/src/render3/instructions/shared';
import {TNodeType} from '@angular/core/src/render3/interfaces/node';
import {domRendererFactory3} from '@angular/core/src/render3/interfaces/renderer';
import {HEADER_OFFSET, LViewFlags, TVIEW, TViewType} from '@angular/core/src/render3/interfaces/view';
import {enterView, getBindingRoot, getLView, setBindingIndex} from '@angular/core/src/render3/state';
/**
* Setups a simple `LView` so that it is possible to do unit tests on instructions.
*
* ```
* describe('styling', () => {
* beforeEach(enterViewWithOneDiv);
* afterEach(leaveView);
*
* it('should ...', () => {
* expect(getLView()).toBeDefined();
* const div = getNativeByIndex(1, getLView());
* });
* });
* ```
*/
export function enterViewWithOneDiv() {
const renderer = domRendererFactory3.createRenderer(null, null);
const div = renderer.createElement('div');
const tView =
createTView(TViewType.Component, -1, emptyTemplate, 1, 10, null, null, null, null, null);
const tNode = tView.firstChild = createTNode(tView, null !, TNodeType.Element, 0, 'div', null);
const lView = createLView(
null, tView, null, LViewFlags.CheckAlways, null, null, domRendererFactory3, renderer, null,
null);
lView[0 + HEADER_OFFSET] = div;
tView.data[0 + HEADER_OFFSET] = tNode;
enterView(lView, tNode);
}
export function clearFirstUpdatePass() {
getLView()[TVIEW].firstUpdatePass = false;
}
export function rewindBindingIndex() {
setBindingIndex(getBindingRoot());
}
function emptyTemplate() {}

View File

@ -0,0 +1,358 @@
/**
* @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
*/
import {classStringParser, initializeStylingStaticArrayMap, styleStringParser, toStylingArrayMap, ɵɵclassProp, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer} from '@angular/core/src/render3/instructions/styling';
import {AttributeMarker} from '@angular/core/src/render3/interfaces/node';
import {TVIEW} from '@angular/core/src/render3/interfaces/view';
import {getLView, leaveView} from '@angular/core/src/render3/state';
import {getNativeByIndex} from '@angular/core/src/render3/util/view_utils';
import {bypassSanitizationTrustStyle} from '@angular/core/src/sanitization/bypass';
import {ɵɵsanitizeStyle} from '@angular/core/src/sanitization/sanitization';
import {arrayMapSet} from '@angular/core/src/util/array_utils';
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
import {getElementClasses, getElementStyles} from '@angular/core/testing/src/styling';
import {expect} from '@angular/core/testing/src/testing_internal';
import {clearFirstUpdatePass, enterViewWithOneDiv, rewindBindingIndex} from './shared_spec';
describe('styling', () => {
beforeEach(enterViewWithOneDiv);
afterEach(leaveView);
let div !: HTMLElement;
beforeEach(() => div = getNativeByIndex(0, getLView()) as HTMLElement);
it('should do set basic style', () => {
ɵɵstyleProp('color', 'red');
expectStyle(div).toEqual({color: 'red'});
});
it('should search across multiple instructions backwards', () => {
ɵɵstyleProp('color', 'red');
ɵɵstyleProp('color', undefined);
ɵɵstyleProp('color', 'blue');
expectStyle(div).toEqual({color: 'blue'});
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵstyleProp('color', 'red');
ɵɵstyleProp('color', undefined);
ɵɵstyleProp('color', undefined);
expectStyle(div).toEqual({color: 'red'});
});
it('should search across multiple instructions forwards', () => {
ɵɵstyleProp('color', 'red');
ɵɵstyleProp('color', 'green');
ɵɵstyleProp('color', 'blue');
expectStyle(div).toEqual({color: 'blue'});
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵstyleProp('color', 'white');
expectStyle(div).toEqual({color: 'blue'});
});
it('should set style based on priority', () => {
ɵɵstyleProp('color', 'red');
ɵɵstyleProp('color', 'blue'); // Higher priority, should win.
expectStyle(div).toEqual({color: 'blue'});
// The intermediate value has to set since it does not know if it is last one.
expect(ngDevMode !.rendererSetStyle).toEqual(2);
ngDevModeResetPerfCounters();
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵstyleProp('color', 'red'); // no change
ɵɵstyleProp('color', 'green'); // change to green
expectStyle(div).toEqual({color: 'green'});
expect(ngDevMode !.rendererSetStyle).toEqual(1);
ngDevModeResetPerfCounters();
rewindBindingIndex();
ɵɵstyleProp('color', 'black'); // First binding update
expectStyle(div).toEqual({color: 'green'}); // Green still has priority.
expect(ngDevMode !.rendererSetStyle).toEqual(0);
ngDevModeResetPerfCounters();
rewindBindingIndex();
ɵɵstyleProp('color', 'black'); // no change
ɵɵstyleProp('color', undefined); // Clear second binding
expectStyle(div).toEqual({color: 'black'}); // default to first binding
expect(ngDevMode !.rendererSetStyle).toEqual(1);
ngDevModeResetPerfCounters();
rewindBindingIndex();
ɵɵstyleProp('color', null);
expectStyle(div).toEqual({}); // default to first binding
expect(ngDevMode !.rendererSetStyle).toEqual(0);
expect(ngDevMode !.rendererRemoveStyle).toEqual(1);
});
it('should set class based on priority', () => {
ɵɵclassProp('foo', false);
ɵɵclassProp('foo', true); // Higher priority, should win.
expectClass(div).toEqual({foo: true});
expect(ngDevMode !.rendererAddClass).toEqual(1);
ngDevModeResetPerfCounters();
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵclassProp('foo', false); // no change
ɵɵclassProp('foo', undefined); // change (have no opinion)
expectClass(div).toEqual({});
expect(ngDevMode !.rendererAddClass).toEqual(0);
expect(ngDevMode !.rendererRemoveClass).toEqual(1);
ngDevModeResetPerfCounters();
rewindBindingIndex();
ɵɵclassProp('foo', false); // no change
ɵɵclassProp('foo', 'truthy' as any);
expectClass(div).toEqual({foo: true});
rewindBindingIndex();
ɵɵclassProp('foo', true); // change
ɵɵclassProp('foo', undefined); // change
expectClass(div).toEqual({foo: true});
});
describe('styleMap', () => {
it('should work with maps', () => {
ɵɵstyleMap({});
expectStyle(div).toEqual({});
expect(ngDevMode !.rendererSetStyle).toEqual(0);
expect(ngDevMode !.rendererRemoveStyle).toEqual(0);
ngDevModeResetPerfCounters();
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵstyleMap({color: 'blue'});
expectStyle(div).toEqual({color: 'blue'});
expect(ngDevMode !.rendererSetStyle).toEqual(1);
expect(ngDevMode !.rendererRemoveStyle).toEqual(0);
ngDevModeResetPerfCounters();
rewindBindingIndex();
ɵɵstyleMap({color: 'red'});
expectStyle(div).toEqual({color: 'red'});
expect(ngDevMode !.rendererSetStyle).toEqual(1);
expect(ngDevMode !.rendererRemoveStyle).toEqual(0);
ngDevModeResetPerfCounters();
rewindBindingIndex();
ɵɵstyleMap({color: null, width: '100px'});
expectStyle(div).toEqual({width: '100px'});
expect(ngDevMode !.rendererSetStyle).toEqual(1);
expect(ngDevMode !.rendererRemoveStyle).toEqual(1);
ngDevModeResetPerfCounters();
});
it('should work with object literal and strings', () => {
ɵɵstyleMap('');
expectStyle(div).toEqual({});
expect(ngDevMode !.rendererSetStyle).toEqual(0);
expect(ngDevMode !.rendererRemoveStyle).toEqual(0);
ngDevModeResetPerfCounters();
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵstyleMap('color: blue');
expectStyle(div).toEqual({color: 'blue'});
expect(ngDevMode !.rendererSetStyle).toEqual(1);
expect(ngDevMode !.rendererRemoveStyle).toEqual(0);
ngDevModeResetPerfCounters();
rewindBindingIndex();
ɵɵstyleMap('color: red');
expectStyle(div).toEqual({color: 'red'});
expect(ngDevMode !.rendererSetStyle).toEqual(1);
expect(ngDevMode !.rendererRemoveStyle).toEqual(0);
ngDevModeResetPerfCounters();
rewindBindingIndex();
ɵɵstyleMap('width: 100px');
expectStyle(div).toEqual({width: '100px'});
expect(ngDevMode !.rendererSetStyle).toEqual(1);
expect(ngDevMode !.rendererRemoveStyle).toEqual(1);
ngDevModeResetPerfCounters();
});
it('should collaborate with properties', () => {
ɵɵstyleProp('color', 'red');
ɵɵstyleMap({color: 'blue', width: '100px'});
expectStyle(div).toEqual({color: 'blue', width: '100px'});
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵstyleProp('color', 'red');
ɵɵstyleMap({width: '200px'});
expectStyle(div).toEqual({color: 'red', width: '200px'});
});
it('should collaborate with other maps', () => {
ɵɵstyleMap('color: red');
ɵɵstyleMap({color: 'blue', width: '100px'});
expectStyle(div).toEqual({color: 'blue', width: '100px'});
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵstyleMap('color: red');
ɵɵstyleMap({width: '200px'});
expectStyle(div).toEqual({color: 'red', width: '200px'});
});
describe('suffix', () => {
it('should append suffix', () => {
ɵɵstyleProp('width', 200, 'px');
ɵɵstyleProp('width', 100, 'px');
expectStyle(div).toEqual({width: '100px'});
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵstyleProp('width', 200, 'px');
ɵɵstyleProp('width', undefined, 'px');
expectStyle(div).toEqual({width: '200px'});
});
it('should append suffix and non-suffix bindings', () => {
ɵɵstyleProp('width', 200, 'px');
ɵɵstyleProp('width', '100px');
expectStyle(div).toEqual({width: '100px'});
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵstyleProp('width', 200, 'px');
ɵɵstyleProp('width', undefined, 'px');
expectStyle(div).toEqual({width: '200px'});
});
});
describe('sanitization', () => {
it('should sanitize property', () => {
ɵɵstyleSanitizer(ɵɵsanitizeStyle);
ɵɵstyleProp('background', 'url("javascript:/unsafe")');
expect(div.style.getPropertyValue('background')).not.toContain('javascript');
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵstyleProp('background', bypassSanitizationTrustStyle('url("javascript:/trusted")'));
expect(div.style.getPropertyValue('background')).toContain('url("javascript:/trusted")');
});
it('should sanitize map', () => {
ɵɵstyleSanitizer(ɵɵsanitizeStyle);
ɵɵstyleMap('background: url("javascript:/unsafe")');
expect(div.style.getPropertyValue('background')).not.toContain('javascript');
clearFirstUpdatePass();
rewindBindingIndex();
ɵɵstyleMap({'background': bypassSanitizationTrustStyle('url("javascript:/trusted")')});
expect(div.style.getPropertyValue('background')).toContain('url("javascript:/trusted")');
});
});
describe('populateStylingStaticArrayMap', () => {
it('should initialize to null if no mergedAttrs', () => {
const tNode = getLView()[TVIEW].firstChild !;
expect(tNode.stylesMap).toEqual(undefined);
expect(tNode.classesMap).toEqual(undefined);
initializeStylingStaticArrayMap(tNode);
expect(tNode.stylesMap).toEqual(null);
expect(tNode.classesMap).toEqual(null);
});
it('should initialize from mergeAttrs', () => {
const tNode = getLView()[TVIEW].firstChild !;
expect(tNode.stylesMap).toEqual(undefined);
expect(tNode.classesMap).toEqual(undefined);
tNode.mergedAttrs = [
'ignore', 'value', //
AttributeMarker.Classes, 'foo', 'bar', //
AttributeMarker.Styles, 'width', '0', 'color', 'red', //
];
initializeStylingStaticArrayMap(tNode);
expect(tNode.classesMap).toEqual(['bar', true, 'foo', true] as any);
expect(tNode.stylesMap).toEqual(['color', 'red', 'width', '0'] as any);
});
});
});
describe('toStylingArray', () => {
describe('falsy', () => {
it('should return empty ArrayMap', () => {
expect(toStylingArrayMap(arrayMapSet, null !, '')).toEqual([] as any);
expect(toStylingArrayMap(arrayMapSet, null !, null)).toEqual([] as any);
expect(toStylingArrayMap(arrayMapSet, null !, undefined)).toEqual([] as any);
expect(toStylingArrayMap(arrayMapSet, null !, [])).toEqual([] as any);
expect(toStylingArrayMap(arrayMapSet, null !, {})).toEqual([] as any);
});
describe('string', () => {
it('should parse classes', () => {
expect(toStylingArrayMap(arrayMapSet, classStringParser, ' ')).toEqual([] as any);
expect(toStylingArrayMap(arrayMapSet, classStringParser, ' X A ')).toEqual([
'A', true, 'X', true
] as any);
});
it('should parse styles', () => {
expect(toStylingArrayMap(arrayMapSet, styleStringParser, ' ')).toEqual([] as any);
expect(toStylingArrayMap(arrayMapSet, styleStringParser, 'B:b;A:a')).toEqual([
'A', 'a', 'B', 'b'
] as any);
});
});
describe('array', () => {
it('should parse', () => {
expect(toStylingArrayMap(arrayMapSet, null !, ['X', 'A'])).toEqual([
'A', true, 'X', true
] as any);
});
});
describe('object', () => {
it('should parse', () => {
expect(toStylingArrayMap(arrayMapSet, null !, {X: 'x', A: 'a'})).toEqual([
'A', 'a', 'X', 'x'
] as any);
});
});
describe('Map', () => {
it('should parse', () => {
expect(toStylingArrayMap(
arrayMapSet, null !, new Map<string, string>([['X', 'x'], ['A', 'a']])))
.toEqual(['A', 'a', 'X', 'x'] as any);
});
});
describe('Iterable', () => {
it('should parse', () => {
expect(toStylingArrayMap(arrayMapSet, null !, new Set<string>(['X', 'A']))).toEqual([
'A', true, 'X', true
] as any);
});
});
});
});
});
function expectStyle(element: HTMLElement) {
return expect(getElementStyles(element));
}
function expectClass(element: HTMLElement) {
return expect(getElementClasses(element));
}

View File

@ -216,16 +216,3 @@ ng_benchmark(
name = "duplicate_map_based_style_and_class_bindings",
bundle = ":duplicate_map_based_style_and_class_bindings_lib",
)
ng_rollup_bundle(
name = "split_class_list_lib",
entry_point = ":split_class_list.ts",
deps = [
":perf_lib",
],
)
ng_benchmark(
name = "split_class_list",
bundle = ":split_class_list_lib",
)

View File

@ -1,64 +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
*/
import {processClassToken, splitClassList} from '@angular/core/src/render3/styling/class_differ';
import {createBenchmark} from './micro_bench';
const benchmark = createBenchmark('split_class_list');
const splitTime = benchmark('String.split(" ")');
const splitRegexpTime = benchmark('String.split(/\\s+/)');
const splitClassListTime = benchmark('splitClassList');
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const CLASSES: string[] = [LETTERS];
for (let i = 0; i < LETTERS.length; i++) {
CLASSES.push(LETTERS.substring(0, i) + ' ' + LETTERS.substring(i, LETTERS.length));
}
let index = 0;
let changes = new Map<string, boolean|null>();
let parts: string[] = [];
while (splitTime()) {
changes = clearArray(changes);
const classes = CLASSES[index++];
parts = classes.split(' ');
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part !== '') {
processClassToken(changes, part, false);
}
}
if (index === CLASSES.length) index = 0;
}
const WHITESPACE = /\s+/m;
while (splitRegexpTime()) {
changes = clearArray(changes);
const classes = CLASSES[index++];
parts = classes.split(WHITESPACE);
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part !== '') {
processClassToken(changes, part, false);
}
}
if (index === CLASSES.length) index = 0;
}
while (splitClassListTime()) {
changes = clearArray(changes);
splitClassList(CLASSES[index++], changes, false);
if (index === CLASSES.length) index = 0;
}
benchmark.report();
function clearArray(a: Map<any, any>): any {
a.clear();
}

View File

@ -6,111 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {classIndexOf, computeClassChanges, splitClassList, toggleClass} from '../../../src/render3/styling/class_differ';
import {classIndexOf} from '../../../src/render3/styling/class_differ';
describe('class differ', () => {
describe('computeClassChanges', () => {
function expectComputeClassChanges(oldValue: string, newValue: string) {
const changes: (boolean | null | string)[] = [];
const newLocal = computeClassChanges(oldValue, newValue);
sortedForEach(newLocal, (value, key) => { changes.push(key, value); });
return expect(changes);
}
it('should detect no changes', () => {
expectComputeClassChanges('', '').toEqual([]);
expectComputeClassChanges('A', 'A').toEqual(['A', null]);
expectComputeClassChanges('A B', 'A B').toEqual(['A', null, 'B', null]);
});
it('should detect no changes when out of order', () => {
expectComputeClassChanges('A B', 'B A').toEqual(['A', null, 'B', null]);
expectComputeClassChanges('A B C', 'B C A').toEqual(['A', null, 'B', null, 'C', null]);
});
it('should detect additions', () => {
expectComputeClassChanges('A B', 'A B C').toEqual(['A', null, 'B', null, 'C', true]);
expectComputeClassChanges('Alpha Bravo', 'Bravo Alpha Charlie').toEqual([
'Alpha', null, 'Bravo', null, 'Charlie', true
]);
expectComputeClassChanges('A B ', 'C B A').toEqual(['A', null, 'B', null, 'C', true]);
});
it('should detect removals', () => {
expectComputeClassChanges('A B C', 'A B').toEqual(['A', null, 'B', null, 'C', false]);
expectComputeClassChanges('B A C', 'B A').toEqual(['A', null, 'B', null, 'C', false]);
expectComputeClassChanges('C B A', 'A B').toEqual(['A', null, 'B', null, 'C', false]);
});
it('should detect duplicates and ignore them', () => {
expectComputeClassChanges('A A B C', 'A B C').toEqual(['A', null, 'B', null, 'C', null]);
expectComputeClassChanges('A A B', 'A A C').toEqual(['A', null, 'B', false, 'C', true]);
});
});
describe('splitClassList', () => {
function expectSplitClassList(text: string) {
const changes: (boolean | null | string)[] = [];
const changesMap = new Map<string, boolean|null>();
splitClassList(text, changesMap, false);
changesMap.forEach((value, key) => changes.push(key, value));
return expect(changes);
}
it('should parse a list', () => {
expectSplitClassList('').toEqual([]);
expectSplitClassList('A').toEqual(['A', false]);
expectSplitClassList('A B').toEqual(['A', false, 'B', false]);
expectSplitClassList('Alpha Bravo').toEqual(['Alpha', false, 'Bravo', false]);
});
it('should ignore extra spaces', () => {
expectSplitClassList(' \n\r\t').toEqual([]);
expectSplitClassList(' A ').toEqual(['A', false]);
expectSplitClassList(' \n\r\t A \n\r\t B\n\r\t ').toEqual(['A', false, 'B', false]);
expectSplitClassList(' \n\r\t Alpha \n\r\t Bravo \n\r\t ').toEqual([
'Alpha', false, 'Bravo', false
]);
});
it('should remove duplicates', () => {
expectSplitClassList('').toEqual([]);
expectSplitClassList('A A').toEqual(['A', false]);
expectSplitClassList('A B B A').toEqual(['A', false, 'B', false]);
expectSplitClassList('Alpha Bravo Bravo Alpha').toEqual(['Alpha', false, 'Bravo', false]);
});
});
describe('toggleClass', () => {
it('should remove class name from a class-list string', () => {
expect(toggleClass('', '', false)).toEqual('');
expect(toggleClass('A', 'A', false)).toEqual('');
expect(toggleClass('AB', 'AB', false)).toEqual('');
expect(toggleClass('A B', 'A', false)).toEqual('B');
expect(toggleClass('A B', 'A', false)).toEqual('B');
expect(toggleClass('A B', 'B', false)).toEqual('A');
expect(toggleClass(' B ', 'B', false)).toEqual('');
});
it('should not remove a sub-string', () => {
expect(toggleClass('ABC', 'A', false)).toEqual('ABC');
expect(toggleClass('ABC', 'B', false)).toEqual('ABC');
expect(toggleClass('ABC', 'C', false)).toEqual('ABC');
expect(toggleClass('ABC', 'AB', false)).toEqual('ABC');
expect(toggleClass('ABC', 'BC', false)).toEqual('ABC');
});
it('should toggle a class', () => {
expect(toggleClass('', 'B', false)).toEqual('');
expect(toggleClass('', 'B', true)).toEqual('B');
expect(toggleClass('A B C', 'B', true)).toEqual('A B C');
expect(toggleClass('A C', 'B', true)).toEqual('A C B');
expect(toggleClass('A B C', 'B', false)).toEqual('A C');
expect(toggleClass('A B B C', 'B', false)).toEqual('A C');
expect(toggleClass('A B B C', 'B', true)).toEqual('A B B C');
});
});
describe('classIndexOf', () => {
it('should match simple case', () => {
expect(classIndexOf('A', 'A', 0)).toEqual(0);
@ -128,10 +26,3 @@ describe('class differ', () => {
});
});
});
export function sortedForEach<V>(map: Map<string, V>, fn: (value: V, key: string) => void): void {
const keys: string[] = [];
map.forEach((value, key) => keys.push(key));
keys.sort();
keys.forEach((key) => fn(map.get(key) !, key));
}

View File

@ -1,87 +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
*/
import {Renderer3, domRendererFactory3} from '@angular/core/src/render3/interfaces/renderer';
import {writeAndReconcileClass, writeAndReconcileStyle} from '@angular/core/src/render3/styling/reconcile';
import {getSortedClassName, getSortedStyle} from '@angular/core/testing/src/styling';
describe('styling reconcile', () => {
[document, domRendererFactory3.createRenderer(null, null)].forEach((renderer: Renderer3) => {
let element: HTMLDivElement;
beforeEach(() => { element = document.createElement('div'); });
describe('writeAndReconcileClass', () => {
it('should write new value to DOM', () => {
writeAndReconcileClass(renderer, element, '', 'A');
expect(getSortedClassName(element)).toEqual('A');
writeAndReconcileClass(renderer, element, 'A', 'C B A');
expect(getSortedClassName(element)).toEqual('A B C');
writeAndReconcileClass(renderer, element, 'C B A', '');
expect(getSortedClassName(element)).toEqual('');
});
it('should write value alphabetically when existing class present', () => {
element.className = 'X';
writeAndReconcileClass(renderer, element, '', 'A');
expect(getSortedClassName(element)).toEqual('A X');
writeAndReconcileClass(renderer, element, 'A', 'C B A');
expect(getSortedClassName(element)).toEqual('A B C X');
writeAndReconcileClass(renderer, element, 'C B A', '');
expect(getSortedClassName(element)).toEqual('X');
});
});
describe('writeAndReconcileStyle', () => {
it('should write new value to DOM', () => {
writeAndReconcileStyle(renderer, element, '', 'width: 100px;');
expect(getSortedStyle(element)).toEqual('width: 100px;');
writeAndReconcileStyle(
renderer, element, 'width: 100px;', 'color: red; height: 100px; width: 100px;');
expect(getSortedStyle(element)).toEqual('color: red; height: 100px; width: 100px;');
writeAndReconcileStyle(renderer, element, 'color: red; height: 100px; width: 100px;', '');
expect(getSortedStyle(element)).toEqual('');
});
it('should not clobber out of bound styles', () => {
element.style.cssText = 'color: red;';
writeAndReconcileStyle(renderer, element, '', 'width: 100px;');
expect(getSortedStyle(element)).toEqual('color: red; width: 100px;');
writeAndReconcileStyle(renderer, element, 'width: 100px;', 'width: 200px;');
expect(getSortedStyle(element)).toEqual('color: red; width: 200px;');
writeAndReconcileStyle(renderer, element, 'width: 200px;', 'width: 200px; height: 100px;');
expect(getSortedStyle(element)).toEqual('color: red; height: 100px; width: 200px;');
writeAndReconcileStyle(renderer, element, 'width: 200px; height: 100px;', '');
expect(getSortedStyle(element)).toEqual('color: red;');
});
it('should support duplicate styles', () => {
element.style.cssText = 'color: red;';
writeAndReconcileStyle(renderer, element, '', 'width: 100px; width: 200px;');
expect(getSortedStyle(element)).toEqual('color: red; width: 200px;');
writeAndReconcileStyle(
renderer, element, 'width: 100px; width: 200px;',
'width: 100px; width: 200px; height: 100px;');
expect(getSortedStyle(element)).toEqual('color: red; height: 100px; width: 200px;');
writeAndReconcileStyle(renderer, element, 'width: 100px; height: 100px;', '');
expect(getSortedStyle(element)).toEqual('color: red;');
});
});
});
});

View File

@ -11,7 +11,7 @@ import {TNode, TNodeType} from '@angular/core/src/render3/interfaces/node';
import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate} from '@angular/core/src/render3/interfaces/styling';
import {LView, TData} from '@angular/core/src/render3/interfaces/view';
import {enterView, leaveView} from '@angular/core/src/render3/state';
import {CLASS_MAP_STYLING_KEY, STYLE_MAP_STYLING_KEY, appendStyling, flushStyleBinding, insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list';
import {insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list';
import {newArray} from '@angular/core/src/util/array_utils';
describe('TNode styling linked list', () => {
@ -116,21 +116,20 @@ describe('TNode styling linked list', () => {
const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null);
tNode.styles = '';
const tData: TData = newArray(32, null);
const STYLE = STYLE_MAP_STYLING_KEY;
insertTStylingBinding(tData, tNode, STYLE, 10, false, false);
insertTStylingBinding(tData, tNode, null, 10, false, false);
expectRange(tNode.styleBindings).toEqual([10, 10]);
expectTData(tData).toEqual([
...empty_0_through_9, //
STYLE, [false, 0, false, 0], // 10 - Template: ɵɵstyleMap({color: '#001'});
null, null, // 12
...empty_14_through_19, // 14-19
null, null, // 20
null, null, // 22
null, null, // 24
null, null, // 26
null, null, // 28
null, null, // 30
...empty_0_through_9, //
null, [false, 0, false, 0], // 10 - Template: ɵɵstyleMap({color: '#001'});
null, null, // 12
...empty_14_through_19, // 14-19
null, null, // 20
null, null, // 22
null, null, // 24
null, null, // 26
null, null, // 28
null, null, // 30
]);
expectPriorityOrder(tData, tNode, false).toEqual([
[10, null, false, false], // 10 - Template: ɵɵstyleMap({color: '#001'});
@ -141,7 +140,7 @@ describe('TNode styling linked list', () => {
expectRange(tNode.styleBindings).toEqual([10, 12]);
expectTData(tData).toEqual([
...empty_0_through_9, //
STYLE, [false, 0, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
null, [false, 0, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'});
...empty_14_through_19, // 14-19
null, null, // 20
@ -156,14 +155,14 @@ describe('TNode styling linked list', () => {
[12, 'color', true, false], // 12 - Template: ɵɵstyleProp('color', '#002'});
]);
insertTStylingBinding(tData, tNode, STYLE, 20, true, false);
insertTStylingBinding(tData, tNode, null, 20, true, false);
expectRange(tNode.styleBindings).toEqual([10, 12]);
expectTData(tData).toEqual([
...empty_0_through_9, //
STYLE, [false, 20, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
null, [false, 20, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'});
...empty_14_through_19, // 14-19
STYLE, [false, 0, false, 10], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
null, [false, 0, false, 10], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
null, null, // 22
null, null, // 24
null, null, // 26
@ -180,10 +179,10 @@ describe('TNode styling linked list', () => {
expectRange(tNode.styleBindings).toEqual([10, 12]);
expectTData(tData).toEqual([
...empty_0_through_9, // 00-09
STYLE, [false, 22, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
null, [false, 22, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'});
...empty_14_through_19, // 14-19
STYLE, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
null, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
'color', [false, 20, false, 10], // 22 - MyComponent: ɵɵstyleProp('color', '#004'});
null, null, // 24
null, null, // 26
@ -197,16 +196,16 @@ describe('TNode styling linked list', () => {
[12, 'color', true, false], // 12 - Template: ɵɵstyleProp('color', '#002'});
]);
insertTStylingBinding(tData, tNode, STYLE, 24, true, false);
insertTStylingBinding(tData, tNode, null, 24, true, false);
expectRange(tNode.styleBindings).toEqual([10, 12]);
expectTData(tData).toEqual([
...empty_0_through_9, //
STYLE, [false, 24, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
null, [false, 24, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'});
...empty_14_through_19, // 14-19
STYLE, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
null, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
'color', [false, 20, false, 24], // 22 - MyComponent: ɵɵstyleProp('color', '#004'});
STYLE, [false, 22, false, 10], // 24 - Style1Directive: ɵɵstyleMap({color: '#003'});
null, [false, 22, false, 10], // 24 - Style1Directive: ɵɵstyleMap({color: '#003'});
null, null, // 26
null, null, // 28
null, null, // 30
@ -223,12 +222,12 @@ describe('TNode styling linked list', () => {
expectRange(tNode.styleBindings).toEqual([10, 12]);
expectTData(tData).toEqual([
...empty_0_through_9, // 00-09
STYLE, [false, 26, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
null, [false, 26, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'});
...empty_14_through_19, // 14-19
STYLE, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
null, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
'color', [false, 20, false, 24], // 22 - MyComponent: ɵɵstyleProp('color', '#004'});
STYLE, [false, 22, false, 26], // 24 - Style1Directive: ɵɵstyleMap({color: '#005'});
null, [false, 22, false, 26], // 24 - Style1Directive: ɵɵstyleMap({color: '#005'});
'color', [false, 24, false, 10], // 26 - Style1Directive: ɵɵstyleProp('color', '#006'});
null, null, // 28
null, null, // 30
@ -243,18 +242,18 @@ describe('TNode styling linked list', () => {
]);
insertTStylingBinding(tData, tNode, STYLE, 28, true, false);
insertTStylingBinding(tData, tNode, null, 28, true, false);
expectRange(tNode.styleBindings).toEqual([10, 12]);
expectTData(tData).toEqual([
...empty_0_through_9, //
STYLE, [false, 28, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
null, [false, 28, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'});
...empty_14_through_19, // 14-19
STYLE, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
null, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
'color', [false, 20, false, 24], // 22 - MyComponent: ɵɵstyleProp('color', '#004'});
STYLE, [false, 22, false, 26], // 24 - Style1Directive: ɵɵstyleMap({color: '#005'});
null, [false, 22, false, 26], // 24 - Style1Directive: ɵɵstyleMap({color: '#005'});
'color', [false, 24, false, 28], // 26 - Style1Directive: ɵɵstyleProp('color', '#006'});
STYLE, [false, 26, false, 10], // 28 - Style2Directive: ɵɵstyleMap({color: '#007'});
null, [false, 26, false, 10], // 28 - Style2Directive: ɵɵstyleMap({color: '#007'});
null, null, // 30
]);
expectPriorityOrder(tData, tNode, false).toEqual([
@ -271,14 +270,14 @@ describe('TNode styling linked list', () => {
expectRange(tNode.styleBindings).toEqual([10, 12]);
expectTData(tData).toEqual([
...empty_0_through_9, // 00-09
STYLE, [false, 30, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
null, [false, 30, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'});
'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'});
...empty_14_through_19, // 14-19
STYLE, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
null, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'});
'color', [false, 20, false, 24], // 22 - MyComponent: ɵɵstyleProp('color', '#004'});
STYLE, [false, 22, false, 26], // 24 - Style1Directive: ɵɵstyleMap({color: '#005'});
null, [false, 22, false, 26], // 24 - Style1Directive: ɵɵstyleMap({color: '#005'});
'color', [false, 24, false, 28], // 26 - Style1Directive: ɵɵstyleProp('color', '#006'});
STYLE, [false, 26, false, 30], // 28 - Style2Directive: ɵɵstyleMap({color: '#007'});
null, [false, 26, false, 30], // 28 - Style2Directive: ɵɵstyleMap({color: '#007'});
'color', [false, 28, false, 10], // 30 - Style2Directive: ɵɵstyleProp('color', '#008'});
]);
expectPriorityOrder(tData, tNode, false).toEqual([
@ -356,7 +355,7 @@ describe('TNode styling linked list', () => {
[2, 'color', false, false],
]);
insertTStylingBinding(tData, tNode, STYLE_MAP_STYLING_KEY /*Map*/, 6, true, false);
insertTStylingBinding(tData, tNode, null /*Map*/, 6, true, false);
expectPriorityOrder(tData, tNode, false).toEqual([
// PREV, NEXT
[4, 'height', false, true],
@ -368,7 +367,7 @@ describe('TNode styling linked list', () => {
it('should mark all things after map as duplicate', () => {
const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null);
const tData: TData = [null, null];
insertTStylingBinding(tData, tNode, STYLE_MAP_STYLING_KEY, 2, false, false);
insertTStylingBinding(tData, tNode, null, 2, false, false);
insertTStylingBinding(tData, tNode, 'height', 4, false, false);
insertTStylingBinding(tData, tNode, 'color', 6, true, false);
expectPriorityOrder(tData, tNode, false).toEqual([
@ -383,13 +382,13 @@ describe('TNode styling linked list', () => {
const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null);
const tData: TData = [null, null];
insertTStylingBinding(tData, tNode, 'width', 2, false, false);
insertTStylingBinding(tData, tNode, {key: 'height', extra: 'px'}, 4, false, false);
insertTStylingBinding(tData, tNode, 'height', 4, false, false);
expectPriorityOrder(tData, tNode, false).toEqual([
// PREV, NEXT
[2, 'width', false, false],
[4, 'height', false, false],
]);
insertTStylingBinding(tData, tNode, {key: 'height', extra: 'em'}, 6, false, false);
insertTStylingBinding(tData, tNode, 'height', 6, false, false);
expectPriorityOrder(tData, tNode, false).toEqual([
// PREV, NEXT
[2, 'width', false, false],
@ -423,7 +422,7 @@ describe('TNode styling linked list', () => {
[4, 'color', true, false],
]);
insertTStylingBinding(tData, tNode, STYLE_MAP_STYLING_KEY, 6, false, false);
insertTStylingBinding(tData, tNode, null, 6, false, false);
expectPriorityOrder(tData, tNode, false).toEqual([
// PREV, NEXT
[2, 'width', false, true],
@ -433,171 +432,6 @@ describe('TNode styling linked list', () => {
});
});
describe('styleBindingFlush', () => {
it('should write basic value', () => {
const fixture = new StylingFixture([['color']], false);
fixture.setBinding(0, 'red');
expect(fixture.flush(0)).toEqual('color: red;');
});
it('should chain values and allow update mid list', () => {
const fixture = new StylingFixture([['color', {key: 'width', extra: 'px'}]], false);
fixture.setBinding(0, 'red');
fixture.setBinding(1, '100');
expect(fixture.flush(0)).toEqual('color: red; width: 100px;');
fixture.setBinding(0, 'blue');
fixture.setBinding(1, '200');
expect(fixture.flush(1)).toEqual('color: red; width: 200px;');
expect(fixture.flush(0)).toEqual('color: blue; width: 200px;');
});
it('should remove duplicates', () => {
const fixture = new StylingFixture([['color', 'color']], false);
fixture.setBinding(0, 'red');
fixture.setBinding(1, 'blue');
expect(fixture.flush(0)).toEqual('color: blue;');
});
it('should treat undefined values as previous value', () => {
const fixture = new StylingFixture([['color', 'color']], false);
fixture.setBinding(0, 'red');
fixture.setBinding(1, undefined);
expect(fixture.flush(0)).toEqual('color: red;');
});
it('should treat null value as removal', () => {
const fixture = new StylingFixture([['color']], false);
fixture.setBinding(0, null);
expect(fixture.flush(0)).toEqual('');
});
});
describe('appendStyling', () => {
it('should append simple style', () => {
expect(appendStyling('', 'color', 'red', null, false, false)).toEqual('color: red;');
expect(appendStyling('', 'color', 'red', null, true, false)).toEqual('color: red;');
expect(appendStyling('', 'color', 'red', null, false, true)).toEqual('color');
expect(appendStyling('', 'color', 'red', null, true, true)).toEqual('color');
expect(appendStyling('', 'color', true, null, true, true)).toEqual('color');
expect(appendStyling('', 'color', false, null, true, true)).toEqual('');
expect(appendStyling('', 'color', 0, null, true, true)).toEqual('');
expect(appendStyling('', 'color', '', null, true, true)).toEqual('');
});
it('should append simple style with suffix', () => {
expect(appendStyling('', {key: 'width', extra: 'px'}, 100, null, false, false))
.toEqual('width: 100px;');
});
it('should append simple style with sanitizer', () => {
expect(
appendStyling('', {key: 'width', extra: (v: any) => `-${v}-`}, 100, null, false, false))
.toEqual('width: -100-;');
});
it('should append class/style', () => {
expect(appendStyling('color: white;', 'color', 'red', null, false, false))
.toEqual('color: white; color: red;');
expect(appendStyling('MY-CLASS', 'color', true, null, false, true)).toEqual('MY-CLASS color');
expect(appendStyling('MY-CLASS', 'color', false, null, true, true)).toEqual('MY-CLASS');
});
it('should remove existing', () => {
expect(appendStyling('color: white;', 'color', 'blue', null, true, false))
.toEqual('color: blue;');
expect(appendStyling('A YES B', 'YES', false, null, true, true)).toEqual('A B');
});
it('should support maps/arrays for classes', () => {
expect(appendStyling('', CLASS_MAP_STYLING_KEY, {A: true, B: false}, null, true, true))
.toEqual('A');
expect(appendStyling('A B C', CLASS_MAP_STYLING_KEY, {A: true, B: false}, null, true, true))
.toEqual('A C');
expect(appendStyling('', CLASS_MAP_STYLING_KEY, ['A', 'B'], null, true, true)).toEqual('A B');
expect(appendStyling('A B C', CLASS_MAP_STYLING_KEY, ['A', 'B'], null, true, true))
.toEqual('A B C');
});
it('should support maps for styles', () => {
expect(appendStyling('', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false))
.toEqual('A: a; B: b;');
expect(appendStyling(
'A:_; B:_; C:_;', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false))
.toEqual('C:_; A: a; B: b;');
});
it('should support strings for classes', () => {
expect(appendStyling('', CLASS_MAP_STYLING_KEY, 'A B', null, true, true)).toEqual('A B');
expect(appendStyling('A B C', CLASS_MAP_STYLING_KEY, 'A B', null, false, true))
.toEqual('A B C A B');
expect(appendStyling('A B C', CLASS_MAP_STYLING_KEY, 'A B', null, true, true))
.toEqual('A B C');
});
it('should support strings for styles', () => {
expect(appendStyling('A:a;B:b;', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, false, false))
.toEqual('A:a;B:b; A : a ; B : b;');
expect(appendStyling(
'A:_; B:_; C:_;', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, true, false))
.toEqual('C:_; A: a; B: b;');
});
it('should throw no arrays for styles', () => {
expect(() => appendStyling('', STYLE_MAP_STYLING_KEY, ['A', 'a'], null, true, false))
.toThrow();
});
describe('style sanitization', () => {
it('should sanitize properties', () => {
// Verify map
expect(appendStyling(
'', STYLE_MAP_STYLING_KEY, {
'background-image': 'url(javascript:evil())',
'background': 'url(javascript:evil())',
'border-image': 'url(javascript:evil())',
'filter': 'url(javascript:evil())',
'list-style': 'url(javascript:evil())',
'list-style-image': 'url(javascript:evil())',
'clip-path': 'url(javascript:evil())',
'width': 'url(javascript:evil())', // should not sanitize
},
null, true, false))
.toEqual(
'background-image: unsafe; ' +
'background: unsafe; ' +
'border-image: unsafe; ' +
'filter: unsafe; ' +
'list-style: unsafe; ' +
'list-style-image: unsafe; ' +
'clip-path: unsafe; ' +
'width: url(javascript:evil());');
// verify string
expect(appendStyling(
'', STYLE_MAP_STYLING_KEY,
'background-image: url(javascript:evil());' +
'background: url(javascript:evil());' +
'border-image: url(javascript:evil());' +
'filter: url(javascript:evil());' +
'list-style: url(javascript:evil());' +
'list-style-image: url(javascript:evil());' +
'clip-path: url(javascript:evil());' +
'width: url(javascript:evil());' // should not sanitize
,
null, true, false))
.toEqual(
'background-image: unsafe; ' +
'background: unsafe; ' +
'border-image: unsafe; ' +
'filter: unsafe; ' +
'list-style: unsafe; ' +
'list-style-image: unsafe; ' +
'clip-path: unsafe; ' +
'width: url(javascript:evil());');
});
});
});
});
const empty_0_through_9 = [null, null, null, null, null, null, null, null, null, null];
@ -629,9 +463,6 @@ function expectPriorityOrder(tData: TData, tNode: TNode, isClassBinding: boolean
const indexes: [number, string | null, boolean, boolean][] = [];
while (index !== 0) {
let key = tData[index] as TStylingKey | null;
if (key !== null && typeof key === 'object') {
key = key.key;
}
const tStylingRange = tData[index + 1] as TStylingRange;
indexes.push([
index, //
@ -660,32 +491,4 @@ export function getStylingBindingHead(tData: TData, tNode: TNode, isClassBinding
index = prev;
}
}
}
class StylingFixture {
tData: TData = [null, null];
lView: LView = [null, null !] as any;
tNode: TNode = createTNode(null !, null !, TNodeType.Element, 0, '', null);
constructor(bindingSources: TStylingKey[][], public isClassBinding: boolean) {
this.tNode.classes = '';
this.tNode.styles = '';
let bindingIndex = this.tData.length;
for (let i = 0; i < bindingSources.length; i++) {
const bindings = bindingSources[i];
for (let j = 0; j < bindings.length; j++) {
const binding = bindings[j];
insertTStylingBinding(
this.tData, this.tNode, binding, bindingIndex, i === 0, isClassBinding);
this.lView.push(null, null);
bindingIndex += 2;
}
}
}
setBinding(index: number, value: any) { this.lView[index * 2 + 2] = value; }
flush(index: number): string {
return flushStyleBinding(
this.tData, this.tNode, this.lView, index * 2 + 2, this.isClassBinding);
}
}

View File

@ -1,139 +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
*/
import {StyleChangesMap, parseKeyValue, removeStyle} from '@angular/core/src/render3/styling/style_differ';
import {getLastParsedValue, parseStyle} from '@angular/core/src/render3/styling/styling_parser';
import {sortedForEach} from './class_differ_spec';
describe('style differ', () => {
describe('parseStyleValue', () => {
it('should parse empty value', () => {
expectParseValue(':').toBe('');
expectParseValue(':;🛑ignore').toBe('');
expectParseValue(': ;🛑ignore').toBe('');
expectParseValue(':;🛑ignore').toBe('');
expectParseValue(': \n\t\r ;🛑').toBe('');
});
it('should parse basic value', () => {
expectParseValue(':a').toBe('a');
expectParseValue(':text').toBe('text');
expectParseValue(': text2 ;🛑').toBe('text2');
expectParseValue(':text3;🛑').toBe('text3');
expectParseValue(': text3 ;🛑').toBe('text3');
expectParseValue(': text1 text2;🛑').toBe('text1 text2');
expectParseValue(': text1 text2 ;🛑').toBe('text1 text2');
});
it('should parse empty vale', () => {
expectParseValue(':').toBe('');
expectParseValue(': ').toBe('');
expectParseValue(': ;🛑').toBe('');
expectParseValue(':;🛑').toBe('');
});
it('should parse quoted values', () => {
expectParseValue(':""').toBe('""');
expectParseValue(':"\\\\"').toBe('"\\\\"');
expectParseValue(': ""').toBe('""');
expectParseValue(': "" ').toBe('""');
expectParseValue(': "text1" text2 ').toBe('"text1" text2');
expectParseValue(':"text"').toBe('"text"');
expectParseValue(': \'hello world\'').toBe('\'hello world\'');
expectParseValue(':"some \n\t\r text ,;";🛑').toBe('"some \n\t\r text ,;"');
expectParseValue(':"\\"\'";🛑').toBe('"\\"\'"');
});
it('should parse url()', () => {
expectParseValue(':url(:;)').toBe('url(:;)');
expectParseValue(':URL(some :; text)').toBe('URL(some :; text)');
expectParseValue(': url(text);🛑').toBe('url(text)');
expectParseValue(': url(text) more text;🛑').toBe('url(text) more text');
expectParseValue(':url(;"\':\\))').toBe('url(;"\':\\))');
expectParseValue(': url(;"\':\\)) ;🛑').toBe('url(;"\':\\))');
});
});
describe('parseKeyValue', () => {
it('should parse empty string', () => {
expectParseKeyValue('').toEqual([]);
expectParseKeyValue(' \n\t\r ').toEqual([]);
});
it('should parse empty value', () => {
expectParseKeyValue('key:').toEqual(['key', '', null]);
expectParseKeyValue('key: \n\t\r; ').toEqual(['key', '', null]);
});
it('should prase single style', () => {
expectParseKeyValue('width: 100px').toEqual(['width', '100px', null]);
expectParseKeyValue(' width : 100px ;').toEqual(['width', '100px', null]);
});
it('should prase multi style', () => {
expectParseKeyValue('width: 100px; height: 200px').toEqual([
'height', '200px', null, //
'width', '100px', null, //
]);
expectParseKeyValue(' height : 200px ; width : 100px ').toEqual([
'height', '200px', null, //
'width', '100px', null //
]);
});
});
describe('removeStyle', () => {
it('should remove no style', () => {
expect(removeStyle('', 'foo')).toEqual('');
expect(removeStyle('abc: bar;', 'a')).toEqual('abc: bar;');
expect(removeStyle('abc: bar;', 'b')).toEqual('abc: bar;');
expect(removeStyle('abc: bar;', 'c')).toEqual('abc: bar;');
expect(removeStyle('abc: bar;', 'bar')).toEqual('abc: bar;');
});
it('should remove all style', () => {
expect(removeStyle('foo: bar;', 'foo')).toEqual('');
expect(removeStyle('foo: bar; foo: bar;', 'foo')).toEqual('');
});
it('should remove some of the style', () => {
expect(removeStyle('a: a; foo: bar; b: b;', 'foo')).toEqual('a: a; b: b;');
expect(removeStyle('a: a; foo: bar; b: b;', 'foo')).toEqual('a: a; b: b;');
expect(removeStyle('a: a; foo: bar; b: b; foo: bar; c: c;', 'foo'))
.toEqual('a: a; b: b; c: c;');
});
it('should remove trailing ;', () => {
expect(removeStyle('a: a; foo: bar;', 'foo')).toEqual('a: a;');
expect(removeStyle('a: a ; foo: bar ; ', 'foo')).toEqual('a: a ;');
});
});
});
function expectParseValue(
/**
* The text to parse.
*
* The text can contain special 🛑 character which demarcates where the parsing should stop
* and asserts that the parsing ends at that location.
*/
text: string) {
let stopIndex = text.indexOf('🛑');
if (stopIndex < 0) stopIndex = text.length;
let i = parseStyle(text);
expect(i).toBe(stopIndex);
return expect(getLastParsedValue(text));
}
function expectParseKeyValue(text: string) {
const changes: StyleChangesMap = new Map<string, any>();
parseKeyValue(text, changes, false);
const list: any[] = [];
sortedForEach(changes, (value, key) => list.push(key, value.old, value.new));
return expect(list);
}