refactor(ivy): remame styling_next directory to styling (#32731)

PR Close #32731
This commit is contained in:
Matias Niemelä
2019-09-17 13:26:52 -07:00
committed by Andrew Kushnir
parent 0618bed83e
commit f88f717094
26 changed files with 33 additions and 33 deletions

View File

@ -0,0 +1,808 @@
/**
* @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 {SafeValue, unwrapSafeValue} from '../../sanitization/bypass';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {NO_CHANGE} from '../tokens';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
import {getStylingState, resetStylingState} from './state';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, getBindingValue, getConfig, getDefaultValue, getGuardMask, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from './util';
/**
* --------
*
* This file contains the core logic for styling in Angular.
*
* All styling bindings (i.e. `[style]`, `[style.prop]`, `[class]` and `[class.name]`)
* will have their values be applied through the logic in this file.
*
* When a binding is encountered (e.g. `<div [style.width]="w">`) then
* the binding data will be populated into a `TStylingContext` data-structure.
* There is only one `TStylingContext` per `TNode` and each element instance
* will update its style/class binding values in concert with the styling
* context.
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
/**
* The guard/update mask bit index location for map-based bindings.
*
* All map-based bindings (i.e. `[style]` and `[class]` )
*/
const STYLING_INDEX_FOR_MAP_BINDING = 0;
/**
* Visits a class-based binding and updates the new value (if changed).
*
* This function is called each time a class-based styling instruction
* 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 it's called (which then allows the `TStylingContext`
* and the bit mask values to be in sync).
*/
export function updateClassViaContext(
context: TStylingContext, data: LStylingData, element: RElement, directiveIndex: number,
prop: string | null, bindingIndex: number,
value: boolean | string | null | undefined | StylingMapArray | NO_CHANGE,
forceUpdate?: boolean): boolean {
const isMapBased = !prop;
const state = getStylingState(element, directiveIndex);
const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++;
if (value !== NO_CHANGE) {
const updated = updateBindingData(
context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate,
false);
if (updated || forceUpdate) {
// We flip the bit in the bitMask to reflect that the binding
// at the `index` slot has changed. This identifies to the flushing
// phase that the bindings for this particular CSS class need to be
// applied again because on or more of the bindings for the CSS
// class have changed.
state.classesBitMask |= 1 << countIndex;
return true;
}
}
return false;
}
/**
* Visits a style-based binding and updates the new value (if changed).
*
* This function is called each time a style-based styling instruction
* 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 it's called (which then allows the `TStylingContext`
* and the bit mask values to be in sync).
*/
export function updateStyleViaContext(
context: TStylingContext, data: LStylingData, element: RElement, directiveIndex: number,
prop: string | null, bindingIndex: number,
value: string | number | SafeValue | null | undefined | StylingMapArray | NO_CHANGE,
sanitizer: StyleSanitizeFn | null, forceUpdate?: boolean): boolean {
const isMapBased = !prop;
const state = getStylingState(element, directiveIndex);
const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++;
if (value !== NO_CHANGE) {
const sanitizationRequired = isMapBased ?
true :
(sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false);
const updated = updateBindingData(
context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate,
sanitizationRequired);
if (updated || forceUpdate) {
// We flip the bit in the bitMask to reflect that the binding
// at the `index` slot has changed. This identifies to the flushing
// phase that the bindings for this particular property need to be
// applied again because on or more of the bindings for the CSS
// property have changed.
state.stylesBitMask |= 1 << countIndex;
return true;
}
}
return false;
}
/**
* 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.
*
* 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: LStylingData, counterIndex: number, sourceIndex: number,
prop: string | null, bindingIndex: number,
value: string | SafeValue | number | boolean | null | undefined | StylingMapArray,
forceUpdate?: boolean, sanitizationRequired?: boolean): boolean {
const hostBindingsMode = isHostStylingActive(sourceIndex);
if (!isContextLocked(context, hostBindingsMode)) {
// this will only happen during the first update pass of the
// context. The reason why we can't use `tNode.firstTemplatePass`
// here is because its not guaranteed to be true when the first
// update pass is executed (remember that all styling instructions
// are run in the update phase, and, as a result, are no more
// styling instructions that are run in the creation phase).
registerBinding(context, counterIndex, sourceIndex, prop, bindingIndex, sanitizationRequired);
patchConfig(
context,
hostBindingsMode ? TStylingConfig.HasHostBindings : TStylingConfig.HasTemplateBindings);
patchConfig(context, prop ? TStylingConfig.HasPropBindings : TStylingConfig.HasMapBindings);
}
const changed = forceUpdate || hasValueChanged(data[bindingIndex], value);
if (changed) {
setValue(data, bindingIndex, value);
const doSetValuesAsStale = (getConfig(context) & TStylingConfig.HasHostBindings) &&
!hostBindingsMode && (prop ? !value : true);
if (doSetValuesAsStale) {
renderHostBindingsAsStale(context, data, prop, !prop);
}
}
return changed;
}
/**
* Iterates over all host-binding values for the given `prop` value in the context and sets their
* corresponding binding values to `null`.
*
* Whenever a template binding changes its value to `null`, all host-binding values should be
* re-applied
* to the element when the host bindings are evaluated. This may not always happen in the event
* that none of the bindings changed within the host bindings code. For this reason this function
* is expected to be called each time a template binding becomes falsy or when a map-based template
* binding changes.
*/
function renderHostBindingsAsStale(
context: TStylingContext, data: LStylingData, prop: string | null, isMapBased: boolean): void {
const valuesCount = getValuesCount(context);
if (hasConfig(context, TStylingConfig.HasPropBindings)) {
const itemsPerRow = TStylingContextIndex.BindingsStartOffset + valuesCount;
let i = TStylingContextIndex.ValuesStartPosition;
while (i < context.length) {
if (getProp(context, i) === prop) {
break;
}
i += itemsPerRow;
}
const bindingsStart = i + TStylingContextIndex.BindingsStartOffset;
const valuesStart = bindingsStart + 1; // the first column is template bindings
const valuesEnd = bindingsStart + valuesCount - 1;
for (let i = valuesStart; i < valuesEnd; i++) {
const bindingIndex = context[i] as number;
if (bindingIndex !== 0) {
setValue(data, bindingIndex, null);
}
}
}
if (hasConfig(context, TStylingConfig.HasMapBindings)) {
const bindingsStart =
TStylingContextIndex.ValuesStartPosition + TStylingContextIndex.BindingsStartOffset;
const valuesStart = bindingsStart + 1; // the first column is template bindings
const valuesEnd = bindingsStart + valuesCount - 1;
for (let i = valuesStart; i < valuesEnd; i++) {
const stylingMap = getValue<StylingMapArray>(data, context[i] as number);
if (stylingMap) {
setMapAsDirty(stylingMap);
}
}
}
}
/**
* Registers the provided binding (prop + bindingIndex) into the context.
*
* It is needed because it will either update or insert a styling property
* into the context at the correct spot.
*
* When called, one of two things will happen:
*
* 1) If the property already exists in the context then it will just add
* the provided `bindingValue` to the end of the binding sources region
* for that particular property.
*
* - If the binding value is a number then it will be added as a new
* binding index source next to the other binding sources for the property.
*
* - Otherwise, if the binding value is a string/boolean/null type then it will
* replace the default value for the property if the default value is `null`.
*
* 2) If the property does not exist then it will be inserted into the context.
* The styling context relies on all properties being stored in alphabetical
* order, so it knows exactly where to store it.
*
* When inserted, a default `null` value is created for the property which exists
* 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, their property name value is set as `[MAP]`.
*/
export function registerBinding(
context: TStylingContext, countId: number, sourceIndex: number, prop: string | null,
bindingValue: number | null | string | boolean, sanitizationRequired?: boolean): void {
let found = false;
prop = prop || MAP_BASED_ENTRY_PROP_NAME;
let totalSources = getTotalSources(context);
// if a new source is detected then a new column needs to be allocated into
// the styling context. The column is basically a new allocation of binding
// sources that will be available to each property.
while (totalSources <= sourceIndex) {
addNewSourceColumn(context);
totalSources++;
}
const isBindingIndexValue = typeof bindingValue === 'number';
const entriesPerRow = TStylingContextIndex.BindingsStartOffset + getValuesCount(context);
let i = TStylingContextIndex.ValuesStartPosition;
// all style/class bindings are sorted by property name
while (i < context.length) {
const p = getProp(context, i);
if (prop <= p) {
if (prop < p) {
allocateNewContextEntry(context, i, prop, sanitizationRequired);
} else if (isBindingIndexValue) {
patchConfig(context, TStylingConfig.HasCollisions);
}
addBindingIntoContext(context, i, bindingValue, countId, sourceIndex);
found = true;
break;
}
i += entriesPerRow;
}
if (!found) {
allocateNewContextEntry(context, context.length, prop, sanitizationRequired);
addBindingIntoContext(context, i, bindingValue, countId, sourceIndex);
}
}
/**
* Inserts a new row into the provided `TStylingContext` and assigns the provided `prop` value as
* the property entry.
*/
function allocateNewContextEntry(
context: TStylingContext, index: number, prop: string, sanitizationRequired?: boolean): void {
const config = sanitizationRequired ? TStylingContextPropConfigFlags.SanitizationRequired :
TStylingContextPropConfigFlags.Default;
context.splice(
index, 0,
config, // 1) config value
DEFAULT_GUARD_MASK_VALUE, // 2) template bit mask
DEFAULT_GUARD_MASK_VALUE, // 3) host bindings bit mask
prop, // 4) prop value (e.g. `width`, `myClass`, etc...)
);
index += 4; // the 4 values above
// 5...) default binding index for the template value
// depending on how many sources already exist in the context,
// multiple default index entries may need to be inserted for
// the new value in the context.
const totalBindingsPerEntry = getTotalSources(context);
for (let i = 0; i < totalBindingsPerEntry; i++) {
context.splice(index, 0, DEFAULT_BINDING_INDEX);
index++;
}
// 6) default binding value for the new entry
context.splice(index, 0, DEFAULT_BINDING_VALUE);
}
/**
* Inserts a new binding value into a styling property tuple in the `TStylingContext`.
*
* A bindingValue is inserted into a context during the first update pass
* of a template or host bindings function. When this occurs, two things
* happen:
*
* - If the bindingValue value is a number then it is treated as a bindingIndex
* value (a index in the `LView`) and it will be inserted next to the other
* binding index entries.
*
* - Otherwise the binding value will update the default value for the property
* and this will only happen if the default value is `null`.
*/
function addBindingIntoContext(
context: TStylingContext, index: number, bindingValue: number | string | boolean | null,
bitIndex: number, sourceIndex: number) {
if (typeof bindingValue === 'number') {
const hostBindingsMode = isHostStylingActive(sourceIndex);
const cellIndex = index + TStylingContextIndex.BindingsStartOffset + sourceIndex;
context[cellIndex] = bindingValue;
const updatedBitMask = getGuardMask(context, index, hostBindingsMode) | (1 << bitIndex);
setGuardMask(context, index, updatedBitMask, hostBindingsMode);
} else if (bindingValue !== null && getDefaultValue(context, index) === null) {
setDefaultValue(context, index, bindingValue);
}
}
/**
* Registers a new column into the provided `TStylingContext`.
*
* If and when a new source is detected then a new column needs to
* be allocated into the styling context. The column is basically
* a new allocation of binding sources that will be available to each
* property.
*
* Each column that exists in the styling context resembles a styling
* source. A styling source an either be the template or one or more
* components or directives all containing styling host bindings.
*/
function addNewSourceColumn(context: TStylingContext): void {
// we use -1 here because we want to insert right before the last value (the default value)
const insertOffset = TStylingContextIndex.BindingsStartOffset + getValuesCount(context) - 1;
let index = TStylingContextIndex.ValuesStartPosition;
while (index < context.length) {
index += insertOffset;
context.splice(index++, 0, DEFAULT_BINDING_INDEX);
// the value was inserted just before the default value, but the
// next entry in the context starts just after it. Therefore++.
index++;
}
context[TStylingContextIndex.TotalSourcesPosition]++;
}
/**
* Applies all pending style and class bindings to the provided element.
*
* This function will attempt to flush styling via the provided `classesContext`
* and `stylesContext` context values. This function is designed to be run from
* the internal `stylingApply` function (which is scheduled to run at the very
* end of change detection for an element if one or more style/class bindings
* were processed) and will rely on any state values that are set from when
* any of the styling bindings executed.
*
* This function is designed to be called twice: one when change detection has
* processed an element within the template bindings (i.e. just as `advance()`
* is called) and when host bindings have been processed. In both cases the
* styles and classes in both contexts will be applied to the element, but the
* algorithm will selectively decide which bindings to run depending on the
* columns in the context. The provided `directiveIndex` value will help the
* algorithm determine which bindings to apply: either the template bindings or
* the host bindings (see `applyStylingToElement` for more information).
*
* Note that once this function is called all temporary styling state data
* (i.e. the `bitMask` and `counter` values for styles and classes will be cleared).
*/
export function flushStyling(
renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData,
classesContext: TStylingContext | null, stylesContext: TStylingContext | null,
element: RElement, directiveIndex: number, styleSanitizer: StyleSanitizeFn | null): void {
ngDevMode && ngDevMode.flushStyling++;
const state = getStylingState(element, directiveIndex);
const hostBindingsMode = isHostStylingActive(state.sourceIndex);
if (stylesContext) {
if (!isContextLocked(stylesContext, hostBindingsMode)) {
lockAndFinalizeContext(stylesContext, hostBindingsMode);
}
if (state.stylesBitMask !== 0) {
applyStylingViaContext(
stylesContext, renderer, element, data, state.stylesBitMask, setStyle, styleSanitizer,
hostBindingsMode);
}
}
if (classesContext) {
if (!isContextLocked(classesContext, hostBindingsMode)) {
lockAndFinalizeContext(classesContext, hostBindingsMode);
}
if (state.classesBitMask !== 0) {
applyStylingViaContext(
classesContext, renderer, element, data, state.classesBitMask, setClass, null,
hostBindingsMode);
}
}
resetStylingState();
}
/**
* Locks the context (so no more bindings can be added) and also copies over initial class/style
* values into their binding areas.
*
* There are two main actions that take place in this function:
*
* - Locking the context:
* Locking the context is required so that the style/class instructions know NOT to
* register a binding again after the first update pass has run. If a locking bit was
* not used then it would need to scan over the context each time an instruction is run
* (which is expensive).
*
* - Patching initial values:
* Directives and component host bindings may include static class/style values which are
* bound to the host element. When this happens, the styling context will need to be informed
* so it can use these static styling values as defaults when a matching binding is falsy.
* These initial styling values are read from the initial styling values slot within the
* provided `TStylingContext` (which is an instance of a `StylingMapArray`). This inner map will
* be updated each time a host binding applies its static styling values (via `elementHostAttrs`)
* so these values are only read at this point because this is the very last point before the
* first style/class values are flushed to the element.
*
* Note that the `TStylingContext` styling context contains two locks: one for template bindings
* and another for host bindings. Either one of these locks will be set when styling is applied
* during the template binding flush and/or during the host bindings flush.
*/
function lockAndFinalizeContext(context: TStylingContext, hostBindingsMode: boolean): void {
const initialValues = getStylingMapArray(context) !;
updateInitialStylingOnContext(context, initialValues);
lockContext(context, hostBindingsMode);
}
/**
* Registers all initial styling entries into the provided context.
*
* This function will iterate over all entries in the provided `initialStyling` ar}ray and register
* them as default (initial) values in the provided context. Initial styling values in a context are
* the default values that are to be applied unless overwritten by a binding.
*
* The reason why this function exists and isn't a part of the context construction is because
* host binding is evaluated at a later stage after the element is created. This means that
* if a directive or component contains any initial styling code (i.e. `<div class="foo">`)
* then that initial styling data can only be applied once the styling for that element
* is first applied (at the end of the update phase). Once that happens then the context will
* update itself with the complete initial styling for the element.
*/
function updateInitialStylingOnContext(
context: TStylingContext, initialStyling: StylingMapArray): void {
// `-1` is used here because all initial styling data is not a apart
// of a binding (since it's static)
const COUNT_ID_FOR_STYLING = -1;
let hasInitialStyling = false;
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < initialStyling.length;
i += StylingMapArrayIndex.TupleSize) {
const value = getMapValue(initialStyling, i);
if (value) {
const prop = getMapProp(initialStyling, i);
registerBinding(context, COUNT_ID_FOR_STYLING, 0, prop, value, false);
hasInitialStyling = true;
}
}
if (hasInitialStyling) {
patchConfig(context, TStylingConfig.HasInitialStyling);
}
}
/**
* 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 a part 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/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
* the `flushStyling` function so that it can call this function for both
* the styles and classes contexts).
*/
export function applyStylingViaContext(
context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
bindingData: LStylingData, bitMaskValue: number | boolean, applyStylingFn: ApplyStylingFn,
sanitizer: StyleSanitizeFn | null, hostBindingsMode: boolean): void {
const bitMask = normalizeBitMaskValue(bitMaskValue);
let stylingMapsSyncFn: SyncStylingMapsFn|null = null;
let applyAllValues = false;
if (hasConfig(context, TStylingConfig.HasMapBindings)) {
stylingMapsSyncFn = getStylingMapsSyncFn();
const mapsGuardMask =
getGuardMask(context, TStylingContextIndex.ValuesStartPosition, hostBindingsMode);
applyAllValues = (bitMask & mapsGuardMask) !== 0;
}
const valuesCount = getValuesCount(context);
let totalBindingsToVisit = 1;
let mapsMode =
applyAllValues ? StylingMapsSyncMode.ApplyAllValues : StylingMapsSyncMode.TraverseValues;
if (hostBindingsMode) {
mapsMode |= StylingMapsSyncMode.RecurseInnerMaps;
totalBindingsToVisit = valuesCount - 1;
}
let i = getPropValuesStartPosition(context);
while (i < context.length) {
const guardMask = getGuardMask(context, i, hostBindingsMode);
if (bitMask & guardMask) {
let valueApplied = false;
const prop = getProp(context, i);
const defaultValue = getDefaultValue(context, i);
// Part 1: Visit the `[styling.prop]` value
for (let j = 0; j < totalBindingsToVisit; j++) {
const bindingIndex = getBindingValue(context, i, j) as number;
if (!valueApplied && bindingIndex !== 0) {
const value = getValue(bindingData, bindingIndex);
if (isStylingValueDefined(value)) {
const checkValueOnly = hostBindingsMode && j === 0;
if (!checkValueOnly) {
const finalValue = sanitizer && isSanitizationRequired(context, i) ?
sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) :
unwrapSafeValue(value);
applyStylingFn(renderer, element, prop, finalValue, bindingIndex);
}
valueApplied = true;
}
}
// Part 2: Visit the `[style]` or `[class]` map-based value
if (stylingMapsSyncFn) {
// determine whether or not to apply the target property or to skip it
let mode = mapsMode | (valueApplied ? StylingMapsSyncMode.SkipTargetProp :
StylingMapsSyncMode.ApplyTargetProp);
if (hostBindingsMode && j === 0) {
mode |= StylingMapsSyncMode.CheckValuesOnly;
}
const valueAppliedWithinMap = stylingMapsSyncFn(
context, renderer, element, bindingData, j, applyStylingFn, sanitizer, mode, prop,
defaultValue);
valueApplied = valueApplied || valueAppliedWithinMap;
}
}
// Part 3: apply the default value (e.g. `<div style="width:200">` => `200px` gets applied)
// 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) {
if (hostBindingsMode) {
mapsMode |= StylingMapsSyncMode.CheckValuesOnly;
}
stylingMapsSyncFn(
context, renderer, element, bindingData, 0, applyStylingFn, sanitizer, mapsMode);
}
}
/**
* Applies the provided styling map to the element directly (without context resolution).
*
* This function is designed to be run from the styling instructions and will be called
* automatically. This function is intended to be used for performance reasons in the
* event that there is no need to apply styling via context resolution.
*
* See `allowDirectStylingApply`.
*
* @returns whether or not the styling map was applied to the element.
*/
export function applyStylingMapDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, map: StylingMapArray, applyFn: ApplyStylingFn,
sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean): boolean {
if (forceUpdate || hasValueChanged(data[bindingIndex], map)) {
setValue(data, bindingIndex, map);
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i);
applyStylingValue(renderer, context, element, prop, value, applyFn, bindingIndex, sanitizer);
}
return true;
}
return false;
}
/**
* Applies the provided styling prop/value to the element directly (without context resolution).
*
* This function is designed to be run from the styling instructions and will be called
* automatically. This function is intended to be used for performance reasons in the
* event that there is no need to apply styling via context resolution.
*
* See `allowDirectStylingApply`.
*
* @returns whether or not the prop/value styling was applied to the element.
*/
export function applyStylingValueDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, prop: string, value: any, applyFn: ApplyStylingFn,
sanitizer?: StyleSanitizeFn | null): boolean {
if (hasValueChanged(data[bindingIndex], value)) {
setValue(data, bindingIndex, value);
applyStylingValue(renderer, context, element, prop, value, applyFn, bindingIndex, sanitizer);
return true;
}
return false;
}
function applyStylingValue(
renderer: any, context: TStylingContext, element: RElement, prop: string, value: any,
applyFn: ApplyStylingFn, bindingIndex: number, sanitizer?: StyleSanitizeFn | null) {
let valueToApply: string|null = unwrapSafeValue(value);
if (isStylingValueDefined(valueToApply)) {
valueToApply =
sanitizer ? sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) : valueToApply;
} else if (hasConfig(context, TStylingConfig.HasInitialStyling)) {
const initialStyles = getStylingMapArray(context);
if (initialStyles) {
valueToApply = findInitialStylingValue(initialStyles, prop);
}
}
applyFn(renderer, element, prop, valueToApply, bindingIndex);
}
function findInitialStylingValue(map: StylingMapArray, prop: string): string|null {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const p = getMapProp(map, i);
if (p >= prop) {
return p === prop ? getMapValue(map, i) : null;
}
}
return null;
}
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;
}
/**
* Assigns a style value to a style property for the given element.
*/
export const setStyle: ApplyStylingFn =
(renderer: Renderer3 | null, native: RElement, prop: string, value: string | null) => {
if (renderer !== null) {
if (value) {
// opacity, z-index and flexbox all have number values
// and these need to be converted into strings so that
// they can be assigned properly.
value = value.toString();
ngDevMode && ngDevMode.rendererSetStyle++;
if (isProceduralRenderer(renderer)) {
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase);
} else {
// The reason why native style may be `null` is either because
// it's a container element or it's a part of a test
// environment that doesn't have styling. In either
// case it's safe not to apply styling to the element.
const nativeStyle = native.style;
if (nativeStyle != null) {
nativeStyle.setProperty(prop, value);
}
}
} else {
ngDevMode && ngDevMode.rendererRemoveStyle++;
if (isProceduralRenderer(renderer)) {
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase);
} else {
const nativeStyle = native.style;
if (nativeStyle != null) {
nativeStyle.removeProperty(prop);
}
}
}
}
};
/**
* Adds/removes the provided className value to the provided element.
*/
export const setClass: ApplyStylingFn =
(renderer: Renderer3 | null, native: RElement, className: string, value: any) => {
if (renderer !== null && className !== '') {
if (value) {
ngDevMode && ngDevMode.rendererAddClass++;
if (isProceduralRenderer(renderer)) {
renderer.addClass(native, className);
} else {
// the reason why classList may be `null` is either because
// it's a container element or it's a part of a test
// environment that doesn't have styling. In either
// case it's safe not to apply styling to the element.
const classList = native.classList;
if (classList != null) {
classList.add(className);
}
}
} else {
ngDevMode && ngDevMode.rendererRemoveClass++;
if (isProceduralRenderer(renderer)) {
renderer.removeClass(native, className);
} else {
const classList = native.classList;
if (classList != null) {
classList.remove(className);
}
}
}
}
};
/**
* Iterates over all provided styling entries and renders them on the element.
*
* This function is used alongside a `StylingMapArray` entry. This entry is not
* the same as the `TStylingContext` and is only really used when an element contains
* initial styling values (e.g. `<div style="width:200px">`), but no style/class bindings
* are present. If and when that happens then this function will be called to render all
* initial styling values on an element.
*/
export function renderStylingMap(
renderer: Renderer3, element: RElement, stylingValues: TStylingContext | StylingMapArray | null,
isClassBased: boolean): void {
const stylingMapArr = getStylingMapArray(stylingValues);
if (stylingMapArr) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < stylingMapArr.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(stylingMapArr, i);
const value = getMapValue(stylingMapArr, i);
if (isClassBased) {
setClass(renderer, element, prop, value, null);
} else {
setStyle(renderer, element, prop, value, null);
}
}
}
}

