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 4c7087ccdb
commit 9bd9590767
42 changed files with 1172 additions and 2497 deletions

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2987,
"main-es2015": 456890,
"main-es2015": 455803,
"polyfills-es2015": 52503
}
}

View File

@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 141985,
"main-es2015": 141746,
"polyfills-es2015": 36808
}
}
@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 16787,
"main-es2015": 16593,
"polyfills-es2015": 36808
}
}
@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 148115,
"main-es2015": 147911,
"polyfills-es2015": 36808
}
}
@ -30,7 +30,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 137226,
"main-es2015": 137026,
"polyfills-es2015": 37494
}
}
@ -39,7 +39,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 254857,
"main-es2015": 254657,
"polyfills-es2015": 36808,
"5-es2015": 751
}
@ -49,7 +49,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 226519,
"main-es2015": 226321,
"polyfills-es2015": 36808,
"5-es2015": 779
}

View File

@ -684,7 +684,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// the code here will collect all update-level styling instructions and add them to the
// update block of the template function AOT code. Instructions like `styleProp`,
// `styleMap`, `classMap`, `classProp` and `flushStyling`
// `styleMap`, `classMap`, `classProp`
// are all generated and assigned in the code below.
const stylingInstructions = stylingBuilder.buildUpdateLevelInstructions(this._valueConverter);
const limit = stylingInstructions.length - 1;

View File

@ -80,6 +80,9 @@ export abstract class RendererFactory2 {
* @publicApi
*/
export enum RendererStyleFlags2 {
// TODO(misko): This needs to be refactored into a separate file so that it can be imported from
// `node_manipulation.ts` Currently doing the import cause resolution order to change and fails
// the tests. The work around is to have hard coded value in `node_manipulation.ts` for now.
/**
* Marks a style as important.
*/

View File

@ -22,8 +22,8 @@ import {TElementNode, TNode, TNodeType} from './interfaces/node';
import {PlayerHandler} from './interfaces/player';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, LView, LViewFlags, RootContext, RootContextFlags, TVIEW, TViewType} from './interfaces/view';
import {writeDirectClass, writeDirectStyle} from './node_manipulation';
import {enterView, getPreviousOrParentTNode, leaveView, setActiveHostElement} from './state';
import {writeDirectClass, writeDirectStyle} from './styling/reconcile';
import {computeStaticStyling} from './styling/static_styling';
import {setUpAttributes} from './util/attrs_utils';
import {publishDefaultGlobalUtils} from './util/global_utils';

View File

@ -8,7 +8,7 @@
import {assertDataInRange, assertGreaterThan} from '../../util/assert';
import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks';
import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, TVIEW} from '../interfaces/view';
import {executeElementExitFn, getCheckNoChangesMode, getLView, getSelectedIndex, setSelectedIndex} from '../state';
import {getCheckNoChangesMode, getLView, getSelectedIndex, setSelectedIndex} from '../state';
@ -54,8 +54,6 @@ export function selectIndexInternal(lView: LView, index: number, checkNoChangesM
ngDevMode && assertGreaterThan(index, -1, 'Invalid index');
ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET);
executeElementExitFn();
// Flush the initial hooks for elements in the view that have been added up to this point.
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
if (!checkNoChangesMode) {

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {arrayMapSet} from '../../util/array_utils';
import {getLView} from '../state';
import {CLASS_MAP_STYLING_KEY} from '../styling/style_binding_list';
import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation';
import {checkStylingMap} from './styling';
import {checkStylingMap, classStringParser} from './styling';
@ -37,7 +37,7 @@ import {checkStylingMap} from './styling';
export function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void {
const lView = getLView();
const interpolatedValue = interpolation1(lView, prefix, v0, suffix);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
checkStylingMap(arrayMapSet, classStringParser, interpolatedValue, true);
}
/**
@ -67,7 +67,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);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
checkStylingMap(arrayMapSet, classStringParser, interpolatedValue, true);
}
/**
@ -100,7 +100,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);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
checkStylingMap(arrayMapSet, classStringParser, interpolatedValue, true);
}
/**
@ -136,7 +136,7 @@ export function ɵɵclassMapInterpolate4(
suffix: string): void {
const lView = getLView();
const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
checkStylingMap(arrayMapSet, classStringParser, interpolatedValue, true);
}
/**
@ -175,7 +175,7 @@ export function ɵɵclassMapInterpolate5(
const lView = getLView();
const interpolatedValue =
interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
checkStylingMap(arrayMapSet, classStringParser, interpolatedValue, true);
}
/**
@ -216,7 +216,7 @@ export function ɵɵclassMapInterpolate6(
const lView = getLView();
const interpolatedValue =
interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
checkStylingMap(arrayMapSet, classStringParser, interpolatedValue, true);
}
/**
@ -259,7 +259,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);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
checkStylingMap(arrayMapSet, classStringParser, interpolatedValue, true);
}
/**
@ -305,7 +305,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);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
checkStylingMap(arrayMapSet, classStringParser, interpolatedValue, true);
}
/**
@ -334,5 +334,5 @@ export function ɵɵclassMapInterpolate8(
export function ɵɵclassMapInterpolateV(values: any[]): void {
const lView = getLView();
const interpolatedValue = interpolationV(lView, values);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
checkStylingMap(arrayMapSet, classStringParser, interpolatedValue, true);
}

View File

@ -15,9 +15,8 @@ import {RElement} from '../interfaces/renderer';
import {isContentQueryHost, isDirectiveHost} from '../interfaces/type_checks';
import {HEADER_OFFSET, LView, RENDERER, TVIEW, TView, T_HOST} from '../interfaces/view';
import {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation';
import {appendChild, writeDirectClass, writeDirectStyle} from '../node_manipulation';
import {decreaseElementDepthCount, getBindingIndex, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state';
import {writeDirectClass, writeDirectStyle} from '../styling/reconcile';
import {computeStaticStyling} from '../styling/static_styling';
import {setUpAttributes} from '../util/attrs_utils';
import {getConstant} from '../util/view_utils';

View File

@ -14,7 +14,7 @@ import {TContainerNode, TNodeType} from '../interfaces/node';
import {CONTEXT, LView, LViewFlags, PARENT, TVIEW, TView, TViewType, T_HOST} from '../interfaces/view';
import {assertNodeType} from '../node_assert';
import {insertView, removeView} from '../node_manipulation';
import {enterView, getIsParent, getLView, getPreviousOrParentTNode, leaveViewProcessExit, setIsParent, setPreviousOrParentTNode} from '../state';
import {enterView, getIsParent, getLView, getPreviousOrParentTNode, leaveView, setIsParent, setPreviousOrParentTNode} from '../state';
import {getLContainerActiveIndex, isCreationMode} from '../util/view_utils';
import {assignTViewNodeToLView, createLView, createTView, refreshView, renderView} from './shared';
@ -139,6 +139,6 @@ export function ɵɵembeddedViewEnd(): void {
const lContainer = lView[PARENT] as LContainer;
ngDevMode && assertLContainerOrUndefined(lContainer);
leaveViewProcessExit();
leaveView();
setPreviousOrParentTNode(viewHost !, false);
}

View File

@ -8,6 +8,7 @@
import {AttributeMarker, ComponentTemplate} from '..';
import {SchemaMetadata} from '../../core';
import {ArrayMap} from '../../util/array_utils';
import {assertDefined} from '../../util/assert';
import {createNamedArrayType} from '../../util/named_array_type';
import {initNgDevMode} from '../../util/ng_dev_mode';
@ -175,7 +176,9 @@ class TNode implements ITNode {
public parent: TElementNode|TContainerNode|null, //
public projection: number|(ITNode|RNode[])[]|null, //
public styles: string|null, //
public stylesMap: ArrayMap<any>|undefined|null, //
public classes: string|null, //
public classesMap: ArrayMap<any>|undefined|null, //
public classBindings: TStylingRange, //
public styleBindings: TStylingRange, //
) {}

View File

@ -31,12 +31,13 @@ import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRoo
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TData, TVIEW, TView, TViewType, T_HOST} from '../interfaces/view';
import {assertNodeOfPossibleTypes} from '../node_assert';
import {isNodeMatchingSelectorList} from '../node_selector_matcher';
import {clearActiveHostElement, enterView, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, leaveView, leaveViewProcessExit, setActiveHostElement, setBindingIndex, setBindingRoot, setCheckNoChangesMode, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state';
import {clearActiveHostElement, enterView, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, leaveView, setActiveHostElement, setBindingIndex, setBindingRootForHostBindings, setCheckNoChangesMode, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {isAnimationProp, mergeHostAttrs} from '../util/attrs_utils';
import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils';
import {getLViewParent} from '../util/view_traversal_utils';
import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isCreationMode, readPatchedLView, resetPreOrderHookFlags, viewAttachedToChangeDetector} from '../util/view_utils';
import {selectIndexInternal} from './advance';
import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeDebug, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData} from './lview_debug';
@ -56,12 +57,10 @@ const _CLEAN_PROMISE = (() => Promise.resolve(null))();
*/
export function setHostBindingsByExecutingExpandoInstructions(tView: TView, lView: LView): void {
ngDevMode && assertSame(tView, lView[TVIEW], '`LView` is not associated with the `TView`!');
const selectedIndex = getSelectedIndex();
try {
const expandoInstructions = tView.expandoInstructions;
if (expandoInstructions !== null) {
let bindingRootIndex = setBindingIndex(tView.expandoStartIndex);
setBindingRoot(bindingRootIndex);
let bindingRootIndex = tView.expandoStartIndex;
let currentDirectiveIndex = -1;
let currentElementIndex = -1;
// TODO(misko): PERF It is possible to get here with `TVIew.expandoInstructions` containing no
@ -96,11 +95,10 @@ export function setHostBindingsByExecutingExpandoInstructions(tView: TView, lVie
// (to get to the next set of host bindings on this node).
bindingRootIndex += instruction;
}
setBindingRoot(bindingRootIndex);
} else {
// If it's not a number, it's a host binding function that needs to be executed.
if (instruction !== null) {
setBindingIndex(bindingRootIndex);
setBindingRootForHostBindings(bindingRootIndex);
const hostCtx = lView[currentDirectiveIndex];
instruction(RenderFlags.Update, hostCtx, currentElementIndex);
}
@ -112,7 +110,6 @@ export function setHostBindingsByExecutingExpandoInstructions(tView: TView, lVie
// iterate over those directives which actually have `hostBindings`.
currentDirectiveIndex++;
}
setBindingRoot(bindingRootIndex);
}
}
} finally {
@ -503,7 +500,7 @@ export function refreshView<T>(
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
}
} finally {
leaveViewProcessExit();
leaveView();
}
}
@ -540,7 +537,6 @@ function executeTemplate<T>(
}
templateFn(rf, context);
} finally {
executeElementExitFn();
setSelectedIndex(prevSelectedIndex);
}
}
@ -828,7 +824,9 @@ export function createTNode(
tParent, // parent: TElementNode|TContainerNode|null
null, // projection: number|(ITNode|RNode[])[]|null
null, // styles: string|null
undefined, // stylesMap: string|null
null, // classes: string|null
undefined, // classesMap: string|null
0 as any, // classBindings: TStylingRange;
0 as any, // styleBindings: TStylingRange;
) :
@ -855,7 +853,9 @@ export function createTNode(
parent: tParent,
projection: null,
styles: null,
stylesMap: undefined,
classes: null,
classesMap: undefined,
classBindings: 0 as any,
styleBindings: 0 as any,
};

View File

