refactor(ivy): delete ɵɵelementHostAttrs instruction (#34717)

PR Close #34717
This commit is contained in:
Misko Hevery 2020-01-09 21:48:16 -08:00 committed by Miško Hevery
parent 2227d471a4
commit da7e362bce
14 changed files with 219 additions and 265 deletions

View File

@ -17,13 +17,15 @@ import {assertComponentType} from './assert';
import {getComponentDef} from './definition'; import {getComponentDef} from './definition';
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di'; import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
import {registerPostOrderHooks} from './hooks'; import {registerPostOrderHooks} from './hooks';
import {CLEAN_PROMISE, addHostBindingsToExpandoInstructions, addToViewTree, createLView, createTView, getOrCreateTComponentView, getOrCreateTNode, growHostVarsSpace, initNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, markAsComponentHost, refreshView, renderView} from './instructions/shared'; import {CLEAN_PROMISE, addHostBindingsToExpandoInstructions, addToViewTree, createLView, createTView, getOrCreateTComponentView, getOrCreateTNode, growHostVarsSpace, initTNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, markAsComponentHost, refreshView, renderInitialStyling, renderView} from './instructions/shared';
import {registerInitialStylingOnTNode} from './instructions/styling';
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition'; import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
import {TElementNode, TNode, TNodeType} from './interfaces/node'; import {TElementNode, TNode, TNodeType} from './interfaces/node';
import {PlayerHandler} from './interfaces/player'; import {PlayerHandler} from './interfaces/player';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, LView, LViewFlags, RootContext, RootContextFlags, TVIEW, TViewType} from './interfaces/view'; import {CONTEXT, HEADER_OFFSET, LView, LViewFlags, RootContext, RootContextFlags, TVIEW, TViewType} from './interfaces/view';
import {enterView, getPreviousOrParentTNode, incrementActiveDirectiveId, leaveView, setActiveHostElement} from './state'; import {enterView, getPreviousOrParentTNode, incrementActiveDirectiveId, leaveView, setActiveHostElement} from './state';
import {setUpAttributes} from './util/attrs_utils';
import {publishDefaultGlobalUtils} from './util/global_utils'; import {publishDefaultGlobalUtils} from './util/global_utils';
import {defaultScheduler, stringifyForError} from './util/misc_utils'; import {defaultScheduler, stringifyForError} from './util/misc_utils';
import {getRootContext} from './util/view_traversal_utils'; import {getRootContext} from './util/view_traversal_utils';
@ -171,6 +173,14 @@ export function createRootComponentView(
ngDevMode && assertDataInRange(rootView, 0 + HEADER_OFFSET); ngDevMode && assertDataInRange(rootView, 0 + HEADER_OFFSET);
rootView[0 + HEADER_OFFSET] = rNode; rootView[0 + HEADER_OFFSET] = rNode;
const tNode: TElementNode = getOrCreateTNode(tView, null, 0, TNodeType.Element, null, null); const tNode: TElementNode = getOrCreateTNode(tView, null, 0, TNodeType.Element, null, null);
const mergedAttrs = tNode.mergedAttrs = def.hostAttrs;
if (mergedAttrs !== null) {
registerInitialStylingOnTNode(tNode, mergedAttrs, 0);
if (rNode !== null) {
setUpAttributes(renderer, rNode, mergedAttrs);
renderInitialStyling(renderer, rNode, tNode, false);
}
}
const componentView = createLView( const componentView = createLView(
rootView, getOrCreateTComponentView(def), null, rootView, getOrCreateTComponentView(def), null,
def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, rootView[HEADER_OFFSET], tNode, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, rootView[HEADER_OFFSET], tNode,
@ -179,7 +189,7 @@ export function createRootComponentView(
if (tView.firstCreatePass) { if (tView.firstCreatePass) {
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), tView, def.type); diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), tView, def.type);
markAsComponentHost(tView, tNode); markAsComponentHost(tView, tNode);
initNodeFlags(tNode, rootView.length, 1); initTNodeFlags(tNode, rootView.length, 1);
} }
addToViewTree(rootView, componentView); addToViewTree(rootView, componentView);
@ -216,7 +226,8 @@ export function createRootComponent<T>(
// part of the `hostAttrs`. // part of the `hostAttrs`.
// The check for componentDef.hostBindings is wrong since now some directives may not // The check for componentDef.hostBindings is wrong since now some directives may not
// have componentDef.hostBindings but they still need to process hostVars and hostAttrs // have componentDef.hostBindings but they still need to process hostVars and hostAttrs
if (tView.firstCreatePass && (componentDef.hostBindings || componentDef.hostAttrs !== null)) { if (tView.firstCreatePass &&
(componentDef.hostBindings !== null || componentDef.hostAttrs !== null)) {
const elementIndex = rootTNode.index - HEADER_OFFSET; const elementIndex = rootTNode.index - HEADER_OFFSET;
setActiveHostElement(elementIndex); setActiveHostElement(elementIndex);
incrementActiveDirectiveId(); incrementActiveDirectiveId();
@ -225,9 +236,7 @@ export function createRootComponent<T>(
addHostBindingsToExpandoInstructions(rootTView, componentDef); addHostBindingsToExpandoInstructions(rootTView, componentDef);
growHostVarsSpace(rootTView, rootLView, componentDef.hostVars); growHostVarsSpace(rootTView, rootLView, componentDef.hostVars);
const expando = tView.expandoInstructions !; invokeHostBindingsInCreationMode(componentDef, component, rootTNode);
invokeHostBindingsInCreationMode(
componentDef, expando, component, rootTNode, tView.firstCreatePass);
setActiveHostElement(null); setActiveHostElement(null);
} }

View File

@ -7,7 +7,7 @@
*/ */
import {assertDataInRange, assertDefined, assertEqual} from '../../util/assert'; import {assertDataInRange, assertDefined, assertEqual} from '../../util/assert';
import {assertHasParent} from '../assert'; import {assertFirstCreatePass, assertHasParent} from '../assert';
import {attachPatchData} from '../context_discovery'; import {attachPatchData} from '../context_discovery';
import {registerPostOrderHooks} from '../hooks'; import {registerPostOrderHooks} from '../hooks';
import {TAttributes, TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; import {TAttributes, TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
@ -28,20 +28,21 @@ import {registerInitialStylingOnTNode} from './styling';
function elementStartFirstCreatePass( function elementStartFirstCreatePass(
index: number, tView: TView, lView: LView, native: RElement, name: string, index: number, tView: TView, lView: LView, native: RElement, name: string,
attrsIndex?: number | null, localRefsIndex?: number): TElementNode { attrsIndex?: number | null, localRefsIndex?: number): TElementNode {
ngDevMode && assertFirstCreatePass(tView);
ngDevMode && ngDevMode.firstCreatePass++; ngDevMode && ngDevMode.firstCreatePass++;
const tViewConsts = tView.consts; const tViewConsts = tView.consts;
const attrs = getConstant<TAttributes>(tViewConsts, attrsIndex); const attrs = getConstant<TAttributes>(tViewConsts, attrsIndex);
const tNode = getOrCreateTNode(tView, lView[T_HOST], index, TNodeType.Element, name, attrs); const tNode = getOrCreateTNode(tView, lView[T_HOST], index, TNodeType.Element, name, attrs);
if (attrs !== null) {
registerInitialStylingOnTNode(tNode, attrs, 0);
}
const hasDirectives = const hasDirectives =
resolveDirectives(tView, lView, tNode, getConstant<string[]>(tViewConsts, localRefsIndex)); resolveDirectives(tView, lView, tNode, getConstant<string[]>(tViewConsts, localRefsIndex));
ngDevMode && warnAboutUnknownElement(lView, native, tNode, hasDirectives); ngDevMode && warnAboutUnknownElement(lView, native, tNode, hasDirectives);
if (tNode.mergedAttrs !== null) {
registerInitialStylingOnTNode(tNode, tNode.mergedAttrs, 0);
}
if (tView.queries !== null) { if (tView.queries !== null) {
tView.queries.elementStart(tView, tNode); tView.queries.elementStart(tView, tNode);
} }
@ -83,9 +84,9 @@ export function ɵɵelementStart(
tView.data[adjustedIndex] as TElementNode; tView.data[adjustedIndex] as TElementNode;
setPreviousOrParentTNode(tNode, true); setPreviousOrParentTNode(tNode, true);
const attrs = tNode.attrs; const mergedAttrs = tNode.mergedAttrs;
if (attrs != null) { if (mergedAttrs !== null) {
setUpAttributes(renderer, native, attrs); setUpAttributes(renderer, native, mergedAttrs);
} }
if ((tNode.flags & TNodeFlags.hasInitialStyling) === TNodeFlags.hasInitialStyling) { if ((tNode.flags & TNodeFlags.hasInitialStyling) === TNodeFlags.hasInitialStyling) {
renderInitialStyling(renderer, native, tNode, false); renderInitialStyling(renderer, native, tNode, false);
@ -106,7 +107,7 @@ export function ɵɵelementStart(
createDirectivesInstances(tView, lView, tNode); createDirectivesInstances(tView, lView, tNode);
executeContentQueries(tView, tNode, lView); executeContentQueries(tView, tNode, lView);
} }
if (localRefsIndex != null) { if (localRefsIndex !== null) {
saveResolvedLocalsInData(lView, tNode); saveResolvedLocalsInData(lView, tNode);
} }
} }
@ -169,78 +170,6 @@ export function ɵɵelement(
ɵɵelementEnd(); ɵɵelementEnd();
} }
/**
* Assign static attribute values to a host element.
*
* This instruction will assign static attribute values as well as class and style
* values to an element within the host bindings function. Since attribute values
* can consist of different types of values, the `attrs` array must include the values in
* the following format:
*
* attrs = [
* // static attributes (like `title`, `name`, `id`...)
* attr1, value1, attr2, value,
*
* // a single namespace value (like `x:id`)
* NAMESPACE_MARKER, namespaceUri1, name1, value1,
*
* // another single namespace value (like `x:name`)
* NAMESPACE_MARKER, namespaceUri2, name2, value2,
*
* // a series of CSS classes that will be applied to the element (no spaces)
* CLASSES_MARKER, class1, class2, class3,
*
* // a series of CSS styles (property + value) that will be applied to the element
* STYLES_MARKER, prop1, value1, prop2, value2
* ]
*
* All non-class and non-style attributes must be defined at the start of the list
* first before all class and style values are set. When there is a change in value
* type (like when classes and styles are introduced) a marker must be used to separate
* the entries. The marker values themselves are set via entries found in the
* [AttributeMarker] enum.
*
* NOTE: This instruction is meant to used from `hostBindings` function only.
*
* @param directive A directive instance the styling is associated with.
* @param attrs An array of static values (attributes, classes and styles) with the correct marker
* values.
*
* @codeGenApi
*/
export function ɵɵelementHostAttrs(attrs: TAttributes) {
const hostElementIndex = getSelectedIndex();
const lView = getLView();
const tView = lView[TVIEW];
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;
// TODO(misko-next): setup attributes need to be moved out of `ɵɵelementHostAttrs`
const lastAttrIndex = setUpAttributes(lView[RENDERER], native, attrs);
if (tView.firstCreatePass) {
const stylingNeedsToBeRendered = registerInitialStylingOnTNode(tNode, attrs, lastAttrIndex);
// this is only called during the first template pass in the
// event that this current directive assigned initial style/class
// host attribute values to the element. Because initial styling
// values are applied before directives are first rendered (within
// `createElement`) this means that initial styling for any directives
// still needs to be applied. Note that this will only happen during
// the first template pass and not each time a directive applies its
// attribute values to the element.
if (stylingNeedsToBeRendered) {
const renderer = lView[RENDERER];
// TODO(misko-next): Styling initialization should move out of `ɵɵelementHostAttrs`
renderInitialStyling(renderer, native, tNode, true);
}
}
}
}
function setDirectiveStylingInput( function setDirectiveStylingInput(
context: TStylingContext | StylingMapArray | null, lView: LView, context: TStylingContext | StylingMapArray | null, lView: LView,
stylingInputs: (string | number)[], propName: string) { stylingInputs: (string | number)[], propName: string) {

View File

@ -154,31 +154,32 @@ export const TViewConstructor = class TView implements ITView {
export const TNodeConstructor = class TNode implements ITNode { export const TNodeConstructor = class TNode implements ITNode {
constructor( constructor(
public tView_: TView, // public tView_: TView, //
public type: TNodeType, // public type: TNodeType, //
public index: number, // public index: number, //
public injectorIndex: number, // public injectorIndex: number, //
public directiveStart: number, // public directiveStart: number, //
public directiveEnd: number, // public directiveEnd: number, //
public propertyBindings: number[]|null, // public propertyBindings: number[]|null, //
public flags: TNodeFlags, // public flags: TNodeFlags, //
public providerIndexes: TNodeProviderIndexes, // public providerIndexes: TNodeProviderIndexes, //
public tagName: string|null, // public tagName: string|null, //
public attrs: (string|AttributeMarker|(string|SelectorFlags)[])[]|null, // public attrs: (string|AttributeMarker|(string|SelectorFlags)[])[]|null, //
public localNames: (string|number)[]|null, // public mergedAttrs: (string|AttributeMarker|(string|SelectorFlags)[])[]|null, //
public initialInputs: (string[]|null)[]|null|undefined, // public localNames: (string|number)[]|null, //
public inputs: PropertyAliases|null, // public initialInputs: (string[]|null)[]|null|undefined, //
public outputs: PropertyAliases|null, // public inputs: PropertyAliases|null, //
public tViews: ITView|ITView[]|null, // public outputs: PropertyAliases|null, //
public next: ITNode|null, // public tViews: ITView|ITView[]|null, //
public projectionNext: ITNode|null, // public next: ITNode|null, //
public child: ITNode|null, // public projectionNext: ITNode|null, //
public parent: TElementNode|TContainerNode|null, // public child: ITNode|null, //
public projection: number|(ITNode|RNode[])[]|null, // public parent: TElementNode|TContainerNode|null, //
public styles: TStylingContext|null, // public projection: number|(ITNode|RNode[])[]|null, //
public classes: TStylingContext|null, // public styles: TStylingContext|null, //
public classBindings: TStylingRange, // public classes: TStylingContext|null, //
public styleBindings: TStylingRange, // public classBindings: TStylingRange, //
public styleBindings: TStylingRange, //
) {} ) {}
get type_(): string { get type_(): string {

View File

@ -20,7 +20,7 @@ import {attachPatchData} from '../context_discovery';
import {getFactoryDef} from '../definition'; import {getFactoryDef} from '../definition';
import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di'; import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di';
import {throwMultipleComponentError} from '../errors'; import {throwMultipleComponentError} from '../errors';
import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags, registerPreOrderHooks} from '../hooks'; import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} from '../hooks';
import {ACTIVE_INDEX, ActiveIndexFlag, CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS} from '../interfaces/container'; import {ACTIVE_INDEX, ActiveIndexFlag, CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS} from '../interfaces/container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition'; import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from '../interfaces/injector'; import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from '../interfaces/injector';
@ -28,20 +28,18 @@ import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, Pro
import {RComment, RElement, RNode, RText, Renderer3, RendererFactory3, isProceduralRenderer} from '../interfaces/renderer'; import {RComment, RElement, RNode, RText, Renderer3, RendererFactory3, isProceduralRenderer} from '../interfaces/renderer';
import {SanitizerFn} from '../interfaces/sanitization'; import {SanitizerFn} from '../interfaces/sanitization';
import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRootView} from '../interfaces/type_checks'; import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRootView} from '../interfaces/type_checks';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TData, TVIEW, TView, TViewType, T_HOST} from '../interfaces/view'; import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TData, TVIEW, TView, TViewType, T_HOST} from '../interfaces/view';
import {assertNodeOfPossibleTypes} from '../node_assert'; import {assertNodeOfPossibleTypes} from '../node_assert';
import {isNodeMatchingSelectorList} from '../node_selector_matcher'; import {isNodeMatchingSelectorList} from '../node_selector_matcher';
import {ActiveElementFlags, enterView, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, hasActiveElementFlag, incrementActiveDirectiveId, leaveView, leaveViewProcessExit, setActiveHostElement, setBindingIndex, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state'; import {ActiveElementFlags, enterView, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, hasActiveElementFlag, incrementActiveDirectiveId, leaveView, leaveViewProcessExit, setActiveHostElement, setBindingIndex, setBindingRoot, setCheckNoChangesMode, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state';
import {renderStylingMap, writeStylingValueDirectly} from '../styling/bindings'; import {renderStylingMap, writeStylingValueDirectly} from '../styling/bindings';
import {NO_CHANGE} from '../tokens'; import {NO_CHANGE} from '../tokens';
import {isAnimationProp} from '../util/attrs_utils'; import {isAnimationProp, mergeHostAttrs} from '../util/attrs_utils';
import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils'; import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils';
import {getInitialStylingValue} from '../util/styling_utils'; import {getInitialStylingValue} from '../util/styling_utils';
import {getLViewParent} from '../util/view_traversal_utils'; import {getLViewParent} from '../util/view_traversal_utils';
import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isCreationMode, readPatchedLView, resetPreOrderHookFlags, unwrapRNode, viewAttachedToChangeDetector} from '../util/view_utils'; import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isCreationMode, readPatchedLView, resetPreOrderHookFlags, viewAttachedToChangeDetector} from '../util/view_utils';
import {selectIndexInternal} from './advance'; import {selectIndexInternal} from './advance';
import {ɵɵelementHostAttrs} from './element';
import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeConstructor, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData} from './lview_debug'; import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeConstructor, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData} from './lview_debug';
@ -828,6 +826,7 @@ export function createTNode(
0, // providerIndexes: TNodeProviderIndexes 0, // providerIndexes: TNodeProviderIndexes
tagName, // tagName: string|null tagName, // tagName: string|null
attrs, // attrs: (string|AttributeMarker|(string|SelectorFlags)[])[]|null attrs, // attrs: (string|AttributeMarker|(string|SelectorFlags)[])[]|null
null, // mergedAttrs
null, // localNames: (string|number)[]|null null, // localNames: (string|number)[]|null
undefined, // initialInputs: (string[]|null)[]|null|undefined undefined, // initialInputs: (string[]|null)[]|null|undefined
null, // inputs: PropertyAliases|null null, // inputs: PropertyAliases|null
@ -854,6 +853,7 @@ export function createTNode(
providerIndexes: 0, providerIndexes: 0,
tagName: tagName, tagName: tagName,
attrs: attrs, attrs: attrs,
mergedAttrs: null,
localNames: null, localNames: null,
initialInputs: undefined, initialInputs: undefined,
inputs: null, inputs: null,
@ -1115,64 +1115,69 @@ export function resolveDirectives(
// tsickle. // tsickle.
ngDevMode && assertFirstCreatePass(tView); ngDevMode && assertFirstCreatePass(tView);
if (!getBindingsEnabled()) return false;
const directives: DirectiveDef<any>[]|null = findDirectiveMatches(tView, lView, tNode);
const exportsMap: ({[key: string]: number} | null) = localRefs === null ? null : {'': -1};
let hasDirectives = false; let hasDirectives = false;
if (getBindingsEnabled()) {
const directiveDefs: DirectiveDef<any>[]|null = findDirectiveDefMatches(tView, lView, tNode);
const exportsMap: ({[key: string]: number} | null) = localRefs === null ? null : {'': -1};
if (directives !== null) { if (directiveDefs !== null) {
let totalDirectiveHostVars = 0; let totalDirectiveHostVars = 0;
hasDirectives = true; hasDirectives = true;
initNodeFlags(tNode, tView.data.length, directives.length); initTNodeFlags(tNode, tView.data.length, directiveDefs.length);
// When the same token is provided by several directives on the same node, some rules apply in // When the same token is provided by several directives on the same node, some rules apply in
// the viewEngine: // the viewEngine:
// - viewProviders have priority over providers // - viewProviders have priority over providers
// - the last directive in NgModule.declarations has priority over the previous one // - the last directive in NgModule.declarations has priority over the previous one
// So to match these rules, the order in which providers are added in the arrays is very // So to match these rules, the order in which providers are added in the arrays is very
// important. // important.
for (let i = 0; i < directives.length; i++) { for (let i = 0; i < directiveDefs.length; i++) {
const def = directives[i]; const def = directiveDefs[i];
if (def.providersResolver) def.providersResolver(def); if (def.providersResolver) def.providersResolver(def);
} }
generateExpandoInstructionBlock(tView, tNode, directives.length); generateExpandoInstructionBlock(tView, tNode, directiveDefs.length);
let preOrderHooksFound = false; let preOrderHooksFound = false;
let preOrderCheckHooksFound = false; let preOrderCheckHooksFound = false;
for (let i = 0; i < directives.length; i++) { for (let i = 0; i < directiveDefs.length; i++) {
const def = directives[i]; const def = directiveDefs[i];
// Merge the attrs in the order of matches. This assumes that the first directive is the
// component itself, so that the component has the least priority.
tNode.mergedAttrs = mergeHostAttrs(tNode.mergedAttrs, def.hostAttrs);
baseResolveDirective(tView, lView, def); baseResolveDirective(tView, lView, def);
saveNameToExportMap(tView.data !.length - 1, def, exportsMap); saveNameToExportMap(tView.data !.length - 1, def, exportsMap);
if (def.contentQueries !== null) tNode.flags |= TNodeFlags.hasContentQuery; if (def.contentQueries !== null) tNode.flags |= TNodeFlags.hasContentQuery;
if (def.hostBindings !== null || def.hostAttrs !== null || def.hostVars !== 0) if (def.hostBindings !== null || def.hostAttrs !== null || def.hostVars !== 0)
tNode.flags |= TNodeFlags.hasHostBindings; tNode.flags |= TNodeFlags.hasHostBindings;
// Only push a node index into the preOrderHooks array if this is the first // Only push a node index into the preOrderHooks array if this is the first
// pre-order hook found on this node. // pre-order hook found on this node.
if (!preOrderHooksFound && (def.onChanges || def.onInit || def.doCheck)) { if (!preOrderHooksFound && (def.onChanges || def.onInit || def.doCheck)) {
// We will push the actual hook function into this array later during dir instantiation. // We will push the actual hook function into this array later during dir instantiation.
// We cannot do it now because we must ensure hooks are registered in the same // We cannot do it now because we must ensure hooks are registered in the same
// order that directives are created (i.e. injection order). // order that directives are created (i.e. injection order).
(tView.preOrderHooks || (tView.preOrderHooks = [])).push(tNode.index - HEADER_OFFSET); (tView.preOrderHooks || (tView.preOrderHooks = [])).push(tNode.index - HEADER_OFFSET);
preOrderHooksFound = true; preOrderHooksFound = true;
}
if (!preOrderCheckHooksFound && (def.onChanges || def.doCheck)) {
(tView.preOrderCheckHooks || (tView.preOrderCheckHooks = [
])).push(tNode.index - HEADER_OFFSET);
preOrderCheckHooksFound = true;
}
addHostBindingsToExpandoInstructions(tView, def);
totalDirectiveHostVars += def.hostVars;
} }
if (!preOrderCheckHooksFound && (def.onChanges || def.doCheck)) { initializeInputAndOutputAliases(tView, tNode);
(tView.preOrderCheckHooks || (tView.preOrderCheckHooks = [ growHostVarsSpace(tView, lView, totalDirectiveHostVars);
])).push(tNode.index - HEADER_OFFSET);
preOrderCheckHooksFound = true;
}
addHostBindingsToExpandoInstructions(tView, def);
totalDirectiveHostVars += def.hostVars;
} }
if (exportsMap) cacheMatchingLocalNames(tNode, localRefs, exportsMap);
initializeInputAndOutputAliases(tView, tNode);
growHostVarsSpace(tView, lView, totalDirectiveHostVars);
} }
if (exportsMap) cacheMatchingLocalNames(tNode, localRefs, exportsMap); // Merge the template attrs last so that they have the highest priority.
tNode.mergedAttrs = mergeHostAttrs(tNode.mergedAttrs, tNode.attrs);
return hasDirectives; return hasDirectives;
} }
@ -1187,9 +1192,7 @@ export function addHostBindingsToExpandoInstructions(
ngDevMode && assertFirstCreatePass(tView); ngDevMode && assertFirstCreatePass(tView);
const expando = tView.expandoInstructions !; const expando = tView.expandoInstructions !;
// TODO(misko): PERF we are adding `hostBindings` even if there is nothing to add! This is // TODO(misko): PERF we are adding `hostBindings` even if there is nothing to add! This is
// suboptimal for performance. See `currentDirectiveIndex` comment in // suboptimal for performance. `def.hostBindings` may be null,
// `setHostBindingsByExecutingExpandoInstructions` for details.
// TODO(misko): PERF `def.hostBindings` may be null,
// but we still need to push null to the array as a placeholder // but we still need to push null to the array as a placeholder
// to ensure the directive counter is incremented (so host // to ensure the directive counter is incremented (so host
// binding functions always line up with the corrective directive). // binding functions always line up with the corrective directive).
@ -1277,7 +1280,7 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
// It is important that this be called first before the actual instructions // It is important that this be called first before the actual instructions
// are run because this way the first directive ID value is not zero. // are run because this way the first directive ID value is not zero.
incrementActiveDirectiveId(); incrementActiveDirectiveId();
invokeHostBindingsInCreationMode(def, expando, directive, tNode, firstCreatePass); invokeHostBindingsInCreationMode(def, directive, tNode);
} else if (firstCreatePass) { } else if (firstCreatePass) {
expando.push(null); expando.push(null);
} }
@ -1287,22 +1290,13 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
} }
} }
// TODO(COMMIT): jsdoc
export function invokeHostBindingsInCreationMode( export function invokeHostBindingsInCreationMode(
def: DirectiveDef<any>, expando: ExpandoInstructions, directive: any, tNode: TNode, def: DirectiveDef<any>, directive: any, tNode: TNode) {
firstCreatePass: boolean) {
const previousExpandoLength = expando.length;
setCurrentDirectiveDef(def);
const elementIndex = tNode.index - HEADER_OFFSET;
// TODO(misko-next): This is a temporary work around for the fact that we moved the information
// from instruction to declaration. The workaround is to just call the instruction as if it was
// part of the `hostAttrs`.
if (def.hostAttrs !== null) {
ɵɵelementHostAttrs(def.hostAttrs);
}
if (def.hostBindings !== null) { if (def.hostBindings !== null) {
const elementIndex = tNode.index - HEADER_OFFSET;
def.hostBindings !(RenderFlags.Create, directive, elementIndex); def.hostBindings !(RenderFlags.Create, directive, elementIndex);
} }
setCurrentDirectiveDef(null);
} }
/** /**
@ -1331,7 +1325,7 @@ export function generateExpandoInstructionBlock(
* Matches the current node against all available selectors. * Matches the current node against all available selectors.
* If a component is matched (at most one), it is returned in first position in the array. * If a component is matched (at most one), it is returned in first position in the array.
*/ */
function findDirectiveMatches( function findDirectiveDefMatches(
tView: TView, viewData: LView, tView: TView, viewData: LView,
tNode: TElementNode | TContainerNode | TElementContainerNode): DirectiveDef<any>[]|null { tNode: TElementNode | TContainerNode | TElementContainerNode): DirectiveDef<any>[]|null {
ngDevMode && assertFirstCreatePass(tView); ngDevMode && assertFirstCreatePass(tView);
@ -1413,7 +1407,7 @@ function saveNameToExportMap(
* the directive count to 0, and adding the isComponent flag. * the directive count to 0, and adding the isComponent flag.
* @param index the initial index * @param index the initial index
*/ */
export function initNodeFlags(tNode: TNode, index: number, numberOfDirectives: number) { export function initTNodeFlags(tNode: TNode, index: number, numberOfDirectives: number) {
ngDevMode && assertNotEqual( ngDevMode && assertNotEqual(
numberOfDirectives, tNode.directiveEnd - tNode.directiveStart, numberOfDirectives, tNode.directiveEnd - tNode.directiveStart,
'Reached the max number of directives'); 'Reached the max number of directives');

View File

@ -434,6 +434,19 @@ export interface TNode {
*/ */
attrs: TAttributes|null; attrs: TAttributes|null;
/**
* Same as `TNode.attrs` but contains merged data across all directive host bindings.
*
* We need to keep `attrs` as unmerged so that it can be used for attribute selectors.
* We merge attrs here so that it can be used in a performant way for initial rendering.
*
* The `attrs` are merged in first pass in following order:
* - Component's `hostAttrs`
* - Directives' `hostAttrs`
* - Template `TNode.attrs` associated with the current `TNode`.
*/
mergedAttrs: TAttributes|null;
/** /**
* A set of local names under which a given element is exported in a template and * A set of local names under which a given element is exported in a template and
* visible to queries. An entry in this array can be created for different reasons: * visible to queries. An entry in this array can be created for different reasons:

View File

@ -8,32 +8,52 @@
import '../util/ng_dev_mode'; import '../util/ng_dev_mode';
import {assertDefined, assertNotEqual} from '../util/assert'; import {assertDefined, assertEqual, assertNotEqual} from '../util/assert';
import {AttributeMarker, TAttributes, TNode, TNodeType, unusedValueExportToPlacateAjd as unused1} from './interfaces/node'; import {AttributeMarker, TAttributes, TNode, TNodeType, unusedValueExportToPlacateAjd as unused1} from './interfaces/node';
import {CssSelector, CssSelectorList, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection'; import {CssSelector, CssSelectorList, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection';
import {classIndexOf} from './styling/class_differ';
import {isNameOnlyAttributeMarker} from './util/attrs_utils'; import {isNameOnlyAttributeMarker} from './util/attrs_utils';
import {getInitialStylingValue} from './util/styling_utils';
const unusedValueToPlacateAjd = unused1 + unused2; const unusedValueToPlacateAjd = unused1 + unused2;
const NG_TEMPLATE_SELECTOR = 'ng-template'; const NG_TEMPLATE_SELECTOR = 'ng-template';
function isCssClassMatching(nodeClassAttrVal: string, cssClassToMatch: string): boolean { /**
const nodeClassesLen = nodeClassAttrVal.length; * Search the `TAttributes` to see if it contains `cssClassToMatch` (case insensitive)
// we lowercase the class attribute value to be able to match *
// selectors without case-sensitivity * @param attrs `TAttributes` to search through.
// (selectors are already in lowercase when generated) * @param cssClassToMatch class to match (lowercase)
const matchIndex = nodeClassAttrVal.toLowerCase().indexOf(cssClassToMatch); * @param isProjectionMode Whether or not class matching should look into the attribute `class` in
const matchEndIdx = matchIndex + cssClassToMatch.length; * addition to the `AttributeMarker.Classes`.
if (matchIndex === -1 // no match */
|| (matchIndex > 0 && nodeClassAttrVal ![matchIndex - 1] !== ' ') // no space before function isCssClassMatching(
|| attrs: TAttributes, cssClassToMatch: string, isProjectionMode: boolean): boolean {
(matchEndIdx < nodeClassesLen && nodeClassAttrVal ![matchEndIdx] !== ' ')) // no space after // TODO(misko): The fact that this function needs to know about `isProjectionMode` seems suspect.
{ // It is strange to me that sometimes the class information comes in form of `class` attribute
return false; // and sometimes in form of `AttributeMarker.Classes`. Some investigation is needed to determine
// if that is the right behavior.
ngDevMode &&
assertEqual(
cssClassToMatch, cssClassToMatch.toLowerCase(), 'Class name expected to be lowercase.');
let i = 0;
while (i < attrs.length) {
let item = attrs[i++];
if (isProjectionMode && item === 'class') {
item = attrs[i] as string;
if (classIndexOf(item.toLowerCase(), cssClassToMatch, 0) !== -1) {
return true;
}
} else if (item === AttributeMarker.Classes) {
// We found the classes section. Start searching for the class.
while (i < attrs.length && typeof(item = attrs[i++]) == 'string') {
// while we have strings
if (item.toLowerCase() === cssClassToMatch) return true;
}
return false;
}
} }
return true; return false;
} }
/** /**
@ -106,9 +126,8 @@ export function isNodeMatchingSelector(
// special case for matching against classes when a tNode has been instantiated with // special case for matching against classes when a tNode has been instantiated with
// class and style values as separate attribute values (e.g. ['title', CLASS, 'foo']) // class and style values as separate attribute values (e.g. ['title', CLASS, 'foo'])
if ((mode & SelectorFlags.CLASS) && tNode.classes) { if ((mode & SelectorFlags.CLASS) && tNode.attrs !== null) {
if (!isCssClassMatching( if (!isCssClassMatching(tNode.attrs, selectorAttrValue as string, isProjectionMode)) {
getInitialStylingValue(tNode.classes), selectorAttrValue as string)) {
if (isPositive(mode)) return false; if (isPositive(mode)) return false;
skipToNextSelector = true; skipToNextSelector = true;
} }
@ -143,7 +162,7 @@ export function isNodeMatchingSelector(
const compareAgainstClassName = mode & SelectorFlags.CLASS ? nodeAttrValue : null; const compareAgainstClassName = mode & SelectorFlags.CLASS ? nodeAttrValue : null;
if (compareAgainstClassName && if (compareAgainstClassName &&
!isCssClassMatching(compareAgainstClassName, selectorAttrValue as string) || classIndexOf(compareAgainstClassName, selectorAttrValue as string, 0) !== -1 ||
mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) { mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) {
if (isPositive(mode)) return false; if (isPositive(mode)) return false;
skipToNextSelector = true; skipToNextSelector = true;

View File

@ -194,14 +194,6 @@ export function decreaseElementDepthCount() {
instructionState.lFrame.elementDepthCount--; instructionState.lFrame.elementDepthCount--;
} }
export function getCurrentDirectiveDef(): DirectiveDef<any>|ComponentDef<any>|null {
return instructionState.lFrame.currentDirectiveDef;
}
export function setCurrentDirectiveDef(def: DirectiveDef<any>| ComponentDef<any>| null): void {
instructionState.lFrame.currentDirectiveDef = def;
}
export function getBindingsEnabled(): boolean { export function getBindingsEnabled(): boolean {
return instructionState.bindingsEnabled; return instructionState.bindingsEnabled;
} }

View File

@ -113,7 +113,7 @@ export function removeClass(className: string, classToRemove: string): string {
* *
* @param className A string containing classes (whitespace separated) * @param className A string containing classes (whitespace separated)
* @param classToToggle A class name to remove or add to the `className` * @param classToToggle A class name to remove or add to the `className`
* @param toggle Weather the resulting `className` should contain or not the `classToToggle` * @param toggle Whether the resulting `className` should contain or not the `classToToggle`
* @returns a new class-list which does not have `classToRemove` * @returns a new class-list which does not have `classToRemove`
*/ */
export function toggleClass(className: string, classToToggle: string, toggle: boolean): string { export function toggleClass(className: string, classToToggle: string, toggle: boolean): string {

View File

@ -5,7 +5,6 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {assertEqual} from '../../util/assert';
import {CharCode} from '../../util/char_code'; import {CharCode} from '../../util/char_code';
import {AttributeMarker, TAttributes} from '../interfaces/node'; import {AttributeMarker, TAttributes} from '../interfaces/node';
import {CssSelector} from '../interfaces/projection'; import {CssSelector} from '../interfaces/projection';

View File

@ -830,6 +830,30 @@ describe('host bindings', () => {
expect(hostElement.title).toBe('other title'); expect(hostElement.title).toBe('other title');
}); });
it('should merge attributes on host and template', () => {
@Directive({selector: '[dir1]', host: {id: 'dir1'}})
class MyDir1 {
}
@Directive({selector: '[dir2]', host: {id: 'dir2'}})
class MyDir2 {
}
@Component({template: `<div dir1 dir2 id="tmpl"></div>`})
class MyComp {
}
TestBed.configureTestingModule({declarations: [MyComp, MyDir1, MyDir2]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const div: HTMLElement = fixture.debugElement.nativeElement.firstChild;
expect(div.id).toEqual(
ivyEnabled ?
// In ivy the correct result is `tmpl` because template has the highest priority.
'tmpl' :
// In VE the order was simply that of execution and so dir2 would win.
'dir2');
});
onlyInIvy('Host bindings do not get merged in ViewEngine') onlyInIvy('Host bindings do not get merged in ViewEngine')
.it('should work correctly with inherited directives with hostBindings', () => { .it('should work correctly with inherited directives with hostBindings', () => {
@Directive({selector: '[superDir]', host: {'[id]': 'id'}}) @Directive({selector: '[superDir]', host: {'[id]': 'id'}})

View File

@ -191,6 +191,9 @@
{ {
"name": "callHooks" "name": "callHooks"
}, },
{
"name": "classIndexOf"
},
{ {
"name": "concatString" "name": "concatString"
}, },
@ -279,7 +282,7 @@
"name": "findAttrIndexInNode" "name": "findAttrIndexInNode"
}, },
{ {
"name": "findDirectiveMatches" "name": "findDirectiveDefMatches"
}, },
{ {
"name": "forceStylesAsString" "name": "forceStylesAsString"
@ -407,9 +410,6 @@
{ {
"name": "getStylingMapArray" "name": "getStylingMapArray"
}, },
{
"name": "getTNode"
},
{ {
"name": "growHostVarsSpace" "name": "growHostVarsSpace"
}, },
@ -444,7 +444,7 @@
"name": "incrementInitPhaseFlags" "name": "incrementInitPhaseFlags"
}, },
{ {
"name": "initNodeFlags" "name": "initTNodeFlags"
}, },
{ {
"name": "initializeInputAndOutputAliases" "name": "initializeInputAndOutputAliases"
@ -533,6 +533,12 @@
{ {
"name": "matchTemplateAttribute" "name": "matchTemplateAttribute"
}, },
{
"name": "mergeHostAttribute"
},
{
"name": "mergeHostAttrs"
},
{ {
"name": "nativeAppendChild" "name": "nativeAppendChild"
}, },
@ -638,9 +644,6 @@
{ {
"name": "setClassName" "name": "setClassName"
}, },
{
"name": "setCurrentDirectiveDef"
},
{ {
"name": "setCurrentQueryIndex" "name": "setCurrentQueryIndex"
}, },
@ -719,9 +722,6 @@
{ {
"name": "ɵɵelementEnd" "name": "ɵɵelementEnd"
}, },
{
"name": "ɵɵelementHostAttrs"
},
{ {
"name": "ɵɵelementStart" "name": "ɵɵelementStart"
}, },

View File

@ -347,9 +347,6 @@
{ {
"name": "getStylingMapArray" "name": "getStylingMapArray"
}, },
{
"name": "getTNode"
},
{ {
"name": "growHostVarsSpace" "name": "growHostVarsSpace"
}, },
@ -372,7 +369,7 @@
"name": "incrementInitPhaseFlags" "name": "incrementInitPhaseFlags"
}, },
{ {
"name": "initNodeFlags" "name": "initTNodeFlags"
}, },
{ {
"name": "insertBloom" "name": "insertBloom"
@ -518,9 +515,6 @@
{ {
"name": "setClassName" "name": "setClassName"
}, },
{
"name": "setCurrentDirectiveDef"
},
{ {
"name": "setCurrentQueryIndex" "name": "setCurrentQueryIndex"
}, },
@ -572,9 +566,6 @@
{ {
"name": "ɵɵdefineComponent" "name": "ɵɵdefineComponent"
}, },
{
"name": "ɵɵelementHostAttrs"
},
{ {
"name": "ɵɵtext" "name": "ɵɵtext"
} }

View File

@ -437,6 +437,9 @@
{ {
"name": "checkNoChangesInternal" "name": "checkNoChangesInternal"
}, },
{
"name": "classIndexOf"
},
{ {
"name": "cleanUpView" "name": "cleanUpView"
}, },
@ -579,7 +582,7 @@
"name": "findAttrIndexInNode" "name": "findAttrIndexInNode"
}, },
{ {
"name": "findDirectiveMatches" "name": "findDirectiveDefMatches"
}, },
{ {
"name": "findExistingListener" "name": "findExistingListener"
@ -867,7 +870,7 @@
"name": "incrementInitPhaseFlags" "name": "incrementInitPhaseFlags"
}, },
{ {
"name": "initNodeFlags" "name": "initTNodeFlags"
}, },
{ {
"name": "initializeInputAndOutputAliases" "name": "initializeInputAndOutputAliases"
@ -1040,6 +1043,12 @@
{ {
"name": "matchTemplateAttribute" "name": "matchTemplateAttribute"
}, },
{
"name": "mergeHostAttribute"
},
{
"name": "mergeHostAttrs"
},
{ {
"name": "nativeAppendChild" "name": "nativeAppendChild"
}, },
@ -1217,9 +1226,6 @@
{ {
"name": "setClassName" "name": "setClassName"
}, },
{
"name": "setCurrentDirectiveDef"
},
{ {
"name": "setCurrentQueryIndex" "name": "setCurrentQueryIndex"
}, },
@ -1382,9 +1388,6 @@
{ {
"name": "ɵɵelementEnd" "name": "ɵɵelementEnd"
}, },
{
"name": "ɵɵelementHostAttrs"
},
{ {
"name": "ɵɵelementStart" "name": "ɵɵelementStart"
}, },

View File

@ -22,7 +22,7 @@ describe('css selector matching', () => {
const tNode = (!attrsOrTNode || Array.isArray(attrsOrTNode)) ? const tNode = (!attrsOrTNode || Array.isArray(attrsOrTNode)) ?
createTNode(null !, null, TNodeType.Element, 0, tagName, attrsOrTNode as TAttributes) : createTNode(null !, null, TNodeType.Element, 0, tagName, attrsOrTNode as TAttributes) :
(attrsOrTNode as TNode); (attrsOrTNode as TNode);
return isNodeMatchingSelector(tNode, selector, false); return isNodeMatchingSelector(tNode, selector, true);
} }
describe('isNodeMatchingSimpleSelector', () => { describe('isNodeMatchingSimpleSelector', () => {
@ -322,26 +322,6 @@ describe('css selector matching', () => {
// <div class="foo"> // <div class="foo">
expect(isMatching('div', ['class', 'foo'], selector)).toBeFalsy(); expect(isMatching('div', ['class', 'foo'], selector)).toBeFalsy();
}); });
it('should match against a class value before and after the styling context is created',
() => {
// selector: 'div.abc'
const selector = ['div', SelectorFlags.CLASS, 'abc'];
const tNode = createTNode(null !, null, TNodeType.Element, 0, 'div', []);
// <div> (without attrs or styling context)
expect(isMatching('div', tNode, selector)).toBeFalsy();
// <div class="abc"> (with attrs but without styling context)
tNode.attrs = ['class', 'abc'];
tNode.classes = null;
expect(isMatching('div', tNode, selector)).toBeTruthy();
// <div class="abc"> (with styling context but without attrs)
tNode.classes = ['abc', 'abc', true];
tNode.attrs = null;
expect(isMatching('div', tNode, selector)).toBeTruthy();
});
}); });
}); });