Angular - 动态组件



Angular 允许在运行时动态创建组件并在宿主(另一个)组件的特定位置加载。在运行时加载组件为实现高级功能提供了许多机会。例如,横幅轮播组件可以接受高度定制的横幅项目,而不是接受符合特定模板的横幅,这些模板始终是预定义且静态的。

创建动态组件

让我们在本节中学习如何在运行时动态创建组件并将其附加到宿主组件。

步骤1:创建一个属性指令(例如 HelloDirective)。属性指令的目的是在宿主组件的模板中定位元素(ng-template),动态组件将被注入到该元素中。

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({
   selector: '[appHello]'
})
export class HelloDirective {
}

这里,

  • appHello 是用于定位和选择元素(ng-template)的选择器。稍后将创建动态组件、初始化它并将其注入到 appHello 元素(ng-template)中。

步骤2:在指令的构造函数中初始化目标元素的视图组件。

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({
   selector: '[appHello]'
})
export class HelloDirective {
   constructor(public viewContainerRef: ViewContainerRef) { }
}

这里,

viewContainerRef 是目标元素(ng-template)的视图容器。它是通过构造函数注入创建的。ViewContainerRef 具有动态创建组件并将其追加到其中的方法。我们将在后面的步骤中学习它。

步骤3:创建一个接口,例如 Hello,以共享我们计划在运行时动态创建的组件集合的公共属性。

export interface Hello {
   name: string
}

这里,

  • 该接口将有助于对组件的共享信息进行分组。

步骤4:通过实现接口(Hello)创建任意数量的组件。

import { Component, Input } from '@angular/core';
import { Hello } from '../hello'
@Component({
   selector: 'app-my-hello',
   templateUrl: './my-hello.component.html',
   styleUrls: ['./my-hello.component.css']
})
export class MyHelloComponent implements Hello {
   @Input() name : string = 'Everyone';
}

这里,

  • MyHelloComponent 实现 Hello 接口

  • 它包含接口属性,name 作为其输入之一

  • my-hello.component.html 将包含组件的视图

步骤5:接下来,创建宿主组件,例如 MyHostComponent。宿主组件将负责动态创建子组件并将其放置在应用程序的适当位置。

@Component({
   // ...
})
export class MyHostComponent implements OnInit {
}

步骤6:使用步骤 1 中创建的指令(HelloDirective)选择器 appHello 在组件的模板中设置目标,如下所示:

<ng-template appHello></ng-template>

步骤7:接下来,开始在宿主组件中实现 OnInit 生命周期,如下所示:

@Component({
   // ...
})
export class MyHostComponent implements OnInit {
   ngOnInit() {
      // ...
   }
}

这里,

  • ngOnInit 生命周期钩子将用于创建动态组件并将动态创建的组件追加到指令的视图组件中

步骤8:使用 @ViewChild 装饰器和组件中的指令类型(HelloDirective)获取视图容器

export class MyHostComponent implements OnInit {
   // ...
   @ViewChild(HelloDirective, {static: true}) helloHost!: HelloDirective;
   // ...
}

这里,

@ViewChild 将获取具有 appHello 指令的特定 ng-template 元素并创建其局部变量。

步骤9:接下来,访问上一步中创建的目标/宿主元素 (helloHost) 的视图容器(类型为 ViewContainerRef)

ngOnInit() {
   // ...
   const viewContainerRef = this.helloHost.viewContainerRef;
   viewContainerRef.clear()
   // ...
}

这里,

  • 从步骤 2 中创建的指令属性中获取视图容器。

  • clear() 方法用于清除视图容器的现有内容。

步骤10:接下来,使用 viewContainerRef 对象及其方法创建实现 Hello 接口的任何组件,如下所示:

ngOnInit() {
   // ...
   const compRef = viewContainerRef.createComponent<Hello>(MyHelloComponent);
   // ...
}

这里,

  • createComponent() 是 ViewContainerRef 对象的方法,它接受组件的名称(类型)。它将在运行时创建指定的组件并返回对它的引用。

  • Hello 是公共接口

  • MyHelloCompnent 是要动态创建的组件

步骤11:接下来,从引用 (compRef) 对象获取动态创建的组件的实例并分配输入 (name),如下所示:

