动态组件 #

一、动态组件概述 #

动态组件允许在运行时动态创建和销毁组件,适用于模态框、标签页、动态表单等场景。

二、基本动态组件 #

2.1 使用ViewContainerRef #

typescript
import { Component, ViewChild, ViewContainerRef, AfterViewInit, ComponentRef } from '@angular/core';
import { UserCardComponent } from './user-card.component';

@Component({
  selector: 'app-dynamic-demo',
  template: `
    <button (click)="createComponent()">创建组件</button>
    <button (click)="destroyComponent()">销毁组件</button>
    <ng-template #container></ng-template>
  `
})
export class DynamicDemoComponent implements AfterViewInit {
  @ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
  private componentRef: ComponentRef<UserCardComponent> | null = null;
  
  ngAfterViewInit() {}
  
  createComponent() {
    this.container.clear();
    this.componentRef = this.container.createComponent(UserCardComponent);
    
    this.componentRef.instance.user = {
      name: 'John',
      email: 'john@example.com'
    };
    
    this.componentRef.instance.selected.subscribe((user) => {
      console.log('选中用户:', user);
    });
  }
  
  destroyComponent() {
    if (this.componentRef) {
      this.componentRef.destroy();
      this.componentRef = null;
    }
  }
}

2.2 传递数据给动态组件 #

typescript
@Component({
  selector: 'app-user-card',
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
      <button (click)="onSelect()">选择</button>
    </div>
  `
})
export class UserCardComponent {
  @Input() user: User;
  @Output() selected = new EventEmitter<User>();
  
  onSelect() {
    this.selected.emit(this.user);
  }
}

// 创建并传递数据
createComponent() {
  const componentRef = this.container.createComponent(UserCardComponent);
  componentRef.instance.user = this.currentUser;
  
  componentRef.instance.selected.subscribe(user => {
    console.log('选中:', user);
  });
}

三、动态模态框 #

3.1 模态框服务 #

typescript
import { Injectable, inject } from '@angular/core';
import { ViewContainerRef } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class ModalService {
  private container: ViewContainerRef;
  
  setContainer(container: ViewContainerRef) {
    this.container = container;
  }
  
  open<T>(component: Type<T>, inputs?: Partial<T>): ComponentRef<T> {
    this.container.clear();
    const componentRef = this.container.createComponent(component);
    
    if (inputs) {
      Object.assign(componentRef.instance, inputs);
    }
    
    return componentRef;
  }
  
  close() {
    this.container.clear();
  }
}

3.2 模态框组件 #

typescript
@Component({
  selector: 'app-modal',
  template: `
    <div class="modal-overlay" (click)="onOverlayClick()">
      <div class="modal-content" (click)="$event.stopPropagation()">
        <div class="modal-header">
          <h3>{{ title }}</h3>
          <button (click)="close.emit()">×</button>
        </div>
        <div class="modal-body">
          <ng-content></ng-content>
        </div>
        <div class="modal-footer">
          <button (click)="cancel.emit()">取消</button>
          <button (click)="confirm.emit()">确认</button>
        </div>
      </div>
    </div>
  `,
  styles: [`
    .modal-overlay {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.5);
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .modal-content {
      background: white;
      padding: 20px;
      border-radius: 8px;
      min-width: 300px;
    }
  `]
})
export class ModalComponent {
  @Input() title: string = '模态框';
  @Output() close = new EventEmitter<void>();
  @Output() confirm = new EventEmitter<void>();
  @Output() cancel = new EventEmitter<void>();
  
  onOverlayClick() {
    this.close.emit();
  }
}

3.3 使用模态框 #

typescript
@Component({
  selector: 'app-root',
  template: `
    <button (click)="openModal()">打开模态框</button>
    <ng-template #modalContainer></ng-template>
  `
})
export class AppComponent implements AfterViewInit {
  @ViewChild('modalContainer', { read: ViewContainerRef }) modalContainer!: ViewContainerRef;
  
  constructor(private modalService: ModalService) {}
  
  ngAfterViewInit() {
    this.modalService.setContainer(this.modalContainer);
  }
  
  openModal() {
    const modalRef = this.modalService.open(ModalComponent, {
      title: '确认操作'
    });
    
    modalRef.instance.confirm.subscribe(() => {
      console.log('确认');
      this.modalService.close();
    });
    
    modalRef.instance.cancel.subscribe(() => {
      console.log('取消');
      this.modalService.close();
    });
  }
}

四、动态标签页 #

4.1 标签页配置 #

typescript
interface Tab {
  id: string;
  title: string;
  component: Type<any>;
  inputs?: Record<string, any>;
}

@Component({
  selector: 'app-tabs',
  template: `
    <div class="tabs">
      <div class="tab-headers">
        <button 
          *ngFor="let tab of tabs"
          [class.active]="activeTab === tab.id"
          (click)="selectTab(tab.id)">
          {{ tab.title }}
          <span (click)="closeTab(tab.id); $event.stopPropagation()">×</span>
        </button>
      </div>
      <div class="tab-content">
        <ng-template #tabContainer></ng-template>
      </div>
    </div>
  `
})
export class TabsComponent implements AfterViewInit {
  @ViewChild('tabContainer', { read: ViewContainerRef }) container!: ViewContainerRef;
  @Input() tabs: Tab[] = [];
  @Output() tabClosed = new EventEmitter<string>();
  
  activeTab: string | null = null;
  private componentRef: ComponentRef<any> | null = null;
  
  ngAfterViewInit() {
    if (this.tabs.length > 0) {
      this.selectTab(this.tabs[0].id);
    }
  }
  
  selectTab(tabId: string) {
    const tab = this.tabs.find(t => t.id === tabId);
    if (!tab) return;
    
    this.activeTab = tabId;
    this.container.clear();
    
    this.componentRef = this.container.createComponent(tab.component);
    
    if (tab.inputs) {
      Object.assign(this.componentRef.instance, tab.inputs);
    }
  }
  
  closeTab(tabId: string) {
    this.tabClosed.emit(tabId);
  }
}

4.2 使用标签页 #

typescript
@Component({
  selector: 'app-dashboard',
  template: `
    <app-tabs 
      [tabs]="tabs"
      (tabClosed)="onTabClosed($event)">
    </app-tabs>
  `
})
export class DashboardComponent {
  tabs: Tab[] = [
    { id: 'home', title: '首页', component: HomeComponent },
    { id: 'users', title: '用户', component: UsersComponent },
    { id: 'settings', title: '设置', component: SettingsComponent }
  ];
  
  onTabClosed(tabId: string) {
    this.tabs = this.tabs.filter(t => t.id !== tabId);
  }
}

五、动态表单字段 #

5.1 字段配置 #

typescript
interface FieldConfig {
  type: 'text' | 'number' | 'select' | 'date';
  name: string;
  label: string;
  options?: { label: string; value: any }[];
  validators?: ValidatorFn[];
}

@Component({
  selector: 'app-dynamic-field',
  template: `
    <div [ngSwitch]="config.type">
      <input 
        *ngSwitchCase="'text'"
        type="text"
        [formControl]="control"
        [placeholder]="config.label"
      />
      
      <input 
        *ngSwitchCase="'number'"
        type="number"
        [formControl]="control"
        [placeholder]="config.label"
      />
      
      <select 
        *ngSwitchCase="'select'"
        [formControl]="control">
        <option value="">{{ config.label }}</option>
        <option 
          *ngFor="let option of config.options"
          [value]="option.value">
          {{ option.label }}
        </option>
      </select>
      
      <input 
        *ngSwitchCase="'date'"
        type="date"
        [formControl]="control"
      />
    </div>
  `
})
export class DynamicFieldComponent {
  @Input() config: FieldConfig;
  @Input() control: FormControl;
}

5.2 动态表单组件 #

typescript
@Component({
  selector: 'app-dynamic-form',
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <div *ngFor="let field of fields">
        <label>{{ field.label }}</label>
        <app-dynamic-field 
          [config]="field"
          [control]="form.get(field.name)">
        </app-dynamic-field>
      </div>
      <button type="submit">提交</button>
    </form>
  `
})
export class DynamicFormComponent {
  @Input() fields: FieldConfig[] = [];
  @Output() submit = new EventEmitter<any>();
  
