/** * @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 {TNodeFlags} from './node'; import {ProceduralRenderer3, RElement, Renderer3} from './renderer'; import {SanitizerFn} from './sanitization'; import {LView} from './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. `
`) * tNode.styles = TStylingContext; // one or more styling bindings present (e.g. `
`) * ``` * * 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 * //
// lView binding index = 22 * // ... * //
* tNode.styles = [ * 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: * * ``` *
// 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`. * * ``` *
// 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. * `
` 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. `
`): * * ```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 *
* ``` * * 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 { /** The total amount of sources present in the context */ [TStylingContextIndex.TotalSourcesPosition]: number; /** Initial value position for static styles */ [TStylingContextIndex.InitialStylingValuePosition]: StylingMapArray; } /** * An index of position and offset values used to navigate the `TStylingContext`. */ export const enum TStylingContextIndex { TotalSourcesPosition = 0, InitialStylingValuePosition = 1, ValuesStartPosition = 2, // 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|undefined> { /** * The last raw value used to generate the entries in the map. */ [StylingMapArrayIndex.RawValuePosition]: {}|string|number|null|undefined; } /** * 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, } /** * Simplified `TNode` interface for styling-related code. * * The styling algorithm code only needs access to `flags`. */ export interface TStylingNode { flags: TNodeFlags; } /** * Value stored in the `TData` which is needed to re-concatenate the styling. * * - `string`: Stores the property name. Used with `ɵɵstyleProp`/`ɵɵclassProp` instruction which * don't have suffix or don't need sanitization. */ export type TStylingKey = string | TStylingSuffixKey | TStylingSanitizationKey | TStylingMapKey; /** * For performance reasons we want to make sure that all subclasses have the same shape object. * * See subclasses for implementation details. */ export interface TStylingKeyShape { key: string|null; extra: string|SanitizerFn|TStylingMapFn; } /** * Used in the case of `ɵɵstyleProp('width', exp, 'px')`. */ export interface TStylingSuffixKey extends TStylingKeyShape { /// Stores the property key. key: string; /// Stores the property suffix. extra: string; } /** * Used in the case of `ɵɵstyleProp('url', exp, styleSanitizationFn)`. */ export interface TStylingSanitizationKey extends TStylingKeyShape { /// Stores the property key. key: string; /// Stores sanitization function. extra: SanitizerFn; } /** * Used in the case of `ɵɵstyleMap()`/`ɵɵclassMap()`. */ export interface TStylingMapKey extends TStylingKeyShape { /// There is no key key: null; /// Invoke this function to process the value (convert it into the result) /// This is implemented this way so that the logic associated with `ɵɵstyleMap()`/`ɵɵclassMap()` /// can be tree shaken away. Internally the function will break the `Map`/`Array` down into /// parts and call `appendStyling` on parts. /// /// See: `CLASS_MAP_STYLING_KEY` and `STYLE_MAP_STYLING_KEY` for details. extra: TStylingMapFn; } /** * Invoke this function to process the styling value which is non-primitive (Map/Array) * This is implemented this way so that the logic associated with `ɵɵstyleMap()`/`ɵɵclassMap()` * can be tree shaken away. Internally the function will break the `Map`/`Array` down into * parts and call `appendStyling` on parts. * * See: `CLASS_MAP_STYLING_KEY` and `STYLE_MAP_STYLING_KEY` for details. */ export type TStylingMapFn = (text: string, value: any, hasPreviousDuplicate: boolean) => string; /** * This is a branded number which contains previous and next index. * * When we come across styling instructions we need to store the `TStylingKey` in the correct * order so that we can re-concatenate the styling value in the desired priority. * * The insertion can happen either at the: * - end of template as in the case of coming across additional styling instruction in the template * - in front of the template in the case of coming across additional instruction in the * `hostBindings`. * * We use `TStylingRange` to store the previous and next index into the `TData` where the template * bindings can be found. * * - bit 0 is used to mark that the previous index has a duplicate for current value. * - bit 1 is used to mark that the next index has a duplicate for the current value. * - bits 2-16 are used to encode the next/tail of the template. * - bits 17-32 are used to encode the previous/head of template. * * NODE: *duplicate* false implies that it is statically known that this binding will not collide * with other bindings and therefore there is no need to check other bindings. For example the * bindings in `
` will never collide and will have * their bits set accordingly. Previous duplicate means that we may need to check previous if the * current binding is `null`. Next duplicate means that we may need to check next bindings if the * current binding is not `null`. * * NOTE: `0` has special significance and represents `null` as in no additional pointer. */ export interface TStylingRange { __brand__: 'TStylingRange'; } /** * Shift and masks constants for encoding two numbers into and duplicate info into a single number. */ export const enum StylingRange { /// Number of bits to shift for the previous pointer PREV_SHIFT = 18, /// Previous pointer mask. PREV_MASK = 0xFFFC0000, /// Number of bits to shift for the next pointer NEXT_SHIFT = 2, /// Next pointer mask. NEXT_MASK = 0x0003FFC, /** * This bit is set if the previous bindings contains a binding which could possibly cause a * duplicate. For example: `
`, the `width` binding will * have previous duplicate set. The implication is that if `width` binding becomes `null`, it is * necessary to defer the value to `map.width`. (Because `width` overwrites `map.width`.) */ PREV_DUPLICATE = 0x02, /** * This bit is set to if the next binding contains a binding which could possibly cause a * duplicate. For example: `
`, the `map` binding will * have next duplicate set. The implication is that if `map.width` binding becomes not `null`, it * is necessary to defer the value to `width`. (Because `width` overwrites `map.width`.) */ NEXT_DUPLICATE = 0x01, } export function toTStylingRange(prev: number, next: number): TStylingRange { return (prev << StylingRange.PREV_SHIFT | next << StylingRange.NEXT_SHIFT) as any; } export function getTStylingRangePrev(tStylingRange: TStylingRange): number { return (tStylingRange as any as number) >> StylingRange.PREV_SHIFT; } export function getTStylingRangePrevDuplicate(tStylingRange: TStylingRange): boolean { return ((tStylingRange as any as number) & StylingRange.PREV_DUPLICATE) == StylingRange.PREV_DUPLICATE; } export function setTStylingRangePrev( tStylingRange: TStylingRange, previous: number): TStylingRange { return ( ((tStylingRange as any as number) & ~StylingRange.PREV_MASK) | (previous << StylingRange.PREV_SHIFT)) as any; } export function setTStylingRangePrevDuplicate(tStylingRange: TStylingRange): TStylingRange { return ((tStylingRange as any as number) | StylingRange.PREV_DUPLICATE) as any; } export function getTStylingRangeNext(tStylingRange: TStylingRange): number { return ((tStylingRange as any as number) & StylingRange.NEXT_MASK) >> StylingRange.NEXT_SHIFT; } export function setTStylingRangeNext(tStylingRange: TStylingRange, next: number): TStylingRange { return ( ((tStylingRange as any as number) & ~StylingRange.NEXT_MASK) | // next << StylingRange.NEXT_SHIFT) as any; } export function getTStylingRangeNextDuplicate(tStylingRange: TStylingRange): boolean { return ((tStylingRange as any as number) & StylingRange.NEXT_DUPLICATE) === StylingRange.NEXT_DUPLICATE; } export function setTStylingRangeNextDuplicate(tStylingRange: TStylingRange): TStylingRange { return ((tStylingRange as any as number) | StylingRange.NEXT_DUPLICATE) as any; } export function getTStylingRangeTail(tStylingRange: TStylingRange): number { const next = getTStylingRangeNext(tStylingRange); return next === 0 ? getTStylingRangePrev(tStylingRange) : next; }