ngOnInit() {
   // ...
   compRef.instance.name = this.name;
}

ngOnInit 钩子的完整列表如下:

ngOnInit() {
   const viewContainerRef = this.helloHost.viewContainerRef;
   viewContainerRef.clear()
   
   const compRef = viewContainerRef.createComponent<Hello>(MyHelloComponent);
   compRef.instance.name = this.name;
}

这更像是伪代码。我们将在下一节中创建一个实时应用程序。

步骤12:接下来,我们可以将宿主组件用于任何其他组件(在应用程序/根组件的模板中)进行测试,如下所示:

<app-my-host-component name="Angular" />

在实际场景中,我们必须使用逻辑从符合指定接口(Hello)的现有组件列表中找到一个组件。这里,我们使用了一个静态组件(MyHelloComponent)来理解。在实际场景中,我们检查特定条件并从组件列表中加载符合 Hello 接口的相应组件。使用接口不是严格必要的,但它将帮助我们更好地理解代码并强制执行某些逻辑。

工作示例

让我们创建一个实时应用程序,以两种不同的格式(表格和画廊)显示员工集合。我们将创建两个组件,一个用于以表格形式显示员工,另一个用于以画廊形式显示员工。然后,我们将创建一个宿主组件,该组件将根据配置动态加载基于表格的组件或画廊组件。

让我们考虑以下员工信息列表以进行测试。

data = [
   {
      'name': 'John',
      'role': "Manager"
   },
   {
      'name': 'Peter',
      'role': "Marketing Intern"
   },
   {
      'name': 'Mary',
      'role': "Technical Intern"
   },
   {
      'name': 'Jack',
      'role': "Sales Manager"
   },
   {
      'name': 'Jessica',
      'role': "Delivery Head"
   },
]

现在,我们将按照以下步骤创建应用程序。

步骤1:创建一个接口来保存跨多个动态组件的公共数据

$ ng generate interface DynData
CREATE src/app/dyn-data.ts (29 bytes)

步骤2:创建一个指令,DynSample。指令的目的是在宿主组件模板中定位和选择视图容器(动态组件将放置在其中)。

$ ng generate directive DynSample
CREATE src/app/dyn-sample.directive.spec.ts (237 bytes)
CREATE src/app/dyn-sample.directive.ts (147 bytes)
UPDATE src/app/app.module.ts (2142 bytes)

步骤3:创建一个动态组件,DynList。组件的目的是将员工列为画廊。

$ ng generate component DynList
CREATE src/app/dyn-list/dyn-list.component.css (0 bytes)
CREATE src/app/dyn-list/dyn-list.component.html (23 bytes)
CREATE src/app/dyn-list/dyn-list.component.spec.ts (567 bytes)
CREATE src/app/dyn-list/dyn-list.component.ts (209 bytes)
UPDATE src/app/app.module.ts (2230 bytes)

步骤4:创建一个动态组件,DynTable。组件的目的是以表格格式列出员工。

$ ng generate component DynTable
CREATE src/app/dyn-table/dyn-table.component.css (0 bytes)
CREATE src/app/dyn-table/dyn-table.component.html (24 bytes)
CREATE src/app/dyn-table/dyn-table.component.spec.ts (574 bytes)
CREATE src/app/dyn-table/dyn-table.component.ts (213 bytes)
UPDATE src/app/app.module.ts (2322 bytes)

步骤5:创建一个宿主组件,DynHost。组件的目的是托管 DynList 和 DynTable 中的任何一个组件,具体取决于配置。

$ ng generate component DynHost
CREATE src/app/dyn-host/dyn-host.component.css (0 bytes)
CREATE src/app/dyn-host/dyn-host.component.html (23 bytes)
CREATE src/app/dyn-host/dyn-host.component.spec.ts (567 bytes)
CREATE src/app/dyn-host/dyn-host.component.ts (209 bytes)
UPDATE src/app/app.module.ts (2410 bytes)

步骤6:接下来,打开 DynSampleDirective 并开始更新代码。Angular CLI 生成的代码如下:

import { Directive } from '@angular/core';

@Directive({
   selector: '[appDynSample]'
})
export class DynSampleDirective {

   constructor() { }
}

