Skip to content

Commit 82aa2c1

Browse files
crisbetommalerba
authored andcommitted
feat(core): add the ability to apply directives to dynamically-created components (#60137)
Updates `createComponent`, `ViewContainerRef.createComponent` and `ComponentFactory.create` to allow the user to specify directives that should be applied when creating the component. PR Close #60137
1 parent 25cae45 commit 82aa2c1

File tree

8 files changed

+172
-7
lines changed

8 files changed

+172
-7
lines changed

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

Lines changed: 4 additions & 2 deletions
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>): ComponentRef<C>;
298+
abstract create(injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string | any, environmentInjector?: EnvironmentInjector | NgModuleRef<any>, directives?: Type<unknown>[]): ComponentRef<C>;
299299
abstract get inputs(): {
300300
propName: string;
301301
templateName: string;
@@ -454,6 +454,7 @@ export function createComponent<C>(component: Type<C>, options: {
454454
hostElement?: Element;
455455
elementInjector?: Injector;
456456
projectableNodes?: Node[][];
457+
directives?: Type<unknown>[];
457458
}): ComponentRef<C>;
458459

459460
// @public
@@ -1980,9 +1981,10 @@ export abstract class ViewContainerRef {
19801981
ngModuleRef?: NgModuleRef<unknown>;
19811982
environmentInjector?: EnvironmentInjector | NgModuleRef<unknown>;
19821983
projectableNodes?: Node[][];
1984+
directives?: Type<unknown>[];
19831985
}): ComponentRef<C>;
19841986
// @deprecated
1985-
abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], environmentInjector?: EnvironmentInjector | NgModuleRef<any>): ComponentRef<C>;
1987+
abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], environmentInjector?: EnvironmentInjector | NgModuleRef<any>, directives?: Type<unknown>[]): ComponentRef<C>;
19861988
abstract createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, options?: {
19871989
index?: number;
19881990
injector?: Injector;

packages/core/src/linker/component_factory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,6 @@ export abstract class ComponentFactory<C> {
122122
projectableNodes?: any[][],
123123
rootSelectorOrNode?: string | any,
124124
environmentInjector?: EnvironmentInjector | NgModuleRef<any>,
125+
directives?: Type<unknown>[],
125126
): ComponentRef<C>;
126127
}

packages/core/src/linker/view_container_ref.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export abstract class ViewContainerRef {
225225
* replace the `ngModuleRef` parameter.
226226
* * projectableNodes: list of DOM nodes that should be projected through
227227
* [`<ng-content>`](api/core/ng-content) of the new component instance.
228+
* * directives: Directives that should be applied to the component.
228229
*
229230
* @returns The new `ComponentRef` which contains the component instance and the host view.
230231
*/
@@ -236,6 +237,7 @@ export abstract class ViewContainerRef {
236237
ngModuleRef?: NgModuleRef<unknown>;
237238
environmentInjector?: EnvironmentInjector | NgModuleRef<unknown>;
238239
projectableNodes?: Node[][];
240+
directives?: Type<unknown>[];
239241
},
240242
): ComponentRef<C>;
241243

