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

Angular新手求助:如何在Angular Material表格中渲染FormGroup并解决报错

Angular Material表格行过滤:FormGroup渲染报错修复

问题背景

我是Angular新手,想通过FormGroup在Angular Material表格里实现行数据过滤功能,更新模板后出现两个错误:

ERROR TypeError: control.registerOnChange is not a function
ERROR TypeError: this.ngControl.control.hasValidator is not a function

我的实现代码

组件TS代码

@Component({
  selector: 'app-grid-view',
  templateUrl: './grid-view.component.html',
  styleUrls: ['./grid-view.component.scss']
})
export class GridViewComponent implements OnInit, AfterViewInit {
  @Input() dataSource!: IndexDataSource;
  @Input() sortField: string = 'id'
  @Input() sortOrder: SortDirection = 'desc';
  @Input() tableColumns: Array<IColumn> = [];
  @Input() model = '';

  // Handle pagination property
  @ViewChild(MatPaginator, {static: false}) paginator!: MatPaginator;

  // Handle sorting property
  @ViewChild(MatSort, {static: false}) sort!: MatSort;

  // Handle column that will be displayed
  displayedColumns: Array<string> = [];

  // For handle row search by each column
  displayedColumnsSearch: Array<string> = [];
  fieldsOfFormSearch: Array<any> = [];

  // For filters, accommodate the filter inputs
  filterValues: { [key: string]: string } = {};
  filterString!: string;

  // FormGroup to hold filter form controls
  formGroup: FormGroup = new FormGroup({});
  @Output() filterChange = new EventEmitter<{ [key: string]: string }>();

  ngOnInit(): void {

    // => Init rows based on param url
    let paramMap = this.dataSource.activatedRoute.snapshot.queryParamMap;
    this.dataSource.loadData(
      Number(paramMap.get('page')) || 1,
      Number(paramMap.get('per-page')) || 10,
      paramMap.get('sort') || this.sortField,
      paramMap.get('sort')?.startsWith("-") ? 'asc' : this.sortOrder,
      paramMap.get('filter') || this.filterString
    );

    //  => Column that will display in table
    this.displayedColumns = this.tableColumns.map((column) => column.columnDef);

    // => Form element that will be displayed in second row
    this.fieldsOfFormSearch = this.tableColumns.map((column) => ({
      status: column.isFilterable !== undefined,
      formControl: column.columnDef + 'Search',
      urlSearch: this.model + `[${column.columnDef}]`,
    }));

    // => Create form controls for filter inputs and add them to the FormGroup
    this.fieldsOfFormSearch
      .filter((field) => field.status)
      .forEach((field) => {

        let formControl = new FormControl();
        this.formGroup.addControl(field.urlSearch, formControl);

        formControl.valueChanges
          .pipe(debounceTime(300), distinctUntilChanged())
          .subscribe((value: any) => {
            this.filterValues[field.urlSearch] = value;
            this.applyFilters();
          });
      });

    // Column that will be display in table
    this.displayedColumnsSearch = this.fieldsOfFormSearch.map((c) => c.urlSearch);

    // Debug the form control
    console.log(this.formGroup);

  }

  applyFilters() {
    this.filterString = Object.keys(this.filterValues)
      .map((key) => `${key}=${this.filterValues[key]}`)
      .join('&');
    this.dataSource.applyFilters(this.filterString);
    this.filterChange.emit(this.filterValues);
  }

}

初始HTML模板代码

<!-- Actual Data -->
<ng-container *ngFor="let column of tableColumns;"
              matColumnDef="{{column.columnDef}}">
  <th mat-header-cell *matHeaderCellDef>{{column.header}}</th>
  <td mat-cell *matCellDef="let element">{{column.cell(element)}}</td>
</ng-container>

<!--  Filter Data  -->
<ng-container *ngFor="let field of fieldsOfFormSearch"
              matColumnDef="{{field.urlSearch}}"