@ -5,27 +5,30 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SafeValue} from '../../sanitization/bypass';
import {SafeValue, unwrapSafeValue} from '../../sanitization/bypass';
import {stylePropNeedsSanitization, ɵɵsanitizeStyle} from '../../sanitization/sanitization';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {assertEqual, assertGreaterThan, assertLessThan} from '../../util/assert';
import {concatStringsWithSpace} from '../../util/stringify';
import {ArrayMap, arrayMapGet, arrayMapSet} from '../../util/array_utils';
import {assertDefined, assertEqual, assertLessThan, throwError} from '../../util/assert';
import {EMPTY_ARRAY} from '../../util/empty';
import {concatStringsWithSpace, stringify} from '../../util/stringify';
import {assertFirstUpdatePass} from '../assert';
import {bindingUpdated} from '../bindings';
import {TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {AttributeMarker, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {RElement, Renderer3} from '../interfaces/renderer';
import {SanitizerFn} from '../interfaces/sanitization';
import {TStylingKey, TStylingMapKey, TStylingSanitizationKey, TStylingSuffixKey, getTStylingRangeTail} from '../interfaces/styling';
import {HEADER_OFFSET, RENDERER, TVIEW, TView} from '../interfaces/view';
import {getCheckNoChangesMode, getClassBindingChanged, getCurrentStyleSanitizer, getLView, getSelectedIndex, getStyleBindingChanged, incrementBindingIndex, isActiveHostElement, markStylingBindingDirty, setCurrentStyleSanitizer, setElementExitFn} from '../state';
import {writeAndReconcileClass, writeAndReconcileStyle} from '../styling/reconcile';
import {CLASS_MAP_STYLING_KEY, IGNORE_DUE_TO_INPUT_SHADOW, STYLE_MAP_STYLING_KEY, flushStyleBinding, insertTStylingBinding} from '../styling/style_binding_list';
import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate} from '../interfaces/styling';
import {HEADER_OFFSET, LView, RENDERER, TData, TVIEW, TView} from '../interfaces/view';
import {applyStyling} from '../node_manipulation';
import {getCurrentStyleSanitizer, getLView, getSelectedIndex, incrementBindingIndex, setCurrentStyleSanitizer} from '../state';
import {insertTStylingBinding} from '../styling/style_binding_list';
import {getLastParsedKey, getLastParsedValue, parseClassName, parseClassNameNext, parseStyle, parseStyleNext} from '../styling/styling_parser';
import {NO_CHANGE} from '../tokens';
import {unwrapRNode} from '../util/view_utils';
import {getNativeByIndex} from '../util/view_utils';
import {setDirectiveInputsWhichShadowsStyling} from './property';
/**
* Sets the current style sanitizer function which will then be used
* within all follow-up prop and map-based style binding instructions
@ -68,7 +71,7 @@ export function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void {
* @codeGenApi
*/
export function ɵɵstyleProp(
prop: string, value: string | number | SafeValue | null | undefined,
prop: string, value: string | number | SafeValue | undefined | null,
suffix?: string | null): typeof ɵɵstyleProp {
checkStylingProperty(prop, value, suffix, false);
return ɵɵstyleProp;
@ -90,7 +93,7 @@ export function ɵɵstyleProp(
* @codeGenApi
*/
export function ɵɵclassProp(
className: string, value: boolean | null | undefined): typeof ɵɵclassProp {
className: string, value: boolean | undefined | null): typeof ɵɵclassProp {
checkStylingProperty(className, value, null, true);
return ɵɵclassProp;
}
@ -116,11 +119,28 @@ export function ɵɵclassProp(
* @codeGenApi
*/
export function ɵɵstyleMap(
styles: {[styleName: string]: any} | Map<string, string|number|null|undefined>| string | null |
undefined): void {
checkStylingMap(STYLE_MAP_STYLING_KEY, styles, false);
styles: {[styleName: string]: any} | Map<string, string|number|null|undefined>| string |
undefined | null): void {
checkStylingMap(styleArrayMapSet, styleStringParser, styles, false);
}
/**
* Parse text as style and add values to ArrayMap.
*
* This code is pulled out to a separate function so that it can be tree shaken away if it is not
* needed. It is only reference from `ɵɵstyleMap`.
*
* @param arrayMap ArrayMap to add parsed values to.
* @param text text to parse.
*/
export function styleStringParser(arrayMap: ArrayMap<any>, text: string): void {
for (let i = parseStyle(text); i >= 0; i = parseStyleNext(text, i)) {
styleArrayMapSet(arrayMap, getLastParsedKey(text), getLastParsedValue(text));
}
}
/**
* Update class bindings using an object literal or class-string on an element.
*
@ -140,11 +160,25 @@ export function ɵɵstyleMap(
* @codeGenApi
*/
export function ɵɵclassMap(
classes: {[className: string]: boolean | null | undefined} |
Map<string, boolean|undefined|null>| Set<string>| string[] | string | null | undefined): void {
checkStylingMap(CLASS_MAP_STYLING_KEY, classes, true);
classes: {[className: string]: boolean | undefined | null} |
Map<string, boolean|undefined|null>| Set<string>| string[] | string | undefined | null): void {
checkStylingMap(arrayMapSet, classStringParser, classes, true);
}
/**
* Parse text as class and add values to ArrayMap.
*
* This code is pulled out to a separate function so that it can be tree shaken away if it is not
* needed. It is only reference from `ɵɵclassMap`.
*
* @param arrayMap ArrayMap to add parsed values to.
* @param text text to parse.
*/
export function classStringParser(arrayMap: ArrayMap<any>, text: string): void {
for (let i = parseClassName(text); i >= 0; i = parseClassNameNext(text, i)) {
arrayMapSet(arrayMap, getLastParsedKey(text), true);
}
}
/**
* Common code between `ɵɵclassProp` and `ɵɵstyleProp`.
@ -156,7 +190,7 @@ export function ɵɵclassMap(
*/
export function checkStylingProperty(
prop: string, value: any | NO_CHANGE,
suffixOrSanitizer: SanitizerFn | string | null | undefined, isClassBased: boolean): void {
suffixOrSanitizer: SanitizerFn | string | undefined | null, isClassBased: boolean): void {
const lView = getLView();
const tView = lView[TVIEW];
// Styling instructions use 2 slots per binding.
@ -164,6 +198,9 @@ export function checkStylingProperty(
// 2. one for the intermittent-value / TStylingRange
const bindingIndex = incrementBindingIndex(2);
if (tView.firstUpdatePass) {
stylingPropertyFirstUpdatePass(tView, prop, bindingIndex, isClassBased);
}
if (value !== NO_CHANGE && bindingUpdated(lView, bindingIndex, value)) {
// This is a work around. Once PR#34480 lands the sanitizer is passed explicitly and this line
// can be removed.
let styleSanitizer: StyleSanitizeFn|null;
@ -172,11 +209,11 @@ export function checkStylingProperty(
suffixOrSanitizer = styleSanitizer as any;
}
}
stylingPropertyFirstUpdatePass(tView, prop, suffixOrSanitizer, bindingIndex, isClassBased);
}
if (value !== NO_CHANGE && bindingUpdated(lView, bindingIndex, value)) {
markStylingBindingDirty(bindingIndex, isClassBased);
setElementExitFn(flushStylingOnElementExit);
const tNode = tView.data[getSelectedIndex() + HEADER_OFFSET] as TNode;
updateStyling(
tView, tNode, lView, lView[RENDERER], prop,
lView[bindingIndex + 1] = normalizeAndApplySuffixOrSanitizer(value, suffixOrSanitizer),
isClassBased, bindingIndex);
}
}
@ -188,20 +225,27 @@ export function checkStylingProperty(
* @param isClassBased `true` if `class` change (`false` if `style`)
*/
export function checkStylingMap(
tStylingMapKey: TStylingMapKey, value: any | NO_CHANGE, isClassBased: boolean): void {
arrayMapSet: (arrayMap: ArrayMap<any>, key: string, value: any) => void,
stringParser: (styleArrayMap: ArrayMap<any>, text: string) => void, value: any|NO_CHANGE,
isClassBased: boolean): void {
const lView = getLView();
const tView = lView[TVIEW];
const bindingIndex = incrementBindingIndex(2);
if (tView.firstUpdatePass) {
stylingPropertyFirstUpdatePass(tView, tStylingMapKey, null, bindingIndex, isClassBased);
stylingPropertyFirstUpdatePass(tView, null, bindingIndex, isClassBased);
}
if (value !== NO_CHANGE && bindingUpdated(lView, bindingIndex, value)) {
// `getSelectedIndex()` should be here (rather than in instruction) so that it is guarded by the
// if so as not to read unnecessarily.
const tNode = tView.data[getSelectedIndex() + HEADER_OFFSET] as TNode;
if (hasStylingInputShadow(tNode, isClassBased) && !isInHostBindings(tView, bindingIndex)) {
// VE concatenates the static portion with the dynamic portion.
// We are doing the same.
// VE does not concatenate the static portion like we are doing here.
// Instead VE just ignores the static completely if dynamic binding is present.
// Because of locality we have already set the static portion because we don't know if there
// is a dynamic portion until later. If we would ignore the static portion it would look like
// tha the binding has removed it. This would confuse `[ngStyle]`/`[ngClass]` to do the wrong
// thing as it would think tha the static portion was removed. For this reason we
// concatenate it so that `[ngStyle]`/`[ngClass]` can continue to work on changed.
let staticPrefix = isClassBased ? tNode.classes : tNode.styles;
ngDevMode && isClassBased === false && staticPrefix !== null &&
assertEqual(
@ -213,8 +257,10 @@ export function checkStylingMap(
// This takes over the `[style]` binding. (Same for `[class]`)
setDirectiveInputsWhichShadowsStyling(tNode, lView, value, isClassBased);
} else {
markStylingBindingDirty(bindingIndex, isClassBased);
setElementExitFn(flushStylingOnElementExit);
updateStylingMap(
tView, tNode, lView, lView[RENDERER], lView[bindingIndex + 1],
lView[bindingIndex + 1] = toStylingArrayMap(arrayMapSet, stringParser, value),
isClassBased, bindingIndex);
}
}
}
@ -241,15 +287,7 @@ function isInHostBindings(tView: TView, bindingIndex: number): boolean {
* @param isClassBased `true` if `class` change (`false` if `style`)
*/
function stylingPropertyFirstUpdatePass(
tView: TView, prop: TStylingMapKey, suffix: null, bindingIndex: number,
isClassBased: boolean): void;
function stylingPropertyFirstUpdatePass(
tView: TView, prop: string, suffix: SanitizerFn | string | null | undefined,
bindingIndex: number, isClassBased: boolean): void;
function stylingPropertyFirstUpdatePass(
tView: TView, prop: string | TStylingMapKey,
suffixOrSanitization: SanitizerFn | string | null | undefined, bindingIndex: number,
isClassBased: boolean): void {
tView: TView, tStylingKey: TStylingKey, bindingIndex: number, isClassBased: boolean): void {
ngDevMode && assertFirstUpdatePass(tView);
const tData = tView.data;
if (tData[bindingIndex + 1] === null) {
@ -259,23 +297,326 @@ function stylingPropertyFirstUpdatePass(
// `getSelectedIndex()` should be here (rather than in instruction) so that it is guarded by the
// if so as not to read unnecessarily.
const tNode = tData[getSelectedIndex() + HEADER_OFFSET] as TNode;
if (hasStylingInputShadow(tNode, isClassBased) && typeof prop === 'object' &&
!isInHostBindings(tView, bindingIndex)) {
// typeof prop === 'object' implies that we are either `STYLE_MAP_STYLING_KEY` or
// `CLASS_MAP_STYLING_KEY` which means that we are either `[style]` or `[class]` binding.
const isHostBindings = isInHostBindings(tView, bindingIndex);
if (hasStylingInputShadow(tNode, isClassBased) && tStylingKey === null && !isHostBindings) {
// `tStylingKey === null` implies that we are either `[style]` or `[class]` binding.
// If there is a directive which uses `@Input('style')` or `@Input('class')` than
// we need to neutralize this binding since that directive is shadowing it.
// We turn this into a noop using `IGNORE_DUE_TO_INPUT_SHADOW`
prop = IGNORE_DUE_TO_INPUT_SHADOW;
// We turn this into a noop by setting the key to `false`
tStylingKey = false;
}
const tStylingKey: TStylingKey = suffixOrSanitization == null ? prop : ({
key: prop as string, extra: suffixOrSanitization
} as TStylingSuffixKey | TStylingSanitizationKey);
insertTStylingBinding(
tData, tNode, tStylingKey, bindingIndex, isActiveHostElement(), isClassBased);
insertTStylingBinding(tData, tNode, tStylingKey, bindingIndex, isHostBindings, isClassBased);
}
}
/**
* Convert user input to `ArrayMap`.
*
* This function takes user input which could be `string`, Object literal, or iterable and converts
* it into a consistent representation. The output of this is `ArrayMap` (which is an array where
* even indexes contain keys and odd indexes contain values for those keys).
*
* The advantage of converting to `ArrayMap` is that we can perform diff in a input independent way.
* (ie we can compare `foo bar` to `['bar', 'baz'] and determine a set of changes which need to be
* applied)
*
* The fact that `ArrayMap` is sorted is very important because it allows us to compute the
* difference in linear fashion without the need to allocate any additional data.
*
* For example if we kept this as a `Map` we would have to iterate over previous `Map` to determine
* which values need to be delete, over the new `Map` to determine additions, and we would have to
* keep additional `Map` to keep track of duplicates or items which have not yet been visited.
*
* @param stringParser The parser is passed in so that it will be tree shakable. See
* `styleStringParser` and `classStringParser`
* @param value The value to parse/convert to `ArrayMap`
*/
export function toStylingArrayMap(
arrayMapSet: (arrayMap: ArrayMap<any>, key: string, value: any) => void,
stringParser: (styleArrayMap: ArrayMap<any>, text: string) => void, value: string|string[]|
{[key: string]: any}|Map<any, any>|Set<any>|null|undefined): ArrayMap<any> {
if (value === null || value === undefined || value === '') return EMPTY_ARRAY as any;
const styleArrayMap: ArrayMap<any> = [] as any;
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
arrayMapSet(styleArrayMap, value[i], true);
}
} else if (typeof value === 'object') {
if (value instanceof Map) {
value.forEach((v, k) => arrayMapSet(styleArrayMap, k, v));
} else if (value instanceof Set) {
value.forEach((k) => arrayMapSet(styleArrayMap, k, true));
} else {
for (const key in value) {
if (value.hasOwnProperty(key)) {
arrayMapSet(styleArrayMap, key, value[key]);
}
}
}
} else if (typeof value === 'string') {
stringParser(styleArrayMap, value);
} else {
ngDevMode && throwError('Unsupported styling type ' + typeof value);
}
return styleArrayMap;
}
/**
* Set a `value` for a `key` taking style sanitization into account.
*
* See: `arrayMapSet` for details
*
* @param arrayMap ArrayMap to add to.
* @param key Style key to add. (This key will be checked if it needs sanitization)
* @param value The value to set (If key needs sanitization it will be sanitized)
*/
function styleArrayMapSet(arrayMap: ArrayMap<any>, key: string, value: any) {
if (stylePropNeedsSanitization(key)) {
value = ɵɵsanitizeStyle(value);
}
arrayMapSet(arrayMap, key, value);
}
/**
* Update map based styling.
*
* Map based styling could be anything which contains more than one binding. For example `string`,
* `Map`, `Set` or object literal. Dealing with all of these types would complicate the logic so
* instead this function expects that the complex input is first converted into normalized
* `ArrayMap`. The advantage of normalization is that we get the values sorted, which makes it very
* cheap to compute deltas between the previous and current value.
*
* @param tView Associated `TView.data` contains the linked list of binding priorities.
* @param tNode `TNode` where the binding is located.
* @param lView `LView` contains the values associated with other styling binding at this `TNode`.
* @param renderer Renderer to use if any updates.
* @param oldArrayMap Previous value represented as `ArrayMap`
* @param newArrayMap Current value represented as `ArrayMap`
* @param isClassBased `true` if `class` (`false` if `style`)
* @param bindingIndex Binding index of the binding.
*/
function updateStylingMap(
tView: TView, tNode: TNode, lView: LView, renderer: Renderer3, oldArrayMap: ArrayMap<any>,
newArrayMap: ArrayMap<any>, isClassBased: boolean, bindingIndex: number) {
if (oldArrayMap as ArrayMap<any>| NO_CHANGE === NO_CHANGE) {
// ON first execution the oldArrayMap is NO_CHANGE => treat is as empty ArrayMap.
oldArrayMap = EMPTY_ARRAY as any;
}
let oldIndex = 0;
let newIndex = 0;
let oldKey: string|null = 0 < oldArrayMap.length ? oldArrayMap[0] : null;
let newKey: string|null = 0 < newArrayMap.length ? newArrayMap[0] : null;
while (oldKey !== null || newKey !== null) {
ngDevMode && assertLessThan(oldIndex, 999, 'Are we stuck in infinite loop?');
ngDevMode && assertLessThan(newIndex, 999, 'Are we stuck in infinite loop?');
const oldValue = oldIndex < oldArrayMap.length ? oldArrayMap[oldIndex + 1] : undefined;
const newValue = newIndex < newArrayMap.length ? newArrayMap[newIndex + 1] : undefined;
let setKey: string|null = null;
let setValue: any = undefined;
if (oldKey === newKey) {
// UPDATE: Keys are equal => new value is overwriting old value.
oldIndex += 2;
newIndex += 2;
if (oldValue !== newValue) {
setKey = newKey;
setValue = newValue;
}
} else if (newKey === null || oldKey !== null && oldKey < newKey !) {
// DELETE: oldKey key is missing or we did not find the oldKey in the newValue.
oldIndex += 2;
setKey = oldKey;
} else {
// CREATE: newKey is less than oldKey (or no oldKey) => we have new key.
ngDevMode && assertDefined(newKey, 'Expecting to have a valid key');
newIndex += 2;
setKey = newKey;
setValue = newValue;
}
if (setKey !== null) {
updateStyling(tView, tNode, lView, renderer, setKey, setValue, isClassBased, bindingIndex);
}
oldKey = oldIndex < oldArrayMap.length ? oldArrayMap[oldIndex] : null;
newKey = newIndex < newArrayMap.length ? newArrayMap[newIndex] : null;
}
}
/**
* Update a simple (property name) styling.
*
* This function takes `prop` and updates the DOM to that value. The function takes the binding
* value as well as binding priority into consideration to determine which value should be written
* to DOM. (For example it may be determined that there is a higher priority overwrite which blocks
* the DOM write, or if the value goes to `undefined` a lower priority overwrite may be consulted.)
*
* @param tView Associated `TView.data` contains the linked list of binding priorities.
* @param tNode `TNode` where the binding is located.
* @param lView `LView` contains the values associated with other styling binding at this `TNode`.
* @param renderer Renderer to use if any updates.
* @param prop Either style property name or a class name.
* @param value Either style vale for `prop` or `true`/`false` if `prop` is class.
* @param isClassBased `true` if `class` (`false` if `style`)
* @param bindingIndex Binding index of the binding.
*/
function updateStyling(
tView: TView, tNode: TNode, lView: LView, renderer: Renderer3, prop: string,
value: string | undefined | null | boolean, isClassBased: boolean, bindingIndex: number) {
if (tNode.type !== TNodeType.Element) {
// It is possible to have styling on non-elements (such as ng-container).
// This is rare, but it does happen. In such a case, just ignore the binding.
return;
}
const tData = tView.data;
const tRange = tData[bindingIndex + 1] as TStylingRange;
const higherPriorityValue = getTStylingRangeNextDuplicate(tRange) ?
findStylingValue(tData, null, lView, prop, getTStylingRangeNext(tRange), isClassBased) :
undefined;
if (!isStylingValuePresent(higherPriorityValue)) {
// We don't have a next duplicate, or we did not find a duplicate value.
if (!isStylingValuePresent(value)) {
// We should delete current value or restore to lower priority value.
if (getTStylingRangePrevDuplicate(tRange)) {
// We have a possible prev duplicate, let's retrieve it.
value =
findStylingValue(tData, tNode, lView, prop, getTStylingRangePrev(tRange), isClassBased);
}
}
const rNode = getNativeByIndex(getSelectedIndex(), lView) as RElement;
applyStyling(renderer, isClassBased, rNode, prop, value);
}
}
/**
* Search for styling value with higher priority which is overwriting current value.
*
* When value is being applied at a location related values need to be consulted.
* - If there is a higher priority binding, we should be using that one instead.
* For example `<div [style]="{color:exp1}" [style.color]="exp2">` change to `exp1`
* requires that we check `exp2` to see if it is set to value other than `undefined`.
* - If there is a lower priority binding and we are changing to `undefined`
* For example `<div [style]="{color:exp1}" [style.color]="exp2">` change to `exp2` to
* `undefined` requires that we check `exp` (and static values) and use that as new value.
*
* NOTE: The styling stores two values.
* 1. The raw value which came from the application is stored at `index + 0` location. (This value
* is used for dirty checking).
* 2. The normalized value (converted to `ArrayMap` if map and sanitized) is stored at `index + 1`.
* The advantage of storing the sanitized value is that once the value is written we don't need
* to worry about sanitizing it later or keeping track of the sanitizer.
*
* @param tData `TData` used for traversing the priority.
* @param tNode `TNode` to use for resolving static styling. Also controls search direction.
* - `TNode` search previous and quit as soon as `isStylingValuePresent(value)` is true.
* If no value found consult `tNode.styleMap`/`tNode.classMap` for default value.
* - `null` search next and go all the way to end. Return last value where
* `isStylingValuePresent(value)` is true.
* @param lView `LView` used for retrieving the actual values.
* @param prop Property which we are interested in.
* @param index Starting index in the linked list of styling bindings where the search should start.
* @param isClassBased `true` if `class` (`false` if `style`)
*/
function findStylingValue(
tData: TData, tNode: TNode | null, lView: LView, prop: string, index: number,
isClassBased: boolean): any {
let value: any = undefined;
while (index > 0) {
const key = tData[index] as TStylingKey;
const currentValue = key === null ? arrayMapGet(lView[index + 1], prop) :
key === prop ? lView[index + 1] : undefined;
if (isStylingValuePresent(currentValue)) {
value = currentValue;
if (tNode !== null) {
return value;
}
}
const tRange = tData[index + 1] as TStylingRange;
index = tNode !== null ? getTStylingRangePrev(tRange) : getTStylingRangeNext(tRange);
}
if (tNode !== null) {
// in case where we are going in previous direction AND we did not find anything, we need to
// consult static styling
let staticArrayMap = isClassBased ? tNode.classesMap : tNode.stylesMap;
if (staticArrayMap === undefined) {
// This is the first time we are here, and we need to initialize it.
initializeStylingStaticArrayMap(tNode);
staticArrayMap = isClassBased ? tNode.classesMap : tNode.stylesMap;
}
if (staticArrayMap !== null) {
value = arrayMapGet(staticArrayMap !, prop);
}
}
return value;
}
/**
* Determines if the binding value should be used (or if the value is 'undefined' and hence priority
* resolution should be used.)
*
* @param value Binding style value.
*/
function isStylingValuePresent(value: any): boolean {
// Currently only `undefined` value is considered non-binding. That is `undefined` says I don't
// have an opinion as to what this binding should be and you should consult other bindings by
// priority to determine the valid value.
// This is extracted into a single function so that we have a single place to control this.
return value !== undefined;
}
/**
* Lazily computes `tNode.classesMap`/`tNode.stylesMap`.
*
* This code is here because we don't want to included it in `elementStart` as it would make hello
* world bigger even if no styling would be present. Instead we initialize the values here so that
* tree shaking will only bring it in if styling is present.
*
* @param tNode `TNode` to initialize.
*/
export function initializeStylingStaticArrayMap(tNode: TNode) {
ngDevMode && assertEqual(tNode.classesMap, undefined, 'Already initialized!');
ngDevMode && assertEqual(tNode.stylesMap, undefined, 'Already initialized!');
let styleMap: ArrayMap<any>|null = null;
let classMap: ArrayMap<any>|null = null;
const mergeAttrs = tNode.mergedAttrs || EMPTY_ARRAY as TAttributes;
let mode: AttributeMarker = AttributeMarker.ImplicitAttributes;
for (let i = 0; i < mergeAttrs.length; i++) {
let item = mergeAttrs[i];
if (typeof item === 'number') {
mode = item;
} else if (mode === AttributeMarker.Classes) {
classMap = classMap || [] as any;
arrayMapSet(classMap !, item as string, true);
} else if (mode === AttributeMarker.Styles) {
styleMap = styleMap || [] as any;
arrayMapSet(styleMap !, item as string, mergeAttrs[++i] as string);
}
}
tNode.classesMap = classMap;
tNode.stylesMap = styleMap;
}
/**
* Sanitizes or adds suffix to the value.
*
* If value is `null`/`undefined` no suffix is added
* @param value
* @param suffixOrSanitizer
*/
function normalizeAndApplySuffixOrSanitizer(
value: any, suffixOrSanitizer: SanitizerFn | string | undefined | null): string|null|undefined|
boolean {
if (value === null || value === undefined) {
// do nothing
} else if (typeof suffixOrSanitizer === 'function') {
// sanitize the value.
value = suffixOrSanitizer(value);
} else if (typeof suffixOrSanitizer === 'string') {
value = value + suffixOrSanitizer;
} else if (typeof value === 'object') {
value = stringify(unwrapSafeValue(value));
}
return value;
}
/**
* Tests if the `TNode` has input shadow.
*
@ -283,80 +624,8 @@ function stylingPropertyFirstUpdatePass(
* `@Input('class')` as input.
*
* @param tNode `TNode` which we would like to see if it has shadow.
* @param isClassBased `true` if `class` (`false` if `style`)
* @param isClassBased `true` if `class` (`false` if `style`)
*/
export function hasStylingInputShadow(tNode: TNode, isClassBased: boolean) {
return (tNode.flags & (isClassBased ? TNodeFlags.hasClassInput : TNodeFlags.hasStyleInput)) !== 0;
}
/**
* Flushes styling into DOM element from the bindings.
*
* The function starts at `LFrame.stylingBindingChanged` and computes new styling information from
* the bindings progressing towards the tail of the list. At the end the resulting style is written
* into the DOM Element.
*
* This function is invoked from:
* 1. Template `advance` instruction.
* 2. HostBinding instruction.
*/
function flushStylingOnElementExit() {
ngDevMode && assertEqual(
getStyleBindingChanged() > 0 || getClassBindingChanged() > 0, true,
'Only expected to be here if binding has changed.');
ngDevMode &&
assertEqual(
getCheckNoChangesMode(), false, 'Should never get here during check no changes mode');
const lView = getLView();
const tView = lView[TVIEW];
const tData = tView.data;
const elementIndex = getSelectedIndex() + HEADER_OFFSET;
const tNode = tData[elementIndex] as TNode;
const renderer = lView[RENDERER];
const element = unwrapRNode(lView[elementIndex]) as RElement;
const classBindingIndex = getClassBindingChanged();
if (classBindingIndex > 0) {
const classLastWrittenValueIndex = getTStylingRangeTail(tNode.classBindings) + 1;
ngDevMode &&
assertGreaterThan(
classLastWrittenValueIndex, 1,
'Ignoring `class` binding because there is no `class` metadata associated with the element. ' +
'(Was exception thrown during `firstUpdatePass` which prevented the metadata creation?)');
ngDevMode &&
assertLessThan(classLastWrittenValueIndex, lView.length, 'Reading past end of LView');
const lastValue: string|NO_CHANGE = lView[classLastWrittenValueIndex];
const newValue = flushStyleBinding(tData, tNode, lView, classBindingIndex, true);
if (lastValue !== newValue) {
if (tNode.type === TNodeType.Element) {
writeAndReconcileClass(
renderer, element, lastValue === NO_CHANGE ? tNode.classes || '' : lastValue as string,
newValue);
}
lView[classLastWrittenValueIndex] = newValue;
}
}
const styleBindingIndex = getStyleBindingChanged();
if (styleBindingIndex > 0) {
const styleLastWrittenValueIndex = getTStylingRangeTail(tNode.styleBindings) + 1;
ngDevMode &&
assertGreaterThan(
styleLastWrittenValueIndex, 1,
'Ignoring `style` binding because there is no `style` metadata associated with the element. ' +
'(Was exception thrown during `firstUpdatePass` which prevented the metadata creation?)');
ngDevMode &&
assertLessThan(styleLastWrittenValueIndex, lView.length, 'Reading past end of LView');
const lastValue: string|NO_CHANGE = lView[styleLastWrittenValueIndex];
const newValue = flushStyleBinding(tData, tNode, lView, styleBindingIndex, false);
if (lastValue !== newValue) {
if (tNode.type === TNodeType.Element) {
writeAndReconcileStyle(
renderer, element, lastValue === NO_CHANGE ? tNode.styles || '' : lastValue as string,
newValue);
}
lView[styleLastWrittenValueIndex] = newValue;
}
}
ngDevMode && ngDevMode.flushStyling++;
}

View File

@ -5,12 +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 {StylingMapArray, TStylingContext, TStylingRange} from '../interfaces/styling';
import {ArrayMap} from '../../util/array_utils';
import {TStylingRange} from '../interfaces/styling';
import {CssSelector} from './projection';
import {RNode} from './renderer';
import {LView, TView} from './view';
/**
* TNodeType corresponds to the {@link TNode} `type` property.
*/
@ -486,6 +489,20 @@ export interface TNode {
*/
styles: string|null;
/**
* An `ArrayMap` version of `styles.
*
* We need this when style bindings are resolving. This gets populated only if there are styling
* binding instructions. The laziness is important since we don't want to allocate the memory
* because most styling is static. For tree shaking purposes the code to create these only comes
* with styling.
*
* - `undefined': not initialized.
* - `null`: initialized but `styles` is `null`
* - `ArrayMap`: parsed version of `styles`.
*/
stylesMap: ArrayMap<any>|undefined|null;
/**
* A collection of all class bindings and/or static class values for an element.
*
@ -495,6 +512,20 @@ export interface TNode {
*/
classes: string|null;
/**
* An `ArrayMap` version of `classes`.
*
* We need this when style bindings are resolving. This gets populated only if there are styling
* binding instructions. The laziness is important since we don't want to allocate the memory
* because most styling is static. For tree shaking purposes the code to create these only comes
* with styling.
*
* - `undefined': not initialized.
* - `null`: initialized but `classes` is `null`
* - `ArrayMap`: parsed version of `S`.
*/
classesMap: ArrayMap<any>|undefined|null;
/**
* Stores the head/tail index of the class bindings.
*

View File

@ -5,538 +5,17 @@
* 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 {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {TNodeFlags} from './node';
import {ProceduralRenderer3, RElement, Renderer3} from './renderer';
import {SanitizerFn} from './sanitization';
import {LView} from './view';
/**
* --------
*
* This file contains the core interfaces for styling in Angular.
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
/**
* A static-level representation of all style or class bindings/values
* associated with a `TNode`.
*
* The `TStylingContext` unites all template styling bindings (i.e.
* `[class]` and `[style]` bindings) as well as all host-level
* styling bindings (for components and directives) together into
* a single manifest
*
* The styling context is stored on a `TNode` on and there are
* two instances of it: one for classes and another for styles.
*
* ```typescript
* tNode.styles = [ ... a context only for styles ... ];
* tNode.classes = [ ... a context only for classes ... ];
* ```
*
* The styling context is created each time there are one or more
* styling bindings (style or class bindings) present for an element,
* but is only created once per `TNode`.
*
* `tNode.styles` and `tNode.classes` can be an instance of the following:
*
* ```typescript
* 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.)
*
* The code below shows a breakdown of two instances of `TStylingContext`
* (one for `tNode.styles` and another for `tNode.classes`):
*
* ```typescript
* // <div [class.active]="c" // lView binding index = 20
* // [style.width]="x" // lView binding index = 21
* // [style.height]="y"> // lView binding index = 22
* // ...
* // </div>
* tNode.styles = [
* 1, // the total amount of sources present (only `1` b/c there are only template
* bindings)
* [null], // initial values array (an instance of `StylingMapArray`)
*
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b010, // template guard mask for height
* 0, // host bindings guard mask for height
* 'height', // the property name
* 22, // the binding location for the "y" binding in the lView
* null, // the default value for height
*
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b001, // template guard mask for width
* 0, // host bindings guard mask for width
* 'width', // the property name
* 21, // the binding location for the "x" binding in the lView
* null, // the default value for width
* ];
*
* tNode.classes = [
* 0, // the context config value (see `TStylingContextConfig`)
* 1, // the total amount of sources present (only `1` b/c there are only template
* bindings)
* [null], // initial values array (an instance of `StylingMapArray`)
*
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b001, // template guard mask for width
* 0, // host bindings guard mask for width
* 'active', // the property name
* 20, // the binding location for the "c" binding in the lView
* null, // the default value for the `active` class
* ];
* ```
*
* Entry value present in an entry (called a tuple) within the
* styling context is as follows:
*
* ```typescript
* context = [
* //...
* configValue,
* templateGuardMask,
* hostBindingsGuardMask,
* propName,
* ...bindingIndices...,
* defaultValue
* //...
* ];
* ```
*
* Below is a breakdown of each value:
*
* - **configValue**:
* Property-specific configuration values. The only config setting
* that is implemented right now is whether or not to sanitize the
* value.
*
* - **templateGuardMask**:
* A numeric value where each bit represents a binding index
* location. Each binding index location is assigned based on
* a local counter value that increments each time an instruction
* is called:
*
* ```
* <div [style.width]="x" // binding index = 21 (counter index = 0)
* [style.height]="y"> // binding index = 22 (counter index = 1)
* ```
*
* In the example code above, if the `width` value where to change
* then the first bit in the local bit mask value would be flipped
* (and the second bit for when `height`).
*
* If and when there are more than 32 binding sources in the context
* (more than 32 `[style/class]` bindings) then the bit masking will
* overflow and we are left with a situation where a `-1` value will
* represent the bit mask. Due to the way that JavaScript handles
* negative values, when the bit mask is `-1` then all bits within
* that value will be automatically flipped (this is a quick and
* efficient way to flip all bits on the mask when a special kind
* of caching scenario occurs or when there are more than 32 bindings).
*
* - **hostBindingsGuardMask**:
* Another instance of a guard mask that is specific to host bindings.
* This behaves exactly the same way as does the `templateGuardMask`,
* but will not contain any binding information processed in the template.
* The reason why there are two instances of guard masks (one for the
* template and another for host bindings) is because the template bindings
* are processed before host bindings and the state information is not
* carried over into the host bindings code. As soon as host bindings are
* processed for an element the counter and state-based bit mask values are
* set to `0`.
*
* ```
* <div [style.width]="x" // binding index = 21 (counter index = 0)
* [style.height]="y" // binding index = 22 (counter index = 1)
* dir-that-sets-width // binding index = 30 (counter index = 0)
* dir-that-sets-width> // binding index = 31 (counter index = 1)
* ```
*
* - **propName**:
* The CSS property name or class name (e.g `width` or `active`).
*
* - **bindingIndices...**:
* A series of numeric binding values that reflect where in the
* lView to find the style/class values associated with the property.
* Each value is in order in terms of priority (templates are first,
* then directives and then components). When the context is flushed
* and the style/class values are applied to the element (this happens
* inside of the `stylingApply` instruction) then the flushing code
* will keep checking each binding index against the associated lView
* to find the first style/class value that is non-null.
*
* - **defaultValue**:
* This is the default that will always be applied to the element if
* and when all other binding sources return a result that is null.
* Usually this value is `null` but it can also be a static value that
* is intercepted when the tNode is first constructured (e.g.
* `<div style="width:200px">` has a default value of `200px` for
* the `width` property).
*
* Each time a new binding is encountered it is registered into the
* context. The context then is continually updated until the first
* styling apply call has been called (which is automatically scheduled
* to be called once an element exits during change detection). Note that
* each entry in the context is stored in alphabetical order.
*
* Once styling has been flushed for the first time for an element the
* context will set as locked (this prevents bindings from being added
* to the context later on).
*
* # How Styles/Classes are Rendered
* Each time a styling instruction (e.g. `[class.name]`, `[style.prop]`,
* etc...) is executed, the associated `lView` for the view is updated
* at the current binding location. Also, when this happens, a local
* counter value is incremented. If the binding value has changed then
* a local `bitMask` variable is updated with the specific bit based
* on the counter value.
*
* Below is a lightweight example of what happens when a single style
* property is updated (i.e. `<div [style.prop]="val">`):
*
* ```typescript
* function updateStyleProp(prop: string, value: string) {
* const lView = getLView();
* const bindingIndex = BINDING_INDEX++;
*
* // update the local counter value
* const indexForStyle = stylingState.stylesCount++;
* if (lView[bindingIndex] !== value) {
* lView[bindingIndex] = value;
*
* // tell the local state that we have updated a style value
* // by updating the bit mask
* stylingState.bitMaskForStyles |= 1 << indexForStyle;
* }
* }
* ```
*
* Once all the bindings have updated a `bitMask` value will be populated.
* This `bitMask` value is used in the apply algorithm (which is called
* context resolution).
*
* ## The Apply Algorithm (Context Resolution)
* As explained above, each time a binding updates its value, the resulting
* value is stored in the `lView` array. These styling values have yet to
* be flushed to the element.
*
* Once all the styling instructions have been evaluated, then the styling
* context(s) are flushed to the element. When this happens, the context will
* be iterated over (property by property) and each binding source will be
* examined and the first non-null value will be applied to the element.
*
* Let's say that we the following template code:
*
* ```html
* <div [style.width]="w1" dir-that-set-width="w2"></div>
* ```
*
* There are two styling bindings in the code above and they both write
* to the `width` property. When styling is flushed on the element, the
* algorithm will try and figure out which one of these values to write
* to the element.
*
* In order to figure out which value to apply, the following
* binding prioritization is adhered to:
*
* 1. First template-level styling bindings are applied (if present).
* This includes things like `[style.width]` and `[class.active]`.
*
* 2. Second are styling-level host bindings present in directives.
* (if there are sub/super directives present then the sub directives
* are applied first).
*
* 3. Third are styling-level host bindings present in components.
* (if there are sub/super components present then the sub directives
* are applied first).
*
* This means that in the code above the styling binding present in the
* template is applied first and, only if its falsy, then the directive
* styling binding for width will be applied.
*
* ### What about map-based styling bindings?
* Map-based styling bindings are activated when there are one or more
* `[style]` and/or `[class]` bindings present on an element. When this
* code is activated, the apply algorithm will iterate over each map
* entry and apply each styling value to the element with the same
* prioritization rules as above.
*
* For the algorithm to apply styling values efficiently, the
* styling map entries must be applied in sync (property by property)
* with prop-based bindings. (The map-based algorithm is described
* more inside of the `render3/styling/map_based_bindings.ts` file.)
*
* ## Sanitization
* Sanitization is used to prevent invalid style values from being applied to
* the element.
*
* It is enabled in two cases:
*
* 1. The `styleSanitizer(sanitizerFn)` instruction was called (just before any other
* styling instructions are run).
*
* 2. The component/directive `LView` instance has a sanitizer object attached to it
* (this happens when `renderComponent` is executed with a `sanitizer` value or
* if the ngModule contains a sanitizer provider attached to it).
*
* If and when sanitization is active then all property/value entries will be evaluated
* through the active sanitizer before they are applied to the element (or the styling
* debug handler).
*
* If a `Sanitizer` object is used (via the `LView[SANITIZER]` value) then that object
* will be used for every property.
*
* If a `StyleSanitizerFn` function is used (via the `styleSanitizer`) then it will be
* called in two ways:
*
* 1. property validation mode: this will be called early to mark whether a property
* should be sanitized or not at during the flushing stage.
*
* 2. value sanitization mode: this will be called during the flushing stage and will
* run the sanitizer function against the value before applying it to the element.
*
* 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|StylingMapArray|{}> {
/** The total amount of sources present in the context */
[TStylingContextIndex.TotalSourcesPosition]: number;
/** Initial value position for static styles */
[TStylingContextIndex.InitialStylingValuePosition]: StylingMapArray;
}
/**
* An index of position and offset values used to navigate the `TStylingContext`.
*/
export const enum TStylingContextIndex {
TotalSourcesPosition = 0,
InitialStylingValuePosition = 1,
ValuesStartPosition = 2,
// each tuple entry in the context
// (config, templateBitGuard, hostBindingBitGuard, prop, ...bindings||default-value)
ConfigOffset = 0,
TemplateBitGuardOffset = 1,
HostBindingsBitGuardOffset = 2,
PropOffset = 3,
BindingsStartOffset = 4
}
/**
* A series of flags used for each property entry within the `TStylingContext`.
*/
export const enum TStylingContextPropConfigFlags {
Default = 0b0,
SanitizationRequired = 0b1,
TotalBits = 1,
Mask = 0b1,
}
/**
* A function used to apply or remove styling from an element for a given property.
*/
export interface ApplyStylingFn {
(renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string, value: any,
bindingIndex?: number|null): void;
}
/**
* Runtime data type that is used to store binding data referenced from the `TStylingContext`.
*
* Because `LView` is just an array with data, there is no reason to
* special case `LView` everywhere in the styling algorithm. By allowing
* this data type to be an array that contains various scalar data types,
* an instance of `LView` doesn't need to be constructed for tests.
*/
export type LStylingData = LView | (string | number | boolean | null)[];
/**
* Array-based representation of a key/value array.
*
* The format of the array is "property", "value", "property2",
* "value2", etc...
*
* The first value in the array is reserved to store the instance
* 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 StylingMapArray extends Array<{}|string|number|null|undefined> {
/**
* The last raw value used to generate the entries in the map.
*/
[StylingMapArrayIndex.RawValuePosition]: {}|string|number|null|undefined;
}
/**
* An index of position and offset points for any data stored within a `StylingMapArray` instance.
*/
export const enum StylingMapArrayIndex {
/** Where the values start in the array */
ValuesStartPosition = 1,
/** The location of the raw key/value map instance used last to populate the array entries */
RawValuePosition = 0,
/** The size of each property/value entry */
TupleSize = 2,
/** The offset for the property entry in the tuple */
PropOffset = 0,
/** The offset for the value entry in the tuple */
ValueOffset = 1,
}
/**
* Used to apply/traverse across all map-based styling entries up to the provided `targetProp`
* value.
*
* 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.
*
* If `targetProp` value is provided the iteration code will stop once it reaches
* the property (if found). Otherwise if the target property is not encountered then
* it will stop once it reaches the next value that appears alphabetically after it.
*
* If a `defaultValue` is provided then it will be applied to the element only if the
* `targetProp` property value is encountered and the value associated with the target
* property is `null`. The reason why the `defaultValue` is needed is to avoid having the
* algorithm apply a `null` value and then apply a default value afterwards (this would
* end up being two style property writes).
*
* @returns whether or not the target property was reached and its value was
* applied to the element.
*/
export interface SyncStylingMapsFn {
(context: TStylingContext, renderer: Renderer3|ProceduralRenderer3|null, element: RElement,
data: LStylingData, sourceIndex: number, applyStylingFn: ApplyStylingFn,
sanitizer: StyleSanitizeFn|null, mode: StylingMapsSyncMode, targetProp?: string|null,
defaultValue?: boolean|string|null): boolean;
}
/**
* Used to direct how map-based values are applied/traversed when styling is flushed.
*/
export const enum StylingMapsSyncMode {
/** Only traverse values (no prop/value styling entries get applied) */
TraverseValues = 0b000,
/** Apply every prop/value styling entry to the element */
ApplyAllValues = 0b001,
/** Only apply the target prop/value entry */
ApplyTargetProp = 0b010,
/** Skip applying the target prop/value entry */
SkipTargetProp = 0b100,
/** Iterate over inner maps map values in the context */
RecurseInnerMaps = 0b1000,
/** Only check to see if a value was set somewhere in each map */
CheckValuesOnly = 0b10000,
}
/**
* Simplified `TNode` interface for styling-related code.
*
* The styling algorithm code only needs access to `flags`.
*/
export interface TStylingNode { flags: TNodeFlags; }
/**
* Value stored in the `TData` which is needed to re-concatenate the styling.
*
* - `string`: Stores the property name. Used with `ɵɵstyleProp`/`ɵɵclassProp` instruction which
* don't have suffix or don't need sanitization.
* - `string`: Stores the property name. Used with `ɵɵstyleProp`/`ɵɵclassProp` instruction.
* - `null`: Represents map, so there is no name. Used with `ɵɵstyleMap`/`ɵɵclassMap`.
* - `false`: Represents an ignore case. This happens when `ɵɵstyleProp`/`ɵɵclassProp` instruction
* is combined with directive which shadows its input `@Input('class')`. That way the binding
* should not participate in the styling resolution.
*/
export type TStylingKey = string | TStylingSuffixKey | TStylingSanitizationKey | TStylingMapKey;
/**
* For performance reasons we want to make sure that all subclasses have the same shape object.
*
* See subclasses for implementation details.
*/
export interface TStylingKeyShape {
key: string|null;
extra: string|SanitizerFn|TStylingMapFn;
}
/**
* Used in the case of `ɵɵstyleProp('width', exp, 'px')`.
*/
export interface TStylingSuffixKey extends TStylingKeyShape {
/// Stores the property key.
key: string;
/// Stores the property suffix.
extra: string;
}
/**
* Used in the case of `ɵɵstyleProp('url', exp, styleSanitizationFn)`.
*/
export interface TStylingSanitizationKey extends TStylingKeyShape {
/// Stores the property key.
key: string;
/// Stores sanitization function.
extra: SanitizerFn;
}
/**
* Used in the case of `ɵɵstyleMap()`/`ɵɵclassMap()`.
*/
export interface TStylingMapKey extends TStylingKeyShape {
/// There is no key
key: null;
/// Invoke this function to process the value (convert it into the result)
/// This is implemented this way so that the logic associated with `ɵɵstyleMap()`/`ɵɵclassMap()`
/// can be tree shaken away. Internally the function will break the `Map`/`Array` down into
/// parts and call `appendStyling` on parts.
///
/// See: `CLASS_MAP_STYLING_KEY` and `STYLE_MAP_STYLING_KEY` for details.
extra: TStylingMapFn;
}
/**
* Invoke this function to process the styling value which is non-primitive (Map/Array)
* This is implemented this way so that the logic associated with `ɵɵstyleMap()`/`ɵɵclassMap()`
* can be tree shaken away. Internally the function will break the `Map`/`Array` down into
* parts and call `appendStyling` on parts.
*
* See: `CLASS_MAP_STYLING_KEY` and `STYLE_MAP_STYLING_KEY` for details.
*/
export type TStylingMapFn = (text: string, value: any, hasPreviousDuplicate: boolean) => string;
export type TStylingKey = string | null | false;
/**
* This is a branded number which contains previous and next index.

View File

@ -6,9 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Renderer2} from '../core';
import {ViewEncapsulation} from '../metadata/view';
import {addToArray, removeFromArray} from '../util/array_utils';
import {assertDefined, assertDomNode, assertEqual} from '../util/assert';
import {assertDefined, assertDomNode, assertEqual, assertString} from '../util/assert';
import {assertLContainer, assertLView, assertTNodeForLView} from './assert';
import {attachPatchData} from './context_discovery';
import {ACTIVE_INDEX, ActiveIndexFlag, CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS, NATIVE, unusedValueExportToPlacateAjd as unused1} from './interfaces/container';
@ -906,3 +907,102 @@ function applyContainer(
applyView(renderer, action, lView, renderParent, anchor);
}
}
/**
* Writes class/style to element.
*
* @param renderer Renderer to use.
* @param isClassBased `true` if it should be written to `class` (`false` to write to `style`)
* @param rNode The Node to write to.
* @param prop Property to write to. This would be the class/style name.
* @param value Value to wiret. If `null`/`undefined`/`false` this is consider a remove (set/add
* otherwise).
*/
export function applyStyling(
renderer: Renderer3, isClassBased: boolean, rNode: RElement, prop: string, value: any) {
const isProcedural = isProceduralRenderer(renderer);
if (isClassBased) {
if (!value) { // We actually want JS falseness here
ngDevMode && ngDevMode.rendererRemoveClass++;
if (isProcedural) {
(renderer as Renderer2).removeClass(rNode, prop);
} else {
(rNode as HTMLElement).classList.remove(prop);
}
} else {
ngDevMode && ngDevMode.rendererAddClass++;
if (isProcedural) {
(renderer as Renderer2).addClass(rNode, prop);
} else {
ngDevMode && assertDefined((rNode as HTMLElement).classList, 'HTMLElement expected');
(rNode as HTMLElement).classList.add(prop);
}
}
} else {
// TODO(misko): Can't import RendererStyleFlags2.DashCase as it causes imports to be resolved in
// different order which causes failures. Using direct constant as workaround for now.
const flags = prop.indexOf('-') == -1 ? undefined : 2 /* RendererStyleFlags2.DashCase */;
if (value === null || value === undefined) {
ngDevMode && ngDevMode.rendererRemoveStyle++;
if (isProcedural) {
(renderer as Renderer2).removeStyle(rNode, prop, flags);
} else {
(rNode as HTMLElement).style.removeProperty(prop);
}
} else {
ngDevMode && ngDevMode.rendererSetStyle++;
if (isProcedural) {
(renderer as Renderer2).setStyle(rNode, prop, value, flags);
} else {
ngDevMode && assertDefined((rNode as HTMLElement).style, 'HTMLElement expected');
(rNode as HTMLElement).style.setProperty(prop, value);
}
}
}
}
/**
* Write `cssText` to `RElement`.
*
* This function does direct write without any reconciliation. Used for writing initial values, so
* that static styling values do not pull in the style parser.
*
* @param renderer Renderer to use
* @param element The element which needs to be updated.
* @param newValue The new class list to write.
*/
export function writeDirectStyle(renderer: Renderer3, element: RElement, newValue: string) {
ngDevMode && assertString(newValue, '\'newValue\' should be a string');
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(element, 'style', newValue);
} else {
(element as HTMLElement).style.cssText = newValue;
}
ngDevMode && ngDevMode.rendererSetStyle++;
}
/**
* Write `className` to `RElement`.
*
* This function does direct write without any reconciliation. Used for writing initial values, so
* that static styling values do not pull in the style parser.
*
* @param renderer Renderer to use
* @param element The element which needs to be updated.
* @param newValue The new class list to write.
*/
export function writeDirectClass(renderer: Renderer3, element: RElement, newValue: string) {
ngDevMode && assertString(newValue, '\'newValue\' should be a string');
if (isProceduralRenderer(renderer)) {
if (newValue === '') {
// There are tests in `google3` which expect `element.getAttribute('class')` to be `null`.
renderer.removeAttribute(element, 'class');
} else {
renderer.setAttribute(element, 'class', newValue);
}
} else {
element.className = newValue;
}
ngDevMode && ngDevMode.rendererSetClassName++;
}

View File

@ -9,7 +9,6 @@
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {assertDefined, assertEqual, assertGreaterThan} from '../util/assert';
import {assertLViewOrUndefined} from './assert';
import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {TNode} from './interfaces/node';
import {CONTEXT, DECLARATION_VIEW, LView, OpaqueViewState, TVIEW} from './interfaces/view';
@ -105,18 +104,6 @@ interface LFrame {
* We iterate over the list of Queries and increment current query index at every step.
*/
currentQueryIndex: number;
/**
* Stores the index of the style binding which changed first.
*
* A change in styling binding implies that all bindings starting with this index need to be
* recomputed. See: `flushStylingOnElementExit` and `markStylingBindingDirty` functions for
* details.
*
* If this value is set then `flushStylingOnElementExit` needs to execute during the `advance`
* instruction to update the styling.
*/
stylingBindingChanged: number;
}
/**
@ -162,19 +149,11 @@ interface InstructionState {
* Necessary to support ChangeDetectorRef.checkNoChanges().
*/
checkNoChangesMode: boolean;
/**
* Function to be called when the element is exited.
*
* NOTE: The function is here for tree shakable purposes since it is only needed by styling.
*/
elementExitFn: (() => void)|null;
}
export const instructionState: InstructionState = {
lFrame: createLFrame(null),
bindingsEnabled: true,
elementExitFn: null,
checkNoChangesMode: false,
};
@ -254,28 +233,6 @@ export function getLView(): LView {
return lFrame === null ? null ! : lFrame.lView;
}
/**
* Flags used for an active element during change detection.
*
* These flags are used within other instructions to inform cleanup or
* exit operations to run when an element is being processed.
*
* Note that these flags are reset each time an element changes (whether it
* happens when `advance()` is run or when change detection exits out of a template
* function or when all host bindings are processed for an element).
*/
export const enum ActiveElementFlags {
HostMode = 0b1,
RunExitFn = 0b1,
Size = 1,
}
export function isActiveHostElement(): boolean {
return (instructionState.lFrame.selectedIndex & ActiveElementFlags.HostMode) ===
ActiveElementFlags.HostMode;
}
/**
* Sets the active directive host element and resets the directive id value
* (when the provided elementIndex value has changed).
@ -284,45 +241,13 @@ export function isActiveHostElement(): boolean {
* the directive/component instance lives
*/
export function setActiveHostElement(elementIndex: number) {
executeElementExitFn();
setSelectedIndex(elementIndex);
instructionState.lFrame.selectedIndex |= ActiveElementFlags.HostMode;
}
export function clearActiveHostElement() {
executeElementExitFn();
setSelectedIndex(-1);
}
export function executeElementExitFn() {
const lFrame = instructionState.lFrame;
if (lFrame.stylingBindingChanged !== 0) {
instructionState.elementExitFn !();
lFrame.stylingBindingChanged = 0;
}
}
/**
* Queues a function to be run once the element is "exited" in CD.
*
* Change detection will focus on an element either when the `advance()`
* instruction is called or when the template or host bindings instruction
* code is invoked. The element is then "exited" when the next element is
* selected or when change detection for the template or host bindings is
* complete. When this occurs (the element change operation) then an exit
* function will be invoked if it has been set. This function can be used
* to assign that exit function.
*
* @param fn
*/
export function setElementExitFn(fn: () => void): void {
if (instructionState.elementExitFn === null) {
instructionState.elementExitFn = fn;
}
ngDevMode &&
assertEqual(instructionState.elementExitFn, fn, 'Expecting to always get the same function');
}
/**
* Restores `contextViewData` to the given OpaqueViewState instance.
*
@ -409,8 +334,9 @@ export function incrementBindingIndex(count: number): number {
* 0 index and we just shift the root so that they match next available location in the LView.
* @param value
*/
export function setBindingRoot(value: number) {
instructionState.lFrame.bindingRootIndex = value;
export function setBindingRootForHostBindings(value: number) {
const lframe = instructionState.lFrame;
lframe.bindingIndex = lframe.bindingRootIndex = value;
}
export function getCurrentQueryIndex(): number {
@ -482,7 +408,6 @@ export function enterView(newView: LView, tNode: TNode | null): void {
newLFrame.bindingRootIndex = -1;
newLFrame.bindingIndex = newView === null ? -1 : newView[TVIEW].bindingStartIndex;
newLFrame.currentQueryIndex = 0;
newLFrame.stylingBindingChanged = 0;
}
/**
@ -510,17 +435,11 @@ function createLFrame(parent: LFrame | null): LFrame {
currentQueryIndex: 0, //
parent: parent !, //
child: null, //
stylingBindingChanged: 0, //
};
parent !== null && (parent.child = lFrame); // link the new LFrame for reuse.
return lFrame;
}
export function leaveViewProcessExit() {
executeElementExitFn();
leaveView();
}
export function leaveView() {
instructionState.lFrame = instructionState.lFrame.parent;
}
@ -549,7 +468,7 @@ function walkUpViews(nestingLevel: number, currentView: LView): LView {
* current `LView` to act on.
*/
export function getSelectedIndex() {
return instructionState.lFrame.selectedIndex >> ActiveElementFlags.Size;
return instructionState.lFrame.selectedIndex;
}
/**
@ -562,7 +481,7 @@ export function getSelectedIndex() {
* run if and when the provided `index` value is different from the current selected index value.)
*/
export function setSelectedIndex(index: number) {
instructionState.lFrame.selectedIndex = index << ActiveElementFlags.Size;
instructionState.lFrame.selectedIndex = index;
}
@ -628,40 +547,3 @@ const enum BindingChanged {
CLASS_SHIFT = 16,
STYLE_MASK = 0xFFFF,
}
/**
* Store the first binding location from where the style flushing should start.
*
* This function stores the first binding location. Any subsequent binding changes are ignored as
* they are downstream from this change and will be picked up once the flushing starts traversing
* forward.
*
* Because flushing for template and flushing for host elements are separate, we don't need to worry
* about the fact that they will be out of order.
*
* @param bindingIndex Index of binding location. This will be a binding location from which the
* flushing of styling should start.
* @param isClassBased `true` if `class` change (`false` if `style`)
*/
export function markStylingBindingDirty(bindingIndex: number, isClassBased: boolean) {
ngDevMode && assertGreaterThan(bindingIndex, 0, 'expected valid binding index changed');
ngDevMode &&
assertEqual(
getCheckNoChangesMode(), false, 'Should never get here during check no changes mode');
const lFrame = instructionState.lFrame;
const stylingBindingChanged = lFrame.stylingBindingChanged;
const stylingBindingChangedExtracted = isClassBased ?
stylingBindingChanged >> BindingChanged.CLASS_SHIFT :
stylingBindingChanged & BindingChanged.STYLE_MASK;
if (stylingBindingChangedExtracted === 0) {
lFrame.stylingBindingChanged = stylingBindingChanged |
(isClassBased ? bindingIndex << BindingChanged.CLASS_SHIFT : bindingIndex);
}
}
export function getClassBindingChanged() {
return instructionState.lFrame.stylingBindingChanged >> BindingChanged.CLASS_SHIFT;
}
export function getStyleBindingChanged() {
return instructionState.lFrame.stylingBindingChanged & BindingChanged.STYLE_MASK;
}

View File

@ -8,134 +8,8 @@
import {assertNotEqual} from '../../util/assert';
import {CharCode} from '../../util/char_code';
import {concatStringsWithSpace} from '../../util/stringify';
import {consumeWhitespace, getLastParsedKey, parseClassName, parseClassNameNext} from './styling_parser';
/**
* Computes the diff between two class-list strings.
*
* Example:
* `oldValue` => `"A B C"`
* `newValue` => `"A C D"`
* will result in:
* ```
* new Map([
* ['A', null],
* ['B', false],
* ['C', null],
* ['D', true]
* ])
* ```
*
* @param oldValue Previous class-list string.
* @param newValue New class-list string.
* @returns A `Map` which will be filled with changes.
* - `true`: Class needs to be added to the element.
* - `false: Class needs to be removed from the element.
* - `null`: No change (leave class as is.)
*/
export function computeClassChanges(oldValue: string, newValue: string): Map<string, boolean|null> {
const changes = new Map<string, boolean|null>();
splitClassList(oldValue, changes, false);
splitClassList(newValue, changes, true);
return changes;
}
/**
* Splits the class list into array, ignoring whitespace and add it to corresponding categories
* `changes`.
*
* @param text Class list to split
* @param changes Map which will be filled with changes. (`false` - remove; `null` - noop;
* `true` - add.)
* @param isNewValue `true` if we are processing new list.
*/
export function splitClassList(
text: string, changes: Map<string, boolean|null>, isNewValue: boolean): void {
for (let i = parseClassName(text); i >= 0; i = parseClassNameNext(text, i)) {
processClassToken(changes, getLastParsedKey(text), isNewValue);
}
}
/**
* Processes the token by adding it to the `changes` Map.
*
* @param changes Map which keeps track of what should be done with each value.
* - `false` The token should be deleted. (It was in old list, but not in new list.)
* - `null` The token should be ignored. (It was present in old list as well as new list.)
* - `true` the token should be added. (It was only present in the new value)
* @param token Token to add to set.
* @param isNewValue True if invocation represents an addition (removal otherwise.)
* - `false` means that we are processing the old value, which may need to be deleted.
* Initially all tokens are labeled `false` (remove it.)
* - `true` means that we are processing new value which may need to be added. If a token
* with same key already exists with `false` then the resulting token is `null` (no
* change.) If no token exists then the new token value is `true` (add it.)
*/
export function processClassToken(
changes: Map<string, boolean|null>, token: string, isNewValue: boolean) {
if (isNewValue) {
// This code path is executed when we are iterating over new values.
const existingTokenValue = changes.get(token);
if (existingTokenValue === undefined) {
// the new list has a token which is not present in the old list. Mark it for addition.
changes.set(token, true);
} else if (existingTokenValue === false) {
// If the existing value is `false` this means it was in the old list. Because it is in the
// new list as well we marked it as `null` (noop.)
changes.set(token, null);
}
} else {
// This code path is executed when we are iterating over previous values.
// This means that we store the tokens in `changes` with `false` (removals).
changes.set(token, false);
}
}
/**
* Toggles a class in `className` string.
*
* @param className A string containing classes (whitespace separated)
* @param classToToggle A class name to remove or add to the `className`
* @param toggle Whether the resulting `className` should contain or not the `classToToggle`
* @returns a new class-list which does not have `classToRemove`
*/
export function toggleClass(className: string, classToToggle: string, toggle: boolean): string {
if (className === '') {
return toggle ? classToToggle : '';
}
let start = 0;
let end = className.length;
while (start < end) {
start = classIndexOf(className, classToToggle, start);
if (start === -1) {
if (toggle === true) {
className = concatStringsWithSpace(className, classToToggle);
}
break;
}
if (toggle === true) {
// we found it and we should have it so just return
return className;
} else {
const length = classToToggle.length;
// Cut out the class which should be removed.
const endWhitespace = consumeWhitespace(className, start + length, end);
if (endWhitespace === end) {
// If we are the last token then we need back search trailing whitespace.
while (start > 0 && className.charCodeAt(start - 1) <= CharCode.SPACE) {
start--;
}
}
className = className.substring(0, start) + className.substring(endWhitespace, end);
end = className.length;
}
}
return className;
}
/**
* Returns an index of `classToSearch` in `className` taking token boundaries into account.
*

View File

@ -1,216 +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 {assertDefined, assertString} from '../../util/assert';
import {ProceduralRenderer3, RElement, Renderer3, isProceduralRenderer} from '../interfaces/renderer';
import {computeClassChanges} from './class_differ';
import {computeStyleChanges} from './style_differ';
/**
* Writes new `className` value in the DOM node.
*
* In its simplest form this function just writes the `newValue` into the `element.className`
* property.
*
* However, under some circumstances this is more complex because there could be other code which
* has added `class` information to the DOM element. In such a case writing our new value would
* clobber what is already on the element and would result in incorrect behavior.
*
* To solve the above the function first reads the `element.className` to see if it matches the
* `expectedValue`. (In our case `expectedValue` is just last value written into the DOM.) In this
* way we can detect to see if anyone has modified the DOM since our last write.
* - If we detect no change we simply write: `element.className = newValue`.
* - If we do detect change then we compute the difference between the `expectedValue` and
* `newValue` and then use `element.classList.add` and `element.classList.remove` to modify the
* DOM.
*
* NOTE: Some platforms (such as NativeScript and WebWorkers) will not have `element.className`
* available and reading the value will result in `undefined`. This means that for those platforms
* we will always fail the check and will always use `element.classList.add` and
* `element.classList.remove` to modify the `element`. (A good mental model is that we can do
* `element.className === expectedValue` but we may never know the actual value of
* `element.className`)
*
* @param renderer Renderer to use
* @param element The element which needs to be updated.
* @param expectedValue The expected (previous/old) value of the class list which we will use to
* check if out of bounds modification has happened to the `element`.
* @param newValue The new class list to write.
*/
export function writeAndReconcileClass(
renderer: Renderer3, element: RElement, expectedValue: string, newValue: string): void {
ngDevMode && assertDefined(element, 'Expecting DOM element');
ngDevMode && assertString(expectedValue, '\'oldValue\' should be a string');
ngDevMode && assertString(newValue, '\'newValue\' should be a string');
if (element.className === expectedValue) {
writeDirectClass(renderer, element, newValue);
} else {
// The expected value is not the same as last value. Something changed the DOM element without
// our knowledge so we need to do reconciliation instead.
reconcileClassNames(renderer, element, expectedValue, newValue);
}
}
/**
* Write `className` to `RElement`.
*
* This function does direct write without any reconciliation. Used for writing initial values, so
* that static styling values do not pull in the style parser.
*
* @param renderer Renderer to use
* @param element The element which needs to be updated.
* @param newValue The new class list to write.
*/
export function writeDirectClass(renderer: Renderer3, element: RElement, newValue: string) {
ngDevMode && assertString(newValue, '\'newValue\' should be a string');
if (isProceduralRenderer(renderer)) {
if (newValue === '') {
// There are tests in `google3` which expect `element.getAttribute('class')` to be `null`.
// TODO(commit): add test case
renderer.removeAttribute(element, 'class');
} else {
renderer.setAttribute(element, 'class', newValue);
}
} else {
element.className = newValue;
}
ngDevMode && ngDevMode.rendererSetClassName++;
}
/**
* Writes new `cssText` value in the DOM node.
*
* In its simplest form this function just writes the `newValue` into the `element.style.cssText`
* property.
*
* However, under some circumstances this is more complex because there could be other code which
* has added `style` information to the DOM element. In such a case writing our new value would
* clobber what is already on the element and would result in incorrect behavior.
*
* To solve the above the function first reads the `element.style.cssText` to see if it matches the
* `expectedValue`. (In our case `expectedValue` is just last value written into the DOM.) In this
* way we can detect to see if anyone has modified the DOM since our last write.
* - If we detect no change we simply write: `element.style.cssText = newValue`
* - If we do detect change then we compute the difference between the `expectedValue` and
* `newValue` and then use `element.style[property]` to modify the DOM.
*
* NOTE: Some platforms (such as NativeScript and WebWorkers) will not have `element.style`
* available and reading the value will result in `undefined` This means that for those platforms we
* will always fail the check and will always use `element.style[property]` to
* modify the `element`. (A good mental model is that we can do `element.style.cssText ===
* expectedValue` but we may never know the actual value of `element.style.cssText`)
*
* @param renderer Renderer to use
* @param element The element which needs to be updated.
* @param expectedValue The expected (previous/old) value of the class list to write.
* @param newValue The new class list to write
*/
export function writeAndReconcileStyle(
renderer: Renderer3, element: RElement, expectedValue: string, newValue: string): void {
ngDevMode && assertDefined(element, 'Expecting DOM element');
ngDevMode && assertString(expectedValue, '\'expectedValue\' should be a string');
ngDevMode && assertString(newValue, '\'newValue\' should be a string');
const style = expectedValue === null ? null : (element as HTMLElement).style;
if (expectedValue === null || style != null && (style !.cssText === expectedValue)) {
writeDirectStyle(renderer, element, newValue);
} else {
// The expected value is not the same as last value. Something changed the DOM element without
// our knowledge so we need to do reconciliation instead.
reconcileStyleNames(renderer, element, expectedValue, newValue);
}
}
/**
* Write `cssText` to `RElement`.
*
* This function does direct write without any reconciliation. Used for writing initial values, so
* that static styling values do not pull in the style parser.
*
* @param renderer Renderer to use
* @param element The element which needs to be updated.
* @param newValue The new class list to write.
*/
export function writeDirectStyle(renderer: Renderer3, element: RElement, newValue: string) {
ngDevMode && assertString(newValue, '\'newValue\' should be a string');
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(element, 'style', newValue);
} else {
(element as HTMLElement).style.cssText = newValue;
}
ngDevMode && ngDevMode.rendererSetStyle++;
}
/**
* Writes to `classNames` by computing the difference between `oldValue` and `newValue` and using
* `classList.add` and `classList.remove`.
*
* NOTE: Keep this a separate function so that `writeAndReconcileClass` is small and subject to
* inlining. (We expect that this function will be called rarely.)
*
* @param renderer Renderer to use when updating DOM.
* @param element The native element to update.
* @param oldValue Old value of `classNames`.
* @param newValue New value of `classNames`.
*/
function reconcileClassNames(
renderer: Renderer3, element: RElement, oldValue: string, newValue: string) {
const isProcedural = isProceduralRenderer(renderer);
computeClassChanges(oldValue, newValue).forEach((classValue, className) => {
if (classValue === true) {
if (isProcedural) {
(renderer as ProceduralRenderer3).addClass(element, className);
} else {
(element as HTMLElement).classList.add(className);
}
ngDevMode && ngDevMode.rendererAddClass++;
} else if (classValue === false) {
if (isProcedural) {
(renderer as ProceduralRenderer3).removeClass(element, className);
} else {
(element as HTMLElement).classList.remove(className);
}
ngDevMode && ngDevMode.rendererRemoveClass++;
}
});
}
/**
* Writes to `styles` by computing the difference between `oldValue` and `newValue` and using
* `styles.setProperty` and `styles.removeProperty`.
*
* NOTE: Keep this a separate function so that `writeAndReconcileStyle` is small and subject to
* inlining. (We expect that this function will be called rarely.)
*
* @param renderer Renderer to use when updating DOM.
* @param element The DOM element to update.
* @param oldValue Old value of `classNames`.
* @param newValue New value of `classNames`.
*/
function reconcileStyleNames(
renderer: Renderer3, element: RElement, oldValue: string, newValue: string) {
const isProcedural = isProceduralRenderer(renderer);
const changes = computeStyleChanges(oldValue, newValue);
changes.forEach((styleValue, styleName) => {
const newValue = styleValue.new;
if (newValue === null) {
if (isProcedural) {
(renderer as ProceduralRenderer3).removeStyle(element, styleName);
} else {
(element as HTMLElement).style.removeProperty(styleName);
}
ngDevMode && ngDevMode.rendererRemoveStyle++;
} else if (styleValue.old !== newValue) {
if (isProcedural) {
(renderer as ProceduralRenderer3).setStyle(element, styleName, newValue);
} else {
(element as HTMLElement).style.setProperty(styleName, newValue);
}
ngDevMode && ngDevMode.rendererSetStyle++;
}
});
}

View File

@ -6,24 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {unwrapSafeValue} from '../../sanitization/bypass';
import {stylePropNeedsSanitization, ɵɵsanitizeStyle} from '../../sanitization/sanitization';
import {assertEqual, assertString, throwError} from '../../util/assert';
import {CharCode} from '../../util/char_code';
import {concatStringsWithSpace} from '../../util/stringify';
import {assertEqual} from '../../util/assert';
import {assertFirstUpdatePass} from '../assert';
import {TNode} from '../interfaces/node';
import {SanitizerFn} from '../interfaces/sanitization';
import {TStylingKey, TStylingMapKey, TStylingRange, getTStylingRangeNext, getTStylingRangePrev, getTStylingRangePrevDuplicate, setTStylingRangeNext, setTStylingRangeNextDuplicate, setTStylingRangePrev, setTStylingRangePrevDuplicate, toTStylingRange} from '../interfaces/styling';
import {LView, TData, TVIEW} from '../interfaces/view';
import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangePrev, setTStylingRangeNext, setTStylingRangeNextDuplicate, setTStylingRangePrev, setTStylingRangePrevDuplicate, toTStylingRange} from '../interfaces/styling';
import {TData, TVIEW} from '../interfaces/view';
import {getLView} from '../state';
import {NO_CHANGE} from '../tokens';
import {splitClassList, toggleClass} from './class_differ';
import {StyleChangesMap, parseKeyValue, removeStyle} from './style_differ';
import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseStyleNext} from './styling_parser';
/**
* NOTE: The word `styling` is used interchangeably as style or class styling.
*
@ -328,8 +319,7 @@ function markDuplicates(
tData: TData, tStylingKey: TStylingKey, index: number, staticValues: string, isPrevDir: boolean,
isClassBinding: boolean) {
const tStylingAtIndex = tData[index + 1] as TStylingRange;
const key: string|null = typeof tStylingKey === 'object' ? tStylingKey.key : tStylingKey;
const isMap = key === null;
const isMap = tStylingKey === null;
let cursor =
isPrevDir ? getTStylingRangePrev(tStylingAtIndex) : getTStylingRangeNext(tStylingAtIndex);
let foundDuplicate = false;
@ -340,9 +330,8 @@ function markDuplicates(
while (cursor !== 0 && (foundDuplicate === false || isMap)) {
const tStylingValueAtCursor = tData[cursor] as TStylingKey;
const tStyleRangeAtCursor = tData[cursor + 1] as TStylingRange;
const keyAtCursor = typeof tStylingValueAtCursor === 'object' ? tStylingValueAtCursor.key :
tStylingValueAtCursor;
if (keyAtCursor === null || key == null || keyAtCursor === key) {
if (tStylingValueAtCursor === null || tStylingKey == null ||
tStylingValueAtCursor === tStylingKey) {
foundDuplicate = true;
tData[cursor + 1] = isPrevDir ? setTStylingRangeNextDuplicate(tStyleRangeAtCursor) :
setTStylingRangePrevDuplicate(tStyleRangeAtCursor);
@ -365,7 +354,7 @@ function markDuplicates(
i >= 0; //
i = isClassBinding ? parseClassNameNext(staticValues, i) :
parseStyleNext(staticValues, i)) {
if (getLastParsedKey(staticValues) === key) {
if (getLastParsedKey(staticValues) === tStylingKey) {
foundDuplicate = true;
break;
}
@ -378,222 +367,3 @@ function markDuplicates(
setTStylingRangeNextDuplicate(tStylingAtIndex);
}
}
/**
* Computes the new styling value starting at `index` styling binding.
*
* @param tData `TData` containing the styling binding linked list.
* - `TData[index]` contains the binding name.
* - `TData[index + 1]` contains the `TStylingRange` a linked list of other bindings.
* @param tNode `TNode` containing the initial styling values.
* @param lView `LView` containing the styling values.
* - `LView[index]` contains the binding value.
* - `LView[index + 1]` contains the concatenated value up to this point.
* @param index the location in `TData`/`LView` where the styling search should start.
* @param isClassBinding `true` if binding to `className`; `false` when binding to `style`.
*/
export function flushStyleBinding(
tData: TData, tNode: TNode, lView: LView, index: number, isClassBinding: boolean): string {
const tStylingRangeAtIndex = tData[index + 1] as TStylingRange;
// When styling changes we don't have to start at the begging. Instead we start at the change
// value and look up the previous concatenation as a starting point going forward.
const lastUnchangedValueIndex = getTStylingRangePrev(tStylingRangeAtIndex);
let text = lastUnchangedValueIndex === 0 ?
(isClassBinding ? tNode.classes : tNode.styles) :
lView[lastUnchangedValueIndex + 1] as string | NO_CHANGE;
if (text === null || text === NO_CHANGE) text = '';
ngDevMode && assertString(text, 'Last unchanged value should be a string');
let cursor = index;
while (cursor !== 0) {
const value = lView[cursor];
const key = tData[cursor] as TStylingKey;
const stylingRange = tData[cursor + 1] as TStylingRange;
lView[cursor + 1] = text = appendStyling(
text as string, key, value, null, getTStylingRangePrevDuplicate(stylingRange),
isClassBinding);
cursor = getTStylingRangeNext(stylingRange);
}
return text as string;
}
/**
* Append new styling to the currently concatenated styling text.
*
* This function concatenates the existing `className`/`cssText` text with the binding value.
*
* @param text Text to concatenate to.
* @param stylingKey `TStylingKey` holding the key (className or style property name).
* @param value The value for the key.
* - `isClassBinding === true`
* - `boolean` if `true` then add the key to the class list string.
* - `Array` add each string value to the class list string.
* - `Object` add object key to the class list string if the key value is truthy.
* - `isClassBinding === false`
* - `Array` Not supported.
* - `Object` add object key/value to the styles.
* @param sanitizer Optional sanitizer to use. If `null` the `stylingKey` sanitizer will be used.
* This is provided so that `ɵɵstyleMap()`/`ɵɵclassMap()` can recursively call
* `appendStyling` without having ta package the sanitizer into `TStylingSanitizationKey`.
* @param hasPreviousDuplicate determines if there is a chance of duplicate.
* - `true` the existing `text` should be searched for duplicates and if any found they
* should be removed.
* - `false` Fast path, just concatenate the strings.
* @param isClassBinding Determines if the `text` is `className` or `cssText`.
* @returns new styling string with the concatenated values.
*/
export function appendStyling(
text: string, stylingKey: TStylingKey, value: any, sanitizer: SanitizerFn | null,
hasPreviousDuplicate: boolean, isClassBinding: boolean): string {
let key: string;
let suffixOrSanitizer: string|SanitizerFn|undefined|null = sanitizer;
if (typeof stylingKey === 'object') {
if (stylingKey.key === null) {
return value != null ? stylingKey.extra(text, value, hasPreviousDuplicate) : text;
} else {
suffixOrSanitizer = stylingKey.extra;
key = stylingKey.key;
}
} else {
key = stylingKey;
}
if (isClassBinding) {
ngDevMode && assertEqual(typeof stylingKey === 'string', true, 'Expecting key to be string');
if (hasPreviousDuplicate) {
text = toggleClass(text, stylingKey as string, !!value);
} else if (value) {
text = concatStringsWithSpace(text, stylingKey as string);
}
} else {
if (value === undefined) {
// If undefined than treat it as if we have no value. This means that we will fallback to the
// previous value (if any).
// `<div style="width: 10px" [style.width]="{width: undefined}">` => `width: 10px`.
return text;
}
if (hasPreviousDuplicate) {
text = removeStyle(text, key);
}
if (value !== false && value !== null) {
// `<div style="width: 10px" [style.width]="{width: null}">` => ``. (remove it)
// `<div style="width: 10px" [style.width]="{width: false}">` => ``. (remove it)
value = typeof suffixOrSanitizer === 'function' ? suffixOrSanitizer(value) :
unwrapSafeValue(value);
const keyValue = key + ': ' +
(typeof suffixOrSanitizer === 'string' ? value + suffixOrSanitizer : value) + ';';
text = concatStringsWithSpace(text, keyValue);
}
}
return text;
}
/**
* `ɵɵclassMap()` inserts `CLASS_MAP_STYLING_KEY` as a key to the `insertTStylingBinding()`.
*
* The purpose of this key is to add class map abilities to the concatenation in a tree shakable
* way. If `ɵɵclassMap()` is not referenced than `CLASS_MAP_STYLING_KEY` will become eligible for
* tree shaking.
*
* This key supports: `strings`, `object` (as Map) and `Array`. In each case it is necessary to
* break the classes into parts and concatenate the parts into the `text`. The concatenation needs
* to be done in parts as each key is individually subject to overwrites.
*/
export const CLASS_MAP_STYLING_KEY: TStylingMapKey = {
key: null,
extra: (text: string, value: any, hasPreviousDuplicate: boolean): string => {
if (Array.isArray(value)) {
// We support Arrays
for (let i = 0; i < value.length; i++) {
text = appendStyling(text, value[i], true, null, hasPreviousDuplicate, true);
}
} else if (typeof value === 'object') {
// We support maps
for (let key in value) {
if (key !== '') {
// We have to guard for `""` empty string as key since it will break search and replace.
text = appendStyling(text, key, value[key], null, hasPreviousDuplicate, true);
}
}
} else if (typeof value === 'string') {
// We support strings
if (hasPreviousDuplicate) {
// We need to parse and process it.
const changes = new Map<string, boolean|null>();
splitClassList(value, changes, false);
changes.forEach((_, key) => text = appendStyling(text, key, true, null, true, true));
} else {
// No duplicates, just append it.
text = concatStringsWithSpace(text, value);
}
} else {
// All other cases are not supported.
ngDevMode && throwError('Unsupported value for class binding: ' + value);
}
return text;
}
};
/**
* `ɵɵstyleMap()` inserts `STYLE_MAP_STYLING_KEY` as a key to the `insertTStylingBinding()`.
*
* The purpose of this key is to add style map abilities to the concatenation in a tree shakable
* way. If `ɵɵstyleMap()` is not referenced than `STYLE_MAP_STYLING_KEY` will become eligible for
* tree shaking. (`STYLE_MAP_STYLING_KEY` also pulls in the sanitizer as `ɵɵstyleMap()` could have
* a sanitizable property.)
*
* This key supports: `strings`, and `object` (as Map). In each case it is necessary to
* break the style into parts and concatenate the parts into the `text`. The concatenation needs
* to be done in parts as each key is individually subject to overwrites.
*/
export const STYLE_MAP_STYLING_KEY: TStylingMapKey = {
key: null,
extra: (text: string, value: any, hasPreviousDuplicate: boolean): string => {
if (Array.isArray(value)) {
// We don't support Arrays
ngDevMode && throwError('Style bindings do not support array bindings: ' + value);
} else if (typeof value === 'object') {
// We support maps
for (let key in value) {
if (key !== '') {
// We have to guard for `""` empty string as key since it will break search and replace.
text = appendStyling(
text, key, value[key], stylePropNeedsSanitization(key) ? ɵɵsanitizeStyle : null,
hasPreviousDuplicate, false);
}
}
} else if (typeof value === 'string') {
// We support strings
if (hasPreviousDuplicate) {
// We need to parse and process it.
const changes: StyleChangesMap =
new Map<string, {old: string | null, new: string | null}>();
parseKeyValue(value, changes, false);
changes.forEach(
(value, key) => text = appendStyling(
text, key, value.old, stylePropNeedsSanitization(key) ? ɵɵsanitizeStyle : null,
true, false));
} else {
// No duplicates, just append it.
if (value.charCodeAt(value.length - 1) !== CharCode.SEMI_COLON) {
value += ';';
}
text = concatStringsWithSpace(text, value);
}
} else {
// All other cases are not supported.
ngDevMode && throwError('Unsupported value for style binding: ' + value);
}
return text;
}
};
/**
* If we have `<div [class] my-dir>` such that `my-dir` has `@Input('class')`, the `my-dir` captures
* the `[class]` binding, so that it no longer participates in the style bindings. For this case
* we use `IGNORE_DUE_TO_INPUT_SHADOW` so that `flushStyleBinding` ignores it.
*/
export const IGNORE_DUE_TO_INPUT_SHADOW: TStylingMapKey = {
key: null,
extra: (text: string, value: any, hasPreviousDuplicate: boolean): string => { return text;}
};

View File

@ -1,131 +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 {concatStringsWithSpace} from '../../util/stringify';
import {consumeWhitespace, getLastParsedKey, getLastParsedValue, parseStyle, parseStyleNext, resetParserState} from './styling_parser';
/**
* Stores changes to Style values.
* - `key`: style name.
* - `value`:
* - `old`: previous value (or `null`)
* - `new`: new value (or `null`).
*
* If `old === new` do nothing.
* If `old === null` then add `new`.
* If `new === null` then remove `old`.
*/
export type StyleChangesMap = Map<string, {old: string | null, new: string | null}>;
/**
* Computes the diff between two style strings.
*
* Example:
* `oldValue` => `"a: 1; b: 2, c: 3"`
* `newValue` => `"b: 2; c: 4; d: 5;"`
* will result in:
* ```
* changes = Map(
* 'a', { old: '1', new: null },
* 'b', { old: '2', new: '2' },
* 'c', { old: '3', new: '4' },
* 'd', { old: null, new: '5' },
* )
* ``
*
* @param oldValue Previous style string.
* @param newValue New style string.
* @returns `StyleChangesArrayMap`.
*/
export function computeStyleChanges(oldValue: string, newValue: string): StyleChangesMap {
const changes: StyleChangesMap = new Map<string, {old: string | null, new: string | null}>();
parseKeyValue(oldValue, changes, false);
parseKeyValue(newValue, changes, true);
return changes;
}
/**
* Splits the style list into array, ignoring whitespace and add it to corresponding categories
* changes.
*
* @param text Style list to split
* @param changes Where changes will be stored.
* @param isNewValue `true` if parsing new value (effects how values get added to `changes`)
*/
export function parseKeyValue(text: string, changes: StyleChangesMap, isNewValue: boolean): void {
for (let i = parseStyle(text); i >= 0; i = parseStyleNext(text, i)) {
processStyleKeyValue(changes, getLastParsedKey(text), getLastParsedValue(text), isNewValue);
}
}
/**
* Appends style `key`/`value` information into the list of `changes`.
*
* Once all of the parsing is complete, the `changes` will contain a
* set of operations which need to be performed on the DOM to reconcile it.
*
* @param changes An `StyleChangesMap which tracks changes.
* @param key Style key to be added to the `changes`.
* @param value Style value to be added to the `changes`.
* @param isNewValue true if `key`/`value` should be processed as new value.
*/
function processStyleKeyValue(
changes: StyleChangesMap, key: string, value: string, isNewValue: boolean): void {
if (isNewValue) {
// This code path is executed when we are iterating over new values.
const existing = changes.get(key);
if (existing === undefined) {
// Key we have not seen before
changes.set(key, styleKeyValue(null, value));
} else {
// Already seen, update value.
existing.new = value;
}
} else {
// This code path is executed when we are iteration over previous values.
changes.set(key, styleKeyValue(value, null));
}
}
function styleKeyValue(oldValue: string | null, newValue: string | null) {
return {old: oldValue, new: newValue};
}
/**
* Removes a style from a `cssText` string.
*
* @param cssText A string which contains styling.
* @param styleToRemove A style (and its value) to remove from `cssText`.
* @returns a new style text which does not have `styleToRemove` (and its value)
*/
export function removeStyle(cssText: string, styleToRemove: string): string {
if (cssText.indexOf(styleToRemove) === -1) {
// happy case where we don't need to invoke parser.
return cssText;
}
let lastValueEnd = 0;
for (let i = parseStyle(cssText); i >= 0; i = parseStyleNext(cssText, i)) {
const key = getLastParsedKey(cssText);
if (key === styleToRemove) {
// Consume any remaining whitespace.
i = consumeWhitespace(cssText, i, cssText.length);
if (lastValueEnd === 0) {
cssText = cssText.substring(i);
i = 0;
} else if (i === cssText.length) {
return cssText.substring(0, lastValueEnd);
} else {
cssText = concatStringsWithSpace(cssText.substring(0, lastValueEnd), cssText.substring(i));
i = lastValueEnd + 1; // 1 is for ';'.length(so that we skip the separator)
}
resetParserState(cssText);
}
lastValueEnd = i;
}
return cssText;
}

View File

@ -1,162 +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 {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {TNode, TNodeFlags} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, TStylingContext, TStylingContextIndex, TStylingNode, TStylingRange, getTStylingRangePrev} from '../interfaces/styling';
import {TData} from '../interfaces/view';
import {getCurrentStyleSanitizer} from '../state';
import {attachDebugObject} from '../util/debug_utils';
/**
* --------
*
* This file contains the core debug functionality for styling in Angular.
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
/**
* A debug-friendly version of `TStylingContext`.
*
* An instance of this is attached to `tStylingContext.debug` when `ngDevMode` is active.
*/
export interface DebugStylingContext {
/** The configuration settings of the associated `TStylingContext` */
config: DebugStylingConfig;
/** The associated TStylingContext instance */
context: TStylingContext;
/** The associated TStylingContext instance */
entries: {[prop: string]: DebugStylingContextEntry};
/** A status report of all the sources within the context */
printSources(): void;
/** A status report of all the entire context as a table */
printTable(): void;
}
/**
* A debug/testing-oriented summary of all styling information in `TNode.flags`.
*/
export interface DebugStylingConfig {
hasMapBindings: boolean; //
hasPropBindings: boolean; //
hasCollisions: boolean; //
hasTemplateBindings: boolean; //
hasHostBindings: boolean; //
allowDirectStyling: boolean; //
}
/**
* A debug/testing-oriented summary of all styling entries within a `TStylingContext`.
*/
export interface DebugStylingContextEntry {
/** The property (style or class property) that this entry represents */
prop: string;
/** The total amount of styling entries a part of this entry */
valuesCount: number;
/**
* The bit guard mask that is used to compare and protect against
* styling changes when any template style/class bindings update
*/
templateBitMask: number;
/**
* The bit guard mask that is used to compare and protect against
* styling changes when any host style/class bindings update
*/
hostBindingsBitMask: number;
/**
* Whether or not the entry requires sanitization
*/
sanitizationRequired: boolean;
/**
* The default value that will be applied if any bindings are falsy
*/
defaultValue: string|boolean|null;
/**
* All bindingIndex sources that have been registered for this style
*/
sources: (number|null|string)[];
}
/**
* A debug/testing-oriented summary of all styling entries for a `DebugNode` instance.
*/
export interface DebugNodeStyling {
/** The associated debug context of the TStylingContext instance */
context: DebugStylingContext;
/**
* A summarization of each style/class property
* present in the context
*/
summary: {[propertyName: string]: DebugNodeStylingEntry};
/**
* A key/value map of all styling properties and their
* runtime values
*/
values: {[propertyName: string]: string | number | null | boolean};
/**
* Overrides the sanitizer used to process styles
*/
overrideSanitizer(sanitizer: StyleSanitizeFn|null): void;
}
/**
* A debug/testing-oriented summary of a styling entry.
*
* A value such as this is generated as an artifact of the `DebugStyling`
* summary.
*/
export interface DebugNodeStylingEntry {
/** The style/class property that the summary is attached to */
prop: string;
/** The last applied value for the style/class property */
value: string|null;
/** The binding index of the last applied style/class property */
bindingIndex: number|null;
}
/**
* Find the head of the styling binding linked list.
*/
export function getStylingBindingHead(tData: TData, tNode: TNode, isClassBinding: boolean): number {
let index = getTStylingRangePrev(isClassBinding ? tNode.classBindings : tNode.styleBindings);
while (true) {
const tStylingRange = tData[index + 1] as TStylingRange;
const prev = getTStylingRangePrev(tStylingRange);
if (prev === 0) {
// found head exit.
return index;
} else {
index = prev;
}
}
}

View File

@ -70,7 +70,7 @@ export function getLastParsedValue(text: string): string {
*
* This function is intended to be used in this format:
* ```
* for (let i = parseClassName(text); i >= 0; i = parseClassNameNext(text, i))) {
* for (let i = parseClassName(text); i >= 0; i = parseClassNameNext(text, i)) {
* const key = getLastParsedKey();
* ...
* }
@ -88,7 +88,7 @@ export function parseClassName(text: string): number {
*
* This function is intended to be used in this format:
* ```
* for (let i = parseClassName(text); i >= 0; i = parseClassNameNext(text, i))) {
* for (let i = parseClassName(text); i >= 0; i = parseClassNameNext(text, i)) {
* const key = getLastParsedKey();
* ...
* }

View File

@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertEqual} from '../util/assert';
export const enum BypassType {
Url = 'URL',

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertLessThanOrEqual} from './assert';
import {assertEqual, assertLessThanOrEqual} from './assert';
/**
* Equivalent to ES6 spread, add each item to an array.
@ -322,6 +322,7 @@ export function arrayMapDelete<V>(arrayMap: ArrayMap<V>, key: string): number {
* inserted)
*/
function _arrayIndexOfSorted(array: string[], value: string, shift: number): number {
ngDevMode && assertEqual(Array.isArray(array), true, 'Expecting an array');
let start = 0;
let end = array.length >> shift;
while (end !== start) {

View File

@ -9,6 +9,10 @@
import {getSymbolIterator} from './symbol';
export function isIterable(obj: any): obj is Iterable<any> {
return obj !== null && typeof obj === 'object' && (obj as any)[getSymbolIterator()] !== undefined;
}
export function isListLikeIterable(obj: any): boolean {
if (!isJsObject(obj)) return false;
return Array.isArray(obj) ||

View File

@ -51,7 +51,6 @@ declare global {
rendererAppendChild: number;
rendererInsertBefore: number;
rendererCreateComment: number;
flushStyling: number;
}
}
@ -81,7 +80,6 @@ export function ngDevModeResetPerfCounters(): NgDevModePerfCounters {
rendererAppendChild: 0,
rendererInsertBefore: 0,
rendererCreateComment: 0,
flushStyling: 0,
};
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.

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);
}

View File

@ -145,7 +145,7 @@ class DefaultServerRenderer2 implements Renderer2 {
setStyle(el: any, style: string, value: any, flags: RendererStyleFlags2): void {
style = style.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
const styleMap = _readStyleAttribute(el);
styleMap[style] = value || '';
styleMap[style] = value == null ? '' : value;
_writeStyleAttribute(el, styleMap);
}
@ -276,7 +276,7 @@ function _writeStyleAttribute(element: any, styleMap: {[name: string]: string})
let styleAttrValue = '';
for (const key in styleMap) {
const newValue = styleMap[key];
if (newValue) {
if (newValue != null) {
styleAttrValue += key + ':' + styleMap[key] + ';';
}
}

View File

@ -703,8 +703,8 @@ export declare function ɵɵattributeInterpolate8(attrName: string, prefix: stri
export declare function ɵɵattributeInterpolateV(attrName: string, values: any[], sanitizer?: SanitizerFn, namespace?: string): typeof ɵɵattributeInterpolateV;
export declare function ɵɵclassMap(classes: {
[className: string]: boolean | null | undefined;
} | Map<string, boolean | undefined | null> | Set<string> | string[] | string | null | undefined): void;
[className: string]: boolean | undefined | null;
} | Map<string, boolean | undefined | null> | Set<string> | string[] | string | undefined | null): void;
export declare function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void;
@ -724,7 +724,7 @@ export declare function ɵɵclassMapInterpolate8(prefix: string, v0: any, i0: st
export declare function ɵɵclassMapInterpolateV(values: any[]): void;
export declare function ɵɵclassProp(className: string, value: boolean | null | undefined): typeof ɵɵclassProp;
export declare function ɵɵclassProp(className: string, value: boolean | undefined | null): typeof ɵɵclassProp;
export declare type ɵɵComponentDefWithMeta<T, Selector extends String, ExportAs extends string[], InputMap extends {
[key: string]: string;
@ -1035,9 +1035,9 @@ export declare function ɵɵstaticViewQuery<T>(predicate: Type<any> | string[],
export declare function ɵɵstyleMap(styles: {
[styleName: string]: any;
} | Map<string, string | number | null | undefined> | string | null | undefined): void;
} | Map<string, string | number | null | undefined> | string | undefined | null): void;
export declare function ɵɵstyleProp(prop: string, value: string | number | SafeValue | null | undefined, suffix?: string | null): typeof ɵɵstyleProp;
export declare function ɵɵstyleProp(prop: string, value: string | number | SafeValue | undefined | null, suffix?: string | null): typeof ɵɵstyleProp;
export declare function ɵɵstylePropInterpolate1(prop: string, prefix: string, v0: any, suffix: string, valueSuffix?: string | null): typeof ɵɵstylePropInterpolate1;