View File

@ -0,0 +1,541 @@
/**
* @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 {SafeValue} from '../../sanitization/bypass';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {setInputsForProperty} from '../instructions/shared';
import {AttributeMarker, TAttributes, TNode, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {BINDING_INDEX, LView, RENDERER} from '../interfaces/view';
import {getActiveDirectiveId, getCurrentStyleSanitizer, getLView, getSelectedIndex, setCurrentStyleSanitizer, setElementExitFn} from '../state';
import {NO_CHANGE} from '../tokens';
import {renderStringify} from '../util/misc_utils';
import {getNativeByTNode, getTNode} from '../util/view_utils';
import {applyStylingMapDirectly, applyStylingValueDirectly, flushStyling, setClass, setStyle, updateClassViaContext, updateStyleViaContext} from './bindings';
import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from './interfaces';
import {activateStylingMapFeature} from './map_based_bindings';
import {attachStylingDebugObject} from './styling_debug';
import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isHostStylingActive, isStylingContext, normalizeIntoStylingMap, setValue, stylingMapToString} from './util';
/**
* --------
*
* This file contains the core logic for how styling instructions are processed in Angular.
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
/**
* Sets the current style sanitizer function which will then be used
* within all follow-up prop and map-based style binding instructions
* for the given element.
*
* Note that once styling has been applied to the element (i.e. once
* `advance(n)` is executed or the hostBindings/template function exits)
* then the active `sanitizerFn` will be set to `null`. This means that
* once styling is applied to another element then a another call to
* `styleSanitizer` will need to be made.
*
* @param sanitizerFn The sanitization function that will be used to
* process style prop/value entries.
*
* @codeGenApi
*/
export function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void {
setCurrentStyleSanitizer(sanitizer);
}
/**
* Update a style binding on an element with the provided value.
*
* If the style value is falsy then it will be removed from the element
* (or assigned a different value depending if there are any styles placed
* on the element with `styleMap` or any static styles that are
* present from when the element was created with `styling`).
*
* Note that the styling element is updated as part of `stylingApply`.
*
* @param prop A valid CSS property.
* @param value New value to write (`null` or an empty string to remove).
* @param suffix Optional suffix. Used with scalar values to add unit such as `px`.
* Note that when a suffix is provided then the underlying sanitizer will
* be ignored.
*
* Note that this will apply the provided style value to the host element if this function is called
* within a host binding function.
*
* @codeGenApi
*/
export function ɵɵstyleProp(
prop: string, value: string | number | SafeValue | null, suffix?: string | null): void {
stylePropInternal(getSelectedIndex(), prop, value, suffix);
}
/**
* Internal function for applying a single style to an element.
*
* The reason why this function has been separated from `ɵɵstyleProp` is because
* it is also called from `ɵɵstylePropInterpolate`.
*/
export function stylePropInternal(
elementIndex: number, prop: string, value: string | number | SafeValue | null,
suffix?: string | null | undefined): void {
const lView = getLView();
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
const updated =
stylingProp(elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false);
if (ngDevMode) {
ngDevMode.styleProp++;
if (updated) {
ngDevMode.stylePropCacheMiss++;
}
}
}
/**
* Update a class binding on an element with the provided value.
*
* This instruction is meant to handle the `[class.foo]="exp"` case and,
* therefore, the class binding itself must already be allocated using
* `styling` within the creation block.
*
* @param prop A valid CSS class (only one).
* @param value A true/false value which will turn the class on or off.
*
* Note that this will apply the provided class value to the host element if this function
* is called within a host binding function.
*
* @codeGenApi
*/
export function ɵɵclassProp(className: string, value: boolean | null): void {
const lView = getLView();
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
const updated = stylingProp(getSelectedIndex(), bindingIndex, className, value, true);
if (ngDevMode) {
ngDevMode.classProp++;
if (updated) {
ngDevMode.classPropCacheMiss++;
}
}
}
/**
* Shared function used to update a prop-based styling binding for an element.
*
* Depending on the state of the `tNode.styles` styles context, the style/prop
* value may be applied directly to the element instead of being processed
* through the context. The reason why this occurs is for performance and fully
* depends on the state of the context (i.e. whether or not there are duplicate
* bindings or whether or not there are map-based bindings and property bindings
* present together).
*/
function stylingProp(
elementIndex: number, bindingIndex: number, prop: string,
value: boolean | number | SafeValue | string | null | undefined | NO_CHANGE,
isClassBased: boolean): boolean {
let updated = false;
const lView = getLView();
const tNode = getTNode(elementIndex, lView);
const native = getNativeByTNode(tNode, lView) as RElement;
const hostBindingsMode = isHostStyling();
const context = isClassBased ? getClassesContext(tNode) : getStylesContext(tNode);
const sanitizer = isClassBased ? null : getCurrentStyleSanitizer();
// Direct Apply Case: bypass context resolution and apply the
// style/class value directly to the element
if (allowDirectStyling(context, hostBindingsMode)) {
const renderer = getRenderer(tNode, lView);
updated = applyStylingValueDirectly(
renderer, context, native, lView, bindingIndex, prop, value,
isClassBased ? setClass : setStyle, sanitizer);
} else {
// Context Resolution (or first update) Case: save the value
// and defer to the context to flush and apply the style/class binding
// value to the element.
const directiveIndex = getActiveDirectiveId();
if (isClassBased) {
updated = updateClassViaContext(
context, lView, native, directiveIndex, prop, bindingIndex,
value as string | boolean | null);
} else {
updated = updateStyleViaContext(
context, lView, native, directiveIndex, prop, bindingIndex,
value as string | SafeValue | null, sanitizer);
}
setElementExitFn(stylingApply);
}
return updated;
}
/**
* Update style bindings using an object literal on an element.
*
* This instruction is meant to apply styling via the `[style]="exp"` template bindings.
* When styles are applied to the element they will then be updated with respect to
* any styles/classes set via `styleProp`. If any styles are set to falsy
* then they will be removed from the element.
*
* Note that the styling instruction will not be applied until `stylingApply` is called.
*
* @param styles A key/value style map of the styles that will be applied to the given element.
* Any missing styles (that have already been applied to the element beforehand) will be
* removed (unset) from the element's styling.
*
* Note that this will apply the provided styleMap value to the host element if this function
* is called within a host binding.
*
* @codeGenApi
*/
export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | null): void {
const index = getSelectedIndex();
const lView = getLView();
const tNode = getTNode(index, lView);
const context = getStylesContext(tNode);
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
// inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function)
if (!isHostStyling() && hasStyleInput(tNode) && styles !== NO_CHANGE) {
updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false);
styles = NO_CHANGE;
}
const updated = _stylingMap(index, context, bindingIndex, styles, false);
if (ngDevMode) {
ngDevMode.styleMap++;
if (updated) {
ngDevMode.styleMapCacheMiss++;
}
}
}
/**
* Update class bindings using an object literal or class-string on an element.
*
* This instruction is meant to apply styling via the `[class]="exp"` template bindings.
* When classes are applied to the element they will then be updated with
* respect to any styles/classes set via `classProp`. If any
* classes are set to falsy then they will be removed from the element.
*
* Note that the styling instruction will not be applied until `stylingApply` is called.
* Note that this will the provided classMap value to the host element if this function is called
* within a host binding.
*
* @param classes A key/value map or string of CSS classes that will be added to the
* given element. Any missing classes (that have already been applied to the element
* beforehand) will be removed (unset) from the element's list of CSS classes.
*
* @codeGenApi
*/
export function ɵɵclassMap(classes: {[className: string]: any} | NO_CHANGE | string | null): void {
classMapInternal(getSelectedIndex(), classes);
}
/**
* Internal function for applying a class string or key/value map of classes to an element.
*
* The reason why this function has been separated from `ɵɵclassMap` is because
* it is also called from `ɵɵclassMapInterpolate`.
*/
export function classMapInternal(
elementIndex: number, classes: {[className: string]: any} | NO_CHANGE | string | null): void {
const lView = getLView();
const tNode = getTNode(elementIndex, lView);
const context = getClassesContext(tNode);
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
// inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function)
if (!isHostStyling() && hasClassInput(tNode) && classes !== NO_CHANGE) {
updateDirectiveInputValue(context, lView, tNode, bindingIndex, classes, true);
classes = NO_CHANGE;
}
const updated = _stylingMap(elementIndex, context, bindingIndex, classes, true);
if (ngDevMode) {
ngDevMode.classMap++;
if (updated) {
ngDevMode.classMapCacheMiss++;
}
}
}
/**
* 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(
elementIndex: number, context: TStylingContext, bindingIndex: number,
value: {[key: string]: any} | string | null, isClassBased: boolean): boolean {
let updated = false;
const lView = getLView();
const directiveIndex = getActiveDirectiveId();
const tNode = getTNode(elementIndex, lView);
const native = getNativeByTNode(tNode, lView) as RElement;
const oldValue = lView[bindingIndex] as StylingMapArray | null;
const hostBindingsMode = isHostStyling();
const sanitizer = getCurrentStyleSanitizer();
const valueHasChanged = hasValueChanged(oldValue, value);
const stylingMapArr =
value === NO_CHANGE ? NO_CHANGE : normalizeIntoStylingMap(oldValue, value, !isClassBased);
// Direct Apply Case: bypass context resolution and apply the
// style/class map values directly to the element
if (allowDirectStyling(context, hostBindingsMode)) {
const renderer = getRenderer(tNode, lView);
updated = applyStylingMapDirectly(
renderer, context, native, lView, bindingIndex, stylingMapArr as StylingMapArray,
isClassBased ? setClass : setStyle, sanitizer, valueHasChanged);
} else {
updated = valueHasChanged;
activateStylingMapFeature();
// Context Resolution (or first update) Case: save the map value
// and defer to the context to flush and apply the style/class binding
// value to the element.
if (isClassBased) {
updateClassViaContext(
context, lView, native, directiveIndex, null, bindingIndex, stylingMapArr,
valueHasChanged);
} else {
updateStyleViaContext(
context, lView, native, directiveIndex, null, bindingIndex, stylingMapArr, sanitizer,
valueHasChanged);
}
setElementExitFn(stylingApply);
}
return updated;
}
/**
* Writes a value to a directive's `style` or `class` input binding (if it has changed).
*
* If a directive has a `@Input` binding that is set on `style` or `class` then that value
* will take priority over the underlying style/class styling bindings. This value will
* be updated for the binding each time during change detection.
*
* When this occurs this function will attempt to write the value to the input binding
* depending on the following situations:
*
* - If `oldValue !== newValue`
* - If `newValue` is `null` (but this is skipped if it is during the first update pass--
* which is when the context is not locked yet)
*/
function updateDirectiveInputValue(
context: TStylingContext, lView: LView, tNode: TNode, bindingIndex: number, newValue: any,
isClassBased: boolean): void {
const oldValue = lView[bindingIndex];
if (oldValue !== newValue) {
// even if the value has changed we may not want to emit it to the
// directive input(s) in the event that it is falsy during the
// first update pass.
if (newValue || isContextLocked(context, false)) {
const inputName = isClassBased ? 'class' : 'style';
const inputs = tNode.inputs ![inputName] !;
const initialValue = getInitialStylingValue(context);
const value = normalizeStylingDirectiveInputValue(initialValue, newValue, isClassBased);
setInputsForProperty(lView, inputs, value);
setElementExitFn(stylingApply);
}
setValue(lView, bindingIndex, newValue);
}
}
/**
* Returns the appropriate directive input value for `style` or `class`.
*
* Earlier versions of Angular expect a binding value to be passed into directive code
* exactly as it is unless there is a static value present (in which case both values
* will be stringified and concatenated).
*/
function normalizeStylingDirectiveInputValue(
initialValue: string, bindingValue: string | {[key: string]: any} | null,
isClassBased: boolean) {
let value = bindingValue;
// we only concat values if there is an initial value, otherwise we return the value as is.
// Note that this is to satisfy backwards-compatibility in Angular.
if (initialValue.length) {
if (isClassBased) {
value = concatString(initialValue, forceClassesAsString(bindingValue));
} else {
value = concatString(
initialValue, forceStylesAsString(bindingValue as{[key: string]: any} | null | undefined),
';');
}
}
return value;
}
/**
* Flushes all styling code to the element.
*
* This function is designed to be scheduled from any of the four styling instructions
* in this file. When called it will flush all style and class bindings to the element
* via the context resolution algorithm.
*/
function stylingApply(): void {
const lView = getLView();
const elementIndex = getSelectedIndex();
const tNode = getTNode(elementIndex, lView);
const native = getNativeByTNode(tNode, lView) as RElement;
const directiveIndex = getActiveDirectiveId();
const renderer = getRenderer(tNode, lView);
const sanitizer = getCurrentStyleSanitizer();
const classesContext = isStylingContext(tNode.classes) ? tNode.classes as TStylingContext : null;
const stylesContext = isStylingContext(tNode.styles) ? tNode.styles as TStylingContext : null;
flushStyling(renderer, lView, classesContext, stylesContext, native, directiveIndex, sanitizer);
setCurrentStyleSanitizer(null);
}
function getRenderer(tNode: TNode, lView: LView) {
return tNode.type === TNodeType.Element ? lView[RENDERER] : null;
}
/**
* Searches and assigns provided all static style/class entries (found in the `attrs` value)
* and registers them in their respective styling contexts.
*/
export function registerInitialStylingOnTNode(
tNode: TNode, attrs: TAttributes, startIndex: number): boolean {
let hasAdditionalInitialStyling = false;
let styles = getStylingMapArray(tNode.styles);
let classes = getStylingMapArray(tNode.classes);
let mode = -1;
for (let i = startIndex; i < attrs.length; i++) {
const attr = attrs[i] as string;
if (typeof attr == 'number') {
mode = attr;
} else if (mode == AttributeMarker.Classes) {
classes = classes || allocStylingMapArray();
addItemToStylingMap(classes, attr, true);
hasAdditionalInitialStyling = true;
} else if (mode == AttributeMarker.Styles) {
const value = attrs[++i] as string | null;
styles = styles || allocStylingMapArray();
addItemToStylingMap(styles, attr, value);
hasAdditionalInitialStyling = true;
}
}
if (classes && classes.length > StylingMapArrayIndex.ValuesStartPosition) {
if (!tNode.classes) {
tNode.classes = classes;
}
updateRawValueOnContext(tNode.classes, stylingMapToString(classes, true));
}
if (styles && styles.length > StylingMapArrayIndex.ValuesStartPosition) {
if (!tNode.styles) {
tNode.styles = styles;
}
updateRawValueOnContext(tNode.styles, stylingMapToString(styles, false));
}
return hasAdditionalInitialStyling;
}
function updateRawValueOnContext(context: TStylingContext | StylingMapArray, value: string) {
const stylingMapArr = getStylingMapArray(context) !;
stylingMapArr[StylingMapArrayIndex.RawValuePosition] = value;
}
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): TStylingContext {
let context = isClassBased ? tNode.classes : tNode.styles;
if (!isStylingContext(context)) {
context = allocTStylingContext(context as StylingMapArray | null);
if (ngDevMode) {
attachStylingDebugObject(context as TStylingContext);
}
if (isClassBased) {
tNode.classes = context;
} else {
tNode.styles = context;
}
}
return context as TStylingContext;
}
function resolveStylePropValue(
value: string | number | SafeValue | null | NO_CHANGE,
suffix: string | null | undefined): string|SafeValue|null|undefined|NO_CHANGE {
if (value === NO_CHANGE) return value;
let resolvedValue: string|null = null;
if (value !== null) {
if (suffix) {
// when a suffix is applied then it will bypass
// sanitization entirely (b/c a new string is created)
resolvedValue = renderStringify(value) + suffix;
} else {
// sanitization happens by dealing with a string value
// this means that the string value will be passed through
// into the style rendering later (which is where the value
// will be sanitized before it is applied)
resolvedValue = value as any as string;
}
}
return resolvedValue;
}
/**
* Whether or not the style/class binding being applied was executed within a host bindings
* function.
*/
function isHostStyling(): boolean {
return isHostStylingActive(getActiveDirectiveId());
}

