Skip to content

Commit

Permalink
refactor(core): expose host directives to their host through DI (#47476)
Browse files Browse the repository at this point in the history
Exposes the host directives to the host and its descendants through DI. This can be useful, because it allows the host to further configure the host directives.

PR Close #47476
  • Loading branch information
crisbeto authored and alxhub committed Sep 23, 2022
1 parent 7a5ba93 commit 3fe21a6
Show file tree
Hide file tree
Showing 5 changed files with 466 additions and 101 deletions.
13 changes: 6 additions & 7 deletions packages/core/src/render3/context_discovery.ts
Expand Up @@ -63,7 +63,7 @@ export function getLContext(target: any): LContext|null {
if (nodeIndex == -1) {
throw new Error('The provided directive was not found in the application');
}
directives = getDirectivesAtNodeIndex(nodeIndex, lView, false);
directives = getDirectivesAtNodeIndex(nodeIndex, lView);
} else {
nodeIndex = findViaNativeElement(lView, target as RElement);
if (nodeIndex == -1) {
Expand Down Expand Up @@ -294,21 +294,20 @@ function findViaDirective(lView: LView, directiveInstance: {}): number {
}

/**
* Returns a list of directives extracted from the given view based on the
* provided list of directive index values.
* Returns a list of directives applied to a node at a specific index. The list includes
* directives matched by selector and any host directives, but it excludes components.
* Use `getComponentAtNodeIndex` to find the component applied to a node.
*
* @param nodeIndex The node index
* @param lView The target view data
* @param includeComponents Whether or not to include components in returned directives
*/
export function getDirectivesAtNodeIndex(
nodeIndex: number, lView: LView, includeComponents: boolean): any[]|null {
export function getDirectivesAtNodeIndex(nodeIndex: number, lView: LView): any[]|null {
const tNode = lView[TVIEW].data[nodeIndex] as TNode;
if (tNode.directiveStart === 0) return EMPTY_ARRAY;
const results: any[] = [];
for (let i = tNode.directiveStart; i < tNode.directiveEnd; i++) {
const directiveInstance = lView[i];
if (!isComponentInstance(directiveInstance) || includeComponents) {
if (!isComponentInstance(directiveInstance)) {
results.push(directiveInstance);
}
}
Expand Down
Expand Up @@ -68,11 +68,9 @@ function findHostDirectiveDefs(

// Host directives execute before the host so that its host bindings can be overwritten.
findHostDirectiveDefs(matches, hostDirectiveDef, tView, lView, tNode);
matches.push(hostDirectiveDef);
}
}

// Push the def itself at the end since it needs to execute after the host directives.
matches.push(def);
}

/**
Expand Down
79 changes: 41 additions & 38 deletions packages/core/src/render3/instructions/shared.ts
Expand Up @@ -1065,13 +1065,17 @@ export function resolveDirectives(

let hasDirectives = false;
if (getBindingsEnabled()) {
const directiveDefsMatchedBySelectors = findDirectiveDefMatches(tView, lView, tNode);
const directiveDefs = directiveDefsMatchedBySelectors ?
findHostDirectiveDefs(directiveDefsMatchedBySelectors, tView, lView, tNode) :
null;
const directiveDefs = findDirectiveDefMatches(tView, lView, tNode);
const exportsMap: ({[key: string]: number}|null) = localRefs === null ? null : {'': -1};

if (directiveDefs !== null) {
// Publishes the directive types to DI so they can be injected. Needs to
// happen in a separate pass before the TNode flags have been initialized.
for (let i = 0; i < directiveDefs.length; i++) {
diPublicInInjector(
getOrCreateNodeInjectorForNode(tNode, lView), tView, directiveDefs[i].type);
}

hasDirectives = true;
initTNodeFlags(tNode, tView.data.length, directiveDefs.length);
// When the same token is provided by several directives on the same node, some rules apply in
Expand Down Expand Up @@ -1261,19 +1265,18 @@ export function invokeHostBindingsInCreationMode(def: DirectiveDef<any>, directi
* If a component is matched (at most one), it is returned in first position in the array.
*/
function findDirectiveDefMatches(
tView: TView, viewData: LView,
tNode: TElementNode|TContainerNode|TElementContainerNode): DirectiveDef<any>[]|null {
tView: TView, lView: LView,
tNode: TElementNode|TContainerNode|TElementContainerNode): DirectiveDef<unknown>[]|null {
ngDevMode && assertFirstCreatePass(tView);
ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode | TNodeType.AnyContainer);

const registry = tView.directiveRegistry;
let matches: any[]|null = null;
let matches: DirectiveDef<unknown>[]|null = null;
if (registry) {
for (let i = 0; i < registry.length; i++) {
const def = registry[i] as ComponentDef<any>| DirectiveDef<any>;
if (isNodeMatchingSelectorList(tNode, def.selectors!, /* isProjectionMode */ false)) {
matches || (matches = ngDevMode ? new MatchesArray() : []);
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, viewData), tView, def.type);

if (isComponentDef(def)) {
if (ngDevMode) {
Expand All @@ -1283,15 +1286,39 @@ function findDirectiveDefMatches(
`Please use a different tag to activate the ${stringify(def.type)} component.`);

if (isComponentHost(tNode)) {
// If another component has been matched previously, it's the first element in the
// `matches` array, see how we store components/directives in `matches` below.
throwMultipleComponentError(tNode, matches[0].type, def.type);
throwMultipleComponentError(tNode, matches.find(isComponentDef)!.type, def.type);
}
}
markAsComponentHost(tView, tNode, 0);
// The component is always stored first with directives after.
matches.unshift(def);

// Components are inserted at the front of the matches array so that their lifecycle
// hooks run before any directive lifecycle hooks. This appears to be for ViewEngine
// compatibility. This logic doesn't make sense with host directives, because it
// would allow the host directives to undo any overrides the host may have made.
// To handle this case, the host directives of components are inserted at the beginning
// of the array, followed by the component. As such, the insertion order is as follows:
// 1. Host directives belonging to the selector-matched component.
// 2. Selector-matched component.
// 3. Host directives belonging to selector-matched directives.
// 4. Selector-matched directives.
if (def.findHostDirectiveDefs !== null) {
const hostDirectiveMatches: DirectiveDef<unknown>[] = [];
def.findHostDirectiveDefs(hostDirectiveMatches, def, tView, lView, tNode);
// Add all host directives declared on this component, followed by the component itself.
// Host directives should execute first so the host has a chance to override changes
// to the DOM made by them.
matches.unshift(...hostDirectiveMatches, def);
// Component is offset starting from the beginning of the host directives array.
const componentOffset = hostDirectiveMatches.length;
markAsComponentHost(tView, tNode, componentOffset);
} else {
// No host directives on this component, just add the
// component def to the beginning of the matches.
matches.unshift(def);
markAsComponentHost(tView, tNode, 0);
}
} else {
// Append any host directives to the matches first.
def.findHostDirectiveDefs?.(matches, def, tView, lView, tNode);
matches.push(def);
}
}
Expand All @@ -1313,30 +1340,6 @@ export function markAsComponentHost(tView: TView, hostTNode: TNode, componentOff
.push(hostTNode.index);
}

/**
* Given an array of directives that were matched by their selectors, this function
* produces a new array that also includes any host directives that have to be applied.
* @param selectorMatches Directives matched in a template based on their selectors.
* @param tView Current TView.
* @param lView Current LView.
* @param tNode Current TNode that is being matched.
*/
function findHostDirectiveDefs(
selectorMatches: DirectiveDef<unknown>[], tView: TView, lView: LView,
tNode: TElementNode|TContainerNode|TElementContainerNode): DirectiveDef<unknown>[] {
const matches: DirectiveDef<unknown>[] = [];

for (const def of selectorMatches) {
if (def.findHostDirectiveDefs === null) {
matches.push(def);
} else {
def.findHostDirectiveDefs(matches, def, tView, lView, tNode);
}
}

return matches;
}

/** Caches local names and their matching directive indices for query and template lookups. */
function cacheMatchingLocalNames(
tNode: TNode, localRefs: string[]|null, exportsMap: {[key: string]: number}): void {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/render3/util/discovery_utils.ts
Expand Up @@ -221,7 +221,7 @@ export function getDirectives(node: Node): {}[] {
return [];
}
if (context.directives === undefined) {
context.directives = getDirectivesAtNodeIndex(nodeIndex, lView, false);
context.directives = getDirectivesAtNodeIndex(nodeIndex, lView);
}

// The `directives` in this case are a named array called `LComponentView`. Clone the
Expand Down

0 comments on commit 3fe21a6

Please sign in to comment.