Skip to content

Commit fe57332

Browse files
crisbetommalerba
authored andcommittedMar 3, 2025
feat(core): add input binding support to dynamically-created components (#60137)
Adds the ability to bind to inputs on dynamically-created components, either by targeting the component itself or one of its directives. The new API looks as follows: ```ts const value = signal(123); createComponent(MyComp, { // Bind the value `'hello'` to `someInput` of `MyComp`. bindings: [inputBinding('someInput', () => 'hello')], directives: [{ type: MyDir, // Bind the `value` signal to the `otherInput` of `MyDir`. bindings: [inputBinding('otherInput', value)] }] }); ``` This behavior overlaps with `ComponentRef.setInput`, with a few key differences: 1. `setInput` sets the value on *all* inputs whereas `inputBinding` only targets the specified directive and its host directives. This makes it easier to know which directive you're targeting. 2. `inputBinding` is executed as if it's in a template, making it consistent with how bindings behave for selector-matched components, whereas `setInput` executes outside the lifecycle of the component. 3. It resolves a long-standing issue with `setInput` where it wasn't possible to set the initial value of an input before the first change detection run. Currently `inputBinding` is used only for `createComponent`, `ViewContainerRef.createComponent` and `ComponentFactory.create`, however it is going to be base for more APIs in the future. PR Close #60137
1 parent ea5eb28 commit fe57332

18 files changed

+698
-39
lines changed
 

‎goldens/public-api/core/index.api.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ export interface ComponentDecorator {
295295
// @public @deprecated
296296
export abstract class ComponentFactory<C> {
297297
abstract get componentType(): Type<any>;
298-
abstract create(injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string | any, environmentInjector?: EnvironmentInjector | NgModuleRef<any>, directives?: Type<unknown>[]): ComponentRef<C>;
298+
abstract create(injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string | any, environmentInjector?: EnvironmentInjector | NgModuleRef<any>, directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[], bindings?: Binding[]): ComponentRef<C>;
299299
abstract get inputs(): {
300300
propName: string;
301301
templateName: string;
@@ -454,7 +454,8 @@ export function createComponent<C>(component: Type<C>, options: {
454454
hostElement?: Element;
455455
elementInjector?: Injector;
456456
projectableNodes?: Node[][];
457-
directives?: Type<unknown>[];
457+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[];
458+
bindings?: Binding[];
458459
}): ComponentRef<C>;
459460

460461
// @public
@@ -1984,10 +1985,11 @@ export abstract class ViewContainerRef {
19841985
ngModuleRef?: NgModuleRef<unknown>;
19851986
environmentInjector?: EnvironmentInjector | NgModuleRef<unknown>;
19861987
projectableNodes?: Node[][];
1987-
directives?: Type<unknown>[];
1988+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[];
1989+
bindings?: Binding[];
19881990
}): ComponentRef<C>;
19891991
// @deprecated
1990-
abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], environmentInjector?: EnvironmentInjector | NgModuleRef<any>, directives?: Type<unknown>[]): ComponentRef<C>;
1992+
abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], environmentInjector?: EnvironmentInjector | NgModuleRef<any>, directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[], bindings?: Binding[]): ComponentRef<C>;
19911993
abstract createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, options?: {
19921994
index?: number;
19931995
injector?: Injector;

‎packages/core/src/linker/component_factory.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import type {ChangeDetectorRef} from '../change_detection/change_detection';
1010
import type {Injector} from '../di/injector';
1111
import type {EnvironmentInjector} from '../di/r3_injector';
12-
import {Type} from '../interface/type';
12+
import type {Type} from '../interface/type';
13+
import type {Binding, DirectiveWithBindings} from '../render3/dynamic_bindings';
1314

1415
import type {ElementRef} from './element_ref';
1516
import type {NgModuleRef} from './ng_module_factory';
@@ -122,6 +123,7 @@ export abstract class ComponentFactory<C> {
122123
projectableNodes?: any[][],
123124
rootSelectorOrNode?: string | any,
124125
environmentInjector?: EnvironmentInjector | NgModuleRef<any>,
125-
directives?: Type<unknown>[],
126+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[],
127+
bindings?: Binding[],
126128
): ComponentRef<C>;
127129
}

‎packages/core/src/linker/view_container_ref.ts

