diff --git a/packages/core/src/render3/instructions/styling.ts b/packages/core/src/render3/instructions/styling.ts index aaf5e5158a9f0..5e8c6c4dd15f0 100644 --- a/packages/core/src/render3/instructions/styling.ts +++ b/packages/core/src/render3/instructions/styling.ts @@ -13,14 +13,14 @@ import {AttributeMarker, TAttributes, TNode, TNodeFlags, TNodeType} from '../int import {RElement} from '../interfaces/renderer'; import {StylingMapArray, StylingMapArrayIndex, TStylingConfig, TStylingContext} from '../interfaces/styling'; import {isDirectiveHost} from '../interfaces/type_checks'; -import {LView, RENDERER} from '../interfaces/view'; +import {LView, RENDERER, TVIEW, TView} from '../interfaces/view'; import {getActiveDirectiveId, getCheckNoChangesMode, getCurrentStyleSanitizer, getLView, getSelectedIndex, incrementBindingIndex, nextBindingIndex, resetCurrentStyleSanitizer, setCurrentStyleSanitizer, setElementExitFn} from '../state'; import {applyStylingMapDirectly, applyStylingValueDirectly, flushStyling, setClass, setStyle, updateClassViaContext, updateStyleViaContext} from '../styling/bindings'; import {activateStylingMapFeature} from '../styling/map_based_bindings'; import {attachStylingDebugObject} from '../styling/styling_debug'; import {NO_CHANGE} from '../tokens'; import {renderStringify} from '../util/misc_utils'; -import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, getValue, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isHostStylingActive, isStylingContext, normalizeIntoStylingMap, patchConfig, selectClassBasedInputName, setValue, stylingMapToString} from '../util/styling_utils'; +import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, getValue, hasClassInput, hasStyleInput, hasValueChanged, isHostStylingActive, isStylingContext, isStylingValueDefined, normalizeIntoStylingMap, patchConfig, selectClassBasedInputName, setValue, stylingMapToString} from '../util/styling_utils'; import {getNativeByTNode, getTNode} from '../util/view_utils'; @@ -154,17 +154,16 @@ function stylingProp( let updated = false; const lView = getLView(); + const firstUpdatePass = lView[TVIEW].firstUpdatePass; const tNode = getTNode(elementIndex, lView); const native = getNativeByTNode(tNode, lView) as RElement; - - const hostBindingsMode = isHostStyling(); const context = isClassBased ? getClassesContext(tNode) : getStylesContext(tNode); const sanitizer = isClassBased ? null : getCurrentStyleSanitizer(); // we check for this in the instruction code so that the context can be notified // about prop or map bindings so that the direct apply check can decide earlier // if it allows for context resolution to be bypassed. - if (!isContextLocked(context, hostBindingsMode)) { + if (firstUpdatePass) { patchConfig(context, TStylingConfig.HasPropBindings); } @@ -181,7 +180,7 @@ function stylingProp( // Direct Apply Case: bypass context resolution and apply the // style/class value directly to the element - if (allowDirectStyling(context, hostBindingsMode)) { + if (allowDirectStyling(context, firstUpdatePass)) { const sanitizerToUse = isClassBased ? null : sanitizer; const renderer = getRenderer(tNode, lView); updated = applyStylingValueDirectly( @@ -201,11 +200,11 @@ function stylingProp( if (isClassBased) { updated = updateClassViaContext( context, lView, native, directiveIndex, prop, bindingIndex, - value as string | boolean | null); + value as string | boolean | null, false, firstUpdatePass); } else { updated = updateStyleViaContext( context, lView, native, directiveIndex, prop, bindingIndex, - value as string | SafeValue | null, sanitizer); + value as string | SafeValue | null, sanitizer, false, firstUpdatePass); } setElementExitFn(stylingApply); @@ -237,6 +236,7 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu const index = getSelectedIndex(); const lView = getLView(); const tNode = getTNode(index, lView); + const firstUpdatePass = lView[TVIEW].firstUpdatePass; const context = getStylesContext(tNode); const hasDirectiveInput = hasStyleInput(tNode); @@ -250,11 +250,12 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu // there should not be a situation where a directive host bindings function // evaluates the inputs (this should only happen in the template function) if (!isHostStyling() && hasDirectiveInput && styles !== NO_CHANGE) { - updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false); + updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false, firstUpdatePass); styles = NO_CHANGE; } - stylingMap(context, tNode, lView, bindingIndex, styles, false, hasDirectiveInput); + stylingMap( + context, tNode, firstUpdatePass, lView, bindingIndex, styles, false, hasDirectiveInput); } /** @@ -289,6 +290,7 @@ export function classMapInternal( elementIndex: number, classes: {[className: string]: any} | NO_CHANGE | string | null): void { const lView = getLView(); const tNode = getTNode(elementIndex, lView); + const firstUpdatePass = lView[TVIEW].firstUpdatePass; const context = getClassesContext(tNode); const hasDirectiveInput = hasClassInput(tNode); @@ -302,11 +304,12 @@ export function classMapInternal( // there should not be a situation where a directive host bindings function // evaluates the inputs (this should only happen in the template function) if (!isHostStyling() && hasDirectiveInput && classes !== NO_CHANGE) { - updateDirectiveInputValue(context, lView, tNode, bindingIndex, classes, true); + updateDirectiveInputValue(context, lView, tNode, bindingIndex, classes, true, firstUpdatePass); classes = NO_CHANGE; } - stylingMap(context, tNode, lView, bindingIndex, classes, true, hasDirectiveInput); + stylingMap( + context, tNode, firstUpdatePass, lView, bindingIndex, classes, true, hasDirectiveInput); } /** @@ -316,13 +319,12 @@ export function classMapInternal( * `[class]` bindings in Angular. */ function stylingMap( - context: TStylingContext, tNode: TNode, lView: LView, bindingIndex: number, - value: {[key: string]: any} | string | null, isClassBased: boolean, + context: TStylingContext, tNode: TNode, firstUpdatePass: boolean, lView: LView, + bindingIndex: number, value: {[key: string]: any} | string | null, isClassBased: boolean, hasDirectiveInput: boolean): void { const directiveIndex = getActiveDirectiveId(); const native = getNativeByTNode(tNode, lView) as RElement; const oldValue = getValue(lView, bindingIndex); - const hostBindingsMode = isHostStyling(); const sanitizer = getCurrentStyleSanitizer(); const valueHasChanged = hasValueChanged(oldValue, value); @@ -337,13 +339,13 @@ function stylingMap( // we check for this in the instruction code so that the context can be notified // about prop or map bindings so that the direct apply check can decide earlier // if it allows for context resolution to be bypassed. - if (!isContextLocked(context, hostBindingsMode)) { + if (firstUpdatePass) { patchConfig(context, TStylingConfig.HasMapBindings); } // Direct Apply Case: bypass context resolution and apply the // style/class map values directly to the element - if (allowDirectStyling(context, hostBindingsMode)) { + if (allowDirectStyling(context, firstUpdatePass)) { const sanitizerToUse = isClassBased ? null : sanitizer; const renderer = getRenderer(tNode, lView); applyStylingMapDirectly( @@ -367,11 +369,11 @@ function stylingMap( if (isClassBased) { updateClassViaContext( context, lView, native, directiveIndex, null, bindingIndex, stylingMapArr, - valueHasChanged); + valueHasChanged, firstUpdatePass); } else { updateStyleViaContext( context, lView, native, directiveIndex, null, bindingIndex, stylingMapArr, sanitizer, - valueHasChanged); + valueHasChanged, firstUpdatePass); } setElementExitFn(stylingApply); @@ -396,18 +398,17 @@ function stylingMap( * depending on the following situations: * * - If `oldValue !== newValue` - * - If `newValue` is `null` (but this is skipped if it is during the first update pass-- - * which is when the context is not locked yet) + * - If `newValue` is `null` (but this is skipped if it is during the first update pass) */ function updateDirectiveInputValue( context: TStylingContext, lView: LView, tNode: TNode, bindingIndex: number, newValue: any, - isClassBased: boolean): void { - const oldValue = lView[bindingIndex]; - if (oldValue !== newValue) { + isClassBased: boolean, firstUpdatePass: boolean): void { + const oldValue = getValue(lView, bindingIndex); + if (hasValueChanged(oldValue, newValue)) { // even if the value has changed we may not want to emit it to the // directive input(s) in the event that it is falsy during the // first update pass. - if (newValue || isContextLocked(context, false)) { + if (isStylingValueDefined(newValue) || !firstUpdatePass) { const inputName: string = isClassBased ? selectClassBasedInputName(tNode.inputs !) : 'style'; const inputs = tNode.inputs ![inputName] !; const initialValue = getInitialStylingValue(context); @@ -452,6 +453,7 @@ function normalizeStylingDirectiveInputValue( */ function stylingApply(): void { const lView = getLView(); + const tView = lView[TVIEW]; const elementIndex = getSelectedIndex(); const tNode = getTNode(elementIndex, lView); const native = getNativeByTNode(tNode, lView) as RElement; @@ -460,7 +462,9 @@ function stylingApply(): void { const sanitizer = getCurrentStyleSanitizer(); const classesContext = isStylingContext(tNode.classes) ? tNode.classes as TStylingContext : null; const stylesContext = isStylingContext(tNode.styles) ? tNode.styles as TStylingContext : null; - flushStyling(renderer, lView, classesContext, stylesContext, native, directiveIndex, sanitizer); + flushStyling( + renderer, lView, classesContext, stylesContext, native, directiveIndex, sanitizer, + tView.firstUpdatePass); resetCurrentStyleSanitizer(); } diff --git a/packages/core/src/render3/interfaces/styling.ts b/packages/core/src/render3/interfaces/styling.ts index 53762fff9b616..7a4b4b202c2ec 100644 --- a/packages/core/src/render3/interfaces/styling.ts +++ b/packages/core/src/render3/interfaces/styling.ts @@ -341,7 +341,7 @@ export const enum TStylingConfig { /** * The initial state of the styling context config. */ - Initial = 0b00000000, + Initial = 0b000000, /** * Whether or not there are any directives on this element. @@ -359,7 +359,7 @@ export const enum TStylingConfig { * 3. `` * 4. `` */ - HasDirectives = 0b00000001, + HasDirectives = 0b000001, /** * Whether or not there are prop-based bindings present. @@ -370,7 +370,7 @@ export const enum TStylingConfig { * 3. `@HostBinding('style.prop') x` * 4. `@HostBinding('class.prop') x` */ - HasPropBindings = 0b00000010, + HasPropBindings = 0b000010, /** * Whether or not there are map-based bindings present. @@ -381,7 +381,7 @@ export const enum TStylingConfig { * 3. `@HostBinding('style') x` * 4. `@HostBinding('class') x` */ - HasMapBindings = 0b00000100, + HasMapBindings = 0b000100, /** * Whether or not there are map-based and prop-based bindings present. @@ -402,7 +402,7 @@ export const enum TStylingConfig { * 2. map + prop: `
` * 3. map + map: `
` */ - HasCollisions = 0b00001000, + HasCollisions = 0b001000, /** * Whether or not the context contains initial styling values. @@ -413,7 +413,7 @@ export const enum TStylingConfig { * 3. `@Directive({ host: { 'style': 'width:200px' } })` * 4. `@Directive({ host: { 'class': 'one two three' } })` */ - HasInitialStyling = 0b000010000, + HasInitialStyling = 0b0010000, /** * Whether or not the context contains one or more template bindings. @@ -424,7 +424,7 @@ export const enum TStylingConfig { * 3. `
` * 4. `
` */ - HasTemplateBindings = 0b000100000, + HasTemplateBindings = 0b0100000, /** * Whether or not the context contains one or more host bindings. @@ -435,35 +435,13 @@ export const enum TStylingConfig { * 3. `@HostBinding('class') x` * 4. `@HostBinding('class.name') x` */ - HasHostBindings = 0b001000000, - - /** - * Whether or not the template bindings are allowed to be registered in the context. - * - * This flag is after one or more template-based style/class bindings were - * set and processed for an element. Once the bindings are processed then a call - * to stylingApply is issued and the lock will be put into place. - * - * Note that this is only set once. - */ - TemplateBindingsLocked = 0b010000000, - - /** - * Whether or not the host bindings are allowed to be registered in the context. - * - * This flag is after one or more host-based style/class bindings were - * set and processed for an element. Once the bindings are processed then a call - * to stylingApply is issued and the lock will be put into place. - * - * Note that this is only set once. - */ - HostBindingsLocked = 0b100000000, + HasHostBindings = 0b1000000, /** A Mask of all the configurations */ - Mask = 0b111111111, + Mask = 0b1111111, /** Total amount of configuration bits used */ - TotalBits = 9, + TotalBits = 7, } /** diff --git a/packages/core/src/render3/styling/bindings.ts b/packages/core/src/render3/styling/bindings.ts index 4d2ee44d3fc25..7a01d81b56484 100644 --- a/packages/core/src/render3/styling/bindings.ts +++ b/packages/core/src/render3/styling/bindings.ts @@ -11,7 +11,7 @@ import {global} from '../../util/global'; 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, TEMPLATE_DIRECTIVE_INDEX, concatString, forceStylesAsString, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingMapArray, isStylingValueDefined, lockContext, normalizeIntoStylingMap, 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, concatString, forceStylesAsString, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isHostStylingActive, isSanitizationRequired, isStylingMapArray, isStylingValueDefined, normalizeIntoStylingMap, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils'; import {getStylingState, resetStylingState} from './state'; @@ -56,20 +56,19 @@ const STYLING_INDEX_FOR_MAP_BINDING = 0; export function updateClassViaContext( context: TStylingContext, data: LStylingData, element: RElement, directiveIndex: number, prop: string | null, bindingIndex: number, - value: boolean | string | null | undefined | StylingMapArray | NO_CHANGE, - forceUpdate?: boolean): boolean { + value: boolean | string | null | undefined | StylingMapArray | NO_CHANGE, forceUpdate: boolean, + firstUpdatePass: boolean): boolean { const isMapBased = !prop; const state = getStylingState(element, directiveIndex); const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++; - const hostBindingsMode = isHostStylingActive(state.sourceIndex); // even if the initial value is a `NO_CHANGE` value (e.g. interpolation or [ngClass]) // then we still need to register the binding within the context so that the context - // is aware of the binding before it gets locked. - if (!isContextLocked(context, hostBindingsMode) || value !== NO_CHANGE) { + // is aware of the binding even if things change after the first update pass. + if (firstUpdatePass || value !== NO_CHANGE) { const updated = updateBindingData( - context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate, - false); + context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate, false, + firstUpdatePass); if (updated || forceUpdate) { // We flip the bit in the bitMask to reflect that the binding // at the `index` slot has changed. This identifies to the flushing @@ -97,22 +96,21 @@ export function updateStyleViaContext( context: TStylingContext, data: LStylingData, element: RElement, directiveIndex: number, prop: string | null, bindingIndex: number, value: string | number | SafeValue | null | undefined | StylingMapArray | NO_CHANGE, - sanitizer: StyleSanitizeFn | null, forceUpdate?: boolean): boolean { + sanitizer: StyleSanitizeFn | null, forceUpdate: boolean, firstUpdatePass: boolean): boolean { const isMapBased = !prop; const state = getStylingState(element, directiveIndex); const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++; - const hostBindingsMode = isHostStylingActive(state.sourceIndex); // even if the initial value is a `NO_CHANGE` value (e.g. interpolation or [ngStyle]) // then we still need to register the binding within the context so that the context - // is aware of the binding before it gets locked. - if (!isContextLocked(context, hostBindingsMode) || value !== NO_CHANGE) { + // is aware of the binding even if things change after the first update pass. + if (firstUpdatePass || value !== NO_CHANGE) { const sanitizationRequired = isMapBased ? true : (sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false); const updated = updateBindingData( context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate, - sanitizationRequired); + sanitizationRequired, firstUpdatePass); if (updated || forceUpdate) { // We flip the bit in the bitMask to reflect that the binding // at the `index` slot has changed. This identifies to the flushing @@ -141,9 +139,9 @@ function updateBindingData( context: TStylingContext, data: LStylingData, counterIndex: number, sourceIndex: number, prop: string | null, bindingIndex: number, value: string | SafeValue | number | boolean | null | undefined | StylingMapArray, - forceUpdate?: boolean, sanitizationRequired?: boolean): boolean { + forceUpdate: boolean, sanitizationRequired: boolean, firstUpdatePass: boolean): boolean { const hostBindingsMode = isHostStylingActive(sourceIndex); - if (!isContextLocked(context, hostBindingsMode)) { + if (firstUpdatePass) { // this will only happen during the first update pass of the // context. The reason why we can't use `tView.firstCreatePass` // here is because its not guaranteed to be true when the first @@ -409,16 +407,16 @@ function addNewSourceColumn(context: TStylingContext): void { export function flushStyling( renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, classesContext: TStylingContext | null, stylesContext: TStylingContext | null, - element: RElement, directiveIndex: number, styleSanitizer: StyleSanitizeFn | null): void { + element: RElement, directiveIndex: number, styleSanitizer: StyleSanitizeFn | null, + firstUpdatePass: boolean): void { ngDevMode && ngDevMode.flushStyling++; const state = getStylingState(element, directiveIndex); const hostBindingsMode = isHostStylingActive(state.sourceIndex); if (stylesContext) { - if (!isContextLocked(stylesContext, hostBindingsMode)) { - lockAndFinalizeContext(stylesContext, hostBindingsMode); - } + firstUpdatePass && syncContextInitialStyling(stylesContext); + if (state.stylesBitMask !== 0) { applyStylingViaContext( stylesContext, renderer, element, data, state.stylesBitMask, setStyle, styleSanitizer, @@ -427,9 +425,8 @@ export function flushStyling( } if (classesContext) { - if (!isContextLocked(classesContext, hostBindingsMode)) { - lockAndFinalizeContext(classesContext, hostBindingsMode); - } + firstUpdatePass && syncContextInitialStyling(classesContext); + if (state.classesBitMask !== 0) { applyStylingViaContext( classesContext, renderer, element, data, state.classesBitMask, setClass, null, @@ -441,35 +438,64 @@ export function flushStyling( } /** - * Locks the context (so no more bindings can be added) and also copies over initial class/style - * values into their binding areas. + * Registers all static styling values into the context as default values. + * + * Static styles are stored on the `tNode.styles` and `tNode.classes` + * properties as instances of `StylingMapArray`. When an instance of + * `TStylingContext` is assigned to `tNode.styles` and `tNode.classes` + * then the existing initial styling values are copied into the the + * `InitialStylingValuePosition` slot. + * + * Because all static styles/classes are collected and registered on + * the initial styling array each time a directive is instantiated, + * the context may not yet know about the static values. When this + * function is called it will copy over all the static style/class + * values from the initial styling array into the context as default + * values for each of the matching entries in the context. + * + * Let's imagine the following example: + * + * ```html + *
+ * ... + *
+ * ``` + * + * When the code above is processed, the underlying element/styling + * instructions will create an instance of `TStylingContext` for + * the `tNode.styles` property. Here's what that looks like: + * + * ```typescript + * tNode.styles = [ + * // ... + * // initial styles + * ['color:red; height:200px', 'color', 'red', 'height', '200px'], * - * There are two main actions that take place in this function: + * 0, 0b1, 0b0, 'color', 20, null, // [style.color] binding + * ] + * ``` * - * - Locking the context: - * Locking the context is required so that the style/class instructions know NOT to - * register a binding again after the first update pass has run. If a locking bit was - * not used then it would need to scan over the context each time an instruction is run - * (which is expensive). + * After this function is called it will balance out the context with + * the static `color` and `height` values and set them as defaults within + * the context: * - * - Patching initial values: - * Directives and component host bindings may include static class/style values which are - * bound to the host element. When this happens, the styling context will need to be informed - * so it can use these static styling values as defaults when a matching binding is falsy. - * These initial styling values are read from the initial styling values slot within the - * provided `TStylingContext` (which is an instance of a `StylingMapArray`). This inner map will - * be updated each time a host binding applies its static styling values (via `elementHostAttrs`) - * so these values are only read at this point because this is the very last point before the - * first style/class values are flushed to the element. + * ```typescript + * tNode.styles = [ + * // ... + * // initial styles + * ['color:red; height:200px', 'color', 'red', 'height', '200px'], * - * Note that the `TStylingContext` styling context contains two locks: one for template bindings - * and another for host bindings. Either one of these locks will be set when styling is applied - * during the template binding flush and/or during the host bindings flush. + * 0, 0b1, 0b0, 'color', 20, 'red', + * 0, 0b0, 0b0, 'height', 0, '200px', + * ] + * ``` */ -function lockAndFinalizeContext(context: TStylingContext, hostBindingsMode: boolean): void { - const initialValues = getStylingMapArray(context) !; - updateInitialStylingOnContext(context, initialValues); - lockContext(context, hostBindingsMode); +function syncContextInitialStyling(context: TStylingContext): void { + // the TStylingContext always has initial style/class values which are + // stored in styling array format. + updateInitialStylingOnContext(context, getStylingMapArray(context) !); } /** diff --git a/packages/core/src/render3/styling/styling_debug.ts b/packages/core/src/render3/styling/styling_debug.ts index 1369d411b750b..13e97065d173e 100644 --- a/packages/core/src/render3/styling/styling_debug.ts +++ b/packages/core/src/render3/styling/styling_debug.ts @@ -11,7 +11,7 @@ import {RElement} from '../interfaces/renderer'; import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from '../interfaces/styling'; import {getCurrentStyleSanitizer} from '../state'; import {attachDebugObject} from '../util/debug_utils'; -import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValue, getValuesCount, hasConfig, isContextLocked, isSanitizationRequired, isStylingContext, normalizeIntoStylingMap, setValue} from '../util/styling_utils'; +import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValue, getValuesCount, hasConfig, isSanitizationRequired, isStylingContext, normalizeIntoStylingMap, setValue} from '../util/styling_utils'; import {applyStylingViaContext} from './bindings'; import {activateStylingMapFeature} from './map_based_bindings'; @@ -55,14 +55,12 @@ export interface DebugStylingContext { * A debug/testing-oriented summary of `TStylingConfig`. */ export interface DebugStylingConfig { - hasMapBindings: boolean; // - hasPropBindings: boolean; // - hasCollisions: boolean; // - hasTemplateBindings: boolean; // - hasHostBindings: boolean; // - templateBindingsLocked: boolean; // - hostBindingsLocked: boolean; // - allowDirectStyling: boolean; // + hasMapBindings: boolean; // + hasPropBindings: boolean; // + hasCollisions: boolean; // + hasTemplateBindings: boolean; // + hasHostBindings: boolean; // + allowDirectStyling: boolean; // } @@ -496,19 +494,17 @@ function buildConfig(context: TStylingContext) { const hasCollisions = hasConfig(context, TStylingConfig.HasCollisions); const hasTemplateBindings = hasConfig(context, TStylingConfig.HasTemplateBindings); const hasHostBindings = hasConfig(context, TStylingConfig.HasHostBindings); - const templateBindingsLocked = hasConfig(context, TStylingConfig.TemplateBindingsLocked); - const hostBindingsLocked = hasConfig(context, TStylingConfig.HostBindingsLocked); - const allowDirectStyling = - _allowDirectStyling(context, false) || _allowDirectStyling(context, true); + // `firstTemplatePass` here is false because the context has already been constructed + // directly within the behavior of the debugging tools (outside of style/class debugging, + // the context is constructed during the first template pass). + const allowDirectStyling = _allowDirectStyling(context, false); return { - hasMapBindings, // - hasPropBindings, // - hasCollisions, // - hasTemplateBindings, // - hasHostBindings, // - templateBindingsLocked, // - hostBindingsLocked, // - allowDirectStyling, // + hasMapBindings, // + hasPropBindings, // + hasCollisions, // + hasTemplateBindings, // + hasHostBindings, // + allowDirectStyling, // }; } diff --git a/packages/core/src/render3/util/styling_utils.ts b/packages/core/src/render3/util/styling_utils.ts index 8f8eb65b8582e..fd62e9f5c822a 100644 --- a/packages/core/src/render3/util/styling_utils.ts +++ b/packages/core/src/render3/util/styling_utils.ts @@ -74,15 +74,16 @@ 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. there are no directives present AND ngDevMode is falsy - * 2. context is locked for template or host bindings (depending on `hostBindingsMode`) + * 1. there are no directives present AND `ngDevMode` is falsy + * 2. the `firstUpdatePass` has not already run (which means that + * there are more bindings to register and, therefore, direct + * style/class application is not yet possible) * 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 { +export function allowDirectStyling(context: TStylingContext, firstUpdatePass: boolean): boolean { let allow = false; const config = getConfig(context); - 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 @@ -93,8 +94,8 @@ export function allowDirectStyling(context: TStylingContext, hostBindingsMode: b // `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) { + allow = ngDevMode ? !firstUpdatePass : true; + } else if (!firstUpdatePass) { const hasNoCollisions = (config & TStylingConfig.HasCollisions) === 0; const hasOnlyMapsOrOnlyProps = (config & TStylingConfig.HasPropAndMapBindings) !== TStylingConfig.HasPropAndMapBindings; @@ -172,19 +173,6 @@ export function getValue(data: LStylingData, bindingIndex: number): T|n return bindingIndex !== 0 ? data[bindingIndex] as T : null; } -export function lockContext(context: TStylingContext, hostBindingsMode: boolean): void { - patchConfig(context, getLockedConfig(hostBindingsMode)); -} - -export function isContextLocked(context: TStylingContext, hostBindingsMode: boolean): boolean { - return hasConfig(context, getLockedConfig(hostBindingsMode)); -} - -export function getLockedConfig(hostBindingsMode: boolean) { - return hostBindingsMode ? TStylingConfig.HostBindingsLocked : - TStylingConfig.TemplateBindingsLocked; -} - export function getPropValuesStartPosition(context: TStylingContext) { let startPosition = TStylingContextIndex.ValuesStartPosition; if (hasConfig(context, TStylingConfig.HasMapBindings)) { diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index ce81644958b82..6cd8042102a95 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -692,9 +692,6 @@ { "name": "getLViewParent" }, - { - "name": "getLockedConfig" - }, { "name": "getMapProp" }, @@ -917,9 +914,6 @@ { "name": "isContentQueryHost" }, - { - "name": "isContextLocked" - }, { "name": "isCreationMode" }, @@ -938,9 +932,6 @@ { "name": "isForwardRef" }, - { - "name": "isHostStyling" - }, { "name": "isHostStylingActive" }, @@ -1007,12 +998,6 @@ { "name": "locateHostElement" }, - { - "name": "lockAndFinalizeContext" - }, - { - "name": "lockContext" - }, { "name": "looseIdentical" }, @@ -1292,6 +1277,9 @@ { "name": "stylingProp" }, + { + "name": "syncContextInitialStyling" + }, { "name": "syncViewWithBlueprint" }, diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index 68cfff3b1e493..fbe13c2534d4b 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -170,21 +170,21 @@ describe('instructions', () => { const detectedValues: string[] = []; const sanitizerInterceptor = new MockSanitizerInterceptor(value => { detectedValues.push(value); }); - const fixture = - createTemplateFixtureWithSanitizer(() => createDiv(), 1, sanitizerInterceptor); - - fixture.update(() => { - ɵɵstyleSanitizer(sanitizerInterceptor.getStyleSanitizer()); - ɵɵstyleMap({ - 'background-image': 'background-image', - 'background': 'background', - 'border-image': 'border-image', - 'list-style': 'list-style', - 'list-style-image': 'list-style-image', - 'filter': 'filter', - 'width': 'width' - }); - }); + const fixture = new TemplateFixture( + () => { return createDiv(); }, // + () => { + ɵɵstyleSanitizer(sanitizerInterceptor.getStyleSanitizer()); + ɵɵstyleMap({ + 'background-image': 'background-image', + 'background': 'background', + 'border-image': 'border-image', + 'list-style': 'list-style', + 'list-style-image': 'list-style-image', + 'filter': 'filter', + 'width': 'width' + }); + }, + 1, 0, null, null, sanitizerInterceptor); const props = detectedValues.sort(); expect(props).toEqual([ @@ -197,8 +197,8 @@ describe('instructions', () => { function createDivWithStyling() { ɵɵelement(0, 'div'); } it('should add class', () => { - const fixture = new TemplateFixture(createDivWithStyling, () => {}, 1); - fixture.update(() => { ɵɵclassMap('multiple classes'); }); + const fixture = + new TemplateFixture(createDivWithStyling, () => { ɵɵclassMap('multiple classes'); }, 1); expect(fixture.html).toEqual('
'); }); }); @@ -486,8 +486,3 @@ function stripStyleWsCharacters(value: string): string { // color: blue; => color:blue return value.replace(/;/g, '').replace(/:\s+/g, ':'); } - -function createTemplateFixtureWithSanitizer( - buildFn: () => any, decls: number, sanitizer: Sanitizer) { - return new TemplateFixture(buildFn, () => {}, decls, 0, null, null, sanitizer); -}