>
  <th mat-header-cell *matHeaderCellDef>
    <span *ngIf="!field.status; else displayField"></span>
    <ng-template #displayField>
      <!--          Filtering here-->
      A form control here how to render it ?
      
    </ng-template>
  </th>
</ng-container>


<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matHeaderRowDef="displayedColumnsSearch"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>

更新后的HTML模板代码

<ng-container *ngFor="let field of fieldsOfFormSearch"
              matColumnDef="{{field.urlSearch}}"
>
  <th mat-header-cell *matHeaderCellDef>
    <span *ngIf="!field.status; else displayField"></span>
    <ng-template #displayField>
      <!--          Filtering here-->
      <mat-form-field appearance="fill">
        <input
          matInput
          [formControl]="field.formControl"
          (input)="applyFilters()"
        >
      </mat-form-field>
    </ng-template>
  </th>
</ng-container>

报错原因

你在模板里绑定的[formControl]="field.formControl"是个字符串(比如column.columnDef + 'Search'),但formControl指令要求接收FormControl实例,不是字符串,这就导致Angular无法识别控件,抛出错误。

修复方案

1. 修正TS代码:给field绑定FormControl实例

修改fieldsOfFormSearch的初始化和控件创建逻辑,直接把FormControl实例赋值给field.formControl:

// => Form element that will be displayed in second row
this.fieldsOfFormSearch = this.tableColumns.map((column) => ({
  status: column.isFilterable !== undefined,
  formControl: null, // 先占位,后续赋值实例
  urlSearch: this.model + `[${column.columnDef}]`,
}));

// => Create form controls for filter inputs and add them to the FormGroup
this.fieldsOfFormSearch
  .filter((field) => field.status)
  .forEach((field) => {
    let formControl = new FormControl();
    field.formControl = formControl; // 把实例绑定到field的formControl属性
    this.formGroup.addControl(field.urlSearch, formControl);

    formControl.valueChanges
      .pipe(debounceTime(300), distinctUntilChanged())
      .subscribe((value: any) => {
        this.filterValues[field.urlSearch] = value;
        this.applyFilters();
      });
  });

2. 修正模板:移除冗余事件,绑定正确的控件

已经通过valueChanges监听值变化,不需要再绑定(input)事件,模板修改为:

<ng-container *ngFor="let field of fieldsOfFormSearch"
              matColumnDef="{{field.urlSearch}}"
>
  <th mat-header-cell *matHeaderCellDef>
    <span *ngIf="!field.status; else displayField"></span>
    <ng-template #displayField>
      <mat-form-field appearance="fill">
        <input
          matInput
          [formControl]="field.formControl"
        >
      </mat-form-field>
    </ng-template>
  </th>
</ng-container>

3. 可选优化:用FormBuilder简化FormGroup创建

注入FormBuilder可以让代码更简洁,在组件里做如下修改:

import { FormBuilder, FormGroup, FormControl } from '@angular/forms';

// 组件构造函数
constructor(private fb: FormBuilder) {
  this.formGroup = this.fb.group({});
}

额外优化建议

  • 记得在组件销毁时取消订阅,避免内存泄漏:
    import { Subscription } from 'rxjs';
    import { OnInit, AfterViewInit, OnDestroy } from '@angular/core';
    
    // 实现OnDestroy接口
    export class GridViewComponent implements OnInit, AfterViewInit, OnDestroy {
      private subscriptions = new Subscription();
    
      // 在创建订阅时添加到集合
      this.subscriptions.add(
        formControl.valueChanges
          .pipe(debounceTime(300), distinctUntilChanged())
          .subscribe((value: any) => {
            this.filterValues[field.urlSearch] = value;
            this.applyFilters();
          })
      );
    
      // 组件销毁时取消所有订阅
      ngOnDestroy(): void {
        this.subscriptions.unsubscribe();
      }
    }
    
  • 确保tableColumns中的isFilterable属性设置正确,避免创建不必要的过滤控件。

内容的提问来源于stack exchange,提问作者Fadly Dzil

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 00:14:57