Skip to content

Commit

Permalink
refactor(core): add input and output filtering for host directives
Browse files Browse the repository at this point in the history
Adds the logic that will filter out unexposed inputs/outputs and apply the aliases that the author specified when writing the host directives.
  • Loading branch information
crisbeto committed Sep 24, 2022
1 parent 8637253 commit 31f98fc
Show file tree
Hide file tree
Showing 13 changed files with 975 additions and 28 deletions.
2 changes: 1 addition & 1 deletion goldens/size-tracking/integration-payloads.json
Expand Up @@ -33,7 +33,7 @@
"cli-hello-world-lazy": {
"uncompressed": {
"runtime": 2835,
"main": 226324,
"main": 226902,
"polyfills": 33842,
"src_app_lazy_lazy_routes_ts": 487
}
Expand Down
12 changes: 7 additions & 5 deletions packages/core/src/render3/features/host_directives_feature.ts
Expand Up @@ -9,7 +9,7 @@ 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 {DirectiveDef, HostDirectiveBindingMap, HostDirectiveDefinitionMap} from '../interfaces/definition';
import {TContainerNode, TElementContainerNode, TElementNode} from '../interfaces/node';
import {LView, TView} from '../interfaces/view';

Expand Down Expand Up @@ -58,7 +58,8 @@ export function ɵɵHostDirectivesFeature(rawHostDirectives: HostDirectiveConfig
}

function findHostDirectiveDefs(
matches: DirectiveDef<unknown>[], def: DirectiveDef<unknown>, tView: TView, lView: LView,
matches: DirectiveDef<unknown>[], definitionMap: HostDirectiveDefinitionMap,
def: DirectiveDef<unknown>, tView: TView, lView: LView,
tNode: TElementNode|TContainerNode|TElementContainerNode): void {
if (def.hostDirectives !== null) {
for (const hostDirectiveConfig of def.hostDirectives) {
Expand All @@ -67,7 +68,8 @@ function findHostDirectiveDefs(
// 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);
findHostDirectiveDefs(matches, definitionMap, hostDirectiveDef, tView, lView, tNode);
definitionMap.set(hostDirectiveDef, hostDirectiveConfig);
matches.push(hostDirectiveDef);
}
}
Expand All @@ -77,12 +79,12 @@ function findHostDirectiveDefs(
* 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) {
function bindingArrayToMap(bindings: string[]|undefined): HostDirectiveBindingMap {
if (bindings === undefined || bindings.length === 0) {
return EMPTY_OBJ;
}

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

for (let i = 0; i < bindings.length; i += 2) {
result[bindings[i]] = bindings[i + 1];
Expand Down
65 changes: 47 additions & 18 deletions packages/core/src/render3/instructions/shared.ts
Expand Up @@ -25,7 +25,7 @@ import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} f
import {throwMultipleComponentError} from '../errors';
import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} from '../hooks';
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, HostBindingsFunction, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, HostBindingsFunction, HostDirectiveDefinitionMap, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {NodeInjectorFactory} from '../interfaces/injector';
import {getUniqueLViewId} from '../interfaces/lview_tracking';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from '../interfaces/node';
Expand Down Expand Up @@ -863,28 +863,39 @@ export function createTNode(


function generatePropertyAliases(
inputAliasMap: {[publicName: string]: string}, directiveDefIdx: number,
propStore: PropertyAliases|null): PropertyAliases|null {
inputAliasMap: {[publicName: string]: string}, directiveIndex: number,
propStore: PropertyAliases|null,
aliasMap: {[internalName: string]: string}|null): PropertyAliases|null {
for (let publicName in inputAliasMap) {
if (inputAliasMap.hasOwnProperty(publicName)) {
propStore = propStore === null ? {} : propStore;
const internalName = inputAliasMap[publicName];

if (propStore.hasOwnProperty(publicName)) {
propStore[publicName].push(directiveDefIdx, internalName);
} else {
(propStore[publicName] = [directiveDefIdx, internalName]);
if (aliasMap === null) {
addToPropStore(propStore, directiveIndex, publicName, internalName);
} else if (aliasMap.hasOwnProperty(publicName)) {
addToPropStore(propStore, directiveIndex, aliasMap[publicName], internalName);
}
}
}
return propStore;
}

function addToPropStore(
propStore: PropertyAliases, directiveIndex: number, publicName: string, internalName: string) {
if (propStore.hasOwnProperty(publicName)) {
propStore[publicName].push(directiveIndex, internalName);
} else {
(propStore[publicName] = [directiveIndex, internalName]);
}
}

/**
* Initializes data structures required to work with directive inputs and outputs.
* Initialization is done for all directives matched on a given TNode.
*/
export function initializeInputAndOutputAliases(tView: TView, tNode: TNode): void {
export function initializeInputAndOutputAliases(
tView: TView, tNode: TNode, hostDirectiveDefinitionMap: HostDirectiveDefinitionMap|null): void {
ngDevMode && assertFirstCreatePass(tView);

const start = tNode.directiveStart;
Expand All @@ -898,8 +909,13 @@ export function initializeInputAndOutputAliases(tView: TView, tNode: TNode): voi

for (let i = start; i < end; i++) {
const directiveDef = tViewData[i] as DirectiveDef<any>;
inputsStore = generatePropertyAliases(directiveDef.inputs, i, inputsStore);
outputsStore = generatePropertyAliases(directiveDef.outputs, i, outputsStore);
const aliasData =
hostDirectiveDefinitionMap ? hostDirectiveDefinitionMap.get(directiveDef) : null;
const aliasedInputs = aliasData ? aliasData.inputs : null;
const aliasedOutputs = aliasData ? aliasData.outputs : null;

inputsStore = generatePropertyAliases(directiveDef.inputs, i, inputsStore, aliasedInputs);
outputsStore = generatePropertyAliases(directiveDef.outputs, i, outputsStore, aliasedOutputs);
// Do not use unbound attributes as inputs to structural directives, since structural
// directive inputs can only be set using microsyntax (e.g. `<div *dir="exp">`).
// TODO(FW-1930): microsyntax expressions may also contain unbound/static attributes, which
Expand Down Expand Up @@ -1040,7 +1056,7 @@ export function instantiateRootComponent<T>(tView: TView, lView: LView, def: Com
directiveIndex, rootTNode.directiveStart,
'Because this is a root component the allocated expando should match the TNode component.');
configureViewWithDirective(tView, rootTNode, lView, directiveIndex, def);
initializeInputAndOutputAliases(tView, rootTNode);
initializeInputAndOutputAliases(tView, rootTNode, null);
}
const directive = getNodeInjectable(
lView, tView, rootTNode.directiveStart + rootTNode.componentOffset,
Expand All @@ -1065,7 +1081,16 @@ export function resolveDirectives(

let hasDirectives = false;
if (getBindingsEnabled()) {
const directiveDefs = findDirectiveDefMatches(tView, lView, tNode);
const matchResult = findDirectiveDefMatches(tView, lView, tNode);
let directiveDefs: DirectiveDef<unknown>[]|null;
let definitionMap: HostDirectiveDefinitionMap|null;

if (matchResult === null) {
directiveDefs = definitionMap = null;
} else {
[directiveDefs, definitionMap] = matchResult;
}

const exportsMap: ({[key: string]: number}|null) = localRefs === null ? null : {'': -1};

if (directiveDefs !== null) {
Expand Down Expand Up @@ -1129,7 +1154,7 @@ export function resolveDirectives(
directiveIdx++;
}

initializeInputAndOutputAliases(tView, tNode);
initializeInputAndOutputAliases(tView, tNode, definitionMap);
}
if (exportsMap) cacheMatchingLocalNames(tNode, localRefs, exportsMap);
}
Expand Down Expand Up @@ -1265,13 +1290,14 @@ 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, lView: LView,
tNode: TElementNode|TContainerNode|TElementContainerNode): DirectiveDef<unknown>[]|null {
tView: TView, lView: LView, tNode: TElementNode|TContainerNode|TElementContainerNode):
[matches: DirectiveDef<unknown>[], definitionMap: HostDirectiveDefinitionMap|null]|null {
ngDevMode && assertFirstCreatePass(tView);
ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode | TNodeType.AnyContainer);

const registry = tView.directiveRegistry;
let matches: DirectiveDef<unknown>[]|null = null;
let definitionMap: HostDirectiveDefinitionMap|null = null;
if (registry) {
for (let i = 0; i < registry.length; i++) {
const def = registry[i] as ComponentDef<any>| DirectiveDef<any>;
Expand Down Expand Up @@ -1302,7 +1328,9 @@ function findDirectiveDefMatches(
// 4. Selector-matched directives.
if (def.findHostDirectiveDefs !== null) {
const hostDirectiveMatches: DirectiveDef<unknown>[] = [];
def.findHostDirectiveDefs(hostDirectiveMatches, def, tView, lView, tNode);
definitionMap = definitionMap || new Map();
def.findHostDirectiveDefs(
hostDirectiveMatches, definitionMap, 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.
Expand All @@ -1318,13 +1346,14 @@ function findDirectiveDefMatches(
}
} else {
// Append any host directives to the matches first.
def.findHostDirectiveDefs?.(matches, def, tView, lView, tNode);
definitionMap = definitionMap || new Map();
def.findHostDirectiveDefs?.(matches, definitionMap, def, tView, lView, tNode);
matches.push(def);
}
}
}
}
return matches;
return matches === null ? null : [matches, definitionMap];
}

/**
Expand Down
22 changes: 19 additions & 3 deletions packages/core/src/render3/interfaces/definition.ts
Expand Up @@ -211,7 +211,8 @@ export interface DirectiveDef<T> {
* Patched onto the definition by the `HostDirectivesFeature`.
*/
findHostDirectiveDefs:
((matches: DirectiveDef<unknown>[], def: DirectiveDef<unknown>, tView: TView, lView: LView,
((matches: DirectiveDef<unknown>[], definitionMap: HostDirectiveDefinitionMap,
def: DirectiveDef<unknown>, tView: TView, lView: LView,
tNode: TElementNode|TContainerNode|TElementContainerNode) => void)|null;

/** Additional directives to be applied whenever the directive has been matched. */
Expand Down Expand Up @@ -412,12 +413,27 @@ export interface HostDirectiveDef<T = unknown> {
directive: Type<T>;

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

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

/**
* Mapping between the public aliases of directive bindings and the underlying inputs/outputs that
* they represent. Also serves as an allowlist of the inputs/outputs from the host directive that
* the author has decided to expose.
*/
export type HostDirectiveBindingMap = {
[publicName: string]: string
};

/**
* Mapping between a directive that was used as a host directive
* and the configuration that was used to define it as such.
*/
export type HostDirectiveDefinitionMap = Map<DirectiveDef<unknown>, HostDirectiveDef>;

export interface ComponentDefFeature {
<T>(componentDef: ComponentDef<T>): void;
/**
Expand Down

0 comments on commit 31f98fc

Please sign in to comment.