这里,

  • 属性指令的目的是在宿主组件的模板中定位元素(ng-template),动态组件将被注入到该元素中。

  • appDynSample 是用于定位和选择元素(ng-template)的选择器。稍后将创建动态组件、初始化它并将其注入到 appHello 元素(ng-template)中。

步骤7:接下来,在指令中初始化视图组件引用对象(ViewComponentRef),如下所示:

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({
   selector: '[appDynSample]'
})
export class DynSampleDirective {

   constructor(public viewContainerRef: ViewContainerRef) { }

}

这里,

  • 从 @angular/core 模块导入 ViewContainerRef

  • 通过构造函数依赖注入初始化 viewContainerRef 对象。viewContainerRef 是宿主组件模板中目标元素(ng-template)的视图容器。ViewContainerRef 具有动态创建组件并将其追加到其中的方法。我们将在后面的宿主组件中使用它来动态创建 DynListComponent 和 DynTableComponent。

步骤8:接下来,打开接口 DynData 并添加一个数组属性 data

export interface DynData {
   data : any[]
}

步骤9:接下来,打开 DynListComponent 组件并实现 DynData 接口。

import { Component } from '@angular/core';

import { DynData } from '../dyn-data'

@Component({
   selector: 'app-dyn-list',
   templateUrl: './dyn-list.component.html',
   styleUrls: ['./dyn-list.component.css']
})
export class DynListComponent implements DynData {
   data: any[] = []
}

这里,

  • 导入 DynData 接口并在类定义中实现它

  • 根据 DynData 接口规范包含 data 属性

步骤10:接下来,打开组件的模板并将数据呈现为项目列表

<div class="gallery">
   <div *ngFor="let item of data" class="card">
      <div class="container">
         <h4><b>{{ item.name }}</b></h4>
         <p>{{ item.role }}</p>
      </div>
   </div>
</div>

这里,

  • data 保存具有两个属性(名称和角色)的员工列表

  • 使用 ngFor 将员工显示为卡片列表

步骤11:接下来,打开组件的样式并添加必要的 css,如下所示:

.gallery {
   display: flex;
   flex-wrap: wrap;
   justify-content: left;
   gap: 10px;
}
.card {
   flex-basis: 200px;
   box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
}
.card:hover {
   box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
.container {
   padding: 2px 16px;
   max-width: 200px;
}

这里,我们使用 CSS 的 flex 功能以画廊格式显示员工

步骤12:接下来,打开 DynTableComponent 组件并实现 DynData 接口。

import { Component } from '@angular/core';

import { DynData } from '../dyn-data'

@Component({
   selector: 'app-dyn-table',
   templateUrl: './dyn-table.component.html',
   styleUrls: ['./dyn-table.component.css']
})
export class DynTableComponent implements DynData {
   data: any[] = []
}

这里,

  • 导入 DynData 接口并在类定义中实现它

  • 根据 DynData 接口规范包含 data 属性

步骤13:接下来,打开组件的模板并将数据呈现为项目列表

<table class="employees">
   <thead>
      <tr>
         <th>Name</th>
         <th>Role</th>
      </tr>
   </thead>
   <tbody>
      <tr *ngFor="let item of data">
         <td>{{ item.name }}</td>
         <td>{{ item.role }}</td>
      </tr>
   </tbody>
</table>

这里,

  • data 保存具有两个属性(名称和角色)的员工列表

  • 使用 ngFor 将员工呈现为 html 表格中的行

步骤14:接下来,打开组件的样式并添加必要的 css,如下所示:

.employees {
   border-collapse: collapse;
   width: 400px;
}
.employees td, .employees th {
   padding: 8px;
}
.employees tbody tr:nth-child(even){background-color: #f2f2f2;}
.employees tbody tr:hover {background-color: #ddd;}
.employees thead th {
   padding-top: 12px;
   padding-bottom: 12px;
   text-align: left;
   background-color: brown;
   color: white;
}

步骤15:接下来,打开 DynHostComponent 组件的模板并包含 DynSampleDirective,如下所示:

<ng-templte appDynSample></ng-template>

这里,我们使用 DynSample 指令显示了 ng-template。

步骤16:接下来,打开 DynHostComponent 并导入必要的类

import { Component, ViewContainerRef, OnInit, ViewChild, Input } from '@angular/core';

步骤17:接下来,导入接口、列表组件、表格组件和指令

import { DynData } from '../dyn-data'
import { DynSampleDirective } from '../dyn-sample.directive'
import { DynListComponent } from '../dyn-list/dyn-list.component'
import { DynTableComponent } from '../dyn-table/dyn-table.component'

步骤18:在类声明中实现 OnInit 生命周期钩子

export class DynHostComponent implements OnInit {
}

步骤19:声明一个输入属性以从用户获取格式信息(表格/列表)。

@Input() format: string = 'list'

步骤20:声明一个属性 data 并设置示例数据

private data = [
   {
      'name': 'John',
      'role': "Manager"
   },
   {
      'name': 'Peter',
      'role': "Marketing Intern"
   },
   {
      'name': 'Mary',
      'role': "Technical Intern"
   },
   {
      'name': 'Jack',
      'role': "Sales Manager"
   },
   {
      'name': 'Jessica',
      'role': "Delivery Head"
   },
]

步骤21:声明宿主属性并使用 @ViewChild 和指令类型 DynSampleDirective 从模板获取 ng-template 视图组件

@ViewChild(DynSampleDirective, {static: true}) host!: DynSampleDirective;

步骤22:在 ngOnInit 生命周期钩子中执行动态创建组件并将其加载到 ng-template 中的实际实现

ngOnInit() {
   const viewContainerRef = this.host.viewContainerRef;
   viewContainerRef.clear()
   
   if(this.format == 'table') {
      const compRef = viewContainerRef.createComponent<DynData>(DynTableComponent);
      compRef.instance.data = this.data;
   } else {
      const compRef = viewContainerRef.createComponent<DynData>(DynListComponent);
      compRef.instance.data = this.data;
   }
}

步骤23:宿主组件的完整列表如下:

import { Component, ViewContainerRef, OnInit, ViewChild, Input } from '@angular/core';

import { DynData } from '../dyn-data'
import { DynSampleDirective } from '../dyn-sample.directive'
import { DynListComponent } from '../dyn-list/dyn-list.component'
import { DynTableComponent } from '../dyn-table/dyn-table.component'

@Component({
   selector: 'app-dyn-host',
   templateUrl: './dyn-host.component.html',
   styleUrls: ['./dyn-host.component.css']
})
export class DynHostComponent implements OnInit {
   @Input() format: string = 'table'
   
   private data = [
   {
      'name': 'John',
      'role': "Manager"
   },
   {
      'name': 'Peter',
      'role': "Marketing Intern"
   },
   {
      'name': 'Mary',
      'role': "Technical Intern"
   },
   {
      'name': 'Jack',
      'role': "Sales Manager"
   },
   {
      'name': 'Jessica',
      'role': "Delivery Head"
   },
   ]
   
   @ViewChild(DynSampleDirective, {static: true}) host!: DynSampleDirective;
   
   ngOnInit() {
      const viewContainerRef = this.host.viewContainerRef;
      viewContainerRef.clear()
      
      if(this.format == 'table') {
         const compRef = viewContainerRef.createComponent<DynData>(DynTableComponent);
         compRef.instance.data = this.data;
      } else {
         const compRef = viewContainerRef.createComponent<DynData>(DynListComponent);
         compRef.instance.data = this.data;
      }
   }
}

步骤24:接下来,打开 app 组件的模板并包含宿主组件。

<app-dyn-host format="table" />

这里,我们指示宿主组件以表格格式呈现员工数据。宿主组件将动态创建 DynTableComponent 并将其注入到宿主组件模板中。

步骤25:接下来,运行应用程序,您将看到员工数据以表格格式显示,如下所示:

employee data

步骤 26:接下来,打开应用组件的模板,并将格式更改为图库。

<app-dyn-host format="gallery" />

在这里,我们指示宿主组件以图库格式渲染员工数据。宿主组件将动态创建 DynListComponent 并注入到宿主组件模板中。

步骤 27:接下来,运行应用程序,您将看到员工数据以如下所示的图库格式显示 -

gallery format

结论

在本节中,我们学习了动态创建组件最重要的和最强大的概念。Angular 通过几个易于使用的 API 简化了生成动态组件的过程。它将帮助开发人员专注于应用程序的逻辑,而不是花费时间创建运行时组件。

广告