`
*/
- HasCollisions = 0b0000100,
+ HasCollisions = 0b00001000,
/**
* Whether or not the context contains initial styling values.
@@ -395,7 +413,7 @@ export const enum TStylingConfig {
* 3. `@Directive({ host: { 'style': 'width:200px' } })`
* 4. `@Directive({ host: { 'class': 'one two three' } })`
*/
- HasInitialStyling = 0b00001000,
+ HasInitialStyling = 0b000010000,
/**
* Whether or not the context contains one or more template bindings.
@@ -406,7 +424,7 @@ export const enum TStylingConfig {
* 3. `
`
* 4. `
`
*/
- HasTemplateBindings = 0b00010000,
+ HasTemplateBindings = 0b000100000,
/**
* Whether or not the context contains one or more host bindings.
@@ -417,7 +435,7 @@ export const enum TStylingConfig {
* 3. `@HostBinding('class') x`
* 4. `@HostBinding('class.name') x`
*/
- HasHostBindings = 0b00100000,
+ HasHostBindings = 0b001000000,
/**
* Whether or not the template bindings are allowed to be registered in the context.
@@ -428,7 +446,7 @@ export const enum TStylingConfig {
*
* Note that this is only set once.
*/
- TemplateBindingsLocked = 0b01000000,
+ TemplateBindingsLocked = 0b010000000,
/**
* Whether or not the host bindings are allowed to be registered in the context.
@@ -439,13 +457,13 @@ export const enum TStylingConfig {
*
* Note that this is only set once.
*/
- HostBindingsLocked = 0b10000000,
+ HostBindingsLocked = 0b100000000,
/** A Mask of all the configurations */
- Mask = 0b11111111,
+ Mask = 0b111111111,
/** Total amount of configuration bits used */
- TotalBits = 8,
+ TotalBits = 9,
}
/**
diff --git a/packages/core/src/render3/styling/bindings.ts b/packages/core/src/render3/styling/bindings.ts
index ceeea0b3e8..f1ef045ddd 100644
--- a/packages/core/src/render3/styling/bindings.ts
+++ b/packages/core/src/render3/styling/bindings.ts
@@ -10,7 +10,7 @@ import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanit
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling';
import {NO_CHANGE} from '../tokens';
-import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, getBindingValue, getConfig, getDefaultValue, getGuardMask, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
+import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
import {getStylingState, resetStylingState} from './state';
@@ -143,7 +143,6 @@ function updateBindingData(
patchConfig(
context,
hostBindingsMode ? TStylingConfig.HasHostBindings : TStylingConfig.HasTemplateBindings);
- patchConfig(context, prop ? TStylingConfig.HasPropBindings : TStylingConfig.HasMapBindings);
}
const changed = forceUpdate || hasValueChanged(data[bindingIndex], value);
@@ -629,22 +628,58 @@ export function applyStylingViaContext(
* automatically. This function is intended to be used for performance reasons in the
* event that there is no need to apply styling via context resolution.
*
- * See `allowDirectStylingApply`.
+ * This function has three different cases that can occur (for each item in the map):
+ *
+ * - Case 1: Attempt to apply the current value in the map to the element (if it's `non null`).
+ *
+ * - Case 2: If a map value fails to be applied then the algorithm will find a matching entry in
+ * the initial values present in the context and attempt to apply that.
+ *
+ * - Default Case: If the initial value cannot be applied then a default value of `null` will be
+ * applied (which will remove the style/class value from the element).
+ *
+ * See `allowDirectStylingApply` to learn the logic used to determine whether any style/class
+ * bindings can be directly applied.
*
* @returns whether or not the styling map was applied to the element.
*/
export function applyStylingMapDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
- bindingIndex: number, map: StylingMapArray, applyFn: ApplyStylingFn,
+ bindingIndex: number, map: StylingMapArray, isClassBased: boolean, applyFn: ApplyStylingFn,
sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean): boolean {
if (forceUpdate || hasValueChanged(data[bindingIndex], map)) {
setValue(data, bindingIndex, map);
+ const initialStyles =
+ hasConfig(context, TStylingConfig.HasInitialStyling) ? getStylingMapArray(context) : null;
+
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i);
- applyStylingValue(renderer, context, element, prop, value, applyFn, bindingIndex, sanitizer);
+
+ // case 1: apply the map value (if it exists)
+ let applied =
+ applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
+
+ // case 2: apply the initial value (if it exists)
+ if (!applied && initialStyles) {
+ applied = findAndApplyMapValue(
+ renderer, element, applyFn, initialStyles, prop, bindingIndex, sanitizer);
+ }
+
+ // default case: apply `null` to remove the value
+ if (!applied) {
+ applyFn(renderer, element, prop, null, bindingIndex);
+ }
}
+
+ const state = getStylingState(element, TEMPLATE_DIRECTIVE_INDEX);
+ if (isClassBased) {
+ state.lastDirectClassMap = map;
+ } else {
+ state.lastDirectStyleMap = map;
+ }
+
return true;
}
return false;
@@ -657,47 +692,95 @@ export function applyStylingMapDirectly(
* automatically. This function is intended to be used for performance reasons in the
* event that there is no need to apply styling via context resolution.
*
- * See `allowDirectStylingApply`.
+ * This function has four different cases that can occur:
+ *
+ * - Case 1: Apply the provided prop/value (style or class) entry to the element
+ * (if it is `non null`).
+ *
+ * - Case 2: If value does not get applied (because its `null` or `undefined`) then the algorithm
+ * will check to see if a styling map value was applied to the element as well just
+ * before this (via `styleMap` or `classMap`). If and when a map is present then the
+ * algorithm will find the matching property in the map and apply its value.
+ *
+ * - Case 3: If a map value fails to be applied then the algorithm will check to see if there
+ * are any initial values present and attempt to apply a matching value based on
+ * the target prop.
+ *
+ * - Default Case: If a matching initial value cannot be applied then a default value
+ * of `null` will be applied (which will remove the style/class value
+ * from the element).
+ *
+ * See `allowDirectStylingApply` to learn the logic used to determine whether any style/class
+ * bindings can be directly applied.
*
* @returns whether or not the prop/value styling was applied to the element.
*/
export function applyStylingValueDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
- bindingIndex: number, prop: string, value: any, applyFn: ApplyStylingFn,
+ bindingIndex: number, prop: string, value: any, isClassBased: boolean, applyFn: ApplyStylingFn,
sanitizer?: StyleSanitizeFn | null): boolean {
+ let applied = false;
if (hasValueChanged(data[bindingIndex], value)) {
setValue(data, bindingIndex, value);
- applyStylingValue(renderer, context, element, prop, value, applyFn, bindingIndex, sanitizer);
+
+ // case 1: apply the provided value (if it exists)
+ applied = applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
+
+ // case 2: find the matching property in a styling map and apply the detected value
+ if (!applied && hasConfig(context, TStylingConfig.HasMapBindings)) {
+ const state = getStylingState(element, TEMPLATE_DIRECTIVE_INDEX);
+ const map = isClassBased ? state.lastDirectClassMap : state.lastDirectStyleMap;
+ applied = map ?
+ findAndApplyMapValue(renderer, element, applyFn, map, prop, bindingIndex, sanitizer) :
+ false;
+ }
+
+ // case 3: apply the initial value (if it exists)
+ if (!applied && hasConfig(context, TStylingConfig.HasInitialStyling)) {
+ const map = getStylingMapArray(context);
+ applied =
+ map ? findAndApplyMapValue(renderer, element, applyFn, map, prop, bindingIndex) : false;
+ }
+
+ // default case: apply `null` to remove the value
+ if (!applied) {
+ applyFn(renderer, element, prop, null, bindingIndex);
+ }
+ }
+ return applied;
+}
+
+function applyStylingValue(
+ renderer: any, element: RElement, prop: string, value: any, applyFn: ApplyStylingFn,
+ bindingIndex: number, sanitizer?: StyleSanitizeFn | null): boolean {
+ let valueToApply: string|null = unwrapSafeValue(value);
+ if (isStylingValueDefined(valueToApply)) {
+ valueToApply =
+ sanitizer ? sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) : valueToApply;
+ applyFn(renderer, element, prop, valueToApply, bindingIndex);
return true;
}
return false;
}
-function applyStylingValue(
- renderer: any, context: TStylingContext, element: RElement, prop: string, value: any,
- applyFn: ApplyStylingFn, bindingIndex: number, sanitizer?: StyleSanitizeFn | null) {
- let valueToApply: string|null = unwrapSafeValue(value);
- if (isStylingValueDefined(valueToApply)) {
- valueToApply =
- sanitizer ? sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) : valueToApply;
- } else if (hasConfig(context, TStylingConfig.HasInitialStyling)) {
- const initialStyles = getStylingMapArray(context);
- if (initialStyles) {
- valueToApply = findInitialStylingValue(initialStyles, prop);
- }
- }
- applyFn(renderer, element, prop, valueToApply, bindingIndex);
-}
-
-function findInitialStylingValue(map: StylingMapArray, prop: string): string|null {
+function findAndApplyMapValue(
+ renderer: any, element: RElement, applyFn: ApplyStylingFn, map: StylingMapArray, prop: string,
+ bindingIndex: number, sanitizer?: StyleSanitizeFn | null) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const p = getMapProp(map, i);
- if (p >= prop) {
- return p === prop ? getMapValue(map, i) : null;
+ if (p === prop) {
+ let valueToApply = getMapValue(map, i);
+ valueToApply =
+ sanitizer ? sanitizer(prop, valueToApply, StyleSanitizeMode.SanitizeOnly) : valueToApply;
+ applyFn(renderer, element, prop, valueToApply, bindingIndex);
+ return true;
+ }
+ if (p > prop) {
+ break;
}
}
- return null;
+ return false;
}
function normalizeBitMaskValue(value: number | boolean): number {
diff --git a/packages/core/src/render3/styling/state.ts b/packages/core/src/render3/styling/state.ts
index a160a9f999..73f5fc42e1 100644
--- a/packages/core/src/render3/styling/state.ts
+++ b/packages/core/src/render3/styling/state.ts
@@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {RElement} from '../interfaces/renderer';
+import {StylingMapArray} from '../interfaces/styling';
import {TEMPLATE_DIRECTIVE_INDEX} from '../util/styling_utils';
/**
@@ -57,6 +58,26 @@ export interface StylingState {
/** The styles update bit index value that is processed during each style binding */
stylesIndex: number;
+
+ /**
+ * The last class map that was applied (i.e. `[class]="x"`).
+ *
+ * Note that this property is only populated when direct class values are applied
+ * (i.e. context resolution is not used).
+ *
+ * See `allowDirectStyling` for more info.
+ */
+ lastDirectClassMap: StylingMapArray|null;
+
+ /**
+ * The last style map that was applied (i.e. `[style]="x"`)
+ *
+ * Note that this property is only populated when direct style values are applied
+ * (i.e. context resolution is not used).
+ *
+ * See `allowDirectStyling` for more info.
+ */
+ lastDirectStyleMap: StylingMapArray|null;
}
// these values will get filled in the very first time this is accessed...
@@ -68,6 +89,8 @@ const _state: StylingState = {
classesIndex: -1,
stylesBitMask: -1,
stylesIndex: -1,
+ lastDirectClassMap: null,
+ lastDirectStyleMap: null,
};
const BIT_MASK_START_VALUE = 0;
@@ -99,6 +122,8 @@ export function getStylingState(element: RElement, directiveIndex: number): Styl
_state.classesIndex = INDEX_START_VALUE;
_state.stylesBitMask = BIT_MASK_START_VALUE;
_state.stylesIndex = INDEX_START_VALUE;
+ _state.lastDirectClassMap = null;
+ _state.lastDirectStyleMap = null;
} else if (_state.directiveIndex !== directiveIndex) {
_state.directiveIndex = directiveIndex;
_state.sourceIndex++;
diff --git a/packages/core/src/render3/util/styling_utils.ts b/packages/core/src/render3/util/styling_utils.ts
index 7c69873c77..c3f30930d1 100644
--- a/packages/core/src/render3/util/styling_utils.ts
+++ b/packages/core/src/render3/util/styling_utils.ts
@@ -41,12 +41,20 @@ export const DEFAULT_GUARD_MASK_VALUE = 0b1;
* tNode for styles and for classes. This function allocates a new instance of a
* `TStylingContext` with the initial values (see `interfaces.ts` for more info).
*/
-export function allocTStylingContext(initialStyling?: StylingMapArray | null): TStylingContext {
+export function allocTStylingContext(
+ initialStyling: StylingMapArray | null, hasDirectives: boolean): TStylingContext {
initialStyling = initialStyling || allocStylingMapArray();
+ let config = TStylingConfig.Initial;
+ if (hasDirectives) {
+ config |= TStylingConfig.HasDirectives;
+ }
+ if (initialStyling.length > StylingMapArrayIndex.ValuesStartPosition) {
+ config |= TStylingConfig.HasInitialStyling;
+ }
return [
- TStylingConfig.Initial, // 1) config for the styling context
- DEFAULT_TOTAL_SOURCES, // 2) total amount of styling sources (template, directives, etc...)
- initialStyling, // 3) initial styling values
+ config, // 1) config for the styling context
+ DEFAULT_TOTAL_SOURCES, // 2) total amount of styling sources (template, directives, etc...)
+ initialStyling, // 3) initial styling values
];
}
@@ -66,15 +74,34 @@ export function hasConfig(context: TStylingContext, flag: TStylingConfig) {
* Determines whether or not to apply styles/classes directly or via context resolution.
*
* There are three cases that are matched here:
- * 1. context is locked for template or host bindings (depending on `hostBindingsMode`)
- * 2. There are no collisions (i.e. properties with more than one binding)
- * 3. There are only "prop" or "map" bindings present, but not both
+ * 1. there are no directives present AND ngDevMode is falsy
+ * 2. context is locked for template or host bindings (depending on `hostBindingsMode`)
+ * 3. There are no collisions (i.e. properties with more than one binding) across multiple
+ * sources (i.e. template + directive, directive + directive, directive + component)
*/
export function allowDirectStyling(context: TStylingContext, hostBindingsMode: boolean): boolean {
+ let allow = false;
const config = getConfig(context);
- return ((config & getLockedConfig(hostBindingsMode)) !== 0) &&
- ((config & TStylingConfig.HasCollisions) === 0) &&
- ((config & TStylingConfig.HasPropAndMapBindings) !== TStylingConfig.HasPropAndMapBindings);
+ const contextIsLocked = (config & getLockedConfig(hostBindingsMode)) !== 0;
+ const hasNoDirectives = (config & TStylingConfig.HasDirectives) === 0;
+
+ // if no directives are present then we do not need populate a context at all. This
+ // is because duplicate prop bindings cannot be registered through the template. If
+ // and when this happens we can safely apply the value directly without context
+ // resolution...
+ if (hasNoDirectives) {
+ // `ngDevMode` is required to be checked here because tests/debugging rely on the context being
+ // populated. If things are in production mode then there is no need to build a context
+ // therefore the direct apply can be allowed (even on the first update).
+ allow = ngDevMode ? contextIsLocked : true;
+ } else if (contextIsLocked) {
+ const hasNoCollisions = (config & TStylingConfig.HasCollisions) === 0;
+ const hasOnlyMapsOrOnlyProps =
+ (config & TStylingConfig.HasPropAndMapBindings) !== TStylingConfig.HasPropAndMapBindings;
+ allow = hasNoCollisions && hasOnlyMapsOrOnlyProps;
+ }
+
+ return allow;
}
export function setConfig(context: TStylingContext, value: TStylingConfig): void {
diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json
index a8bea9dbbc..251a879e93 100644
--- a/packages/core/test/bundling/todo/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json
@@ -575,6 +575,9 @@
{
"name": "extractPipeDef"
},
+ {
+ "name": "findAndApplyMapValue"
+ },
{
"name": "findAttrIndexInNode"
},
@@ -587,9 +590,6 @@
{
"name": "findExistingListener"
},
- {
- "name": "findInitialStylingValue"
- },
{
"name": "findViaComponent"
},
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 1d103953d2..a13d5b9d49 100644
--- a/packages/core/test/render3/styling_next/styling_context_spec.ts
+++ b/packages/core/test/render3/styling_next/styling_context_spec.ts
@@ -110,7 +110,7 @@ describe('styling context', () => {
});
function makeContextWithDebug() {
- const ctx = allocTStylingContext();
+ const ctx = allocTStylingContext(null, false);
return attachStylingDebugObject(ctx);
}
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 65ba95f4d0..f77cf8d689 100644
--- a/packages/core/test/render3/styling_next/styling_debug_spec.ts
+++ b/packages/core/test/render3/styling_next/styling_debug_spec.ts
@@ -64,6 +64,6 @@ describe('styling debugging tools', () => {
});
function makeContextWithDebug() {
- const ctx = allocTStylingContext();
+ const ctx = allocTStylingContext(null, false);
return attachStylingDebugObject(ctx);
}