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

@ -12,7 +12,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime": 1440, "runtime": 1440,
"main": 14106, "main": 14287,
"polyfills": 43567 "polyfills": 43567
} }
} }

View File

@ -23,7 +23,7 @@ import {PlayerHandler} from './interfaces/player';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, RENDERER, RootContext, RootContextFlags, TVIEW} from './interfaces/view'; import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, RENDERER, RootContext, RootContextFlags, TVIEW} from './interfaces/view';
import {applyOnCreateInstructions} from './node_util'; import {applyOnCreateInstructions} from './node_util';
import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState} from './state'; import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setActiveHostElement} from './state';
import {renderInitialClasses, renderInitialStyles} from './styling/class_and_style_bindings'; import {renderInitialClasses, renderInitialStyles} from './styling/class_and_style_bindings';
import {publishDefaultGlobalUtils} from './util/global_utils'; import {publishDefaultGlobalUtils} from './util/global_utils';
import {defaultScheduler, renderStringify} from './util/misc_utils'; import {defaultScheduler, renderStringify} from './util/misc_utils';
@ -210,10 +210,15 @@ export function createRootComponent<T>(
const rootTNode = getPreviousOrParentTNode(); const rootTNode = getPreviousOrParentTNode();
if (tView.firstTemplatePass && componentDef.hostBindings) { if (tView.firstTemplatePass && componentDef.hostBindings) {
const elementIndex = rootTNode.index - HEADER_OFFSET;
setActiveHostElement(elementIndex);
const expando = tView.expandoInstructions !; const expando = tView.expandoInstructions !;
invokeHostBindingsInCreationMode( invokeHostBindingsInCreationMode(
componentDef, expando, component, rootTNode, tView.firstTemplatePass); componentDef, expando, component, rootTNode, tView.firstTemplatePass);
rootTNode.onElementCreationFns && applyOnCreateInstructions(rootTNode); rootTNode.onElementCreationFns && applyOnCreateInstructions(rootTNode);
setActiveHostElement(null);
} }
if (rootTNode.stylingTemplate) { if (rootTNode.stylingTemplate) {

View File

@ -10,6 +10,7 @@ import {Type} from '../../interface/type';
import {fillProperties} from '../../util/property'; import {fillProperties} from '../../util/property';
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition'; import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition';
import {adjustActiveDirectiveSuperClassDepthPosition} from '../state';
import {isComponentDef} from '../util/view_utils'; import {isComponentDef} from '../util/view_utils';
import {NgOnChangesFeature} from './ng_onchanges_feature'; import {NgOnChangesFeature} from './ng_onchanges_feature';
@ -63,8 +64,24 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
const superHostBindings = superDef.hostBindings; const superHostBindings = superDef.hostBindings;
if (superHostBindings) { if (superHostBindings) {
if (prevHostBindings) { if (prevHostBindings) {
// because inheritance is unknown during compile time, the runtime code
// needs to be informed of the super-class depth so that instruction code
// can distinguish one host bindings function from another. The reason why
// relying on the directive uniqueId exclusively is not enough is because the
// uniqueId value and the directive instance stay the same between hostBindings
// calls throughout the directive inheritance chain. This means that without
// a super-class depth value, there is no way to know whether a parent or
// sub-class host bindings function is currently being executed.
definition.hostBindings = (rf: RenderFlags, ctx: any, elementIndex: number) => { definition.hostBindings = (rf: RenderFlags, ctx: any, elementIndex: number) => {
superHostBindings(rf, ctx, elementIndex); // The reason why we increment first and then decrement is so that parent
// hostBindings calls have a higher id value compared to sub-class hostBindings
// calls (this way the leaf directive is always at a super-class depth of 0).
adjustActiveDirectiveSuperClassDepthPosition(1);
try {
superHostBindings(rf, ctx, elementIndex);
} finally {
adjustActiveDirectiveSuperClassDepthPosition(-1);
}
prevHostBindings(rf, ctx, elementIndex); prevHostBindings(rf, ctx, elementIndex);
}; };
} else { } else {

View File

@ -11,20 +11,23 @@ import {assertHasParent} from '../assert';
import {attachPatchData} from '../context_discovery'; import {attachPatchData} from '../context_discovery';
import {registerPostOrderHooks} from '../hooks'; import {registerPostOrderHooks} from '../hooks';
import {TAttributes, TNodeFlags, TNodeType} from '../interfaces/node'; import {TAttributes, TNodeFlags, TNodeType} from '../interfaces/node';
import {RElement, Renderer3, isProceduralRenderer} from '../interfaces/renderer'; import {RElement, isProceduralRenderer} from '../interfaces/renderer';
import {SanitizerFn} from '../interfaces/sanitization'; import {SanitizerFn} from '../interfaces/sanitization';
import {BINDING_INDEX, QUERIES, RENDERER, TVIEW} from '../interfaces/view'; import {BINDING_INDEX, QUERIES, RENDERER, TVIEW} from '../interfaces/view';
import {assertNodeType} from '../node_assert'; import {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation'; import {appendChild} from '../node_manipulation';
import {applyOnCreateInstructions} from '../node_util'; import {applyOnCreateInstructions} from '../node_util';
import {decreaseElementDepthCount, getActiveHostContext, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, increaseElementDepthCount, setIsParent, setPreviousOrParentTNode} from '../state'; import {decreaseElementDepthCount, getActiveDirectiveId, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsParent, setPreviousOrParentTNode} from '../state';
import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles} from '../styling/class_and_style_bindings'; import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles} from '../styling/class_and_style_bindings';
import {getStylingContext, hasClassInput, hasStyleInput} from '../styling/util'; import {getStylingContext, hasClassInput, hasStyleInput} from '../styling/util';
import {NO_CHANGE} from '../tokens'; import {NO_CHANGE} from '../tokens';
import {attrsStylingIndexOf, setUpAttributes} from '../util/attrs_utils'; import {attrsStylingIndexOf, setUpAttributes} from '../util/attrs_utils';
import {renderStringify} from '../util/misc_utils'; import {renderStringify} from '../util/misc_utils';
import {getNativeByIndex, getNativeByTNode, getTNode} from '../util/view_utils'; import {getNativeByIndex, getNativeByTNode, getTNode} from '../util/view_utils';
import {createDirectivesAndLocals, createNodeAtIndex, elementCreate, executeContentQueries, initializeTNodeInputs, setInputsForProperty, setNodeStylingTemplate} from './shared'; import {createDirectivesAndLocals, createNodeAtIndex, elementCreate, executeContentQueries, initializeTNodeInputs, setInputsForProperty, setNodeStylingTemplate} from './shared';
import {getActiveDirectiveStylingIndex} from './styling_instructions';
/** /**
* Create DOM element. The instruction must later be followed by `elementEnd()` call. * Create DOM element. The instruction must later be followed by `elementEnd()` call.
@ -256,17 +259,26 @@ export function elementAttribute(
* @publicApi * @publicApi
*/ */
export function elementHostAttrs(attrs: TAttributes) { export function elementHostAttrs(attrs: TAttributes) {
const tNode = getPreviousOrParentTNode(); const hostElementIndex = getSelectedIndex();
const lView = getLView(); const lView = getLView();
const native = getNativeByTNode(tNode, lView) as RElement; const tNode = getTNode(hostElementIndex, lView);
const lastAttrIndex = setUpAttributes(native, attrs);
const stylingAttrsStartIndex = attrsStylingIndexOf(attrs, lastAttrIndex); // non-element nodes (e.g. `<ng-container>`) are not rendered as actual
if (stylingAttrsStartIndex >= 0) { // element nodes and adding styles/classes on to them will cause runtime
const directive = getActiveHostContext(); // errors...
if (tNode.stylingTemplate) { if (tNode.type === TNodeType.Element) {
patchContextWithStaticAttrs(tNode.stylingTemplate, attrs, stylingAttrsStartIndex, directive); const native = getNativeByTNode(tNode, lView) as RElement;
} else { const lastAttrIndex = setUpAttributes(native, attrs);
tNode.stylingTemplate = initializeStaticContext(attrs, stylingAttrsStartIndex, directive); const stylingAttrsStartIndex = attrsStylingIndexOf(attrs, lastAttrIndex);
if (stylingAttrsStartIndex >= 0) {
const directiveStylingIndex = getActiveDirectiveStylingIndex();
if (tNode.stylingTemplate) {
patchContextWithStaticAttrs(
tNode.stylingTemplate, attrs, stylingAttrsStartIndex, directiveStylingIndex);
} else {
tNode.stylingTemplate =
initializeStaticContext(attrs, stylingAttrsStartIndex, directiveStylingIndex);
}
} }
} }
} }

View File

@ -13,6 +13,7 @@ import {TAttributes, TNodeType} from '../interfaces/node';
import {BINDING_INDEX, QUERIES, RENDERER, TVIEW} from '../interfaces/view'; import {BINDING_INDEX, QUERIES, RENDERER, TVIEW} from '../interfaces/view';
import {assertNodeType} from '../node_assert'; import {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation'; import {appendChild} from '../node_manipulation';
import {applyOnCreateInstructions} from '../node_util';
import {getIsParent, getLView, getPreviousOrParentTNode, setIsParent, setPreviousOrParentTNode} from '../state'; import {getIsParent, getLView, getPreviousOrParentTNode, setIsParent, setPreviousOrParentTNode} from '../state';
import {createDirectivesAndLocals, createNodeAtIndex, executeContentQueries, setNodeStylingTemplate} from './shared'; import {createDirectivesAndLocals, createNodeAtIndex, executeContentQueries, setNodeStylingTemplate} from './shared';
@ -83,5 +84,9 @@ export function elementContainerEnd(): void {
lView[QUERIES] = currentQueries.parent; lView[QUERIES] = currentQueries.parent;
} }
// this is required for all host-level styling-related instructions to run
// in the correct order
previousOrParentTNode.onElementCreationFns && applyOnCreateInstructions(previousOrParentTNode);
registerPostOrderHooks(tView, previousOrParentTNode); registerPostOrderHooks(tView, previousOrParentTNode);
} }

View File

@ -27,7 +27,7 @@ import {StylingContext} from '../interfaces/styling';
import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TVIEW, TView, T_HOST} from '../interfaces/view'; import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TVIEW, TView, T_HOST} from '../interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from '../node_assert'; import {assertNodeOfPossibleTypes, assertNodeType} from '../node_assert';
import {isNodeMatchingSelectorList} from '../node_selector_matcher'; import {isNodeMatchingSelectorList} from '../node_selector_matcher';
import {enterView, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, isCreationMode, leaveView, namespaceHTML, resetComponentState, setActiveHost, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode, setSelectedIndex} from '../state'; import {enterView, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, incrementActiveDirectiveId, isCreationMode, leaveView, namespaceHTML, resetComponentState, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode, setSelectedIndex} from '../state';
import {initializeStaticContext as initializeStaticStylingContext} from '../styling/class_and_style_bindings'; import {initializeStaticContext as initializeStaticStylingContext} from '../styling/class_and_style_bindings';
import {NO_CHANGE} from '../tokens'; import {NO_CHANGE} from '../tokens';
import {attrsStylingIndexOf} from '../util/attrs_utils'; import {attrsStylingIndexOf} from '../util/attrs_utils';
@ -35,6 +35,7 @@ import {INTERPOLATION_DELIMITER, renderStringify} from '../util/misc_utils';
import {getLViewParent, getRootContext} from '../util/view_traversal_utils'; import {getLViewParent, getRootContext} from '../util/view_traversal_utils';
import {getComponentViewByIndex, getNativeByTNode, isComponentDef, isContentQueryHost, isRootView, readPatchedLView, resetPreOrderHookFlags, unwrapRNode, viewAttachedToChangeDetector} from '../util/view_utils'; import {getComponentViewByIndex, getNativeByTNode, isComponentDef, isContentQueryHost, isRootView, readPatchedLView, resetPreOrderHookFlags, unwrapRNode, viewAttachedToChangeDetector} from '../util/view_utils';
/** /**
* A permanent marker promise which signifies that the current CD tree is * A permanent marker promise which signifies that the current CD tree is
* clean. * clean.
@ -107,6 +108,8 @@ export function setHostBindings(tView: TView, viewData: LView): void {
// Negative numbers mean that we are starting new EXPANDO block and need to update // Negative numbers mean that we are starting new EXPANDO block and need to update
// the current element and directive index. // the current element and directive index.
currentElementIndex = -instruction; currentElementIndex = -instruction;
setActiveHostElement(currentElementIndex);
// Injector block and providers are taken into account. // Injector block and providers are taken into account.
const providerCount = (tView.expandoInstructions[++i] as number); const providerCount = (tView.expandoInstructions[++i] as number);
bindingRootIndex += INJECTOR_BLOOM_PARENT_SIZE + providerCount; bindingRootIndex += INJECTOR_BLOOM_PARENT_SIZE + providerCount;
@ -124,14 +127,20 @@ export function setHostBindings(tView: TView, viewData: LView): void {
if (instruction !== null) { if (instruction !== null) {
viewData[BINDING_INDEX] = bindingRootIndex; viewData[BINDING_INDEX] = bindingRootIndex;
const hostCtx = unwrapRNode(viewData[currentDirectiveIndex]); const hostCtx = unwrapRNode(viewData[currentDirectiveIndex]);
setActiveHost(hostCtx, currentElementIndex);
instruction(RenderFlags.Update, hostCtx, currentElementIndex); instruction(RenderFlags.Update, hostCtx, currentElementIndex);
setActiveHost(null);
// Each directive gets a uniqueId value that is the same for both
// create and update calls when the hostBindings function is called. The
// directive uniqueId is not set anywhere--it is just incremented between
// each hostBindings call and is useful for helping instruction code
// uniquely determine which directive is currently active when executed.
incrementActiveDirectiveId();
} }
currentDirectiveIndex++; currentDirectiveIndex++;
} }
} }
} }
setActiveHostElement(null);
} }
/** Refreshes content queries for all directives in the given view. */ /** Refreshes content queries for all directives in the given view. */
@ -897,15 +906,27 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
const end = tNode.directiveEnd; const end = tNode.directiveEnd;
const expando = tView.expandoInstructions !; const expando = tView.expandoInstructions !;
const firstTemplatePass = tView.firstTemplatePass; const firstTemplatePass = tView.firstTemplatePass;
const elementIndex = tNode.index - HEADER_OFFSET;
setActiveHostElement(elementIndex);
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
const def = tView.data[i] as DirectiveDef<any>; const def = tView.data[i] as DirectiveDef<any>;
const directive = viewData[i]; const directive = viewData[i];
if (def.hostBindings) { if (def.hostBindings) {
invokeHostBindingsInCreationMode(def, expando, directive, tNode, firstTemplatePass); invokeHostBindingsInCreationMode(def, expando, directive, tNode, firstTemplatePass);
// Each directive gets a uniqueId value that is the same for both
// create and update calls when the hostBindings function is called. The
// directive uniqueId is not set anywhere--it is just incremented between
// each hostBindings call and is useful for helping instruction code
// uniquely determine which directive is currently active when executed.
incrementActiveDirectiveId();
} else if (firstTemplatePass) { } else if (firstTemplatePass) {
expando.push(null); expando.push(null);
} }
} }
setActiveHostElement(null);
} }
export function invokeHostBindingsInCreationMode( export function invokeHostBindingsInCreationMode(
@ -914,9 +935,7 @@ export function invokeHostBindingsInCreationMode(
const previousExpandoLength = expando.length; const previousExpandoLength = expando.length;
setCurrentDirectiveDef(def); setCurrentDirectiveDef(def);
const elementIndex = tNode.index - HEADER_OFFSET; const elementIndex = tNode.index - HEADER_OFFSET;
setActiveHost(directive, elementIndex);
def.hostBindings !(RenderFlags.Create, directive, elementIndex); def.hostBindings !(RenderFlags.Create, directive, elementIndex);
setActiveHost(null);
setCurrentDirectiveDef(null); setCurrentDirectiveDef(null);
// `hostBindings` function may or may not contain `allocHostVars` call // `hostBindings` function may or may not contain `allocHostVars` call
// (e.g. it may not if it only contains host listeners), so we need to check whether // (e.g. it may not if it only contains host listeners), so we need to check whether

View File

@ -6,19 +6,24 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {TNode} from '../interfaces/node'; import {TNode, TNodeType} from '../interfaces/node';
import {PlayerFactory} from '../interfaces/player'; import {PlayerFactory} from '../interfaces/player';
import {FLAGS, HEADER_OFFSET, LViewFlags, RENDERER, RootContextFlags} from '../interfaces/view'; import {FLAGS, HEADER_OFFSET, LViewFlags, RENDERER, RootContextFlags} from '../interfaces/view';
import {getActiveHostContext, getActiveHostElementIndex, getLView, getPreviousOrParentTNode} from '../state'; import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getLView, getPreviousOrParentTNode, getSelectedIndex} from '../state';
import {getInitialClassNameValue, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from '../styling/class_and_style_bindings'; import {getInitialClassNameValue, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from '../styling/class_and_style_bindings';
import {ParamsOf, enqueueHostInstruction, registerHostDirective} from '../styling/host_instructions_queue';
import {BoundPlayerFactory} from '../styling/player_factory'; import {BoundPlayerFactory} from '../styling/player_factory';
import {allocateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput} from '../styling/util'; import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from '../styling/shared';
import {allocateOrUpdateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput} from '../styling/util';
import {NO_CHANGE} from '../tokens'; import {NO_CHANGE} from '../tokens';
import {renderStringify} from '../util/misc_utils'; import {renderStringify} from '../util/misc_utils';
import {getRootContext} from '../util/view_traversal_utils'; import {getRootContext} from '../util/view_traversal_utils';
import {getTNode} from '../util/view_utils'; import {getTNode} from '../util/view_utils';
import {scheduleTick, setInputsForProperty} from './shared'; import {scheduleTick, setInputsForProperty} from './shared';
/* /*
* The contents of this file include the instructions for all styling-related * The contents of this file include the instructions for all styling-related
* operations in Angular. * operations in Angular.
@ -40,7 +45,6 @@ import {scheduleTick, setInputsForProperty} from './shared';
* - elementHostStylingApply * - elementHostStylingApply
*/ */
/** /**
* Allocates style and class binding properties on the element during creation mode. * Allocates style and class binding properties on the element during creation mode.
* *
@ -74,7 +78,9 @@ export function elementStyling(
// components) then they will be applied at the end of the `elementEnd` // components) then they will be applied at the end of the `elementEnd`
// instruction (because directives are created first before styling is // instruction (because directives are created first before styling is
// executed for a new element). // executed for a new element).
initElementStyling(tNode, classBindingNames, styleBindingNames, styleSanitizer, null); initElementStyling(
tNode, classBindingNames, styleBindingNames, styleSanitizer,
DEFAULT_TEMPLATE_DIRECTIVE_INDEX);
} }
/** /**
@ -108,25 +114,28 @@ export function elementHostStyling(
tNode.stylingTemplate = createEmptyStylingContext(); tNode.stylingTemplate = createEmptyStylingContext();
} }
const directive = getActiveHostContext(); const directiveStylingIndex = getActiveDirectiveStylingIndex();
// despite the binding being applied in a queue (below), the allocation // despite the binding being applied in a queue (below), the allocation
// of the directive into the context happens right away. The reason for // of the directive into the context happens right away. The reason for
// this is to retain the ordering of the directives (which is important // this is to retain the ordering of the directives (which is important
// for the prioritization of bindings). // for the prioritization of bindings).
allocateDirectiveIntoContext(tNode.stylingTemplate, directive); allocateOrUpdateDirectiveIntoContext(tNode.stylingTemplate, directiveStylingIndex);
const fns = tNode.onElementCreationFns = tNode.onElementCreationFns || []; const fns = tNode.onElementCreationFns = tNode.onElementCreationFns || [];
fns.push( fns.push(() => {
() => initElementStyling( initElementStyling(
tNode, classBindingNames, styleBindingNames, styleSanitizer, directive)); tNode, classBindingNames, styleBindingNames, styleSanitizer, directiveStylingIndex);
registerHostDirective(tNode.stylingTemplate !, directiveStylingIndex);
});
} }
function initElementStyling( function initElementStyling(
tNode: TNode, classBindingNames?: string[] | null, styleBindingNames?: string[] | null, tNode: TNode, classBindingNames: string[] | null | undefined,
styleSanitizer?: StyleSanitizeFn | null, directive?: {} | null): void { styleBindingNames: string[] | null | undefined,
styleSanitizer: StyleSanitizeFn | null | undefined, directiveStylingIndex: number): void {
updateContextWithBindings( updateContextWithBindings(
tNode.stylingTemplate !, directive || null, classBindingNames, styleBindingNames, tNode.stylingTemplate !, directiveStylingIndex, classBindingNames, styleBindingNames,
styleSanitizer); styleSanitizer);
} }
@ -160,7 +169,10 @@ function initElementStyling(
export function elementStyleProp( export function elementStyleProp(
index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, index: number, styleIndex: number, value: string | number | String | PlayerFactory | null,
suffix?: string | null, forceOverride?: boolean): void { suffix?: string | null, forceOverride?: boolean): void {
elementStylePropInternal(null, index, styleIndex, value, suffix, forceOverride); const valueToAdd = resolveStylePropValue(value, suffix);
updateElementStyleProp(
getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd,
DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride);
} }
/** /**
@ -191,15 +203,20 @@ export function elementStyleProp(
export function elementHostStyleProp( export function elementHostStyleProp(
styleIndex: number, value: string | number | String | PlayerFactory | null, styleIndex: number, value: string | number | String | PlayerFactory | null,
suffix?: string | null, forceOverride?: boolean): void { suffix?: string | null, forceOverride?: boolean): void {
elementStylePropInternal( const directiveStylingIndex = getActiveDirectiveStylingIndex();
getActiveHostContext() !, getActiveHostElementIndex() !, styleIndex, value, suffix, const hostElementIndex = getSelectedIndex();
forceOverride);
const lView = getLView();
const stylingContext = getStylingContext(hostElementIndex + HEADER_OFFSET, lView);
const valueToAdd = resolveStylePropValue(value, suffix);
const args: ParamsOf<typeof updateElementStyleProp> =
[stylingContext, styleIndex, valueToAdd, directiveStylingIndex, forceOverride];
enqueueHostInstruction(stylingContext, directiveStylingIndex, updateElementStyleProp, args);
} }
function elementStylePropInternal( function resolveStylePropValue(
directive: {} | null, index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, suffix: string | null | undefined) {
value: string | number | String | PlayerFactory | null, suffix?: string | null,
forceOverride?: boolean): void {
let valueToAdd: string|null = null; let valueToAdd: string|null = null;
if (value !== null) { if (value !== null) {
if (suffix) { if (suffix) {
@ -214,9 +231,7 @@ function elementStylePropInternal(
valueToAdd = value as any as string; valueToAdd = value as any as string;
} }
} }
updateElementStyleProp( return valueToAdd;
getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, directive,
forceOverride);
} }
@ -241,7 +256,12 @@ function elementStylePropInternal(
export function elementClassProp( export function elementClassProp(
index: number, classIndex: number, value: boolean | PlayerFactory, index: number, classIndex: number, value: boolean | PlayerFactory,
forceOverride?: boolean): void { forceOverride?: boolean): void {
elementClassPropInternal(null, index, classIndex, value, forceOverride); const input = (value instanceof BoundPlayerFactory) ?
(value as BoundPlayerFactory<boolean|null>) :
booleanOrNull(value);
updateElementClassProp(
getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, input,
DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride);
} }
@ -265,19 +285,19 @@ export function elementClassProp(
*/ */
export function elementHostClassProp( export function elementHostClassProp(
classIndex: number, value: boolean | PlayerFactory, forceOverride?: boolean): void { classIndex: number, value: boolean | PlayerFactory, forceOverride?: boolean): void {
elementClassPropInternal( const directiveStylingIndex = getActiveDirectiveStylingIndex();
getActiveHostContext() !, getActiveHostElementIndex() !, classIndex, value, forceOverride); const hostElementIndex = getSelectedIndex();
}
const lView = getLView();
const stylingContext = getStylingContext(hostElementIndex + HEADER_OFFSET, lView);
function elementClassPropInternal(
directive: {} | null, index: number, classIndex: number, value: boolean | PlayerFactory,
forceOverride?: boolean): void {
const input = (value instanceof BoundPlayerFactory) ? const input = (value instanceof BoundPlayerFactory) ?
(value as BoundPlayerFactory<boolean|null>) : (value as BoundPlayerFactory<boolean|null>) :
booleanOrNull(value); booleanOrNull(value);
updateElementClassProp(
getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, input, directive, const args: ParamsOf<typeof updateElementClassProp> =
forceOverride); [stylingContext, classIndex, input, directiveStylingIndex, forceOverride];
enqueueHostInstruction(stylingContext, directiveStylingIndex, updateElementClassProp, args);
} }
function booleanOrNull(value: any): boolean|null { function booleanOrNull(value: any): boolean|null {
@ -309,7 +329,30 @@ function booleanOrNull(value: any): boolean|null {
export function elementStylingMap( export function elementStylingMap(
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
styles?: {[styleName: string]: any} | NO_CHANGE | null): void { styles?: {[styleName: string]: any} | NO_CHANGE | null): void {
elementStylingMapInternal(null, index, classes, styles); const lView = getLView();
const tNode = getTNode(index, lView);
const stylingContext = getStylingContext(index + HEADER_OFFSET, lView);
// inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function)
if (hasClassInput(tNode) && classes !== NO_CHANGE) {
const initialClasses = getInitialClassNameValue(stylingContext);
const classInputVal =
(initialClasses.length ? (initialClasses + ' ') : '') + forceClassesAsString(classes);
setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal);
classes = NO_CHANGE;
}
if (hasStyleInput(tNode) && styles !== NO_CHANGE) {
const initialStyles = getInitialClassNameValue(stylingContext);
const styleInputVal =
(initialStyles.length ? (initialStyles + ' ') : '') + forceStylesAsString(styles);
setInputsForProperty(lView, tNode.inputs !['style'] !, styleInputVal);
styles = NO_CHANGE;
}
updateStylingMap(stylingContext, classes, styles);
} }
@ -339,39 +382,15 @@ export function elementStylingMap(
export function elementHostStylingMap( export function elementHostStylingMap(
classes: {[key: string]: any} | string | NO_CHANGE | null, classes: {[key: string]: any} | string | NO_CHANGE | null,
styles?: {[styleName: string]: any} | NO_CHANGE | null): void { styles?: {[styleName: string]: any} | NO_CHANGE | null): void {
elementStylingMapInternal( const directiveStylingIndex = getActiveDirectiveStylingIndex();
getActiveHostContext() !, getActiveHostElementIndex() !, classes, styles); const hostElementIndex = getSelectedIndex();
}
function elementStylingMapInternal(
directive: {} | null, index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
styles?: {[styleName: string]: any} | NO_CHANGE | null): void {
const lView = getLView(); const lView = getLView();
const tNode = getTNode(index, lView); const stylingContext = getStylingContext(hostElementIndex + HEADER_OFFSET, lView);
const stylingContext = getStylingContext(index + HEADER_OFFSET, lView);
// inputs are only evaluated from a template binding into a directive, therefore, const args: ParamsOf<typeof updateStylingMap> =
// there should not be a situation where a directive host bindings function [stylingContext, classes, styles, directiveStylingIndex];
// evaluates the inputs (this should only happen in the template function) enqueueHostInstruction(stylingContext, directiveStylingIndex, updateStylingMap, args);
if (!directive) {
if (hasClassInput(tNode) && classes !== NO_CHANGE) {
const initialClasses = getInitialClassNameValue(stylingContext);
const classInputVal =
(initialClasses.length ? (initialClasses + ' ') : '') + forceClassesAsString(classes);
setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal);
classes = NO_CHANGE;
}
if (hasStyleInput(tNode) && styles !== NO_CHANGE) {
const initialStyles = getInitialClassNameValue(stylingContext);
const styleInputVal =
(initialStyles.length ? (initialStyles + ' ') : '') + forceStylesAsString(styles);
setInputsForProperty(lView, tNode.inputs !['style'] !, styleInputVal);
styles = NO_CHANGE;
}
}
updateStylingMap(stylingContext, classes, styles, directive);
} }
@ -387,7 +406,7 @@ function elementStylingMapInternal(
* @publicApi * @publicApi
*/ */
export function elementStylingApply(index: number): void { export function elementStylingApply(index: number): void {
elementStylingApplyInternal(null, index); elementStylingApplyInternal(DEFAULT_TEMPLATE_DIRECTIVE_INDEX, index);
} }
/** /**
@ -401,17 +420,33 @@ export function elementStylingApply(index: number): void {
* @publicApi * @publicApi
*/ */
export function elementHostStylingApply(): void { export function elementHostStylingApply(): void {
elementStylingApplyInternal(getActiveHostContext() !, getActiveHostElementIndex() !); elementStylingApplyInternal(getActiveDirectiveStylingIndex(), getSelectedIndex());
} }
export function elementStylingApplyInternal(directive: {} | null, index: number): void { export function elementStylingApplyInternal(directiveStylingIndex: number, index: number): void {
const lView = getLView(); const lView = getLView();
const tNode = getTNode(index, lView);
// if a non-element value is being processed then we can't render values
// on the element at all therefore by setting the renderer to null then
// the styling apply code knows not to actually apply the values...
const renderer = tNode.type === TNodeType.Element ? lView[RENDERER] : null;
const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0; const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0;
const stylingContext = getStylingContext(index + HEADER_OFFSET, lView);
const totalPlayersQueued = renderStyling( const totalPlayersQueued = renderStyling(
getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender, null, stylingContext, renderer, lView, isFirstRender, null, null, directiveStylingIndex);
null, directive);
if (totalPlayersQueued > 0) { if (totalPlayersQueued > 0) {
const rootContext = getRootContext(lView); const rootContext = getRootContext(lView);
scheduleTick(rootContext, RootContextFlags.FlushPlayers); scheduleTick(rootContext, RootContextFlags.FlushPlayers);
} }
} }
export function getActiveDirectiveStylingIndex() {
// whenever a directive's hostBindings function is called a uniqueId value
// is assigned. Normally this is enough to help distinguish one directive
// from another for the styling context, but there are situations where a
// sub-class directive could inherit and assign styling in concert with a
// parent directive. To help the styling code distinguish between a parent
// sub-classed directive the inheritance depth is taken into account as well.
return getActiveDirectiveId() + getActiveDirectiveSuperClassDepth();
}

View File

@ -309,6 +309,21 @@ export interface StylingContext extends
*/ */
[StylingIndex.CachedMultiStyles]: any|MapBasedOffsetValues; [StylingIndex.CachedMultiStyles]: any|MapBasedOffsetValues;
/**
* A queue of all hostStyling instructions.
*
* This array (queue) is populated only when host-level styling instructions
* (e.g. `hostStylingMap` and `hostClassProp`) are used to apply style and
* class values via host bindings to the host element. Despite these being
* standard angular instructions, they are not designed to immediately apply
* their values to the styling context when executed. What happens instead is
* a queue is constructed and each instruction is populated into the queue.
* Then, once the style/class values are set to flush (via `elementStylingApply` or
* `hostStylingApply`), the queue is flushed and the values are rendered onto
* the host element.
*/
[StylingIndex.HostInstructionsQueue]: HostInstructionsQueue|null;
/** /**
* Location of animation context (which contains the active players) for this element styling * Location of animation context (which contains the active players) for this element styling
* context. * context.
@ -316,6 +331,66 @@ export interface StylingContext extends
[StylingIndex.PlayerContext]: PlayerContext|null; [StylingIndex.PlayerContext]: PlayerContext|null;
} }
/**
* A queue of all host-related styling instructions (these are buffered and evaluated just before
* the styling is applied).
*
* This queue is used when any `hostStyling` instructions are executed from the `hostBindings`
* function. Template-level styling functions (e.g. `elementStylingMap` and `elementClassProp`)
* do not make use of this queue (they are applied to the styling context immediately).
*
* Due to the nature of how components/directives are evaluated, directives (both parent and
* subclass directives) may not apply their styling at the right time for the styling
* algorithm code to prioritize them. Therefore, all host-styling instructions are queued up
* (buffered) into the array below and are automatically sorted in terms of priority. The
* priority for host-styling is as follows:
*
* 1. The template (this doesn't get queued, but gets evaluated immediately)
* 2. Any directives present on the host
* 2a) first child directive styling bindings are updated
* 2b) then any parent directives
* 3. Component host bindings
*
* Angular runs change detection for each of these cases in a different order. Because of this
* the array below is populated with each of the host styling functions + their arguments.
*
* context[HostInstructionsQueue] = [
* directiveIndex,
* hostStylingFn,
* [argumentsForFn],
* ...
* anotherDirectiveIndex, <-- this has a lower priority (a higher directive index)
* anotherHostStylingFn,
* [argumentsForFn],
* ]
*
* When `renderStyling` is called (within `class_and_host_bindings.ts`) then the queue is
* drained and each of the instructions are executed. Once complete the queue is empty then
* the style/class binding code is rendered on the element (which is what happens normally
* inside of `renderStyling`).
*
* Right now each directive's hostBindings function, as well the template function, both
* call `elementStylingApply()` and `hostStylingApply()`. The fact that this is called
* multiple times for the same element (b/c of change detection) causes some issues. To avoid
* having styling code be rendered on an element multiple times, the `HostInstructionsQueue`
* reserves a slot for a reference pointing to the very last directive that was registered and
* only allows for styling to be applied once that directive is encountered (which will happen
* as the last update for that element).
*/
export interface HostInstructionsQueue extends Array<number|Function|any[]> { [0]: number; }
/**
* Used as a reference for any values contained within `HostInstructionsQueue`.
*/
export const enum HostInstructionsQueueIndex {
LastRegisteredDirectiveIndexPosition = 0,
ValuesStartPosition = 1,
DirectiveIndexOffset = 0,
InstructionFnOffset = 1,
ParamsOffset = 2,
Size = 3,
}
/** /**
* Used as a styling array to house static class and style values that were extracted * Used as a styling array to house static class and style values that were extracted
* by the compiler and placed in the animation context via `elementStart` and * by the compiler and placed in the animation context via `elementStart` and
@ -511,9 +586,7 @@ export const enum InitialStylingValuesIndex {
* index value by the size of the array entries (so if DirA is at spot 8 then its index will be 2). * index value by the size of the array entries (so if DirA is at spot 8 then its index will be 2).
*/ */
export interface DirectiveRegistryValues extends Array<null|{}|boolean|number|StyleSanitizeFn> { export interface DirectiveRegistryValues extends Array<null|{}|boolean|number|StyleSanitizeFn> {
[DirectiveRegistryValuesIndex.DirectiveValueOffset]: null;
[DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset]: number; [DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset]: number;
[DirectiveRegistryValuesIndex.DirtyFlagOffset]: boolean;
[DirectiveRegistryValuesIndex.StyleSanitizerOffset]: StyleSanitizeFn|null; [DirectiveRegistryValuesIndex.StyleSanitizerOffset]: StyleSanitizeFn|null;
} }
@ -522,11 +595,9 @@ export interface DirectiveRegistryValues extends Array<null|{}|boolean|number|St
* that are housed inside of [DirectiveRegistryValues]. * that are housed inside of [DirectiveRegistryValues].
*/ */
export const enum DirectiveRegistryValuesIndex { export const enum DirectiveRegistryValuesIndex {
DirectiveValueOffset = 0, SinglePropValuesIndexOffset = 0,
SinglePropValuesIndexOffset = 1, StyleSanitizerOffset = 1,
DirtyFlagOffset = 2, Size = 2
StyleSanitizerOffset = 3,
Size = 4
} }
/** /**
@ -678,9 +749,10 @@ export const enum StylingIndex {
CachedMultiStyles = 7, CachedMultiStyles = 7,
// Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue // Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue
// Position of where the initial styles are stored in the styling context // Position of where the initial styles are stored in the styling context
PlayerContext = 8, HostInstructionsQueue = 8,
PlayerContext = 9,
// Location of single (prop) value entries are stored within the context // Location of single (prop) value entries are stored within the context
SingleStylesStartPosition = 9, SingleStylesStartPosition = 10,
FlagsOffset = 0, FlagsOffset = 0,
PropertyOffset = 1, PropertyOffset = 1,
ValueOffset = 2, ValueOffset = 2,
@ -705,3 +777,13 @@ export const enum DirectiveOwnerAndPlayerBuilderIndex {
BitCountSize = 16, BitCountSize = 16,
BitMask = 0b1111111111111111 BitMask = 0b1111111111111111
} }
/**
* 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

@ -119,27 +119,139 @@ export function getLView(): LView {
return lView; return lView;
} }
let activeHostContext: {}|null = null; /**
let activeHostElementIndex: number|null = null; * Used as the starting directive id value.
*
* All subsequent directives are incremented from this value onwards.
* The reason why this value is `1` instead of `0` is because the `0`
* value is reserved for the template.
*/
const MIN_DIRECTIVE_ID = 1;
let activeDirectiveId = MIN_DIRECTIVE_ID;
/** /**
* Sets the active host context (the directive/component instance) and its host element index. * Position depth (with respect from leaf to root) in a directive sub-class inheritance chain.
*
* @param host the directive/component instance
* @param index the element index value for the host element where the directive/component instance
* lives
*/ */
export function setActiveHost(host: {} | null, index: number | null = null) { let activeDirectiveSuperClassDepthPosition = 0;
activeHostContext = host;
activeHostElementIndex = index; /**
* Total count of how many directives are a part of an inheritance chain.
*
* When directives are sub-classed (extended) from one to another, Angular
* needs to keep track of exactly how many were encountered so it can accurately
* generate the next directive id (once the next directive id is visited).
* Normally the next directive id just a single incremented value from the
* previous one, however, if the previous directive is a part of an inheritance
* chain (a series of sub-classed directives) then the incremented value must
* also take into account the total amount of sub-classed values.
*
* Note that this value resets back to zero once the next directive is
* visited (when `incrementActiveDirectiveId` or `setActiveHostElement`
* is called).
*/
let activeDirectiveSuperClassHeight = 0;
/**
* Sets the active directive host element and resets the directive id value
* (when the provided elementIndex value has changed).
*
* @param elementIndex the element index value for the host element where
* the directive/component instance lives
*/
export function setActiveHostElement(elementIndex: number | null = null) {
if (_selectedIndex !== elementIndex) {
setSelectedIndex(elementIndex == null ? -1 : elementIndex);
activeDirectiveId = MIN_DIRECTIVE_ID;
activeDirectiveSuperClassDepthPosition = 0;
activeDirectiveSuperClassHeight = 0;
}
} }
export function getActiveHostContext() { /**
return activeHostContext; * Returns the current id value of the current directive.
*
* For example we have an element that has two directives on it:
* <div dir-one dir-two></div>
*
* dirOne->hostBindings() (id == 1)
* dirTwo->hostBindings() (id == 2)
*
* Note that this is only active when `hostBinding` functions are being processed.
*
* Note that directive id values are specific to an element (this means that
* the same id value could be present on another element with a completely
* different set of directives).
*/
export function getActiveDirectiveId() {
return activeDirectiveId;
} }
export function getActiveHostElementIndex() { /**
return activeHostElementIndex; * Increments the current directive id value.
*
* For example we have an element that has two directives on it:
* <div dir-one dir-two></div>
*
* dirOne->hostBindings() (index = 1)
* // increment
* dirTwo->hostBindings() (index = 2)
*
* Depending on whether or not a previous directive had any inherited
* directives present, that value will be incremented in addition
* to the id jumping up by one.
*
* Note that this is only active when `hostBinding` functions are being processed.
*
* Note that directive id values are specific to an element (this means that
* the same id value could be present on another element with a completely
* different set of directives).
*/
export function incrementActiveDirectiveId() {
activeDirectiveId += 1 + activeDirectiveSuperClassHeight;
// because we are dealing with a new directive this
// means we have exited out of the inheritance chain
activeDirectiveSuperClassDepthPosition = 0;
activeDirectiveSuperClassHeight = 0;
}
/**
* Set the current super class (reverse inheritance) position depth for a directive.
*
* For example we have two directives: Child and Other (but Child is a sub-class of Parent)
* <div child-dir other-dir></div>
*
* // increment
* parentInstance->hostBindings() (depth = 1)
* // decrement
* childInstance->hostBindings() (depth = 0)
* otherInstance->hostBindings() (depth = 0 b/c it's a different directive)
*
* Note that this is only active when `hostBinding` functions are being processed.
*/
export function adjustActiveDirectiveSuperClassDepthPosition(delta: number) {
activeDirectiveSuperClassDepthPosition += delta;
// we keep track of the height value so that when the next directive is visited
// then Angular knows to generate a new directive id value which has taken into
// account how many sub-class directives were a part of the previous directive.
activeDirectiveSuperClassHeight =
Math.max(activeDirectiveSuperClassHeight, activeDirectiveSuperClassDepthPosition);
}
/**
* Returns the current super class (reverse inheritance) depth for a directive.
*
* This is designed to help instruction code distinguish different hostBindings
* calls from each other when a directive has extended from another directive.
* Normally using the directive id value is enough, but with the case
* of parent/sub-class directive inheritance more information is required.
*
* Note that this is only active when `hostBinding` functions are being processed.
*/
export function getActiveDirectiveSuperClassDepth() {
return activeDirectiveSuperClassDepthPosition;
} }
/** /**

View File

@ -10,14 +10,14 @@ import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {AttributeMarker, TAttributes} from '../interfaces/node'; import {AttributeMarker, TAttributes} from '../interfaces/node';
import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player'; import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player';
import {RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer'; 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 {LView, RootContext} from '../interfaces/view';
import {NO_CHANGE} from '../tokens'; import {NO_CHANGE} from '../tokens';
import {getRootContext} from '../util/view_traversal_utils'; import {getRootContext} from '../util/view_traversal_utils';
import {allowFlush as allowHostInstructionsQueueFlush, flushQueue as flushHostInstructionsQueue} from './host_instructions_queue';
import {BoundPlayerFactory} from './player_factory'; 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. * Creates a new StylingContext an fills it with the provided static styling attribute values.
*/ */
export function initializeStaticContext( export function initializeStaticContext(
attrs: TAttributes, stylingStartIndex: number, directiveRef?: any | null): StylingContext { attrs: TAttributes, stylingStartIndex: number, directiveIndex: number = 0): StylingContext {
const context = createEmptyStylingContext(); const context = createEmptyStylingContext();
patchContextWithStaticAttrs(context, attrs, stylingStartIndex, directiveRef); patchContextWithStaticAttrs(context, attrs, stylingStartIndex, directiveIndex);
return context; return context;
} }
@ -57,25 +57,14 @@ export function initializeStaticContext(
* assigned to the context * assigned to the context
* @param attrsStylingStartIndex what index to start iterating within the * @param attrsStylingStartIndex what index to start iterating within the
* provided `attrs` array to start reading style and class values * 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( export function patchContextWithStaticAttrs(
context: StylingContext, attrs: TAttributes, attrsStylingStartIndex: number, context: StylingContext, attrs: TAttributes, attrsStylingStartIndex: number,
directiveRef?: any | null): void { directiveIndex: number): void {
// this means the context has already been set and instantiated // this means the context has already been set and instantiated
if (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) return; if (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) return;
// If the styling context has already been patched with the given directive's bindings, allocateOrUpdateDirectiveIntoContext(context, directiveIndex);
// 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;
let initialClasses: InitialStylingValues|null = null; let initialClasses: InitialStylingValues|null = null;
let initialStyles: InitialStylingValues|null = null; let initialStyles: InitialStylingValues|null = null;
@ -199,7 +188,6 @@ export function allowNewBindingsForStylingContext(context: StylingContext): bool
* reference the provided directive. * reference the provided directive.
* *
* @param context the existing styling context * @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 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 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 * @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. * instance will only be active if and when the directive updates the bindings that it owns.
*/ */
export function updateContextWithBindings( export function updateContextWithBindings(
context: StylingContext, directiveRef: any | null, classBindingNames?: string[] | null, context: StylingContext, directiveIndex: number, classBindingNames?: string[] | null,
styleBindingNames?: string[] | null, styleSanitizer?: StyleSanitizeFn | null) { styleBindingNames?: string[] | null, styleSanitizer?: StyleSanitizeFn | null) {
if (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) return; if (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) return;
// this means the context has already been patched with the directive's bindings // this means the context has already been patched with the directive's bindings
const directiveIndex = findOrPatchDirectiveIntoRegistry(context, directiveRef, styleSanitizer); const isNewDirective =
if (directiveIndex === -1) { findOrPatchDirectiveIntoRegistry(context, directiveIndex, false, styleSanitizer);
if (!isNewDirective) {
// this means the directive has already been patched in ... No point in doing anything // this means the directive has already been patched in ... No point in doing anything
return; return;
} }
@ -470,46 +459,22 @@ export function updateContextWithBindings(
* Searches through the existing registry of directives * Searches through the existing registry of directives
*/ */
export function findOrPatchDirectiveIntoRegistry( export function findOrPatchDirectiveIntoRegistry(
context: StylingContext, directiveRef: any, styleSanitizer?: StyleSanitizeFn | null) { context: StylingContext, directiveIndex: number, staticModeOnly: boolean,
const directiveRefs = context[StylingIndex.DirectiveRegistryPosition]; styleSanitizer?: StyleSanitizeFn | null): boolean {
const nextOffsetInsertionIndex = context[StylingIndex.SinglePropOffsetPositions].length; const directiveRegistry = context[StylingIndex.DirectiveRegistryPosition];
const index = directiveIndex * DirectiveRegistryValuesIndex.Size;
const singlePropStartPosition = index + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset;
let directiveIndex: number; // this means that the directive has already been registered into the registry
let detectedIndex = getDirectiveRegistryValuesIndexOf(directiveRefs, directiveRef); if (index < directiveRegistry.length &&
(directiveRegistry[singlePropStartPosition] as number) >= 0)
return false;
if (detectedIndex === -1) { const singlePropsStartIndex =
detectedIndex = directiveRefs.length; staticModeOnly ? -1 : context[StylingIndex.SinglePropOffsetPositions].length;
directiveIndex = directiveRefs.length / DirectiveRegistryValuesIndex.Size; allocateOrUpdateDirectiveIntoContext(
context, directiveIndex, singlePropsStartIndex, styleSanitizer);
allocateDirectiveIntoContext(context, directiveRef); return true;
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;
} }
function getMatchingBindingIndex( function getMatchingBindingIndex(
@ -543,18 +508,13 @@ function getMatchingBindingIndex(
* newly provided style values. * newly provided style values.
* @param classesInput The key/value map of CSS class names that will be used for the update. * @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 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( export function updateStylingMap(
context: StylingContext, classesInput: {[key: string]: any} | string | context: StylingContext, classesInput: {[key: string]: any} | string |
BoundPlayerFactory<null|string|{[key: string]: any}>| null, BoundPlayerFactory<null|string|{[key: string]: any}>| null,
stylesInput?: {[key: string]: any} | BoundPlayerFactory<null|{[key: string]: any}>| null, stylesInput?: {[key: string]: any} | BoundPlayerFactory<null|{[key: string]: any}>| null,
directiveRef?: any): void { directiveIndex: number = 0): void {
const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null); ngDevMode && assertValidDirectiveIndex(context, directiveIndex);
classesInput = classesInput || null; classesInput = classesInput || null;
stylesInput = stylesInput || null; stylesInput = stylesInput || null;
const ignoreAllClassUpdates = isMultiValueCacheHit(context, true, directiveIndex, classesInput); const ignoreAllClassUpdates = isMultiValueCacheHit(context, true, directiveIndex, classesInput);
@ -887,7 +847,6 @@ function patchStylingMapIntoContext(
if (dirty) { if (dirty) {
setContextDirty(context, true); setContextDirty(context, true);
setDirectiveDirty(context, directiveIndex, true);
} }
return totalNewAllocatedSlots; return totalNewAllocatedSlots;
@ -901,18 +860,14 @@ function patchStylingMapIntoContext(
* newly provided class value. * newly provided class value.
* @param offset The index of the CSS class which is being updated. * @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 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 * @param forceOverride whether or not to skip all directive prioritization
* and just apply the value regardless. * and just apply the value regardless.
*/ */
export function updateClassProp( export function updateClassProp(
context: StylingContext, offset: number, context: StylingContext, offset: number,
input: boolean | BoundPlayerFactory<boolean|null>| null, directiveRef?: any, input: boolean | BoundPlayerFactory<boolean|null>| null, directiveIndex: number = 0,
forceOverride?: boolean): void { 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. * newly provided style value.
* @param offset The index of the property which is being updated. * @param offset The index of the property which is being updated.
* @param value The CSS style value that will be assigned * @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 * @param forceOverride whether or not to skip all directive prioritization
* and just apply the value regardless. * and just apply the value regardless.
*/ */
export function updateStyleProp( export function updateStyleProp(
context: StylingContext, offset: number, context: StylingContext, offset: number,
input: string | boolean | null | BoundPlayerFactory<string|boolean|null>, directiveRef?: any, input: string | boolean | null | BoundPlayerFactory<string|boolean|null>,
forceOverride?: boolean): void { directiveIndex: number = 0, forceOverride?: boolean): void {
updateSingleStylingValue(context, offset, input, false, directiveRef, forceOverride); updateSingleStylingValue(context, offset, input, false, directiveIndex, forceOverride);
} }
function updateSingleStylingValue( function updateSingleStylingValue(
context: StylingContext, offset: number, context: StylingContext, offset: number,
input: string | boolean | null | BoundPlayerFactory<string|boolean|null>, isClassBased: boolean, input: string | boolean | null | BoundPlayerFactory<string|boolean|null>, isClassBased: boolean,
directiveRef: any, forceOverride?: boolean): void { directiveIndex: number, forceOverride?: boolean): void {
const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null); ngDevMode && assertValidDirectiveIndex(context, directiveIndex);
const singleIndex = getSinglePropIndexValue(context, directiveIndex, offset, isClassBased); const singleIndex = getSinglePropIndexValue(context, directiveIndex, offset, isClassBased);
const currValue = getValue(context, singleIndex); const currValue = getValue(context, singleIndex);
const currFlag = getPointers(context, singleIndex); const currFlag = getPointers(context, singleIndex);
@ -1001,7 +952,6 @@ function updateSingleStylingValue(
setDirty(context, indexForMulti, multiDirty); setDirty(context, indexForMulti, multiDirty);
setDirty(context, singleIndex, singleDirty); setDirty(context, singleIndex, singleDirty);
setDirectiveDirty(context, directiveIndex, true);
setContextDirty(context, true); setContextDirty(context, true);
} }
@ -1029,120 +979,128 @@ function updateSingleStylingValue(
* to this key/value map instead of being renderered via the renderer. * to this key/value map instead of being renderered via the renderer.
* @param stylesStore if provided, the updated style values will be applied * @param stylesStore if provided, the updated style values will be applied
* to this key/value map instead of being renderered via the renderer. * 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) * @returns number the total amount of players that got queued for animation (if any)
*/ */
export function renderStyling( 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, isFirstRender: boolean, classesStore?: BindingStore | null, stylesStore?: BindingStore | null,
directiveRef?: any): number { directiveIndex: number = 0): number {
let totalPlayersQueued = 0; let totalPlayersQueued = 0;
const targetDirectiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null);
if (isContextDirty(context) && isDirectiveDirty(context, targetDirectiveIndex)) { // this prevents multiple attempts to render style/class values on
const flushPlayerBuilders: any = // the same element...
context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty; if (allowHostInstructionsQueueFlush(context, directiveIndex)) {
const native = context[StylingIndex.ElementPosition] !; // all styling instructions present within any hostBindings functions
const multiStartIndex = getMultiStylesStartIndex(context); // 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; if (isContextDirty(context)) {
for (let i = StylingIndex.SingleStylesStartPosition; i < context.length; // this is here to prevent things like <ng-container [style] [class]>...</ng-container>
i += StylingIndex.Size) { // or if there are any host style or class bindings present in a directive set on
// there is no point in rendering styles that have not changed on screen // a container node
if (isDirty(context, i)) { const native = context[StylingIndex.ElementPosition] !as HTMLElement;
const flag = getPointers(context, i);
const directiveIndex = getDirectiveIndexFromEntry(context, i);
if (targetDirectiveIndex !== directiveIndex) {
stillDirty = true;
continue;
}
const prop = getProp(context, i); const flushPlayerBuilders: any =
const value = getValue(context, i); context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty;
const styleSanitizer = const multiStartIndex = getMultiStylesStartIndex(context);
(flag & StylingFlags.Sanitize) ? getStyleSanitizer(context, directiveIndex) : null;
const playerBuilder = getPlayerBuilder(context, i);
const isClassBased = flag & StylingFlags.Class ? true : false;
const isInSingleRegion = i < multiStartIndex;
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 let valueToApply: string|boolean|null = 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);
}
// VALUE DEFER CASE 2: Use the initial value if all else fails (is falsy) // VALUE DEFER CASE 1: Use a multi value instead of a null single value
// the initial value will always be a string or null, // this check implies that a single value was removed and we
// therefore we can safely adopt it in case there's nothing else // should now defer to a multi value and use that (if set).
// note that this should always be a falsy check since `false` is used if (isInSingleRegion && !valueExists(valueToApply, isClassBased)) {
// for both class and style comparisons (styles can't be false and false // single values ALWAYS have a reference to a multi index
// classes are turned off and should therefore defer to their initial values) const multiIndex = getMultiOrSingleIndex(flag);
// Note that we ignore class-based deferals because otherwise a class can never valueToApply = getValue(context, multiIndex);
// 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);
} }
}
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) { // if the first render is true then we do not want to start applying falsy
const rootContext = // values to the DOM element's styling. Otherwise then we know there has
Array.isArray(rootOrView) ? getRootContext(rootOrView) : rootOrView as RootContext; // been a change and even if it's falsy then it's removing something that
const playerContext = getPlayerContext(context) !; // was truthy before.
const playersStartIndex = playerContext[PlayerIndex.NonBuilderPlayersStart]; const doApplyValue = renderer && (isFirstRender ? valueToApply : true);
for (let i = PlayerIndex.PlayerBuildersStartPosition; i < playersStartIndex; if (doApplyValue) {
i += PlayerIndex.PlayerAndPlayerBuildersTupleSize) { if (isClassBased) {
const builder = playerContext[i] as ClassAndStylePlayerBuilder<any>| null; setClass(
const playerInsertionIndex = i + PlayerIndex.PlayerOffsetPosition; native, prop, valueToApply ? true : false, renderer !, classesStore,
const oldPlayer = playerContext[playerInsertionIndex] as Player | null; playerBuilder);
if (builder) { } else {
const player = builder.buildPlayer(oldPlayer, isFirstRender); setStyle(
if (player !== undefined) { native, prop, valueToApply as string | null, renderer !, styleSanitizer,
if (player != null) { stylesStore, playerBuilder);
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 setDirty(context, i, false);
// player
oldPlayer.destroy();
} }
} }
setContextPlayersDirty(context, false);
}
setDirectiveDirty(context, targetDirectiveIndex, false); if (flushPlayerBuilders) {
setContextDirty(context, stillDirty); 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; return totalPlayersQueued;
@ -1622,43 +1580,6 @@ export function getDirectiveIndexFromEntry(context: StylingContext, index: numbe
return value & DirectiveOwnerAndPlayerBuilderIndex.BitMask; 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 { function getInitialStylingValuesIndexOf(keyValues: InitialStylingValues, key: string): number {
for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < keyValues.length; for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < keyValues.length;
i += InitialStylingValuesIndex.Size) { i += InitialStylingValuesIndex.Size) {
@ -1724,21 +1645,6 @@ function getStyleSanitizer(context: StylingContext, directiveIndex: number): Sty
return value as StyleSanitizeFn | null; 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( function allowValueChange(
currentValue: string | boolean | null, newValue: string | boolean | null, currentValue: string | boolean | null, newValue: string | boolean | null,
currentDirectiveOwner: number, newDirectiveOwner: number) { currentDirectiveOwner: number, newDirectiveOwner: number) {
@ -1994,3 +1900,12 @@ function addOrUpdateStaticStyle(
staticStyles[index + InitialStylingValuesIndex.DirectiveOwnerOffset] = directiveOwnerIndex; staticStyles[index + InitialStylingValuesIndex.DirectiveOwnerOffset] = directiveOwnerIndex;
return index; 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 {getTNode, isStylingContext} from '../util/view_utils';
import {CorePlayerHandler} from './core_player_handler'; import {CorePlayerHandler} from './core_player_handler';
import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from './shared';
export const ANIMATION_PROP_PREFIX = '@'; export const ANIMATION_PROP_PREFIX = '@';
@ -35,12 +36,13 @@ export function createEmptyStylingContext(
[0, 0], // SinglePropOffsets [0, 0], // SinglePropOffsets
[0], // CachedMultiClassValue [0], // CachedMultiClassValue
[0], // CachedMultiStyleValue [0], // CachedMultiStyleValue
null, // HostBuffer
null, // PlayerContext null, // PlayerContext
]; ];
// whenever a context is created there is always a `null` directive // whenever a context is created there is always a `null` directive
// that is registered (which is a placeholder for the "template"). // that is registered (which is a placeholder for the "template").
allocateDirectiveIntoContext(context, null); allocateOrUpdateDirectiveIntoContext(context, DEFAULT_TEMPLATE_DIRECTIVE_INDEX);
return context; return context;
} }
@ -60,25 +62,28 @@ export function createEmptyStylingContext(
* @param directiveRef the directive that will be allocated into the context * @param directiveRef the directive that will be allocated into the context
* @returns the index where the directive was inserted into * @returns the index where the directive was inserted into
*/ */
export function allocateDirectiveIntoContext( export function allocateOrUpdateDirectiveIntoContext(
context: StylingContext, directiveRef: any | null): number { context: StylingContext, directiveIndex: number, singlePropValuesIndex: number = -1,
// this is a new directive which we have not seen yet. styleSanitizer?: StyleSanitizeFn | null | undefined): void {
const dirs = context[StylingIndex.DirectiveRegistryPosition]; const directiveRegistry = context[StylingIndex.DirectiveRegistryPosition];
const i = dirs.length;
const index = directiveIndex * DirectiveRegistryValuesIndex.Size;
// we preemptively make space into the directives array and then // we preemptively make space into the directives array and then
// assign values slot-by-slot to ensure that if the directive ordering // assign values slot-by-slot to ensure that if the directive ordering
// changes then it will still function // changes then it will still function
dirs.push(null, null, null, null); const limit = index + DirectiveRegistryValuesIndex.Size;
dirs[i + DirectiveRegistryValuesIndex.DirectiveValueOffset] = directiveRef; for (let i = directiveRegistry.length; i < limit; i += DirectiveRegistryValuesIndex.Size) {
dirs[i + DirectiveRegistryValuesIndex.DirtyFlagOffset] = false; // -1 is used to signal that the directive has been allocated, but
dirs[i + DirectiveRegistryValuesIndex.StyleSanitizerOffset] = null; // 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 const propValuesStartPosition = index + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset;
// no actual style or class bindings have been registered yet... if (singlePropValuesIndex >= 0 && directiveRegistry[propValuesStartPosition] === -1) {
dirs[i + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] = -1; directiveRegistry[propValuesStartPosition] = singlePropValuesIndex;
directiveRegistry[index + DirectiveRegistryValuesIndex.StyleSanitizerOffset] =
return i; styleSanitizer || null;
}
} }
/** /**

View File

@ -11,12 +11,12 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
describe('acceptance integration tests', () => { describe('acceptance integration tests', () => {
onlyInIvy('[style] and [class] bindings are a new feature') onlyInIvy('map-based [style] and [class] bindings are not supported in VE')
.it('should render host bindings on the root component', () => { .it('should render host bindings on the root component', () => {
@Component({template: '...'}) @Component({template: '...'})
class MyApp { class MyApp {
@HostBinding('style') public myStylesExp = {}; @HostBinding('style') myStylesExp = {};
@HostBinding('class') public myClassesExp = {}; @HostBinding('class') myClassesExp = {};
} }
TestBed.configureTestingModule({declarations: [MyApp]}); TestBed.configureTestingModule({declarations: [MyApp]});
@ -153,4 +153,166 @@ describe('acceptance integration tests', () => {
expect(element.style.width).toEqual('300px'); expect(element.style.width).toEqual('300px');
expect(element.classList.contains('abc')).toBeFalsy(); expect(element.classList.contains('abc')).toBeFalsy();
}); });
it('should render styling for parent and sub-classed components in order', () => {
@Component({
template: `
<child-and-parent-cmp></child-and-parent-cmp>
`
})
class MyApp {
}
@Component({template: '...'})
class ParentCmp {
@HostBinding('style.width') width1 = '100px';
@HostBinding('style.height') height1 = '100px';
@HostBinding('style.opacity') opacity1 = '0.5';
}
@Component({selector: 'child-and-parent-cmp', template: '...'})
class ChildCmp extends ParentCmp {
@HostBinding('style.width') width2 = '200px';
@HostBinding('style.height') height2 = '200px';
}
TestBed.configureTestingModule({declarations: [MyApp, ParentCmp, ChildCmp]});
const fixture = TestBed.createComponent(MyApp);
const element = fixture.nativeElement;
fixture.detectChanges();
const childElement = element.querySelector('child-and-parent-cmp');
expect(childElement.style.width).toEqual('200px');
expect(childElement.style.height).toEqual('200px');
expect(childElement.style.opacity).toEqual('0.5');
});
onlyInIvy('[style.prop] and [class.name] prioritization is a new feature')
.it('should prioritize styling present in the order of directive hostBinding evaluation, but consider sub-classed directive styling to be the most important',
() => {
const log: string[] = [];
@Component({template: '<div child-dir sibling-dir></div>'})
class MyApp {
}
@Directive({selector: '[parent-dir]'})
class ParentDir {
@HostBinding('style.width')
get width1() { return '100px'; }
@HostBinding('style.height')
get height1() { return '100px'; }
@HostBinding('style.color')
get color1() { return 'red'; }
}
@Directive({selector: '[child-dir]'})
class ChildDir extends ParentDir {
@HostBinding('style.width')
get width2() { return '200px'; }
@HostBinding('style.height')
get height2() { return '200px'; }
}
@Directive({selector: '[sibling-dir]'})
class SiblingDir {
@HostBinding('style.width')
get width3() { return '300px'; }
@HostBinding('style.height')
get height3() { return '300px'; }
@HostBinding('style.opacity')
get opacity3() { return '0.5'; }
@HostBinding('style.color')
get color1() { return 'blue'; }
}
TestBed.configureTestingModule(
{declarations: [MyApp, ParentDir, ChildDir, SiblingDir]});
const fixture = TestBed.createComponent(MyApp);
const element = fixture.nativeElement;
fixture.detectChanges();
const childElement = element.querySelector('div');
// width/height values were set in all directives, but the sub-class directive
// (ChildDir)
// had priority over the parent directive (ParentDir) which is why its value won. It
// also
// won over Dir because the SiblingDir directive was evaluated later on.
expect(childElement.style.width).toEqual('200px');
expect(childElement.style.height).toEqual('200px');
// ParentDir styled the color first before Dir
expect(childElement.style.color).toEqual('red');
// Dir was the only directive to style opacity
expect(childElement.style.opacity).toEqual('0.5');
});
it('should ensure that static classes are assigned to ng-container elements and picked up for content projection',
() => {
@Component({
template: `
<project>
outer
<ng-container class="inner">
inner
</ng-container>
</project>
`
})
class MyApp {
}
@Component({
selector: 'project',
template: `
<div class="outer-area">
<ng-content></ng-content>
</div>
<div class="inner-area">
<ng-content select=".inner"></ng-content>
</div>
`
})
class ProjectCmp {
}
TestBed.configureTestingModule({declarations: [MyApp, ProjectCmp]});
const fixture = TestBed.createComponent(MyApp);
const element = fixture.nativeElement;
fixture.detectChanges();
const inner = element.querySelector('.inner-area');
expect(inner.textContent.trim()).toEqual('inner');
const outer = element.querySelector('.outer-area');
expect(outer.textContent.trim()).toEqual('outer');
});
it('should allow class-bindings to be placed on ng-container elements', () => {
@Component({
template: `
<ng-container [class.foo]="true" dir-that-adds-other-classes>...</ng-container>
`
})
class MyApp {
}
@Directive({selector: '[dir-that-adds-other-classes]'})
class DirThatAddsOtherClasses {
@HostBinding('class.other-class') bool = true;
}
TestBed.configureTestingModule({declarations: [MyApp, DirThatAddsOtherClasses]});
expect(() => {
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
}).not.toThrow();
});
}); });

View File

@ -29,6 +29,9 @@
{ {
"name": "DECLARATION_VIEW" "name": "DECLARATION_VIEW"
}, },
{
"name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX"
},
{ {
"name": "DepComponent" "name": "DepComponent"
}, },
@ -158,6 +161,9 @@
{ {
"name": "_renderCompCount" "name": "_renderCompCount"
}, },
{
"name": "_selectedIndex"
},
{ {
"name": "addComponentLogic" "name": "addComponentLogic"
}, },
@ -171,7 +177,7 @@
"name": "allocStylingContext" "name": "allocStylingContext"
}, },
{ {
"name": "allocateDirectiveIntoContext" "name": "allocateOrUpdateDirectiveIntoContext"
}, },
{ {
"name": "allowValueChange" "name": "allowValueChange"
@ -338,9 +344,6 @@
{ {
"name": "getDirectiveDef" "name": "getDirectiveDef"
}, },
{
"name": "getDirectiveRegistryValuesIndexOf"
},
{ {
"name": "getElementDepthCount" "name": "getElementDepthCount"
}, },
@ -449,6 +452,9 @@
{ {
"name": "increaseElementDepthCount" "name": "increaseElementDepthCount"
}, },
{
"name": "incrementActiveDirectiveId"
},
{ {
"name": "initNodeFlags" "name": "initNodeFlags"
}, },
@ -627,7 +633,7 @@
"name": "saveResolvedLocalsInData" "name": "saveResolvedLocalsInData"
}, },
{ {
"name": "setActiveHost" "name": "setActiveHostElement"
}, },
{ {
"name": "setBindingRoot" "name": "setBindingRoot"
@ -668,6 +674,9 @@
{ {
"name": "setPreviousOrParentTNode" "name": "setPreviousOrParentTNode"
}, },
{
"name": "setSelectedIndex"
},
{ {
"name": "setStyle" "name": "setStyle"
}, },

View File

@ -134,6 +134,9 @@
{ {
"name": "_renderCompCount" "name": "_renderCompCount"
}, },
{
"name": "_selectedIndex"
},
{ {
"name": "addToViewTree" "name": "addToViewTree"
}, },
@ -323,6 +326,9 @@
{ {
"name": "includeViewProviders" "name": "includeViewProviders"
}, },
{
"name": "incrementActiveDirectiveId"
},
{ {
"name": "initNodeFlags" "name": "initNodeFlags"
}, },
@ -435,7 +441,7 @@
"name": "resetPreOrderHookFlags" "name": "resetPreOrderHookFlags"
}, },
{ {
"name": "setActiveHost" "name": "setActiveHostElement"
}, },
{ {
"name": "setBindingRoot" "name": "setBindingRoot"
@ -464,6 +470,9 @@
{ {
"name": "setPreviousOrParentTNode" "name": "setPreviousOrParentTNode"
}, },
{
"name": "setSelectedIndex"
},
{ {
"name": "setStyle" "name": "setStyle"
}, },

View File

@ -44,6 +44,9 @@
{ {
"name": "DECLARATION_VIEW" "name": "DECLARATION_VIEW"
}, },
{
"name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX"
},
{ {
"name": "DefaultIterableDiffer" "name": "DefaultIterableDiffer"
}, },
@ -371,6 +374,9 @@
{ {
"name": "_renderCompCount" "name": "_renderCompCount"
}, },
{
"name": "_selectedIndex"
},
{ {
"name": "_symbolIterator" "name": "_symbolIterator"
}, },
@ -399,7 +405,10 @@
"name": "allocStylingContext" "name": "allocStylingContext"
}, },
{ {
"name": "allocateDirectiveIntoContext" "name": "allocateOrUpdateDirectiveIntoContext"
},
{
"name": "allowFlush"
}, },
{ {
"name": "allowValueChange" "name": "allowValueChange"
@ -572,9 +581,6 @@
{ {
"name": "elementClassProp" "name": "elementClassProp"
}, },
{
"name": "elementClassPropInternal"
},
{ {
"name": "elementCreate" "name": "elementCreate"
}, },
@ -644,6 +650,9 @@
{ {
"name": "findViaComponent" "name": "findViaComponent"
}, },
{
"name": "flushQueue"
},
{ {
"name": "forwardRef" "name": "forwardRef"
}, },
@ -698,12 +707,6 @@
{ {
"name": "getDirectiveIndexFromEntry" "name": "getDirectiveIndexFromEntry"
}, },
{
"name": "getDirectiveIndexFromRegistry"
},
{
"name": "getDirectiveRegistryValuesIndexOf"
},
{ {
"name": "getElementDepthCount" "name": "getElementDepthCount"
}, },
@ -755,9 +758,6 @@
{ {
"name": "getMatchingBindingIndex" "name": "getMatchingBindingIndex"
}, },
{
"name": "getMultiClassesStartIndex"
},
{ {
"name": "getMultiOrSingleIndex" "name": "getMultiOrSingleIndex"
}, },
@ -908,6 +908,9 @@
{ {
"name": "increaseElementDepthCount" "name": "increaseElementDepthCount"
}, },
{
"name": "incrementActiveDirectiveId"
},
{ {
"name": "initElementStyling" "name": "initElementStyling"
}, },
@ -983,9 +986,6 @@
{ {
"name": "isDifferent" "name": "isDifferent"
}, },
{
"name": "isDirectiveDirty"
},
{ {
"name": "isDirty" "name": "isDirty"
}, },
@ -1224,7 +1224,7 @@
"name": "select" "name": "select"
}, },
{ {
"name": "setActiveHost" "name": "setActiveHostElement"
}, },
{ {
"name": "setBindingRoot" "name": "setBindingRoot"
@ -1247,9 +1247,6 @@
{ {
"name": "setCurrentQueryIndex" "name": "setCurrentQueryIndex"
}, },
{
"name": "setDirectiveDirty"
},
{ {
"name": "setDirty" "name": "setDirty"
}, },
@ -1292,6 +1289,9 @@
{ {
"name": "setSanitizeFlag" "name": "setSanitizeFlag"
}, },
{
"name": "setSelectedIndex"
},
{ {
"name": "setStyle" "name": "setStyle"
}, },

View File

@ -16,14 +16,5 @@
// clang-format off // clang-format off
// tslint:disable // tslint:disable
window.testBlocklist = { window.testBlocklist = {};
"MatSidenav should be fixed position when in fixed mode": { // clang-format on
"error": "Error: Expected ng-tns-c380-0 ng-trigger ng-trigger-transform mat-drawer mat-sidenav mat-drawer-over ng-star-inserted to contain 'mat-sidenav-fixed'.",
"notes": "FW-1132: Host class bindings don't work if super class has host class bindings"
},
"MatSidenav should set fixed bottom and top when in fixed mode": {
"error": "Error: Expected '' to be '20px'.",
"notes": "FW-1132: Host class bindings don't work if super class has host class bindings"
}
};
// clang-format on