Skip to content

Commit

Permalink
refactor(core): invoke basic host directives (angular#47430)
Browse files Browse the repository at this point in the history
Expands the runtime to allow for basic host directives to be invoked within a template. This is achieved by making a second pass over the directives that were matched based on their selectors and producing a new array of directives that include host directives. Note that the ordering in the array is important, because it determines which host bindings and DI tokens will be overwritten.

PR Close angular#47430
  • Loading branch information
crisbeto authored and pkozlowski-opensource committed Sep 19, 2022
1 parent 8654e5c commit adb1a61
Show file tree
Hide file tree
Showing 7 changed files with 739 additions and 29 deletions.
2 changes: 1 addition & 1 deletion goldens/size-tracking/integration-payloads.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"forms": {
"uncompressed": {
"runtime": 1060,
"main": 156626,
"main": 157136,
"polyfills": 33915
}
},
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/render3/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,8 @@ export function ɵɵdefineComponent<T>(componentDefinition: {
setInput: null,
schemas: componentDefinition.schemas || null,
tView: null,
applyHostDirectives: null,
findHostDirectiveDefs: null,
hostDirectives: null,
};
const dependencies = componentDefinition.dependencies;
const feature = componentDefinition.features;
Expand Down
56 changes: 37 additions & 19 deletions packages/core/src/render3/features/host_directives_feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
* 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 {resolveForwardRef} from '../../di';
import {Type} from '../../interface/type';
import {EMPTY_OBJ} from '../../util/empty';
import {getDirectiveDef} from '../definition';
import {DirectiveDef} from '../interfaces/definition';
import {TContainerNode, TElementContainerNode, TElementNode} from '../interfaces/node';
import {LView, TView} from '../interfaces/view';

/** Values that can be used to define a host directive through the `HostDirectivesFeature`. */
type HostDirectiveDefiniton = Type<unknown>|{
type HostDirectiveConfig = Type<unknown>|{
directive: Type<unknown>;
inputs?: string[];
outputs?: string[];
Expand All @@ -38,38 +40,54 @@ type HostDirectiveDefiniton = Type<unknown>|{
*
* @codeGenApi
*/
export function ɵɵHostDirectivesFeature(rawHostDirectives: HostDirectiveDefiniton[]|
(() => HostDirectiveDefiniton[])) {
const unwrappedHostDirectives =
Array.isArray(rawHostDirectives) ? rawHostDirectives : rawHostDirectives();
const hostDirectives = unwrappedHostDirectives.map(
dir => typeof dir === 'function' ? {directive: dir, inputs: EMPTY_OBJ, outputs: EMPTY_OBJ} : {
directive: dir.directive,
inputs: bindingArrayToMap(dir.inputs),
outputs: bindingArrayToMap(dir.outputs)
});

export function ɵɵHostDirectivesFeature(rawHostDirectives: HostDirectiveConfig[]|
(() => HostDirectiveConfig[])) {
return (definition: DirectiveDef<unknown>) => {
// TODO(crisbeto): implement host directive matching logic.
definition.applyHostDirectives =
(tView: TView, viewData: LView, tNode: TElementNode|TContainerNode|TElementContainerNode,
matches: any[]) => {};
definition.findHostDirectiveDefs = findHostDirectiveDefs;
definition.hostDirectives =
(Array.isArray(rawHostDirectives) ? rawHostDirectives : rawHostDirectives()).map(dir => {
return typeof dir === 'function' ?
{directive: resolveForwardRef(dir), inputs: EMPTY_OBJ, outputs: EMPTY_OBJ} :
{
directive: resolveForwardRef(dir.directive),
inputs: bindingArrayToMap(dir.inputs),
outputs: bindingArrayToMap(dir.outputs)
};
});
};
}

function findHostDirectiveDefs(
matches: DirectiveDef<unknown>[], def: DirectiveDef<unknown>, tView: TView, lView: LView,
tNode: TElementNode|TContainerNode|TElementContainerNode): void {
if (def.hostDirectives !== null) {
for (const hostDirectiveConfig of def.hostDirectives) {
const hostDirectiveDef = getDirectiveDef(hostDirectiveConfig.directive)!;

// TODO(crisbeto): assert that the def exists.

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

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

/**
* Converts an array in the form of `['publicName', 'alias', 'otherPublicName', 'otherAlias']` into
* a map in the form of `{publicName: 'alias', otherPublicName: 'otherAlias'}`.
*/
function bindingArrayToMap(bindings: string[]|undefined) {
if (!bindings || bindings.length === 0) {
if (bindings === undefined || bindings.length === 0) {
return EMPTY_OBJ;
}

const result: {[publicName: string]: string} = {};

for (let i = 1; i < bindings.length; i += 2) {
result[bindings[i - 1]] = bindings[i];
for (let i = 0; i < bindings.length; i += 2) {
result[bindings[i]] = bindings[i + 1];
}

return result;
Expand Down
30 changes: 27 additions & 3 deletions packages/core/src/render3/instructions/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,10 @@ export function resolveDirectives(

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

if (directiveDefs !== null) {
Expand Down Expand Up @@ -1288,8 +1291,6 @@ function findDirectiveDefMatches(
} else {
matches.push(def);
}

def.applyHostDirectives?.(tView, viewData, tNode, matches);
}
}
}
Expand All @@ -1308,6 +1309,29 @@ export function markAsComponentHost(tView: TView, hostTNode: TNode): void {
.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(
Expand Down
23 changes: 19 additions & 4 deletions packages/core/src/render3/interfaces/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,15 @@ export interface DirectiveDef<T> {
readonly features: DirectiveDefFeature[]|null;

/**
* Function that will apply the host directives to the list of matches during directive matching.
* Function that will add the host directives to the list of matches during directive matching.
* Patched onto the definition by the `HostDirectivesFeature`.
*/
applyHostDirectives:
((tView: TView, viewData: LView, tNode: TElementNode|TContainerNode|TElementContainerNode,
matches: any[]) => void)|null;
findHostDirectiveDefs:
((matches: DirectiveDef<unknown>[], def: DirectiveDef<unknown>, tView: TView, lView: LView,
tNode: TElementNode|TContainerNode|TElementContainerNode) => void)|null;

/** Additional directives to be applied whenever the directive has been matched. */
hostDirectives: HostDirectiveDef[]|null;

setInput:
(<U extends T>(
Expand Down Expand Up @@ -403,6 +406,18 @@ export interface DirectiveDefFeature {
ngInherit?: true;
}

/** Runtime information used to configure a host directive. */
export interface HostDirectiveDef<T = unknown> {
/** Class representing the host directive. */
directive: Type<T>;

/** Directive inputs that have been exposed. */
inputs: {[publicName: string]: string};

/** Directive outputs that have been exposed. */
outputs: {[publicName: string]: string};
}

export interface ComponentDefFeature {
<T>(componentDef: ComponentDef<T>): void;
/**
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/render3/jit/directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,13 @@ export function directiveMetadata(type: Type<any>, metadata: Directive): R3Direc
providers: metadata.providers || null,
viewQueries: extractQueriesMetadata(type, propMetadata, isViewQuery),
isStandalone: !!metadata.standalone,
hostDirectives: null,
hostDirectives:
// TODO(crisbeto): remove the `as any` usage here and down in the `map` call once
// host directives are exposed in the public API.
(metadata as any)
.hostDirectives?.map(
(directive: any) => typeof directive === 'function' ? {directive} : directive) ||
null
};
}

Expand Down
Loading

0 comments on commit adb1a61

Please sign in to comment.