You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:基类组件+具象包装组件(最常用)

抽象类仅作为逻辑基类,不关联模板;创建具象通用组件承载模板并声明在模块中,业务组件继承抽象基类后通过内容投影使用通用组件。

  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;
}
  1. 创建具象通用组件(关联模板)
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();
  }
}
  1. 模块中声明组件并导入依赖
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 { }
  1. 业务组件使用示例
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结构。

  1. 创建结构型指令封装逻辑
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();
  }
}
  1. 创建独立模板组件
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>;
}
  1. 模块中声明指令与模板
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 { }
  1. 业务组件使用示例
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 15:07:53