+19-7
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import {TemplateRef} from './template_ref';
7777
import {EmbeddedViewRef, ViewRef} from './view_ref';
7878
import {addLViewToLContainer, createLContainer, detachView} from '../render3/view/container';
7979
import {addToEndOfViewTree} from '../render3/view/construction';
80+
import {Binding, DirectiveWithBindings} from '../render3/dynamic_bindings';
8081

8182
/**
8283
* Represents a container where one or more views can be attached to a component.
@@ -226,6 +227,7 @@ export abstract class ViewContainerRef {
226227
* * projectableNodes: list of DOM nodes that should be projected through
227228
* [`<ng-content>`](api/core/ng-content) of the new component instance.
228229
* * directives: Directives that should be applied to the component.
230+
* * bindings: Bindings that should be applied to the component.
229231
*
230232
* @returns The new `ComponentRef` which contains the component instance and the host view.
231233
*/
@@ -237,7 +239,8 @@ export abstract class ViewContainerRef {
237239
ngModuleRef?: NgModuleRef<unknown>;
238240
environmentInjector?: EnvironmentInjector | NgModuleRef<unknown>;
239241
projectableNodes?: Node[][];
240-
directives?: Type<unknown>[];
242+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[];
243+
bindings?: Binding[];
241244
},
242245
): ComponentRef<C>;
243246

