Miško Hevery 5aabe93abe refactor(ivy): Switch styling to new reconcile algorithm (#34616)
NOTE: This change must be reverted with previous deletes so that it code remains in build-able state.

This change deletes old styling code and replaces it with a simplified styling algorithm.

The mental model for the new algorithm is:
- Create a linked list of styling bindings in the order of priority. All styling bindings ere executed in compiled order and than a linked list of bindings is created in priority order.
- Flush the style bindings at the end of `advance()` instruction. This implies that there are two flush events. One at the end of template `advance` instruction in the template. Second one at the end of `hostBindings` `advance` instruction when processing host bindings (if any).
- Each binding instructions effectively updates the string to represent the string at that location. Because most of the bindings are additive, this is a cheap strategy in most cases. In rare cases the strategy requires removing tokens from the styling up to this point. (We expect that to be rare case)S Because, the bindings are presorted in the order of priority, it is safe to resume the processing of the concatenated string from the last change binding.

PR Close #34616
2020-01-24 12:23:00 -08:00

649 lines
25 KiB
TypeScript

/**
* @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. `<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 = [
* 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|{}> {
/** 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 `<div [style.color]="exp" [style.width]="exp">` 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: `<div [style]="map" [style.width]="width">`, 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: `<div [style]="map" [style.width]="width">`, 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;
}