Skip to content

Commit

Permalink
refactor(core): expose host directives to their host through DI (angu…
Browse files Browse the repository at this point in the history
…lar#47476)

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 angular#47476
  • Loading branch information
crisbeto authored and sonukapoor committed Sep 27, 2022
1 parent bfaf793 commit aa5ca02
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 aa5ca02

Please sign in to comment.