Angular带泛型与内容投影的抽象组件声明及模板依赖问题
Angular 抽象组件复用方案及问题解决
场景说明
提取应用中多处复用的代码逻辑,针对不同数据类型及子组件实现相同业务流程。例如客户、发票等页面结构一致,通过抽象组件封装通用方法(如抽象search())与属性(如搜索结果),部分方法允许重写,模板中通过内容投影注入搜索栏、结果表格等自定义内容。
抽象组件代码
export abstract class AbstractListEditContainerComponent<T, U extends SearchFilters> { protected readonly service?: AbstractListEditService<T, U>; protected filters: U; results: T[] = []; resultsCount: number = 0; protected constructor(... ) {} protected abstract initialSearchFilters(): U; abstract create(): Promise<void>; abstract find($event: U): void; }
模板代码
<div class="container"> <div class="header"> <div class="page-title"> {{pageTitle}} </div> <div> <button mat-button mat-raised-button color="primary" class="submit-button" (click)="create()" type="button"> <mat-icon fontIcon="add"></mat-icon> {{createButtonTitle}} </button> </div> </div> <ng-content select=".search-bar"></ng-content> <ng-content select=".list-headers"></ng-content> <ng-content select=".list-items"></ng-content> </div>
当前遇到的问题
- 抽象组件无法在模块中声明
- 因未在模块中声明,Angular无法识别模板中的依赖(如
mat-button、mat-icon),添加CUSTOM_ELEMENTS_SCHEMA也无效
解决方案
方案1:基类组件+具象包装组件(最常用)
抽象类仅作为逻辑基类,不关联模板;创建具象通用组件承载模板并声明在模块中,业务组件继承抽象基类后通过内容投影使用通用组件。
- 拆分抽象基类(仅存逻辑)
export abstract class AbstractListEditBase<T, U extends SearchFilters> { protected readonly service?: AbstractListEditService<T, U>; protected filters: U; results: T[] = []; resultsCount: number = 0; protected constructor(... ) {} protected abstract initialSearchFilters(): U; abstract create(): Promise<void>; abstract find($event: U): void; }
- 创建具象通用组件(关联模板)
import { Component, Input } from '@angular/core'; import { AbstractListEditBase } from './abstract-list-edit.base'; import { SearchFilters } from './search-filters.interface'; @Component({ selector: 'app-list-edit-container', templateUrl: './list-edit-container.component.html', // 即你提供的模板文件 styleUrls: ['./list-edit-container.component.css'] }) export class ListEditContainerComponent<T, U extends SearchFilters> extends AbstractListEditBase<T, U> { @Input() pageTitle!: string; @Input() createButtonTitle!: string; // 若基类构造函数有依赖,需在此传递参数 constructor() { super(); } }
- 模块中声明组件并导入依赖
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ListEditContainerComponent } from './list-edit-container.component'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @NgModule({ declarations: [ListEditContainerComponent], imports: [CommonModule, MatButtonModule, MatIconModule], // 导入Material依赖模块 exports: [ListEditContainerComponent] // 导出供业务组件使用 }) export class ListEditModule { }
- 业务组件使用示例
import { Component } from '@angular/core'; import { AbstractListEditBase } from '../abstract-list-edit.base'; import { Customer } from './customer.model'; import { CustomerFilters } from './customer-filters.model'; @Component({ selector: 'app-customer-list', template: ` <app-list-edit-container [pageTitle]="'客户列表'" [createButtonTitle]="'新增客户'"> <!-- 投影自定义搜索栏 --> <div class="search-bar"> <input [(ngModel)]="filters.name" placeholder="搜索客户名称"> <button (click)="find(filters)">搜索</button> </div> <!-- 投影自定义列表表头 --> <div class="list-headers"> <th>客户ID</th> <th>客户名称</th> </div> <!-- 投影自定义列表内容 --> <div class="list-items"> <tr *ngFor="let item of results"> <td>{{item.id}}</td> <td>{{item.name}}</td> </tr> </div> </app-list-edit-container> ` }) export class CustomerListComponent extends AbstractListEditBase<Customer, CustomerFilters> { // 实现抽象方法 protected initialSearchFilters(): CustomerFilters { return { name: '' }; } async create(): Promise<void> { // 新增客户业务逻辑 } find($event: CustomerFilters): void { // 搜索客户业务逻辑 this.results = []; // 替换为实际接口请求结果 } }
方案2:指令+模板组合(无继承场景)
若不需要类继承,可通过结构型指令封装通用逻辑,配合独立模板组件复用UI结构。
- 创建结构型指令封装逻辑
import { Directive, Input, OnInit } from '@angular/core'; import { SearchFilters } from './search-filters.interface'; @Directive({ selector: '[appListEdit]' }) export class ListEditDirective<T, U extends SearchFilters> implements OnInit { @Input() appListEditResults!: T[]; @Input() appListEditResultsCount!: number; @Input() appListEditInitialFilters!: () => U; results: T[] = []; resultsCount: number = 0; protected filters!: U; ngOnInit(): void { this.filters = this.appListEditInitialFilters(); this.results = this.appListEditResults; this.resultsCount = this.appListEditResultsCount; } // 封装通用方法,如重置筛选 resetFilters(): void { this.filters = this.appListEditInitialFilters(); } }
- 创建独立模板组件
import { Component, Input } from '@angular/core'; @Component({ selector: 'app-list-edit-template', templateUrl: './list-edit-template.component.html' // 即你提供的模板文件 }) export class ListEditTemplateComponent { @Input() pageTitle!: string; @Input() createButtonTitle!: string; @Input() onCreate!: () => Promise<void>; }
- 模块中声明指令与模板
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ListEditDirective } from './list-edit.directive'; import { ListEditTemplateComponent } from './list-edit-template.component'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @NgModule({ declarations: [ListEditDirective, ListEditTemplateComponent], imports: [CommonModule, MatButtonModule, MatIconModule], exports: [ListEditDirective, ListEditTemplateComponent] }) export class ListEditModule { }
- 业务组件使用示例
import { Component } from '@angular/core'; import { Customer } from './customer.model'; import { CustomerFilters } from './customer-filters.model'; @Component({ selector: 'app-customer-list', template: ` <app-list-edit-template [pageTitle]="'客户列表'" [createButtonTitle]="'新增客户'" [onCreate]="create"> <div class="search-bar"> <input [(ngModel)]="filters.name" placeholder="搜索客户名称"> <button (click)="find(filters)">搜索</button> </div> <div class="list-headers"> <th>客户ID</th> <th>客户名称</th> </div> <div class="list-items" *appListEdit=" results: customerResults; resultsCount: customerCount; initialFilters: getInitialFilters "> <tr *ngFor="let item of customerResults"> <td>{{item.id}}</td> <td>{{item.name}}</td> </tr> </div> </app-list-edit-template> ` }) export class CustomerListComponent { customerResults: Customer[] = []; customerCount = 0; filters!: CustomerFilters; getInitialFilters(): CustomerFilters { return { name: '' }; } async create(): Promise<void> { // 新增客户逻辑 } find(filters: CustomerFilters): void { // 搜索客户逻辑 this.customerResults = []; } }
关键说明
- 抽象类无法被Angular实例化,因此不能直接声明为组件,必须通过具象组件承载模板
- 模板中使用的第三方组件(如Material的
mat-button)必须在模块中导入对应模块,CUSTOM_ELEMENTS_SCHEMA仅用于非Angular自定义元素,对Angular组件无效
内容的提问来源于stack exchange,提问作者Stephane David
相关产品推荐
相关产品推荐