View File

@ -0,0 +1,580 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {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`.
*
* The `TStylingContext` unites all template styling bindings (i.e.
* `[class]` and `[style]` bindings) as well as all host-level
* styling bindings (for components and directives) together into
* a single manifest
*
* The styling context is stored on a `TNode` on and there are
* two instances of it: one for classes and another for styles.
*
* ```typescript
* tNode.styles = [ ... a context only for styles ... ];
* tNode.classes = [ ... a context only for classes ... ];
* ```
*
* The styling context is created each time there are one or more
* styling bindings (style or class bindings) present for an element,
* but is only created once per `TNode`.
*
* `tNode.styles` and `tNode.classes` can be an instance of the following:
*
* ```typescript
* tNode.styles = null; // no static styling or styling bindings active
* tNode.styles = StylingMapArray; // only static values present (e.g. `<div style="width:200">`)
* tNode.styles = TStylingContext; // one or more styling bindings present (e.g. `<div
* [style.width]>`)
* ```
*
* Both `tNode.styles` and `tNode.classes` are instantiated when anything
* styling-related is active on an element. They are first created from
* from the any of the element-level instructions (e.g. `element`,
* `elementStart`, `elementHostAttrs`). When any static style/class
* values are encountered they are registered on the `tNode.styles`
* and `tNode.classes` data-structures. By default (when any static
* values are encountered) the `tNode.styles` or `tNode.classes` values
* are instances of a `StylingMapArray`. Only when style/class bindings
* are detected then that styling map is converted into an instance of
* `TStylingContext`.
*
* Due to the fact the the `TStylingContext` is stored on a `TNode`
* this means that all data within the context is static. Instead of
* storing actual styling binding values, the lView binding index values
* are stored within the context. (static nature means it is more compact.)
*
* The code below shows a breakdown of two instances of `TStylingContext`
* (one for `tNode.styles` and another for `tNode.classes`):
*
* ```typescript
* // <div [class.active]="c" // lView binding index = 20
* // [style.width]="x" // lView binding index = 21
* // [style.height]="y"> // lView binding index = 22
* // ...
* // </div>
* tNode.styles = [
* 0, // the context config value (see `TStylingContextConfig`)
* 1, // the total amount of sources present (only `1` b/c there are only template
* bindings)
* [null], // initial values array (an instance of `StylingMapArray`)
*
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b010, // template guard mask for height
* 0, // host bindings guard mask for height
* 'height', // the property name
* 22, // the binding location for the "y" binding in the lView
* null, // the default value for height
*
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b001, // template guard mask for width
* 0, // host bindings guard mask for width
* 'width', // the property name
* 21, // the binding location for the "x" binding in the lView
* null, // the default value for width
* ];
*
* tNode.classes = [
* 0, // the context config value (see `TStylingContextConfig`)
* 1, // the total amount of sources present (only `1` b/c there are only template
* bindings)
* [null], // initial values array (an instance of `StylingMapArray`)
*
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b001, // template guard mask for width
* 0, // host bindings guard mask for width
* 'active', // the property name
* 20, // the binding location for the "c" binding in the lView
* null, // the default value for the `active` class
* ];
* ```
*
* Entry value present in an entry (called a tuple) within the
* styling context is as follows:
*
* ```typescript
* context = [
* //...
* configValue,
* templateGuardMask,
* hostBindingsGuardMask,
* propName,
* ...bindingIndices...,
* defaultValue
* //...
* ];
* ```
*
* Below is a breakdown of each value:
*
* - **configValue**:
* Property-specific configuration values. The only config setting
* that is implemented right now is whether or not to sanitize the
* value.
*
* - **templateGuardMask**:
* A numeric value where each bit represents a binding index
* location. Each binding index location is assigned based on
* a local counter value that increments each time an instruction
* is called:
*
* ```
* <div [style.width]="x" // binding index = 21 (counter index = 0)
* [style.height]="y"> // binding index = 22 (counter index = 1)
* ```
*
* In the example code above, if the `width` value where to change
* then the first bit in the local bit mask value would be flipped
* (and the second bit for when `height`).
*
* If and when there are more than 32 binding sources in the context
* (more than 32 `[style/class]` bindings) then the bit masking will
* overflow and we are left with a situation where a `-1` value will
* represent the bit mask. Due to the way that JavaScript handles
* negative values, when the bit mask is `-1` then all bits within
* that value will be automatically flipped (this is a quick and
* efficient way to flip all bits on the mask when a special kind
* of caching scenario occurs or when there are more than 32 bindings).
*
* - **hostBindingsGuardMask**:
* Another instance of a guard mask that is specific to host bindings.
* This behaves exactly the same way as does the `templateGuardMask`,
* but will not contain any binding information processed in the template.
* The reason why there are two instances of guard masks (one for the
* template and another for host bindings) is because the template bindings
* are processed before host bindings and the state information is not
* carried over into the host bindings code. As soon as host bindings are
* processed for an element the counter and state-based bit mask values are
* set to `0`.
*
* ```
* <div [style.width]="x" // binding index = 21 (counter index = 0)
* [style.height]="y" // binding index = 22 (counter index = 1)
* dir-that-sets-width // binding index = 30 (counter index = 0)
* dir-that-sets-width> // binding index = 31 (counter index = 1)
* ```
*
* - **propName**:
* The CSS property name or class name (e.g `width` or `active`).
*
* - **bindingIndices...**:
* A series of numeric binding values that reflect where in the
* lView to find the style/class values associated with the property.
* Each value is in order in terms of priority (templates are first,
* then directives and then components). When the context is flushed
* and the style/class values are applied to the element (this happens
* inside of the `stylingApply` instruction) then the flushing code
* will keep checking each binding index against the associated lView
* to find the first style/class value that is non-null.
*
* - **defaultValue**:
* This is the default that will always be applied to the element if
* and when all other binding sources return a result that is null.
* Usually this value is `null` but it can also be a static value that
* is intercepted when the tNode is first constructured (e.g.
* `<div style="width:200px">` has a default value of `200px` for
* the `width` property).
*
* Each time a new binding is encountered it is registered into the
* context. The context then is continually updated until the first
* styling apply call has been called (which is automatically scheduled
* to be called once an element exits during change detection). Note that
* each entry in the context is stored in alphabetical order.
*
* Once styling has been flushed for the first time for an element the
* context will set as locked (this prevents bindings from being added
* to the context later on).
*
* # How Styles/Classes are Rendered
* Each time a styling instruction (e.g. `[class.name]`, `[style.prop]`,
* etc...) is executed, the associated `lView` for the view is updated
* at the current binding location. Also, when this happens, a local
* counter value is incremented. If the binding value has changed then
* a local `bitMask` variable is updated with the specific bit based
* on the counter value.
*
* Below is a lightweight example of what happens when a single style
* property is updated (i.e. `<div [style.prop]="val">`):
*
* ```typescript
* function updateStyleProp(prop: string, value: string) {
* const lView = getLView();
* const bindingIndex = BINDING_INDEX++;
*
* // update the local counter value
* const indexForStyle = stylingState.stylesCount++;
* if (lView[bindingIndex] !== value) {
* lView[bindingIndex] = value;
*
* // tell the local state that we have updated a style value
* // by updating the bit mask
* stylingState.bitMaskForStyles |= 1 << indexForStyle;
* }
* }
* ```
*
* Once all the bindings have updated a `bitMask` value will be populated.
* This `bitMask` value is used in the apply algorithm (which is called
* context resolution).
*
* ## The Apply Algorithm (Context Resolution)
* As explained above, each time a binding updates its value, the resulting
* value is stored in the `lView` array. These styling values have yet to
* be flushed to the element.
*
* Once all the styling instructions have been evaluated, then the styling
* context(s) are flushed to the element. When this happens, the context will
* be iterated over (property by property) and each binding source will be
* examined and the first non-null value will be applied to the element.
*
* Let's say that we the following template code:
*
* ```html
* <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/styling/map_based_bindings.ts` file.)
*
* ## Sanitization
* Sanitization is used to prevent invalid style values from being applied to
* the element.
*
* It is enabled in two cases:
*
* 1. The `styleSanitizer(sanitizerFn)` instruction was called (just before any other
* styling instructions are run).
*
* 2. The component/directive `LView` instance has a sanitizer object attached to it
* (this happens when `renderComponent` is executed with a `sanitizer` value or
* if the ngModule contains a sanitizer provider attached to it).
*
* If and when sanitization is active then all property/value entries will be evaluated
* through the active sanitizer before they are applied to the element (or the styling
* debug handler).
*
* If a `Sanitizer` object is used (via the `LView[SANITIZER]` value) then that object
* will be used for every property.
*
* If a `StyleSanitizerFn` function is used (via the `styleSanitizer`) then it will be
* called in two ways:
*
* 1. property validation mode: this will be called early to mark whether a property
* should be sanitized or not at during the flushing stage.
*
* 2. value sanitization mode: this will be called during the flushing stage and will
* run the sanitizer function against the value before applying it to the element.
*
* If sanitization returns an empty value then that empty value will be applied
* to the element.
*/
export interface TStylingContext extends
Array<number|string|number|boolean|null|StylingMapArray|{}> {
/** Configuration data for the context */
[TStylingContextIndex.ConfigPosition]: TStylingConfig;
/** The total amount of sources present in the context */
[TStylingContextIndex.TotalSourcesPosition]: number;
/** Initial value position for static styles */
[TStylingContextIndex.InitialStylingValuePosition]: StylingMapArray;
}
/**
* A series of flags used to configure the config value present within an instance of
* `TStylingContext`.
*/
export const enum TStylingConfig {
/**
* The initial state of the styling context config.
*/
Initial = 0b0000000,
/**
* Whether or not there are prop-based bindings present.
*
* Examples include:
* 1. `<div [style.prop]="x">`
* 2. `<div [class.prop]="x">`
* 3. `@HostBinding('style.prop') x`
* 4. `@HostBinding('class.prop') x`
*/
HasPropBindings = 0b0000001,
/**
* Whether or not there are map-based bindings present.
*
* Examples include:
* 1. `<div [style]="x">`
* 2. `<div [class]="x">`
* 3. `@HostBinding('style') x`
* 4. `@HostBinding('class') x`
*/
HasMapBindings = 0b0000010,
/**
* Whether or not there are map-based and prop-based bindings present.
*
* Examples include:
* 1. `<div [style]="x" [style.prop]="y">`
* 2. `<div [class]="x" [style.prop]="y">`
* 3. `<div [style]="x" dir-that-sets-some-prop>`
* 4. `<div [class]="x" dir-that-sets-some-class>`
*/
HasPropAndMapBindings = 0b0000011,
/**
* Whether or not there are two or more sources for a single property in the context.
*
* Examples include:
* 1. prop + prop: `<div [style.width]="x" dir-that-sets-width>`
* 2. map + prop: `<div [style]="x" [style.prop]>`
* 3. map + map: `<div [style]="x" dir-that-sets-style>`
*/
HasCollisions = 0b0000100,
/**
* Whether or not the context contains initial styling values.
*
* Examples include:
* 1. `<div style="width:200px">`
* 2. `<div class="one two three">`
* 3. `@Directive({ host: { 'style': 'width:200px' } })`
* 4. `@Directive({ host: { 'class': 'one two three' } })`
*/
HasInitialStyling = 0b00001000,
/**
* Whether or not the context contains one or more template bindings.
*
* Examples include:
* 1. `<div [style]="x">`
* 2. `<div [style.width]="x">`
* 3. `<div [class]="x">`
* 4. `<div [class.name]="x">`
*/
HasTemplateBindings = 0b00010000,
/**
* Whether or not the context contains one or more host bindings.
*
* Examples include:
* 1. `@HostBinding('style') x`
* 2. `@HostBinding('style.width') x`
* 3. `@HostBinding('class') x`
* 4. `@HostBinding('class.name') x`
*/
HasHostBindings = 0b00100000,
/**
* Whether or not the template bindings are allowed to be registered in the context.
*
* This flag is after one or more template-based style/class bindings were
* set and processed for an element. Once the bindings are processed then a call
* to stylingApply is issued and the lock will be put into place.
*
* Note that this is only set once.
*/
TemplateBindingsLocked = 0b01000000,
/**
* Whether or not the host bindings are allowed to be registered in the context.
*
* This flag is after one or more host-based style/class bindings were
* set and processed for an element. Once the bindings are processed then a call
* to stylingApply is issued and the lock will be put into place.
*
* Note that this is only set once.
*/
HostBindingsLocked = 0b10000000,
/** A Mask of all the configurations */
Mask = 0b11111111,
/** Total amount of configuration bits used */
TotalBits = 8,
}
/**
* An index of position and offset values used to navigate the `TStylingContext`.
*/
export const enum TStylingContextIndex {
ConfigPosition = 0,
TotalSourcesPosition = 1,
InitialStylingValuePosition = 2,
ValuesStartPosition = 3,
// each tuple entry in the context
// (config, templateBitGuard, hostBindingBitGuard, prop, ...bindings||default-value)
ConfigOffset = 0,
TemplateBitGuardOffset = 1,
HostBindingsBitGuardOffset = 2,
PropOffset = 3,
BindingsStartOffset = 4
}
/**
* A series of flags used for each property entry within the `TStylingContext`.
*/
export const enum TStylingContextPropConfigFlags {
Default = 0b0,
SanitizationRequired = 0b1,
TotalBits = 1,
Mask = 0b1,
}
/**
* A function used to apply or remove styling from an element for a given property.
*/
export interface ApplyStylingFn {
(renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string, value: any,
bindingIndex?: number|null): void;
}
/**
* Runtime data type that is used to store binding data referenced from the `TStylingContext`.
*
* Because `LView` is just an array with data, there is no reason to
* special case `LView` everywhere in the styling algorithm. By allowing
* this data type to be an array that contains various scalar data types,
* an instance of `LView` doesn't need to be constructed for tests.
*/
export type LStylingData = LView | (string | number | boolean | null)[];
/**
* Array-based representation of a key/value array.
*
* The format of the array is "property", "value", "property2",
* "value2", etc...
*
* The first value in the array is reserved to store the instance
* of the key/value array that was used to populate the property/
* value entries that take place in the remainder of the array.
*/
export interface StylingMapArray extends Array<{}|string|number|null> {
[StylingMapArrayIndex.RawValuePosition]: {}|string|null;
}
/**
* An index of position and offset points for any data stored within a `StylingMapArray` instance.
*/
export const enum StylingMapArrayIndex {
/** Where the values start in the array */
ValuesStartPosition = 1,
/** The location of the raw key/value map instance used last to populate the array entries */
RawValuePosition = 0,
/** The size of each property/value entry */
TupleSize = 2,
/** The offset for the property entry in the tuple */
PropOffset = 0,
/** The offset for the value entry in the tuple */
ValueOffset = 1,
}
/**
* Used to apply/traverse across all map-based styling entries up to the provided `targetProp`
* value.
*
* When called, each of the map-based `StylingMapArray` entries (which are stored in
* the provided `LStylingData` array) will be iterated over. Depending on the provided
* `mode` value, each prop/value entry may be applied or skipped over.
*
* If `targetProp` value is provided the iteration code will stop once it reaches
* the property (if found). Otherwise if the target property is not encountered then
* it will stop once it reaches the next value that appears alphabetically after it.
*
* If a `defaultValue` is provided then it will be applied to the element only if the
* `targetProp` property value is encountered and the value associated with the target
* property is `null`. The reason why the `defaultValue` is needed is to avoid having the
* algorithm apply a `null` value and then apply a default value afterwards (this would
* end up being two style property writes).
*
* @returns whether or not the target property was reached and its value was
* applied to the element.
*/
export interface SyncStylingMapsFn {
(context: TStylingContext, renderer: Renderer3|ProceduralRenderer3|null, element: RElement,
data: LStylingData, sourceIndex: number, applyStylingFn: ApplyStylingFn,
sanitizer: StyleSanitizeFn|null, mode: StylingMapsSyncMode, targetProp?: string|null,
defaultValue?: boolean|string|null): boolean;
}
/**
* Used to direct how map-based values are applied/traversed when styling is flushed.
*/
export const enum StylingMapsSyncMode {
/** Only traverse values (no prop/value styling entries get applied) */
TraverseValues = 0b000,
/** Apply every prop/value styling entry to the element */
ApplyAllValues = 0b001,
/** Only apply the target prop/value entry */
ApplyTargetProp = 0b010,
/** Skip applying the target prop/value entry */
SkipTargetProp = 0b100,
/** Iterate over inner maps map values in the context */
RecurseInnerMaps = 0b1000,
/** Only check to see if a value was set somewhere in each map */
CheckValuesOnly = 0b10000,
}

