diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json index a616f87284..9ee4492d84 100755 --- a/aio/scripts/_payload-limits.json +++ b/aio/scripts/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 2987, - "main-es2015": 456890, + "main-es2015": 455803, "polyfills-es2015": 52503 } } diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index a66545d810..4ff1e37a2e 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -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 } diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index aa45902db6..e1862adc6a 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -684,7 +684,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, 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; diff --git a/packages/core/src/render/api.ts b/packages/core/src/render/api.ts index cbd9343128..edc0bc9a5f 100644 --- a/packages/core/src/render/api.ts +++ b/packages/core/src/render/api.ts @@ -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. */ diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 5ec1ec3c75..3d9b8888c3 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -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'; diff --git a/packages/core/src/render3/instructions/advance.ts b/packages/core/src/render3/instructions/advance.ts index 3ba2dcab3d..fb09a8ab41 100644 --- a/packages/core/src/render3/instructions/advance.ts +++ b/packages/core/src/render3/instructions/advance.ts @@ -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) { diff --git a/packages/core/src/render3/instructions/class_map_interpolation.ts b/packages/core/src/render3/instructions/class_map_interpolation.ts index 5387faf798..409b987e09 100644 --- a/packages/core/src/render3/instructions/class_map_interpolation.ts +++ b/packages/core/src/render3/instructions/class_map_interpolation.ts @@ -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); } diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index c851ad43dd..b8ac798d69 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -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'; diff --git a/packages/core/src/render3/instructions/embedded_view.ts b/packages/core/src/render3/instructions/embedded_view.ts index f6ff3b550f..46ef37b186 100644 --- a/packages/core/src/render3/instructions/embedded_view.ts +++ b/packages/core/src/render3/instructions/embedded_view.ts @@ -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); } diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index 791ef2bd43..1f3d5d163a 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -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|undefined|null, // public classes: string|null, // + public classesMap: ArrayMap|undefined|null, // public classBindings: TStylingRange, // public styleBindings: TStylingRange, // ) {} diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 998b70bf7e..c4ac30268e 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -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( lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass); } } finally { - leaveViewProcessExit(); + leaveView(); } } @@ -540,7 +537,6 @@ function executeTemplate( } 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, }; diff --git a/packages/core/src/render3/instructions/styling.ts b/packages/core/src/render3/instructions/styling.ts index 2418a15dbb..15baf9d315 100644 --- a/packages/core/src/render3/instructions/styling.ts +++ b/packages/core/src/render3/instructions/styling.ts @@ -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 | null | - undefined): void { - checkStylingMap(STYLE_MAP_STYLING_KEY, styles, false); + styles: {[styleName: string]: any} | Map| 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, 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| Set| string[] | string | null | undefined): void { - checkStylingMap(CLASS_MAP_STYLING_KEY, classes, true); + classes: {[className: string]: boolean | undefined | null} | + Map| Set| 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, 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, key: string, value: any) => void, + stringParser: (styleArrayMap: ArrayMap, 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, key: string, value: any) => void, + stringParser: (styleArrayMap: ArrayMap, text: string) => void, value: string|string[]| + {[key: string]: any}|Map|Set|null|undefined): ArrayMap { + if (value === null || value === undefined || value === '') return EMPTY_ARRAY as any; + const styleArrayMap: ArrayMap = [] 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, 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, + newArrayMap: ArrayMap, isClassBased: boolean, bindingIndex: number) { + if (oldArrayMap as ArrayMap| 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 `
` 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 `
` 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|null = null; + let classMap: ArrayMap|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++; -} \ No newline at end of file diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 3992291252..73d58b2bf7 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -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|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|undefined|null; + /** * Stores the head/tail index of the class bindings. * diff --git a/packages/core/src/render3/interfaces/styling.ts b/packages/core/src/render3/interfaces/styling.ts index fffdb5a8e2..aeb585bd86 100644 --- a/packages/core/src/render3/interfaces/styling.ts +++ b/packages/core/src/render3/interfaces/styling.ts @@ -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. `
`) - * tNode.styles = TStylingContext; // one or more styling bindings present (e.g. `
`) - * ``` - * - * Both `tNode.styles` and `tNode.classes` are instantiated when anything - * styling-related is active on an element. They are first created from - * from the any of the element-level instructions (e.g. `element`, - * `elementStart`, `elementHostAttrs`). When any static style/class - * values are encountered they are registered on the `tNode.styles` - * and `tNode.classes` data-structures. By default (when any static - * values are encountered) the `tNode.styles` or `tNode.classes` values - * are instances of a `StylingMapArray`. Only when style/class bindings - * are detected then that styling map is converted into an instance of - * `TStylingContext`. - * - * Due to the fact the the `TStylingContext` is stored on a `TNode` - * this means that all data within the context is static. Instead of - * storing actual styling binding values, the lView binding index values - * are stored within the context. (static nature means it is more compact.) - * - * The code below shows a breakdown of two instances of `TStylingContext` - * (one for `tNode.styles` and another for `tNode.classes`): - * - * ```typescript - * //
// lView binding index = 22 - * // ... - * //
- * 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: - * - * ``` - *
// 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`. - * - * ``` - *
// 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. - * `
` 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. `
`): - * - * ```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 - *
- * ``` - * - * 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 { - /** 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. diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 1f22e6da4f..0b173b4663 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -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++; +} \ No newline at end of file diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index 4aaa4f751d..31faa40935 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -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; -} \ No newline at end of file diff --git a/packages/core/src/render3/styling/class_differ.ts b/packages/core/src/render3/styling/class_differ.ts index 24338ecc82..36b8f05c0b 100644 --- a/packages/core/src/render3/styling/class_differ.ts +++ b/packages/core/src/render3/styling/class_differ.ts @@ -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 { - const changes = new Map(); - 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, 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, 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. * diff --git a/packages/core/src/render3/styling/reconcile.ts b/packages/core/src/render3/styling/reconcile.ts deleted file mode 100644 index 9d6c78bfd3..0000000000 --- a/packages/core/src/render3/styling/reconcile.ts +++ /dev/null @@ -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++; - } - }); -} diff --git a/packages/core/src/render3/styling/style_binding_list.ts b/packages/core/src/render3/styling/style_binding_list.ts index f73e28c33a..9c180208f8 100644 --- a/packages/core/src/render3/styling/style_binding_list.ts +++ b/packages/core/src/render3/styling/style_binding_list.ts @@ -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). - // `
` => `width: 10px`. - return text; - } - if (hasPreviousDuplicate) { - text = removeStyle(text, key); - } - if (value !== false && value !== null) { - // `
` => ``. (remove it) - // `
` => ``. (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(); - 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(); - 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 `
` 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;} -}; \ No newline at end of file diff --git a/packages/core/src/render3/styling/style_differ.ts b/packages/core/src/render3/styling/style_differ.ts deleted file mode 100644 index 1238d01ec4..0000000000 --- a/packages/core/src/render3/styling/style_differ.ts +++ /dev/null @@ -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; - -/** - * 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(); - 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; -} diff --git a/packages/core/src/render3/styling/styling_debug.ts b/packages/core/src/render3/styling/styling_debug.ts deleted file mode 100644 index 937c09aadb..0000000000 --- a/packages/core/src/render3/styling/styling_debug.ts +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/packages/core/src/render3/styling/styling_parser.ts b/packages/core/src/render3/styling/styling_parser.ts index 84f711049e..89f24757a1 100644 --- a/packages/core/src/render3/styling/styling_parser.ts +++ b/packages/core/src/render3/styling/styling_parser.ts @@ -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(); * ... * } diff --git a/packages/core/src/sanitization/bypass.ts b/packages/core/src/sanitization/bypass.ts index a72b20ada6..7a0272cd30 100644 --- a/packages/core/src/sanitization/bypass.ts +++ b/packages/core/src/sanitization/bypass.ts @@ -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', diff --git a/packages/core/src/util/array_utils.ts b/packages/core/src/util/array_utils.ts index 6c7fc38047..fa27103a3a 100644 --- a/packages/core/src/util/array_utils.ts +++ b/packages/core/src/util/array_utils.ts @@ -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(arrayMap: ArrayMap, 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) { diff --git a/packages/core/src/util/iterable.ts b/packages/core/src/util/iterable.ts index 4f85e0cbdc..27d1bd9de9 100644 --- a/packages/core/src/util/iterable.ts +++ b/packages/core/src/util/iterable.ts @@ -9,6 +9,10 @@ import {getSymbolIterator} from './symbol'; +export function isIterable(obj: any): obj is Iterable { + 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) || diff --git a/packages/core/src/util/ng_dev_mode.ts b/packages/core/src/util/ng_dev_mode.ts index 70a9b65d8a..119c391f96 100644 --- a/packages/core/src/util/ng_dev_mode.ts +++ b/packages/core/src/util/ng_dev_mode.ts @@ -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. diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index 8fdd3da292..5deae41c4f 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.ts @@ -222,8 +222,8 @@ describe('styling', () => { return; @Component({ template: ` -
- CONTENT +
+ CONTENT
` }) 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: ` -
- ` +
+ ` }) 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: ` +
+ ` + }) + 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: `
`}) - class MyComp { - myStyles = {}; - } + onlyInIvy('[style] bindings are ivy only') + .it('should convert camelCased style property names to snake-case', () => { + @Component({template: `
`}) + 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]'}) diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index b56ac936cc..6bbafbb610 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -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" diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 6c68029791..ed4904d76b 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -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" diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 3917b6be2c..db28e91f9c 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -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" }, diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 894feb8eb9..1bcd79c5a1 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -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(); } }); }); diff --git a/packages/core/test/render3/instructions/lview_debug_spec.ts b/packages/core/test/render3/instructions/lview_debug_spec.ts index 6b5a3f5800..67207f1854 100644 --- a/packages/core/test/render3/instructions/lview_debug_spec.ts +++ b/packages/core/test/render3/instructions/lview_debug_spec.ts @@ -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, diff --git a/packages/core/test/render3/instructions/shared_spec.ts b/packages/core/test/render3/instructions/shared_spec.ts new file mode 100644 index 0000000000..54f114da16 --- /dev/null +++ b/packages/core/test/render3/instructions/shared_spec.ts @@ -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() {} \ No newline at end of file diff --git a/packages/core/test/render3/instructions/styling_spec.ts b/packages/core/test/render3/instructions/styling_spec.ts new file mode 100644 index 0000000000..e8c0e33b0d --- /dev/null +++ b/packages/core/test/render3/instructions/styling_spec.ts @@ -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([['X', 'x'], ['A', 'a']]))) + .toEqual(['A', 'a', 'X', 'x'] as any); + }); + }); + describe('Iterable', () => { + it('should parse', () => { + expect(toStylingArrayMap(arrayMapSet, null !, new Set(['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)); +} \ No newline at end of file diff --git a/packages/core/test/render3/perf/BUILD.bazel b/packages/core/test/render3/perf/BUILD.bazel index c92cc5e155..a580272711 100644 --- a/packages/core/test/render3/perf/BUILD.bazel +++ b/packages/core/test/render3/perf/BUILD.bazel @@ -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", -) diff --git a/packages/core/test/render3/perf/split_class_list.ts b/packages/core/test/render3/perf/split_class_list.ts deleted file mode 100644 index 2b1ec9d341..0000000000 --- a/packages/core/test/render3/perf/split_class_list.ts +++ /dev/null @@ -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(); -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 { - a.clear(); -} \ No newline at end of file diff --git a/packages/core/test/render3/styling_next/class_differ_spec.ts b/packages/core/test/render3/styling_next/class_differ_spec.ts index 3c377803e5..45aed5f4ca 100644 --- a/packages/core/test/render3/styling_next/class_differ_spec.ts +++ b/packages/core/test/render3/styling_next/class_differ_spec.ts @@ -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(); - 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(map: Map, 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)); -} diff --git a/packages/core/test/render3/styling_next/reconcile_spec.ts b/packages/core/test/render3/styling_next/reconcile_spec.ts deleted file mode 100644 index 8ee940a6c4..0000000000 --- a/packages/core/test/render3/styling_next/reconcile_spec.ts +++ /dev/null @@ -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;'); - }); - }); - }); -}); diff --git a/packages/core/test/render3/styling_next/style_binding_list_spec.ts b/packages/core/test/render3/styling_next/style_binding_list_spec.ts index 45fe5238c3..b0b413c5cf 100644 --- a/packages/core/test/render3/styling_next/style_binding_list_spec.ts +++ b/packages/core/test/render3/styling_next/style_binding_list_spec.ts @@ -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); - } } \ No newline at end of file diff --git a/packages/core/test/render3/styling_next/style_differ_spec.ts b/packages/core/test/render3/styling_next/style_differ_spec.ts deleted file mode 100644 index b8ce8f0f33..0000000000 --- a/packages/core/test/render3/styling_next/style_differ_spec.ts +++ /dev/null @@ -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(); - parseKeyValue(text, changes, false); - const list: any[] = []; - sortedForEach(changes, (value, key) => list.push(key, value.old, value.new)); - return expect(list); -} \ No newline at end of file diff --git a/packages/platform-server/src/server_renderer.ts b/packages/platform-server/src/server_renderer.ts index 7767196f52..83f2f830dc 100644 --- a/packages/platform-server/src/server_renderer.ts +++ b/packages/platform-server/src/server_renderer.ts @@ -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] + ';'; } } diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index b9ec3e0da5..1b3220ba04 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -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 | Set | string[] | string | null | undefined): void; + [className: string]: boolean | undefined | null; +} | Map | Set | 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(predicate: Type | string[], export declare function ɵɵstyleMap(styles: { [styleName: string]: any; -} | Map | string | null | undefined): void; +} | Map | 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;