@@ -253,6 +256,7 @@ export abstract class ViewContainerRef {
253256
* @param ngModuleRef An instance of the NgModuleRef that represent an NgModule.
254257
* This information is used to retrieve corresponding NgModule injector.
255258
* @param directives Directives that should be applied to the component.
259+
* @param bindings Bindings that should be applied to the component.
256260
*
257261
* @returns The new `ComponentRef` which contains the component instance and the host view.
258262
*
@@ -266,7 +270,8 @@ export abstract class ViewContainerRef {
266270
injector?: Injector,
267271
projectableNodes?: any[][],
268272
environmentInjector?: EnvironmentInjector | NgModuleRef<any>,
269-
directives?: Type<unknown>[],
273+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[],
274+
bindings?: Binding[],
270275
): ComponentRef<C>;
271276

272277
/**
@@ -430,7 +435,8 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
430435
injector?: Injector;
431436
projectableNodes?: Node[][];
432437
ngModuleRef?: NgModuleRef<unknown>;
433-
directives?: Type<unknown>[];
438+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[];
439+
bindings?: Binding[];
434440
},
435441
): ComponentRef<C>;
436442
/**
@@ -444,7 +450,8 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
444450
injector?: Injector | undefined,
445451
projectableNodes?: any[][] | undefined,
446452
environmentInjector?: EnvironmentInjector | NgModuleRef<any> | undefined,
447-
directives?: Type<unknown>[],
453+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[],
454+
bindings?: Binding[],
448455
): ComponentRef<C>;
449456
override createComponent<C>(
450457
componentFactoryOrType: ComponentFactory<C> | Type<C>,
@@ -457,12 +464,14 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
457464
ngModuleRef?: NgModuleRef<unknown>;
458465
environmentInjector?: EnvironmentInjector | NgModuleRef<unknown>;
459466
projectableNodes?: Node[][];
460-
directives?: Type<unknown>[];
467+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[];
468+
bindings?: Binding[];
461469
},
462470
injector?: Injector | undefined,
463471
projectableNodes?: any[][] | undefined,
464472
environmentInjector?: EnvironmentInjector | NgModuleRef<any> | undefined,
465-
directives?: Type<unknown>[],
473+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[],
474+
bindings?: Binding[],
466475
): ComponentRef<C> {
467476
const isComponentFactory = componentFactoryOrType && !isType(componentFactoryOrType);
468477
let index: number | undefined;
@@ -507,7 +516,8 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
507516
ngModuleRef?: NgModuleRef<unknown>;
508517
environmentInjector?: EnvironmentInjector | NgModuleRef<unknown>;
509518
projectableNodes?: Node[][];
510-
directives?: Type<unknown>[];
519+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[];
520+
bindings?: Binding[];
511521
};
512522
if (ngDevMode && options.environmentInjector && options.ngModuleRef) {
513523
throwError(
@@ -519,6 +529,7 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
519529
projectableNodes = options.projectableNodes;
520530
environmentInjector = options.environmentInjector || options.ngModuleRef;
521531
directives = options.directives;
532+
bindings = options.bindings;
522533
}
523534

524535
const componentFactory: ComponentFactory<C> = isComponentFactory
@@ -564,6 +575,7 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
564575
rNode,
565576
environmentInjector,
566577
directives,
578+
bindings,
567579
);
568580
this.insertImpl(
569581
componentRef.hostView,

‎packages/core/src/render3/component.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {ComponentRef} from '../linker/component_factory';
1313

1414
import {ComponentFactory} from './component_ref';
1515
import {getComponentDef} from './def_getters';
16+
import {Binding, DirectiveWithBindings} from './dynamic_bindings';
1617
import {assertComponentDef} from './errors';
1718

1819
/**
@@ -74,6 +75,7 @@ import {assertComponentDef} from './errors';
7475
* `[[element1, element2], [element3]]`: projects `element1` and `element2` into one `<ng-content>`,
7576
* and `element3` into a separate `<ng-content>`.
7677
* * `directives` (optional): Directives that should be applied to the component.
78+
* * `binding` (optional): Bindings to apply to the root component.
7779
* @returns ComponentRef instance that represents a given Component.
7880
*
7981
* @publicApi
@@ -85,7 +87,8 @@ export function createComponent<C>(
8587
hostElement?: Element;
8688
elementInjector?: Injector;
8789
projectableNodes?: Node[][];
88-
directives?: Type<unknown>[];
90+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[];
91+
bindings?: Binding[];
8992
},
9093
): ComponentRef<C> {
9194
ngDevMode && assertComponentDef(component);
@@ -98,6 +101,7 @@ export function createComponent<C>(
98101
options.hostElement,
99102
options.environmentInjector,
100103
options.directives,
104+
options.bindings,
101105
);
102106
}
103107

‎packages/core/src/render3/component_ref.ts

+101-23
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import {Injector} from '../di/injector';
1717
import {EnvironmentInjector} from '../di/r3_injector';
1818
import {RuntimeError, RuntimeErrorCode} from '../errors';
19-
import {Type} from '../interface/type';
19+
import {Type, Writable} from '../interface/type';
2020
import {
2121
ComponentFactory as AbstractComponentFactory,
2222
ComponentRef as AbstractComponentRef,
@@ -40,7 +40,7 @@ import {
4040
locateHostElement,
4141
setAllInputsForProperty,
4242
} from './instructions/shared';
43-
import {ComponentDef, DirectiveDef} from './interfaces/definition';
43+
import {ComponentDef, ComponentTemplate, DirectiveDef, RenderFlags} from './interfaces/definition';
4444
import {InputFlags} from './interfaces/input_flags';
4545
import {TContainerNode, TElementContainerNode, TElementNode, TNode} from './interfaces/node';
4646
import {RElement, RNode} from './interfaces/renderer_dom';
@@ -50,6 +50,7 @@ import {
5050
LView,
5151
LViewEnvironment,
5252
LViewFlags,
53+
TView,
5354
TVIEW,
5455
TViewType,
5556
} from './interfaces/view';
@@ -73,6 +74,7 @@ import {getComponentLViewByIndex, getTNode} from './util/view_utils';
7374
import {elementEndFirstCreatePass, elementStartFirstCreatePass} from './view/elements';
7475
import {ViewRef} from './view_ref';
7576
import {createLView, createTView, getInitialLViewFlagsFromDef} from './view/construction';
77+
import {BINDING, Binding, DirectiveWithBindings} from './dynamic_bindings';
7678

7779
export class ComponentFactoryResolver extends AbstractComponentFactoryResolver {
7880
/**
@@ -231,7 +233,8 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
231233
projectableNodes?: any[][] | undefined,
232234
rootSelectorOrNode?: any,
233235
environmentInjector?: NgModuleRef<any> | EnvironmentInjector | undefined,
234-
directives?: Type<unknown>[],
236+
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[],
237+
componentBindings?: Binding[],
235238
): AbstractComponentRef<T> {
236239
profiler(ProfilerEvent.DynamicComponentStart);
237240

@@ -240,25 +243,7 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
240243
const cmpDef = this.componentDef;
241244
ngDevMode && verifyNotAnOrphanComponent(cmpDef);
242245

243-
const tAttributes = rootSelectorOrNode
244-
? ['ng-version', '0.0.0-PLACEHOLDER']
245-
: // Extract attributes and classes from the first selector only to match VE behavior.
246-
extractAttrsAndClassesFromSelector(this.componentDef.selectors[0]);
247-
// Create the root view. Uses empty TView and ContentTemplate.
248-
const rootTView = createTView(
249-
TViewType.Root,
250-
null,
251-
null,
252-
1,
253-
0,
254-
null,
255-
null,
256-
null,
257-
null,
258-
[tAttributes],
259-
null,
260-
);
261-
246+
const rootTView = createRootTView(rootSelectorOrNode, cmpDef, componentBindings, directives);
262247
const rootViewInjector = createRootViewInjector(
263248
cmpDef,
264249
environmentInjector || this.ngModule,
@@ -293,7 +278,8 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
293278
const directivesToApply: DirectiveDef<unknown>[] = [this.componentDef];
294279

295280
if (directives) {
296-
for (const directiveType of directives) {
281+
for (const directive of directives) {
282+
const directiveType = typeof directive === 'function' ? directive : directive.type;
297283
const directiveDef = getDirectiveDef(directiveType, true);
298284

299285
if (ngDevMode && !directiveDef.standalone) {
@@ -376,6 +362,98 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
376362
}
377363
}
378364

365+
function createRootTView(
366+
rootSelectorOrNode: any,
367+
componentDef: ComponentDef<unknown>,
368+
componentBindings: Binding[] | undefined,
369+
directives: (Type<unknown> | DirectiveWithBindings<unknown>)[] | undefined,
370+
): TView {
371+
const tAttributes = rootSelectorOrNode
372+
? ['ng-version', '0.0.0-PLACEHOLDER']
373+
: // Extract attributes and classes from the first selector only to match VE behavior.
374+
extractAttrsAndClassesFromSelector(componentDef.selectors[0]);
375+
let creationBindings: Binding[] | null = null;
376+
let updateBindings: Binding[] | null = null;
377+
let varsToAllocate = 0;
378+
379+
if (componentBindings) {
380+
for (const binding of componentBindings) {
381+
varsToAllocate += binding[BINDING].requiredVars;
382+
383+
if (binding.create) {
384+
(binding as Writable<Binding>).target = componentDef;
385+
(creationBindings ??= []).push(binding);
386+
}
387+
388+
if (binding.update) {
389+
(binding as Writable<Binding>).target = componentDef;
390+
(updateBindings ??= []).push(binding);
391+
}
392+
}
393+
}
394+
395+
if (directives) {
396+
for (const directive of directives) {
397+
if (typeof directive !== 'function') {
398+
const def: DirectiveDef<unknown> = getDirectiveDef(directive.type, true);
399+
400+
for (const binding of directive.bindings) {
401+
varsToAllocate += binding[BINDING].requiredVars;
402+
403+
if (binding.create) {
404+
(binding as Writable<Binding>).target = def;
405+
(creationBindings ??= []).push(binding);
406+
}
407+
408+
if (binding.update) {
409+
(binding as Writable<Binding>).target = def;
410+
(updateBindings ??= []).push(binding);
411+
}
412+
}
413+
}
414+
}
415+
}
416+
417+
const rootTView = createTView(
418+
TViewType.Root,
419+
null,
420+
getRootTViewTemplate(creationBindings, updateBindings),
421+
1,
422+
varsToAllocate,
423+
null,
424+
null,
425+
null,
426+
null,
427+
[tAttributes],
428+
null,
429+
);
430+
431+
return rootTView;
432+
}
433+
434+
function getRootTViewTemplate(
435+
creationBindings: Binding[] | null,
436+
updateBindings: Binding[] | null,
437+
): ComponentTemplate<unknown> | null {
438+
if (!creationBindings && !updateBindings) {
439+
return null;
440+
}
441+
442+
return (flags) => {
443+
if (flags & RenderFlags.Create && creationBindings) {
444+
for (const binding of creationBindings) {
445+
binding.create!();
446+
}
447+
}
448+
449+
if (flags & RenderFlags.Update && updateBindings) {
450+
for (const binding of updateBindings) {
451+
binding.update!();
452+
}
453+
}
454+
};
455+
}
456+
379457
/**
380458
* Represents an instance of a Component created via a {@link ComponentFactory}.
381459
*

0 commit comments

Comments
 (0)
Please sign in to comment.