View File

@ -0,0 +1,351 @@
/**
* @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 {unwrapSafeValue} from '../../sanitization/bypass';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer';
import {setStylingMapsSyncFn} from './bindings';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from './interfaces';
import {getBindingValue, getMapProp, getMapValue, getValue, getValuesCount, isStylingValueDefined} from './util';
/**
* --------
*
* This file contains the algorithm logic for applying map-based bindings
* such as `[style]` and `[class]`.
*
* --------
*/
/**
* Enables support for map-based styling bindings (e.g. `[style]` and `[class]` bindings).
*/
export function activateStylingMapFeature() {
setStylingMapsSyncFn(syncStylingMap);
}
/**
* 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 `StylingMapArray` array
* is produced. The `StylingMapArray` 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 `advance(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, sourceIndex: number, applyStylingFn: ApplyStylingFn,
sanitizer: StyleSanitizeFn | null, mode: StylingMapsSyncMode, targetProp?: string | null,
defaultValue?: string | boolean | 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);
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) === 0) {
runTheSyncAlgorithm = false;
targetPropValueWasApplied = true;
}
if (runTheSyncAlgorithm) {
targetPropValueWasApplied = innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp || null,
sourceIndex, 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, sanitizer: StyleSanitizeFn | null,
mode: StylingMapsSyncMode, targetProp: string | null, currentMapIndex: number,
defaultValue: string | boolean | null): boolean {
const totalMaps = getValuesCount(context) - 1; // maps have no default value
const mapsLimit = totalMaps - 1;
const recurseInnerMaps =
currentMapIndex < mapsLimit && (mode & StylingMapsSyncMode.RecurseInnerMaps) !== 0;
const checkValuesOnly = (mode & StylingMapsSyncMode.CheckValuesOnly) !== 0;
if (checkValuesOnly) {
// inner modes do not check values ever (that can only happen
// when sourceIndex === 0)
mode &= ~StylingMapsSyncMode.CheckValuesOnly;
}
let targetPropValueWasApplied = false;
if (currentMapIndex <= mapsLimit) {
let cursor = getCurrentSyncCursor(currentMapIndex);
const bindingIndex = getBindingValue(
context, TStylingContextIndex.ValuesStartPosition, currentMapIndex) as number;
const stylingMapArr = getValue<StylingMapArray>(data, bindingIndex);
if (stylingMapArr) {
while (cursor < stylingMapArr.length) {
const prop = getMapProp(stylingMapArr, cursor);
const iteratedTooFar = targetProp && prop > targetProp;
const isTargetPropMatched = !iteratedTooFar && prop === targetProp;
const value = getMapValue(stylingMapArr, 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 = recurseInnerMaps ?
innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, sanitizer, innerMode, innerProp,
currentMapIndex + 1, defaultValue) :
false;
if (iteratedTooFar) {
if (!targetPropValueWasApplied) {
targetPropValueWasApplied = valueApplied;
}
break;
}
if (!valueApplied && isValueAllowedToBeApplied(mode, isTargetPropMatched)) {
valueApplied = true;
if (!checkValuesOnly) {
const useDefault = isTargetPropMatched && !valueIsDefined;
const bindingIndexToApply = isTargetPropMatched ? bindingIndex : null;
let finalValue: any;
if (useDefault) {
finalValue = defaultValue;
} else {
finalValue = sanitizer ?
sanitizer(prop, value, StyleSanitizeMode.ValidateAndSanitize) :
(value ? unwrapSafeValue(value) : null);
}
applyStylingFn(renderer, element, prop, finalValue, bindingIndexToApply);
}
}
targetPropValueWasApplied = valueApplied && isTargetPropMatched;
cursor += StylingMapArrayIndex.TupleSize;
}
setCurrentSyncCursor(currentMapIndex, cursor);
// this is a fallback case in the event that the styling map is `null` for this
// binding but there are other map-based bindings that need to be evaluated
// afterwards. If the `prop` value is falsy then the intention is to cycle
// through all of the properties in the remaining maps as well. If the current
// styling map is too short then there are no values to iterate over. In either
// case the follow-up maps need to be iterated over.
if (recurseInnerMaps &&
(stylingMapArr.length === StylingMapArrayIndex.ValuesStartPosition || !targetProp)) {
targetPropValueWasApplied = innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp,
currentMapIndex + 1, defaultValue);
}
} else if (recurseInnerMaps) {
targetPropValueWasApplied = innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp,
currentMapIndex + 1, defaultValue);
}
}
return targetPropValueWasApplied;
}
/**
* 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:
*
* - value is being applied:
* if the value is being applied from this current styling
* map then there is no need to apply it in a deeper map.
*
* - value is being not applied:
* apply the value if it is found in a deeper map.
*
* When these reasons are encountered the flags will for the
* inner map mode will be configured.
*/
function resolveInnerMapMode(
currentMode: number, valueIsDefined: boolean, isExactMatch: boolean): number {
let innerMode = currentMode;
if (!valueIsDefined && !(currentMode & StylingMapsSyncMode.SkipTargetProp) &&
(isExactMatch || (currentMode & StylingMapsSyncMode.ApplyAllValues))) {
// 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: StylingMapsSyncMode, 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] = StylingMapArrayIndex.ValuesStartPosition;
}
}
/**
* Returns an active cursor value at a given mapIndex location.
*/
function getCurrentSyncCursor(mapIndex: number) {
if (mapIndex >= MAP_CURSORS.length) {
MAP_CURSORS.push(StylingMapArrayIndex.ValuesStartPosition);
}
return MAP_CURSORS[mapIndex];
}
/**
* Sets a cursor value at a given mapIndex location.
*/
function setCurrentSyncCursor(mapIndex: number, indexValue: number) {
MAP_CURSORS[mapIndex] = indexValue;
}