  form: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnChanges() {
    this.createForm();
  }
  
  private createForm() {
    const controls: { [key: string]: FormControl } = {};
    
    for (const field of this.fields) {
      controls[field.name] = new FormControl('', field.validators || []);
    }
    
    this.form = this.fb.group(controls);
  }
  
  onSubmit() {
    if (this.form.valid) {
      this.submit.emit(this.form.value);
    }
  }
}

六、懒加载组件 #

6.1 懒加载服务 #

typescript
import { Injectable } from '@angular/core';
import { ViewContainerRef, Type } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class LazyLoadService {
  async loadComponent<T>(
    container: ViewContainerRef,
    importFn: () => Promise<{ [key: string]: Type<T> }>,
    componentName: string
  ): Promise<ComponentRef<T>> {
    container.clear();
    
    const module = await importFn();
    const component = module[componentName];
    
    return container.createComponent(component);
  }
}

6.2 使用懒加载 #

typescript
@Component({
  selector: 'app-lazy-demo',
  template: `
    <button (click)="loadComponent()">加载组件</button>
    <ng-template #container></ng-template>
  `
})
export class LazyDemoComponent {
  @ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
  
  constructor(private lazyLoadService: LazyLoadService) {}
  
  async loadComponent() {
    const componentRef = await this.lazyLoadService.loadComponent(
      this.container,
      () => import('./heavy-component/heavy-component.component'),
      'HeavyComponent'
    );
    
    componentRef.instance.data = { id: 1 };
  }
}

七、最佳实践 #

7.1 清理资源 #

typescript
ngOnDestroy() {
  if (this.componentRef) {
    this.componentRef.destroy();
  }
}

7.2 使用类型安全 #

typescript
interface ComponentInputs {
  user: User;
  mode: 'edit' | 'view';
}

createComponent(inputs: ComponentInputs) {
  const componentRef = this.container.createComponent(UserFormComponent);
  Object.assign(componentRef.instance, inputs);
}

7.3 处理订阅 #

typescript
createComponent() {
  const componentRef = this.container.createComponent(MyComponent);
  
  const subscription = componentRef.instance.someEvent.subscribe(data => {
    console.log(data);
  });
  
  componentRef.onDestroy(() => {
    subscription.unsubscribe();
  });
}

八、总结 #

概念 说明
ViewContainerRef 视图容器引用
createComponent 创建动态组件
ComponentRef 组件引用
instance 组件实例
destroy 销毁组件

下一步:模块与懒加载

最后更新:2026-03-26