Skip to content
⚠️ This article was written in 2022. Some content may be outdated.

Angular 13 動態組件 API 簡化:告別 ComponentFactoryResolver

Angular 13 最低調但最實用的變化之一,是徹底簡化了動態創建組件的 API。以前需要三個步驟才能動態渲染組件,現在一行代碼就夠了。這篇文章通過幾個實際場景展示新 API 的用法。

舊 API 的繁瑣

typescript
// Angular 12 及之前:動態創建組件需要 ComponentFactoryResolver
@Component({
  selector: "app-host",
  template: "<ng-container #container></ng-container>",
})
export class HostComponent {
  @ViewChild("container", { read: ViewContainerRef })
  container!: ViewContainerRef;

  constructor(private resolver: ComponentFactoryResolver) {}

  loadComponent(type: Type<any>) {
    this.container.clear();
    // 第一步:獲取 Factory
    const factory = this.resolver.resolveComponentFactory(type);
    // 第二步:用 Factory 創建
    const ref = this.container.createComponent(factory);
    // 第三步:操作實例
    ref.instance.data = "hello";
  }
}

Angular 13 新 API

typescript
// Angular 13:直接傳組件類
@Component({
  selector: "app-host",
  template: "<ng-container #container></ng-container>",
})
export class HostComponent {
  @ViewChild("container", { read: ViewContainerRef })
  container!: ViewContainerRef;

  loadComponent(type: Type<any>, inputs?: Record<string, any>) {
    this.container.clear();
    const ref = this.container.createComponent(type);
    // 直接設置 inputs
    if (inputs) {
      Object.assign(ref.instance, inputs);
    }
    ref.changeDetectorRef.detectChanges();
    return ref;
  }
}

ComponentFactoryResolver 在 Angular 13 中標記為廢棄(deprecated),但仍然可用,不會報錯。

實際場景:動態 Toast 通知

typescript
// notification.service.ts
@Injectable({ providedIn: "root" })
export class NotificationService {
  private container?: ViewContainerRef;

  setContainer(ref: ViewContainerRef) {
    this.container = ref;
  }

  show(message: string, type: "success" | "error" | "warning" = "success") {
    if (!this.container) return;

    const ref = this.container.createComponent(ToastComponent);
    ref.instance.message = message;
    ref.instance.type = type;
    ref.changeDetectorRef.detectChanges();

    // 3 秒後自動銷燬
    setTimeout(() => {
      ref.destroy();
    }, 3000);
  }
}

// toast.component.ts
@Component({
  selector: "app-toast",
  template: ` <div class="toast" [class]="type">{{ message }}</div> `,
})
export class ToastComponent {
  message = "";
  type: "success" | "error" | "warning" = "success";
}

實際場景:路由級彈窗(無需路由配置)

typescript
// 點擊彈出詳情,不改變 URL
@Component({
  selector: "app-list",
  template: `
    <div *ngFor="let item of items" (click)="openDetail(item)">
      {{ item.name }}
    </div>
    <ng-container #modalHost></ng-container>
  `,
})
export class ListComponent {
  @ViewChild("modalHost", { read: ViewContainerRef })
  modalHost!: ViewContainerRef;

  items = [
    { id: 1, name: "Item 1" },
    { id: 2, name: "Item 2" },
  ];

  openDetail(item: any) {
    this.modalHost.clear();
    const ref = this.modalHost.createComponent(DetailModalComponent);
    ref.instance.item = item;
    ref.instance.close.subscribe(() => {
      ref.destroy();
    });
  }
}

NgComponentOutlet 指令

對於模板驅動的動態組件,NgComponentOutlet 也做了同步更新,支持 inputs:

html
<!-- Angular 13 的 NgComponentOutlet -->
<ng-container
  *ngComponentOutlet="currentComponent; inputs: componentInputs"
></ng-container>
typescript
export class DynamicPageComponent {
  currentComponent: Type<any> = HomeComponent;
  componentInputs = { userId: 123 };

  navigateTo(component: Type<any>, inputs: any) {
    this.currentComponent = component;
    this.componentInputs = inputs;
  }
}

注意:inputs 參數是 Angular 14 才正式支持的(Angular 13 的 NgComponentOutlet 還不支持),這裏僅作預覽。

遷移建議

typescript
// 舊代碼(Angular 12 以下)
const factory = this.resolver.resolveComponentFactory(MyComponent);
const ref = this.container.createComponent(factory);

// 新代碼(Angular 13+)
const ref = this.container.createComponent(MyComponent);

// 如果有 injector 需要傳遞
const ref = this.container.createComponent(MyComponent, {
  injector: this.injector,
  environmentInjector: this.environmentInjector,
});

總結

Angular 13 的動態組件 API 簡化雖然是"小"改變,但消除了一個長期困擾 Angular 開發者的 API 繁瑣問題。ComponentFactoryResolver 作為歷史遺留徹底退出舞台,代碼更簡潔,也更符合直覺。

MIT Licensed