Skip to content

Commit 9f44b41

Browse files
crisbetokirjs
authored andcommitted
refactor(core): separate host directive inputs from selector-matched ones (angular#60036)
Currently `TNode.inputs`/`TNode.outputs` store all of the available bindings on that node, no matter if they came from a directive that the user applied directly or from a host directive. This has a couple of drawbacks: 1. We need to store more information that necessary. For example, the only reason we have strings in the arrays is to facilitate host directive aliasing. 2. It doesn't allow us to distinguish which host directives belong to which selector-matched directives. These changes are a step towards resolving both issues by storing the host directive binding information in separate data structures. PR Close angular#60036
1 parent 0cac2a2 commit 9f44b41

File tree

19 files changed

+304
-199
lines changed

19 files changed

+304
-199
lines changed

packages/core/src/render3/component_ref.ts

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,7 @@ import {
4242
} from './instructions/shared';
4343
import {ComponentDef, DirectiveDef} from './interfaces/definition';
4444
import {InputFlags} from './interfaces/input_flags';
45-
import {
46-
NodeInputBindings,
47-
TContainerNode,
48-
TElementContainerNode,
49-
TElementNode,
50-
TNode,
51-
} from './interfaces/node';
45+
import {TContainerNode, TElementContainerNode, TElementNode, TNode} from './interfaces/node';
5246
import {RElement, RNode} from './interfaces/renderer_dom';
5347
import {
5448
CONTEXT,
@@ -386,31 +380,28 @@ export class ComponentRef<T> extends AbstractComponentRef<T> {
386380
}
387381

388382
override setInput(name: string, value: unknown): void {
389-
const inputData = this._tNode.inputs;
390-
let dataValue: NodeInputBindings[typeof name] | undefined;
391-
if (inputData !== null && (dataValue = inputData[name])) {
392-
this.previousInputValues ??= new Map();
393-
// Do not set the input if it is the same as the last value
394-
// This behavior matches `bindingUpdated` when binding inputs in templates.
395-
if (
396-
this.previousInputValues.has(name) &&
397-
Object.is(this.previousInputValues.get(name), value)
398-
) {
399-
return;
400-
}
383+
const tNode = this._tNode;
384+
this.previousInputValues ??= new Map();
385+
// Do not set the input if it is the same as the last value
386+
// This behavior matches `bindingUpdated` when binding inputs in templates.
387+
if (
388+
this.previousInputValues.has(name) &&
389+
Object.is(this.previousInputValues.get(name), value)
390+
) {
391+
return;
392+
}
401393

402-
const lView = this._rootLView;
403-
setInputsForProperty(lView[TVIEW], lView, dataValue, name, value);
404-
this.previousInputValues.set(name, value);
405-
const childComponentLView = getComponentLViewByIndex(this._tNode.index, lView);
406-
markViewDirty(childComponentLView, NotificationSource.SetInput);
407-
} else {
408-
if (ngDevMode) {
409-
const cmpNameForError = stringifyForError(this.componentType);
410-
let message = `Can't set value of the '${name}' input on the '${cmpNameForError}' component. `;
411-
message += `Make sure that the '${name}' property is annotated with @Input() or a mapped @Input('${name}') exists.`;
412-
reportUnknownPropertyError(message);
413-
}
394+
const lView = this._rootLView;
395+
const hasSetInput = setInputsForProperty(tNode, lView[TVIEW], lView, name, value);
396+
this.previousInputValues.set(name, value);
397+
const childComponentLView = getComponentLViewByIndex(tNode.index, lView);
398+
markViewDirty(childComponentLView, NotificationSource.SetInput);
399+
400+
if (ngDevMode && !hasSetInput) {
401+
const cmpNameForError = stringifyForError(this.componentType);
402+
let message = `Can't set value of the '${name}' input on the '${cmpNameForError}' component. `;
403+
message += `Make sure that the '${name}' property is annotated with @Input() or a mapped @Input('${name}') exists.`;
404+
reportUnknownPropertyError(message);
414405
}
415406
}
416407

packages/core/src/render3/instructions/listener.ts

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929

3030
import {markViewDirty} from './mark_view_dirty';
3131
import {handleError, loadComponentRenderer} from './shared';
32+
import {DirectiveDef} from '../interfaces/definition';
3233

3334
/**
3435
* Contains a reference to a function that disables event replay feature
@@ -159,7 +160,7 @@ export function listenerInternal(
159160
): void {
160161
const isTNodeDirectiveHost = isDirectiveHost(tNode);
161162
const firstCreatePass = tView.firstCreatePass;
162-
const tCleanup: false | any[] = firstCreatePass && getOrCreateTViewCleanup(tView);
163+
const tCleanup = firstCreatePass ? getOrCreateTViewCleanup(tView) : null;
163164
const context = lView[CONTEXT];
164165

165166
// When the ɵɵlistener instruction was generated and is executed we know that there is either a
@@ -230,34 +231,55 @@ export function listenerInternal(
230231
listenerFn = wrapListener(tNode, lView, context, listenerFn);
231232
}
232233

233-
// subscribe to directive outputs
234-
const outputs = tNode.outputs;
235-
let props: NodeOutputBindings[keyof NodeOutputBindings] | undefined;
236-
if (processOutputs && outputs !== null && (props = outputs[eventName])) {
237-
const propsLength = props.length;
238-
if (propsLength) {
239-
for (let i = 0; i < propsLength; i += 2) {
240-
const index = props[i] as number;
241-
ngDevMode && assertIndexInRange(lView, index);
242-
const minifiedName = props[i + 1];
243-
const directiveInstance = lView[index];
244-
const output = directiveInstance[minifiedName];
234+
if (processOutputs) {
235+
const outputConfig = tNode.outputs?.[eventName];
236+
const hostDirectiveOutputConfig = tNode.hostDirectiveOutputs?.[eventName];
245237

246-
if (ngDevMode && !isOutputSubscribable(output)) {
247-
throw new Error(
248-
`@Output ${minifiedName} not initialized in '${directiveInstance.constructor.name}'.`,
249-
);
250-
}
238+
if (hostDirectiveOutputConfig && hostDirectiveOutputConfig.length) {
239+
for (let i = 0; i < hostDirectiveOutputConfig.length; i += 2) {
240+
const index = hostDirectiveOutputConfig[i] as number;
241+
ngDevMode && assertIndexInRange(lView, index);
242+
const instance = lView[index];
243+
const lookupName = hostDirectiveOutputConfig[i + 1] as string;
244+
const def = tView.data[index] as DirectiveDef<unknown>;
245+
const minifiedName = def.outputs[lookupName];
246+
listenToOutput(tNode, instance, eventName, minifiedName, listenerFn, lCleanup, tCleanup);
247+
}
248+
}
251249

252-
const subscription = (output as SubscribableOutput<unknown>).subscribe(listenerFn);
253-
const idx = lCleanup.length;
254-
lCleanup.push(listenerFn, subscription);
255-
tCleanup && tCleanup.push(eventName, tNode.index, idx, -(idx + 1));
250+
if (outputConfig && outputConfig.length) {
251+
for (let i = 0; i < outputConfig.length; i += 2) {
252+
const index = outputConfig[i] as number;
253+
ngDevMode && assertIndexInRange(lView, index);
254+
const instance = lView[index];
255+
const minifiedName = outputConfig[i + 1] as string;
256+
listenToOutput(tNode, instance, eventName, minifiedName, listenerFn, lCleanup, tCleanup);
256257
}
257258
}
258259
}
259260
}
260261

262+
function listenToOutput(
263+
tNode: TNode,
264+
instance: any,
265+
eventName: string,
266+
propertyName: string,
267+
listenerFn: (e?: any) => any,
268+
lCleanup: any[],
269+
tCleanup: any[] | null,
270+
) {
271+
const output = instance[propertyName];
272+
273+
if (ngDevMode && !isOutputSubscribable(output)) {
274+
throw new Error(`@Output ${propertyName} not initialized in '${instance.constructor.name}'.`);
275+
}
276+
277+
const subscription = (output as SubscribableOutput<unknown>).subscribe(listenerFn);
278+
const idx = lCleanup.length;
279+
lCleanup.push(listenerFn, subscription);
280+
tCleanup && tCleanup.push(eventName, tNode.index, idx, -(idx + 1));
281+
}
282+
261283
function executeListenerWithErrorHandling(
262284
lView: LView,
263285
context: {} | null,

packages/core/src/render3/instructions/property.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ export function setDirectiveInputsWhichShadowsStyling(
7171
value: any,
7272
isClassBased: boolean,
7373
) {
74-
const inputs = tNode.inputs!;
75-
const property = isClassBased ? 'class' : 'style';
7674
// We support both 'class' and `className` hence the fallback.
77-
setInputsForProperty(tView, lView, inputs[property], property, value);
75+
setInputsForProperty(tNode, tView, lView, isClassBased ? 'class' : 'style', value);
7876
}

packages/core/src/render3/instructions/shared.ts

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -249,15 +249,18 @@ export function elementPropertyInternal<T>(
249249
nativeOnly: boolean,
250250
): void {
251251
ngDevMode && assertNotSame(value, NO_CHANGE as any, 'Incoming value should never be NO_CHANGE.');
252-
let inputData = tNode.inputs;
253-
let dataValue: NodeInputBindings[typeof propName] | undefined;
254-
if (!nativeOnly && inputData != null && (dataValue = inputData[propName])) {
255-
setInputsForProperty(tView, lView, dataValue, propName, value);
256-
if (isComponentHost(tNode)) markDirtyIfOnPush(lView, tNode.index);
257-
if (ngDevMode) {
258-
setNgReflectProperties(lView, tView, tNode, dataValue, value);
252+
253+
if (!nativeOnly) {
254+
const hasSetInput = setInputsForProperty(tNode, tView, lView, propName, value);
255+
256+
if (hasSetInput) {
257+
isComponentHost(tNode) && markDirtyIfOnPush(lView, tNode.index);
258+
ngDevMode && setNgReflectProperties(lView, tView, tNode, propName, value);
259+
return; // Stop propcessing if we've matched at least one input.
259260
}
260-
} else if (tNode.type & TNodeType.AnyRNode) {
261+
}
262+
263+
if (tNode.type & TNodeType.AnyRNode) {
261264
const element = getNativeByTNode(tNode, lView) as RElement | RComment;
262265
propName = mapPropName(propName);
263266

@@ -310,15 +313,33 @@ function setNgReflectProperty(lView: LView, tNode: TNode, attrName: string, valu
310313
}
311314
}
312315

313-
export function setNgReflectProperties(
316+
function setNgReflectProperties(
314317
lView: LView,
315318
tView: TView,
316319
tNode: TNode,
317-
inputConfig: NodeInputBindings[string],
320+
publicName: string,
318321
value: any,
319322
) {
320-
if (tNode.type & (TNodeType.AnyRNode | TNodeType.Container)) {
321-
// Note: we set the private name of the input as the reflected property, not the public one.
323+
if (!(tNode.type & (TNodeType.AnyRNode | TNodeType.Container))) {
324+
return;
325+
}
326+
327+
// TODO: this is identical to the block below, but will diverge in a future refactor.
328+
// Figure out if we still can't consolidate them somehow.
329+
const inputConfig = tNode.inputs?.[publicName];
330+
const hostInputConfig = tNode.hostDirectiveInputs?.[publicName];
331+
332+
if (hostInputConfig) {
333+
for (let i = 0; i < hostInputConfig.length; i += 2) {
334+
const index = hostInputConfig[i] as number;
335+
const publicName = hostInputConfig[i + 1] as string;
336+
const def = tView.data[index] as DirectiveDef<unknown>;
337+
setNgReflectProperty(lView, tNode, def.inputs[publicName][0], value);
338+
}
339+
}
340+
341+
// Note: we set the private name of the input as the reflected property, not the public one.
342+
if (inputConfig) {
322343
for (let i = 0; i < inputConfig.length; i += 2) {
323344
const index = inputConfig[i] as number;
324345
const lookupName = inputConfig[i + 1] as string;
@@ -555,7 +576,7 @@ export function storePropertyBindingMetadata(
555576
// Since we don't have a concept of the "first update pass" we need to check for presence of the
556577
// binding meta-data to decide if one should be stored (or if was stored already).
557578
if (tData[bindingIndex] === null) {
558-
if (tNode.inputs == null || !tNode.inputs[propertyName]) {
579+
if (!tNode.inputs?.[propertyName] && !tNode.hostDirectiveInputs?.[propertyName]) {
559580
const propBindingIdxs = tNode.propertyBindings || (tNode.propertyBindings = []);
560581
propBindingIdxs.push(bindingIndex);
561582
let bindingMetadata = propertyName;
@@ -599,25 +620,46 @@ export function handleError(lView: LView, error: any): void {
599620
/**
600621
* Set the inputs of directives at the current node to corresponding value.
601622
*
623+
* @param tNode TNode on which the input is being set.
602624
* @param tView The current TView
603625
* @param lView the `LView` which contains the directives.
604-
* @param inputs mapping between the public "input" name and privately-known,
605-
* possibly minified, property names to write to.
606626
* @param value Value to set.
607627
*/
608628
export function setInputsForProperty(
629+
tNode: TNode,
609630
tView: TView,
610631
lView: LView,
611-
inputs: NodeInputBindings[typeof publicName],
612632
publicName: string,
613633
value: unknown,
614-
): void {
615-
for (let i = 0; i < inputs.length; i += 2) {
616-
const index = inputs[i] as number;
617-
ngDevMode && assertIndexInRange(lView, index);
618-
const privateName = inputs[i + 1] as string;
619-
const instance = lView[index];
620-
const def = tView.data[index] as DirectiveDef<any>;
621-
writeToDirectiveInput(def, instance, privateName, value);
634+
): boolean {
635+
const inputs = tNode.inputs?.[publicName];
636+
const hostDirectiveInputs = tNode.hostDirectiveInputs?.[publicName];
637+
let hasMatch = false;
638+
639+
// TODO: this is identical to the block below, but will diverge in a future refactor.
640+
// Figure out if we still can't consolidate them somehow.
641+
if (hostDirectiveInputs) {
642+
for (let i = 0; i < hostDirectiveInputs.length; i += 2) {
643+
const index = hostDirectiveInputs[i] as number;
644+
ngDevMode && assertIndexInRange(lView, index);
645+
const publicName = hostDirectiveInputs[i + 1] as string;
646+
const def = tView.data[index] as DirectiveDef<unknown>;
647+
writeToDirectiveInput(def, lView[index], publicName, value);
648+
hasMatch = true;
649+
}
622650
}
651+
652+
if (inputs) {
653+
for (let i = 0; i < inputs.length; i += 2) {
654+
const index = inputs[i] as number;
655+
ngDevMode && assertIndexInRange(lView, index);
656+
const privateName = inputs[i + 1] as string;
657+
const instance = lView[index];
658+
const def = tView.data[index] as DirectiveDef<any>;
659+
writeToDirectiveInput(def, instance, privateName, value);
660+
hasMatch = true;
661+
}
662+
}
663+
664+
return hasMatch;
623665
}

packages/core/src/render3/interfaces/node.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,12 +425,22 @@ export interface TNode {
425425
*/
426426
inputs: NodeInputBindings | null;
427427

428+
/**
429+
* Input data for host directives applied to the node.
430+
*/
431+
hostDirectiveInputs: HostDirectiveInputs | null;
432+
428433
/**
429434
* Output data for all directives on this node. `null` means that there are no directives with
430435
* outputs on this node.
431436
*/
432437
outputs: NodeOutputBindings | null;
433438

439+
/**
440+
* Input data for host directives applied to the node.
441+
*/
442+
hostDirectiveOutputs: HostDirectiveOutputs | null;
443+
434444
/**
435445
* The TView attached to this node.
436446
*
@@ -835,6 +845,26 @@ export type InitialInputData = (InitialInputs | null)[];
835845
*/
836846
export type InitialInputs = string[];
837847

848+
/**
849+
* Represents inputs coming from a host directive and exposed on a TNode.
850+
*
851+
* - The key is the public name of an input as it is exposed on the specific node.
852+
* - The value is an array where:
853+
* - i+0: Index of the host directive that should be written to.
854+
* - i+1: Public name of the input as it was defined on the host directive before aliasing.
855+
*/
856+
export type HostDirectiveInputs = Record<string, (number | string)[]>;
857+
858+
/**
859+
* Represents outputs coming from a host directive and exposed on a TNode.
860+
*
861+
* - The key is the public name of an output as it is exposed on the specific node.
862+
* - The value is an array where:
863+
* - i+0: Index of the host directive on which the output is defined..
864+
* - i+1: Public name of the output as it was defined on the host directive before aliasing.
865+
*/
866+
export type HostDirectiveOutputs = Record<string, (number | string)[]>;
867+
838868
/**
839869
* Type representing a set of TNodes that can have local refs (`#foo`) placed on them.
840870
*/

packages/core/src/render3/tnode_manipulation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,9 @@ export function createTNode(
293293
localNames: null,
294294
initialInputs: null,
295295
inputs: null,
296+
hostDirectiveInputs: null,
296297
outputs: null,
298+
hostDirectiveOutputs: null,
297299
tView: null,
298300
next: null,
299301
prev: null,

0 commit comments

Comments
 (0)