fix(ivy): ensure parent/sub-class components evaluate styling correctly (#29602)

The new styling algorithm in angular is designed to evaluate host
bindings stylinh priority in order of directive evaluation order. This,
however, does not work with respect to parent/sub-class directives
because sub-class host bindings are run after the parent host bindings
but still have priority. This patch ensures that the host styling bindings
for parent and sub-class components/directives are executed with respect
to the styling algorithm prioritization.

Jira Issue: FW-1132

PR Close #29602
This commit is contained in:
Matias Niemelä
2019-04-02 16:16:00 -07:00
committed by Igor Minar
parent 5c13feebfd
commit ec56354306
19 changed files with 1404 additions and 913 deletions

View File

@ -10,14 +10,14 @@ import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {AttributeMarker, TAttributes} from '../interfaces/node';
import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player';
import {RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {DirectiveOwnerAndPlayerBuilderIndex, DirectiveRegistryValues, DirectiveRegistryValuesIndex, InitialStylingValues, InitialStylingValuesIndex, MapBasedOffsetValues, MapBasedOffsetValuesIndex, SinglePropOffsetValues, SinglePropOffsetValuesIndex, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
import {DirectiveOwnerAndPlayerBuilderIndex, DirectiveRegistryValuesIndex, InitialStylingValues, InitialStylingValuesIndex, MapBasedOffsetValues, MapBasedOffsetValuesIndex, SinglePropOffsetValues, SinglePropOffsetValuesIndex, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
import {LView, RootContext} from '../interfaces/view';
import {NO_CHANGE} from '../tokens';
import {getRootContext} from '../util/view_traversal_utils';
import {allowFlush as allowHostInstructionsQueueFlush, flushQueue as flushHostInstructionsQueue} from './host_instructions_queue';
import {BoundPlayerFactory} from './player_factory';
import {addPlayerInternal, allocPlayerContext, allocateDirectiveIntoContext, createEmptyStylingContext, getPlayerContext} from './util';
import {addPlayerInternal, allocPlayerContext, allocateOrUpdateDirectiveIntoContext, createEmptyStylingContext, getPlayerContext} from './util';
/**
@ -42,9 +42,9 @@ import {addPlayerInternal, allocPlayerContext, allocateDirectiveIntoContext, cre
* Creates a new StylingContext an fills it with the provided static styling attribute values.
*/
export function initializeStaticContext(
attrs: TAttributes, stylingStartIndex: number, directiveRef?: any | null): StylingContext {
attrs: TAttributes, stylingStartIndex: number, directiveIndex: number = 0): StylingContext {
const context = createEmptyStylingContext();
patchContextWithStaticAttrs(context, attrs, stylingStartIndex, directiveRef);
patchContextWithStaticAttrs(context, attrs, stylingStartIndex, directiveIndex);
return context;
}
@ -57,25 +57,14 @@ export function initializeStaticContext(
* assigned to the context
* @param attrsStylingStartIndex what index to start iterating within the
* provided `attrs` array to start reading style and class values
* @param directiveRef the directive instance with which static data is associated with.
*/
export function patchContextWithStaticAttrs(
context: StylingContext, attrs: TAttributes, attrsStylingStartIndex: number,
directiveRef?: any | null): void {
directiveIndex: number): void {
// this means the context has already been set and instantiated
if (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) return;
// If the styling context has already been patched with the given directive's bindings,
// then there is no point in doing it again. The reason why this may happen (the directive
// styling being patched twice) is because the `stylingBinding` function is called each time
// an element is created (both within a template function and within directive host bindings).
const directives = context[StylingIndex.DirectiveRegistryPosition];
let detectedIndex = getDirectiveRegistryValuesIndexOf(directives, directiveRef || null);
if (detectedIndex === -1) {
// this is a new directive which we have not seen yet.
detectedIndex = allocateDirectiveIntoContext(context, directiveRef);
}
const directiveIndex = detectedIndex / DirectiveRegistryValuesIndex.Size;
allocateOrUpdateDirectiveIntoContext(context, directiveIndex);
let initialClasses: InitialStylingValues|null = null;
let initialStyles: InitialStylingValues|null = null;
@ -199,7 +188,6 @@ export function allowNewBindingsForStylingContext(context: StylingContext): bool
* reference the provided directive.
*
* @param context the existing styling context
* @param directiveRef the directive that the new bindings will reference
* @param classBindingNames an array of class binding names that will be added to the context
* @param styleBindingNames an array of style binding names that will be added to the context
* @param styleSanitizer an optional sanitizer that handle all sanitization on for each of
@ -207,13 +195,14 @@ export function allowNewBindingsForStylingContext(context: StylingContext): bool
* instance will only be active if and when the directive updates the bindings that it owns.
*/
export function updateContextWithBindings(
context: StylingContext, directiveRef: any | null, classBindingNames?: string[] | null,
context: StylingContext, directiveIndex: number, classBindingNames?: string[] | null,
styleBindingNames?: string[] | null, styleSanitizer?: StyleSanitizeFn | null) {
if (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) return;
// this means the context has already been patched with the directive's bindings
const directiveIndex = findOrPatchDirectiveIntoRegistry(context, directiveRef, styleSanitizer);
if (directiveIndex === -1) {
const isNewDirective =
findOrPatchDirectiveIntoRegistry(context, directiveIndex, false, styleSanitizer);
if (!isNewDirective) {
// this means the directive has already been patched in ... No point in doing anything
return;
}
@ -470,46 +459,22 @@ export function updateContextWithBindings(
* Searches through the existing registry of directives
*/
export function findOrPatchDirectiveIntoRegistry(
context: StylingContext, directiveRef: any, styleSanitizer?: StyleSanitizeFn | null) {
const directiveRefs = context[StylingIndex.DirectiveRegistryPosition];
const nextOffsetInsertionIndex = context[StylingIndex.SinglePropOffsetPositions].length;
context: StylingContext, directiveIndex: number, staticModeOnly: boolean,
styleSanitizer?: StyleSanitizeFn | null): boolean {
const directiveRegistry = context[StylingIndex.DirectiveRegistryPosition];
const index = directiveIndex * DirectiveRegistryValuesIndex.Size;
const singlePropStartPosition = index + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset;
let directiveIndex: number;
let detectedIndex = getDirectiveRegistryValuesIndexOf(directiveRefs, directiveRef);
// this means that the directive has already been registered into the registry
if (index < directiveRegistry.length &&
(directiveRegistry[singlePropStartPosition] as number) >= 0)
return false;
if (detectedIndex === -1) {
detectedIndex = directiveRefs.length;
directiveIndex = directiveRefs.length / DirectiveRegistryValuesIndex.Size;
allocateDirectiveIntoContext(context, directiveRef);
directiveRefs[detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] =
nextOffsetInsertionIndex;
directiveRefs[detectedIndex + DirectiveRegistryValuesIndex.StyleSanitizerOffset] =
styleSanitizer || null;
} else {
const singlePropStartPosition =
detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset;
if (directiveRefs[singlePropStartPosition] ! >= 0) {
// the directive has already been patched into the context
return -1;
}
directiveIndex = detectedIndex / DirectiveRegistryValuesIndex.Size;
// because the directive already existed this means that it was set during elementHostAttrs or
// elementStart which means that the binding values were not here. Therefore, the values below
// need to be applied so that single class and style properties can be assigned later.
const singlePropPositionIndex =
detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset;
directiveRefs[singlePropPositionIndex] = nextOffsetInsertionIndex;
// the sanitizer is also apart of the binding process and will be used when bindings are
// applied.
const styleSanitizerIndex = detectedIndex + DirectiveRegistryValuesIndex.StyleSanitizerOffset;
directiveRefs[styleSanitizerIndex] = styleSanitizer || null;
}
return directiveIndex;
const singlePropsStartIndex =
staticModeOnly ? -1 : context[StylingIndex.SinglePropOffsetPositions].length;
allocateOrUpdateDirectiveIntoContext(
context, directiveIndex, singlePropsStartIndex, styleSanitizer);
return true;
}
function getMatchingBindingIndex(
@ -543,18 +508,13 @@ function getMatchingBindingIndex(
* newly provided style values.
* @param classesInput The key/value map of CSS class names that will be used for the update.
* @param stylesInput The key/value map of CSS styles that will be used for the update.
* @param directiveRef an optional reference to the directive responsible
* for this binding change. If present then style binding will only
* actualize if the directive has ownership over this binding
* (see styling.ts#directives for more information about the algorithm).
*/
export function updateStylingMap(
context: StylingContext, classesInput: {[key: string]: any} | string |
BoundPlayerFactory<null|string|{[key: string]: any}>| null,
stylesInput?: {[key: string]: any} | BoundPlayerFactory<null|{[key: string]: any}>| null,
directiveRef?: any): void {
const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null);
directiveIndex: number = 0): void {
ngDevMode && assertValidDirectiveIndex(context, directiveIndex);
classesInput = classesInput || null;
stylesInput = stylesInput || null;
const ignoreAllClassUpdates = isMultiValueCacheHit(context, true, directiveIndex, classesInput);
@ -887,7 +847,6 @@ function patchStylingMapIntoContext(
if (dirty) {
setContextDirty(context, true);
setDirectiveDirty(context, directiveIndex, true);
}
return totalNewAllocatedSlots;
@ -901,18 +860,14 @@ function patchStylingMapIntoContext(
* newly provided class value.
* @param offset The index of the CSS class which is being updated.
* @param addOrRemove Whether or not to add or remove the CSS class
* @param directiveRef an optional reference to the directive responsible
* for this binding change. If present then style binding will only
* actualize if the directive has ownership over this binding
* (see styling.ts#directives for more information about the algorithm).
* @param forceOverride whether or not to skip all directive prioritization
* and just apply the value regardless.
*/
export function updateClassProp(
context: StylingContext, offset: number,
input: boolean | BoundPlayerFactory<boolean|null>| null, directiveRef?: any,
input: boolean | BoundPlayerFactory<boolean|null>| null, directiveIndex: number = 0,
forceOverride?: boolean): void {
updateSingleStylingValue(context, offset, input, true, directiveRef, forceOverride);
updateSingleStylingValue(context, offset, input, true, directiveIndex, forceOverride);
}
/**
@ -928,25 +883,21 @@ export function updateClassProp(
* newly provided style value.
* @param offset The index of the property which is being updated.
* @param value The CSS style value that will be assigned
* @param directiveRef an optional reference to the directive responsible
* for this binding change. If present then style binding will only
* actualize if the directive has ownership over this binding
* (see styling.ts#directives for more information about the algorithm).
* @param forceOverride whether or not to skip all directive prioritization
* and just apply the value regardless.
*/
export function updateStyleProp(
context: StylingContext, offset: number,
input: string | boolean | null | BoundPlayerFactory<string|boolean|null>, directiveRef?: any,
forceOverride?: boolean): void {
updateSingleStylingValue(context, offset, input, false, directiveRef, forceOverride);
input: string | boolean | null | BoundPlayerFactory<string|boolean|null>,
directiveIndex: number = 0, forceOverride?: boolean): void {
updateSingleStylingValue(context, offset, input, false, directiveIndex, forceOverride);
}
function updateSingleStylingValue(
context: StylingContext, offset: number,
input: string | boolean | null | BoundPlayerFactory<string|boolean|null>, isClassBased: boolean,
directiveRef: any, forceOverride?: boolean): void {
const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null);
directiveIndex: number, forceOverride?: boolean): void {
ngDevMode && assertValidDirectiveIndex(context, directiveIndex);
const singleIndex = getSinglePropIndexValue(context, directiveIndex, offset, isClassBased);
const currValue = getValue(context, singleIndex);
const currFlag = getPointers(context, singleIndex);
@ -1001,7 +952,6 @@ function updateSingleStylingValue(
setDirty(context, indexForMulti, multiDirty);
setDirty(context, singleIndex, singleDirty);
setDirectiveDirty(context, directiveIndex, true);
setContextDirty(context, true);
}
@ -1029,120 +979,128 @@ function updateSingleStylingValue(
* to this key/value map instead of being renderered via the renderer.
* @param stylesStore if provided, the updated style values will be applied
* to this key/value map instead of being renderered via the renderer.
* @param directiveRef an optional directive that will be used to target which
* styling values are rendered. If left empty, only the bindings that are
* registered on the template will be rendered.
* @returns number the total amount of players that got queued for animation (if any)
*/
export function renderStyling(
context: StylingContext, renderer: Renderer3, rootOrView: RootContext | LView,
context: StylingContext, renderer: Renderer3 | null, rootOrView: RootContext | LView,
isFirstRender: boolean, classesStore?: BindingStore | null, stylesStore?: BindingStore | null,
directiveRef?: any): number {
directiveIndex: number = 0): number {
let totalPlayersQueued = 0;
const targetDirectiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null);
if (isContextDirty(context) && isDirectiveDirty(context, targetDirectiveIndex)) {
const flushPlayerBuilders: any =
context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty;
const native = context[StylingIndex.ElementPosition] !;
const multiStartIndex = getMultiStylesStartIndex(context);
// this prevents multiple attempts to render style/class values on
// the same element...
if (allowHostInstructionsQueueFlush(context, directiveIndex)) {
// all styling instructions present within any hostBindings functions
// do not update the context immediately when called. They are instead
// queued up and applied to the context right at this point. Why? This
// is because Angular evaluates component/directive and directive
// sub-class code at different points and it's important that the
// styling values are applied to the context in the right order
// (see `interfaces/styling.ts` for more information).
flushHostInstructionsQueue(context);
let stillDirty = false;
for (let i = StylingIndex.SingleStylesStartPosition; i < context.length;
i += StylingIndex.Size) {
// there is no point in rendering styles that have not changed on screen
if (isDirty(context, i)) {
const flag = getPointers(context, i);
const directiveIndex = getDirectiveIndexFromEntry(context, i);
if (targetDirectiveIndex !== directiveIndex) {
stillDirty = true;
continue;
}
if (isContextDirty(context)) {
// this is here to prevent things like <ng-container [style] [class]>...</ng-container>
// or if there are any host style or class bindings present in a directive set on
// a container node
const native = context[StylingIndex.ElementPosition] !as HTMLElement;
const prop = getProp(context, i);
const value = getValue(context, i);
const styleSanitizer =
(flag & StylingFlags.Sanitize) ? getStyleSanitizer(context, directiveIndex) : null;
const playerBuilder = getPlayerBuilder(context, i);
const isClassBased = flag & StylingFlags.Class ? true : false;
const isInSingleRegion = i < multiStartIndex;
const flushPlayerBuilders: any =
context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty;
const multiStartIndex = getMultiStylesStartIndex(context);
let valueToApply: string|boolean|null = value;
for (let i = StylingIndex.SingleStylesStartPosition; i < context.length;
i += StylingIndex.Size) {
// there is no point in rendering styles that have not changed on screen
if (isDirty(context, i)) {
const flag = getPointers(context, i);
const directiveIndex = getDirectiveIndexFromEntry(context, i);
const prop = getProp(context, i);
const value = getValue(context, i);
const styleSanitizer =
(flag & StylingFlags.Sanitize) ? getStyleSanitizer(context, directiveIndex) : null;
const playerBuilder = getPlayerBuilder(context, i);
const isClassBased = flag & StylingFlags.Class ? true : false;
const isInSingleRegion = i < multiStartIndex;
// VALUE DEFER CASE 1: Use a multi value instead of a null single value
// this check implies that a single value was removed and we
// should now defer to a multi value and use that (if set).
if (isInSingleRegion && !valueExists(valueToApply, isClassBased)) {
// single values ALWAYS have a reference to a multi index
const multiIndex = getMultiOrSingleIndex(flag);
valueToApply = getValue(context, multiIndex);
}
let valueToApply: string|boolean|null = value;
// VALUE DEFER CASE 2: Use the initial value if all else fails (is falsy)
// the initial value will always be a string or null,
// therefore we can safely adopt it in case there's nothing else
// note that this should always be a falsy check since `false` is used
// for both class and style comparisons (styles can't be false and false
// classes are turned off and should therefore defer to their initial values)
// Note that we ignore class-based deferals because otherwise a class can never
// be removed in the case that it exists as true in the initial classes list...
if (!valueExists(valueToApply, isClassBased)) {
valueToApply = getInitialValue(context, flag);
}
// if the first render is true then we do not want to start applying falsy
// values to the DOM element's styling. Otherwise then we know there has
// been a change and even if it's falsy then it's removing something that
// was truthy before.
const doApplyValue = isFirstRender ? valueToApply : true;
if (doApplyValue) {
if (isClassBased) {
setClass(
native, prop, valueToApply ? true : false, renderer, classesStore, playerBuilder);
} else {
setStyle(
native, prop, valueToApply as string | null, renderer, styleSanitizer, stylesStore,
playerBuilder);
// VALUE DEFER CASE 1: Use a multi value instead of a null single value
// this check implies that a single value was removed and we
// should now defer to a multi value and use that (if set).
if (isInSingleRegion && !valueExists(valueToApply, isClassBased)) {
// single values ALWAYS have a reference to a multi index
const multiIndex = getMultiOrSingleIndex(flag);
valueToApply = getValue(context, multiIndex);
}
}
setDirty(context, i, false);
}
}
// VALUE DEFER CASE 2: Use the initial value if all else fails (is falsy)
// the initial value will always be a string or null,
// therefore we can safely adopt it in case there's nothing else
// note that this should always be a falsy check since `false` is used
// for both class and style comparisons (styles can't be false and false
// classes are turned off and should therefore defer to their initial values)
// Note that we ignore class-based deferals because otherwise a class can never
// be removed in the case that it exists as true in the initial classes list...
if (!valueExists(valueToApply, isClassBased)) {
valueToApply = getInitialValue(context, flag);
}
if (flushPlayerBuilders) {
const rootContext =
Array.isArray(rootOrView) ? getRootContext(rootOrView) : rootOrView as RootContext;
const playerContext = getPlayerContext(context) !;
const playersStartIndex = playerContext[PlayerIndex.NonBuilderPlayersStart];
for (let i = PlayerIndex.PlayerBuildersStartPosition; i < playersStartIndex;
i += PlayerIndex.PlayerAndPlayerBuildersTupleSize) {
const builder = playerContext[i] as ClassAndStylePlayerBuilder<any>| null;
const playerInsertionIndex = i + PlayerIndex.PlayerOffsetPosition;
const oldPlayer = playerContext[playerInsertionIndex] as Player | null;
if (builder) {
const player = builder.buildPlayer(oldPlayer, isFirstRender);
if (player !== undefined) {
if (player != null) {
const wasQueued = addPlayerInternal(
playerContext, rootContext, native as HTMLElement, player, playerInsertionIndex);
wasQueued && totalPlayersQueued++;
}
if (oldPlayer) {
oldPlayer.destroy();
// if the first render is true then we do not want to start applying falsy
// values to the DOM element's styling. Otherwise then we know there has
// been a change and even if it's falsy then it's removing something that
// was truthy before.
const doApplyValue = renderer && (isFirstRender ? valueToApply : true);
if (doApplyValue) {
if (isClassBased) {
setClass(
native, prop, valueToApply ? true : false, renderer !, classesStore,
playerBuilder);
} else {
setStyle(
native, prop, valueToApply as string | null, renderer !, styleSanitizer,
stylesStore, playerBuilder);
}
}
} else if (oldPlayer) {
// the player builder has been removed ... therefore we should delete the associated
// player
oldPlayer.destroy();
setDirty(context, i, false);
}
}
setContextPlayersDirty(context, false);
}
setDirectiveDirty(context, targetDirectiveIndex, false);
setContextDirty(context, stillDirty);
if (flushPlayerBuilders) {
const rootContext =
Array.isArray(rootOrView) ? getRootContext(rootOrView) : rootOrView as RootContext;
const playerContext = getPlayerContext(context) !;
const playersStartIndex = playerContext[PlayerIndex.NonBuilderPlayersStart];
for (let i = PlayerIndex.PlayerBuildersStartPosition; i < playersStartIndex;
i += PlayerIndex.PlayerAndPlayerBuildersTupleSize) {
const builder = playerContext[i] as ClassAndStylePlayerBuilder<any>| null;
const playerInsertionIndex = i + PlayerIndex.PlayerOffsetPosition;
const oldPlayer = playerContext[playerInsertionIndex] as Player | null;
if (builder) {
const player = builder.buildPlayer(oldPlayer, isFirstRender);
if (player !== undefined) {
if (player != null) {
const wasQueued = addPlayerInternal(
playerContext, rootContext, native as HTMLElement, player,
playerInsertionIndex);
wasQueued && totalPlayersQueued++;
}
if (oldPlayer) {
oldPlayer.destroy();
}
}
} else if (oldPlayer) {
// the player builder has been removed ... therefore we should delete the associated
// player
oldPlayer.destroy();
}
}
setContextPlayersDirty(context, false);
}
setContextDirty(context, false);
}
}
return totalPlayersQueued;
@ -1622,43 +1580,6 @@ export function getDirectiveIndexFromEntry(context: StylingContext, index: numbe
return value & DirectiveOwnerAndPlayerBuilderIndex.BitMask;
}
function getDirectiveIndexFromRegistry(context: StylingContext, directiveRef: any) {
let directiveIndex: number;
const dirs = context[StylingIndex.DirectiveRegistryPosition];
let index = getDirectiveRegistryValuesIndexOf(dirs, directiveRef);
if (index === -1) {
// if the directive was not allocated then this means that styling is
// being applied in a dynamic way AFTER the element was already instantiated
index = dirs.length;
directiveIndex = index > 0 ? index / DirectiveRegistryValuesIndex.Size : 0;
dirs.push(null, null, null, null);
dirs[index + DirectiveRegistryValuesIndex.DirectiveValueOffset] = directiveRef;
dirs[index + DirectiveRegistryValuesIndex.DirtyFlagOffset] = false;
dirs[index + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] = -1;
const classesStartIndex =
getMultiClassesStartIndex(context) || StylingIndex.SingleStylesStartPosition;
registerMultiMapEntry(context, directiveIndex, true, context.length);
registerMultiMapEntry(context, directiveIndex, false, classesStartIndex);
} else {
directiveIndex = index > 0 ? index / DirectiveRegistryValuesIndex.Size : 0;
}
return directiveIndex;
}
function getDirectiveRegistryValuesIndexOf(
directives: DirectiveRegistryValues, directive: {}): number {
for (let i = 0; i < directives.length; i += DirectiveRegistryValuesIndex.Size) {
if (directives[i] === directive) {
return i;
}
}
return -1;
}
function getInitialStylingValuesIndexOf(keyValues: InitialStylingValues, key: string): number {
for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < keyValues.length;
i += InitialStylingValuesIndex.Size) {
@ -1724,21 +1645,6 @@ function getStyleSanitizer(context: StylingContext, directiveIndex: number): Sty
return value as StyleSanitizeFn | null;
}
function isDirectiveDirty(context: StylingContext, directiveIndex: number): boolean {
const dirs = context[StylingIndex.DirectiveRegistryPosition];
return dirs
[directiveIndex * DirectiveRegistryValuesIndex.Size +
DirectiveRegistryValuesIndex.DirtyFlagOffset] as boolean;
}
function setDirectiveDirty(
context: StylingContext, directiveIndex: number, dirtyYes: boolean): void {
const dirs = context[StylingIndex.DirectiveRegistryPosition];
dirs
[directiveIndex * DirectiveRegistryValuesIndex.Size +
DirectiveRegistryValuesIndex.DirtyFlagOffset] = dirtyYes;
}
function allowValueChange(
currentValue: string | boolean | null, newValue: string | boolean | null,
currentDirectiveOwner: number, newDirectiveOwner: number) {
@ -1994,3 +1900,12 @@ function addOrUpdateStaticStyle(
staticStyles[index + InitialStylingValuesIndex.DirectiveOwnerOffset] = directiveOwnerIndex;
return index;
}
function assertValidDirectiveIndex(context: StylingContext, directiveIndex: number) {
const dirs = context[StylingIndex.DirectiveRegistryPosition];
const index = directiveIndex * DirectiveRegistryValuesIndex.Size;
if (index >= dirs.length ||
dirs[index + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] === -1) {
throw new Error('The provided directive is not registered with the styling context');
}
}

View File

@ -0,0 +1,95 @@
/**
* @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 {HostInstructionsQueue, HostInstructionsQueueIndex, StylingContext, StylingIndex} from '../interfaces/styling';
import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from '../styling/shared';
/*
* This file contains the logic to defer all hostBindings-related styling code to run
* at a later point, instead of immediately (as is the case with how template-level
* styling instructions are run).
*
* Certain styling instructions, present within directives, components and sub-classed
* directives, are evaluated at different points (depending on priority) and will therefore
* not be applied to the styling context of an element immediately. They are instead
* designed to be applied just before styling is applied to an element.
*
* (The priority for when certain host-related styling operations are executed is discussed
* more within `interfaces/styling.ts`.)
*/
export function registerHostDirective(context: StylingContext, directiveIndex: number) {
let buffer = context[StylingIndex.HostInstructionsQueue];
if (!buffer) {
buffer = context[StylingIndex.HostInstructionsQueue] = [DEFAULT_TEMPLATE_DIRECTIVE_INDEX];
}
buffer[HostInstructionsQueueIndex.LastRegisteredDirectiveIndexPosition] = directiveIndex;
}
/**
* Queues a styling instruction to be run just before `renderStyling()` is executed.
*/
export function enqueueHostInstruction<T extends Function>(
context: StylingContext, priority: number, instructionFn: T, instructionFnArgs: ParamsOf<T>) {
const buffer: HostInstructionsQueue = context[StylingIndex.HostInstructionsQueue] !;
const index = findNextInsertionIndex(buffer, priority);
buffer.splice(index, 0, priority, instructionFn, instructionFnArgs);
}
/**
* Figures out where exactly to to insert the next host instruction queue entry.
*/
function findNextInsertionIndex(buffer: HostInstructionsQueue, priority: number): number {
for (let i = HostInstructionsQueueIndex.ValuesStartPosition; i < buffer.length;
i += HostInstructionsQueueIndex.Size) {
const p = buffer[i + HostInstructionsQueueIndex.DirectiveIndexOffset] as number;
if (p > priority) {
return i;
}
}
return buffer.length;
}
/**
* Iterates through the host instructions queue (if present within the provided
* context) and executes each queued instruction entry.
*/
export function flushQueue(context: StylingContext): void {
const buffer = context[StylingIndex.HostInstructionsQueue];
if (buffer) {
for (let i = HostInstructionsQueueIndex.ValuesStartPosition; i < buffer.length;
i += HostInstructionsQueueIndex.Size) {
const fn = buffer[i + HostInstructionsQueueIndex.InstructionFnOffset] as Function;
const args = buffer[i + HostInstructionsQueueIndex.ParamsOffset] as any[];
fn(...args);
}
buffer.length = HostInstructionsQueueIndex.ValuesStartPosition;
}
}
/**
* Determines whether or not to allow the host instructions queue to be flushed or not.
*
* Because the hostBindings function code is unaware of the presence of other host bindings
* (as well as the template function) then styling is evaluated multiple times per element.
* To prevent style and class values from being applied to the element multiple times, a
* flush is only allowed when the last directive (the directive that was registered into
* the styling context) attempts to render its styling.
*/
export function allowFlush(context: StylingContext, directiveIndex: number): boolean {
const buffer = context[StylingIndex.HostInstructionsQueue];
if (buffer) {
return buffer[HostInstructionsQueueIndex.LastRegisteredDirectiveIndexPosition] ===
directiveIndex;
}
return true;
}
/**
* Infers the parameters of a given function into a typed array.
*/
export type ParamsOf<T> = T extends(...args: infer T) => any ? T : never;

View File

@ -0,0 +1,17 @@
/**
* @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
*/
/**
* The default directive styling index value for template-based bindings.
*
* All host-level bindings (e.g. `hostStyleProp` and `hostStylingMap`) are
* assigned a directive styling index value based on the current directive
* uniqueId and the directive super-class inheritance depth. But for template
* bindings they always have the same directive styling index value.
*/
export const DEFAULT_TEMPLATE_DIRECTIVE_INDEX = 0;

View File

@ -19,6 +19,7 @@ import {HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view';
import {getTNode, isStylingContext} from '../util/view_utils';
import {CorePlayerHandler} from './core_player_handler';
import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from './shared';
export const ANIMATION_PROP_PREFIX = '@';
@ -35,12 +36,13 @@ export function createEmptyStylingContext(
[0, 0], // SinglePropOffsets
[0], // CachedMultiClassValue
[0], // CachedMultiStyleValue
null, // HostBuffer
null, // PlayerContext
];
// whenever a context is created there is always a `null` directive
// that is registered (which is a placeholder for the "template").
allocateDirectiveIntoContext(context, null);
allocateOrUpdateDirectiveIntoContext(context, DEFAULT_TEMPLATE_DIRECTIVE_INDEX);
return context;
}
@ -60,25 +62,28 @@ export function createEmptyStylingContext(
* @param directiveRef the directive that will be allocated into the context
* @returns the index where the directive was inserted into
*/
export function allocateDirectiveIntoContext(
context: StylingContext, directiveRef: any | null): number {
// this is a new directive which we have not seen yet.
const dirs = context[StylingIndex.DirectiveRegistryPosition];
const i = dirs.length;
export function allocateOrUpdateDirectiveIntoContext(
context: StylingContext, directiveIndex: number, singlePropValuesIndex: number = -1,
styleSanitizer?: StyleSanitizeFn | null | undefined): void {
const directiveRegistry = context[StylingIndex.DirectiveRegistryPosition];
const index = directiveIndex * DirectiveRegistryValuesIndex.Size;
// we preemptively make space into the directives array and then
// assign values slot-by-slot to ensure that if the directive ordering
// changes then it will still function
dirs.push(null, null, null, null);
dirs[i + DirectiveRegistryValuesIndex.DirectiveValueOffset] = directiveRef;
dirs[i + DirectiveRegistryValuesIndex.DirtyFlagOffset] = false;
dirs[i + DirectiveRegistryValuesIndex.StyleSanitizerOffset] = null;
const limit = index + DirectiveRegistryValuesIndex.Size;
for (let i = directiveRegistry.length; i < limit; i += DirectiveRegistryValuesIndex.Size) {
// -1 is used to signal that the directive has been allocated, but
// no actual style or class bindings have been registered yet...
directiveRegistry.push(-1, null);
}
// -1 is used to signal that the directive has been allocated, but
// no actual style or class bindings have been registered yet...
dirs[i + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] = -1;
return i;
const propValuesStartPosition = index + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset;
if (singlePropValuesIndex >= 0 && directiveRegistry[propValuesStartPosition] === -1) {
directiveRegistry[propValuesStartPosition] = singlePropValuesIndex;
directiveRegistry[index + DirectiveRegistryValuesIndex.StyleSanitizerOffset] =
styleSanitizer || null;
}
}
/**