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

@ -11,20 +11,23 @@ import {assertHasParent} from '../assert';
import {attachPatchData} from '../context_discovery';
import {registerPostOrderHooks} from '../hooks';
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 {BINDING_INDEX, QUERIES, RENDERER, TVIEW} from '../interfaces/view';
import {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation';
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 {getStylingContext, hasClassInput, hasStyleInput} from '../styling/util';
import {NO_CHANGE} from '../tokens';
import {attrsStylingIndexOf, setUpAttributes} from '../util/attrs_utils';
import {renderStringify} from '../util/misc_utils';
import {getNativeByIndex, getNativeByTNode, getTNode} from '../util/view_utils';
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.
@ -256,17 +259,26 @@ export function elementAttribute(
* @publicApi
*/
export function elementHostAttrs(attrs: TAttributes) {
const tNode = getPreviousOrParentTNode();
const hostElementIndex = getSelectedIndex();
const lView = getLView();
const native = getNativeByTNode(tNode, lView) as RElement;
const lastAttrIndex = setUpAttributes(native, attrs);
const stylingAttrsStartIndex = attrsStylingIndexOf(attrs, lastAttrIndex);
if (stylingAttrsStartIndex >= 0) {
const directive = getActiveHostContext();
if (tNode.stylingTemplate) {
patchContextWithStaticAttrs(tNode.stylingTemplate, attrs, stylingAttrsStartIndex, directive);
} else {
tNode.stylingTemplate = initializeStaticContext(attrs, stylingAttrsStartIndex, directive);
const tNode = getTNode(hostElementIndex, lView);
// non-element nodes (e.g. `<ng-container>`) are not rendered as actual
// element nodes and adding styles/classes on to them will cause runtime
// errors...
if (tNode.type === TNodeType.Element) {
const native = getNativeByTNode(tNode, lView) as RElement;
const lastAttrIndex = setUpAttributes(native, attrs);
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 {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation';
import {applyOnCreateInstructions} from '../node_util';
import {getIsParent, getLView, getPreviousOrParentTNode, setIsParent, setPreviousOrParentTNode} from '../state';
import {createDirectivesAndLocals, createNodeAtIndex, executeContentQueries, setNodeStylingTemplate} from './shared';
@ -83,5 +84,9 @@ export function elementContainerEnd(): void {
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);
}

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 {assertNodeOfPossibleTypes, assertNodeType} from '../node_assert';
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 {NO_CHANGE} from '../tokens';
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 {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
* 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
// the current element and directive index.
currentElementIndex = -instruction;
setActiveHostElement(currentElementIndex);
// Injector block and providers are taken into account.
const providerCount = (tView.expandoInstructions[++i] as number);
bindingRootIndex += INJECTOR_BLOOM_PARENT_SIZE + providerCount;
@ -124,14 +127,20 @@ export function setHostBindings(tView: TView, viewData: LView): void {
if (instruction !== null) {
viewData[BINDING_INDEX] = bindingRootIndex;
const hostCtx = unwrapRNode(viewData[currentDirectiveIndex]);
setActiveHost(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++;
}
}
}
setActiveHostElement(null);
}
/** 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 expando = tView.expandoInstructions !;
const firstTemplatePass = tView.firstTemplatePass;
const elementIndex = tNode.index - HEADER_OFFSET;
setActiveHostElement(elementIndex);
for (let i = start; i < end; i++) {
const def = tView.data[i] as DirectiveDef<any>;
const directive = viewData[i];
if (def.hostBindings) {
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) {
expando.push(null);
}
}
setActiveHostElement(null);
}
export function invokeHostBindingsInCreationMode(
@ -914,9 +935,7 @@ export function invokeHostBindingsInCreationMode(
const previousExpandoLength = expando.length;
setCurrentDirectiveDef(def);
const elementIndex = tNode.index - HEADER_OFFSET;
setActiveHost(directive, elementIndex);
def.hostBindings !(RenderFlags.Create, directive, elementIndex);
setActiveHost(null);
setCurrentDirectiveDef(null);
// `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

View File

@ -6,19 +6,24 @@
* found in the LICENSE file at https://angular.io/license
*/
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {TNode} from '../interfaces/node';
import {TNode, TNodeType} from '../interfaces/node';
import {PlayerFactory} from '../interfaces/player';
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 {ParamsOf, enqueueHostInstruction, registerHostDirective} from '../styling/host_instructions_queue';
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 {renderStringify} from '../util/misc_utils';
import {getRootContext} from '../util/view_traversal_utils';
import {getTNode} from '../util/view_utils';
import {scheduleTick, setInputsForProperty} from './shared';
/*
* The contents of this file include the instructions for all styling-related
* operations in Angular.
@ -40,7 +45,6 @@ import {scheduleTick, setInputsForProperty} from './shared';
* - elementHostStylingApply
*/
/**
* 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`
// instruction (because directives are created first before styling is
// 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();
}
const directive = getActiveHostContext();
const directiveStylingIndex = getActiveDirectiveStylingIndex();
// despite the binding being applied in a queue (below), the allocation
// of the directive into the context happens right away. The reason for
// this is to retain the ordering of the directives (which is important
// for the prioritization of bindings).
allocateDirectiveIntoContext(tNode.stylingTemplate, directive);
allocateOrUpdateDirectiveIntoContext(tNode.stylingTemplate, directiveStylingIndex);
const fns = tNode.onElementCreationFns = tNode.onElementCreationFns || [];
fns.push(
() => initElementStyling(
tNode, classBindingNames, styleBindingNames, styleSanitizer, directive));
fns.push(() => {
initElementStyling(
tNode, classBindingNames, styleBindingNames, styleSanitizer, directiveStylingIndex);
registerHostDirective(tNode.stylingTemplate !, directiveStylingIndex);
});
}
function initElementStyling(
tNode: TNode, classBindingNames?: string[] | null, styleBindingNames?: string[] | null,
styleSanitizer?: StyleSanitizeFn | null, directive?: {} | null): void {
tNode: TNode, classBindingNames: string[] | null | undefined,
styleBindingNames: string[] | null | undefined,
styleSanitizer: StyleSanitizeFn | null | undefined, directiveStylingIndex: number): void {
updateContextWithBindings(
tNode.stylingTemplate !, directive || null, classBindingNames, styleBindingNames,
tNode.stylingTemplate !, directiveStylingIndex, classBindingNames, styleBindingNames,
styleSanitizer);
}
@ -160,7 +169,10 @@ function initElementStyling(
export function elementStyleProp(
index: number, styleIndex: number, value: string | number | String | PlayerFactory | null,
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(
styleIndex: number, value: string | number | String | PlayerFactory | null,
suffix?: string | null, forceOverride?: boolean): void {
elementStylePropInternal(
getActiveHostContext() !, getActiveHostElementIndex() !, styleIndex, value, suffix,
forceOverride);
const directiveStylingIndex = getActiveDirectiveStylingIndex();
const hostElementIndex = getSelectedIndex();
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(
directive: {} | null, index: number, styleIndex: number,
value: string | number | String | PlayerFactory | null, suffix?: string | null,
forceOverride?: boolean): void {
function resolveStylePropValue(
value: string | number | String | PlayerFactory | null, suffix: string | null | undefined) {
let valueToAdd: string|null = null;
if (value !== null) {
if (suffix) {
@ -214,9 +231,7 @@ function elementStylePropInternal(
valueToAdd = value as any as string;
}
}
updateElementStyleProp(
getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, directive,
forceOverride);
return valueToAdd;
}
@ -241,7 +256,12 @@ function elementStylePropInternal(
export function elementClassProp(
index: number, classIndex: number, value: boolean | PlayerFactory,
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(
classIndex: number, value: boolean | PlayerFactory, forceOverride?: boolean): void {
elementClassPropInternal(
getActiveHostContext() !, getActiveHostElementIndex() !, classIndex, value, forceOverride);
}
const directiveStylingIndex = getActiveDirectiveStylingIndex();
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) ?
(value as BoundPlayerFactory<boolean|null>) :
booleanOrNull(value);
updateElementClassProp(
getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, input, directive,
forceOverride);
const args: ParamsOf<typeof updateElementClassProp> =
[stylingContext, classIndex, input, directiveStylingIndex, forceOverride];
enqueueHostInstruction(stylingContext, directiveStylingIndex, updateElementClassProp, args);
}
function booleanOrNull(value: any): boolean|null {
@ -309,7 +329,30 @@ function booleanOrNull(value: any): boolean|null {
export function elementStylingMap(
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
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(
classes: {[key: string]: any} | string | NO_CHANGE | null,
styles?: {[styleName: string]: any} | NO_CHANGE | null): void {
elementStylingMapInternal(
getActiveHostContext() !, getActiveHostElementIndex() !, classes, styles);
}
const directiveStylingIndex = getActiveDirectiveStylingIndex();
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 tNode = getTNode(index, lView);
const stylingContext = getStylingContext(index + HEADER_OFFSET, lView);
const stylingContext = getStylingContext(hostElementIndex + 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 (!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);
const args: ParamsOf<typeof updateStylingMap> =
[stylingContext, classes, styles, directiveStylingIndex];
enqueueHostInstruction(stylingContext, directiveStylingIndex, updateStylingMap, args);
}
@ -387,7 +406,7 @@ function elementStylingMapInternal(
* @publicApi
*/
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
*/
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 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 stylingContext = getStylingContext(index + HEADER_OFFSET, lView);
const totalPlayersQueued = renderStyling(
getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender, null,
null, directive);
stylingContext, renderer, lView, isFirstRender, null, null, directiveStylingIndex);
if (totalPlayersQueued > 0) {
const rootContext = getRootContext(lView);
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();
}