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