Angular跨无关组件调用createComponent()时ViewContainerRef未定义报错
问题现状
我需要从弹窗组件AddStatusBoxComponent调用LiveSummaryComponent里的createComponent()函数,动态创建StatusBoxComponent实例。之前修改了AddStatusBoxComponent的代码,把LiveSummaryComponent从类属性改成构造函数注入,并在app.module.ts的providers中添加了该组件。此时调用仅包含console.log()的空createComponent()是有效的,但恢复原有创建组件的逻辑后,控制台报错:
ERROR TypeError: Cannot read properties of undefined (reading 'createComponent') at LiveSummaryComponent.createComponent (live-summary.component.ts:52:38) at AddStatusBoxComponent.addStatusBox (add-status-box.component.ts:129:20) ...
错误指向LiveSummaryComponent中createComponent()函数的这一行:
let childComponentRef = this.VCR.createComponent(componentFactory);
其中this.VCR是通过@ViewChild获取的ViewContainerRef:
@ViewChild("viewContainerRef", { read: ViewContainerRef }) public VCR!: ViewContainerRef;
对应模板中的<ng-template #viewContainerRef></ng-template>。
问题根源
把LiveSummaryComponent加到providers里并通过构造函数注入,本质是让Angular创建了一个全新的单例实例,这个实例从未被渲染到页面上。而@ViewChild只有在组件被Angular正常渲染到DOM中时,才会找到对应的模板元素并赋值,所以这个注入的实例里VCR永远是undefined。
解决方案
方案一:通过弹窗传参传递组件实例
在LiveSummaryComponent中打开弹窗时,直接把自身实例传给弹窗:
// LiveSummaryComponent.ts openAddStatusBoxDialog() { this.dialog.open(AddStatusBoxComponent, { data: { parentRef: this } }); }
修改AddStatusBoxComponent,通过MAT_DIALOG_DATA接收传递的实例:
// AddStatusBoxComponent.ts import { Component, Inject } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { LiveSummaryComponent } from 'src/app/pages/live-summary/live-summary.component'; @Component({ selector: 'app-add-status-box', templateUrl: './add-status-box.component.html', styleUrls: ['./add-status-box.component.css'] }) export class AddStatusBoxComponent { constructor( @Inject(MAT_DIALOG_DATA) public data: { parentRef: LiveSummaryComponent }, private form: FormBuilder, private dialogRef: MatDialogRef<AddStatusBoxComponent> ) {} addStatusBox() { this.data.parentRef.createComponent(); this.dialogRef.close(); } }
最后记得把app.module.ts中providers里的LiveSummaryComponent移除。
方案二:用共享服务实现组件通信(更推荐)
创建一个共享服务来解耦两个组件:
// component-creation.service.ts import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class ComponentCreationService { private createComponent$ = new Subject<void>(); // 触发创建组件的信号 triggerCreateComponent() { this.createComponent$.next(); } // 订阅创建组件的信号 getCreateComponentSignal() { return this.createComponent$.asObservable(); } }
在LiveSummaryComponent中订阅信号:
// LiveSummaryComponent.ts import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; import { ComponentCreationService } from './component-creation.service'; @Component({ selector: 'app-live-summary', templateUrl: './live-summary.component.html', styleUrls: ['./live-summary.component.css'] }) export class LiveSummaryComponent implements OnInit { @ViewChild("viewContainerRef", { read: ViewContainerRef }) public VCR!: ViewContainerRef; constructor(private creationService: ComponentCreationService) {} ngOnInit() { // 订阅创建组件的信号 this.creationService.getCreateComponentSignal().subscribe(() => { this.createComponent(); }); } createComponent() { // 你的动态创建组件逻辑 // ... } }
在AddStatusBoxComponent中调用服务触发信号:
// AddStatusBoxComponent.ts import { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { MatDialogRef } from '@angular/material/dialog'; import { ComponentCreationService } from './component-creation.service'; @Component({ selector: 'app-add-status-box', templateUrl: './add-status-box.component.html', styleUrls: ['./add-status-box.component.css'] }) export class AddStatusBoxComponent { constructor( private creationService: ComponentCreationService, private form: FormBuilder, private dialogRef: MatDialogRef<AddStatusBoxComponent> ) {} addStatusBox() { this.creationService.triggerCreateComponent(); this.dialogRef.close(); } }
这种方式完全解耦了两个组件,符合Angular的组件通信最佳实践,后续扩展也更灵活。
内容的提问来源于stack exchange,提问作者MotorBlade