View File

@ -0,0 +1,114 @@
/**
* @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 {RElement} from '../interfaces/renderer';
import {TEMPLATE_DIRECTIVE_INDEX} from './util';
/**
* --------
*
* This file contains all state-based logic for styling in Angular.
*
* Styling in Angular is evaluated with a series of styling-specific
* template instructions which are called one after another each time
* change detection occurs in Angular.
*
* Styling makes use of various temporary, state-based variables between
* instructions so that it can better cache and optimize its values.
* These values are usually populated and cleared when an element is
* exited in change detection (once all the instructions are run for
* that element).
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
/**
* Used as a state reference for update values between style/class binding instructions.
*
* In addition to storing the element and bit-mask related values, the state also
* stores the `sourceIndex` value. The `sourceIndex` value is an incremented value
* that identifies what "source" (i.e. the template, a specific directive by index or
* component) is currently applying its styling bindings to the element.
*/
export interface StylingState {
/** The element that is currently being processed */
element: RElement|null;
/** The directive index that is currently active (`0` === template) */
directiveIndex: number;
/** The source (column) index that is currently active (`0` === template) */
sourceIndex: number;
/** The classes update bit mask value that is processed during each class binding */
classesBitMask: number;
/** The classes update bit index value that is processed during each class binding */
classesIndex: number;
/** The styles update bit mask value that is processed during each style binding */
stylesBitMask: number;
/** The styles update bit index value that is processed during each style binding */
stylesIndex: number;
}
// these values will get filled in the very first time this is accessed...
const _state: StylingState = {
element: null,
directiveIndex: -1,
sourceIndex: -1,
classesBitMask: -1,
classesIndex: -1,
stylesBitMask: -1,
stylesIndex: -1,
};
const BIT_MASK_START_VALUE = 0;
// the `0` start value is reserved for [map]-based entries
const INDEX_START_VALUE = 1;
/**
* Returns (or instantiates) the styling state for the given element.
*
* Styling state is accessed and processed each time a style or class binding
* is evaluated.
*
* If and when the provided `element` doesn't match the current element in the
* state then this means that styling was recently cleared or the element has
* changed in change detection. In both cases the styling state is fully reset.
*
* If and when the provided `directiveIndex` doesn't match the current directive
* index in the state then this means that a new source has introduced itself into
* the styling code (or, in other words, another directive or component has started
* to apply its styling host bindings to the element).
*/
export function getStylingState(element: RElement, directiveIndex: number): StylingState {
if (_state.element !== element) {
_state.element = element;
_state.directiveIndex = directiveIndex;
_state.sourceIndex = directiveIndex === TEMPLATE_DIRECTIVE_INDEX ? 0 : 1;
_state.classesBitMask = BIT_MASK_START_VALUE;
_state.classesIndex = INDEX_START_VALUE;
_state.stylesBitMask = BIT_MASK_START_VALUE;
_state.stylesIndex = INDEX_START_VALUE;
} else if (_state.directiveIndex !== directiveIndex) {
_state.directiveIndex = directiveIndex;
_state.sourceIndex++;
}
return _state;
}
/**
* Clears the styling state so that it can be used by another element's styling code.
*/
export function resetStylingState() {
_state.element = null;
}

