`).
+ *
+ * If a map-based styling binding is detected by the compiler, the following
+ * AOT code is produced:
+ *
+ * ```typescript
+ * styleMap(ctx.styles); // styles = {key:value}
+ * classMap(ctx.classes); // classes = {key:value}|string
+ * ```
+ *
+ * If and when either of the instructions above are evaluated, then the code
+ * present in this file is included into the bundle. The mechanism used, to
+ * activate support for map-based bindings at runtime is possible via the
+ * `activeStylingMapFeature` function (which is also present in this file).
+ *
+ * # The Algorithm
+ * Whenever a map-based binding updates (which is when the identity of the
+ * map-value changes) then the map is iterated over and a `LStylingMap` array
+ * is produced. The `LStylingMap` instance is stored in the binding location
+ * where the `BINDING_INDEX` is situated when the `styleMap()` or `classMap()`
+ * instruction were called. Once the binding changes, then the internal `bitMask`
+ * value is marked as dirty.
+ *
+ * Styling values are applied once CD exits the element (which happens when
+ * the `select(n)` instruction is called or the template function exits). When
+ * this occurs, all prop-based bindings are applied. If a map-based binding is
+ * present then a special flushing function (called a sync function) is made
+ * available and it will be called each time a styling property is flushed.
+ *
+ * The flushing algorithm is designed to apply styling for a property (which is
+ * a CSS property or a className value) one by one. If map-based bindings
+ * are present, then the flushing algorithm will keep calling the maps styling
+ * sync function each time a property is visited. This way, the flushing
+ * behavior of map-based bindings will always be at the same property level
+ * as the current prop-based property being iterated over (because everything
+ * is alphabetically sorted).
+ *
+ * Let's imagine we have the following HTML template code:
+ *
+ * ```html
+ *
...
+ * ```
+ *
+ * When CD occurs, both the `[style]` and `[style.width]` bindings
+ * are evaluated. Then when the styles are flushed on screen, the
+ * following operations happen:
+ *
+ * 1. `[style.width]` is attempted to be written to the element.
+ *
+ * 2. Once that happens, the algorithm instructs the map-based
+ * entries (`[style]` in this case) to "catch up" and apply
+ * all values up to the `width` value. When this happens the
+ * `height` value is applied to the element (since it is
+ * alphabetically situated before the `width` property).
+ *
+ * 3. Since there are no more prop-based entries anymore, the
+ * loop exits and then, just before the flushing ends, it
+ * instructs all map-based bindings to "finish up" applying
+ * their values.
+ *
+ * 4. The only remaining value within the map-based entries is
+ * the `z-index` value (`width` got skipped because it was
+ * successfully applied via the prop-based `[style.width]`
+ * binding). Since all map-based entries are told to "finish up",
+ * the `z-index` value is iterated over and it is then applied
+ * to the element.
+ *
+ * The most important thing to take note of here is that prop-based
+ * bindings are evaluated in order alongside map-based bindings.
+ * This allows all styling across an element to be applied in O(n)
+ * time (a similar algorithm is that of the array merge algorithm
+ * in merge sort).
+ */
+export const syncStylingMap: SyncStylingMapsFn =
+ (context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
+ data: LStylingData, applyStylingFn: ApplyStylingFn, mode: StylingMapsSyncMode,
+ targetProp?: string | null, defaultValue?: string | null): boolean => {
+ let targetPropValueWasApplied = false;
+
+ // once the map-based styling code is activate it is never deactivated. For this reason a
+ // check to see if the current styling context has any map based bindings is required.
+ const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition);
+ if (totalMaps) {
+ let runTheSyncAlgorithm = true;
+ const loopUntilEnd = !targetProp;
+
+ // If the code is told to finish up (run until the end), but the mode
+ // hasn't been flagged to apply values (it only traverses values) then
+ // there is no point in iterating over the array because nothing will
+ // be applied to the element.
+ if (loopUntilEnd && (mode & ~StylingMapsSyncMode.ApplyAllValues)) {
+ runTheSyncAlgorithm = false;
+ targetPropValueWasApplied = true;
+ }
+
+ if (runTheSyncAlgorithm) {
+ targetPropValueWasApplied = innerSyncStylingMap(
+ context, renderer, element, data, applyStylingFn, mode, targetProp || null, 0,
+ defaultValue || null);
+ }
+
+ if (loopUntilEnd) {
+ resetSyncCursors();
+ }
+ }
+
+ return targetPropValueWasApplied;
+ };
+
+/**
+ * Recursive function designed to apply map-based styling to an element one map at a time.
+ *
+ * This function is designed to be called from the `syncStylingMap` function and will
+ * apply map-based styling data one map at a time to the provided `element`.
+ *
+ * This function is recursive and it will call itself if a follow-up map value is to be
+ * processed. To learn more about how the algorithm works, see `syncStylingMap`.
+ */
+function innerSyncStylingMap(
+ context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
+ data: LStylingData, applyStylingFn: ApplyStylingFn, mode: StylingMapsSyncMode,
+ targetProp: string | null, currentMapIndex: number, defaultValue: string | null): boolean {
+ let targetPropValueWasApplied = false;
+
+ const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition);
+ if (currentMapIndex < totalMaps) {
+ const bindingIndex = getBindingValue(
+ context, TStylingContextIndex.MapBindingsPosition, currentMapIndex) as number;
+ const lStylingMap = data[bindingIndex] as LStylingMap;
+
+ let cursor = getCurrentSyncCursor(currentMapIndex);
+ while (cursor < lStylingMap.length) {
+ const prop = getMapProp(lStylingMap, cursor);
+ const iteratedTooFar = targetProp && prop > targetProp;
+ const isTargetPropMatched = !iteratedTooFar && prop === targetProp;
+ const value = getMapValue(lStylingMap, cursor);
+ const valueIsDefined = isStylingValueDefined(value);
+
+ // the recursive code is designed to keep applying until
+ // it reaches or goes past the target prop. If and when
+ // this happens then it will stop processing values, but
+ // all other map values must also catch up to the same
+ // point. This is why a recursive call is still issued
+ // even if the code has iterated too far.
+ const innerMode =
+ iteratedTooFar ? mode : resolveInnerMapMode(mode, valueIsDefined, isTargetPropMatched);
+ const innerProp = iteratedTooFar ? targetProp : prop;
+ let valueApplied = innerSyncStylingMap(
+ context, renderer, element, data, applyStylingFn, innerMode, innerProp,
+ currentMapIndex + 1, defaultValue);
+
+ if (iteratedTooFar) {
+ break;
+ }
+
+ if (!valueApplied && isValueAllowedToBeApplied(mode, isTargetPropMatched)) {
+ const useDefault = isTargetPropMatched && !valueIsDefined;
+ const valueToApply = useDefault ? defaultValue : value;
+ const bindingIndexToApply = useDefault ? bindingIndex : null;
+ applyStylingFn(renderer, element, prop, valueToApply, bindingIndexToApply);
+ valueApplied = true;
+ }
+
+ targetPropValueWasApplied = valueApplied && isTargetPropMatched;
+ cursor += LStylingMapIndex.TupleSize;
+ }
+ setCurrentSyncCursor(currentMapIndex, cursor);
+ }
+
+ return targetPropValueWasApplied;
+}
+
+
+/**
+ * Enables support for map-based styling bindings (e.g. `[style]` and `[class]` bindings).
+ */
+export function activeStylingMapFeature() {
+ setStylingMapsSyncFn(syncStylingMap);
+}
+
+/**
+ * Used to determine the mode for the inner recursive call.
+ *
+ * If an inner map is iterated on then this is done so for one
+ * of two reasons:
+ *
+ * - The target property was detected and the inner map
+ * must now "catch up" (pointer-wise) up to where the current
+ * map's cursor is situated.
+ *
+ * - The target property was not detected in the current map
+ * and must be found in an inner map. This can only be allowed
+ * if the current map iteration is not set to skip the target
+ * property.
+ */
+function resolveInnerMapMode(
+ currentMode: number, valueIsDefined: boolean, isExactMatch: boolean): number {
+ let innerMode = currentMode;
+ if (!valueIsDefined && isExactMatch && !(currentMode & StylingMapsSyncMode.SkipTargetProp)) {
+ // case 1: set the mode to apply the targeted prop value if it
+ // ends up being encountered in another map value
+ innerMode |= StylingMapsSyncMode.ApplyTargetProp;
+ innerMode &= ~StylingMapsSyncMode.SkipTargetProp;
+ } else {
+ // case 2: set the mode to skip the targeted prop value if it
+ // ends up being encountered in another map value
+ innerMode |= StylingMapsSyncMode.SkipTargetProp;
+ innerMode &= ~StylingMapsSyncMode.ApplyTargetProp;
+ }
+ return innerMode;
+}
+
+/**
+ * Decides whether or not a prop/value entry will be applied to an element.
+ *
+ * To determine whether or not a value is to be applied,
+ * the following procedure is evaluated:
+ *
+ * First check to see the current `mode` status:
+ * 1. If the mode value permits all props to be applied then allow.
+ * - But do not allow if the current prop is set to be skipped.
+ * 2. Otherwise if the current prop is permitted then allow.
+ */
+function isValueAllowedToBeApplied(mode: number, isTargetPropMatched: boolean) {
+ let doApplyValue = (mode & StylingMapsSyncMode.ApplyAllValues) > 0;
+ if (!doApplyValue) {
+ if (mode & StylingMapsSyncMode.ApplyTargetProp) {
+ doApplyValue = isTargetPropMatched;
+ }
+ } else if ((mode & StylingMapsSyncMode.SkipTargetProp) && isTargetPropMatched) {
+ doApplyValue = false;
+ }
+ return doApplyValue;
+}
+
+/**
+ * Used to keep track of concurrent cursor values for multiple map-based styling bindings present on
+ * an element.
+ */
+const MAP_CURSORS: number[] = [];
+
+/**
+ * Used to reset the state of each cursor value being used to iterate over map-based styling
+ * bindings.
+ */
+function resetSyncCursors() {
+ for (let i = 0; i < MAP_CURSORS.length; i++) {
+ MAP_CURSORS[i] = LStylingMapIndex.ValuesStartPosition;
+ }
+}
+
+/**
+ * Returns an active cursor value at a given mapIndex location.
+ */
+function getCurrentSyncCursor(mapIndex: number) {
+ if (mapIndex >= MAP_CURSORS.length) {
+ MAP_CURSORS.push(LStylingMapIndex.ValuesStartPosition);
+ }
+ return MAP_CURSORS[mapIndex];
+}
+
+/**
+ * Sets a cursor value at a given mapIndex location.
+ */
+function setCurrentSyncCursor(mapIndex: number, indexValue: number) {
+ MAP_CURSORS[mapIndex] = indexValue;
+}
+
+/**
+ * Used to convert a {key:value} map into a `LStylingMap` array.
+ *
+ * This function will either generate a new `LStylingMap` instance
+ * or it will patch the provided `newValues` map value into an
+ * existing `LStylingMap` value (this only happens if `bindingValue`
+ * is an instance of `LStylingMap`).
+ *
+ * If a new key/value map is provided with an old `LStylingMap`
+ * value then all properties will be overwritten with their new
+ * values or with `null`. This means that the array will never
+ * shrink in size (but it will also not be created and thrown
+ * away whenever the {key:value} map entries change).
+ */
+export function normalizeIntoStylingMap(
+ bindingValue: null | LStylingMap,
+ newValues: {[key: string]: any} | string | null | undefined): LStylingMap {
+ const lStylingMap: LStylingMap = Array.isArray(bindingValue) ? bindingValue : [null];
+ lStylingMap[LStylingMapIndex.RawValuePosition] = newValues || null;
+
+ // because the new values may not include all the properties
+ // that the old ones had, all values are set to `null` before
+ // the new values are applied. This way, when flushed, the
+ // styling algorithm knows exactly what style/class values
+ // to remove from the element (since they are `null`).
+ for (let j = LStylingMapIndex.ValuesStartPosition; j < lStylingMap.length;
+ j += LStylingMapIndex.TupleSize) {
+ setMapValue(lStylingMap, j, null);
+ }
+
+ let props: string[]|null = null;
+ let map: {[key: string]: any}|undefined|null;
+ let allValuesTrue = false;
+ if (typeof newValues === 'string') { // [class] bindings allow string values
+ if (newValues.length) {
+ props = newValues.split(/\s+/);
+ allValuesTrue = true;
+ }
+ } else {
+ props = newValues ? Object.keys(newValues) : null;
+ map = newValues;
+ }
+
+ if (props) {
+ outer: for (let i = 0; i < props.length; i++) {
+ const prop = props[i] as string;
+ const value = allValuesTrue ? true : map ![prop];
+ for (let j = LStylingMapIndex.ValuesStartPosition; j < lStylingMap.length;
+ j += LStylingMapIndex.TupleSize) {
+ const propAtIndex = getMapProp(lStylingMap, j);
+ if (prop <= propAtIndex) {
+ if (propAtIndex === prop) {
+ setMapValue(lStylingMap, j, value);
+ } else {
+ lStylingMap.splice(j, 0, prop, value);
+ }
+ continue outer;
+ }
+ }
+ lStylingMap.push(prop, value);
+ }
+ }
+
+ return lStylingMap;
+}
+
+export function getMapProp(map: LStylingMap, index: number): string {
+ return map[index + LStylingMapIndex.PropOffset] as string;
+}
+
+export function setMapValue(map: LStylingMap, index: number, value: string | null): void {
+ map[index + LStylingMapIndex.ValueOffset] = value;
+}
+
+export function getMapValue(map: LStylingMap, index: number): string|null {
+ return map[index + LStylingMapIndex.ValueOffset] as string | null;
+}
diff --git a/packages/core/src/render3/styling_next/styling_debug.ts b/packages/core/src/render3/styling_next/styling_debug.ts
index 5dda787365..ac54022e93 100644
--- a/packages/core/src/render3/styling_next/styling_debug.ts
+++ b/packages/core/src/render3/styling_next/styling_debug.ts
@@ -8,9 +8,20 @@
import {RElement} from '../interfaces/renderer';
import {attachDebugObject} from '../util/debug_utils';
-import {BIT_MASK_APPLY_ALL, DEFAULT_BINDING_INDEX_VALUE, applyStyling} from './bindings';
-import {StylingBindingData, TStylingContext, TStylingContextIndex} from './interfaces';
-import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked} from './util';
+import {applyStyling} from './bindings';
+import {ApplyStylingFn, LStylingData, TStylingContext, TStylingContextIndex} from './interfaces';
+import {activeStylingMapFeature} from './map_based_bindings';
+import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased} from './util';
+
+/**
+ * --------
+ *
+ * This file contains the core debug functionality for styling in Angular.
+ *
+ * To learn more about the algorithm see `TStylingContext`.
+ *
+ * --------
+ */
/**
@@ -19,18 +30,15 @@ import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked}
* A value such as this is generated as an artifact of the `DebugStyling`
* summary.
*/
-export interface StylingSummary {
+export interface LStylingSummary {
/** The style/class property that the summary is attached to */
prop: string;
/** The last applied value for the style/class property */
- value: string|null;
+ value: string|boolean|null;
/** The binding index of the last applied style/class property */
bindingIndex: number|null;
-
- /** Every binding source that is writing the style/class property represented in this tuple */
- sourceValues: {value: string | number | null, bindingIndex: number|null}[];
}
/**
@@ -44,7 +52,7 @@ export interface DebugStyling {
* A summarization of each style/class property
* present in the context.
*/
- summary: {[key: string]: StylingSummary}|null;
+ summary: {[key: string]: LStylingSummary};
/**
* A key/value map of all styling properties and their
@@ -108,23 +116,27 @@ class TStylingContextDebug {
get entries(): {[prop: string]: TStylingTupleSummary} {
const context = this.context;
const entries: {[prop: string]: TStylingTupleSummary} = {};
- const start = TStylingContextIndex.ValuesStartPosition;
+ const start = TStylingContextIndex.MapBindingsPosition;
let i = start;
while (i < context.length) {
- const prop = getProp(context, i);
- const guardMask = getGuardMask(context, i);
const valuesCount = getValuesCount(context, i);
- const defaultValue = getDefaultValue(context, i);
+ // the context may contain placeholder values which are populated ahead of time,
+ // but contain no actual binding values. In this situation there is no point in
+ // classifying this as an "entry" since no real data is stored here yet.
+ if (valuesCount) {
+ const prop = getProp(context, i);
+ const guardMask = getGuardMask(context, i);
+ const defaultValue = getDefaultValue(context, i);
+ const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset;
- const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset;
- const sources: (number | string | null)[] = [];
+ const sources: (number | string | null)[] = [];
+ for (let j = 0; j < valuesCount; j++) {
+ sources.push(context[bindingsStartPosition + j] as number | string | null);
+ }
- for (let j = 0; j < valuesCount; j++) {
- sources.push(context[bindingsStartPosition + j] as number | string | null);
+ entries[prop] = {prop, guardMask, valuesCount, defaultValue, sources};
}
- entries[prop] = {prop, guardMask, valuesCount, defaultValue, sources};
-
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
}
return entries;
@@ -138,51 +150,19 @@ class TStylingContextDebug {
* application has `ngDevMode` activated.
*/
export class NodeStylingDebug implements DebugStyling {
- private _contextDebug: TStylingContextDebug;
-
- constructor(public context: TStylingContext, private _data: StylingBindingData) {
- this._contextDebug = (this.context as any).debug as any;
- }
+ constructor(public context: TStylingContext, private _data: LStylingData) {}
/**
* Returns a detailed summary of each styling entry in the context and
* what their runtime representation is.
*
- * See `StylingSummary`.
+ * See `LStylingSummary`.
*/
- get summary(): {[key: string]: StylingSummary} {
- const contextEntries = this._contextDebug.entries;
- const finalValues: {[key: string]: {value: string, bindingIndex: number}} = {};
- this._mapValues((prop: string, value: any, bindingIndex: number) => {
- finalValues[prop] = {value, bindingIndex};
+ get summary(): {[key: string]: LStylingSummary} {
+ const entries: {[key: string]: LStylingSummary} = {};
+ this._mapValues((prop: string, value: any, bindingIndex: number | null) => {
+ entries[prop] = {prop, value, bindingIndex};
});
-
- const entries: {[key: string]: StylingSummary} = {};
- const values = this.values;
- const props = Object.keys(values);
- for (let i = 0; i < props.length; i++) {
- const prop = props[i];
- const contextEntry = contextEntries[prop];
- const sourceValues = contextEntry.sources.map(v => {
- let value: string|number|null;
- let bindingIndex: number|null;
- if (typeof v === 'number') {
- value = this._data[v];
- bindingIndex = v;
- } else {
- value = v;
- bindingIndex = null;
- }
- return {bindingIndex, value};
- });
-
- const finalValue = finalValues[prop] !;
- let bindingIndex: number|null = finalValue.bindingIndex;
- bindingIndex = bindingIndex === DEFAULT_BINDING_INDEX_VALUE ? null : bindingIndex;
-
- entries[prop] = {prop, value: finalValue.value, bindingIndex, sourceValues};
- }
-
return entries;
}
@@ -195,16 +175,21 @@ export class NodeStylingDebug implements DebugStyling {
return entries;
}
- private _mapValues(fn: (prop: string, value: any, bindingIndex: number) => any) {
+ private _mapValues(fn: (prop: string, value: any, bindingIndex: number|null) => any) {
// there is no need to store/track an element instance. The
// element is only used when the styling algorithm attempts to
// style the value (and we mock out the stylingApplyFn anyway).
const mockElement = {} as any;
+ const hasMaps = getValuesCount(this.context, TStylingContextIndex.MapBindingsPosition) > 0;
+ if (hasMaps) {
+ activeStylingMapFeature();
+ }
- const mapFn =
+ const mapFn: ApplyStylingFn =
(renderer: any, element: RElement, prop: string, value: any, bindingIndex: number) => {
- fn(prop, value, bindingIndex);
+ fn(prop, value, bindingIndex || null);
};
- applyStyling(this.context, null, mockElement, this._data, BIT_MASK_APPLY_ALL, mapFn);
+
+ applyStyling(this.context, null, mockElement, this._data, true, mapFn);
}
}
diff --git a/packages/core/src/render3/styling_next/util.ts b/packages/core/src/render3/styling_next/util.ts
index 13a76aa3f3..3a21ba5fbb 100644
--- a/packages/core/src/render3/styling_next/util.ts
+++ b/packages/core/src/render3/styling_next/util.ts
@@ -7,13 +7,19 @@
*/
import {StylingContext} from '../interfaces/styling';
import {getProp as getOldProp, getSinglePropIndexValue as getOldSinglePropIndexValue} from '../styling/class_and_style_bindings';
-import {TStylingConfigFlags, TStylingContext, TStylingContextIndex} from './interfaces';
+
+import {LStylingMap, LStylingMapIndex, TStylingConfigFlags, TStylingContext, TStylingContextIndex} from './interfaces';
+
+const MAP_BASED_ENTRY_PROP_NAME = '--MAP--';
/**
* Creates a new instance of the `TStylingContext`.
+ *
+ * This function will also pre-fill the context with data
+ * for map-based bindings.
*/
export function allocStylingContext(): TStylingContext {
- return [TStylingConfigFlags.Initial, 0];
+ return [TStylingConfigFlags.Initial, 0, 0, 0, MAP_BASED_ENTRY_PROP_NAME];
}
/**
@@ -48,14 +54,14 @@ export function getProp(context: TStylingContext, index: number) {
}
export function getGuardMask(context: TStylingContext, index: number) {
- return context[index + TStylingContextIndex.MaskOffset] as number;
+ return context[index + TStylingContextIndex.GuardOffset] as number;
}
export function getValuesCount(context: TStylingContext, index: number) {
return context[index + TStylingContextIndex.ValuesCountOffset] as number;
}
-export function getValue(context: TStylingContext, index: number, offset: number) {
+export function getBindingValue(context: TStylingContext, index: number, offset: number) {
return context[index + TStylingContextIndex.BindingsStartOffset + offset] as number | string;
}
@@ -80,3 +86,32 @@ export function lockContext(context: TStylingContext) {
export function isContextLocked(context: TStylingContext): boolean {
return (getConfig(context) & TStylingConfigFlags.Locked) > 0;
}
+
+export function getPropValuesStartPosition(context: TStylingContext) {
+ return TStylingContextIndex.MapBindingsBindingsStartPosition +
+ context[TStylingContextIndex.MapBindingsValuesCountPosition];
+}
+
+export function isMapBased(prop: string) {
+ return prop === MAP_BASED_ENTRY_PROP_NAME;
+}
+
+export function hasValueChanged(
+ a: LStylingMap | number | String | string | null | boolean | undefined | {},
+ b: LStylingMap | number | String | string | null | boolean | undefined | {}): boolean {
+ const compareValueA = Array.isArray(a) ? a[LStylingMapIndex.RawValuePosition] : a;
+ const compareValueB = Array.isArray(b) ? b[LStylingMapIndex.RawValuePosition] : b;
+ return compareValueA !== compareValueB;
+}
+
+/**
+ * Determines whether the provided styling value is truthy or falsy.
+ */
+export function isStylingValueDefined(value: any) {
+ // the reason why null is compared against is because
+ // a CSS class value that is set to `false` must be
+ // respected (otherwise it would be treated as falsy).
+ // Empty string values are because developers usually
+ // set a value to an empty string to remove it.
+ return value != null && value !== '';
+}
diff --git a/packages/core/test/acceptance/styling_next_spec.ts b/packages/core/test/acceptance/styling_next_spec.ts
index 8ca9277a45..a8385e1808 100644
--- a/packages/core/test/acceptance/styling_next_spec.ts
+++ b/packages/core/test/acceptance/styling_next_spec.ts
@@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CompilerStylingMode, compilerSetStylingMode} from '@angular/compiler/src/render3/view/styling_state';
-import {Component, Directive, HostBinding, Input} from '@angular/core';
+import {Component, Directive, HostBinding, Input, ViewChild} from '@angular/core';
import {DebugNode, LViewDebug, toDebug} from '@angular/core/src/render3/debug';
import {RuntimeStylingMode, runtimeSetStylingMode} from '@angular/core/src/render3/styling_next/state';
import {loadLContextFromNode} from '@angular/core/src/render3/util/discovery_utils';
+import {ngDevModeResetPerfCounters as resetStylingCounters} from '@angular/core/src/util/ng_dev_mode';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
@@ -255,7 +256,7 @@ describe('new styling integration', () => {
});
});
- onlyInIvy('only ivy has style debugging support')
+ onlyInIvy('only ivy has style/class bindings debugging support')
.it('should support situations where there are more than 32 bindings', () => {
const TOTAL_BINDINGS = 34;
@@ -314,8 +315,276 @@ describe('new styling integration', () => {
expect(value).toEqual(`final${num}`);
}
});
+
+ onlyInIvy('only ivy has style debugging support')
+ .it('should apply map-based style and class entries', () => {
+ @Component({template: '
'})
+ class Cmp {
+ public c !: {[key: string]: any};
+ updateClasses(prop: string) {
+ this.c = {...this.c || {}};
+ this.c[prop] = true;
+ }
+
+ public s !: {[key: string]: any};
+ updateStyles(prop: string, value: string|number|null) {
+ this.s = {...this.s || {}};
+ this.s[prop] = value;
+ }
+ }
+
+ TestBed.configureTestingModule({declarations: [Cmp]});
+ const fixture = TestBed.createComponent(Cmp);
+ const comp = fixture.componentInstance;
+ comp.updateStyles('width', '100px');
+ comp.updateStyles('height', '200px');
+ comp.updateClasses('abc');
+ fixture.detectChanges();
+
+ const element = fixture.nativeElement.querySelector('div');
+ const node = getDebugNode(element) !;
+ const styles = node.styles !;
+ const classes = node.classes !;
+
+ const stylesSummary = styles.summary;
+ const widthSummary = stylesSummary['width'];
+ expect(widthSummary.prop).toEqual('width');
+ expect(widthSummary.value).toEqual('100px');
+
+ const heightSummary = stylesSummary['height'];
+ expect(heightSummary.prop).toEqual('height');
+ expect(heightSummary.value).toEqual('200px');
+
+ const classesSummary = classes.summary;
+ const abcSummary = classesSummary['abc'];
+ expect(abcSummary.prop).toEqual('abc');
+ expect(abcSummary.value as any).toEqual(true);
+ });
+
+ onlyInIvy('ivy resolves styling across directives, components and templates in unison')
+ .it('should resolve styling collisions across templates, directives and components for prop and map-based entries',
+ () => {
+ @Directive({selector: '[dir-that-sets-styling]'})
+ class DirThatSetsStyling {
+ @HostBinding('style') public map: any = {color: 'red', width: '777px'};
+ }
+
+ @Component({
+ template: `
+
+ `
+ })
+ class Cmp {
+ map: any = {width: '111px', opacity: '0.5'};
+ width: string|null = '555px';
+
+ @ViewChild('dir', {read: DirThatSetsStyling})
+ dir !: DirThatSetsStyling;
+ }
+
+ TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]});
+ const fixture = TestBed.createComponent(Cmp);
+ const comp = fixture.componentInstance;
+ fixture.detectChanges();
+
+ const element = fixture.nativeElement.querySelector('div');
+ const node = getDebugNode(element) !;
+
+ const styles = node.styles !;
+ expect(styles.values).toEqual({
+ 'width': '555px',
+ 'color': 'red',
+ 'font-size': '99px',
+ 'opacity': '0.5',
+ });
+
+ comp.width = null;
+ fixture.detectChanges();
+
+ expect(styles.values).toEqual({
+ 'width': '111px',
+ 'color': 'red',
+ 'font-size': '99px',
+ 'opacity': '0.5',
+ });
+
+ comp.map = null;
+ fixture.detectChanges();
+
+ expect(styles.values).toEqual({
+ 'width': '777px',
+ 'color': 'red',
+ 'font-size': '99px',
+ 'opacity': null,
+ });
+
+ comp.dir.map = null;
+ fixture.detectChanges();
+
+ expect(styles.values).toEqual({
+ 'width': '200px',
+ 'color': null,
+ 'font-size': '99px',
+ 'opacity': null,
+ });
+ });
+
+ onlyInIvy('ivy resolves styling across directives, components and templates in unison')
+ .it('should only apply each styling property once per CD across templates, components, directives',
+ () => {
+ @Directive({selector: '[dir-that-sets-styling]'})
+ class DirThatSetsStyling {
+ @HostBinding('style') public map: any = {width: '999px', height: '999px'};
+ }
+
+ @Component({
+ template: `
+
+ `
+ })
+ class Cmp {
+ width: string|null = '111px';
+ height: string|null = '111px';
+
+ map: any = {width: '555px', height: '555px'};
+
+ @ViewChild('dir', {read: DirThatSetsStyling})
+ dir !: DirThatSetsStyling;
+ }
+
+ TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]});
+ const fixture = TestBed.createComponent(Cmp);
+ const comp = fixture.componentInstance;
+
+ resetStylingCounters();
+ fixture.detectChanges();
+ const element = fixture.nativeElement.querySelector('div');
+
+ // both are applied because this is the first pass
+ assertStyleCounters(2, 0);
+ assertStyle(element, 'width', '111px');
+ assertStyle(element, 'height', '111px');
+
+ comp.width = '222px';
+ resetStylingCounters();
+ fixture.detectChanges();
+
+ assertStyleCounters(1, 0);
+ assertStyle(element, 'width', '222px');
+ assertStyle(element, 'height', '111px');
+
+ comp.height = '222px';
+ resetStylingCounters();
+ fixture.detectChanges();
+
+ assertStyleCounters(1, 0);
+ assertStyle(element, 'width', '222px');
+ assertStyle(element, 'height', '222px');
+
+ comp.width = null;
+ resetStylingCounters();
+ fixture.detectChanges();
+
+ assertStyleCounters(1, 0);
+ assertStyle(element, 'width', '555px');
+ assertStyle(element, 'height', '222px');
+
+ comp.width = '123px';
+ comp.height = '123px';
+ resetStylingCounters();
+ fixture.detectChanges();
+
+ assertStyle(element, 'width', '123px');
+ assertStyle(element, 'height', '123px');
+
+ comp.map = {};
+ resetStylingCounters();
+ fixture.detectChanges();
+
+ // both are applied because the map was altered
+ assertStyleCounters(2, 0);
+ assertStyle(element, 'width', '123px');
+ assertStyle(element, 'height', '123px');
+
+ comp.width = null;
+ resetStylingCounters();
+ fixture.detectChanges();
+
+ assertStyleCounters(1, 0);
+ assertStyle(element, 'width', '999px');
+ assertStyle(element, 'height', '123px');
+
+ comp.dir.map = null;
+ resetStylingCounters();
+ fixture.detectChanges();
+
+ // both are applied because the map was altered
+ assertStyleCounters(2, 0);
+ assertStyle(element, 'width', '0px');
+ assertStyle(element, 'height', '123px');
+
+ comp.dir.map = {width: '1000px', height: '1000px', color: 'red'};
+ resetStylingCounters();
+ fixture.detectChanges();
+
+ // all three are applied because the map was altered
+ assertStyleCounters(3, 0);
+ assertStyle(element, 'width', '1000px');
+ assertStyle(element, 'height', '123px');
+ assertStyle(element, 'color', 'red');
+
+ comp.height = null;
+ resetStylingCounters();
+ fixture.detectChanges();
+
+ assertStyleCounters(1, 0);
+ assertStyle(element, 'width', '1000px');
+ assertStyle(element, 'height', '1000px');
+ assertStyle(element, 'color', 'red');
+
+ comp.map = {color: 'blue', width: '2000px', opacity: '0.5'};
+ resetStylingCounters();
+ fixture.detectChanges();
+
+ // all four are applied because the map was altered
+ assertStyleCounters(4, 0);
+ assertStyle(element, 'width', '2000px');
+ assertStyle(element, 'height', '1000px');
+ assertStyle(element, 'color', 'blue');
+ assertStyle(element, 'opacity', '0.5');
+
+ comp.map = {color: 'blue', width: '2000px'};
+ resetStylingCounters();
+ fixture.detectChanges();
+
+ // all four are applied because the map was altered
+ assertStyleCounters(3, 1);
+ assertStyle(element, 'width', '2000px');
+ assertStyle(element, 'height', '1000px');
+ assertStyle(element, 'color', 'blue');
+ assertStyle(element, 'opacity', '');
+ });
});
+function assertStyleCounters(countForSet: number, countForRemove: number) {
+ expect(ngDevMode !.rendererSetStyle).toEqual(countForSet);
+ expect(ngDevMode !.rendererRemoveStyle).toEqual(countForRemove);
+}
+
+function assertStyle(element: HTMLElement, prop: string, value: any) {
+ expect((element.style as any)[prop]).toEqual(value);
+}
+
function getDebugNode(element: Node): DebugNode|null {
const lContext = loadLContextFromNode(element);
const lViewDebug = toDebug(lContext.lView) as LViewDebug;
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 191b915e60..0488845316 100644
--- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
@@ -36,7 +36,7 @@
"name": "DEFAULT_BINDING_VALUE"
},
{
- "name": "DEFAULT_MASK_VALUE"
+ "name": "DEFAULT_GUARD_MASK_VALUE"
},
{
"name": "DEFAULT_SIZE_VALUE"
@@ -71,6 +71,9 @@
{
"name": "INJECTOR_BLOOM_PARENT_SIZE"
},
+ {
+ "name": "MAP_BASED_ENTRY_PROP_NAME"
+ },
{
"name": "MONKEY_PATCH_KEY_NAME"
},
@@ -431,6 +434,9 @@
{
"name": "getProp"
},
+ {
+ "name": "getPropValuesStartPosition"
+ },
{
"name": "getRenderFlags"
},
diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json
index 6fd35afd5c..d804c23fa1 100644
--- a/packages/core/test/bundling/todo/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json
@@ -11,9 +11,6 @@
{
"name": "BINDING_INDEX"
},
- {
- "name": "BIT_MASK_APPLY_ALL"
- },
{
"name": "BLOOM_MASK"
},
@@ -50,14 +47,11 @@
{
"name": "DECLARATION_VIEW"
},
- {
- "name": "DEFAULT_BINDING_INDEX_VALUE"
- },
{
"name": "DEFAULT_BINDING_VALUE"
},
{
- "name": "DEFAULT_MASK_VALUE"
+ "name": "DEFAULT_GUARD_MASK_VALUE"
},
{
"name": "DEFAULT_SIZE_VALUE"
@@ -122,6 +116,9 @@
{
"name": "IterableDiffers"
},
+ {
+ "name": "MAP_BASED_ENTRY_PROP_NAME"
+ },
{
"name": "MIN_DIRECTIVE_ID"
},
@@ -218,6 +215,12 @@
{
"name": "SANITIZER"
},
+ {
+ "name": "STYLING_INDEX_FOR_MAP_BINDING"
+ },
+ {
+ "name": "STYLING_INDEX_START_VALUE"
+ },
{
"name": "SWITCH_ELEMENT_REF_FACTORY"
},
@@ -305,6 +308,9 @@
{
"name": "__values"
},
+ {
+ "name": "_activeStylingMapApplyFn"
+ },
{
"name": "_c0"
},
@@ -395,6 +401,9 @@
{
"name": "_stylingMode"
},
+ {
+ "name": "_stylingProp"
+ },
{
"name": "_symbolIterator"
},
@@ -590,6 +599,9 @@
{
"name": "currentClassIndex"
},
+ {
+ "name": "currentStyleIndex"
+ },
{
"name": "decreaseElementDepthCount"
},
@@ -731,6 +743,9 @@
{
"name": "getBindingNameFromIndex"
},
+ {
+ "name": "getBindingValue"
+ },
{
"name": "getBindingsEnabled"
},
@@ -920,6 +935,9 @@
{
"name": "getProp"
},
+ {
+ "name": "getPropValuesStartPosition"
+ },
{
"name": "getRenderFlags"
},
@@ -953,6 +971,9 @@
{
"name": "getStylingContextFromLView"
},
+ {
+ "name": "getStylingMapsSyncFn"
+ },
{
"name": "getSymbolIterator"
},
@@ -971,9 +992,6 @@
{
"name": "getValue"
},
- {
- "name": "getValue"
- },
{
"name": "getValuesCount"
},
@@ -1001,6 +1019,9 @@
{
"name": "hasValueChanged"
},
+ {
+ "name": "hasValueChanged"
+ },
{
"name": "hyphenate"
},
@@ -1134,7 +1155,7 @@
"name": "isStylingContext"
},
{
- "name": "isValueDefined"
+ "name": "isStylingValueDefined"
},
{
"name": "iterateListLike"
@@ -1208,6 +1229,9 @@
{
"name": "noSideEffects"
},
+ {
+ "name": "normalizeBitMaskValue"
+ },
{
"name": "patchContextWithStaticAttrs"
},
@@ -1490,6 +1514,9 @@
{
"name": "updateSingleStylingValue"
},
+ {
+ "name": "updateStyleBinding"
+ },
{
"name": "valueExists"
},
diff --git a/packages/core/test/render3/styling_next/map_based_bindings_spec.ts b/packages/core/test/render3/styling_next/map_based_bindings_spec.ts
new file mode 100644
index 0000000000..a6ebd1e439
--- /dev/null
+++ b/packages/core/test/render3/styling_next/map_based_bindings_spec.ts
@@ -0,0 +1,79 @@
+/**
+ * @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 {normalizeIntoStylingMap as createMap} from '../../../src/render3/styling_next/map_based_bindings';
+
+describe('map-based bindings', () => {
+ describe('LStylingMap construction', () => {
+ it('should create a new LStylingMap instance from a given value', () => {
+ createAndAssertValues(null, []);
+ createAndAssertValues(undefined, []);
+ createAndAssertValues({}, []);
+ createAndAssertValues({foo: 'bar'}, ['foo', 'bar']);
+ createAndAssertValues({bar: null}, ['bar', null]);
+ createAndAssertValues('', []);
+ createAndAssertValues('abc xyz', ['abc', true, 'xyz', true]);
+ createAndAssertValues([], []);
+ });
+
+ it('should list each entry in the context in alphabetical order', () => {
+ const value1 = {width: '200px', color: 'red', zIndex: -1};
+ const map1 = createMap(null, value1);
+ expect(map1).toEqual([value1, 'color', 'red', 'width', '200px', 'zIndex', -1]);
+
+ const value2 = 'yes no maybe';
+ const map2 = createMap(null, value2);
+ expect(map2).toEqual([value2, 'maybe', true, 'no', true, 'yes', true]);
+ });
+
+ it('should patch an existing LStylingMap entry with new values and retain the alphabetical order',
+ () => {
+ const value1 = {color: 'red'};
+ const map1 = createMap(null, value1);
+ expect(map1).toEqual([value1, 'color', 'red']);
+
+ const value2 = {backgroundColor: 'red', color: 'blue', opacity: '0.5'};
+ const map2 = createMap(map1, value2);
+ expect(map1).toBe(map2);
+ expect(map1).toEqual(
+ [value2, 'backgroundColor', 'red', 'color', 'blue', 'opacity', '0.5']);
+
+ const value3 = 'myClass';
+ const map3 = createMap(null, value3);
+ expect(map3).toEqual([value3, 'myClass', true]);
+
+ const value4 = 'yourClass everyonesClass myClass';
+ const map4 = createMap(map3, value4);
+ expect(map3).toBe(map4);
+ expect(map4).toEqual([value4, 'everyonesClass', true, 'myClass', true, 'yourClass', true]);
+ });
+
+ it('should nullify old values that are not apart of the new set of values', () => {
+ const value1 = {color: 'red', fontSize: '20px'};
+ const map1 = createMap(null, value1);
+ expect(map1).toEqual([value1, 'color', 'red', 'fontSize', '20px']);
+
+ const value2 = {color: 'blue', borderColor: 'purple', opacity: '0.5'};
+ const map2 = createMap(map1, value2);
+ expect(map2).toEqual(
+ [value2, 'borderColor', 'purple', 'color', 'blue', 'fontSize', null, 'opacity', '0.5']);
+
+ const value3 = 'orange';
+ const map3 = createMap(null, value3);
+ expect(map3).toEqual([value3, 'orange', true]);
+
+ const value4 = 'apple banana';
+ const map4 = createMap(map3, value4);
+ expect(map4).toEqual([value4, 'apple', true, 'banana', true, 'orange', null]);
+ });
+ });
+});
+
+function createAndAssertValues(newValue: any, entries: any[]) {
+ const result = createMap(null, newValue);
+ expect(result).toEqual([newValue || null, ...entries]);
+}
diff --git a/packages/core/test/render3/styling_next/styling_context_spec.ts b/packages/core/test/render3/styling_next/styling_context_spec.ts
index ff21327acd..f3788528c1 100644
--- a/packages/core/test/render3/styling_next/styling_context_spec.ts
+++ b/packages/core/test/render3/styling_next/styling_context_spec.ts
@@ -5,7 +5,7 @@
* 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 {registerBinding} from '@angular/core/src/render3/styling_next/bindings';
+import {DEFAULT_GUARD_MASK_VALUE, registerBinding} from '@angular/core/src/render3/styling_next/bindings';
import {attachStylingDebugObject} from '@angular/core/src/render3/styling_next/styling_debug';
import {allocStylingContext} from '../../../src/render3/styling_next/util';
@@ -16,7 +16,7 @@ describe('styling context', () => {
const context = debug.context;
expect(debug.entries).toEqual({});
- registerBinding(context, 0, 'width', '100px');
+ registerBinding(context, 1, 'width', '100px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
@@ -25,21 +25,21 @@ describe('styling context', () => {
sources: ['100px'],
});
- registerBinding(context, 1, 'width', 20);
+ registerBinding(context, 2, 'width', 20);
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 2,
- guardMask: buildGuardMask(1),
+ guardMask: buildGuardMask(2),
defaultValue: '100px',
sources: [20, '100px'],
});
- registerBinding(context, 2, 'height', 10);
- registerBinding(context, 3, 'height', 15);
+ registerBinding(context, 3, 'height', 10);
+ registerBinding(context, 4, 'height', 15);
expect(debug.entries['height']).toEqual({
prop: 'height',
valuesCount: 3,
- guardMask: buildGuardMask(2, 3),
+ guardMask: buildGuardMask(3, 4),
defaultValue: null,
sources: [10, 15, null],
});
@@ -48,9 +48,8 @@ describe('styling context', () => {
it('should overwrite a default value for an entry only if it is non-null', () => {
const debug = makeContextWithDebug();
const context = debug.context;
- expect(debug.entries).toEqual({});
- registerBinding(context, 0, 'width', null);
+ registerBinding(context, 1, 'width', null);
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
@@ -59,7 +58,7 @@ describe('styling context', () => {
sources: [null]
});
- registerBinding(context, 0, 'width', '100px');
+ registerBinding(context, 1, 'width', '100px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
@@ -68,7 +67,7 @@ describe('styling context', () => {
sources: ['100px']
});
- registerBinding(context, 0, 'width', '200px');
+ registerBinding(context, 1, 'width', '200px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
@@ -85,7 +84,7 @@ function makeContextWithDebug() {
}
function buildGuardMask(...bindingIndices: number[]) {
- let mask = 0;
+ let mask = DEFAULT_GUARD_MASK_VALUE;
for (let i = 0; i < bindingIndices.length; i++) {
mask |= 1 << bindingIndices[i];
}
diff --git a/packages/core/test/render3/styling_next/styling_debug_spec.ts b/packages/core/test/render3/styling_next/styling_debug_spec.ts
index a43e1fd107..499685974e 100644
--- a/packages/core/test/render3/styling_next/styling_debug_spec.ts
+++ b/packages/core/test/render3/styling_next/styling_debug_spec.ts
@@ -24,7 +24,6 @@ describe('styling debugging tools', () => {
prop: 'width',
value: null,
bindingIndex: null,
- sourceValues: [{value: null, bindingIndex: null}],
},
});
@@ -34,9 +33,6 @@ describe('styling debugging tools', () => {
prop: 'width',
value: '100px',
bindingIndex: null,
- sourceValues: [
- {bindingIndex: null, value: '100px'},
- ],
},
});
@@ -49,10 +45,6 @@ describe('styling debugging tools', () => {
prop: 'width',
value: '200px',
bindingIndex: someBindingIndex1,
- sourceValues: [
- {bindingIndex: someBindingIndex1, value: '200px'},
- {bindingIndex: null, value: '100px'},
- ],
},
});
@@ -65,11 +57,6 @@ describe('styling debugging tools', () => {
prop: 'width',
value: '200px',
bindingIndex: someBindingIndex1,
- sourceValues: [
- {bindingIndex: someBindingIndex1, value: '200px'},
- {bindingIndex: someBindingIndex2, value: '500px'},
- {bindingIndex: null, value: '100px'},
- ],
},
});
});