Matias Niemelä fe8301c462 feat(ivy): provide support for map-based host bindings for [style] and [class] (#28246)
Up until now, `[style]` and `[class]` bindings (the map-based ones) have only
worked as template bindings and have not been supported at all inside of host
bindings. This patch ensures that multiple host binding sources (components and
directives) all properly assign style values and merge them correctly in terms
of priority.

Jira: FW-882

PR Close #28246
2019-02-11 16:21:19 -08:00

663 lines
27 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 {RElement} from '../interfaces/renderer';
import {PlayerContext} from './player';
/**
* The styling context acts as a styling manifest (shaped as an array) for determining which
* styling properties have been assigned via the provided `updateStylingMap`, `updateStyleProp`
* and `updateClassProp` functions. It also stores the static style/class values that were
* extracted from the template by the compiler.
*
* A context is created by Angular when:
* 1. An element contains static styling values (like style="..." or class="...")
* 2. An element contains single property binding values (like [style.prop]="x" or
* [class.prop]="y")
* 3. An element contains multi property binding values (like [style]="x" or [class]="y")
* 4. A directive contains host bindings for static, single or multi styling properties/bindings.
* 5. An animation player is added to an element via `addPlayer`
*
* Note that even if an element contains static styling then this context will be created and
* attached to it. The reason why this happens (instead of treating styles/classes as regular
* HTML attributes) is because the style/class bindings must be able to default themselves back
* to their respective static values when they are set to null.
*
* Say for example we have this:
* ```
* <!-- when `myWidthExp=null` then a width of `100px`
* will be used a default value for width -->
* <div style="width:100px" [style.width]="myWidthExp"></div>
* ```
*
* Even in the situation where there are no bindings, the static styling is still placed into the
* context because there may be another directive on the same element that has styling.
*
* When Angular initializes styling data for an element then it will first register the static
* styling values on the element using one of these two instructions:
*
* 1. elementStart or element (within the template function of a component)
* 2. elementHostAttrs (for directive host bindings)
*
* In either case, a styling context will be created and stored within an element's `LViewData`.
* Once the styling context is created then single and multi properties can be stored within it.
* For this to happen, the following function needs to be called:
*
* `elementStyling` (called with style properties, class properties and a sanitizer + a directive
* instance).
*
* When this instruction is called it will populate the styling context with the provided style
* and class names into the context.
*
* The context itself looks like this:
*
* context = [
* // 0-8: header values (about 8 entries of configuration data)
* // 9+: this is where each entry is stored:
* ]
*
* Let's say we have the following template code:
*
* ```
* <div class="foo bar"
* style="width:200px; color:red"
* [style.width]="myWidthExp"
* [style.height]="myHeightExp"
* [class.baz]="myBazExp">
* ```
*
* The context generated from these values will look like this (note that
* for each binding name (the class and style bindings) the values will
* be inserted twice into the array (once for single property entries and
* again for multi property entries).
*
* context = [
* // 0-8: header values (about 8 entries of configuration data)
* // 9+: this is where each entry is stored:
*
* // SINGLE PROPERTIES
* configForWidth,
* 'width'
* myWidthExp, // the binding value not the binding itself
* 0, // the directive owner
*
* configForHeight,
* 'height'
* myHeightExp, // the binding value not the binding itself
* 0, // the directive owner
*
* configForBazClass,
* 'baz
* myBazClassExp, // the binding value not the binding itself
* 0, // the directive owner
*
* // MULTI PROPERTIES
* configForWidth,
* 'width'
* myWidthExp, // the binding value not the binding itself
* 0, // the directive owner
*
* configForHeight,
* 'height'
* myHeightExp, // the binding value not the binding itself
* 0, // the directive owner
*
* configForBazClass,
* 'baz
* myBazClassExp, // the binding value not the binding itself
* 0, // the directive owner
* ]
*
* The configuration values are left out of the example above because
* the ordering of them could change between code patches. Please read the
* documentation below to get a better understand of what the configuration
* values are and how they work.
*
* Each time a binding property is updated (whether it be through a single
* property instruction like `elementStyleProp`, `elementClassProp` or
* `elementStylingMap`) then the values in the context will be updated as
* well.
*
* If for example `[style.width]` updates to `555px` then its value will be reflected
* in the context as so:
*
* context = [
* // ...
* configForWidth, // this will be marked DIRTY
* 'width'
* '555px',
* 0,
* //..
* ]
*
* The context and directive data will also be marked dirty.
*
* Despite the context being updated, nothing has been rendered on screen (not styles or
* classes have been set on the element). To kick off rendering for an element the following
* function needs to be run `elementStylingApply`.
*
* `elementStylingApply` will run through the context and find each dirty value and render them onto
* the element. Once complete, all styles/classes will be set to clean. Because of this, the render
* function will now know not to rerun itself again if called again unless new style/class values
* have changed.
*
* ## Directives
* Directive style/class values (which are provided through host bindings) are also supported and
* housed within the same styling context as are template-level style/class properties/bindings
* So long as they are all assigned to the same element, both directive-level and template-level
* styling bindings share the same context.
*
* Each of the following instructions supports accepting a directive instance as an input parameter:
*
* - `elementHostAttrs`
* - `elementStyling`
* - `elementStyleProp`
* - `elementClassProp`
* - `elementStylingMap`
* - `elementStylingApply`
*
* Each time a directive value is passed in, it will be converted into an index by examining the
* directive registry (which lives in the context configuration area). The index is then used
* to help single style properties figure out where a value is located in the context.
*
*
* ## Single-level styling bindings (`[style.prop]` and `[class.name]`)
*
* Both `[style.prop]` and `[class.name]` bindings are run through the `updateStyleProp`
* and `updateClassProp` functions respectively. They work by examining the provided
* `offset` value and are able to locate the exact spot in the context where the
* matching style is located.
*
* Both `[style.prop]` and `[class.name]` bindings are able to process these values
* from directive host bindings. When evaluated (from the host binding function) the
* `directiveRef` value is then passed in.
*
* If two directives or a directive + a template binding both write to the same style/class
* binding then the styling context code will decide which one wins based on the following
* rule:
*
* 1. If the template binding has a value then it always wins
* 2. Otherwise whichever first-registered directive that has that value first will win
*
* The code example helps make this clear:
*
* ```
* <!--
* <div [style.width]="myWidth"
* [my-width-directive]="'600px'">
* -->
*
* @Directive({
* selector: '[my-width-directive']
* })
* class MyWidthDirective {
* @Input('my-width-directive')
* @HostBinding('style.width')
* public width = null;
* }
* ```
*
* Since there is a style binding for width present on the element (`[style.width]`) then
* it will always win over the width binding that is present as a host binding within
* the `MyWidthDirective`. However, if `[style.width]` renders as `null` (so `myWidth=null`)
* then the `MyWidthDirective` will be able to write to the `width` style within the context.
* Simply put, whichever directive writes to a value first ends up having ownership of it as
* long as the template didn't set anything.
*
* The way in which the ownership is facilitated is through index value. The earliest directives
* get the smallest index values (with 0 being reserved for the template element bindings). Each
* time a value is written from a directive or the template bindings, the value itself gets
* assigned the directive index value in its data. If another directive writes a value again then
* its directive index gets compared against the directive index that exists on the element. Only
* when the new value's directive index is less than the existing directive index then the new
* value will be written to the context. But, if the existing value is null then the new value is
* written by the less important directive.
*
* Each directive also has its own sanitizer and dirty flags. These values are consumed within the
* rendering function.
*
*
* ## Multi-level styling bindings (`[style]` and `[class]`)
*
* Multi-level styling bindings are treated as less important (less specific) as single-level
* bindings (things like `[style.prop]` and `[class.name]`).
*
* Multi-level bindings are still applied to the context in a similar way as are single level
* bindings, but this process works by diffing the new multi-level values (which are key/value
* maps) against the existing set of styles that live in the context. Each time a new map value
* is detected (via identity check) then it will loop through the values and figure out what
* has changed and reorder the context array to match the ordering of the keys. This reordering
* of the context makes sure that follow-up traversals of the context when updated against the
* key/value map are as close as possible to o(n) (where "n" is the size of the key/value map).
*
* If a `directiveRef` value is passed in then the styling algorithm code will take the directive's
* prioritization index into account and update the values with respect to more important
* directives. This means that if a value such as `width` is updated in two different `[style]`
* bindings (say one on the template and another within a directive that sits on the same element)
* then the algorithm will decide how to update the value based on the following heuristic:
*
* 1. If the template binding has a value then it always wins
* 2. If not then whichever first-registered directive that has that value first will win
*
* It will also update the value if it was set to `null` by a previous directive (or the template).
*
* Each time a value is updated (or removed) then the context will change shape to better match
* the ordering of the styling data as well as the ordering of each directive that contains styling
* data. (See `patchStylingMapIntoContext` inside of class_and_style_bindings.ts to better
* understand how this works.)
*
* ## Rendering
* The rendering mechanism (when the styling data is applied on screen) occurs via the
* `elementStylingApply` function and is designed to run after **all** styling functions have been
* evaluated. The rendering algorithm will loop over the context and only apply the styles that are
* flagged as dirty (either because they are new, updated or have been removed via multi or
* single bindings).
*/
export interface StylingContext extends
Array<{[key: string]: any}|number|string|boolean|RElement|StyleSanitizeFn|PlayerContext|null> {
/**
* A numeric value representing the configuration status (whether the context is dirty or not)
* mixed together (using bit shifting) with a index value which tells the starting index value
* of where the multi style entries begin.
*/
[StylingIndex.MasterFlagPosition]: number;
/**
* Location of the collection of directives for this context
*/
[StylingIndex.DirectiveRegistryPosition]: DirectiveRegistryValues;
/**
* Location of all static styles values
*/
[StylingIndex.InitialStyleValuesPosition]: InitialStylingValues;
/**
* Location of all static class values
*/
[StylingIndex.InitialClassValuesPosition]: InitialStylingValues;
/**
* A numeric value representing the class index offset value. Whenever a single class is
* applied (using `elementClassProp`) it should have an styling index value that doesn't
* need to take into account any style values that exist in the context.
*/
[StylingIndex.SinglePropOffsetPositions]: SinglePropOffsetValues;
/**
* Location of element that is used as a target for this context.
*/
[StylingIndex.ElementPosition]: RElement|null;
/**
* The last class value that was interpreted by elementStylingMap. This is cached
* So that the algorithm can exit early incase the value has not changed.
*/
[StylingIndex.CachedMultiClasses]: any|MapBasedOffsetValues;
/**
* The last style value that was interpreted by elementStylingMap. This is cached
* So that the algorithm can exit early incase the value has not changed.
*/
[StylingIndex.CachedMultiStyles]: any|MapBasedOffsetValues;
/**
* Location of animation context (which contains the active players) for this element styling
* context.
*/
[StylingIndex.PlayerContext]: PlayerContext|null;
}
/**
* Used as a styling array to house static class and style values that were extracted
* by the compiler and placed in the animation context via `elementStart` and
* `elementHostAttrs`.
*
* See [InitialStylingValuesIndex] for a breakdown of how all this works.
*/
export interface InitialStylingValues extends Array<string|boolean|null> {
[InitialStylingValuesIndex.DefaultNullValuePosition]: null;
[InitialStylingValuesIndex.InitialClassesStringPosition]: string|null;
}
/**
* Used as an offset/position index to figure out where initial styling
* values are located.
*
* Used as a reference point to provide markers to all static styling
* values (the initial style and class values on an element) within an
* array within the `StylingContext`. This array contains key/value pairs
* where the key is the style property name or className and the value is
* the style value or whether or not a class is present on the elment.
*
* The first value is always null so that a initial index value of
* `0` will always point to a null value.
*
* The second value is also always null unless a string-based representation
* of the styling data was constructed (it gets cached in this slot).
*
* If a <div> elements contains a list of static styling values like so:
*
* <div class="foo bar baz" style="width:100px; height:200px;">
*
* Then the initial styles for that will look like so:
*
* Styles:
* ```
* StylingContext[InitialStylesIndex] = [
* null, null, 'width', '100px', height, '200px'
* ]
* ```
*
* Classes:
* ```
* StylingContext[InitialClassesIndex] = [
* null, null, 'foo', true, 'bar', true, 'baz', true
* ]
* ```
*
* Initial style and class entries have their own arrays. This is because
* it's easier to add to the end of one array and not then have to update
* every context entries' pointer index to the newly offseted values.
*
* When property bindinds are added to a context then initial style/class
* values will also be inserted into the array. This is to create a space
* in the situation when a follow-up directive inserts static styling into
* the array. By default, style values are `null` and class values are
* `false` when inserted by property bindings.
*
* For example:
* ```
* <div class="foo bar baz"
* [class.car]="myCarExp"
* style="width:100px; height:200px;"
* [style.opacity]="myOpacityExp">
* ```
*
* Will construct initial styling values that look like:
*
* Styles:
* ```
* StylingContext[InitialStylesIndex] = [
* null, null, 'width', '100px', height, '200px', 'opacity', null
* ]
* ```
*
* Classes:
* ```
* StylingContext[InitialClassesIndex] = [
* null, null, 'foo', true, 'bar', true, 'baz', true, 'car', false
* ]
* ```
*
* Now if a directive comes along and introduces `car` as a static
* class value or `opacity` then those values will be filled into
* the initial styles array.
*
* For example:
*
* ```
* @Directive({
* selector: 'opacity-car-directive',
* host: {
* 'style': 'opacity:0.5',
* 'class': 'car'
* }
* })
* class OpacityCarDirective {}
* ```
*
* This will render itself as:
*
* Styles:
* ```
* StylingContext[InitialStylesIndex] = [
* null, null, 'width', '100px', height, '200px', 'opacity', '0.5'
* ]
* ```
*
* Classes:
* ```
* StylingContext[InitialClassesIndex] = [
* null, null, 'foo', true, 'bar', true, 'baz', true, 'car', true
* ]
* ```
*/
export const enum InitialStylingValuesIndex {
DefaultNullValuePosition = 0,
InitialClassesStringPosition = 1,
KeyValueStartPosition = 2,
PropOffset = 0,
ValueOffset = 1,
Size = 2
}
/**
* An array located in the StylingContext that houses all directive instances and additional
* data about them.
*
* Each entry in this array represents a source of where style/class binding values could
* come from. By default, there is always at least one directive here with a null value and
* that represents bindings that live directly on an element in the template (not host bindings).
*
* Each successive entry in the array is an actual instance of a directive as well as some
* additional info about that entry.
*
* An entry within this array has the following values:
* [0] = The instance of the directive (the first entry is null because its reserved for the
* template)
* [1] = The pointer that tells where the single styling (stuff like [class.foo] and [style.prop])
* offset values are located. This value will allow for a binding instruction to find exactly
* where a style is located.
* [2] = Whether or not the directive has any styling values that are dirty. This is used as
* reference within the `renderStyling` function to decide whether to skip iterating
* through the context when rendering is executed.
* [3] = The styleSanitizer instance that is assigned to the directive. Although it's unlikely,
* a directive could introduce its own special style sanitizer and for this reach each
* directive will get its own space for it (if null then the very first sanitizer is used).
*
* Each time a new directive is added it will insert these four values at the end of the array.
* When this array is examined then the resulting directiveIndex will be resolved by dividing the
* index value by the size of the array entries (so if DirA is at spot 8 then its index will be 2).
*/
export interface DirectiveRegistryValues extends Array<null|{}|boolean|number|StyleSanitizeFn> {
[DirectiveRegistryValuesIndex.DirectiveValueOffset]: null;
[DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset]: number;
[DirectiveRegistryValuesIndex.DirtyFlagOffset]: boolean;
[DirectiveRegistryValuesIndex.StyleSanitizerOffset]: StyleSanitizeFn|null;
}
/**
* An enum that outlines the offset/position values for each directive entry and its data
* that are housed inside of [DirectiveRegistryValues].
*/
export const enum DirectiveRegistryValuesIndex {
DirectiveValueOffset = 0,
SinglePropValuesIndexOffset = 1,
DirtyFlagOffset = 2,
StyleSanitizerOffset = 3,
Size = 4
}
/**
* An array that contains the index pointer values for every single styling property
* that exists in the context and for every directive. It also contains the total
* single styles and single classes that exists in the context as the first two values.
*
* Let's say we have the following template code:
*
* <div [style.width]="myWidth"
* [style.height]="myHeight"
* [class.flipped]="flipClass"
* directive-with-opacity>
* directive-with-foo-bar-classes>
*
* We have two directive and template-binding sources,
* 2 + 1 styles and 1 + 1 classes. When the bindings are
* registered the SinglePropOffsets array will look like so:
*
* s_0/c_0 = template directive value
* s_1/c_1 = directive one (directive-with-opacity)
* s_2/c_2 = directive two (directive-with-foo-bar-classes)
*
* [3, 2, 2, 1, s_00, s01, c_01, 1, 0, s_10, 0, 1, c_20
*/
export interface SinglePropOffsetValues extends Array<number> {
[SinglePropOffsetValuesIndex.StylesCountPosition]: number;
[SinglePropOffsetValuesIndex.ClassesCountPosition]: number;
}
/**
* An enum that outlines the offset/position values for each single prop/class entry
* that are housed inside of [SinglePropOffsetValues].
*/
export const enum SinglePropOffsetValuesIndex {
StylesCountPosition = 0,
ClassesCountPosition = 1,
ValueStartPosition = 2
}
/**
* Used a reference for all multi styling values (values that are assigned via the
* `[style]` and `[class]` bindings).
*
* Single-styling properties (things set via `[style.prop]` and `[class.name]` bindings)
* are not handled using the same approach as multi-styling bindings (such as `[style]`
* `[class]` bindings).
*
* Multi-styling bindings rely on a diffing algorithm to figure out what properties have been added,
* removed and modified. Multi-styling properties are also evaluated across directives--which means
* that Angular supports having multiple directives all write to the same `[style]` and `[class]`
* bindings (using host bindings) even if the `[style]` and/or `[class]` bindings are being written
* to on the template element.
*
* All multi-styling values that are written to an element (whether it be from the template or any
* directives attached to the element) are all written into the `MapBasedOffsetValues` array. (Note
* that there are two arrays: one for styles and another for classes.)
*
* This array is shaped in the following way:
*
* [0] = The total amount of unique multi-style or multi-class entries that exist currently in the
* context.
* [1+] = Contains an entry of four values ... Each entry is a value assigned by a
* `[style]`/`[class]`
* binding (we call this a **source**).
*
* An example entry looks like so (at a given `i` index):
* [i + 0] = Whether or not the value is dirty
*
* [i + 1] = The index of where the map-based values
* (for this **source**) start within the context
*
* [i + 2] = The untouched, last set value of the binding
*
* [i + 3] = The total amount of unqiue binding values that were
* extracted and set into the context. (Note that this value does
* not reflect the total amount of values within the binding
* value (since it's a map), but instead reflects the total values
* that were not used by another directive).
*
* Each time a directive (or template) writes a value to a `[class]`/`[style]` binding then the
* styling diffing algorithm code will decide whether or not to update the value based on the
* following rules:
*
* 1. If a more important directive (either the template or a directive that was registered
* beforehand) has written a specific styling value into the context then any follow-up styling
* values (set by another directive via its `[style]` and/or `[class]` host binding) will not be
* able to set it. This is because the former directive has priorty.
* 2. Only if a former directive has set a specific styling value to null (whether by actually
* setting it to null or not including it in is map value) then a less imporatant directive can
* set its own value.
*
* ## How the map-based styling algorithm updates itself
*/
export interface MapBasedOffsetValues extends Array<any> {
[MapBasedOffsetValuesIndex.EntriesCountPosition]: number;
}
export const enum MapBasedOffsetValuesIndex {
EntriesCountPosition = 0,
ValuesStartPosition = 1,
DirtyFlagOffset = 0,
PositionStartOffset = 1,
ValueOffset = 2,
ValueCountOffset = 3,
Size = 4
}
/**
* Used to set the context to be dirty or not both on the master flag (position 1)
* or for each single/multi property that exists in the context.
*/
export const enum StylingFlags {
// Implies no configurations
None = 0b00000,
// Whether or not the entry or context itself is dirty
Dirty = 0b00001,
// Whether or not this is a class-based assignment
Class = 0b00010,
// Whether or not a sanitizer was applied to this property
Sanitize = 0b00100,
// Whether or not any player builders within need to produce new players
PlayerBuildersDirty = 0b01000,
// The max amount of bits used to represent these configuration values
BindingAllocationLocked = 0b10000,
BitCountSize = 5,
// There are only five bits here
BitMask = 0b11111
}
/** Used as numeric pointer values to determine what cells to update in the `StylingContext` */
export const enum StylingIndex {
// Index of location where the start of single properties are stored. (`updateStyleProp`)
MasterFlagPosition = 0,
// Position of where the registered directives exist for this styling context
DirectiveRegistryPosition = 1,
// Position of where the initial styles are stored in the styling context
InitialStyleValuesPosition = 2,
InitialClassValuesPosition = 3,
// Index of location where the class index offset value is located
SinglePropOffsetPositions = 4,
// Position of where the initial styles are stored in the styling context
// This index must align with HOST, see interfaces/view.ts
ElementPosition = 5,
// Position of where the last string-based CSS class value was stored (or a cached version of the
// initial styles when a [class] directive is present)
CachedMultiClasses = 6,
// Position of where the last string-based CSS class value was stored
CachedMultiStyles = 7,
// Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue
// Position of where the initial styles are stored in the styling context
PlayerContext = 8,
// Location of single (prop) value entries are stored within the context
SingleStylesStartPosition = 9,
FlagsOffset = 0,
PropertyOffset = 1,
ValueOffset = 2,
PlayerBuilderIndexOffset = 3,
// Size of each multi or single entry (flag + prop + value + playerBuilderIndex)
Size = 4,
// Each flag has a binary digit length of this value
BitCountSize = 14, // (32 - 4) / 2 = ~14
// The binary digit value as a mask
BitMask = 0b11111111111111, // 14 bits
}
/**
* An enum that outlines the bit flag data for directive owner and player index
* values that exist within en entry that lives in the StylingContext.
*
* The values here split a number value into two sets of bits:
* - The first 16 bits are used to store the directiveIndex that owns this style value
* - The other 16 bits are used to store the playerBuilderIndex that is attached to this style
*/
export const enum DirectiveOwnerAndPlayerBuilderIndex {
BitCountSize = 16,
BitMask = 0b1111111111111111
}