@@ -250,6 +252,7 @@ export abstract class ViewContainerRef {
250252
* [`<ng-content>`](api/core/ng-content) of the new component instance.
251253
* @param ngModuleRef An instance of the NgModuleRef that represent an NgModule.
252254
* This information is used to retrieve corresponding NgModule injector.
255+
* @param directives Directives that should be applied to the component.
253256
*
254257
* @returns The new `ComponentRef` which contains the component instance and the host view.
255258
*
@@ -263,6 +266,7 @@ export abstract class ViewContainerRef {
263266
injector?: Injector,
264267
projectableNodes?: any[][],
265268
environmentInjector?: EnvironmentInjector | NgModuleRef<any>,
269+
directives?: Type<unknown>[],
266270
): ComponentRef<C>;
267271

268272
/**
@@ -426,6 +430,7 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
426430
injector?: Injector;
427431
projectableNodes?: Node[][];
428432
ngModuleRef?: NgModuleRef<unknown>;
433+
directives?: Type<unknown>[];
429434
},
430435
): ComponentRef<C>;
431436
/**
@@ -439,6 +444,7 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
439444
injector?: Injector | undefined,
440445
projectableNodes?: any[][] | undefined,
441446
environmentInjector?: EnvironmentInjector | NgModuleRef<any> | undefined,
447+
directives?: Type<unknown>[],
442448
): ComponentRef<C>;
443449
override createComponent<C>(
444450
componentFactoryOrType: ComponentFactory<C> | Type<C>,
@@ -451,10 +457,12 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
451457
ngModuleRef?: NgModuleRef<unknown>;
452458
environmentInjector?: EnvironmentInjector | NgModuleRef<unknown>;
453459
projectableNodes?: Node[][];
460+
directives?: Type<unknown>[];
454461
},
455462
injector?: Injector | undefined,
456463
projectableNodes?: any[][] | undefined,
457464
environmentInjector?: EnvironmentInjector | NgModuleRef<any> | undefined,
465+
directives?: Type<unknown>[],
458466
): ComponentRef<C> {
459467
const isComponentFactory = componentFactoryOrType && !isType(componentFactoryOrType);
460468
let index: number | undefined;
@@ -499,6 +507,7 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
499507
ngModuleRef?: NgModuleRef<unknown>;
500508
environmentInjector?: EnvironmentInjector | NgModuleRef<unknown>;
501509
projectableNodes?: Node[][];
510+
directives?: Type<unknown>[];
502511
};
503512
if (ngDevMode && options.environmentInjector && options.ngModuleRef) {
504513
throwError(
@@ -509,6 +518,7 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
509518
injector = options.injector;
510519
projectableNodes = options.projectableNodes;
511520
environmentInjector = options.environmentInjector || options.ngModuleRef;
521+
directives = options.directives;
512522
}
513523

514524
const componentFactory: ComponentFactory<C> = isComponentFactory
@@ -553,6 +563,7 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
553563
projectableNodes,
554564
rNode,
555565
environmentInjector,
566+
directives,
556567
);
557568
this.insertImpl(
558569
componentRef.hostView,

packages/core/src/render3/component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import {assertComponentDef} from './errors';
7373
* `[[element1, element2]]`: projects `element1` and `element2` into the same `<ng-content>`.
7474
* `[[element1, element2], [element3]]`: projects `element1` and `element2` into one `<ng-content>`,
7575
* and `element3` into a separate `<ng-content>`.
76+
* * `directives` (optional): Directives that should be applied to the component.
7677
* @returns ComponentRef instance that represents a given Component.
7778
*
7879
* @publicApi
@@ -84,6 +85,7 @@ export function createComponent<C>(
8485
hostElement?: Element;
8586
elementInjector?: Injector;
8687
projectableNodes?: Node[][];
88+
directives?: Type<unknown>[];
8789
},
8890
): ComponentRef<C> {
8991
ngDevMode && assertComponentDef(component);
@@ -95,6 +97,7 @@ export function createComponent<C>(
9597
options.projectableNodes,
9698
options.hostElement,
9799
options.environmentInjector,
100+
options.directives,
98101
);
99102
}
100103

packages/core/src/render3/component_ref.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {Sanitizer} from '../sanitization/sanitizer';
2929

3030
import {assertComponentType} from './assert';
3131
import {attachPatchData} from './context_discovery';
32-
import {getComponentDef} from './def_getters';
32+
import {getComponentDef, getDirectiveDef} from './def_getters';
3333
import {depsTracker} from './deps_tracker/deps_tracker';
3434
import {NodeInjector} from './di';
3535
import {reportUnknownPropertyError} from './instructions/element_validation';
@@ -231,6 +231,7 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
231231
projectableNodes?: any[][] | undefined,
232232
rootSelectorOrNode?: any,
233233
environmentInjector?: NgModuleRef<any> | EnvironmentInjector | undefined,
234+
directives?: Type<unknown>[],
234235
): AbstractComponentRef<T> {
235236
profiler(ProfilerEvent.DynamicComponentStart);
236237

@@ -289,6 +290,24 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
289290
retrieveHydrationInfo(hostElement, rootViewInjector, true /* isRootView */),
290291
);
291292

