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:
parent
deb77bd3df
commit
dc6406e5e8
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,35 +200,56 @@ 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;
|
||||
let found = false;
|
||||
while (i < context.length) {
|
||||
const valuesCount = getValuesCount(context, i);
|
||||
const p = getProp(context, i);
|
||||
found = prop <= p;
|
||||
if (found) {
|
||||
// all style/class bindings are sorted by property name
|
||||
if (prop < p) {
|
||||
allocateNewContextEntry(context, i, prop);
|
||||
// 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);
|
||||
found = prop <= p;
|
||||
if (found) {
|
||||
// all style/class bindings are sorted by property name
|
||||
if (prop < p) {
|
||||
allocateNewContextEntry(context, i, prop);
|
||||
}
|
||||
addBindingIntoContext(context, false, i, bindingValue, countId);
|
||||
break;
|
||||
}
|
||||
addBindingIntoContext(context, i, bindingValue, countId);
|
||||
break;
|
||||
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
|
||||
}
|
||||
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
allocateNewContextEntry(context, context.length, prop);
|
||||
addBindingIntoContext(context, i, bindingValue, countId);
|
||||
if (!found) {
|
||||
allocateNewContextEntry(context, context.length, prop);
|
||||
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,51 +265,65 @@ 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;
|
||||
classesBitMask = 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;
|
||||
stylesBitMask = 0;
|
||||
if (stylesBitMask) {
|
||||
applyStyling(context, renderer, element, data, stylesBitMask, setStyle);
|
||||
stylesBitMask = 0;
|
||||
}
|
||||
currentStyleIndex = STYLING_INDEX_START_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,55 +331,114 @@ export function applyStyles(
|
|||
* 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;
|
||||
while (i < context.length) {
|
||||
const valuesCount = getValuesCount(context, i);
|
||||
const guardMask = getGuardMask(context, i);
|
||||
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;
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
applyStylingFn(renderer, element, prop, valueToApply, bindingIndex);
|
||||
break;
|
||||
}
|
||||
let i = getPropValuesStartPosition(context);
|
||||
while (i < context.length) {
|
||||
const valuesCount = getValuesCount(context, i);
|
||||
const guardMask = getGuardMask(context, i);
|
||||
if (bitMask & guardMask) {
|
||||
let valueApplied = false;
|
||||
const prop = getProp(context, i);
|
||||
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;
|
||||
}
|
||||
}
|
||||
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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 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 !== '';
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,23 +116,27 @@ 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 prop = getProp(context, i);
|
||||
const guardMask = getGuardMask(context, i);
|
||||
const valuesCount = getValuesCount(context, i);
|
||||
const defaultValue = getDefaultValue(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 defaultValue = getDefaultValue(context, i);
|
||||
const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
for (let j = 0; j < valuesCount; j++) {
|
||||
sources.push(context[bindingsStartPosition + j] as number | string | null);
|
||||
entries[prop] = {prop, guardMask, valuesCount, defaultValue, sources};
|
||||
}
|
||||
|
||||
entries[prop] = {prop, guardMask, valuesCount, defaultValue, sources};
|
||||
|
||||
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
|
||||
}
|
||||
return entries;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 !== '';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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]);
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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'},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue