refactor(ivy): evaluate map-based styling bindings with a new algorithm (#30543)

This patch in the second runtime change which refactors how styling
bindings work in Angular. This patch refactors how map-based
`[style]` and `[class]` bindings work using a new algorithm which
is faster and less complex than the former one.

This patch is a follow-up to an earlier refactor which enabled
support for prop-based `[style.name]` and `[class.name]`
bindings (see f03475cac8).

PR Close #30543
This commit is contained in:
Matias Niemelä 2019-05-16 16:25:52 -07:00
parent deb77bd3df
commit dc6406e5e8
14 changed files with 1361 additions and 250 deletions

View File

@ -364,6 +364,11 @@ export class StylingBuilder {
private _buildMapBasedInstruction(
valueConverter: ValueConverter, isClassBased: boolean, stylingInput: BoundStylingEntry) {
let totalBindingSlotsRequired = 0;
if (compilerIsNewStylingInUse()) {
// the old implementation does not reserve slot values for
// binding entries. The new one does.
totalBindingSlotsRequired++;
}
// these values must be outside of the update block so that they can
// be evaluated (the AST visit call) during creation time so that any

View File

@ -17,7 +17,7 @@ import {BoundPlayerFactory} from '../styling/player_factory';
import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from '../styling/shared';
import {getCachedStylingContext, setCachedStylingContext} from '../styling/state';
import {allocateOrUpdateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util';
import {classProp as newClassProp, styleProp as newStyleProp, stylingApply as newStylingApply, stylingInit as newStylingInit} from '../styling_next/instructions';
import {classMap as newClassMap, classProp as newClassProp, styleMap as newStyleMap, styleProp as newStyleProp, stylingApply as newStylingApply, stylingInit as newStylingInit} from '../styling_next/instructions';
import {runtimeAllowOldStyling, runtimeIsNewStylingInUse} from '../styling_next/state';
import {getBindingNameFromIndex} from '../styling_next/util';
import {NO_CHANGE} from '../tokens';
@ -285,6 +285,10 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu
}
updateStyleMap(stylingContext, styles);
}
if (runtimeIsNewStylingInUse()) {
newStyleMap(styles);
}
}
@ -328,6 +332,10 @@ export function ɵɵclassMap(classes: {[styleName: string]: any} | NO_CHANGE | s
}
updateClassMap(stylingContext, classes);
}
if (runtimeIsNewStylingInUse()) {
newClassMap(classes);
}
}
/**

View File

@ -6,11 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {ApplyStylingFn, StylingBindingData, TStylingContext, TStylingContextIndex} from './interfaces';
import {allowStylingFlush, getGuardMask, getProp, getValue, getValuesCount, isContextLocked, lockContext} from './util';
import {ApplyStylingFn, LStylingData, LStylingMap, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from './interfaces';
import {allowStylingFlush, getBindingValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasValueChanged, isContextLocked, isStylingValueDefined, lockContext} from './util';
/**
* --------
*
* This file contains the core logic for styling in Angular.
*
* All styling bindings (i.e. `[style]`, `[style.prop]`, `[class]` and `[class.name]`)
@ -23,24 +26,31 @@ import {allowStylingFlush, getGuardMask, getProp, getValue, getValuesCount, isCo
* context.
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
const DEFAULT_BINDING_VALUE = null;
const DEFAULT_SIZE_VALUE = 1;
// The first bit value reflects a map-based binding value's bit.
// The reason why it's always activated for every entry in the map
// is so that if any map-binding values update then all other prop
// based bindings will pass the guard check automatically without
// any extra code or flags.
export const DEFAULT_GUARD_MASK_VALUE = 0b1;
const STYLING_INDEX_FOR_MAP_BINDING = 0;
const STYLING_INDEX_START_VALUE = 1;
// the values below are global to all styling code below. Each value
// will either increment or mutate each time a styling instruction is
// executed. Do not modify the values below.
let currentStyleIndex = 0;
let currentClassIndex = 0;
let currentStyleIndex = STYLING_INDEX_START_VALUE;
let currentClassIndex = STYLING_INDEX_START_VALUE;
let stylesBitMask = 0;
let classesBitMask = 0;
let deferredBindingQueue: (TStylingContext | number | string | null)[] = [];
const DEFAULT_BINDING_VALUE = null;
const DEFAULT_SIZE_VALUE = 1;
const DEFAULT_MASK_VALUE = 0;
export const DEFAULT_BINDING_INDEX_VALUE = -1;
export const BIT_MASK_APPLY_ALL = -1;
/**
* Visits a class-based binding and updates the new value (if changed).
*
@ -48,14 +58,18 @@ export const BIT_MASK_APPLY_ALL = -1;
* is executed. It's important that it's always called (even if the value
* has not changed) so that the inner counter index value is incremented.
* This way, each instruction is always guaranteed to get the same counter
* state each time its called (which then allows the `TStylingContext`
* state each time it's called (which then allows the `TStylingContext`
* and the bit mask values to be in sync).
*/
export function updateClassBinding(
context: TStylingContext, data: StylingBindingData, prop: string, bindingIndex: number,
value: boolean | null | undefined, deferRegistration: boolean): void {
const index = currentClassIndex++;
if (updateBindingData(context, data, index, prop, bindingIndex, value, deferRegistration)) {
context: TStylingContext, data: LStylingData, prop: string | null, bindingIndex: number,
value: boolean | string | null | undefined | LStylingMap, deferRegistration: boolean,
forceUpdate?: boolean): void {
const isMapBased = !prop;
const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : currentClassIndex++;
const updated = updateBindingData(
context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate);
if (updated || forceUpdate) {
classesBitMask |= 1 << index;
}
}
@ -67,22 +81,40 @@ export function updateClassBinding(
* is executed. It's important that it's always called (even if the value
* has not changed) so that the inner counter index value is incremented.
* This way, each instruction is always guaranteed to get the same counter
* state each time its called (which then allows the `TStylingContext`
* state each time it's called (which then allows the `TStylingContext`
* and the bit mask values to be in sync).
*/
export function updateStyleBinding(
context: TStylingContext, data: StylingBindingData, prop: string, bindingIndex: number,
value: String | string | number | null | undefined, deferRegistration: boolean): void {
const index = currentStyleIndex++;
if (updateBindingData(context, data, index, prop, bindingIndex, value, deferRegistration)) {
context: TStylingContext, data: LStylingData, prop: string | null, bindingIndex: number,
value: String | string | number | null | undefined | LStylingMap, deferRegistration: boolean,
forceUpdate?: boolean): void {
const isMapBased = !prop;
const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : currentStyleIndex++;
const updated = updateBindingData(
context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate);
if (updated || forceUpdate) {
stylesBitMask |= 1 << index;
}
}
/**
* Called each time a binding value has changed within the provided `TStylingContext`.
*
* This function is designed to be called from `updateStyleBinding` and `updateClassBinding`.
* If called during the first update pass, the binding will be registered in the context.
* If the binding does get registered and the `deferRegistration` flag is true then the
* binding data will be queued up until the context is later flushed in `applyStyling`.
*
* This function will also update binding slot in the provided `LStylingData` with the
* new binding entry (if it has changed).
*
* @returns whether or not the binding value was updated in the `LStylingData`.
*/
function updateBindingData(
context: TStylingContext, data: StylingBindingData, counterIndex: number, prop: string,
bindingIndex: number, value: string | String | number | boolean | null | undefined,
deferRegistration?: boolean): boolean {
context: TStylingContext, data: LStylingData, counterIndex: number, prop: string | null,
bindingIndex: number,
value: string | String | number | boolean | null | undefined | LStylingMap,
deferRegistration?: boolean, forceUpdate?: boolean): boolean {
if (!isContextLocked(context)) {
if (deferRegistration) {
deferBindingRegistration(context, counterIndex, prop, bindingIndex);
@ -99,11 +131,11 @@ function updateBindingData(
}
}
if (data[bindingIndex] !== value) {
const changed = forceUpdate || hasValueChanged(data[bindingIndex], value);
if (changed) {
data[bindingIndex] = value;
return true;
}
return false;
return changed;
}
/**
@ -118,7 +150,7 @@ function updateBindingData(
* after the inheritance chain exits.
*/
function deferBindingRegistration(
context: TStylingContext, counterIndex: number, prop: string, bindingIndex: number) {
context: TStylingContext, counterIndex: number, prop: string | null, bindingIndex: number) {
deferredBindingQueue.splice(0, 0, context, counterIndex, prop, bindingIndex);
}
@ -168,12 +200,19 @@ function flushDeferredBindings() {
* as the default value for the binding. If the bindingValue property is inserted
* and it is either a string, number or null value then that will replace the default
* value.
*
* Note that this function is also used for map-based styling bindings. They are treated
* much the same as prop-based bindings, but, because they do not have a property value
* (since it's a map), all map-based entries are stored in an already populated area of
* the context at the top (which is reserved for map-based entries).
*/
export function registerBinding(
context: TStylingContext, countId: number, prop: string,
context: TStylingContext, countId: number, prop: string | null,
bindingValue: number | null | string | boolean) {
let i = TStylingContextIndex.ValuesStartPosition;
// prop-based bindings (e.g `<div [style.width]="w" [class.foo]="f">`)
if (prop) {
let found = false;
let i = getPropValuesStartPosition(context);
while (i < context.length) {
const valuesCount = getValuesCount(context, i);
const p = getProp(context, i);
@ -183,7 +222,7 @@ export function registerBinding(
if (prop < p) {
allocateNewContextEntry(context, i, prop);
}
addBindingIntoContext(context, i, bindingValue, countId);
addBindingIntoContext(context, false, i, bindingValue, countId);
break;
}
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
@ -191,12 +230,26 @@ export function registerBinding(
if (!found) {
allocateNewContextEntry(context, context.length, prop);
addBindingIntoContext(context, i, bindingValue, countId);
addBindingIntoContext(context, false, i, bindingValue, countId);
}
} else {
// map-based bindings (e.g `<div [style]="s" [class]="{className:true}">`)
// there is no need to allocate the map-based binding region into the context
// since it is already there when the context is first created.
addBindingIntoContext(
context, true, TStylingContextIndex.MapBindingsPosition, bindingValue, countId);
}
}
function allocateNewContextEntry(context: TStylingContext, index: number, prop: string) {
context.splice(index, 0, DEFAULT_MASK_VALUE, DEFAULT_SIZE_VALUE, prop, DEFAULT_BINDING_VALUE);
// 1,2: splice index locations
// 3: each entry gets a guard mask value that is used to check against updates
// 4. each entry gets a size value (which is always one because there is always a default binding
// value)
// 5. the property that is getting allocated into the context
// 6. the default binding value (usually `null`)
context.splice(
index, 0, DEFAULT_GUARD_MASK_VALUE, DEFAULT_SIZE_VALUE, prop, DEFAULT_BINDING_VALUE);
}
/**
@ -212,107 +265,180 @@ function allocateNewContextEntry(context: TStylingContext, index: number, prop:
*
* - Otherwise the binding value will update the default value for the property
* and this will only happen if the default value is `null`.
*
* Note that this function also handles map-based bindings and will insert them
* at the top of the context.
*/
function addBindingIntoContext(
context: TStylingContext, index: number, bindingValue: number | string | boolean | null,
countId: number) {
context: TStylingContext, isMapBased: boolean, index: number,
bindingValue: number | string | boolean | null, countId: number) {
const valuesCount = getValuesCount(context, index);
// -1 is used because we want the last value that's in the list (not the next slot)
const lastValueIndex = index + TStylingContextIndex.BindingsStartOffset + valuesCount - 1;
let lastValueIndex = index + TStylingContextIndex.BindingsStartOffset + valuesCount;
if (!isMapBased) {
// prop-based values all have default values, but map-based entries do not.
// we want to access the index for the default value in this case and not just
// the bindings...
lastValueIndex--;
}
if (typeof bindingValue === 'number') {
context.splice(lastValueIndex, 0, bindingValue);
(context[index + TStylingContextIndex.ValuesCountOffset] as number)++;
(context[index + TStylingContextIndex.MaskOffset] as number) |= 1 << countId;
(context[index + TStylingContextIndex.GuardOffset] as number) |= 1 << countId;
} else if (typeof bindingValue === 'string' && context[lastValueIndex] == null) {
context[lastValueIndex] = bindingValue;
}
}
/**
* Applies all class entries in the provided context to the provided element.
* Applies all class entries in the provided context to the provided element and resets
* any counter and/or bitMask values associated with class bindings.
*/
export function applyClasses(
renderer: Renderer3 | ProceduralRenderer3 | null, data: StylingBindingData,
context: TStylingContext, element: RElement, directiveIndex: number) {
renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, context: TStylingContext,
element: RElement, directiveIndex: number) {
if (allowStylingFlush(context, directiveIndex)) {
const isFirstPass = isContextLocked(context);
const isFirstPass = !isContextLocked(context);
isFirstPass && lockContext(context);
applyStyling(context, renderer, element, data, classesBitMask, setClass, isFirstPass);
currentClassIndex = 0;
if (classesBitMask) {
applyStyling(context, renderer, element, data, classesBitMask, setClass);
classesBitMask = 0;
}
currentClassIndex = STYLING_INDEX_START_VALUE;
}
}
/**
* Applies all style entries in the provided context to the provided element.
* Applies all style entries in the provided context to the provided element and resets
* any counter and/or bitMask values associated with style bindings.
*/
export function applyStyles(
renderer: Renderer3 | ProceduralRenderer3 | null, data: StylingBindingData,
context: TStylingContext, element: RElement, directiveIndex: number) {
renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, context: TStylingContext,
element: RElement, directiveIndex: number) {
if (allowStylingFlush(context, directiveIndex)) {
const isFirstPass = isContextLocked(context);
const isFirstPass = !isContextLocked(context);
isFirstPass && lockContext(context);
applyStyling(context, renderer, element, data, stylesBitMask, setStyle, isFirstPass);
currentStyleIndex = 0;
if (stylesBitMask) {
applyStyling(context, renderer, element, data, stylesBitMask, setStyle);
stylesBitMask = 0;
}
currentStyleIndex = STYLING_INDEX_START_VALUE;
}
}
/**
* Runs through the provided styling context and applies each value to
* the provided element (via the renderer) if one or more values are present.
*
* This function will iterate over all entries present in the provided
* `TStylingContext` array (both prop-based and map-based bindings).-
*
* Each entry, within the `TStylingContext` array, is stored alphabetically
* and this means that each prop/value entry will be applied in order
* (so long as it is marked dirty in the provided `bitMask` value).
*
* If there are any map-based entries present (which are applied to the
* element via the `[style]` and `[class]` bindings) then those entries
* will be applied as well. However, the code for that is not apart of
* this function. Instead, each time a property is visited, then the
* code below will call an external function called `stylingMapsSyncFn`
* and, if present, it will keep the application of styling values in
* map-based bindings up to sync with the application of prop-based
* bindings.
*
* Visit `styling_next/map_based_bindings.ts` to learn more about how the
* algorithm works for map-based styling bindings.
*
* Note that this function is not designed to be called in isolation (use
* `applyClasses` and `applyStyles` to actually apply styling values).
*/
export function applyStyling(
context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
bindingData: StylingBindingData, bitMask: number, applyStylingFn: ApplyStylingFn,
forceApplyDefaultValues?: boolean) {
bindingData: LStylingData, bitMaskValue: number | boolean, applyStylingFn: ApplyStylingFn) {
deferredBindingQueue.length && flushDeferredBindings();
if (bitMask) {
let processAllEntries = bitMask === BIT_MASK_APPLY_ALL;
let i = TStylingContextIndex.ValuesStartPosition;
const bitMask = normalizeBitMaskValue(bitMaskValue);
const stylingMapsSyncFn = getStylingMapsSyncFn();
const mapsGuardMask = getGuardMask(context, TStylingContextIndex.MapBindingsPosition);
const applyAllValues = (bitMask & mapsGuardMask) > 0;
const mapsMode =
applyAllValues ? StylingMapsSyncMode.ApplyAllValues : StylingMapsSyncMode.TraverseValues;
let i = getPropValuesStartPosition(context);
while (i < context.length) {
const valuesCount = getValuesCount(context, i);
const guardMask = getGuardMask(context, i);
// the guard mask value is non-zero if and when
// there are binding values present for the property.
// If there are ONLY static values (i.e. `style="prop:val")
// then the guard value will stay as zero.
const processEntry =
processAllEntries || (guardMask ? (bitMask & guardMask) : forceApplyDefaultValues);
if (processEntry) {
if (bitMask & guardMask) {
let valueApplied = false;
const prop = getProp(context, i);
const limit = valuesCount - 1;
for (let j = 0; j <= limit; j++) {
const isFinalValue = j === limit;
const bindingValue = getValue(context, i, j);
const bindingIndex =
isFinalValue ? DEFAULT_BINDING_INDEX_VALUE : (bindingValue as number);
const valueToApply: string|null = isFinalValue ? bindingValue : bindingData[bindingIndex];
if (isValueDefined(valueToApply) || isFinalValue) {
const valuesCountUpToDefault = valuesCount - 1;
const defaultValue = getBindingValue(context, i, valuesCountUpToDefault) as string | null;
// case 1: apply prop-based values
// try to apply the binding values and see if a non-null
// value gets set for the styling binding
for (let j = 0; j < valuesCountUpToDefault; j++) {
const bindingIndex = getBindingValue(context, i, j) as number;
const valueToApply = bindingData[bindingIndex];
if (isStylingValueDefined(valueToApply)) {
applyStylingFn(renderer, element, prop, valueToApply, bindingIndex);
valueApplied = true;
break;
}
}
// case 2: apply map-based values
// traverse through each map-based styling binding and update all values up to
// the provided `prop` value. If the property was not applied in the loop above
// then it will be attempted to be applied in the maps sync code below.
if (stylingMapsSyncFn) {
// determine whether or not to apply the target property or to skip it
const mode = mapsMode | (valueApplied ? StylingMapsSyncMode.SkipTargetProp :
StylingMapsSyncMode.ApplyTargetProp);
const valueAppliedWithinMap = stylingMapsSyncFn(
context, renderer, element, bindingData, applyStylingFn, mode, prop, defaultValue);
valueApplied = valueApplied || valueAppliedWithinMap;
}
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
}
// case 3: apply the default value
// if the value has not yet been applied then a truthy value does not exist in the
// prop-based or map-based bindings code. If and when this happens, just apply the
// default value (even if the default value is `null`).
if (!valueApplied) {
applyStylingFn(renderer, element, prop, defaultValue);
}
}
function isValueDefined(value: any) {
// the reason why null is compared against is because
// a CSS class value that is set to `false` must be
// respected (otherwise it would be treated as falsy).
// Empty string values are because developers usually
// set a value to an empty string to remove it.
return value != null && value !== '';
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
}
// the map-based styling entries may have not applied all their
// values. For this reason, one more call to the sync function
// needs to be issued at the end.
if (stylingMapsSyncFn) {
stylingMapsSyncFn(context, renderer, element, bindingData, applyStylingFn, mapsMode);
}
}
function normalizeBitMaskValue(value: number | boolean): number {
// if pass => apply all values (-1 implies that all bits are flipped to true)
if (value === true) return -1;
// if pass => skip all values
if (value === false) return 0;
// return the bit mask value as is
return value;
}
let _activeStylingMapApplyFn: SyncStylingMapsFn|null = null;
export function getStylingMapsSyncFn() {
return _activeStylingMapApplyFn;
}
export function setStylingMapsSyncFn(fn: SyncStylingMapsFn) {
_activeStylingMapApplyFn = fn;
}
/**

View File

@ -11,18 +11,25 @@ import {RElement} from '../interfaces/renderer';
import {StylingContext as OldStylingContext, StylingIndex as OldStylingIndex} from '../interfaces/styling';
import {BINDING_INDEX, HEADER_OFFSET, HOST, LView, RENDERER} from '../interfaces/view';
import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getLView, getSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {getTNode, isStylingContext as isOldStylingContext} from '../util/view_utils';
import {applyClasses, applyStyles, registerBinding, updateClassBinding, updateStyleBinding} from './bindings';
import {TStylingContext} from './interfaces';
import {activeStylingMapFeature, normalizeIntoStylingMap} from './map_based_bindings';
import {attachStylingDebugObject} from './styling_debug';
import {allocStylingContext, updateContextDirectiveIndex} from './util';
import {allocStylingContext, hasValueChanged, updateContextDirectiveIndex} from './util';
/**
* --------
*
* This file contains the core logic for how styling instructions are processed in Angular.
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
/**
@ -49,26 +56,76 @@ export function stylingInit() {
*/
export function styleProp(
prop: string, value: string | number | String | null, suffix?: string | null): void {
const index = getSelectedIndex();
const lView = getLView();
const bindingIndex = lView[BINDING_INDEX]++;
const tNode = getTNode(index, lView);
const tContext = getStylesContext(tNode);
const defer = getActiveDirectiveSuperClassHeight() > 0;
updateStyleBinding(tContext, lView, prop, bindingIndex, value, defer);
_stylingProp(prop, value, false);
}
/**
* Mirror implementation of the `classProp()` instruction (found in `instructions/styling.ts`).
*/
export function classProp(className: string, value: boolean | null): void {
_stylingProp(className, value, true);
}
/**
* Shared function used to update a prop-based styling binding for an element.
*/
function _stylingProp(
prop: string, value: boolean | number | String | string | null, isClassBased: boolean) {
const index = getSelectedIndex();
const lView = getLView();
const bindingIndex = lView[BINDING_INDEX]++;
const tNode = getTNode(index, lView);
const tContext = getClassesContext(tNode);
const defer = getActiveDirectiveSuperClassHeight() > 0;
updateClassBinding(tContext, lView, className, bindingIndex, value, defer);
if (isClassBased) {
updateClassBinding(
getClassesContext(tNode), lView, prop, bindingIndex, value as string | boolean | null,
defer);
} else {
updateStyleBinding(
getStylesContext(tNode), lView, prop, bindingIndex, value as string | null, defer);
}
}
/**
* Mirror implementation of the `styleMap()` instruction (found in `instructions/styling.ts`).
*/
export function styleMap(styles: {[styleName: string]: any} | NO_CHANGE | null): void {
_stylingMap(styles, false);
}
/**
* Mirror implementation of the `classMap()` instruction (found in `instructions/styling.ts`).
*/
export function classMap(classes: {[className: string]: any} | NO_CHANGE | string | null): void {
_stylingMap(classes, true);
}
/**
* Shared function used to update a map-based styling binding for an element.
*
* When this function is called it will activate support for `[style]` and
* `[class]` bindings in Angular.
*/
function _stylingMap(value: {[key: string]: any} | string | null, isClassBased: boolean) {
activeStylingMapFeature();
const index = getSelectedIndex();
const lView = getLView();
const bindingIndex = lView[BINDING_INDEX]++;
if (value !== NO_CHANGE) {
const tNode = getTNode(index, lView);
const defer = getActiveDirectiveSuperClassHeight() > 0;
const oldValue = lView[bindingIndex];
const valueHasChanged = hasValueChanged(oldValue, value);
const lStylingMap = normalizeIntoStylingMap(oldValue, value);
if (isClassBased) {
updateClassBinding(
getClassesContext(tNode), lView, null, bindingIndex, lStylingMap, defer, valueHasChanged);
} else {
updateStyleBinding(
getStylesContext(tNode), lView, null, bindingIndex, lStylingMap, defer, valueHasChanged);
}
}
}
/**
@ -97,33 +154,6 @@ export function stylingApply() {
applyStyles(renderer, lView, getStylesContext(tNode), native, directiveIndex);
}
function getStylesContext(tNode: TNode): TStylingContext {
return getContext(tNode, false);
}
function getClassesContext(tNode: TNode): TStylingContext {
return getContext(tNode, true);
}
/**
* Returns/instantiates a styling context from/to a `tNode` instance.
*/
function getContext(tNode: TNode, isClassBased: boolean) {
let context = isClassBased ? tNode.newClasses : tNode.newStyles;
if (!context) {
context = allocStylingContext();
if (ngDevMode) {
attachStylingDebugObject(context);
}
if (isClassBased) {
tNode.newClasses = context;
} else {
tNode.newStyles = context;
}
}
return context;
}
/**
* Temporary function to bridge styling functionality between this new
* refactor (which is here inside of `styling_next/`) and the old
@ -209,3 +239,30 @@ function updateLastDirectiveIndex(tNode: TNode, directiveIndex: number) {
updateContextDirectiveIndex(getClassesContext(tNode), directiveIndex);
updateContextDirectiveIndex(getStylesContext(tNode), directiveIndex);
}
function getStylesContext(tNode: TNode): TStylingContext {
return getContext(tNode, false);
}
function getClassesContext(tNode: TNode): TStylingContext {
return getContext(tNode, true);
}
/**
* Returns/instantiates a styling context from/to a `tNode` instance.
*/
function getContext(tNode: TNode, isClassBased: boolean) {
let context = isClassBased ? tNode.newClasses : tNode.newStyles;
if (!context) {
context = allocStylingContext();
if (ngDevMode) {
attachStylingDebugObject(context);
}
if (isClassBased) {
tNode.newClasses = context;
} else {
tNode.newStyles = context;
}
}
return context;
}

View File

@ -8,6 +8,16 @@
import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer';
import {LView} from '../interfaces/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`.
@ -143,7 +153,7 @@ import {LView} from '../interfaces/view';
* styling apply call has been called (this is triggered by the
* `stylingApply()` instruction for the active element).
*
* # How Styles/Classes are Applied
* # 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
@ -166,20 +176,78 @@ import {LView} from '../interfaces/view';
* }
* ```
*
* ## The Apply Algorithm
* As explained above, each time a binding updates its value, the resulting
* value is stored in the `lView` array. These styling values have yet to
* be flushed to the element.
*
* Once all the styling instructions have been evaluated, then the styling
* context(s) are flushed to the element. When this happens, the context will
* be iterated over (property by property) and each binding source will be
* examined and the first non-null value will be applied to the element.
*
* Let's say that we the following template code:
*
* ```html
* <div [style.width]="w1" dir-that-set-width="w2"></div>
* ```
*
* There are two styling bindings in the code above and they both write
* to the `width` property. When styling is flushed on the element, the
* algorithm will try and figure out which one of these values to write
* to the element.
*
* In order to figure out which value to apply, the following
* binding prioritization is adhered to:
*
* 1. First template-level styling bindings are applied (if present).
* This includes things like `[style.width]` and `[class.active]`.
*
* 2. Second are styling-level host bindings present in directives.
* (if there are sub/super directives present then the sub directives
* are applied first).
*
* 3. Third are styling-level host bindings present in components.
* (if there are sub/super components present then the sub directives
* are applied first).
*
* This means that in the code above the styling binding present in the
* template is applied first and, only if its falsy, then the directive
* styling binding for width will be applied.
*
* ### What about map-based styling bindings?
* Map-based styling bindings are activated when there are one or more
* `[style]` and/or `[class]` bindings present on an element. When this
* code is activated, the apply algorithm will iterate over each map
* entry and apply each styling value to the element with the same
* prioritization rules as above.
*
* For the algorithm to apply styling values efficiently, the
* styling map entries must be applied in sync (property by property)
* with prop-based bindings. (The map-based algorithm is described
* more inside of the `render3/stlying_next/map_based_bindings.ts` file.)
*/
export interface TStylingContext extends Array<number|string|number|boolean|null> {
export interface TStylingContext extends Array<number|string|number|boolean|null|LStylingMap> {
/** Configuration data for the context */
[TStylingContextIndex.ConfigPosition]: TStylingConfigFlags;
/* Temporary value used to track directive index entries until
/** Temporary value used to track directive index entries until
the old styling code is fully removed. The reason why this
is required is to figure out which directive is last and,
when encountered, trigger a styling flush to happen */
[TStylingContextIndex.MaxDirectiveIndexPosition]: number;
/** The bit guard value for all map-based bindings on an element */
[TStylingContextIndex.MapBindingsBitGuardPosition]: number;
/** The total amount of map-based bindings present on an element */
[TStylingContextIndex.MapBindingsValuesCountPosition]: number;
/** The prop value for map-based bindings (there actually isn't a
* value at all, but this is just used in the context to avoid
* having any special code to update the binding information for
* map-based entries). */
[TStylingContextIndex.MapBindingsPropPosition]: string;
}
/**
@ -210,11 +278,18 @@ export const enum TStylingConfigFlags {
export const enum TStylingContextIndex {
ConfigPosition = 0,
MaxDirectiveIndexPosition = 1,
ValuesStartPosition = 2,
// index/offset values for map-based entries (i.e. `[style]`
// and `[class] bindings).
MapBindingsPosition = 2,
MapBindingsBitGuardPosition = 2,
MapBindingsValuesCountPosition = 3,
MapBindingsPropPosition = 4,
MapBindingsBindingsStartPosition = 5,
// each tuple entry in the context
// (mask, count, prop, ...bindings||default-value)
MaskOffset = 0,
GuardOffset = 0,
ValuesCountOffset = 1,
PropOffset = 2,
BindingsStartOffset = 3,
@ -225,7 +300,7 @@ export const enum TStylingContextIndex {
*/
export interface ApplyStylingFn {
(renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string,
value: string|null, bindingIndex: number): void;
value: string|null, bindingIndex?: number|null): void;
}
/**
@ -236,4 +311,82 @@ export interface ApplyStylingFn {
* 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 StylingBindingData = LView | (string | number | boolean)[];
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 LStylingMap extends Array<{}|string|number|null> {
[LStylingMapIndex.RawValuePosition]: {}|string|null;
}
/**
* An index of position and offset points for any data stored within a `LStylingMap` instance.
*/
export const enum LStylingMapIndex {
/** The location of the raw key/value map instance used last to populate the array entries */
RawValuePosition = 0,
/** Where the values start in the array */
ValuesStartPosition = 1,
/** 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 `LStylingMap` 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, applyStylingFn: ApplyStylingFn, mode: StylingMapsSyncMode,
targetProp?: string|null, defaultValue?: 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,
}

View File

@ -0,0 +1,375 @@
/**
* @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 {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer';
import {setStylingMapsSyncFn} from './bindings';
import {ApplyStylingFn, LStylingData, LStylingMap, LStylingMapIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from './interfaces';
import {getBindingValue, getValuesCount, isStylingValueDefined} from './util';
/**
* --------
*
* This file contains the algorithm logic for applying map-based bindings
* such as `[style]` and `[class]`.
*
* --------
*/
/**
* Used to apply styling values presently within any map-based bindings on an element.
*
* Angular supports map-based styling bindings which can be applied via the
* `[style]` and `[class]` bindings which can be placed on any HTML element.
* These bindings can work independently, together or alongside prop-based
* styling bindings (e.g. `<div [style]="x" [style.width]="w">`).
*
* If a map-based styling binding is detected by the compiler, the following
* AOT code is produced:
*
* ```typescript
* styleMap(ctx.styles); // styles = {key:value}
* classMap(ctx.classes); // classes = {key:value}|string
* ```
*
* If and when either of the instructions above are evaluated, then the code
* present in this file is included into the bundle. The mechanism used, to
* activate support for map-based bindings at runtime is possible via the
* `activeStylingMapFeature` function (which is also present in this file).
*
* # The Algorithm
* Whenever a map-based binding updates (which is when the identity of the
* map-value changes) then the map is iterated over and a `LStylingMap` array
* is produced. The `LStylingMap` instance is stored in the binding location
* where the `BINDING_INDEX` is situated when the `styleMap()` or `classMap()`
* instruction were called. Once the binding changes, then the internal `bitMask`
* value is marked as dirty.
*
* Styling values are applied once CD exits the element (which happens when
* the `select(n)` instruction is called or the template function exits). When
* this occurs, all prop-based bindings are applied. If a map-based binding is
* present then a special flushing function (called a sync function) is made
* available and it will be called each time a styling property is flushed.
*
* The flushing algorithm is designed to apply styling for a property (which is
* a CSS property or a className value) one by one. If map-based bindings
* are present, then the flushing algorithm will keep calling the maps styling
* sync function each time a property is visited. This way, the flushing
* behavior of map-based bindings will always be at the same property level
* as the current prop-based property being iterated over (because everything
* is alphabetically sorted).
*
* Let's imagine we have the following HTML template code:
*
* ```html
* <div [style]="{width:'100px', height:'200px', 'z-index':'10'}"
* [style.width.px]="200">...</div>
* ```
*
* When CD occurs, both the `[style]` and `[style.width]` bindings
* are evaluated. Then when the styles are flushed on screen, the
* following operations happen:
*
* 1. `[style.width]` is attempted to be written to the element.
*
* 2. Once that happens, the algorithm instructs the map-based
* entries (`[style]` in this case) to "catch up" and apply
* all values up to the `width` value. When this happens the
* `height` value is applied to the element (since it is
* alphabetically situated before the `width` property).
*
* 3. Since there are no more prop-based entries anymore, the
* loop exits and then, just before the flushing ends, it
* instructs all map-based bindings to "finish up" applying
* their values.
*
* 4. The only remaining value within the map-based entries is
* the `z-index` value (`width` got skipped because it was
* successfully applied via the prop-based `[style.width]`
* binding). Since all map-based entries are told to "finish up",
* the `z-index` value is iterated over and it is then applied
* to the element.
*
* The most important thing to take note of here is that prop-based
* bindings are evaluated in order alongside map-based bindings.
* This allows all styling across an element to be applied in O(n)
* time (a similar algorithm is that of the array merge algorithm
* in merge sort).
*/
export const syncStylingMap: SyncStylingMapsFn =
(context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
data: LStylingData, applyStylingFn: ApplyStylingFn, mode: StylingMapsSyncMode,
targetProp?: string | null, defaultValue?: string | null): boolean => {
let targetPropValueWasApplied = false;
// once the map-based styling code is activate it is never deactivated. For this reason a
// check to see if the current styling context has any map based bindings is required.
const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition);
if (totalMaps) {
let runTheSyncAlgorithm = true;
const loopUntilEnd = !targetProp;
// If the code is told to finish up (run until the end), but the mode
// hasn't been flagged to apply values (it only traverses values) then
// there is no point in iterating over the array because nothing will
// be applied to the element.
if (loopUntilEnd && (mode & ~StylingMapsSyncMode.ApplyAllValues)) {
runTheSyncAlgorithm = false;
targetPropValueWasApplied = true;
}
if (runTheSyncAlgorithm) {
targetPropValueWasApplied = innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, mode, targetProp || null, 0,
defaultValue || null);
}
if (loopUntilEnd) {
resetSyncCursors();
}
}
return targetPropValueWasApplied;
};
/**
* Recursive function designed to apply map-based styling to an element one map at a time.
*
* This function is designed to be called from the `syncStylingMap` function and will
* apply map-based styling data one map at a time to the provided `element`.
*
* This function is recursive and it will call itself if a follow-up map value is to be
* processed. To learn more about how the algorithm works, see `syncStylingMap`.
*/
function innerSyncStylingMap(
context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
data: LStylingData, applyStylingFn: ApplyStylingFn, mode: StylingMapsSyncMode,
targetProp: string | null, currentMapIndex: number, defaultValue: string | null): boolean {
let targetPropValueWasApplied = false;
const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition);
if (currentMapIndex < totalMaps) {
const bindingIndex = getBindingValue(
context, TStylingContextIndex.MapBindingsPosition, currentMapIndex) as number;
const lStylingMap = data[bindingIndex] as LStylingMap;
let cursor = getCurrentSyncCursor(currentMapIndex);
while (cursor < lStylingMap.length) {
const prop = getMapProp(lStylingMap, cursor);
const iteratedTooFar = targetProp && prop > targetProp;
const isTargetPropMatched = !iteratedTooFar && prop === targetProp;
const value = getMapValue(lStylingMap, cursor);
const valueIsDefined = isStylingValueDefined(value);
// the recursive code is designed to keep applying until
// it reaches or goes past the target prop. If and when
// this happens then it will stop processing values, but
// all other map values must also catch up to the same
// point. This is why a recursive call is still issued
// even if the code has iterated too far.
const innerMode =
iteratedTooFar ? mode : resolveInnerMapMode(mode, valueIsDefined, isTargetPropMatched);
const innerProp = iteratedTooFar ? targetProp : prop;
let valueApplied = innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, innerMode, innerProp,
currentMapIndex + 1, defaultValue);
if (iteratedTooFar) {
break;
}
if (!valueApplied && isValueAllowedToBeApplied(mode, isTargetPropMatched)) {
const useDefault = isTargetPropMatched && !valueIsDefined;
const valueToApply = useDefault ? defaultValue : value;
const bindingIndexToApply = useDefault ? bindingIndex : null;
applyStylingFn(renderer, element, prop, valueToApply, bindingIndexToApply);
valueApplied = true;
}
targetPropValueWasApplied = valueApplied && isTargetPropMatched;
cursor += LStylingMapIndex.TupleSize;
}
setCurrentSyncCursor(currentMapIndex, cursor);
}
return targetPropValueWasApplied;
}
/**
* Enables support for map-based styling bindings (e.g. `[style]` and `[class]` bindings).
*/
export function activeStylingMapFeature() {
setStylingMapsSyncFn(syncStylingMap);
}
/**
* Used to determine the mode for the inner recursive call.
*
* If an inner map is iterated on then this is done so for one
* of two reasons:
*
* - The target property was detected and the inner map
* must now "catch up" (pointer-wise) up to where the current
* map's cursor is situated.
*
* - The target property was not detected in the current map
* and must be found in an inner map. This can only be allowed
* if the current map iteration is not set to skip the target
* property.
*/
function resolveInnerMapMode(
currentMode: number, valueIsDefined: boolean, isExactMatch: boolean): number {
let innerMode = currentMode;
if (!valueIsDefined && isExactMatch && !(currentMode & StylingMapsSyncMode.SkipTargetProp)) {
// case 1: set the mode to apply the targeted prop value if it
// ends up being encountered in another map value
innerMode |= StylingMapsSyncMode.ApplyTargetProp;
innerMode &= ~StylingMapsSyncMode.SkipTargetProp;
} else {
// case 2: set the mode to skip the targeted prop value if it
// ends up being encountered in another map value
innerMode |= StylingMapsSyncMode.SkipTargetProp;
innerMode &= ~StylingMapsSyncMode.ApplyTargetProp;
}
return innerMode;
}
/**
* Decides whether or not a prop/value entry will be applied to an element.
*
* To determine whether or not a value is to be applied,
* the following procedure is evaluated:
*
* First check to see the current `mode` status:
* 1. If the mode value permits all props to be applied then allow.
* - But do not allow if the current prop is set to be skipped.
* 2. Otherwise if the current prop is permitted then allow.
*/
function isValueAllowedToBeApplied(mode: number, isTargetPropMatched: boolean) {
let doApplyValue = (mode & StylingMapsSyncMode.ApplyAllValues) > 0;
if (!doApplyValue) {
if (mode & StylingMapsSyncMode.ApplyTargetProp) {
doApplyValue = isTargetPropMatched;
}
} else if ((mode & StylingMapsSyncMode.SkipTargetProp) && isTargetPropMatched) {
doApplyValue = false;
}
return doApplyValue;
}
/**
* Used to keep track of concurrent cursor values for multiple map-based styling bindings present on
* an element.
*/
const MAP_CURSORS: number[] = [];
/**
* Used to reset the state of each cursor value being used to iterate over map-based styling
* bindings.
*/
function resetSyncCursors() {
for (let i = 0; i < MAP_CURSORS.length; i++) {
MAP_CURSORS[i] = LStylingMapIndex.ValuesStartPosition;
}
}
/**
* Returns an active cursor value at a given mapIndex location.
*/
function getCurrentSyncCursor(mapIndex: number) {
if (mapIndex >= MAP_CURSORS.length) {
MAP_CURSORS.push(LStylingMapIndex.ValuesStartPosition);
}
return MAP_CURSORS[mapIndex];
}
/**
* Sets a cursor value at a given mapIndex location.
*/
function setCurrentSyncCursor(mapIndex: number, indexValue: number) {
MAP_CURSORS[mapIndex] = indexValue;
}
/**
* Used to convert a {key:value} map into a `LStylingMap` array.
*
* This function will either generate a new `LStylingMap` instance
* or it will patch the provided `newValues` map value into an
* existing `LStylingMap` value (this only happens if `bindingValue`
* is an instance of `LStylingMap`).
*
* If a new key/value map is provided with an old `LStylingMap`
* value then all properties will be overwritten with their new
* values or with `null`. This means that the array will never
* shrink in size (but it will also not be created and thrown
* away whenever the {key:value} map entries change).
*/
export function normalizeIntoStylingMap(
bindingValue: null | LStylingMap,
newValues: {[key: string]: any} | string | null | undefined): LStylingMap {
const lStylingMap: LStylingMap = Array.isArray(bindingValue) ? bindingValue : [null];
lStylingMap[LStylingMapIndex.RawValuePosition] = newValues || null;
// because the new values may not include all the properties
// that the old ones had, all values are set to `null` before
// the new values are applied. This way, when flushed, the
// styling algorithm knows exactly what style/class values
// to remove from the element (since they are `null`).
for (let j = LStylingMapIndex.ValuesStartPosition; j < lStylingMap.length;
j += LStylingMapIndex.TupleSize) {
setMapValue(lStylingMap, j, null);
}
let props: string[]|null = null;
let map: {[key: string]: any}|undefined|null;
let allValuesTrue = false;
if (typeof newValues === 'string') { // [class] bindings allow string values
if (newValues.length) {
props = newValues.split(/\s+/);
allValuesTrue = true;
}
} else {
props = newValues ? Object.keys(newValues) : null;
map = newValues;
}
if (props) {
outer: for (let i = 0; i < props.length; i++) {
const prop = props[i] as string;
const value = allValuesTrue ? true : map ![prop];
for (let j = LStylingMapIndex.ValuesStartPosition; j < lStylingMap.length;
j += LStylingMapIndex.TupleSize) {
const propAtIndex = getMapProp(lStylingMap, j);
if (prop <= propAtIndex) {
if (propAtIndex === prop) {
setMapValue(lStylingMap, j, value);
} else {
lStylingMap.splice(j, 0, prop, value);
}
continue outer;
}
}
lStylingMap.push(prop, value);
}
}
return lStylingMap;
}
export function getMapProp(map: LStylingMap, index: number): string {
return map[index + LStylingMapIndex.PropOffset] as string;
}
export function setMapValue(map: LStylingMap, index: number, value: string | null): void {
map[index + LStylingMapIndex.ValueOffset] = value;
}
export function getMapValue(map: LStylingMap, index: number): string|null {
return map[index + LStylingMapIndex.ValueOffset] as string | null;
}

View File

@ -8,9 +8,20 @@
import {RElement} from '../interfaces/renderer';
import {attachDebugObject} from '../util/debug_utils';
import {BIT_MASK_APPLY_ALL, DEFAULT_BINDING_INDEX_VALUE, applyStyling} from './bindings';
import {StylingBindingData, TStylingContext, TStylingContextIndex} from './interfaces';
import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked} from './util';
import {applyStyling} from './bindings';
import {ApplyStylingFn, LStylingData, TStylingContext, TStylingContextIndex} from './interfaces';
import {activeStylingMapFeature} from './map_based_bindings';
import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased} from './util';
/**
* --------
*
* This file contains the core debug functionality for styling in Angular.
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
/**
@ -19,18 +30,15 @@ import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked}
* A value such as this is generated as an artifact of the `DebugStyling`
* summary.
*/
export interface StylingSummary {
export interface LStylingSummary {
/** The style/class property that the summary is attached to */
prop: string;
/** The last applied value for the style/class property */
value: string|null;
value: string|boolean|null;
/** The binding index of the last applied style/class property */
bindingIndex: number|null;
/** Every binding source that is writing the style/class property represented in this tuple */
sourceValues: {value: string | number | null, bindingIndex: number|null}[];
}
/**
@ -44,7 +52,7 @@ export interface DebugStyling {
* A summarization of each style/class property
* present in the context.
*/
summary: {[key: string]: StylingSummary}|null;
summary: {[key: string]: LStylingSummary};
/**
* A key/value map of all styling properties and their
@ -108,22 +116,26 @@ class TStylingContextDebug {
get entries(): {[prop: string]: TStylingTupleSummary} {
const context = this.context;
const entries: {[prop: string]: TStylingTupleSummary} = {};
const start = TStylingContextIndex.ValuesStartPosition;
const start = TStylingContextIndex.MapBindingsPosition;
let i = start;
while (i < context.length) {
const valuesCount = getValuesCount(context, i);
// the context may contain placeholder values which are populated ahead of time,
// but contain no actual binding values. In this situation there is no point in
// classifying this as an "entry" since no real data is stored here yet.
if (valuesCount) {
const prop = getProp(context, i);
const guardMask = getGuardMask(context, i);
const valuesCount = getValuesCount(context, i);
const defaultValue = getDefaultValue(context, i);
const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset;
const sources: (number | string | null)[] = [];
const sources: (number | string | null)[] = [];
for (let j = 0; j < valuesCount; j++) {
sources.push(context[bindingsStartPosition + j] as number | string | null);
}
entries[prop] = {prop, guardMask, valuesCount, defaultValue, sources};
}
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
}
@ -138,51 +150,19 @@ class TStylingContextDebug {
* application has `ngDevMode` activated.
*/
export class NodeStylingDebug implements DebugStyling {
private _contextDebug: TStylingContextDebug;
constructor(public context: TStylingContext, private _data: StylingBindingData) {
this._contextDebug = (this.context as any).debug as any;
}
constructor(public context: TStylingContext, private _data: LStylingData) {}
/**
* Returns a detailed summary of each styling entry in the context and
* what their runtime representation is.
*
* See `StylingSummary`.
* See `LStylingSummary`.
*/
get summary(): {[key: string]: StylingSummary} {
const contextEntries = this._contextDebug.entries;
const finalValues: {[key: string]: {value: string, bindingIndex: number}} = {};
this._mapValues((prop: string, value: any, bindingIndex: number) => {
finalValues[prop] = {value, bindingIndex};
get summary(): {[key: string]: LStylingSummary} {
const entries: {[key: string]: LStylingSummary} = {};
this._mapValues((prop: string, value: any, bindingIndex: number | null) => {
entries[prop] = {prop, value, bindingIndex};
});
const entries: {[key: string]: StylingSummary} = {};
const values = this.values;
const props = Object.keys(values);
for (let i = 0; i < props.length; i++) {
const prop = props[i];
const contextEntry = contextEntries[prop];
const sourceValues = contextEntry.sources.map(v => {
let value: string|number|null;
let bindingIndex: number|null;
if (typeof v === 'number') {
value = this._data[v];
bindingIndex = v;
} else {
value = v;
bindingIndex = null;
}
return {bindingIndex, value};
});
const finalValue = finalValues[prop] !;
let bindingIndex: number|null = finalValue.bindingIndex;
bindingIndex = bindingIndex === DEFAULT_BINDING_INDEX_VALUE ? null : bindingIndex;
entries[prop] = {prop, value: finalValue.value, bindingIndex, sourceValues};
}
return entries;
}
@ -195,16 +175,21 @@ export class NodeStylingDebug implements DebugStyling {
return entries;
}
private _mapValues(fn: (prop: string, value: any, bindingIndex: number) => any) {
private _mapValues(fn: (prop: string, value: any, bindingIndex: number|null) => any) {
// there is no need to store/track an element instance. The
// element is only used when the styling algorithm attempts to
// style the value (and we mock out the stylingApplyFn anyway).
const mockElement = {} as any;
const hasMaps = getValuesCount(this.context, TStylingContextIndex.MapBindingsPosition) > 0;
if (hasMaps) {
activeStylingMapFeature();
}
const mapFn =
const mapFn: ApplyStylingFn =
(renderer: any, element: RElement, prop: string, value: any, bindingIndex: number) => {
fn(prop, value, bindingIndex);
fn(prop, value, bindingIndex || null);
};
applyStyling(this.context, null, mockElement, this._data, BIT_MASK_APPLY_ALL, mapFn);
applyStyling(this.context, null, mockElement, this._data, true, mapFn);
}
}

View File

@ -7,13 +7,19 @@
*/
import {StylingContext} from '../interfaces/styling';
import {getProp as getOldProp, getSinglePropIndexValue as getOldSinglePropIndexValue} from '../styling/class_and_style_bindings';
import {TStylingConfigFlags, TStylingContext, TStylingContextIndex} from './interfaces';
import {LStylingMap, LStylingMapIndex, TStylingConfigFlags, TStylingContext, TStylingContextIndex} from './interfaces';
const MAP_BASED_ENTRY_PROP_NAME = '--MAP--';
/**
* Creates a new instance of the `TStylingContext`.
*
* This function will also pre-fill the context with data
* for map-based bindings.
*/
export function allocStylingContext(): TStylingContext {
return [TStylingConfigFlags.Initial, 0];
return [TStylingConfigFlags.Initial, 0, 0, 0, MAP_BASED_ENTRY_PROP_NAME];
}
/**
@ -48,14 +54,14 @@ export function getProp(context: TStylingContext, index: number) {
}
export function getGuardMask(context: TStylingContext, index: number) {
return context[index + TStylingContextIndex.MaskOffset] as number;
return context[index + TStylingContextIndex.GuardOffset] as number;
}
export function getValuesCount(context: TStylingContext, index: number) {
return context[index + TStylingContextIndex.ValuesCountOffset] as number;
}
export function getValue(context: TStylingContext, index: number, offset: number) {
export function getBindingValue(context: TStylingContext, index: number, offset: number) {
return context[index + TStylingContextIndex.BindingsStartOffset + offset] as number | string;
}
@ -80,3 +86,32 @@ export function lockContext(context: TStylingContext) {
export function isContextLocked(context: TStylingContext): boolean {
return (getConfig(context) & TStylingConfigFlags.Locked) > 0;
}
export function getPropValuesStartPosition(context: TStylingContext) {
return TStylingContextIndex.MapBindingsBindingsStartPosition +
context[TStylingContextIndex.MapBindingsValuesCountPosition];
}
export function isMapBased(prop: string) {
return prop === MAP_BASED_ENTRY_PROP_NAME;
}
export function hasValueChanged(
a: LStylingMap | number | String | string | null | boolean | undefined | {},
b: LStylingMap | number | String | string | null | boolean | undefined | {}): boolean {
const compareValueA = Array.isArray(a) ? a[LStylingMapIndex.RawValuePosition] : a;
const compareValueB = Array.isArray(b) ? b[LStylingMapIndex.RawValuePosition] : b;
return compareValueA !== compareValueB;
}
/**
* Determines whether the provided styling value is truthy or falsy.
*/
export function isStylingValueDefined(value: any) {
// the reason why null is compared against is because
// a CSS class value that is set to `false` must be
// respected (otherwise it would be treated as falsy).
// Empty string values are because developers usually
// set a value to an empty string to remove it.
return value != null && value !== '';
}

View File

@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CompilerStylingMode, compilerSetStylingMode} from '@angular/compiler/src/render3/view/styling_state';
import {Component, Directive, HostBinding, Input} from '@angular/core';
import {Component, Directive, HostBinding, Input, ViewChild} from '@angular/core';
import {DebugNode, LViewDebug, toDebug} from '@angular/core/src/render3/debug';
import {RuntimeStylingMode, runtimeSetStylingMode} from '@angular/core/src/render3/styling_next/state';
import {loadLContextFromNode} from '@angular/core/src/render3/util/discovery_utils';
import {ngDevModeResetPerfCounters as resetStylingCounters} from '@angular/core/src/util/ng_dev_mode';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
@ -255,7 +256,7 @@ describe('new styling integration', () => {
});
});
onlyInIvy('only ivy has style debugging support')
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should support situations where there are more than 32 bindings', () => {
const TOTAL_BINDINGS = 34;
@ -314,8 +315,276 @@ describe('new styling integration', () => {
expect(value).toEqual(`final${num}`);
}
});
onlyInIvy('only ivy has style debugging support')
.it('should apply map-based style and class entries', () => {
@Component({template: '<div [style]="s" [class]="c"></div>'})
class Cmp {
public c !: {[key: string]: any};
updateClasses(prop: string) {
this.c = {...this.c || {}};
this.c[prop] = true;
}
public s !: {[key: string]: any};
updateStyles(prop: string, value: string|number|null) {
this.s = {...this.s || {}};
this.s[prop] = value;
}
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
comp.updateStyles('width', '100px');
comp.updateStyles('height', '200px');
comp.updateClasses('abc');
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
const node = getDebugNode(element) !;
const styles = node.styles !;
const classes = node.classes !;
const stylesSummary = styles.summary;
const widthSummary = stylesSummary['width'];
expect(widthSummary.prop).toEqual('width');
expect(widthSummary.value).toEqual('100px');
const heightSummary = stylesSummary['height'];
expect(heightSummary.prop).toEqual('height');
expect(heightSummary.value).toEqual('200px');
const classesSummary = classes.summary;
const abcSummary = classesSummary['abc'];
expect(abcSummary.prop).toEqual('abc');
expect(abcSummary.value as any).toEqual(true);
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should resolve styling collisions across templates, directives and components for prop and map-based entries',
() => {
@Directive({selector: '[dir-that-sets-styling]'})
class DirThatSetsStyling {
@HostBinding('style') public map: any = {color: 'red', width: '777px'};
}
@Component({
template: `
<div [style.width]="width"
[style]="map"
style="width:200px; font-size:99px"
dir-that-sets-styling
#dir
[class.xyz]="xyz"></div>
`
})
class Cmp {
map: any = {width: '111px', opacity: '0.5'};
width: string|null = '555px';
@ViewChild('dir', {read: DirThatSetsStyling})
dir !: DirThatSetsStyling;
}
TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
const node = getDebugNode(element) !;
const styles = node.styles !;
expect(styles.values).toEqual({
'width': '555px',
'color': 'red',
'font-size': '99px',
'opacity': '0.5',
});
comp.width = null;
fixture.detectChanges();
expect(styles.values).toEqual({
'width': '111px',
'color': 'red',
'font-size': '99px',
'opacity': '0.5',
});
comp.map = null;
fixture.detectChanges();
expect(styles.values).toEqual({
'width': '777px',
'color': 'red',
'font-size': '99px',
'opacity': null,
});
comp.dir.map = null;
fixture.detectChanges();
expect(styles.values).toEqual({
'width': '200px',
'color': null,
'font-size': '99px',
'opacity': null,
});
});
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]'})
class DirThatSetsStyling {
@HostBinding('style') public map: any = {width: '999px', height: '999px'};
}
@Component({
template: `
<div #dir
style="width:0px; height:0px"
[style.width]="width"
[style.height]="height"
[style]="map"
dir-that-sets-styling></div>
`
})
class Cmp {
width: string|null = '111px';
height: string|null = '111px';
map: any = {width: '555px', height: '555px'};
@ViewChild('dir', {read: DirThatSetsStyling})
dir !: DirThatSetsStyling;
}
TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
resetStylingCounters();
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
// both are applied because this is the first pass
assertStyleCounters(2, 0);
assertStyle(element, 'width', '111px');
assertStyle(element, 'height', '111px');
comp.width = '222px';
resetStylingCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
assertStyle(element, 'width', '222px');
assertStyle(element, 'height', '111px');
comp.height = '222px';
resetStylingCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
assertStyle(element, 'width', '222px');
assertStyle(element, 'height', '222px');
comp.width = null;
resetStylingCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
assertStyle(element, 'width', '555px');
assertStyle(element, 'height', '222px');
comp.width = '123px';
comp.height = '123px';
resetStylingCounters();
fixture.detectChanges();
assertStyle(element, 'width', '123px');
assertStyle(element, 'height', '123px');
comp.map = {};
resetStylingCounters();
fixture.detectChanges();
// both are applied because the map was altered
assertStyleCounters(2, 0);
assertStyle(element, 'width', '123px');
assertStyle(element, 'height', '123px');
comp.width = null;
resetStylingCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
assertStyle(element, 'width', '999px');
assertStyle(element, 'height', '123px');
comp.dir.map = null;
resetStylingCounters();
fixture.detectChanges();
// both are applied because the map was altered
assertStyleCounters(2, 0);
assertStyle(element, 'width', '0px');
assertStyle(element, 'height', '123px');
comp.dir.map = {width: '1000px', height: '1000px', color: 'red'};
resetStylingCounters();
fixture.detectChanges();
// all three are applied because the map was altered
assertStyleCounters(3, 0);
assertStyle(element, 'width', '1000px');
assertStyle(element, 'height', '123px');
assertStyle(element, 'color', 'red');
comp.height = null;
resetStylingCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
assertStyle(element, 'width', '1000px');
assertStyle(element, 'height', '1000px');
assertStyle(element, 'color', 'red');
comp.map = {color: 'blue', width: '2000px', opacity: '0.5'};
resetStylingCounters();
fixture.detectChanges();
// all four are applied because the map was altered
assertStyleCounters(4, 0);
assertStyle(element, 'width', '2000px');
assertStyle(element, 'height', '1000px');
assertStyle(element, 'color', 'blue');
assertStyle(element, 'opacity', '0.5');
comp.map = {color: 'blue', width: '2000px'};
resetStylingCounters();
fixture.detectChanges();
// all four are applied because the map was altered
assertStyleCounters(3, 1);
assertStyle(element, 'width', '2000px');
assertStyle(element, 'height', '1000px');
assertStyle(element, 'color', 'blue');
assertStyle(element, 'opacity', '');
});
});
function assertStyleCounters(countForSet: number, countForRemove: number) {
expect(ngDevMode !.rendererSetStyle).toEqual(countForSet);
expect(ngDevMode !.rendererRemoveStyle).toEqual(countForRemove);
}
function assertStyle(element: HTMLElement, prop: string, value: any) {
expect((element.style as any)[prop]).toEqual(value);
}
function getDebugNode(element: Node): DebugNode|null {
const lContext = loadLContextFromNode(element);
const lViewDebug = toDebug(lContext.lView) as LViewDebug;

View File

@ -36,7 +36,7 @@
"name": "DEFAULT_BINDING_VALUE"
},
{
"name": "DEFAULT_MASK_VALUE"
"name": "DEFAULT_GUARD_MASK_VALUE"
},
{
"name": "DEFAULT_SIZE_VALUE"
@ -71,6 +71,9 @@
{
"name": "INJECTOR_BLOOM_PARENT_SIZE"
},
{
"name": "MAP_BASED_ENTRY_PROP_NAME"
},
{
"name": "MONKEY_PATCH_KEY_NAME"
},
@ -431,6 +434,9 @@
{
"name": "getProp"
},
{
"name": "getPropValuesStartPosition"
},
{
"name": "getRenderFlags"
},

View File

@ -11,9 +11,6 @@
{
"name": "BINDING_INDEX"
},
{
"name": "BIT_MASK_APPLY_ALL"
},
{
"name": "BLOOM_MASK"
},
@ -50,14 +47,11 @@
{
"name": "DECLARATION_VIEW"
},
{
"name": "DEFAULT_BINDING_INDEX_VALUE"
},
{
"name": "DEFAULT_BINDING_VALUE"
},
{
"name": "DEFAULT_MASK_VALUE"
"name": "DEFAULT_GUARD_MASK_VALUE"
},
{
"name": "DEFAULT_SIZE_VALUE"
@ -122,6 +116,9 @@
{
"name": "IterableDiffers"
},
{
"name": "MAP_BASED_ENTRY_PROP_NAME"
},
{
"name": "MIN_DIRECTIVE_ID"
},
@ -218,6 +215,12 @@
{
"name": "SANITIZER"
},
{
"name": "STYLING_INDEX_FOR_MAP_BINDING"
},
{
"name": "STYLING_INDEX_START_VALUE"
},
{
"name": "SWITCH_ELEMENT_REF_FACTORY"
},
@ -305,6 +308,9 @@
{
"name": "__values"
},
{
"name": "_activeStylingMapApplyFn"
},
{
"name": "_c0"
},
@ -395,6 +401,9 @@
{
"name": "_stylingMode"
},
{
"name": "_stylingProp"
},
{
"name": "_symbolIterator"
},
@ -590,6 +599,9 @@
{
"name": "currentClassIndex"
},
{
"name": "currentStyleIndex"
},
{
"name": "decreaseElementDepthCount"
},
@ -731,6 +743,9 @@
{
"name": "getBindingNameFromIndex"
},
{
"name": "getBindingValue"
},
{
"name": "getBindingsEnabled"
},
@ -920,6 +935,9 @@
{
"name": "getProp"
},
{
"name": "getPropValuesStartPosition"
},
{
"name": "getRenderFlags"
},
@ -953,6 +971,9 @@
{
"name": "getStylingContextFromLView"
},
{
"name": "getStylingMapsSyncFn"
},
{
"name": "getSymbolIterator"
},
@ -971,9 +992,6 @@
{
"name": "getValue"
},
{
"name": "getValue"
},
{
"name": "getValuesCount"
},
@ -1001,6 +1019,9 @@
{
"name": "hasValueChanged"
},
{
"name": "hasValueChanged"
},
{
"name": "hyphenate"
},
@ -1134,7 +1155,7 @@
"name": "isStylingContext"
},
{
"name": "isValueDefined"
"name": "isStylingValueDefined"
},
{
"name": "iterateListLike"
@ -1208,6 +1229,9 @@
{
"name": "noSideEffects"
},
{
"name": "normalizeBitMaskValue"
},
{
"name": "patchContextWithStaticAttrs"
},
@ -1490,6 +1514,9 @@
{
"name": "updateSingleStylingValue"
},
{
"name": "updateStyleBinding"
},
{
"name": "valueExists"
},

View File

@ -0,0 +1,79 @@
/**
* @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 {normalizeIntoStylingMap as createMap} from '../../../src/render3/styling_next/map_based_bindings';
describe('map-based bindings', () => {
describe('LStylingMap construction', () => {
it('should create a new LStylingMap instance from a given value', () => {
createAndAssertValues(null, []);
createAndAssertValues(undefined, []);
createAndAssertValues({}, []);
createAndAssertValues({foo: 'bar'}, ['foo', 'bar']);
createAndAssertValues({bar: null}, ['bar', null]);
createAndAssertValues('', []);
createAndAssertValues('abc xyz', ['abc', true, 'xyz', true]);
createAndAssertValues([], []);
});
it('should list each entry in the context in alphabetical order', () => {
const value1 = {width: '200px', color: 'red', zIndex: -1};
const map1 = createMap(null, value1);
expect(map1).toEqual([value1, 'color', 'red', 'width', '200px', 'zIndex', -1]);
const value2 = 'yes no maybe';
const map2 = createMap(null, value2);
expect(map2).toEqual([value2, 'maybe', true, 'no', true, 'yes', true]);
});
it('should patch an existing LStylingMap entry with new values and retain the alphabetical order',
() => {
const value1 = {color: 'red'};
const map1 = createMap(null, value1);
expect(map1).toEqual([value1, 'color', 'red']);
const value2 = {backgroundColor: 'red', color: 'blue', opacity: '0.5'};
const map2 = createMap(map1, value2);
expect(map1).toBe(map2);
expect(map1).toEqual(
[value2, 'backgroundColor', 'red', 'color', 'blue', 'opacity', '0.5']);
const value3 = 'myClass';
const map3 = createMap(null, value3);
expect(map3).toEqual([value3, 'myClass', true]);
const value4 = 'yourClass everyonesClass myClass';
const map4 = createMap(map3, value4);
expect(map3).toBe(map4);
expect(map4).toEqual([value4, 'everyonesClass', true, 'myClass', true, 'yourClass', true]);
});
it('should nullify old values that are not apart of the new set of values', () => {
const value1 = {color: 'red', fontSize: '20px'};
const map1 = createMap(null, value1);
expect(map1).toEqual([value1, 'color', 'red', 'fontSize', '20px']);
const value2 = {color: 'blue', borderColor: 'purple', opacity: '0.5'};
const map2 = createMap(map1, value2);
expect(map2).toEqual(
[value2, 'borderColor', 'purple', 'color', 'blue', 'fontSize', null, 'opacity', '0.5']);
const value3 = 'orange';
const map3 = createMap(null, value3);
expect(map3).toEqual([value3, 'orange', true]);
const value4 = 'apple banana';
const map4 = createMap(map3, value4);
expect(map4).toEqual([value4, 'apple', true, 'banana', true, 'orange', null]);
});
});
});
function createAndAssertValues(newValue: any, entries: any[]) {
const result = createMap(null, newValue);
expect(result).toEqual([newValue || null, ...entries]);
}

View File

@ -5,7 +5,7 @@
* 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 {registerBinding} from '@angular/core/src/render3/styling_next/bindings';
import {DEFAULT_GUARD_MASK_VALUE, registerBinding} from '@angular/core/src/render3/styling_next/bindings';
import {attachStylingDebugObject} from '@angular/core/src/render3/styling_next/styling_debug';
import {allocStylingContext} from '../../../src/render3/styling_next/util';
@ -16,7 +16,7 @@ describe('styling context', () => {
const context = debug.context;
expect(debug.entries).toEqual({});
registerBinding(context, 0, 'width', '100px');
registerBinding(context, 1, 'width', '100px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
@ -25,21 +25,21 @@ describe('styling context', () => {
sources: ['100px'],
});
registerBinding(context, 1, 'width', 20);
registerBinding(context, 2, 'width', 20);
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 2,
guardMask: buildGuardMask(1),
guardMask: buildGuardMask(2),
defaultValue: '100px',
sources: [20, '100px'],
});
registerBinding(context, 2, 'height', 10);
registerBinding(context, 3, 'height', 15);
registerBinding(context, 3, 'height', 10);
registerBinding(context, 4, 'height', 15);
expect(debug.entries['height']).toEqual({
prop: 'height',
valuesCount: 3,
guardMask: buildGuardMask(2, 3),
guardMask: buildGuardMask(3, 4),
defaultValue: null,
sources: [10, 15, null],
});
@ -48,9 +48,8 @@ describe('styling context', () => {
it('should overwrite a default value for an entry only if it is non-null', () => {
const debug = makeContextWithDebug();
const context = debug.context;
expect(debug.entries).toEqual({});
registerBinding(context, 0, 'width', null);
registerBinding(context, 1, 'width', null);
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
@ -59,7 +58,7 @@ describe('styling context', () => {
sources: [null]
});
registerBinding(context, 0, 'width', '100px');
registerBinding(context, 1, 'width', '100px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
@ -68,7 +67,7 @@ describe('styling context', () => {
sources: ['100px']
});
registerBinding(context, 0, 'width', '200px');
registerBinding(context, 1, 'width', '200px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
@ -85,7 +84,7 @@ function makeContextWithDebug() {
}
function buildGuardMask(...bindingIndices: number[]) {
let mask = 0;
let mask = DEFAULT_GUARD_MASK_VALUE;
for (let i = 0; i < bindingIndices.length; i++) {
mask |= 1 << bindingIndices[i];
}

View File

@ -24,7 +24,6 @@ describe('styling debugging tools', () => {
prop: 'width',
value: null,
bindingIndex: null,
sourceValues: [{value: null, bindingIndex: null}],
},
});
@ -34,9 +33,6 @@ describe('styling debugging tools', () => {
prop: 'width',
value: '100px',
bindingIndex: null,
sourceValues: [
{bindingIndex: null, value: '100px'},
],
},
});
@ -49,10 +45,6 @@ describe('styling debugging tools', () => {
prop: 'width',
value: '200px',
bindingIndex: someBindingIndex1,
sourceValues: [
{bindingIndex: someBindingIndex1, value: '200px'},
{bindingIndex: null, value: '100px'},
],
},
});
@ -65,11 +57,6 @@ describe('styling debugging tools', () => {
prop: 'width',
value: '200px',
bindingIndex: someBindingIndex1,
sourceValues: [
{bindingIndex: someBindingIndex1, value: '200px'},
{bindingIndex: someBindingIndex2, value: '500px'},
{bindingIndex: null, value: '100px'},
],
},
});
});