View File

@ -0,0 +1,274 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {RElement} from '../interfaces/renderer';
import {getCurrentStyleSanitizer} from '../state';
import {attachDebugObject} from '../util/debug_utils';
import {applyStylingViaContext} from './bindings';
import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from './interfaces';
import {activateStylingMapFeature} from './map_based_bindings';
import {allowDirectStyling as _allowDirectStyling, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isMapBased, isSanitizationRequired} from './util';
/**
* --------
*
* This file contains the core debug functionality for styling in Angular.
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
/**
* A debug/testing-oriented summary of a styling entry.
*
* A value such as this is generated as an artifact of the `DebugStyling`
* summary.
*/
export interface LStylingSummary {
/** The style/class property that the summary is attached to */
prop: string;
/** The last applied value for the style/class property */
value: string|boolean|null;
/** The binding index of the last applied style/class property */
bindingIndex: number|null;
}
/**
* A debug/testing-oriented summary of all styling entries for a `DebugNode` instance.
*/
export interface DebugStyling {
/** The associated TStylingContext instance */
context: TStylingContext;
/** Which configuration flags are active (see `TStylingContextConfig`) */
config: {
hasMapBindings: boolean; //
hasPropBindings: boolean; //
hasCollisions: boolean; //
hasTemplateBindings: boolean; //
hasHostBindings: boolean; //
templateBindingsLocked: boolean; //
hostBindingsLocked: boolean; //
allowDirectStyling: boolean; //
};
/**
* A summarization of each style/class property
* present in the context
*/
summary: {[propertyName: string]: LStylingSummary};
/**
* A key/value map of all styling properties and their
* runtime values
*/
values: {[propertyName: string]: string | number | null | boolean};
/**
* Overrides the sanitizer used to process styles
*/
overrideSanitizer(sanitizer: StyleSanitizeFn|null): void;
}
/**
* A debug/testing-oriented summary of all styling entries within a `TStylingContext`.
*/
export interface TStylingTupleSummary {
/** The property (style or class property) that this tuple represents */
prop: string;
/** The total amount of styling entries a part of this tuple */
valuesCount: number;
/**
* The bit guard mask that is used to compare and protect against
* styling changes when any template style/class bindings update
*/
templateBitMask: number;
/**
* The bit guard mask that is used to compare and protect against
* styling changes when any host style/class bindings update
*/
hostBindingsBitMask: number;
/**
* Whether or not the entry requires sanitization
*/
sanitizationRequired: boolean;
/**
* The default value that will be applied if any bindings are falsy
*/
defaultValue: string|boolean|null;
/**
* All bindingIndex sources that have been registered for this style
*/
sources: (number|null|string)[];
}
/**
* Instantiates and attaches an instance of `TStylingContextDebug` to the provided context
*/
export function attachStylingDebugObject(context: TStylingContext) {
const debug = new TStylingContextDebug(context);
attachDebugObject(context, debug);
return debug;
}
/**
* A human-readable debug summary of the styling data present within `TStylingContext`.
*
* This class is designed to be used within testing code or when an
* application has `ngDevMode` activated.
*/
class TStylingContextDebug {
constructor(public readonly context: TStylingContext) {}
get isTemplateLocked() { return isContextLocked(this.context, true); }
get isHostBindingsLocked() { return isContextLocked(this.context, false); }
/**
* Returns a detailed summary of each styling entry in the context.
*
* See `TStylingTupleSummary`.
*/
get entries(): {[prop: string]: TStylingTupleSummary} {
const context = this.context;
const totalColumns = getValuesCount(context);
const entries: {[prop: string]: TStylingTupleSummary} = {};
const start = getPropValuesStartPosition(context);
let i = start;
while (i < context.length) {
const prop = getProp(context, i);
const templateBitMask = getGuardMask(context, i, false);
const hostBindingsBitMask = getGuardMask(context, i, true);
const defaultValue = getDefaultValue(context, i);
const sanitizationRequired = isSanitizationRequired(context, i);
const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset;
const sources: (number | string | null)[] = [];
for (let j = 0; j < totalColumns; j++) {
const bindingIndex = context[bindingsStartPosition + j] as number | string | null;
if (bindingIndex !== 0) {
sources.push(bindingIndex);
}
}
entries[prop] = {
prop,
templateBitMask,
hostBindingsBitMask,
sanitizationRequired,
valuesCount: sources.length, defaultValue, sources,
};
i += TStylingContextIndex.BindingsStartOffset + totalColumns;
}
return entries;
}
}
/**
* A human-readable debug summary of the styling data present for a `DebugNode` instance.
*
* This class is designed to be used within testing code or when an
* application has `ngDevMode` activated.
*/
export class NodeStylingDebug implements DebugStyling {
private _sanitizer: StyleSanitizeFn|null = null;
constructor(
public context: TStylingContext, private _data: LStylingData,
private _isClassBased?: boolean) {}
/**
* Overrides the sanitizer used to process styles.
*/
overrideSanitizer(sanitizer: StyleSanitizeFn|null) { this._sanitizer = sanitizer; }
/**
* Returns a detailed summary of each styling entry in the context and
* what their runtime representation is.
*
* See `LStylingSummary`.
*/
get summary(): {[key: string]: LStylingSummary} {
const entries: {[key: string]: LStylingSummary} = {};
this._mapValues((prop: string, value: any, bindingIndex: number | null) => {
entries[prop] = {prop, value, bindingIndex};
});
return entries;
}
get config() {
const hasMapBindings = hasConfig(this.context, TStylingConfig.HasMapBindings);
const hasPropBindings = hasConfig(this.context, TStylingConfig.HasPropBindings);
const hasCollisions = hasConfig(this.context, TStylingConfig.HasCollisions);
const hasTemplateBindings = hasConfig(this.context, TStylingConfig.HasTemplateBindings);
const hasHostBindings = hasConfig(this.context, TStylingConfig.HasHostBindings);
const templateBindingsLocked = hasConfig(this.context, TStylingConfig.TemplateBindingsLocked);
const hostBindingsLocked = hasConfig(this.context, TStylingConfig.HostBindingsLocked);
const allowDirectStyling =
_allowDirectStyling(this.context, false) || _allowDirectStyling(this.context, true);
return {
hasMapBindings, //
hasPropBindings, //
hasCollisions, //
hasTemplateBindings, //
hasHostBindings, //
templateBindingsLocked, //
hostBindingsLocked, //
allowDirectStyling, //
};
}
/**
* Returns a key/value map of all the styles/classes that were last applied to the element.
*/
get values(): {[key: string]: any} {
const entries: {[key: string]: any} = {};
this._mapValues((prop: string, value: any) => { entries[prop] = value; });
return entries;
}
private _mapValues(fn: (prop: string, value: string|null, 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 = hasConfig(this.context, TStylingConfig.HasMapBindings);
if (hasMaps) {
activateStylingMapFeature();
}
const mapFn: ApplyStylingFn =
(renderer: any, element: RElement, prop: string, value: string | null,
bindingIndex?: number | null) => fn(prop, value, bindingIndex || null);
const sanitizer = this._isClassBased ? null : (this._sanitizer || getCurrentStyleSanitizer());
// run the template bindings
applyStylingViaContext(
this.context, null, mockElement, this._data, true, mapFn, sanitizer, false);
// and also the host bindings
applyStylingViaContext(
this.context, null, mockElement, this._data, true, mapFn, sanitizer, true);
}
}

View File

@ -0,0 +1,422 @@
/**
* @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 {unwrapSafeValue} from '../../sanitization/bypass';
import {TNode, TNodeFlags} from '../interfaces/node';
import {NO_CHANGE} from '../tokens';
import {LStylingData, StylingMapArray, StylingMapArrayIndex, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
export const MAP_BASED_ENTRY_PROP_NAME = '[MAP]';
export const TEMPLATE_DIRECTIVE_INDEX = 0;
/**
* Default fallback value for a styling binding.
*
* A value of `null` is used here which signals to the styling algorithm that
* the styling value is not present. This way if there are no other values
* detected then it will be removed once the style/class property is dirty and
* diffed within the styling algorithm present in `flushStyling`.
*/
export const DEFAULT_BINDING_VALUE = null;
export const DEFAULT_BINDING_INDEX = 0;
const DEFAULT_TOTAL_SOURCES = 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;
/**
* Creates a new instance of the `TStylingContext`.
*
* The `TStylingContext` is used as a manifest of all style or all class bindings on
* an element. Because it is a T-level data-structure, it is only created once per
* tNode for styles and for classes. This function allocates a new instance of a
* `TStylingContext` with the initial values (see `interfaces.ts` for more info).
*/
export function allocTStylingContext(initialStyling?: StylingMapArray | null): TStylingContext {
initialStyling = initialStyling || allocStylingMapArray();
return [
TStylingConfig.Initial, // 1) config for the styling context
DEFAULT_TOTAL_SOURCES, // 2) total amount of styling sources (template, directives, etc...)
initialStyling, // 3) initial styling values
];
}
export function allocStylingMapArray(): StylingMapArray {
return [''];
}
export function getConfig(context: TStylingContext) {
return context[TStylingContextIndex.ConfigPosition];
}
export function hasConfig(context: TStylingContext, flag: TStylingConfig) {
return (getConfig(context) & flag) !== 0;
}
/**
* Determines whether or not to apply styles/classes directly or via context resolution.
*
* There are three cases that are matched here:
* 1. context is locked for template or host bindings (depending on `hostBindingsMode`)
* 2. There are no collisions (i.e. properties with more than one binding)
* 3. There are only "prop" or "map" bindings present, but not both
*/
export function allowDirectStyling(context: TStylingContext, hostBindingsMode: boolean): boolean {
const config = getConfig(context);
return ((config & getLockedConfig(hostBindingsMode)) !== 0) &&
((config & TStylingConfig.HasCollisions) === 0) &&
((config & TStylingConfig.HasPropAndMapBindings) !== TStylingConfig.HasPropAndMapBindings);
}
export function setConfig(context: TStylingContext, value: TStylingConfig): void {
context[TStylingContextIndex.ConfigPosition] = value;
}
export function patchConfig(context: TStylingContext, flag: TStylingConfig): void {
context[TStylingContextIndex.ConfigPosition] |= flag;
}
export function getProp(context: TStylingContext, index: number): string {
return context[index + TStylingContextIndex.PropOffset] as string;
}
function getPropConfig(context: TStylingContext, index: number): number {
return (context[index + TStylingContextIndex.ConfigOffset] as number) &
TStylingContextPropConfigFlags.Mask;
}
export function isSanitizationRequired(context: TStylingContext, index: number): boolean {
return (getPropConfig(context, index) & TStylingContextPropConfigFlags.SanitizationRequired) !==
0;
}
export function getGuardMask(
context: TStylingContext, index: number, isHostBinding: boolean): number {
const position = index + (isHostBinding ? TStylingContextIndex.HostBindingsBitGuardOffset :
TStylingContextIndex.TemplateBitGuardOffset);
return context[position] as number;
}
export function setGuardMask(
context: TStylingContext, index: number, maskValue: number, isHostBinding: boolean) {
const position = index + (isHostBinding ? TStylingContextIndex.HostBindingsBitGuardOffset :
TStylingContextIndex.TemplateBitGuardOffset);
context[position] = maskValue;
}
export function getValuesCount(context: TStylingContext): number {
return getTotalSources(context) + 1;
}
export function getTotalSources(context: TStylingContext): number {
return context[TStylingContextIndex.TotalSourcesPosition];
}
export function getBindingValue(context: TStylingContext, index: number, offset: number) {
return context[index + TStylingContextIndex.BindingsStartOffset + offset] as number | string;
}
export function getDefaultValue(context: TStylingContext, index: number): string|boolean|null {
return context[index + TStylingContextIndex.BindingsStartOffset + getTotalSources(context)] as
string |
boolean | null;
}
export function setDefaultValue(
context: TStylingContext, index: number, value: string | boolean | null) {
return context[index + TStylingContextIndex.BindingsStartOffset + getTotalSources(context)] =
value;
}
export function setValue(data: LStylingData, bindingIndex: number, value: any) {
data[bindingIndex] = value;
}
export function getValue<T = any>(data: LStylingData, bindingIndex: number): T|null {
return bindingIndex > 0 ? data[bindingIndex] as T : null;
}
export function lockContext(context: TStylingContext, hostBindingsMode: boolean): void {
patchConfig(context, getLockedConfig(hostBindingsMode));
}
export function isContextLocked(context: TStylingContext, hostBindingsMode: boolean): boolean {
return hasConfig(context, getLockedConfig(hostBindingsMode));
}
export function getLockedConfig(hostBindingsMode: boolean) {
return hostBindingsMode ? TStylingConfig.HostBindingsLocked :
TStylingConfig.TemplateBindingsLocked;
}
export function getPropValuesStartPosition(context: TStylingContext) {
let startPosition = TStylingContextIndex.ValuesStartPosition;
if (hasConfig(context, TStylingConfig.HasMapBindings)) {
startPosition += TStylingContextIndex.BindingsStartOffset + getValuesCount(context);
}
return startPosition;
}
export function isMapBased(prop: string) {
return prop === MAP_BASED_ENTRY_PROP_NAME;
}
export function hasValueChanged(
a: NO_CHANGE | StylingMapArray | number | String | string | null | boolean | undefined | {},
b: NO_CHANGE | StylingMapArray | number | String | string | null | boolean | undefined |
{}): boolean {
if (b === NO_CHANGE) return false;
let compareValueA = Array.isArray(a) ? a[StylingMapArrayIndex.RawValuePosition] : a;
let compareValueB = Array.isArray(b) ? b[StylingMapArrayIndex.RawValuePosition] : b;
// these are special cases for String based values (which are created as artifacts
// when sanitization is bypassed on a particular value)
if (compareValueA instanceof String) {
compareValueA = compareValueA.toString();
}
if (compareValueB instanceof String) {
compareValueB = compareValueB.toString();
}
return !Object.is(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 !== '';
}
export function concatString(a: string, b: string, separator = ' '): string {
return a + ((b.length && a.length) ? separator : '') + b;
}
export function hyphenate(value: string): string {
return value.replace(/[a-z][A-Z]/g, v => v.charAt(0) + '-' + v.charAt(1)).toLowerCase();
}
/**
* Returns an instance of `StylingMapArray`.
*
* This function is designed to find an instance of `StylingMapArray` in case it is stored
* inside of an instance of `TStylingContext`. When a styling context is created it
* will copy over an initial styling values from the tNode (which are stored as a
* `StylingMapArray` on the `tNode.classes` or `tNode.styles` values).
*/
export function getStylingMapArray(value: TStylingContext | StylingMapArray | null):
StylingMapArray|null {
return isStylingContext(value) ?
(value as TStylingContext)[TStylingContextIndex.InitialStylingValuePosition] :
value as StylingMapArray;
}
export function isStylingContext(value: TStylingContext | StylingMapArray | null): boolean {
// the StylingMapArray is in the format of [initial, prop, string, prop, string]
// and this is the defining value to distinguish between arrays
return Array.isArray(value) && value.length >= TStylingContextIndex.ValuesStartPosition &&
typeof value[1] !== 'string';
}
export function isStylingMapArray(value: TStylingContext | StylingMapArray | null): boolean {
// the StylingMapArray is in the format of [initial, prop, string, prop, string]
// and this is the defining value to distinguish between arrays
return Array.isArray(value) &&
(typeof(value as StylingMapArray)[StylingMapArrayIndex.ValuesStartPosition] === 'string');
}
export function getInitialStylingValue(context: TStylingContext | StylingMapArray | null): string {
const map = getStylingMapArray(context);
return map && (map[StylingMapArrayIndex.RawValuePosition] as string | null) || '';
}
export function hasClassInput(tNode: TNode) {
return (tNode.flags & TNodeFlags.hasClassInput) !== 0;
}
export function hasStyleInput(tNode: TNode) {
return (tNode.flags & TNodeFlags.hasStyleInput) !== 0;
}
export function getMapProp(map: StylingMapArray, index: number): string {
return map[index + StylingMapArrayIndex.PropOffset] as string;
}
const MAP_DIRTY_VALUE =
typeof ngDevMode !== 'undefined' && ngDevMode ? {} : {MAP_DIRTY_VALUE: true};
export function setMapAsDirty(map: StylingMapArray): void {
map[StylingMapArrayIndex.RawValuePosition] = MAP_DIRTY_VALUE;
}
export function setMapValue(
map: StylingMapArray, index: number, value: string | boolean | null): void {
map[index + StylingMapArrayIndex.ValueOffset] = value;
}
export function getMapValue(map: StylingMapArray, index: number): string|null {
return map[index + StylingMapArrayIndex.ValueOffset] as string | null;
}
export function forceClassesAsString(classes: string | {[key: string]: any} | null | undefined):
string {
if (classes && typeof classes !== 'string') {
classes = Object.keys(classes).join(' ');
}
return (classes as string) || '';
}
export function forceStylesAsString(styles: {[key: string]: any} | null | undefined): string {
let str = '';
if (styles) {
const props = Object.keys(styles);
for (let i = 0; i < props.length; i++) {
const prop = props[i];
str = concatString(str, `${prop}:${styles[prop]}`, ';');
}
}
return str;
}
export function isHostStylingActive(directiveOrSourceId: number): boolean {
return directiveOrSourceId !== TEMPLATE_DIRECTIVE_INDEX;
}
/**
* Converts the provided styling map array into a string.
*
* Classes => `one two three`
* Styles => `prop:value; prop2:value2`
*/
export function stylingMapToString(map: StylingMapArray, isClassBased: boolean): string {
let str = '';
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i) as string;
const attrValue = concatString(prop, isClassBased ? '' : value, ':');
str = concatString(str, attrValue, isClassBased ? ' ' : '; ');
}
return str;
}
/**
* Converts the provided styling map array into a key value map.
*/
export function stylingMapToStringMap(map: StylingMapArray | null): {[key: string]: any} {
let stringMap: {[key: string]: any} = {};
if (map) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i) as string;
stringMap[prop] = value;
}
}
return stringMap;
}
/**
* Inserts the provided item into the provided styling array at the right spot.
*
* The `StylingMapArray` type is a sorted key/value array of entries. This means
* that when a new entry is inserted it must be placed at the right spot in the
* array. This function figures out exactly where to place it.
*/
export function addItemToStylingMap(
stylingMapArr: StylingMapArray, prop: string, value: string | boolean | null,
allowOverwrite?: boolean) {
for (let j = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length;
j += StylingMapArrayIndex.TupleSize) {
const propAtIndex = getMapProp(stylingMapArr, j);
if (prop <= propAtIndex) {
let applied = false;
if (propAtIndex === prop) {
const valueAtIndex = stylingMapArr[j];
if (allowOverwrite || !isStylingValueDefined(valueAtIndex)) {
applied = true;
setMapValue(stylingMapArr, j, value);
}
} else {
applied = true;
stylingMapArr.splice(j, 0, prop, value);
}
return applied;
}
}
stylingMapArr.push(prop, value);
return true;
}
/**
* Used to convert a {key:value} map into a `StylingMapArray` array.
*
* This function will either generate a new `StylingMapArray` instance
* or it will patch the provided `newValues` map value into an
* existing `StylingMapArray` value (this only happens if `bindingValue`
* is an instance of `StylingMapArray`).
*
* If a new key/value map is provided with an old `StylingMapArray`
* 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 | StylingMapArray,
newValues: {[key: string]: any} | string | null | undefined,
normalizeProps?: boolean): StylingMapArray {
const stylingMapArr: StylingMapArray = Array.isArray(bindingValue) ? bindingValue : [null];
stylingMapArr[StylingMapArrayIndex.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 = StylingMapArrayIndex.ValuesStartPosition; j < stylingMapArr.length;
j += StylingMapArrayIndex.TupleSize) {
setMapValue(stylingMapArr, 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) {
for (let i = 0; i < props.length; i++) {
const prop = props[i] as string;
const newProp = normalizeProps ? hyphenate(prop) : prop;
const value = allValuesTrue ? true : map ![prop];
addItemToStylingMap(stylingMapArr, newProp, value, true);
}
}
return stylingMapArr;
}