293+
const directivesToApply: DirectiveDef<unknown>[] = [this.componentDef];
294+
295+
if (directives) {
296+
for (const directiveType of directives) {
297+
const directiveDef = getDirectiveDef(directiveType, true);
298+
299+
if (ngDevMode && !directiveDef.standalone) {
300+
throw new RuntimeError(
301+
RuntimeErrorCode.TYPE_IS_NOT_STANDALONE,
302+
`The ${stringifyForError(directiveType)} directive must be standalone in ` +
303+
`order to be applied to a dynamically-created component.`,
304+
);
305+
}
306+
307+
directivesToApply.push(directiveDef);
308+
}
309+
}
310+
292311
rootLView[HEADER_OFFSET] = hostElement;
293312

294313
// rootView is the parent when bootstrapping
@@ -306,14 +325,14 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
306325
rootTView,
307326
rootLView,
308327
'#host',
309-
() => [this.componentDef],
328+
() => directivesToApply,
310329
true,
311330
0,
312331
);
313332

314333
// ---- element instruction
315334

316-
// TODO(crisbeto): in practice `hostRNode` should always be defined, but there are some
335+
// TODO(crisbeto): in practice `hostElement` should always be defined, but there are some
317336
// tests where the renderer is mocked out and `undefined` is returned. We should update the
318337
// tests so that this check can be removed.
319338
if (hostElement) {

packages/core/test/acceptance/create_component_spec.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,43 @@ describe('createComponent', () => {
489489
expect(createdInstance).toBe(injectedInstance);
490490
});
491491

492+
it('should write to the inputs of the attached directives using setInput', () => {
493+
let dirInstance!: Dir;
494+
495+
@Directive()
496+
class Dir {
497+
@Input() someInput = 0;
498+
499+
constructor() {
500+
dirInstance = this;
501+
}
502+
}
503+
504+
@Component({template: ''})
505+
class HostComponent {
506+
@Input() someInput = 0;
507+
}
508+
509+
const hostElement = document.createElement('div');
510+
const environmentInjector = TestBed.inject(EnvironmentInjector);
511+
const ref = createComponent(HostComponent, {
512+
hostElement,
513+
environmentInjector,
514+
directives: [Dir],
515+
});
516+
517+
expect(dirInstance.someInput).toBe(0);
518+
expect(ref.instance.someInput).toBe(0);
519+
520+
ref.setInput('someInput', 1);
521+
expect(dirInstance.someInput).toBe(1);
522+
expect(ref.instance.someInput).toBe(1);
523+
524+
ref.setInput('someInput', 2);
525+
expect(dirInstance.someInput).toBe(2);
526+
expect(ref.instance.someInput).toBe(2);
527+
});
528+
492529
it('should throw if the same directive is attached multiple times', () => {
493530
@Directive({})
494531
class Dir {}
@@ -523,7 +560,7 @@ describe('createComponent', () => {
523560
environmentInjector,
524561
directives: [NotADir],
525562
});
526-
}).toThrowError(/Class NotADir is not a directive/);
563+
}).toThrowError(/Type NotADir does not have 'ɵdir' property/);
527564
});
528565

