diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json index c6d1019a2e..fb4863b46b 100755 --- a/aio/scripts/_payload-limits.json +++ b/aio/scripts/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 2987, - "main-es2015": 448306, + "main-es2015": 448928, "polyfills-es2015": 52195 } } diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index 1f3d5d163a..5c5926127d 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -15,7 +15,7 @@ import {initNgDevMode} from '../../util/ng_dev_mode'; import {ACTIVE_INDEX, ActiveIndexFlag, CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS, NATIVE} from '../interfaces/container'; import {DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition'; import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from '../interfaces/i18n'; -import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node'; +import {PropertyAliases, TConstants, TContainerNode, TDirectiveDefs, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node'; import {SelectorFlags} from '../interfaces/projection'; import {TQueries} from '../interfaces/query'; import {RComment, RElement, RNode} from '../interfaces/renderer'; @@ -176,11 +176,12 @@ 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 residualStyles: ArrayMap|undefined|null, // public classes: string|null, // - public classesMap: ArrayMap|undefined|null, // + public residualClasses: ArrayMap|undefined|null, // public classBindings: TStylingRange, // public styleBindings: TStylingRange, // + public directives: TDirectiveDefs|null, // ) {} get type_(): string { @@ -240,9 +241,7 @@ class TNode implements ITNode { export const TNodeDebug = TNode; export type TNodeDebug = TNode; -export interface DebugStyleBindings extends Array { - [0]: string|null; -} +export interface DebugStyleBindings extends Array|DebugStyleBinding|string|null> {} export interface DebugStyleBinding { key: TStylingKey; index: number; @@ -276,7 +275,7 @@ function toDebugStyleBinding(tNode: TNode, isClassBased: boolean): DebugStyleBin if (cursor === prev) isTemplate = false; cursor = getTStylingRangePrev(itemRange); } - bindings.unshift(isClassBased ? tNode.classes : tNode.styles); + bindings.push((isClassBased ? tNode.residualClasses : tNode.residualStyles) || null); return bindings; } diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 716baff13b..b144ba2a07 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -24,7 +24,7 @@ import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} fr import {ACTIVE_INDEX, ActiveIndexFlag, CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS} from '../interfaces/container'; import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition'; import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from '../interfaces/injector'; -import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TConstants, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from '../interfaces/node'; +import {AttributeMarker, DirectiveDefsValues, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TConstants, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from '../interfaces/node'; import {RComment, RElement, RNode, RText, Renderer3, RendererFactory3, isProceduralRenderer} from '../interfaces/renderer'; import {SanitizerFn} from '../interfaces/sanitization'; import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRootView} from '../interfaces/type_checks'; @@ -37,7 +37,6 @@ 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'; @@ -98,7 +97,7 @@ export function setHostBindingsByExecutingExpandoInstructions(tView: TView, lVie } else { // If it's not a number, it's a host binding function that needs to be executed. if (instruction !== null) { - setBindingRootForHostBindings(bindingRootIndex); + setBindingRootForHostBindings(bindingRootIndex, currentDirectiveIndex); const hostCtx = lView[currentDirectiveIndex]; instruction(RenderFlags.Update, hostCtx); } @@ -824,11 +823,12 @@ export function createTNode( tParent, // parent: TElementNode|TContainerNode|null null, // projection: number|(ITNode|RNode[])[]|null null, // styles: string|null - undefined, // stylesMap: string|null + undefined, // residualStyles: string|null null, // classes: string|null - undefined, // classesMap: string|null + undefined, // residualClasses: string|null 0 as any, // classBindings: TStylingRange; 0 as any, // styleBindings: TStylingRange; + null, // directives: TDirectiveDefs|null; ) : { type: type, @@ -853,11 +853,12 @@ export function createTNode( parent: tParent, projection: null, styles: null, - stylesMap: undefined, + residualStyles: undefined, classes: null, - classesMap: undefined, + residualClasses: undefined, classBindings: 0 as any, styleBindings: 0 as any, + directives: null }; } @@ -1111,6 +1112,7 @@ export function resolveDirectives( const exportsMap: ({[key: string]: number} | null) = localRefs === null ? null : {'': -1}; if (directiveDefs !== null) { + tNode.directives = [DirectiveDefsValues.INITIAL_STYLING_CURSOR_VALUE]; let totalDirectiveHostVars = 0; hasDirectives = true; initTNodeFlags(tNode, tView.data.length, directiveDefs.length); @@ -1129,6 +1131,7 @@ export function resolveDirectives( let preOrderCheckHooksFound = false; for (let i = 0; i < directiveDefs.length; i++) { const def = directiveDefs[i]; + tNode.directives.push(def); // Merge the attrs in the order of matches. This assumes that the first directive is the // component itself, so that the component has the least priority. tNode.mergedAttrs = mergeHostAttrs(tNode.mergedAttrs, def.hostAttrs); diff --git a/packages/core/src/render3/instructions/styling.ts b/packages/core/src/render3/instructions/styling.ts index 15baf9d315..4d497bb3a4 100644 --- a/packages/core/src/render3/instructions/styling.ts +++ b/packages/core/src/render3/instructions/styling.ts @@ -10,18 +10,19 @@ import {SafeValue, unwrapSafeValue} from '../../sanitization/bypass'; import {stylePropNeedsSanitization, ɵɵsanitizeStyle} from '../../sanitization/sanitization'; import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {ArrayMap, arrayMapGet, arrayMapSet} from '../../util/array_utils'; -import {assertDefined, assertEqual, assertLessThan, throwError} from '../../util/assert'; +import {assertDefined, assertEqual, assertGreaterThanOrEqual, assertLessThan, assertNotEqual, assertNotSame, 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 {AttributeMarker, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; +import {DirectiveDef} from '../interfaces/definition'; +import {AttributeMarker, DirectiveDefs, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; import {RElement, Renderer3} from '../interfaces/renderer'; import {SanitizerFn} from '../interfaces/sanitization'; 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 {getCurrentDirectiveIndex, 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'; @@ -239,6 +240,14 @@ export function checkStylingMap( // if so as not to read unnecessarily. const tNode = tView.data[getSelectedIndex() + HEADER_OFFSET] as TNode; if (hasStylingInputShadow(tNode, isClassBased) && !isInHostBindings(tView, bindingIndex)) { + if (ngDevMode) { + // verify that if we are shadowing then `TData` is appropriately marked so that we skip + // processing this binding in styling resolution. + const tStylingKey = tView.data[bindingIndex]; + assertEqual( + Array.isArray(tStylingKey) ? tStylingKey[1] : tStylingKey, false, + 'Styling linked list shadow input should be marked as \'false\''); + } // 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 @@ -305,10 +314,216 @@ function stylingPropertyFirstUpdatePass( // We turn this into a noop by setting the key to `false` tStylingKey = false; } + tStylingKey = wrapInStaticStylingKey(tData, tNode, tStylingKey, isClassBased); insertTStylingBinding(tData, tNode, tStylingKey, bindingIndex, isHostBindings, isClassBased); } } +/** + * Adds static styling information to the binding if applicable. + * + * The linked list of styles not only stores the list and keys, but also stores static styling + * information on some of the keys. This function determines if the key should contain the styling + * information and computes it. + * + * See `TStylingStatic` for more details. + * + * @param tData `TData` where the linked list is stored. + * @param tNode `TNode` for which the styling is being computed. + * @param stylingKey `TStylingKeyPrimitive` which may need to be wrapped into `TStylingKey` + * @param isClassBased `true` if `class` (`false` if `style`) + */ +export function wrapInStaticStylingKey( + tData: TData, tNode: TNode, stylingKey: TStylingKey, isClassBased: boolean): TStylingKey { + const hostDirectiveDef = getHostDirectiveDef(tData); + let residual = isClassBased ? tNode.residualClasses : tNode.residualStyles; + if (hostDirectiveDef === null) { + // We are in template node. + // If template node already had styling instruction then it has already collected the static + // styling and there is no need to collect them again. We know that we are the first styling + // instruction because the `TNode.*Bindings` points to 0 (nothing has been inserted yet). + const isFirstStylingInstructionInTemplate = + (isClassBased ? tNode.classBindings : tNode.styleBindings) as any as number === 0; + if (isFirstStylingInstructionInTemplate) { + // It would be nice to be able to get the statics from `mergeAttrs`, however, at this point + // they are already merged and it would not be possible to figure which property belongs where + // in the priority. + stylingKey = collectStylingFromDirectives(null, tData, tNode, stylingKey, isClassBased); + stylingKey = collectStylingFromTAttrs(stylingKey, tNode.attrs, isClassBased); + // We know that if we have styling binding in template we can't have residual. + residual = null; + } + } else { + // We are in host binding node and there was no binding instruction in template node. + // This means that we need to compute the residual. + const directives = tNode.directives; + const isFirstStylingInstructionInHostBinding = directives !== null && + directives[directives[DirectiveDefs.STYLING_CURSOR]] !== hostDirectiveDef; + if (isFirstStylingInstructionInHostBinding) { + stylingKey = + collectStylingFromDirectives(hostDirectiveDef, tData, tNode, stylingKey, isClassBased); + if (residual === null) { + // - If `null` than either: + // - Template styling instruction already ran and it has consumed the static + // styling into its `TStylingKey` and so there is no need to update residual. Instead + // we need to update the `TStylingKey` associated with the first template node + // instruction. OR + // - Some other styling instruction ran and determined that there are no residuals + let templateStylingKey = getTemplateHeadTStylingKey(tData, tNode, isClassBased); + if (templateStylingKey !== undefined && Array.isArray(templateStylingKey)) { + // Only recompute if `templateStylingKey` had static values. (If no static value found + // then there is nothing to do since this operation can only produce less static keys, not + // more.) + templateStylingKey = collectStylingFromDirectives( + null, tData, tNode, templateStylingKey[1] /* unwrap previous statics */, + isClassBased); + templateStylingKey = + collectStylingFromTAttrs(templateStylingKey, tNode.attrs, isClassBased); + setTemplateHeadTStylingKey(tData, tNode, isClassBased, templateStylingKey); + } + } else { + // We only need to recompute residual if it is not `null`. + // - If existing residual (implies there was no template styling). This means that some of + // the statics may have moved from the residual to the `stylingKey` and so we have to + // recompute. + // - If `undefined` this is the first time we are running. + residual = collectResidual(tNode, isClassBased); + } + } + } + if (residual !== undefined) { + isClassBased ? (tNode.residualClasses = residual) : (tNode.residualStyles = residual); + } + return stylingKey; +} + +/** + * Retrieve the `TStylingKey` for the template styling instruction. + * + * This is needed since `hostBinding` styling instructions are inserted after the template + * instruction. While the template instruction needs to update the residual in `TNode` the + * `hostBinding` instructions need to update the `TStylingKey` of the template instruction because + * the template instruction is downstream from the `hostBindings` instructions. + * + * @param tData `TData` where the linked list is stored. + * @param tNode `TNode` for which the styling is being computed. + * @param isClassBased `true` if `class` (`false` if `style`) + * @return `TStylingKey` if found or `undefined` if not found. + */ +function getTemplateHeadTStylingKey(tData: TData, tNode: TNode, isClassBased: boolean): TStylingKey| + undefined { + const bindings = isClassBased ? tNode.classBindings : tNode.styleBindings; + if (getTStylingRangeNext(bindings) === 0) { + // There does not seem to be a styling instruction in the `template`. + return undefined; + } + return tData[getTStylingRangePrev(bindings)] as TStylingKey; +} + +function setTemplateHeadTStylingKey( + tData: TData, tNode: TNode, isClassBased: boolean, tStylingKey: TStylingKey): void { + const bindings = isClassBased ? tNode.classBindings : tNode.styleBindings; + ngDevMode && assertNotEqual( + getTStylingRangeNext(bindings), 0, + 'Expecting to have at least one template styling binding.'); + tData[getTStylingRangePrev(bindings)] = tStylingKey; +} + +function collectResidual(tNode: TNode, isClassBased: boolean): ArrayMap|null { + let residual: ArrayMap|null|undefined = undefined; + const directives = tNode.directives; + if (directives) { + for (let i = directives[DirectiveDefs.STYLING_CURSOR] + 1; i < directives.length; i++) { + const attrs = (directives[i] as DirectiveDef).hostAttrs; + residual = collectStylingFromTAttrs(residual, attrs, isClassBased) as ArrayMap| null; + } + } + return collectStylingFromTAttrs(residual, tNode.attrs, isClassBased) as ArrayMap| null; +} + +/** + * Collect the static styling information with lower priority than `hostDirectiveDef`. + * + * (This is opposite of residual styling.) + * + * @param hostDirectiveDef `DirectiveDef` for which we want to collect lower priority static + * styling. (Or `null` if template styling) + * @param tData `TData` where the linked list is stored. + * @param tNode `TNode` for which the styling is being computed. + * @param stylingKey Existing `TStylingKey` to update or wrap. + * @param isClassBased `true` if `class` (`false` if `style`) + */ +function collectStylingFromDirectives( + hostDirectiveDef: DirectiveDef| null, tData: TData, tNode: TNode, stylingKey: TStylingKey, + isClassBased: boolean): TStylingKey { + const directives = tNode.directives; + if (directives != null) { + ngDevMode && hostDirectiveDef && + assertGreaterThanOrEqual( + directives.indexOf(hostDirectiveDef, directives[DirectiveDefs.STYLING_CURSOR]), 0, + 'Expecting that the current directive is in the directive list'); + // We need to loop because there can be directives which have `hostAttrs` but don't have + // `hostBindings` so this loop catches up up to the current directive.. + let currentDirective: DirectiveDef|null = null; + let index = directives[DirectiveDefs.STYLING_CURSOR]; + while (index + 1 < directives.length) { + index++; + currentDirective = directives[index] as DirectiveDef; + ngDevMode && assertDefined(currentDirective, 'expected to be defined'); + stylingKey = collectStylingFromTAttrs(stylingKey, currentDirective.hostAttrs, isClassBased); + if (currentDirective === hostDirectiveDef) break; + } + if (hostDirectiveDef !== null) { + // we only advance the styling cursor if we are collecting data from host bindings. + // Template executes before host bindings and so if we would update the index, + // host bindings would not get their statics. + directives[DirectiveDefs.STYLING_CURSOR] = index; + } + } + return stylingKey; +} + +/** + * Convert `TAttrs` into `TStylingStatic`. + * + * @param stylingKey existing `TStylingKey` to update or wrap. + * @param attrs `TAttributes` to process. + * @param isClassBased `true` if `class` (`false` if `style`) + */ +function collectStylingFromTAttrs( + stylingKey: TStylingKey | undefined, attrs: TAttributes | null, + isClassBased: boolean): TStylingKey { + const desiredMarker = isClassBased ? AttributeMarker.Classes : AttributeMarker.Styles; + let currentMarker = AttributeMarker.ImplicitAttributes; + if (attrs !== null) { + for (let i = 0; i < attrs.length; i++) { + const item = attrs[i] as number | string; + if (typeof item === 'number') { + currentMarker = item; + } else { + if (currentMarker === desiredMarker) { + if (!Array.isArray(stylingKey)) { + stylingKey = stylingKey === undefined ? [] : ['', stylingKey] as any; + } + arrayMapSet(stylingKey as ArrayMap, item, isClassBased ? true : attrs[++i]); + } + } + } + } + return stylingKey === undefined ? null : stylingKey; +} + +/** + * Retrieve the current `DirectiveDef` which is active when `hostBindings` style instruction is + * being executed (or `null` if we are in `template`.) + * + * @param tData Current `TData` where the `DirectiveDef` will be looked up at. + */ +export function getHostDirectiveDef(tData: TData): DirectiveDef|null { + const currentDirectiveIndex = getCurrentDirectiveIndex(); + return currentDirectiveIndex === -1 ? null : tData[currentDirectiveIndex] as DirectiveDef; +} + /** * Convert user input to `ArrayMap`. * @@ -468,7 +683,7 @@ function updateStyling( const tData = tView.data; const tRange = tData[bindingIndex + 1] as TStylingRange; const higherPriorityValue = getTStylingRangeNextDuplicate(tRange) ? - findStylingValue(tData, null, lView, prop, getTStylingRangeNext(tRange), isClassBased) : + findStylingValue(tData, tNode, lView, prop, getTStylingRangeNext(tRange), isClassBased) : undefined; if (!isStylingValuePresent(higherPriorityValue)) { // We don't have a next duplicate, or we did not find a duplicate value. @@ -476,8 +691,7 @@ function updateStyling( // 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); + value = findStylingValue(tData, null, lView, prop, bindingIndex, isClassBased); } } const rNode = getNativeByIndex(getSelectedIndex(), lView) as RElement; @@ -505,9 +719,9 @@ function updateStyling( * * @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 + * - `TNode` search next and quit as soon as `isStylingValuePresent(value)` is true. + * If no value found consult `tNode.residualStyle`/`tNode.residualClass` for default value. + * - `null` search prev 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. @@ -519,29 +733,30 @@ function findStylingValue( 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; + const rawKey = tData[index] as TStylingKey; + const containsStatics = Array.isArray(rawKey); + // Unwrap the key if we contain static values. + const key = containsStatics ? (rawKey as string[])[1] : rawKey; + let currentValue = key === null ? arrayMapGet(lView[index + 1], prop) : + key === prop ? lView[index + 1] : undefined; + if (containsStatics && !isStylingValuePresent(currentValue)) { + currentValue = arrayMapGet(rawKey as ArrayMap, prop); + } if (isStylingValuePresent(currentValue)) { value = currentValue; - if (tNode !== null) { + if (tNode === null) { return value; } } const tRange = tData[index + 1] as TStylingRange; - index = tNode !== null ? getTStylingRangePrev(tRange) : getTStylingRangeNext(tRange); + 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); + // in case where we are going in next direction AND we did not find anything, we need to + // consult residual styling + let residual = isClassBased ? tNode.residualClasses : tNode.residualStyles; + if (residual != null /** OR residual !=== undefined */) { + value = arrayMapGet(residual !, prop); } } return value; @@ -571,8 +786,8 @@ function isStylingValuePresent(value: any): boolean { * @param tNode `TNode` to initialize. */ export function initializeStylingStaticArrayMap(tNode: TNode) { - ngDevMode && assertEqual(tNode.classesMap, undefined, 'Already initialized!'); - ngDevMode && assertEqual(tNode.stylesMap, undefined, 'Already initialized!'); + ngDevMode && assertEqual(tNode.residualClasses, undefined, 'Already initialized!'); + ngDevMode && assertEqual(tNode.residualStyles, undefined, 'Already initialized!'); let styleMap: ArrayMap|null = null; let classMap: ArrayMap|null = null; const mergeAttrs = tNode.mergedAttrs || EMPTY_ARRAY as TAttributes; @@ -589,8 +804,8 @@ export function initializeStylingStaticArrayMap(tNode: TNode) { arrayMapSet(styleMap !, item as string, mergeAttrs[++i] as string); } } - tNode.classesMap = classMap; - tNode.stylesMap = styleMap; + tNode.residualClasses = classMap; + tNode.residualStyles = styleMap; } /** diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 73d58b2bf7..c0b4110061 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -8,6 +8,7 @@ import {ArrayMap} from '../../util/array_utils'; import {TStylingRange} from '../interfaces/styling'; +import {DirectiveDef} from './definition'; import {CssSelector} from './projection'; import {RNode} from './renderer'; import {LView, TView} from './view'; @@ -345,6 +346,13 @@ export interface TNode { */ mergedAttrs: TAttributes|null; + // TODO(misko): pre discussion with Kara, it seems that we don't need `directives` since the same + // information is already present in the TData. Maybe worth refactoring. + /** + * Stores the directive defs matched on the current TNode (along with style cursor.) + */ + directives: TDirectiveDefs|null; + /** * A set of local names under which a given element is exported in a template and * visible to queries. An entry in this array can be created for different reasons: @@ -481,7 +489,7 @@ export interface TNode { projection: (TNode|RNode[])[]|number|null; /** - * A collection of all style bindings and/or static style values for an element. + * A collection of all style static values for an element. * * This field will be populated if and when: * @@ -490,21 +498,36 @@ export interface TNode { styles: string|null; /** - * An `ArrayMap` version of `styles. + * An `ArrayMap` version of residual `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. + * When there are styling instructions than each instruction stores the static styling + * which is of lower priority than itself. This means that there may be a higher priority styling + * than the instruction. + * + * Imagine: + * ``` + *
+ * + * @Directive({ + * host: { + * style: 'color: lowest; ', + * '[styles.color]': 'exp' // ɵɵstyleProp('color', ctx.exp); + * } + * }) + * ``` + * + * In the above case: + * - `color: lowest` is stored with `ɵɵstyleProp('color', ctx.exp);` instruction + * - `color: highest` is the residual and is stored here. * * - `undefined': not initialized. * - `null`: initialized but `styles` is `null` * - `ArrayMap`: parsed version of `styles`. */ - stylesMap: ArrayMap|undefined|null; + residualStyles: ArrayMap|undefined|null; /** - * A collection of all class bindings and/or static class values for an element. + * A collection of all class static values for an element. * * This field will be populated if and when: * @@ -513,18 +536,15 @@ export interface TNode { classes: string|null; /** - * An `ArrayMap` version of `classes`. + * An `ArrayMap` version of residual `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. + * Same as `TNode.residualStyles` but for classes. * * - `undefined': not initialized. * - `null`: initialized but `classes` is `null` * - `ArrayMap`: parsed version of `S`. */ - classesMap: ArrayMap|undefined|null; + residualClasses: ArrayMap|undefined|null; /** * Stores the head/tail index of the class bindings. @@ -793,4 +813,55 @@ export function hasClassInput(tNode: TNode) { */ export function hasStyleInput(tNode: TNode) { return (tNode.flags & TNodeFlags.hasStyleInput) !== 0; +} + +/** + * Constant enums for accessing data in the `TDirectiveDefs` + */ +export const enum DirectiveDefs { + /// Location where the STYLING_CURSOR is stored. + STYLING_CURSOR = 0, + /// Header offset from which iterating over `DirectiveDefs` should start. + HEADER_OFFSET = 1 +} + +/** + * Constant enums for initial values in the `TDirectiveDefs` + */ +export const enum DirectiveDefsValues { + // Initial value for the `STYLING_CURSOR` + INITIAL_STYLING_CURSOR_VALUE = 0, +} + +/** + * Stores `DirectiveDefs` associated with the current `TNode` as well as styling cursor. + */ +export interface TDirectiveDefs extends Array> { + /** + * As styling instructions (`ɵɵstyleProp`/`ɵɵclassProp`/`ɵɵstyleMap`/`ɵɵclassMap`) are executing + * they also need to get a hold of the `DirectiveDef.hostAttrs` and so that they know what + * static styling values to use. The styling instructions need this information so that they can + * lazily create `TStylingStatic`. + * + * When styling is executing it can get a hold of its `DirectiveDefs` but that alone is not + * sufficient for two reasons: + * 1. Styling instruction needs to coalesce other directives which came before it and which have + * static value but may not have a styling instruction to attach the static values to. + * 2. There may be more than one styling instruction per `hostBindings` and only the first + * styling instruction should create the `TStylingStatic`. + * + * The algorithm for doing this is: + * - look up the current `DirectiveDef` associated with the current instruction. + * - If `STYLING_CURSOR === 0 || tDirectiveDefs[stylingCursor] !== currentDirectiveDef` than + * create `TStylingStatic` and: + * - iterate over `TDirectiveDefs[++stylingCursor]` and insert them into the `TStylingStatic` + * until you reach `DirectiveDef` associated with the current instruction. + * - If new `TStylingStatic` was created, recompute the residual styling values. + * + * The above algorithm will ensure that the styling instructions consume static styling values + * associated until a given instruction. After consuming instructions, it is always important to + * clear the residual (See `TNode.residualClass`/`TNode.residualStyle`), since this may be the + * last styling instruction, and we need to lazily recreate the residual value on as needed basis. + */ + [DirectiveDefs.STYLING_CURSOR]: number; } \ No newline at end of file diff --git a/packages/core/src/render3/interfaces/styling.ts b/packages/core/src/render3/interfaces/styling.ts index aeb585bd86..1ff85c66c5 100644 --- a/packages/core/src/render3/interfaces/styling.ts +++ b/packages/core/src/render3/interfaces/styling.ts @@ -6,16 +6,90 @@ * found in the LICENSE file at https://angular.io/license */ +import {ArrayMap} from '../../util/array_utils'; +import {assertNumber, assertNumberInRange} from '../../util/assert'; + /** * Value stored in the `TData` which is needed to re-concatenate the styling. * + * See: `TStylingKeyPrimitive` and `TStylingStatic` + */ +export type TStylingKey = TStylingKeyPrimitive | TStylingStatic; + + +/** + * The primitive portion (`TStylingStatic` removed) of the value stored in the `TData` which is + * needed to re-concatenate the styling. + * * - `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 | null | false; +export type TStylingKeyPrimitive = string | null | false; + +/** + * Store the static values for the styling binding. + * + * The `TStylingStatic` is just `ArrayMap` where key `""` (stored at location 0) contains the + * `TStylingKey` (stored at location 1). In other words this wraps the `TStylingKey` such that the + * `""` contains the wrapped value. + * + * When instructions are resolving styling they may need to look forward or backwards in the linked + * list to resolve the value. For this reason we have to make sure that he linked list also contains + * the static values. However the list only has space for one item per styling instruction. For this + * reason we store the static values here as part of the `TStylingKey`. This means that the + * resolution function when looking for a value needs to first look at the binding value, and than + * at `TStylingKey` (if it exists). + * + * Imagine we have: + * + * ``` + *
+ * + * @Directive({ + * host: { + * class: 'DIR', + * '[class.dynamic]': 'exp' // ɵɵclassProp('dynamic', ctx.exp); + * } + * }) + * ``` + * + * In the above case the linked list will contain one item: + * + * ``` + * // assume binding location: 10 for `ɵɵclassProp('dynamic', ctx.exp);` + * tData[10] = [ + * '': 'dynamic', // This is the wrapped value of `TStylingKey` + * 'DIR': true, // This is the default static value of directive binding. + * ]; + * tData[10 + 1] = 0; // We don't have prev/next. + * + * lView[10] = undefined; // assume `ctx.exp` is `undefined` + * lView[10 + 1] = undefined; // Just normalized `lView[10]` + * ``` + * + * So when the function is resolving styling value, it first needs to look into the linked list + * (there is none) and than into the static `TStylingStatic` too see if there is a default value for + * `dynamic` (there is not). Therefore it is safe to remove it. + * + * If setting `true` case: + * ``` + * lView[10] = true; // assume `ctx.exp` is `true` + * lView[10 + 1] = true; // Just normalized `lView[10]` + * ``` + * So when the function is resolving styling value, it first needs to look into the linked list + * (there is none) and than into `TNode.residualClass` (TNode.residualStyle) which contains + * ``` + * tNode.residualClass = [ + * 'TEMPLATE': true, + * ]; + * ``` + * + * This means that it is safe to add class. + */ +export interface TStylingStatic extends ArrayMap {} /** * This is a branded number which contains previous and next index. @@ -52,14 +126,17 @@ export interface TStylingRange { __brand__: 'TStylingRange'; } */ export const enum StylingRange { /// Number of bits to shift for the previous pointer - PREV_SHIFT = 18, + PREV_SHIFT = 17, /// Previous pointer mask. - PREV_MASK = 0xFFFC0000, + PREV_MASK = 0xFFFE0000, /// Number of bits to shift for the next pointer NEXT_SHIFT = 2, /// Next pointer mask. - NEXT_MASK = 0x0003FFC, + NEXT_MASK = 0x001FFFC, + + // Mask to remove nagative bit. (interpret number as positive) + UNSIGNED_MASK = 0x7FFF, /** * This bit is set if the previous bindings contains a binding which could possibly cause a @@ -80,49 +157,62 @@ export const enum StylingRange { export function toTStylingRange(prev: number, next: number): TStylingRange { + ngDevMode && assertNumberInRange(prev, 0, StylingRange.UNSIGNED_MASK); + ngDevMode && assertNumberInRange(next, 0, StylingRange.UNSIGNED_MASK); return (prev << StylingRange.PREV_SHIFT | next << StylingRange.NEXT_SHIFT) as any; } export function getTStylingRangePrev(tStylingRange: TStylingRange): number { - return (tStylingRange as any as number) >> StylingRange.PREV_SHIFT; + ngDevMode && assertNumber(tStylingRange, 'expected number'); + return ((tStylingRange as any as number) >> StylingRange.PREV_SHIFT) & StylingRange.UNSIGNED_MASK; } export function getTStylingRangePrevDuplicate(tStylingRange: TStylingRange): boolean { + ngDevMode && assertNumber(tStylingRange, 'expected number'); return ((tStylingRange as any as number) & StylingRange.PREV_DUPLICATE) == StylingRange.PREV_DUPLICATE; } export function setTStylingRangePrev( tStylingRange: TStylingRange, previous: number): TStylingRange { + ngDevMode && assertNumber(tStylingRange, 'expected number'); + ngDevMode && assertNumberInRange(previous, 0, StylingRange.UNSIGNED_MASK); return ( ((tStylingRange as any as number) & ~StylingRange.PREV_MASK) | (previous << StylingRange.PREV_SHIFT)) as any; } export function setTStylingRangePrevDuplicate(tStylingRange: TStylingRange): TStylingRange { + ngDevMode && assertNumber(tStylingRange, 'expected number'); return ((tStylingRange as any as number) | StylingRange.PREV_DUPLICATE) as any; } export function getTStylingRangeNext(tStylingRange: TStylingRange): number { + ngDevMode && assertNumber(tStylingRange, 'expected number'); return ((tStylingRange as any as number) & StylingRange.NEXT_MASK) >> StylingRange.NEXT_SHIFT; } export function setTStylingRangeNext(tStylingRange: TStylingRange, next: number): TStylingRange { + ngDevMode && assertNumber(tStylingRange, 'expected number'); + ngDevMode && assertNumberInRange(next, 0, StylingRange.UNSIGNED_MASK); return ( ((tStylingRange as any as number) & ~StylingRange.NEXT_MASK) | // next << StylingRange.NEXT_SHIFT) as any; } export function getTStylingRangeNextDuplicate(tStylingRange: TStylingRange): boolean { + ngDevMode && assertNumber(tStylingRange, 'expected number'); return ((tStylingRange as any as number) & StylingRange.NEXT_DUPLICATE) === StylingRange.NEXT_DUPLICATE; } export function setTStylingRangeNextDuplicate(tStylingRange: TStylingRange): TStylingRange { + ngDevMode && assertNumber(tStylingRange, 'expected number'); return ((tStylingRange as any as number) | StylingRange.NEXT_DUPLICATE) as any; } export function getTStylingRangeTail(tStylingRange: TStylingRange): number { + ngDevMode && assertNumber(tStylingRange, 'expected number'); const next = getTStylingRangeNext(tStylingRange); return next === 0 ? getTStylingRangePrev(tStylingRange) : next; } \ No newline at end of file diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index 31faa40935..81a1512e35 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -7,7 +7,7 @@ */ import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; -import {assertDefined, assertEqual, assertGreaterThan} from '../util/assert'; +import {assertDefined} from '../util/assert'; import {assertLViewOrUndefined} from './assert'; import {TNode} from './interfaces/node'; import {CONTEXT, DECLARATION_VIEW, LView, OpaqueViewState, TVIEW} from './interfaces/view'; @@ -104,6 +104,13 @@ interface LFrame { * We iterate over the list of Queries and increment current query index at every step. */ currentQueryIndex: number; + + /** + * When host binding is executing this points to the directive index. + * `TView.data[currentDirectiveIndex]` is `DirectiveDef` + * `LView[currentDirectiveIndex]` is directive instance. + */ + currentDirectiveIndex: number; } /** @@ -332,11 +339,25 @@ export function incrementBindingIndex(count: number): number { * Bindings inside the host template are 0 index. But because we don't know ahead of time * how many host bindings we have we can't pre-compute them. For this reason they are all * 0 index and we just shift the root so that they match next available location in the LView. - * @param value + * + * @param bindingRootIndex Root index for `hostBindings` + * @param currentDirectiveIndex `TData[currentDirectiveIndex]` will point to the current directive + * whose `hostBindings` are being processed. */ -export function setBindingRootForHostBindings(value: number) { - const lframe = instructionState.lFrame; - lframe.bindingIndex = lframe.bindingRootIndex = value; +export function setBindingRootForHostBindings( + bindingRootIndex: number, currentDirectiveIndex: number) { + const lFrame = instructionState.lFrame; + lFrame.bindingIndex = lFrame.bindingRootIndex = bindingRootIndex; + lFrame.currentDirectiveIndex = currentDirectiveIndex; +} + +/** + * When host binding is executing this points to the directive index. + * `TView.data[getCurrentDirectiveIndex()]` is `DirectiveDef` + * `LView[getCurrentDirectiveIndex()]` is directive instance. + */ +export function getCurrentDirectiveIndex(): number { + return instructionState.lFrame.currentDirectiveIndex; } export function getCurrentQueryIndex(): number { @@ -403,6 +424,7 @@ export function enterView(newView: LView, tNode: TNode | null): void { newLFrame.selectedIndex = 0; newLFrame.contextLView = newView !; newLFrame.elementDepthCount = 0; + newLFrame.currentDirectiveIndex = -1; newLFrame.currentNamespace = null; newLFrame.currentSanitizer = null; newLFrame.bindingRootIndex = -1; @@ -430,6 +452,7 @@ function createLFrame(parent: LFrame | null): LFrame { elementDepthCount: 0, // currentNamespace: null, // currentSanitizer: null, // + currentDirectiveIndex: -1, // bindingRootIndex: -1, // bindingIndex: -1, // currentQueryIndex: 0, // diff --git a/packages/core/src/render3/styling/style_binding_list.ts b/packages/core/src/render3/styling/style_binding_list.ts index 9c180208f8..9f386cc0f9 100644 --- a/packages/core/src/render3/styling/style_binding_list.ts +++ b/packages/core/src/render3/styling/style_binding_list.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {assertEqual} from '../../util/assert'; +import {ArrayMap, arrayMapIndexOf} from '../../util/array_utils'; +import {assertDataInRange, assertEqual, assertNotEqual} from '../../util/assert'; import {assertFirstUpdatePass} from '../assert'; import {TNode} from '../interfaces/node'; -import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangePrev, setTStylingRangeNext, setTStylingRangeNextDuplicate, setTStylingRangePrev, setTStylingRangePrevDuplicate, toTStylingRange} from '../interfaces/styling'; +import {TStylingKey, TStylingKeyPrimitive, TStylingRange, getTStylingRangeNext, getTStylingRangePrev, setTStylingRangeNext, setTStylingRangeNextDuplicate, setTStylingRangePrev, setTStylingRangePrevDuplicate, toTStylingRange} from '../interfaces/styling'; import {TData, TVIEW} from '../interfaces/view'; import {getLView} from '../state'; -import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseStyleNext} from './styling_parser'; + /** @@ -191,14 +192,28 @@ let __unused_const_as_closure_does_not_like_standalone_comment_blocks__: undefin * `tNode.classBindings` should be used (or `tNode.styleBindings` otherwise.) */ export function insertTStylingBinding( - tData: TData, tNode: TNode, tStylingKey: TStylingKey, index: number, isHostBinding: boolean, - isClassBinding: boolean): void { + tData: TData, tNode: TNode, tStylingKeyWithStatic: TStylingKey, index: number, + isHostBinding: boolean, isClassBinding: boolean): void { ngDevMode && assertFirstUpdatePass(getLView()[TVIEW]); let tBindings = isClassBinding ? tNode.classBindings : tNode.styleBindings; let tmplHead = getTStylingRangePrev(tBindings); let tmplTail = getTStylingRangeNext(tBindings); - tData[index] = tStylingKey; + tData[index] = tStylingKeyWithStatic; + let isKeyDuplicateOfStatic = false; + let tStylingKey: TStylingKeyPrimitive; + if (Array.isArray(tStylingKeyWithStatic)) { + // We are case when the `TStylingKey` contains static fields as well. + const staticArrayMap = tStylingKeyWithStatic as ArrayMap; + tStylingKey = staticArrayMap[1]; // unwrap. + // We need to check if our key is present in the static so that we can mark it as duplicate. + if (tStylingKey === null || arrayMapIndexOf(staticArrayMap, tStylingKey as string) > 0) { + // tStylingKey is present in the statics, need to mark it as duplicate. + isKeyDuplicateOfStatic = true; + } + } else { + tStylingKey = tStylingKeyWithStatic; + } if (isHostBinding) { // We are inserting host bindings @@ -248,10 +263,12 @@ export function insertTStylingBinding( // Now we need to update / compute the duplicates. // Starting with our location search towards head (least priority) - markDuplicates( - tData, tStylingKey, index, (isClassBinding ? tNode.classes : tNode.styles) || '', true, - isClassBinding); - markDuplicates(tData, tStylingKey, index, '', false, isClassBinding); + if (isKeyDuplicateOfStatic) { + tData[index + 1] = setTStylingRangePrevDuplicate(tData[index + 1] as TStylingRange); + } + markDuplicates(tData, tStylingKey, index, true, isClassBinding); + markDuplicates(tData, tStylingKey, index, false, isClassBinding); + markDuplicateOfResidualStyling(tNode, tStylingKey, tData, index, isClassBinding); tBindings = toTStylingRange(tmplHead, tmplTail); if (isClassBinding) { @@ -261,6 +278,27 @@ export function insertTStylingBinding( } } +/** + * Look into the residual styling to see if the current `tStylingKey` is duplicate of residual. + * + * @param tNode `TNode` where the residual is stored. + * @param tStylingKey `TStylingKey` to store. + * @param tData `TData` associated with the current `LView`. + * @param index location of where `tStyleValue` should be stored (and linked into list.) + * @param isClassBinding True if the associated `tStylingKey` as a `class` styling. + * `tNode.classBindings` should be used (or `tNode.styleBindings` otherwise.) + */ +function markDuplicateOfResidualStyling( + tNode: TNode, tStylingKey: TStylingKey, tData: TData, index: number, isClassBinding: boolean) { + const residual = isClassBinding ? tNode.residualClasses : tNode.residualStyles; + if (residual != null /* or undefined */ && typeof tStylingKey == 'string' && + arrayMapIndexOf(residual, tStylingKey) >= 0) { + // We have duplicate in the residual so mark ourselves as duplicate. + tData[index + 1] = setTStylingRangeNextDuplicate(tData[index + 1] as TStylingRange); + } +} + + /** * Marks `TStyleValue`s as duplicates if another style binding in the list has the same * `TStyleValue`. @@ -309,14 +347,16 @@ export function insertTStylingBinding( * NOTE: Once `[style]` (Map) is added into the system all things are mapped as duplicates. * NOTE: We use `style` as example, but same logic is applied to `class`es as well. * - * @param tData - * @param tStylingKey - * @param index - * @param staticValues - * @param isPrevDir + * @param tData `TData` where the linked list is stored. + * @param tStylingKey `TStylingKeyPrimitive` which contains the value to compare to other keys in + * the linked list. + * @param index Starting location in the linked list to search from + * @param isPrevDir Direction. + * - `true` for previous (lower priority); + * - `false` for next (higher priority). */ function markDuplicates( - tData: TData, tStylingKey: TStylingKey, index: number, staticValues: string, isPrevDir: boolean, + tData: TData, tStylingKey: TStylingKeyPrimitive, index: number, isPrevDir: boolean, isClassBinding: boolean) { const tStylingAtIndex = tData[index + 1] as TStylingRange; const isMap = tStylingKey === null; @@ -324,14 +364,15 @@ function markDuplicates( isPrevDir ? getTStylingRangePrev(tStylingAtIndex) : getTStylingRangeNext(tStylingAtIndex); let foundDuplicate = false; // We keep iterating as long as we have a cursor - // AND either: We found what we are looking for, or we are a map in which case we have to - // continue searching even after we find what we were looking for since we are a wild card - // and everything needs to be flipped to duplicate. + // AND either: + // - we found what we are looking for, OR + // - we are a map in which case we have to continue searching even after we find what we were + // looking for since we are a wild card and everything needs to be flipped to duplicate. while (cursor !== 0 && (foundDuplicate === false || isMap)) { + ngDevMode && assertDataInRange(tData, cursor); const tStylingValueAtCursor = tData[cursor] as TStylingKey; const tStyleRangeAtCursor = tData[cursor + 1] as TStylingRange; - if (tStylingValueAtCursor === null || tStylingKey == null || - tStylingValueAtCursor === tStylingKey) { + if (isStylingMatch(tStylingValueAtCursor, tStylingKey)) { foundDuplicate = true; tData[cursor + 1] = isPrevDir ? setTStylingRangeNextDuplicate(tStyleRangeAtCursor) : setTStylingRangePrevDuplicate(tStyleRangeAtCursor); @@ -339,31 +380,47 @@ function markDuplicates( cursor = isPrevDir ? getTStylingRangePrev(tStyleRangeAtCursor) : getTStylingRangeNext(tStyleRangeAtCursor); } - // We also need to process the static values. - if (staticValues !== '' && // If we have static values to search - !foundDuplicate // If we have duplicate don't bother since we are already marked as - // duplicate - ) { - if (isMap) { - // if we are a Map (and we have statics) we must assume duplicate - foundDuplicate = true; - } else if (staticValues != null) { - // If we found non-map then we iterate over its keys to determine if any of them match ours - // If we find a match than we mark it as duplicate. - for (let i = isClassBinding ? parseClassName(staticValues) : parseStyle(staticValues); // - i >= 0; // - i = isClassBinding ? parseClassNameNext(staticValues, i) : - parseStyleNext(staticValues, i)) { - if (getLastParsedKey(staticValues) === tStylingKey) { - foundDuplicate = true; - break; - } - } - } - } if (foundDuplicate) { // if we found a duplicate, than mark ourselves. tData[index + 1] = isPrevDir ? setTStylingRangePrevDuplicate(tStylingAtIndex) : setTStylingRangeNextDuplicate(tStylingAtIndex); } } + +/** + * Determines if two `TStylingKey`s are a match. + * + * When computing weather a binding contains a duplicate, we need to compare if the instruction + * `TStylingKey` has a match. + * + * Here are examples of `TStylingKey`s which match given `tStylingKeyCursor` is: + * - `color` + * - `color` // Match another color + * - `null` // That means that `tStylingKey` is a `classMap`/`styleMap` instruction + * - `['', 'color', 'other', true]` // wrapped `color` so match + * - `['', null, 'other', true]` // wrapped `null` so match + * - `['', 'width', 'color', 'value']` // wrapped static value contains a match on `'color'` + * - `null` // `tStylingKeyCursor` always match as it is `classMap`/`styleMap` instruction + * + * @param tStylingKeyCursor + * @param tStylingKey + */ +function isStylingMatch(tStylingKeyCursor: TStylingKey, tStylingKey: TStylingKeyPrimitive) { + ngDevMode && + assertNotEqual( + Array.isArray(tStylingKey), true, 'Expected that \'tStylingKey\' has been unwrapped'); + if (tStylingKeyCursor === null || // If the cursor is `null` it means that we have map at that + // location so we must assume that we have a match. + tStylingKey == null || // If `tStylingKey` is `null` then it is a map therefor assume that it + // contains a match. + (Array.isArray(tStylingKeyCursor) ? tStylingKeyCursor[1] : tStylingKeyCursor) === + tStylingKey // If the keys match explicitly than we are a match. + ) { + return true; + } else if (Array.isArray(tStylingKeyCursor) && typeof tStylingKey === 'string') { + // if we did not find a match, but `tStylingKeyCursor` is `ArrayMap` that means cursor has + // statics and we need to check those as well. + return arrayMapIndexOf(tStylingKeyCursor, tStylingKey) >= 0; // see if we are matching the key + } + return false; +} diff --git a/packages/core/src/util/assert.ts b/packages/core/src/util/assert.ts index 448940010d..6c2cf2eefe 100644 --- a/packages/core/src/util/assert.ts +++ b/packages/core/src/util/assert.ts @@ -18,6 +18,12 @@ export function assertNumber(actual: any, msg: string) { } } +export function assertNumberInRange(actual: any, minInclusive: number, maxInclusive: number) { + assertNumber(actual, 'Expected a number'); + assertLessThanOrEqual(actual, maxInclusive, 'Expected number to be less than or equal to'); + assertGreaterThanOrEqual(actual, minInclusive, 'Expected number to be greater than or equal to'); +} + export function assertString(actual: any, msg: string) { if (!(typeof actual === 'string')) { throwError(msg, actual === null ? 'null' : typeof actual, 'string', '==='); diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts index 3f26cd3597..d53825871c 100644 --- a/packages/core/test/acceptance/integration_spec.ts +++ b/packages/core/test/acceptance/integration_spec.ts @@ -1184,7 +1184,7 @@ describe('acceptance integration tests', () => { @Component({ template: ` -
+
` }) class App { @@ -1198,7 +1198,7 @@ describe('acceptance integration tests', () => { const dirInstance = fixture.componentInstance.dirInstance; const target: HTMLDivElement = fixture.nativeElement.querySelector('div'); expect(target.style.getPropertyValue('width')).toEqual('100px'); - expect(target.style.getPropertyValue('height')).toEqual('200px'); + expect(target.style.getPropertyValue('height')).toEqual(''); expect(target.classList.contains('abc')).toBeTruthy(); expect(target.classList.contains('def')).toBeTruthy(); expect(target.classList.contains('xyz')).toBeFalsy(); @@ -1208,7 +1208,7 @@ describe('acceptance integration tests', () => { dirInstance.activateXYZClass = true; fixture.detectChanges(); - expect(target.style.getPropertyValue('width')).toEqual('444px'); + expect(target.style.getPropertyValue('width')).toEqual('100px'); expect(target.style.getPropertyValue('height')).toEqual('999px'); expect(target.classList.contains('abc')).toBeTruthy(); expect(target.classList.contains('def')).toBeTruthy(); @@ -1219,7 +1219,7 @@ describe('acceptance integration tests', () => { fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('100px'); - expect(target.style.getPropertyValue('height')).toEqual('200px'); + expect(target.style.getPropertyValue('height')).toEqual(''); expect(target.classList.contains('abc')).toBeTruthy(); expect(target.classList.contains('def')).toBeTruthy(); expect(target.classList.contains('xyz')).toBeTruthy(); diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index 5deae41c4f..0c3bbdc5c8 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.ts @@ -1642,7 +1642,7 @@ describe('styling', () => { fixture.detectChanges(); expectStyle(element).toEqual({ - 'width': '777px', + 'width': '200px', 'color': 'red', 'font-size': '99px', }); @@ -1659,7 +1659,8 @@ describe('styling', () => { onlyInIvy('ivy resolves styling across directives, components and templates in unison') .it('should only apply each styling property once per CD across templates, components, directives', () => { - @Directive({selector: '[dir-that-sets-styling]'}) + @Directive( + {selector: '[dir-that-sets-styling]', host: {'style': 'width:0px; height:0px'}}) class DirThatSetsStyling { @HostBinding('style') public map: any = {width: '999px', height: '999px'}; } @@ -1667,7 +1668,6 @@ describe('styling', () => { @Component({ template: `
{ @Component({ template: ` @@ -3015,22 +3015,46 @@ describe('styling', () => { comp.myStyles = {}; comp.myHeight = undefined; fixture.detectChanges(); - expect(elm.style.width).toEqual('200px'); - expect(elm.style.height).toEqual('205px'); - - comp.dir.myStyles = {}; - comp.dir.myHeight = undefined; - fixture.detectChanges(); - expect(elm.style.width).toEqual('300px'); - expect(elm.style.height).toEqual('305px'); + expect(elm.style.width).toEqual('2px'); + expect(elm.style.height).toEqual('1px'); comp.comp.myStyles = {}; comp.comp.myHeight = undefined; fixture.detectChanges(); - expect(elm.style.width).toEqual('1px'); + expect(elm.style.width).toEqual('2px'); + expect(elm.style.height).toEqual('1px'); + + comp.dir.myStyles = {}; + comp.dir.myHeight = undefined; + fixture.detectChanges(); + expect(elm.style.width).toEqual('2px'); expect(elm.style.height).toEqual('1px'); }); + onlyInIvy('Prioritization works in Ivy only') + .it('should prioritize directive static bindings over components', () => { + @Component({selector: 'my-comp-with-styling', host: {style: 'color: blue'}, template: ''}) + class MyCompWithStyling { + } + + @Directive({selector: '[my-dir-with-styling]', host: {style: 'color: red'}}) + class MyDirWithStyling { + } + + @Component({template: ``}) + class MyComp { + } + + TestBed.configureTestingModule( + {declarations: [MyComp, MyCompWithStyling, MyDirWithStyling]}); + const fixture = TestBed.createComponent(MyComp); + const elm = fixture.nativeElement.querySelector('my-comp-with-styling') !; + + fixture.detectChanges(); + expect(elm.style.color).toEqual('red'); + }); + + it('should combine host class.foo bindings from multiple directives', () => { @Directive({ diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index db28e91f9c..ebcb3375f3 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -47,9 +47,6 @@ { "name": "EMPTY_ARRAY" }, - { - "name": "EMPTY_ARRAY" - }, { "name": "EMPTY_OBJ" }, @@ -404,30 +401,21 @@ { "name": "collectNativeNodes" }, + { + "name": "collectResidual" + }, + { + "name": "collectStylingFromDirectives" + }, + { + "name": "collectStylingFromTAttrs" + }, { "name": "computeStaticStyling" }, { "name": "concatStringsWithSpace" }, - { - "name": "consumeClassToken" - }, - { - "name": "consumeQuotedText" - }, - { - "name": "consumeSeparator" - }, - { - "name": "consumeStyleKey" - }, - { - "name": "consumeStyleValue" - }, - { - "name": "consumeWhitespace" - }, { "name": "createContainerRef" }, @@ -611,6 +599,9 @@ { "name": "getContextLView" }, + { + "name": "getCurrentDirectiveIndex" + }, { "name": "getCurrentStyleSanitizer" }, @@ -635,6 +626,9 @@ { "name": "getFirstNativeNode" }, + { + "name": "getHostDirectiveDef" + }, { "name": "getInjectableDef" }, @@ -653,9 +647,6 @@ { "name": "getLViewParent" }, - { - "name": "getLastParsedKey" - }, { "name": "getNameOnlyMarkerIndex" }, @@ -746,6 +737,9 @@ { "name": "getTViewCleanup" }, + { + "name": "getTemplateHeadTStylingKey" + }, { "name": "getTypeName" }, @@ -791,9 +785,6 @@ { "name": "initializeInputAndOutputAliases" }, - { - "name": "initializeStylingStaticArrayMap" - }, { "name": "injectElementRef" }, @@ -902,6 +893,9 @@ { "name": "isRootView" }, + { + "name": "isStylingMatch" + }, { "name": "isStylingValuePresent" }, @@ -947,6 +941,9 @@ { "name": "markDirtyIfOnPush" }, + { + "name": "markDuplicateOfResidualStyling" + }, { "name": "markDuplicates" }, @@ -998,21 +995,6 @@ { "name": "normalizeAndApplySuffixOrSanitizer" }, - { - "name": "parseClassName" - }, - { - "name": "parseClassNameNext" - }, - { - "name": "parseStyle" - }, - { - "name": "parseStyleNext" - }, - { - "name": "parserState" - }, { "name": "readPatchedData" }, @@ -1073,9 +1055,6 @@ { "name": "renderView" }, - { - "name": "resetParserState" - }, { "name": "resetPreOrderHookFlags" }, @@ -1157,6 +1136,9 @@ { "name": "setTStylingRangePrevDuplicate" }, + { + "name": "setTemplateHeadTStylingKey" + }, { "name": "setUpAttributes" }, @@ -1217,6 +1199,9 @@ { "name": "walkUpViews" }, + { + "name": "wrapInStaticStylingKey" + }, { "name": "wrapListener" }, diff --git a/packages/core/test/render3/instructions/lview_debug_spec.ts b/packages/core/test/render3/instructions/lview_debug_spec.ts index 67207f1854..0483b9e9cd 100644 --- a/packages/core/test/render3/instructions/lview_debug_spec.ts +++ b/packages/core/test/render3/instructions/lview_debug_spec.ts @@ -12,6 +12,7 @@ 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 {insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list'; +import {ArrayMap} from '@angular/core/src/util/array_utils'; describe('lView_debug', () => { @@ -35,19 +36,19 @@ describe('lView_debug', () => { }); it('should decode static styling', () => { - tNode.styles = 'color: blue'; - tNode.classes = 'STATIC'; - expect(tNode.styleBindings_).toEqual(['color: blue']); - expect(tNode.classBindings_).toEqual(['STATIC']); + tNode.residualStyles = ['color', 'blue'] as ArrayMap; + tNode.residualClasses = ['STATIC', true] as ArrayMap; + expect(tNode.styleBindings_).toEqual([['color', 'blue'] as ArrayMap]); + expect(tNode.classBindings_).toEqual([['STATIC', true] as ArrayMap]); }); it('should decode no-template property binding', () => { - tNode.classes = 'STATIC'; + tNode.residualClasses = ['STATIC', true] as ArrayMap; insertTStylingBinding(tView.data, tNode, 'CLASS', 2, true, true); insertTStylingBinding(tView.data, tNode, 'color', 4, true, false); expect(tNode.styleBindings_).toEqual([ - null, { + { index: 4, key: 'color', isTemplate: false, @@ -55,10 +56,11 @@ describe('lView_debug', () => { nextDuplicate: false, prevIndex: 0, nextIndex: 0, - } + }, + null ]); expect(tNode.classBindings_).toEqual([ - 'STATIC', { + { index: 2, key: 'CLASS', isTemplate: false, @@ -66,17 +68,18 @@ describe('lView_debug', () => { nextDuplicate: false, prevIndex: 0, nextIndex: 0, - } + }, + ['STATIC', true] as ArrayMap ]); }); it('should decode template and directive property binding', () => { - tNode.classes = 'STATIC'; + tNode.residualClasses = ['STATIC', true] as ArrayMap; insertTStylingBinding(tView.data, tNode, 'CLASS', 2, false, true); insertTStylingBinding(tView.data, tNode, 'color', 4, false, false); expect(tNode.styleBindings_).toEqual([ - null, { + { index: 4, key: 'color', isTemplate: true, @@ -84,10 +87,11 @@ describe('lView_debug', () => { nextDuplicate: false, prevIndex: 0, nextIndex: 0, - } + }, + null ]); expect(tNode.classBindings_).toEqual([ - 'STATIC', { + { index: 2, key: 'CLASS', isTemplate: true, @@ -95,14 +99,15 @@ describe('lView_debug', () => { nextDuplicate: false, prevIndex: 0, nextIndex: 0, - } + }, + ['STATIC', true] as ArrayMap ]); insertTStylingBinding(tView.data, tNode, null, 6, true, true); insertTStylingBinding(tView.data, tNode, null, 8, true, false); expect(tNode.styleBindings_).toEqual([ - null, { + { index: 8, key: null, isTemplate: false, @@ -119,14 +124,15 @@ describe('lView_debug', () => { nextDuplicate: false, prevIndex: 8, nextIndex: 0, - } + }, + null ]); expect(tNode.classBindings_).toEqual([ - 'STATIC', { + { index: 6, key: null, isTemplate: false, - prevDuplicate: true, + prevDuplicate: false, nextDuplicate: true, prevIndex: 0, nextIndex: 2, @@ -139,7 +145,8 @@ describe('lView_debug', () => { nextDuplicate: false, prevIndex: 6, nextIndex: 0, - } + }, + ['STATIC', true] as ArrayMap ]); }); }); diff --git a/packages/core/test/render3/instructions/shared_spec.ts b/packages/core/test/render3/instructions/shared_spec.ts index 54f114da16..1649d8dfe8 100644 --- a/packages/core/test/render3/instructions/shared_spec.ts +++ b/packages/core/test/render3/instructions/shared_spec.ts @@ -32,8 +32,13 @@ import {enterView, getBindingRoot, getLView, setBindingIndex} from '@angular/cor 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 consts = 1; + const vars = 60; // Space for directive expando, template, component + 3 directives if we assume + // that each consume 10 slots. + const tView = createTView( + TViewType.Component, -1, emptyTemplate, consts, vars, null, null, null, null, null); + // Just assume that the expando starts after 10 initial bindings. + tView.expandoStartIndex = HEADER_OFFSET + 10; 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, diff --git a/packages/core/test/render3/instructions/styling_spec.ts b/packages/core/test/render3/instructions/styling_spec.ts index e8c0e33b0d..70cbfdfad5 100644 --- a/packages/core/test/render3/instructions/styling_spec.ts +++ b/packages/core/test/render3/instructions/styling_spec.ts @@ -6,10 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ +import {DirectiveDef} from '@angular/core/src/render3'; +import {ɵɵdefineDirective} from '@angular/core/src/render3/definition'; 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 {AttributeMarker, TAttributes, TDirectiveDefs} from '@angular/core/src/render3/interfaces/node'; +import {StylingRange, TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate, setTStylingRangeNext, setTStylingRangePrev, toTStylingRange} from '@angular/core/src/render3/interfaces/styling'; +import {HEADER_OFFSET, TVIEW} from '@angular/core/src/render3/interfaces/view'; +import {getLView, leaveView, setBindingRootForHostBindings} 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'; @@ -269,25 +272,155 @@ describe('styling', () => { 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); + expect(tNode.residualStyles).toEqual(undefined); + expect(tNode.residualClasses).toEqual(undefined); initializeStylingStaticArrayMap(tNode); - expect(tNode.stylesMap).toEqual(null); - expect(tNode.classesMap).toEqual(null); + expect(tNode.residualStyles).toEqual(null); + expect(tNode.residualClasses).toEqual(null); }); it('should initialize from mergeAttrs', () => { const tNode = getLView()[TVIEW].firstChild !; - expect(tNode.stylesMap).toEqual(undefined); - expect(tNode.classesMap).toEqual(undefined); + expect(tNode.residualStyles).toEqual(undefined); + expect(tNode.residualClasses).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); + expect(tNode.residualClasses).toEqual(['bar', true, 'foo', true] as any); + expect(tNode.residualStyles).toEqual(['color', 'red', 'width', '0'] as any); + }); + }); + }); + + describe('static', () => { + describe('template only', () => { + it('should capture static values in TStylingKey', () => { + givenTemplateAttrs([AttributeMarker.Styles, 'content', '"TEMPLATE"']); + + ɵɵstyleProp('content', '"dynamic"'); + expectTStylingKeys('style').toEqual([ + ['', 'content', 'content', '"TEMPLATE"'], 'prev', // contains statics + null // residual + ]); + expectStyle(div).toEqual({content: '"dynamic"'}); + + ɵɵstyleProp('content', undefined); + expectTStylingKeys('style').toEqual([ + ['', 'content', 'content', '"TEMPLATE"'], 'both', // contains statics + 'content', 'prev', // Making sure that second instruction does not have statics again. + null // residual + ]); + expectStyle(div).toEqual({content: '"dynamic"'}); + }); + }); + + describe('directives only', () => { + it('should update residual on second directive', () => { + givenDirectiveAttrs([ + [AttributeMarker.Styles, 'content', '"lowest"'], // 0 + [AttributeMarker.Styles, 'content', '"middle"'], // 1 + ]); + expectStyle(div).toEqual({content: '"middle"'}); + + activateHostBindings(0); + ɵɵstyleProp('content', '"dynamic"'); + expectTStylingKeys('style').toEqual([ + ['', 'content', 'content', '"lowest"'], 'both', // 1: contains statics + ['content', '"middle"'], // residual + ]); + expectStyle(div).toEqual({content: '"middle"'}); + + // second binding should not get statics + ɵɵstyleProp('content', '"dynamic2"'); + expectTStylingKeys('style').toEqual([ + ['', 'content', 'content', '"lowest"'], 'both', // 1: contains statics + 'content', 'both', // 1: Should not get statics + ['content', '"middle"'] // residual + ]); + expectStyle(div).toEqual({content: '"middle"'}); + + activateHostBindings(1); + ɵɵstyleProp('content', '"dynamic3"'); + expectTStylingKeys('style').toEqual([ + ['', 'content', 'content', '"lowest"'], 'both', // 1: contains statics + 'content', 'both', // 1: Should not get statics + ['', 'content', 'content', '"middle"'], 'prev', // 0: contains statics + null // residual + ]); + expectStyle(div).toEqual({content: '"dynamic3"'}); + }); + }); + + describe('template and directives', () => { + it('should combine property and map', () => { + givenDirectiveAttrs([ + [AttributeMarker.Styles, 'content', '"lowest"', 'color', 'blue'], // 0 + [AttributeMarker.Styles, 'content', '"middle"', 'width', '100px'], // 1 + ]); + givenTemplateAttrs([AttributeMarker.Styles, 'content', '"TEMPLATE"', 'color', 'red']); + + // TEMPLATE + ɵɵstyleProp('content', undefined); + expectTStylingKeys('style').toEqual([ + // TEMPLATE + ['', 'content', 'color', 'red', 'content', '"TEMPLATE"', 'width', '100px'], 'prev', + // RESIDUAL + null + ]); + expectStyle(div).toEqual({content: '"TEMPLATE"', color: 'red', width: '100px'}); + + // Directive 0 + activateHostBindings(0); + ɵɵstyleMap('color: red; width: 0px; height: 50px'); + expectTStylingKeys('style').toEqual([ + // Host Binding 0 + ['', null, 'color', 'blue', 'content', '"lowest"'], 'both', + // TEMPLATE + ['', 'content', 'color', 'red', 'content', '"TEMPLATE"', 'width', '100px'], 'prev', + // RESIDUAL + null + ]); + expectStyle(div).toEqual( + {content: '"TEMPLATE"', color: 'red', width: '100px', height: '50px'}); + + // Directive 1 + activateHostBindings(1); + ɵɵstyleMap('color: red; width: 0px; height: 50px'); + expectTStylingKeys('style').toEqual([ + // Host Binding 0 + ['', null, 'color', 'blue', 'content', '"lowest"'], 'both', + // Host Binding 1 + ['', null, 'content', '"middle"', 'width', '100px'], 'both', + // TEMPLATE + ['', 'content', 'color', 'red', 'content', '"TEMPLATE"'], 'prev', + // RESIDUAL + null + ]); + expectStyle(div).toEqual( + {content: '"TEMPLATE"', color: 'red', width: '0px', height: '50px'}); + }); + + it('should read value from residual', () => { + givenDirectiveAttrs([ + [AttributeMarker.Styles, 'content', '"lowest"', 'color', 'blue'], // 0 + [AttributeMarker.Styles, 'content', '"middle"', 'width', '100px'], // 1 + ]); + givenTemplateAttrs([AttributeMarker.Styles, 'content', '"TEMPLATE"', 'color', 'red']); + + // Directive 1 + activateHostBindings(1); + ɵɵstyleProp('color', 'white'); + expectTStylingKeys('style').toEqual([ + // Host Binding 0 + 1 + ['', 'color', 'color', 'blue', 'content', '"middle"', 'width', '100px'], 'both', + // RESIDUAL + ['color', 'red', 'content', '"TEMPLATE"'] + ]); + expectStyle(div).toEqual({content: '"TEMPLATE"', color: 'red', width: '100px'}); + }); }); }); @@ -346,6 +479,41 @@ describe('styling', () => { }); }); }); + + + describe('TStylingRange', () => { + const MAX_VALUE = StylingRange.UNSIGNED_MASK; + + it('should throw on negative values', () => { + expect(() => toTStylingRange(0, -1)).toThrow(); + expect(() => toTStylingRange(-1, 0)).toThrow(); + }); + + it('should throw on overflow', () => { + expect(() => toTStylingRange(0, MAX_VALUE + 1)).toThrow(); + expect(() => toTStylingRange(MAX_VALUE + 1, 0)).toThrow(); + }); + + it('should retrieve the same value which went in just below overflow', () => { + const range = toTStylingRange(MAX_VALUE, MAX_VALUE); + expect(getTStylingRangePrev(range)).toEqual(MAX_VALUE); + expect(getTStylingRangeNext(range)).toEqual(MAX_VALUE); + }); + + it('should correctly increment', () => { + let range = toTStylingRange(0, 0); + for (let i = 0; i <= MAX_VALUE; i++) { + range = setTStylingRangeNext(range, i); + range = setTStylingRangePrev(range, i); + expect(getTStylingRangeNext(range)).toEqual(i); + expect(getTStylingRangePrev(range)).toEqual(i); + if (i == 10) { + // Skip the boring stuff in the middle. + i = MAX_VALUE - 10; + } + } + }); + }); }); @@ -355,4 +523,97 @@ function expectStyle(element: HTMLElement) { function expectClass(element: HTMLElement) { return expect(getElementClasses(element)); +} + +function givenTemplateAttrs(tAttrs: TAttributes) { + const tNode = getTNode(); + tNode.attrs = tAttrs; + applyTAttributes(tAttrs); +} + +function getTNode() { + return getLView()[TVIEW].firstChild !; +} + +function getTData() { + return getLView()[TVIEW].data; +} + +class MockDir {} + +function givenDirectiveAttrs(tAttrs: TAttributes[]) { + const tNode = getTNode(); + const tData = getTData(); + const directives: TDirectiveDefs = tNode.directives = [0]; + for (let i = 0; i < tAttrs.length; i++) { + const tAttr = tAttrs[i]; + const directiveDef = ɵɵdefineDirective({type: MockDir, hostAttrs: tAttr}) as DirectiveDef; + applyTAttributes(directiveDef.hostAttrs); + tData[getTDataIndexFromDirectiveIndex(i)] = directiveDef; + directives.push(directiveDef); + } +} + +function applyTAttributes(attrs: TAttributes | null) { + if (attrs === null) return; + const div: HTMLElement = getLView()[HEADER_OFFSET]; + let mode: AttributeMarker = AttributeMarker.ImplicitAttributes; + for (let i = 0; i < attrs.length; i++) { + const item = attrs[i]; + if (typeof item === 'number') { + mode = item; + } else if (typeof item === 'string') { + if (mode == AttributeMarker.ImplicitAttributes) { + div.setAttribute(item, attrs[++i] as string); + } else if (mode == AttributeMarker.Classes) { + div.classList.add(item); + } else if (mode == AttributeMarker.Styles) { + div.style.setProperty(item, attrs[++i] as string); + } + } + } +} + +function activateHostBindings(directiveIndex: number) { + const bindingRootIndex = getBindingRootIndexFromDirectiveIndex(directiveIndex); + const currentDirectiveIndex = getTDataIndexFromDirectiveIndex(directiveIndex); + setBindingRootForHostBindings(bindingRootIndex, currentDirectiveIndex); +} + +function getBindingRootIndexFromDirectiveIndex(index: number) { + // For simplicity assume that each directive has 10 vars. + // We need to offset 1 for template, and 1 for expando. + return HEADER_OFFSET + (index + 2) * 10; +} + +function getTDataIndexFromDirectiveIndex(index: number) { + return HEADER_OFFSET + index + 10; // offset to give template bindings space. +} + +function expectTStylingKeys(styling: 'style' | 'class') { + const tNode = getTNode(); + const tData = getTData(); + const isClassBased = styling === 'class'; + const headIndex = getTStylingRangePrev(isClassBased ? tNode.classBindings : tNode.styleBindings); + const tStylingKeys: (string | (null | string)[] | null)[] = []; + let index = headIndex; + let prevIndex = index; + // rewind to beginning of list. + while ((prevIndex = getTStylingRangePrev(tData[index + 1] as TStylingRange)) !== 0) { + index = prevIndex; + } + + // insert into array. + while (index !== 0) { + const tStylingKey = tData[index] as TStylingKey; + const prevDup = getTStylingRangePrevDuplicate(tData[index + 1] as TStylingRange); + const nextDup = getTStylingRangeNextDuplicate(tData[index + 1] as TStylingRange); + tStylingKeys.push(tStylingKey as string[] | string | null); + tStylingKeys.push(prevDup ? (nextDup ? 'both' : 'prev') : (nextDup ? 'next' : '')); + index = getTStylingRangeNext(tData[index + 1] as TStylingRange); + } + tStylingKeys.push( + (isClassBased ? tNode.residualClasses : tNode.residualStyles) as null | string[]); + + return expect(tStylingKeys); } \ No newline at end of file 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 b0b413c5cf..d5e66cc0a5 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 @@ -407,7 +407,7 @@ describe('TNode styling linked list', () => { it('should mark duplicate on static fields', () => { const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); - tNode.styles = 'color: blue;'; + tNode.residualStyles = ['color', 'blue'] as any; const tData: TData = [null, null]; insertTStylingBinding(tData, tNode, 'width', 2, false, false); expectPriorityOrder(tData, tNode, false).toEqual([ @@ -419,14 +419,14 @@ describe('TNode styling linked list', () => { expectPriorityOrder(tData, tNode, false).toEqual([ // PREV, NEXT [2, 'width', false, false], - [4, 'color', true, false], + [4, 'color', false, true], ]); insertTStylingBinding(tData, tNode, null, 6, false, false); expectPriorityOrder(tData, tNode, false).toEqual([ // PREV, NEXT [2, 'width', false, true], - [4, 'color', true, true], + [4, 'color', false, true], [6, null, true, false], ]); });