elements contains a list of static styling values like so:
*
*
@@ -284,14 +348,18 @@ export interface InitialStylingValues extends Array
{ [0]:
* Then the initial styles for that will look like so:
*
* Styles:
+ * ```
* StylingContext[InitialStylesIndex] = [
- * null, 'width', '100px', height, '200px'
+ * null, null, 'width', '100px', height, '200px'
* ]
+ * ```
*
* Classes:
- * StylingContext[InitialStylesIndex] = [
- * null, 'foo', true, 'bar', true, 'baz', true
+ * ```
+ * 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
@@ -300,26 +368,32 @@ export interface InitialStylingValues extends Array { [0]:
* 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
+ * the array. By default, style values are `null` and class values are
* `false` when inserted by property bindings.
*
* For example:
+ * ```
*
+ * ```
*
* Will construct initial styling values that look like:
*
* Styles:
+ * ```
* StylingContext[InitialStylesIndex] = [
- * null, 'width', '100px', height, '200px', 'opacity', null
+ * null, null, 'width', '100px', height, '200px', 'opacity', null
* ]
+ * ```
*
* Classes:
- * StylingContext[InitialStylesIndex] = [
- * null, 'foo', true, 'bar', true, 'baz', true, 'car', false
+ * ```
+ * 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
@@ -327,6 +401,7 @@ export interface InitialStylingValues extends Array
{ [0]:
*
* For example:
*
+ * ```
* @Directive({
* selector: 'opacity-car-directive',
* host: {
@@ -335,21 +410,28 @@ export interface InitialStylingValues extends Array { [0]:
* }
* })
* class OpacityCarDirective {}
+ * ```
*
* This will render itself as:
*
* Styles:
+ * ```
* StylingContext[InitialStylesIndex] = [
- * null, 'width', '100px', height, '200px', 'opacity', null
+ * null, null, 'width', '100px', height, '200px', 'opacity', '0.5'
* ]
+ * ```
*
* Classes:
- * StylingContext[InitialStylesIndex] = [
- * null, 'foo', true, 'bar', true, 'baz', true, 'car', false
+ * ```
+ * StylingContext[InitialClassesIndex] = [
+ * null, null, 'foo', true, 'bar', true, 'baz', true, 'car', true
* ]
+ * ```
*/
export const enum InitialStylingValuesIndex {
- KeyValueStartPosition = 1,
+ DefaultNullValuePosition = 0,
+ InitialClassesStringPosition = 1,
+ KeyValueStartPosition = 2,
PropOffset = 0,
ValueOffset = 1,
Size = 2
@@ -361,28 +443,27 @@ export const enum InitialStylingValuesIndex {
*
* 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 (not host bindings).
+ * 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 an array as well as some
- * additional info.
+ * 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 (or null when it is not a directive, but a template binding
- * source)
+ * [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 renderClassAndStyleBindings function to decide whether to skip
- * iterating through the context when rendering is executed.
+ * 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 (using indexOf) 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).
+ * 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 {
[DirectiveRegistryValuesIndex.DirectiveValueOffset]: null;
@@ -441,29 +522,94 @@ export const enum SinglePropOffsetValuesIndex {
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 {
+ [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 = 0b000000,
+ None = 0b00000,
// Whether or not the entry or context itself is dirty
- Dirty = 0b000001,
+ Dirty = 0b00001,
// Whether or not this is a class-based assignment
- Class = 0b000010,
+ Class = 0b00010,
// Whether or not a sanitizer was applied to this property
- Sanitize = 0b000100,
+ Sanitize = 0b00100,
// Whether or not any player builders within need to produce new players
- PlayerBuildersDirty = 0b001000,
- // If NgClass is present (or some other class handler) then it will handle the map expressions and
- // initial classes
- OnlyProcessSingleClasses = 0b010000,
+ PlayerBuildersDirty = 0b01000,
// The max amount of bits used to represent these configuration values
- BindingAllocationLocked = 0b100000,
- BitCountSize = 6,
- // There are only six bits here
- BitMask = 0b111111
+ 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` */
@@ -482,9 +628,9 @@ export const enum StylingIndex {
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)
- CachedClassValueOrInitialClassString = 6,
+ CachedMultiClasses = 6,
// Position of where the last string-based CSS class value was stored
- CachedStyleValue = 7,
+ 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,
diff --git a/packages/core/src/render3/styling/class_and_style_bindings.ts b/packages/core/src/render3/styling/class_and_style_bindings.ts
index e1d5325ea9..3123eceec4 100644
--- a/packages/core/src/render3/styling/class_and_style_bindings.ts
+++ b/packages/core/src/render3/styling/class_and_style_bindings.ts
@@ -11,13 +11,13 @@ import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {AttributeMarker, TAttributes} from '../interfaces/node';
import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player';
import {RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
-import {DirectiveOwnerAndPlayerBuilderIndex, DirectiveRegistryValues, DirectiveRegistryValuesIndex, InitialStylingValues, InitialStylingValuesIndex, SinglePropOffsetValues, SinglePropOffsetValuesIndex, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
+import {DirectiveOwnerAndPlayerBuilderIndex, DirectiveRegistryValues, DirectiveRegistryValuesIndex, InitialStylingValues, InitialStylingValuesIndex, MapBasedOffsetValues, MapBasedOffsetValuesIndex, SinglePropOffsetValues, SinglePropOffsetValuesIndex, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
import {LView, RootContext} from '../interfaces/view';
import {NO_CHANGE} from '../tokens';
import {getRootContext} from '../util';
import {BoundPlayerFactory} from './player_factory';
-import {addPlayerInternal, allocPlayerContext, createEmptyStylingContext, getPlayerContext} from './util';
+import {addPlayerInternal, allocPlayerContext, allocateDirectiveIntoContext, createEmptyStylingContext, getPlayerContext} from './util';
@@ -30,8 +30,11 @@ import {addPlayerInternal, allocPlayerContext, createEmptyStylingContext, getPla
* [style.prop]="myPropValue"
* [class.name]="myClassValue"
*
+ * It also includes code that will allow style binding code to operate within host
+ * bindings for components/directives.
+ *
* There are many different ways in which these functions below are called. Please see
- * `interfaces/styles.ts` to get a better idea of how the styling algorithm works.
+ * `render3/interfaces/styling.ts` to get a better idea of how the styling algorithm works.
*/
@@ -39,12 +42,12 @@ import {addPlayerInternal, allocPlayerContext, createEmptyStylingContext, getPla
/**
* Creates a new StylingContext an fills it with the provided static styling attribute values.
*/
-export function initializeStaticContext(attrs: TAttributes) {
+export function initializeStaticContext(attrs: TAttributes): StylingContext {
const context = createEmptyStylingContext();
const initialClasses: InitialStylingValues = context[StylingIndex.InitialClassValuesPosition] =
- [null];
+ [null, null];
const initialStyles: InitialStylingValues = context[StylingIndex.InitialStyleValuesPosition] =
- [null];
+ [null, null];
// The attributes array has marker values (numbers) indicating what the subsequent
// values represent. When we encounter a number, we set the mode to that type of attribute.
@@ -72,18 +75,18 @@ export function initializeStaticContext(attrs: TAttributes) {
* @param context the existing styling context
* @param attrs an array of new static styling attributes that will be
* assigned to the context
- * @param directive the directive instance with which static data is associated with.
+ * @param directiveRef the directive instance with which static data is associated with.
*/
export function patchContextWithStaticAttrs(
- context: StylingContext, attrs: TAttributes, startingIndex: number, directive: any): void {
+ context: StylingContext, attrs: TAttributes, startingIndex: number, directiveRef: any): void {
// If the styling context has already been patched with the given directive's bindings,
// then there is no point in doing it again. The reason why this may happen (the directive
// styling being patched twice) is because the `stylingBinding` function is called each time
// an element is created (both within a template function and within directive host bindings).
const directives = context[StylingIndex.DirectiveRegistryPosition];
- if (getDirectiveRegistryValuesIndexOf(directives, directive) == -1) {
+ if (getDirectiveRegistryValuesIndexOf(directives, directiveRef) == -1) {
// this is a new directive which we have not seen yet.
- directives.push(directive, -1, false, null);
+ allocateDirectiveIntoContext(context, directiveRef);
let initialClasses: InitialStylingValues|null = null;
let initialStyles: InitialStylingValues|null = null;
@@ -134,16 +137,23 @@ function patchInitialStylingValue(
}
/**
- * Runs through the initial styling data present in the context and renders
+ * Runs through the initial style data present in the context and renders
* them via the renderer on the element.
*/
-export function renderInitialStylesAndClasses(
+export function renderInitialStyles(
+ element: RElement, context: StylingContext, renderer: Renderer3) {
+ const initialStyles = context[StylingIndex.InitialStyleValuesPosition];
+ renderInitialStylingValues(element, renderer, initialStyles, false);
+}
+
+/**
+ * Runs through the initial class data present in the context and renders
+ * them via the renderer on the element.
+ */
+export function renderInitialClasses(
element: RElement, context: StylingContext, renderer: Renderer3) {
const initialClasses = context[StylingIndex.InitialClassValuesPosition];
renderInitialStylingValues(element, renderer, initialClasses, true);
-
- const initialStyles = context[StylingIndex.InitialStyleValuesPosition];
- renderInitialStylingValues(element, renderer, initialStyles, false);
}
/**
@@ -190,8 +200,7 @@ export function allowNewBindingsForStylingContext(context: StylingContext): bool
*/
export function updateContextWithBindings(
context: StylingContext, directiveRef: any | null, classBindingNames?: string[] | null,
- styleBindingNames?: string[] | null, styleSanitizer?: StyleSanitizeFn | null,
- onlyProcessSingleClasses?: boolean) {
+ styleBindingNames?: string[] | null, styleSanitizer?: StyleSanitizeFn | null) {
if (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) return;
// this means the context has already been patched with the directive's bindings
@@ -201,6 +210,10 @@ export function updateContextWithBindings(
return;
}
+ if (styleBindingNames) {
+ styleBindingNames = hyphenateEntries(styleBindingNames);
+ }
+
// there are alot of variables being used below to track where in the context the new
// binding values will be placed. Because the context consists of multiple types of
// entries (single classes/styles and multi classes/styles) alot of the index positions
@@ -212,6 +225,9 @@ export function updateContextWithBindings(
const totalCurrentStyleBindings =
singlePropOffsetValues[SinglePropOffsetValuesIndex.StylesCountPosition];
+ const cachedClassMapValues = context[StylingIndex.CachedMultiClasses];
+ const cachedStyleMapValues = context[StylingIndex.CachedMultiStyles];
+
const classesOffset = totalCurrentClassBindings * StylingIndex.Size;
const stylesOffset = totalCurrentStyleBindings * StylingIndex.Size;
@@ -390,27 +406,90 @@ export function updateContextWithBindings(
singlePropOffsetValues[SinglePropOffsetValuesIndex.StylesCountPosition] =
totalCurrentStyleBindings + filteredStyleBindingNames.length;
+ // the map-based values also need to know how many entries got inserted
+ cachedClassMapValues[MapBasedOffsetValuesIndex.EntriesCountPosition] +=
+ filteredClassBindingNames.length;
+ cachedStyleMapValues[MapBasedOffsetValuesIndex.EntriesCountPosition] +=
+ filteredStyleBindingNames.length;
+ const newStylesSpaceAllocationSize = filteredStyleBindingNames.length * StylingIndex.Size;
+ const newClassesSpaceAllocationSize = filteredClassBindingNames.length * StylingIndex.Size;
+
+ // update the multi styles cache with a reference for the directive that was just inserted
+ const directiveMultiStylesStartIndex =
+ multiStylesStartIndex + totalCurrentStyleBindings * StylingIndex.Size;
+ const cachedStyleMapIndex = cachedStyleMapValues.length;
+
+ // this means that ONLY directive style styling (like ngStyle) was used
+ // therefore the root directive will still need to be filled in
+ if (directiveIndex > 0 &&
+ cachedStyleMapValues.length <= MapBasedOffsetValuesIndex.ValuesStartPosition) {
+ cachedStyleMapValues.push(0, directiveMultiStylesStartIndex, null, 0);
+ }
+
+ cachedStyleMapValues.push(
+ 0, directiveMultiStylesStartIndex, null, filteredStyleBindingNames.length);
+
+ for (let i = MapBasedOffsetValuesIndex.ValuesStartPosition; i < cachedStyleMapIndex;
+ i += MapBasedOffsetValuesIndex.Size) {
+ // multi values start after all the single values (which is also where classes are) in the
+ // context therefore the new class allocation size should be taken into account
+ cachedStyleMapValues[i + MapBasedOffsetValuesIndex.PositionStartOffset] +=
+ newClassesSpaceAllocationSize + newStylesSpaceAllocationSize;
+ }
+
+ // update the multi classes cache with a reference for the directive that was just inserted
+ const directiveMultiClassesStartIndex =
+ multiClassesStartIndex + totalCurrentClassBindings * StylingIndex.Size;
+ const cachedClassMapIndex = cachedClassMapValues.length;
+
+ // this means that ONLY directive class styling (like ngClass) was used
+ // therefore the root directive will still need to be filled in
+ if (directiveIndex > 0 &&
+ cachedClassMapValues.length <= MapBasedOffsetValuesIndex.ValuesStartPosition) {
+ cachedClassMapValues.push(0, directiveMultiClassesStartIndex, null, 0);
+ }
+
+ cachedClassMapValues.push(
+ 0, directiveMultiClassesStartIndex, null, filteredClassBindingNames.length);
+
+ for (let i = MapBasedOffsetValuesIndex.ValuesStartPosition; i < cachedClassMapIndex;
+ i += MapBasedOffsetValuesIndex.Size) {
+ // the reason why both the styles + classes space is allocated to the existing offsets is
+ // because the styles show up before the classes in the context and any new inserted
+ // styles will offset any existing class entries in the context (even if there are no
+ // new class entries added) also the reason why it's *2 is because both single + multi
+ // entries for each new style have been added in the context before the multi class values
+ // actually start
+ cachedClassMapValues[i + MapBasedOffsetValuesIndex.PositionStartOffset] +=
+ (newStylesSpaceAllocationSize * 2) + newClassesSpaceAllocationSize;
+ }
+
// there is no initial value flag for the master index since it doesn't
// reference an initial style value
- const masterFlag = pointers(0, 0, multiStylesStartIndex) |
- (onlyProcessSingleClasses ? StylingFlags.OnlyProcessSingleClasses : 0);
+ const masterFlag = pointers(0, 0, multiStylesStartIndex);
setFlag(context, StylingIndex.MasterFlagPosition, masterFlag);
}
/**
* Searches through the existing registry of directives
*/
-function findOrPatchDirectiveIntoRegistry(
+export function findOrPatchDirectiveIntoRegistry(
context: StylingContext, directiveRef: any, styleSanitizer?: StyleSanitizeFn | null) {
const directiveRefs = context[StylingIndex.DirectiveRegistryPosition];
const nextOffsetInsertionIndex = context[StylingIndex.SinglePropOffsetPositions].length;
let directiveIndex: number;
- const detectedIndex = getDirectiveRegistryValuesIndexOf(directiveRefs, directiveRef);
+ let detectedIndex = getDirectiveRegistryValuesIndexOf(directiveRefs, directiveRef);
if (detectedIndex === -1) {
+ detectedIndex = directiveRefs.length;
directiveIndex = directiveRefs.length / DirectiveRegistryValuesIndex.Size;
- directiveRefs.push(directiveRef, nextOffsetInsertionIndex, false, styleSanitizer || null);
+
+ allocateDirectiveIntoContext(context, directiveRef);
+ directiveRefs[detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] =
+ nextOffsetInsertionIndex;
+ directiveRefs[detectedIndex + DirectiveRegistryValuesIndex.StyleSanitizerOffset] =
+ styleSanitizer || null;
} else {
const singlePropStartPosition =
detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset;
@@ -446,27 +525,53 @@ function getMatchingBindingIndex(
}
/**
- * Sets and resolves all `multi` styling on an `StylingContext` so that they can be
- * applied to the element once `renderStyling` is called.
+ * Registers the provided multi styling (`[style]` and `[class]`) values to the context.
*
- * All missing styles/class (any values that are not provided in the new `styles`
- * or `classes` params) will resolve to `null` within their respective positions
- * in the context.
+ * This function will iterate over the provided `classesInput` and `stylesInput` map
+ * values and insert/update or remove them from the context at exactly the right
+ * spot.
+ *
+ * This function also takes in a directive which implies that the styling values will
+ * be evaluated for that directive with respect to any other styling that already exists
+ * on the context. When there are styles that conflict (e.g. say `ngStyle` and `[style]`
+ * both update the `width` property at the same time) then the styling algorithm code below
+ * will decide which one wins based on the directive styling prioritization mechanism. This
+ * mechanism is better explained in render3/interfaces/styling.ts#directives).
+ *
+ * This function will not render any styling values on screen, but is rather designed to
+ * prepare the context for that. `renderStyling` must be called afterwards to render any
+ * styling data that was set in this function (note that `updateClassProp` and
+ * `updateStyleProp` are designed to be run after this function is run).
*
* @param context The styling context that will be updated with the
* newly provided style values.
* @param classesInput The key/value map of CSS class names that will be used for the update.
* @param stylesInput The key/value map of CSS styles that will be used for the update.
+ * @param directiveRef an optional reference to the directive responsible
+ * for this binding change. If present then style binding will only
+ * actualize if the directive has ownership over this binding
+ * (see styling.ts#directives for more information about the algorithm).
*/
export function updateStylingMap(
context: StylingContext, classesInput: {[key: string]: any} | string |
- BoundPlayerFactory| NO_CHANGE | null,
- stylesInput?: {[key: string]: any} | BoundPlayerFactory| NO_CHANGE |
- null,
+ BoundPlayerFactory| null,
+ stylesInput?: {[key: string]: any} | BoundPlayerFactory| null,
directiveRef?: any): void {
- stylesInput = stylesInput || null;
-
const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null);
+
+ classesInput = classesInput || null;
+ stylesInput = stylesInput || null;
+ const ignoreAllClassUpdates = isMultiValueCacheHit(context, true, directiveIndex, classesInput);
+ const ignoreAllStyleUpdates = isMultiValueCacheHit(context, false, directiveIndex, stylesInput);
+
+ // early exit (this is what's done to avoid using ctx.bind() to cache the value)
+ if (ignoreAllClassUpdates && ignoreAllStyleUpdates) return;
+
+ classesInput =
+ classesInput === NO_CHANGE ? readCachedMapValue(context, true, directiveIndex) : classesInput;
+ stylesInput =
+ stylesInput === NO_CHANGE ? readCachedMapValue(context, false, directiveIndex) : stylesInput;
+
const element = context[StylingIndex.ElementPosition] !as HTMLElement;
const classesPlayerBuilder = classesInput instanceof BoundPlayerFactory ?
new ClassAndStylePlayerBuilder(classesInput as any, element, BindingType.Class) :
@@ -479,15 +584,6 @@ export function updateStylingMap(
(classesInput as BoundPlayerFactory<{[key: string]: any}|string>) !.value :
classesInput;
const stylesValue = stylesPlayerBuilder ? stylesInput !.value : stylesInput;
- // early exit (this is what's done to avoid using ctx.bind() to cache the value)
- const ignoreAllClassUpdates = limitToSingleClasses(context) || classesValue === NO_CHANGE ||
- classesValue === context[StylingIndex.CachedClassValueOrInitialClassString];
- const ignoreAllStyleUpdates =
- stylesValue === NO_CHANGE || stylesValue === context[StylingIndex.CachedStyleValue];
- if (ignoreAllClassUpdates && ignoreAllStyleUpdates) return;
-
- context[StylingIndex.CachedClassValueOrInitialClassString] = classesValue;
- context[StylingIndex.CachedStyleValue] = stylesValue;
let classNames: string[] = EMPTY_ARRAY;
let applyAllClasses = false;
@@ -522,150 +618,27 @@ export function updateStylingMap(
}
}
- const classes = (classesValue || EMPTY_OBJ) as{[key: string]: any};
- const styleProps = stylesValue ? Object.keys(stylesValue) : EMPTY_ARRAY;
- const styles = stylesValue || EMPTY_OBJ;
+ const multiStylesStartIndex = getMultiStylesStartIndex(context);
+ let multiClassesStartIndex = getMultiClassStartIndex(context);
+ let multiClassesEndIndex = context.length;
- const classesStartIndex = styleProps.length;
- let multiStartIndex = getMultiStartIndex(context);
-
- let dirty = false;
- let ctxIndex = multiStartIndex;
-
- let propIndex = 0;
- const propLimit = styleProps.length + classNames.length;
-
- // the main loop here will try and figure out how the shape of the provided
- // styles differ with respect to the context. Later if the context/styles/classes
- // are off-balance then they will be dealt in another loop after this one
- while (ctxIndex < context.length && propIndex < propLimit) {
- const isClassBased = propIndex >= classesStartIndex;
- const processValue =
- (!isClassBased && !ignoreAllStyleUpdates) || (isClassBased && !ignoreAllClassUpdates);
-
- // when there is a cache-hit for a string-based class then we should
- // avoid doing any work diffing any of the changes
- if (processValue) {
- const adjustedPropIndex = isClassBased ? propIndex - classesStartIndex : propIndex;
- const newProp: string =
- isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex];
- const newValue: string|boolean =
- isClassBased ? (applyAllClasses ? true : classes[newProp]) : styles[newProp];
- const playerBuilderIndex =
- isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex;
-
- const prop = getProp(context, ctxIndex);
- if (prop === newProp) {
- const value = getValue(context, ctxIndex);
- const flag = getPointers(context, ctxIndex);
- setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex);
-
- if (hasValueChanged(flag, value, newValue)) {
- setValue(context, ctxIndex, newValue);
- playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex;
-
- const initialValue = getInitialValue(context, flag);
-
- // SKIP IF INITIAL CHECK
- // If the former `value` is `null` then it means that an initial value
- // could be being rendered on screen. If that is the case then there is
- // no point in updating the value incase it matches. In other words if the
- // new value is the exact same as the previously rendered value (which
- // happens to be the initial value) then do nothing.
- if (value != null || hasValueChanged(flag, initialValue, newValue)) {
- setDirty(context, ctxIndex, true);
- dirty = true;
- }
- }
- } else {
- const indexOfEntry = findEntryPositionByProp(context, newProp, ctxIndex);
- if (indexOfEntry > 0) {
- // it was found at a later point ... just swap the values
- const valueToCompare = getValue(context, indexOfEntry);
- const flagToCompare = getPointers(context, indexOfEntry);
- swapMultiContextEntries(context, ctxIndex, indexOfEntry);
- if (hasValueChanged(flagToCompare, valueToCompare, newValue)) {
- const initialValue = getInitialValue(context, flagToCompare);
- setValue(context, ctxIndex, newValue);
-
- // same if statement logic as above (look for SKIP IF INITIAL CHECK).
- if (valueToCompare != null || hasValueChanged(flagToCompare, initialValue, newValue)) {
- setDirty(context, ctxIndex, true);
- playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex;
- dirty = true;
- }
- }
- } else {
- // we only care to do this if the insertion is in the middle
- const newFlag = prepareInitialFlag(
- context, newProp, isClassBased, getStyleSanitizer(context, directiveIndex));
- playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex;
- insertNewMultiProperty(
- context, ctxIndex, isClassBased, newProp, newFlag, newValue, directiveIndex,
- playerBuilderIndex);
- dirty = true;
- }
- }
+ if (!ignoreAllStyleUpdates) {
+ const styleProps = stylesValue ? Object.keys(stylesValue) : EMPTY_ARRAY;
+ const styles = stylesValue || EMPTY_OBJ;
+ const totalNewEntries = patchStylingMapIntoContext(
+ context, directiveIndex, stylesPlayerBuilderIndex, multiStylesStartIndex,
+ multiClassesStartIndex, styleProps, styles, stylesInput, false);
+ if (totalNewEntries) {
+ multiClassesStartIndex += totalNewEntries * StylingIndex.Size;
+ multiClassesEndIndex += totalNewEntries * StylingIndex.Size;
}
-
- ctxIndex += StylingIndex.Size;
- propIndex++;
}
- // this means that there are left-over values in the context that
- // were not included in the provided styles/classes and in this
- // case the goal is to "remove" them from the context (by nullifying)
- while (ctxIndex < context.length) {
- const flag = getPointers(context, ctxIndex);
- const isClassBased = (flag & StylingFlags.Class) === StylingFlags.Class;
- const processValue =
- (!isClassBased && !ignoreAllStyleUpdates) || (isClassBased && !ignoreAllClassUpdates);
- if (processValue) {
- const value = getValue(context, ctxIndex);
- const doRemoveValue = valueExists(value, isClassBased);
- if (doRemoveValue) {
- setDirty(context, ctxIndex, true);
- setValue(context, ctxIndex, null);
-
- // we keep the player factory the same so that the `nulled` value can
- // be instructed into the player because removing a style and/or a class
- // is a valid animation player instruction.
- const playerBuilderIndex =
- isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex;
- setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex);
- dirty = true;
- }
- }
- ctxIndex += StylingIndex.Size;
- }
-
- // this means that there are left-over properties in the context that
- // were not detected in the context during the loop above. In that
- // case we want to add the new entries into the list
- const sanitizer = getStyleSanitizer(context, directiveIndex);
- while (propIndex < propLimit) {
- const isClassBased = propIndex >= classesStartIndex;
- const processValue =
- (!isClassBased && !ignoreAllStyleUpdates) || (isClassBased && !ignoreAllClassUpdates);
- if (processValue) {
- const adjustedPropIndex = isClassBased ? propIndex - classesStartIndex : propIndex;
- const prop = isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex];
- const value: string|boolean =
- isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop];
- const flag = prepareInitialFlag(context, prop, isClassBased, sanitizer) | StylingFlags.Dirty;
- const playerBuilderIndex =
- isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex;
- const ctxIndex = context.length;
- context.push(flag, prop, value, 0);
- setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex);
- dirty = true;
- }
- propIndex++;
- }
-
- if (dirty) {
- setContextDirty(context, true);
- setDirectiveDirty(context, directiveIndex, true);
+ if (!ignoreAllClassUpdates) {
+ const classes = (classesValue || EMPTY_OBJ) as{[key: string]: any};
+ patchStylingMapIntoContext(
+ context, directiveIndex, classesPlayerBuilderIndex, multiClassesStartIndex,
+ multiClassesEndIndex, classNames, applyAllClasses || classes, classesInput, true);
}
if (playerBuildersAreDirty) {
@@ -674,18 +647,275 @@ export function updateStylingMap(
}
/**
- * This method will toggle the referenced CSS class (by the provided index)
- * within the given context.
+ * Applies the given multi styling (styles or classes) values to the context.
+ *
+ * The styling algorithm code that applies multi-level styling (things like `[style]` and `[class]`
+ * values) resides here.
+ *
+ * Because this function understands that multiple directives may all write to the `[style]` and
+ * `[class]` bindings (through host bindings), it relies of each directive applying its binding
+ * value in order. This means that a directive like `classADirective` will always fire before
+ * `classBDirective` and therefore its styling values (classes and styles) will always be evaluated
+ * in the same order. Because of this consistent ordering, the first directive has a higher priority
+ * than the second one. It is with this prioritzation mechanism that the styling algorithm knows how
+ * to merge and apply redudant styling properties.
+ *
+ * The function itself applies the key/value entries (or an array of keys) to
+ * the context in the following steps.
+ *
+ * STEP 1:
+ * First check to see what properties are already set and in use by another directive in the
+ * context (e.g. `ngClass` set the `width` value and `[style.width]="w"` in a directive is
+ * attempting to set it as well).
+ *
+ * STEP 2:
+ * All remaining properties (that were not set prior to this directive) are now updated in
+ * the context. Any new properties are inserted exactly at their spot in the context and any
+ * previously set properties are shifted to exactly where the cursor sits while iterating over
+ * the context. The end result is a balanced context that includes the exact ordering of the
+ * styling properties/values for the provided input from the directive.
+ *
+ * STEP 3:
+ * Any unmatched properties in the context that belong to the directive are set to null
+ *
+ * Once the updating phase is done, then the algorithm will decide whether or not to flag the
+ * follow-up directives (the directives that will pass in their styling values) depending on if
+ * the "shape" of the multi-value map has changed (either if any keys are removed or added or
+ * if there are any new `null` values). If any follow-up directives are flagged as dirty then the
+ * algorithm will run again for them. Otherwise if the shape did not change then any follow-up
+ * directives will not run (so long as their binding values stay the same).
+ *
+ * @returns the total amount of new slots that were allocated into the context due to new styling
+ * properties that were detected.
+ */
+function patchStylingMapIntoContext(
+ context: StylingContext, directiveIndex: number, playerBuilderIndex: number, ctxStart: number,
+ ctxEnd: number, props: (string | null)[], values: {[key: string]: any} | true, cacheValue: any,
+ entryIsClassBased: boolean): number {
+ let dirty = false;
+
+ const cacheIndex = MapBasedOffsetValuesIndex.ValuesStartPosition +
+ directiveIndex * MapBasedOffsetValuesIndex.Size;
+
+ // the cachedValues array is the registry of all multi style values (map values). Each
+ // value is stored (cached) each time is updated.
+ const cachedValues =
+ context[entryIsClassBased ? StylingIndex.CachedMultiClasses : StylingIndex.CachedMultiStyles];
+
+ // this is the index in which this directive has ownership access to write to this
+ // value (anything before is owned by a previous directive that is more important)
+ const ownershipValuesStartIndex =
+ cachedValues[cacheIndex + MapBasedOffsetValuesIndex.PositionStartOffset];
+
+ const existingCachedValue = cachedValues[cacheIndex + MapBasedOffsetValuesIndex.ValueOffset];
+ const existingCachedValueCount =
+ cachedValues[cacheIndex + MapBasedOffsetValuesIndex.ValueCountOffset];
+ const existingCachedValueIsDirty =
+ cachedValues[cacheIndex + MapBasedOffsetValuesIndex.DirtyFlagOffset] === 1;
+
+ // A shape change means the provided map value has either removed or added new properties
+ // compared to what were in the last time. If a shape change occurs then it means that all
+ // follow-up multi-styling entries are obsolete and will be examined again when CD runs
+ // them. If a shape change has not occurred then there is no reason to check any other
+ // directive values if their identity has not changed. If a previous directive set this
+ // value as dirty (because its own shape changed) then this means that the object has been
+ // offset to a different area in the context. Because its value has been offset then it
+ // can't write to a region that it wrote to before (which may have been apart of another
+ // directive) and therefore its shape changes too.
+ let valuesEntryShapeChange =
+ existingCachedValueIsDirty || ((!existingCachedValue && cacheValue) ? true : false);
+
+ let totalUniqueValues = 0;
+ let totalNewAllocatedSlots = 0;
+
+ // this is a trick to avoid building {key:value} map where all the values
+ // are `true` (this happens when a className string is provided instead of a
+ // map as an input value to this styling algorithm)
+ const applyAllProps = values === true;
+
+ // STEP 1:
+ // loop through the earlier directives and figure out if any properties here will be placed
+ // in their area (this happens when the value is null because the earlier directive erased it).
+ let ctxIndex = ctxStart;
+ let totalRemainingProperties = props.length;
+ while (ctxIndex < ownershipValuesStartIndex) {
+ const currentProp = getProp(context, ctxIndex);
+ if (totalRemainingProperties) {
+ for (let i = 0; i < props.length; i++) {
+ const mapProp = props[i];
+ const normalizedProp = mapProp ? (entryIsClassBased ? mapProp : hyphenate(mapProp)) : null;
+ if (normalizedProp && currentProp === normalizedProp) {
+ const currentValue = getValue(context, ctxIndex);
+ const currentDirectiveIndex = getDirectiveIndexFromEntry(context, ctxIndex);
+ const value = applyAllProps ? true : (values as{[key: string]: any})[normalizedProp];
+ const currentFlag = getPointers(context, ctxIndex);
+ if (hasValueChanged(currentFlag, currentValue, value) &&
+ allowValueChange(currentValue, value, currentDirectiveIndex, directiveIndex)) {
+ setValue(context, ctxIndex, value);
+ setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex);
+ if (hasInitialValueChanged(context, currentFlag, value)) {
+ setDirty(context, ctxIndex, true);
+ dirty = true;
+ }
+ }
+ props[i] = null;
+ totalRemainingProperties--;
+ break;
+ }
+ }
+ }
+ ctxIndex += StylingIndex.Size;
+ }
+
+ // STEP 2:
+ // apply the left over properties to the context in the correct order.
+ if (totalRemainingProperties) {
+ const sanitizer = entryIsClassBased ? null : getStyleSanitizer(context, directiveIndex);
+ propertiesLoop: for (let i = 0; i < props.length; i++) {
+ const mapProp = props[i];
+
+ if (!mapProp) {
+ // this is an early exit incase a value was already encountered above in the
+ // previous loop (which means that the property was applied or rejected)
+ continue;
+ }
+
+ const value = applyAllProps ? true : (values as{[key: string]: any})[mapProp];
+ const normalizedProp = entryIsClassBased ? mapProp : hyphenate(mapProp);
+ const isInsideOwnershipArea = ctxIndex >= ownershipValuesStartIndex;
+
+ for (let j = ctxIndex; j < ctxEnd; j += StylingIndex.Size) {
+ const distantCtxProp = getProp(context, j);
+ if (distantCtxProp === normalizedProp) {
+ const distantCtxDirectiveIndex = getDirectiveIndexFromEntry(context, j);
+ const distantCtxPlayerBuilderIndex = getPlayerBuilderIndex(context, j);
+ const distantCtxValue = getValue(context, j);
+ const distantCtxFlag = getPointers(context, j);
+
+ if (allowValueChange(distantCtxValue, value, distantCtxDirectiveIndex, directiveIndex)) {
+ // even if the entry isn't updated (by value or directiveIndex) then
+ // it should still be moved over to the correct spot in the array so
+ // the iteration loop is tighter.
+ if (isInsideOwnershipArea) {
+ swapMultiContextEntries(context, ctxIndex, j);
+ totalUniqueValues++;
+ }
+
+ if (hasValueChanged(distantCtxFlag, distantCtxValue, value)) {
+ if (value === null || value === undefined && value !== distantCtxValue) {
+ valuesEntryShapeChange = true;
+ }
+
+ setValue(context, ctxIndex, value);
+
+ // SKIP IF INITIAL CHECK
+ // If the former `value` is `null` then it means that an initial value
+ // could be being rendered on screen. If that is the case then there is
+ // no point in updating the value incase it matches. In other words if the
+ // new value is the exact same as the previously rendered value (which
+ // happens to be the initial value) then do nothing.
+ if (distantCtxValue !== null ||
+ hasInitialValueChanged(context, distantCtxFlag, value)) {
+ setDirty(context, ctxIndex, true);
+ dirty = true;
+ }
+ }
+
+ if (distantCtxDirectiveIndex !== directiveIndex ||
+ playerBuilderIndex !== distantCtxPlayerBuilderIndex) {
+ setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex);
+ }
+ }
+
+ ctxIndex += StylingIndex.Size;
+ continue propertiesLoop;
+ }
+ }
+
+ // fallback case ... value not found at all in the context
+ if (value != null) {
+ valuesEntryShapeChange = true;
+ totalUniqueValues++;
+ const flag = prepareInitialFlag(context, normalizedProp, entryIsClassBased, sanitizer) |
+ StylingFlags.Dirty;
+
+ const insertionIndex = isInsideOwnershipArea ?
+ ctxIndex :
+ (ownershipValuesStartIndex + totalNewAllocatedSlots * StylingIndex.Size);
+ insertNewMultiProperty(
+ context, insertionIndex, entryIsClassBased, normalizedProp, flag, value, directiveIndex,
+ playerBuilderIndex);
+
+ totalNewAllocatedSlots++;
+ ctxEnd += StylingIndex.Size;
+ ctxIndex += StylingIndex.Size;
+
+ dirty = true;
+ }
+ }
+ }
+
+ // STEP 3:
+ // Remove (nullify) any existing entries in the context that were not apart of the
+ // map input value that was passed into this algorithm for this directive.
+ while (ctxIndex < ctxEnd) {
+ valuesEntryShapeChange = true; // some values are missing
+ const ctxValue = getValue(context, ctxIndex);
+ const ctxFlag = getPointers(context, ctxIndex);
+ if (ctxValue != null) {
+ valuesEntryShapeChange = true;
+ }
+ if (hasValueChanged(ctxFlag, ctxValue, null)) {
+ setValue(context, ctxIndex, null);
+ // only if the initial value is falsy then
+ if (hasInitialValueChanged(context, ctxFlag, ctxValue)) {
+ setDirty(context, ctxIndex, true);
+ dirty = true;
+ }
+ setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex);
+ }
+ ctxIndex += StylingIndex.Size;
+ }
+
+ // Because the object shape has changed, this means that all follow-up directives will need to
+ // reapply their values into the object. For this to happen, the cached array needs to be updated
+ // with dirty flags so that follow-up calls to `updateStylingMap` will reapply their styling code.
+ // the reapplication of styling code within the context will reshape it and update the offset
+ // values (also follow-up directives can write new values incase earlier directives set anything
+ // to null due to removals or falsy values).
+ valuesEntryShapeChange = valuesEntryShapeChange || existingCachedValueCount !== totalUniqueValues;
+ updateCachedMapValue(
+ context, directiveIndex, entryIsClassBased, cacheValue, ownershipValuesStartIndex, ctxEnd,
+ totalUniqueValues, valuesEntryShapeChange);
+
+ if (dirty) {
+ setContextDirty(context, true);
+ setDirectiveDirty(context, directiveIndex, true);
+ }
+
+ return totalNewAllocatedSlots;
+}
+
+/**
+ * Sets and resolves a single class value on the provided `StylingContext` so
+ * that they can be applied to the element once `renderStyling` is called.
*
* @param context The styling context that will be updated with the
* newly provided class value.
* @param offset The index of the CSS class which is being updated.
* @param addOrRemove Whether or not to add or remove the CSS class
+ * @param directiveRef an optional reference to the directive responsible
+ * for this binding change. If present then style binding will only
+ * actualize if the directive has ownership over this binding
+ * (see styling.ts#directives for more information about the algorithm).
+ * @param forceOverride whether or not to skip all directive prioritization
+ * and just apply the value regardless.
*/
export function updateClassProp(
- context: StylingContext, offset: number, addOrRemove: boolean | BoundPlayerFactory,
- directiveRef?: any): void {
- _updateSingleStylingValue(context, offset, addOrRemove, true, directiveRef);
+ context: StylingContext, offset: number,
+ input: boolean | BoundPlayerFactory| null, directiveRef?: any,
+ forceOverride?: boolean): void {
+ updateSingleStylingValue(context, offset, input, true, directiveRef, forceOverride);
}
/**
@@ -705,18 +935,20 @@ export function updateClassProp(
* for this binding change. If present then style binding will only
* actualize if the directive has ownership over this binding
* (see styling.ts#directives for more information about the algorithm).
+ * @param forceOverride whether or not to skip all directive prioritization
+ * and just apply the value regardless.
*/
export function updateStyleProp(
context: StylingContext, offset: number,
- input: string | boolean | null | BoundPlayerFactory,
- directiveRef?: any): void {
- _updateSingleStylingValue(context, offset, input, false, directiveRef);
+ input: string | boolean | null | BoundPlayerFactory, directiveRef?: any,
+ forceOverride?: boolean): void {
+ updateSingleStylingValue(context, offset, input, false, directiveRef, forceOverride);
}
-function _updateSingleStylingValue(
+function updateSingleStylingValue(
context: StylingContext, offset: number,
input: string | boolean | null | BoundPlayerFactory, isClassBased: boolean,
- directiveRef: any): void {
+ directiveRef: any, forceOverride?: boolean): void {
const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null);
const singleIndex = getSinglePropIndexValue(context, directiveIndex, offset, isClassBased);
const currValue = getValue(context, singleIndex);
@@ -725,7 +957,7 @@ function _updateSingleStylingValue(
const value: string|boolean|null = (input instanceof BoundPlayerFactory) ? input.value : input;
if (hasValueChanged(currFlag, currValue, value) &&
- allowValueChange(currValue, value, currDirective, directiveIndex)) {
+ (forceOverride || allowValueChange(currValue, value, currDirective, directiveIndex))) {
const isClassBased = (currFlag & StylingFlags.Class) === StylingFlags.Class;
const element = context[StylingIndex.ElementPosition] !as HTMLElement;
const playerBuilder = input instanceof BoundPlayerFactory ?
@@ -816,8 +1048,7 @@ export function renderStyling(
const flushPlayerBuilders: any =
context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty;
const native = context[StylingIndex.ElementPosition] !;
- const multiStartIndex = getMultiStartIndex(context);
- const onlySingleClasses = limitToSingleClasses(context);
+ const multiStartIndex = getMultiStylesStartIndex(context);
let stillDirty = false;
for (let i = StylingIndex.SingleStylesStartPosition; i < context.length;
@@ -838,7 +1069,6 @@ export function renderStyling(
const playerBuilder = getPlayerBuilder(context, i);
const isClassBased = flag & StylingFlags.Class ? true : false;
const isInSingleRegion = i < multiStartIndex;
- const readInitialValue = !isClassBased || !onlySingleClasses;
let valueToApply: string|boolean|null = value;
@@ -859,7 +1089,7 @@ export function renderStyling(
// classes are turned off and should therefore defer to their initial values)
// Note that we ignore class-based deferals because otherwise a class can never
// be removed in the case that it exists as true in the initial classes list...
- if (!isClassBased && !valueExists(valueToApply, isClassBased) && readInitialValue) {
+ if (!valueExists(valueToApply, isClassBased)) {
valueToApply = getInitialValue(context, flag);
}
@@ -922,6 +1152,8 @@ export function renderStyling(
}
/**
+ * Assigns a style value to a style property for the given element.
+ *
* This function renders a given CSS prop/value entry using the
* provided renderer. If a `store` value is provided then
* that will be used a render context instead of the provided
@@ -947,20 +1179,22 @@ export function setStyle(
}
} else if (value) {
value = value.toString(); // opacity, z-index and flexbox all have number values which may not
- // assign as numbers
+ // assign as numbers
ngDevMode && ngDevMode.rendererSetStyle++;
isProceduralRenderer(renderer) ?
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) :
- native.style[prop] = value;
+ native.style.setProperty(prop, value);
} else {
ngDevMode && ngDevMode.rendererRemoveStyle++;
isProceduralRenderer(renderer) ?
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) :
- native.style[prop] = '';
+ native.style.removeProperty(prop);
}
}
/**
+ * Adds/removes the provided className value to the provided element.
+ *
* This function renders a given CSS class value using the provided
* renderer (by adding or removing it from the provided element).
* If a `store` value is provided then that will be used a render
@@ -1059,6 +1293,20 @@ function getMultiStartIndex(context: StylingContext): number {
return getMultiOrSingleIndex(context[StylingIndex.MasterFlagPosition]) as number;
}
+function getMultiClassStartIndex(context: StylingContext): number {
+ const classCache = context[StylingIndex.CachedMultiClasses];
+ return classCache
+ [MapBasedOffsetValuesIndex.ValuesStartPosition +
+ MapBasedOffsetValuesIndex.PositionStartOffset];
+}
+
+function getMultiStylesStartIndex(context: StylingContext): number {
+ const stylesCache = context[StylingIndex.CachedMultiStyles];
+ return stylesCache
+ [MapBasedOffsetValuesIndex.ValuesStartPosition +
+ MapBasedOffsetValuesIndex.PositionStartOffset];
+}
+
function setProp(context: StylingContext, index: number, prop: string) {
context[index + StylingIndex.PropertyOffset] = prop;
}
@@ -1148,10 +1396,6 @@ export function isContextDirty(context: StylingContext): boolean {
return isDirty(context, StylingIndex.MasterFlagPosition);
}
-export function limitToSingleClasses(context: StylingContext) {
- return context[StylingIndex.MasterFlagPosition] & StylingFlags.OnlyProcessSingleClasses;
-}
-
export function setContextDirty(context: StylingContext, isDirtyYes: boolean): void {
setDirty(context, StylingIndex.MasterFlagPosition, isDirtyYes);
}
@@ -1164,23 +1408,14 @@ export function setContextPlayersDirty(context: StylingContext, isDirtyYes: bool
}
}
-function findEntryPositionByProp(
- context: StylingContext, prop: string, startIndex?: number): number {
- for (let i = (startIndex || 0) + StylingIndex.PropertyOffset; i < context.length;
- i += StylingIndex.Size) {
- const thisProp = context[i];
- if (thisProp == prop) {
- return i - StylingIndex.PropertyOffset;
- }
- }
- return -1;
-}
-
function swapMultiContextEntries(context: StylingContext, indexA: number, indexB: number) {
+ if (indexA === indexB) return;
+
const tmpValue = getValue(context, indexA);
const tmpProp = getProp(context, indexA);
const tmpFlag = getPointers(context, indexA);
const tmpPlayerBuilderIndex = getPlayerBuilderIndex(context, indexA);
+ const tmpDirectiveIndex = getDirectiveIndexFromEntry(context, indexA);
let flagA = tmpFlag;
let flagB = getPointers(context, indexB);
@@ -1203,13 +1438,13 @@ function swapMultiContextEntries(context: StylingContext, indexA: number, indexB
setProp(context, indexA, getProp(context, indexB));
setFlag(context, indexA, getPointers(context, indexB));
const playerIndexA = getPlayerBuilderIndex(context, indexB);
- const directiveIndexA = 0;
+ const directiveIndexA = getDirectiveIndexFromEntry(context, indexB);
setPlayerBuilderIndex(context, indexA, playerIndexA, directiveIndexA);
setValue(context, indexB, tmpValue);
setProp(context, indexB, tmpProp);
setFlag(context, indexB, tmpFlag);
- setPlayerBuilderIndex(context, indexB, tmpPlayerBuilderIndex, directiveIndexA);
+ setPlayerBuilderIndex(context, indexB, tmpPlayerBuilderIndex, tmpDirectiveIndex);
}
function updateSinglePointerValues(context: StylingContext, indexStartPosition: number) {
@@ -1248,9 +1483,6 @@ function insertNewMultiProperty(
}
function valueExists(value: string | null | boolean, isClassBased?: boolean) {
- if (isClassBased) {
- return value ? true : false;
- }
return value !== null;
}
@@ -1273,6 +1505,11 @@ function prepareInitialFlag(
return pointers(flag, initialIndex, 0);
}
+function hasInitialValueChanged(context: StylingContext, flag: number, newValue: any) {
+ const initialValue = getInitialValue(context, flag);
+ return !initialValue || hasValueChanged(flag, initialValue, newValue);
+}
+
function hasValueChanged(
flag: number, a: string | boolean | null, b: string | boolean | null): boolean {
const isClassBased = flag & StylingFlags.Class;
@@ -1336,12 +1573,11 @@ export interface LogSummary {
dynamicIndex: number; //
value: number; //
flags: {
- dirty: boolean; //
- class: boolean; //
- sanitize: boolean; //
- playerBuildersDirty: boolean; //
- onlyProcessSingleClasses: boolean; //
- bindingAllocationLocked: boolean; //
+ dirty: boolean; //
+ class: boolean; //
+ sanitize: boolean; //
+ playerBuildersDirty: boolean; //
+ bindingAllocationLocked: boolean; //
};
}
@@ -1379,7 +1615,6 @@ export function generateConfigSummary(source: number | StylingContext, index?: n
class: flag & StylingFlags.Class ? true : false,
sanitize: flag & StylingFlags.Sanitize ? true : false,
playerBuildersDirty: flag & StylingFlags.PlayerBuildersDirty ? true : false,
- onlyProcessSingleClasses: flag & StylingFlags.OnlyProcessSingleClasses ? true : false,
bindingAllocationLocked: flag & StylingFlags.BindingAllocationLocked ? true : false,
}
};
@@ -1502,9 +1737,9 @@ function allowValueChange(
// prioritization of directives enables the styling algorithm to decide if a style
// or class should be allowed to be updated/replaced incase an earlier directive
// already wrote to the exact same style-property or className value. In other words
- // ... this decides what to do if and when there is a collision.
- if (currentValue) {
- if (newValue) {
+ // this decides what to do if and when there is a collision.
+ if (currentValue != null) {
+ if (newValue != null) {
// if a directive index is lower than it always has priority over the
// previous directive's value...
return newDirectiveOwner <= currentDirectiveOwner;
@@ -1520,16 +1755,21 @@ function allowValueChange(
}
/**
- * This function is only designed to be called for `[class]` bindings when
- * `[ngClass]` (or something that uses `class` as an input) is present. Once
- * directive host bindings fully work for `[class]` and `[style]` inputs
- * then this can be deleted.
+ * Returns the className string of all the initial classes for the element.
+ *
+ * This function is designed to populate and cache all the static class
+ * values into a className string. The caching mechanism works by placing
+ * the completed className string into the initial values array into a
+ * dedicated slot. This will prevent the function from having to populate
+ * the string each time an element is created or matched.
+ *
+ * @returns the className string (e.g. `on active red`)
*/
export function getInitialClassNameValue(context: StylingContext): string {
- let className = context[StylingIndex.CachedClassValueOrInitialClassString] as string;
- if (className == null) {
+ const initialClassValues = context[StylingIndex.InitialClassValuesPosition];
+ let className = initialClassValues[InitialStylingValuesIndex.InitialClassesStringPosition];
+ if (className === null) {
className = '';
- const initialClassValues = context[StylingIndex.InitialClassValuesPosition];
for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < initialClassValues.length;
i += InitialStylingValuesIndex.Size) {
const isPresent = initialClassValues[i + 1];
@@ -1537,7 +1777,162 @@ export function getInitialClassNameValue(context: StylingContext): string {
className += (className.length ? ' ' : '') + initialClassValues[i];
}
}
- context[StylingIndex.CachedClassValueOrInitialClassString] = className;
+ initialClassValues[InitialStylingValuesIndex.InitialClassesStringPosition] = className;
}
return className;
}
+
+/**
+ * Returns the style string of all the initial styles for the element.
+ *
+ * This function is designed to populate and cache all the static style
+ * values into a style string. The caching mechanism works by placing
+ * the completed style string into the initial values array into a
+ * dedicated slot. This will prevent the function from having to populate
+ * the string each time an element is created or matched.
+ *
+ * @returns the style string (e.g. `width:100px;height:200px`)
+ */
+export function getInitialStyleStringValue(context: StylingContext): string {
+ const initialStyleValues = context[StylingIndex.InitialStyleValuesPosition];
+ let styleString = initialStyleValues[InitialStylingValuesIndex.InitialClassesStringPosition];
+ if (styleString === null) {
+ styleString = '';
+ for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < initialStyleValues.length;
+ i += InitialStylingValuesIndex.Size) {
+ const value = initialStyleValues[i + 1];
+ if (value !== null) {
+ styleString += (styleString.length ? ';' : '') + `${initialStyleValues[i]}:${value}`;
+ }
+ }
+ initialStyleValues[InitialStylingValuesIndex.InitialClassesStringPosition] = styleString;
+ }
+ return styleString;
+}
+
+/**
+ * Returns the current cached mutli-value for a given directiveIndex within the provided context.
+ */
+function readCachedMapValue(
+ context: StylingContext, entryIsClassBased: boolean, directiveIndex: number) {
+ const values: MapBasedOffsetValues =
+ context[entryIsClassBased ? StylingIndex.CachedMultiClasses : StylingIndex.CachedMultiStyles];
+ const index = MapBasedOffsetValuesIndex.ValuesStartPosition +
+ directiveIndex * MapBasedOffsetValuesIndex.Size;
+ return values[index + MapBasedOffsetValuesIndex.ValueOffset] || null;
+}
+
+/**
+ * Determines whether the provided multi styling value should be updated or not.
+ *
+ * Because `[style]` and `[class]` bindings rely on an identity change to occur before
+ * applying new values, the styling algorithm may not update an existing entry into
+ * the context if a previous directive's entry changed shape.
+ *
+ * This function will decide whether or not a value should be applied (if there is a
+ * cache miss) to the context based on the following rules:
+ *
+ * - If there is an identity change between the existing value and new value
+ * - If there is no existing value cached (first write)
+ * - If a previous directive flagged the existing cached value as dirty
+ */
+function isMultiValueCacheHit(
+ context: StylingContext, entryIsClassBased: boolean, directiveIndex: number,
+ newValue: any): boolean {
+ const indexOfCachedValues =
+ entryIsClassBased ? StylingIndex.CachedMultiClasses : StylingIndex.CachedMultiStyles;
+ const cachedValues = context[indexOfCachedValues] as MapBasedOffsetValues;
+ const index = MapBasedOffsetValuesIndex.ValuesStartPosition +
+ directiveIndex * MapBasedOffsetValuesIndex.Size;
+ if (cachedValues[index + MapBasedOffsetValuesIndex.DirtyFlagOffset]) return false;
+ return newValue === NO_CHANGE ||
+ readCachedMapValue(context, entryIsClassBased, directiveIndex) === newValue;
+}
+
+/**
+ * Updates the cached status of a multi-styling value in the context.
+ *
+ * The cached map array (which exists in the context) contains a manifest of
+ * each multi-styling entry (`[style]` and `[class]` entries) for the template
+ * as well as all directives.
+ *
+ * This function will update the cached status of the provided multi-style
+ * entry within the cache.
+ *
+ * When called, this function will update the following information:
+ * - The actual cached value (the raw value that was passed into `[style]` or `[class]`)
+ * - The total amount of unique styling entries that this value has written into the context
+ * - The exact position of where the multi styling entries start in the context for this binding
+ * - The dirty flag will be set to true
+ *
+ * If the `dirtyFutureValues` param is provided then it will update all future entries (binding
+ * entries that exist as apart of other directives) to be dirty as well. This will force the
+ * styling algorithm to reapply those values once change detection checks them (which will in
+ * turn cause the styling context to update itself and the correct styling values will be
+ * rendered on screen).
+ */
+function updateCachedMapValue(
+ context: StylingContext, directiveIndex: number, entryIsClassBased: boolean, cacheValue: any,
+ startPosition: number, endPosition: number, totalValues: number, dirtyFutureValues: boolean) {
+ const values =
+ context[entryIsClassBased ? StylingIndex.CachedMultiClasses : StylingIndex.CachedMultiStyles];
+
+ const index = MapBasedOffsetValuesIndex.ValuesStartPosition +
+ directiveIndex * MapBasedOffsetValuesIndex.Size;
+
+ // in the event that this is true we assume that future values are dirty and therefore
+ // will be checked again in the next CD cycle
+ if (dirtyFutureValues) {
+ const nextStartPosition = startPosition + totalValues * MapBasedOffsetValuesIndex.Size;
+ for (let i = index + MapBasedOffsetValuesIndex.Size; i < values.length;
+ i += MapBasedOffsetValuesIndex.Size) {
+ values[i + MapBasedOffsetValuesIndex.PositionStartOffset] = nextStartPosition;
+ values[i + MapBasedOffsetValuesIndex.DirtyFlagOffset] = 1;
+ }
+ }
+
+ values[index + MapBasedOffsetValuesIndex.DirtyFlagOffset] = 0;
+ values[index + MapBasedOffsetValuesIndex.PositionStartOffset] = startPosition;
+ values[index + MapBasedOffsetValuesIndex.ValueOffset] = cacheValue;
+ values[index + MapBasedOffsetValuesIndex.ValueCountOffset] = totalValues;
+
+ // the code below counts the total amount of styling values that exist in
+ // the context up until this directive. This value will be later used to
+ // update the cached value map's total counter value.
+ let totalStylingEntries = totalValues;
+ for (let i = MapBasedOffsetValuesIndex.ValuesStartPosition; i < index;
+ i += MapBasedOffsetValuesIndex.Size) {
+ totalStylingEntries += values[i + MapBasedOffsetValuesIndex.ValueCountOffset];
+ }
+
+ // because style values come before class values in the context this means
+ // that if any new values were inserted then the cache values array for
+ // classes is out of sync. The code below will update the offsets to point
+ // to their new values.
+ if (!entryIsClassBased) {
+ const classCache = context[StylingIndex.CachedMultiClasses];
+ const classesStartPosition = classCache
+ [MapBasedOffsetValuesIndex.ValuesStartPosition +
+ MapBasedOffsetValuesIndex.PositionStartOffset];
+ const diffInStartPosition = endPosition - classesStartPosition;
+ for (let i = MapBasedOffsetValuesIndex.ValuesStartPosition; i < classCache.length;
+ i += MapBasedOffsetValuesIndex.Size) {
+ classCache[i + MapBasedOffsetValuesIndex.PositionStartOffset] += diffInStartPosition;
+ }
+ }
+
+ values[MapBasedOffsetValuesIndex.EntriesCountPosition] = totalStylingEntries;
+}
+
+function hyphenateEntries(entries: string[]): string[] {
+ const newEntries: string[] = [];
+ for (let i = 0; i < entries.length; i++) {
+ newEntries.push(hyphenate(entries[i]));
+ }
+ return newEntries;
+}
+
+function hyphenate(value: string): string {
+ return value.replace(
+ /[a-z][A-Z]/g, match => `${match.charAt(0)}-${match.charAt(1).toLowerCase()}`);
+}
diff --git a/packages/core/src/render3/styling/util.ts b/packages/core/src/render3/styling/util.ts
index 5bc2c9441f..4d9ea60449 100644
--- a/packages/core/src/render3/styling/util.ts
+++ b/packages/core/src/render3/styling/util.ts
@@ -26,17 +26,24 @@ export function createEmptyStylingContext(
element?: RElement | null, sanitizer?: StyleSanitizeFn | null,
initialStyles?: InitialStylingValues | null,
initialClasses?: InitialStylingValues | null): StylingContext {
- return [
- 0, // MasterFlags
- [null, -1, false, sanitizer || null], // DirectiveRefs
- initialStyles || [null], // InitialStyles
- initialClasses || [null], // InitialClasses
- [0, 0], // SinglePropOffsets
- element || null, // Element
- null, // PreviousMultiClassValue
- null, // PreviousMultiStyleValue
- null, // PlayerContext
+ const context: StylingContext = [
+ 0, // MasterFlags
+ [] as any, // DirectiveRefs (this gets filled below)
+ initialStyles || [null, null], // InitialStyles
+ initialClasses || [null, null], // InitialClasses
+ [0, 0], // SinglePropOffsets
+ element || null, // Element
+ [0], // CachedMultiClassValue
+ [0], // CachedMultiStyleValue
+ null, // PlayerContext
];
+ allocateDirectiveIntoContext(context, null);
+ return context;
+}
+
+export function allocateDirectiveIntoContext(context: StylingContext, directiveRef: any | null) {
+ // this is a new directive which we have not seen yet.
+ context[StylingIndex.DirectiveRegistryPosition].push(directiveRef, -1, false, null);
}
/**
@@ -103,6 +110,34 @@ export function isAnimationProp(name: string): boolean {
return name[0] === ANIMATION_PROP_PREFIX;
}
+export function hasClassInput(tNode: TNode) {
+ return (tNode.flags & TNodeFlags.hasClassInput) !== 0;
+}
+
+export function hasStyleInput(tNode: TNode) {
+ return (tNode.flags & TNodeFlags.hasStyleInput) !== 0;
+}
+
+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 += (i ? ';' : '') + `${prop}:${styles[prop]}`;
+ }
+ }
+ return str;
+}
+
export function addPlayerInternal(
playerContext: PlayerContext, rootContext: RootContext, element: HTMLElement,
player: Player | null, playerContextIndex: number, ref?: any): boolean {
@@ -196,7 +231,3 @@ export function hasStyling(attrs: TAttributes): boolean {
}
return false;
}
-
-export function hasClassInput(tNode: TNode) {
- return tNode.flags & TNodeFlags.hasClassInput ? true : false;
-}
diff --git a/packages/core/test/bundling/animation_world/index.ts b/packages/core/test/bundling/animation_world/index.ts
index 001c8b3ea7..f4b3ae5f69 100644
--- a/packages/core/test/bundling/animation_world/index.ts
+++ b/packages/core/test/bundling/animation_world/index.ts
@@ -9,7 +9,7 @@
import '@angular/core/test/bundling/util/src/reflect_metadata';
import {CommonModule} from '@angular/common';
-import {Component, Directive, ElementRef, HostBinding, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core';
+import {Component, Directive, ElementRef, HostBinding, HostListener, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core';
@Directive({
selector: '[make-color-grey]',
@@ -33,6 +33,36 @@ class MakeColorGreyDirective {
toggle() { this._backgroundColor ? this.off() : this.on(); }
}
+@Component({selector: 'box-with-overridden-styles', template: '...'})
+class BoxWithOverriddenStylesComponent {
+ public active = false;
+
+ @HostBinding('style')
+ styles = {};
+
+ constructor() { this.onInActive(); }
+
+ @HostListener('click', ['$event'])
+ toggle() {
+ if (this.active) {
+ this.onInActive();
+ } else {
+ this.onActive();
+ }
+ markDirty(this);
+ }
+
+ onActive() {
+ this.active = true;
+ this.styles = {height: '500px', 'font-size': '200px', background: 'red'};
+ }
+
+ onInActive() {
+ this.active = false;
+ this.styles = {width: '200px', height: '500px', border: '10px solid black', background: 'grey'};
+ }
+}
+
@Component({
selector: 'animation-world',
template: `
@@ -48,7 +78,7 @@ class MakeColorGreyDirective {
class="record"
[style.transform]="item.active ? 'scale(1.5)' : 'none'"
[class]="makeClass(item)"
- style="border-radius: 10px"
+ style="border-radius: 10px"
[style]="styles"
[style.color]="item.value == 4 ? 'red' : null"
[style.background-color]="item.value == 4 ? 'white' : null"
@@ -56,6 +86,13 @@ class MakeColorGreyDirective {
{{ item.value }}
+
+
+
+
+
`,
})
class AnimationWorldComponent {
@@ -93,8 +130,10 @@ class AnimationWorldComponent {
}
}
-@NgModule(
- {declarations: [AnimationWorldComponent, MakeColorGreyDirective], imports: [CommonModule]})
+@NgModule({
+ declarations: [AnimationWorldComponent, MakeColorGreyDirective, BoxWithOverriddenStylesComponent],
+ imports: [CommonModule]
+})
class AnimationWorldModule {
}
diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
index 9cd68b01b9..2d81d2aa30 100644
--- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
@@ -170,6 +170,9 @@
{
"name": "allocStylingContext"
},
+ {
+ "name": "allocateDirectiveIntoContext"
+ },
{
"name": "appendChild"
},
@@ -335,6 +338,9 @@
{
"name": "getInitialClassNameValue"
},
+ {
+ "name": "getInitialStyleStringValue"
+ },
{
"name": "getInjectorIndex"
},
@@ -407,6 +413,9 @@
{
"name": "hasParentInjector"
},
+ {
+ "name": "hasStyleInput"
+ },
{
"name": "hasStyling"
},
@@ -555,7 +564,10 @@
"name": "renderEmbeddedTemplate"
},
{
- "name": "renderInitialStylesAndClasses"
+ "name": "renderInitialClasses"
+ },
+ {
+ "name": "renderInitialStyles"
},
{
"name": "renderInitialStylingValues"
diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json
index 5045c31c3a..896de5c568 100644
--- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json
@@ -44,9 +44,6 @@
{
"name": "HOST"
},
- {
- "name": "T_HOST"
- },
{
"name": "INJECTOR"
},
@@ -107,6 +104,9 @@
{
"name": "TVIEW"
},
+ {
+ "name": "T_HOST"
+ },
{
"name": "UnsubscriptionErrorImpl"
},
diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json
index 65fb980ad8..a4db8b2a37 100644
--- a/packages/core/test/bundling/todo/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json
@@ -362,9 +362,6 @@
{
"name": "_symbolIterator"
},
- {
- "name": "_updateSingleStylingValue"
- },
{
"name": "addComponentLogic"
},
@@ -386,6 +383,9 @@
{
"name": "allocStylingContext"
},
+ {
+ "name": "allocateDirectiveIntoContext"
+ },
{
"name": "allowValueChange"
},
@@ -419,6 +419,9 @@
{
"name": "bloomHashBitOrFactory"
},
+ {
+ "name": "booleanOrNull"
+ },
{
"name": "cacheMatchingLocalNames"
},
@@ -689,6 +692,9 @@
{
"name": "getInitialIndex"
},
+ {
+ "name": "getInitialStyleStringValue"
+ },
{
"name": "getInitialStylingValuesIndexOf"
},
@@ -720,7 +726,7 @@
"name": "getMultiOrSingleIndex"
},
{
- "name": "getMultiStartIndex"
+ "name": "getMultiStylesStartIndex"
},
{
"name": "getNativeAnchorNode"
@@ -839,6 +845,9 @@
{
"name": "hasPlayerBuilderChanged"
},
+ {
+ "name": "hasStyleInput"
+ },
{
"name": "hasStyling"
},
@@ -848,12 +857,21 @@
{
"name": "hasValueChanged"
},
+ {
+ "name": "hyphenate"
+ },
+ {
+ "name": "hyphenateEntries"
+ },
{
"name": "includeViewProviders"
},
{
"name": "increaseElementDepthCount"
},
+ {
+ "name": "initElementStyling"
+ },
{
"name": "initNodeFlags"
},
@@ -965,9 +983,6 @@
{
"name": "leaveView"
},
- {
- "name": "limitToSingleClasses"
- },
{
"name": "listener"
},
@@ -1104,7 +1119,10 @@
"name": "renderEmbeddedTemplate"
},
{
- "name": "renderInitialStylesAndClasses"
+ "name": "renderInitialClasses"
+ },
+ {
+ "name": "renderInitialStyles"
},
{
"name": "renderInitialStylingValues"
@@ -1256,6 +1274,9 @@
{
"name": "updateContextWithBindings"
},
+ {
+ "name": "updateSingleStylingValue"
+ },
{
"name": "valueExists"
},
diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts
index 4550a91a47..21371db81e 100644
--- a/packages/core/test/render3/integration_spec.ts
+++ b/packages/core/test/render3/integration_spec.ts
@@ -1656,6 +1656,19 @@ describe('render3 integration test', () => {
set klass(value: string) { this.classesVal = value; }
}
+ let mockStyleDirective: DirWithStyleDirective;
+ class DirWithStyleDirective {
+ static ngDirectiveDef = defineDirective({
+ type: DirWithStyleDirective,
+ selectors: [['', 'DirWithStyle', '']],
+ factory: () => mockStyleDirective = new DirWithStyleDirective(),
+ inputs: {'style': 'style'}
+ });
+
+ public stylesVal: string = '';
+ set style(value: string) { this.stylesVal = value; }
+ }
+
it('should delegate initial classes to a [class] input binding if present on a directive on the same element',
() => {
/**
@@ -1678,6 +1691,28 @@ describe('render3 integration test', () => {
expect(mockClassDirective !.classesVal).toEqual('apple orange banana');
});
+ it('should delegate initial styles to a [style] input binding if present on a directive on the same element',
+ () => {
+ /**
+ *
+ */
+ const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
+ if (rf & RenderFlags.Create) {
+ elementStart(
+ 0, 'div',
+ ['DirWithStyle', AttributeMarker.Styles, 'width', '100px', 'height', '200px']);
+ elementStyling();
+ elementEnd();
+ }
+ if (rf & RenderFlags.Update) {
+ elementStylingApply(0);
+ }
+ }, 1, 0, [DirWithStyleDirective]);
+
+ const fixture = new ComponentFixture(App);
+ expect(mockStyleDirective !.stylesVal).toEqual('width:100px;height:200px');
+ });
+
it('should update `[class]` and bindings in the provided directive if the input is matched',
() => {
/**
@@ -1699,6 +1734,27 @@ describe('render3 integration test', () => {
expect(mockClassDirective !.classesVal).toEqual('cucumber grape');
});
+ it('should update `[style]` and bindings in the provided directive if the input is matched',
+ () => {
+ /**
+ *
+ */
+ const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
+ if (rf & RenderFlags.Create) {
+ elementStart(0, 'div', ['DirWithStyle']);
+ elementStyling();
+ elementEnd();
+ }
+ if (rf & RenderFlags.Update) {
+ elementStylingMap(0, null, {width: '200px', height: '500px'});
+ elementStylingApply(0);
+ }
+ }, 1, 0, [DirWithStyleDirective]);
+
+ const fixture = new ComponentFixture(App);
+ expect(mockStyleDirective !.stylesVal).toEqual('width:200px;height:500px');
+ });
+
it('should apply initial styling to the element that contains the directive with host styling',
() => {
class DirWithInitialStyling {
@@ -1723,7 +1779,7 @@ describe('render3 integration test', () => {
/**
*
+ * style="color:black; font-size:200px">
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
@@ -1822,7 +1878,7 @@ describe('render3 integration test', () => {
expect(target.classList.contains('xyz')).toBeTruthy();
});
- it('should properly prioritize style binding collision when they exist on multiple directives',
+ it('should properly prioritize single style binding collisions when they exist on multiple directives',
() => {
let dir1Instance: Dir1WithStyle;
/**
@@ -1873,7 +1929,8 @@ describe('render3 integration test', () => {
}
/**
- *