refactor(ivy): remame styling_next
directory to styling
(#32731)
PR Close #32731
This commit is contained in:

committed by
Andrew Kushnir

parent
0618bed83e
commit
f88f717094
808
packages/core/src/render3/styling/bindings.ts
Normal file
808
packages/core/src/render3/styling/bindings.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
541
packages/core/src/render3/styling/instructions.ts
Normal file
541
packages/core/src/render3/styling/instructions.ts
Normal 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());
|
||||
}
|
580
packages/core/src/render3/styling/interfaces.ts
Normal file
580
packages/core/src/render3/styling/interfaces.ts
Normal 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,
|
||||
}
|
351
packages/core/src/render3/styling/map_based_bindings.ts
Normal file
351
packages/core/src/render3/styling/map_based_bindings.ts
Normal 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;
|
||||
}
|
114
packages/core/src/render3/styling/state.ts
Normal file
114
packages/core/src/render3/styling/state.ts
Normal 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;
|
||||
}
|
274
packages/core/src/render3/styling/styling_debug.ts
Normal file
274
packages/core/src/render3/styling/styling_debug.ts
Normal 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);
|
||||
}
|
||||
}
|
422
packages/core/src/render3/styling/util.ts
Normal file
422
packages/core/src/render3/styling/util.ts
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user