529566
it('should throw if a component class is attached', () => {
@@ -542,7 +579,28 @@ describe('createComponent', () => {
542579
environmentInjector,
543580
directives: [NotADir],
544581
});
545-
}).toThrowError(/Class NotADir is not a directive/);
582+
}).toThrowError(/Type NotADir does not have 'ɵdir' property/);
583+
});
584+
585+
it('should throw if attached directive is not standalone', () => {
586+
@Directive({standalone: false})
587+
class Dir {}
588+
589+
@Component({template: ''})
590+
class HostComponent {}
591+
592+
const hostElement = document.createElement('div');
593+
const environmentInjector = TestBed.inject(EnvironmentInjector);
594+
595+
expect(() => {
596+
createComponent(HostComponent, {
597+
hostElement,
598+
environmentInjector,
599+
directives: [Dir],
600+
});
601+
}).toThrowError(
602+
/The Dir directive must be standalone in order to be applied to a dynamically-created component/,
603+
);
546604
});
547605
});
548606

packages/core/test/acceptance/view_container_ref_spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1790,6 +1790,75 @@ describe('ViewContainerRef', () => {
17901790
);
17911791
});
17921792

1793+
it('should support attaching directives when creating the component', () => {
1794+
const logs: string[] = [];
1795+
1796+
@Directive({
1797+
selector: '[dir-one]',
1798+
host: {
1799+
'class': 'class-1',
1800+
'attr-one': 'one',
1801+
},
1802+
})
1803+
class Dir1 {
1804+
constructor() {
1805+
logs.push('Dir1');
1806+
}
1807+
}
1808+
1809+
@Directive({
1810+
selector: 'dir-two',
1811+
host: {
1812+
'class': 'class-2',
1813+
'attr-two': 'two',
1814+
},
1815+
})
1816+
class Dir2 {
1817+
constructor() {
1818+
logs.push('Dir2');
1819+
}
1820+
}
1821+
1822+
@Component({
1823+
selector: 'host-component',
1824+
template: '',
1825+
standalone: false,
1826+
host: {
1827+
'class': 'host',
1828+
'attr-three': 'host',
1829+
},
1830+
})
1831+
class HostComponent {
1832+
constructor() {
1833+
logs.push('HostComponent');
1834+
}
1835+
}
1836+
1837+
TestBed.resetTestingModule();
1838+
TestBed.configureTestingModule({
1839+
declarations: [EmbeddedViewInsertionComp, VCRefDirective, HostComponent],
1840+
});
1841+
const fixture = TestBed.createComponent(EmbeddedViewInsertionComp);
1842+
const vcRefDir = fixture.debugElement
1843+
.query(By.directive(VCRefDirective))
1844+
.injector.get(VCRefDirective);
1845+
fixture.detectChanges();
1846+
1847+
expect(getElementHtml(fixture.nativeElement)).toEqual('<p vcref=""></p>');
1848+
1849+
vcRefDir.vcref.createComponent(HostComponent, {
1850+
index: 0,
1851+
directives: [Dir1, Dir2],
1852+
});
1853+
fixture.detectChanges();
1854+
1855+
expect(logs).toEqual(['HostComponent', 'Dir1', 'Dir2']);
1856+
expect(getElementHtml(fixture.nativeElement)).toEqual(
1857+
'<p vcref=""></p><host-component attr-three="host" attr-one="one" ' +
1858+
'attr-two="two" class="host class-1 class-2"></host-component>',
1859+
);
1860+
});
1861+
17931862
describe('`options` argument handling', () => {
17941863
it('should work correctly when an empty object is provided', () => {
17951864
fixture.componentInstance.viewContainerRef.createComponent(ChildA, {});

packages/core/test/bundling/hello_world/bundle.golden_symbols.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"LOCALE_ID2",
4949
"NEW_LINE",
5050
"NG_COMP_DEF",
51+
"NG_DIR_DEF",
5152
"NG_ELEMENT_ID",
5253
"NG_ENV_ID",
5354
"NG_FACTORY_DEF",
@@ -197,6 +198,7 @@
197198
"getCurrentTNode",
198199
"getCurrentTNodePlaceholderOk",
199200
"getDeclarationTNode",
201+
"getDirectiveDef",
200202
"getFactoryDef",
201203
"getFirstLContainer",
202204
"getInitialLViewFlagsFromDef",

0 commit comments

Comments
 (0)