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

Angular Material可复用Autocomplete绑定FormControl返回null问题排查

问题描述

我实现了一个基于Angular Material的可复用Autocomplete组件,数据源来自服务器返回的对象数据。按以下方式集成时:

<app-positions-input [formControl]="form.controls.position"></app-positions-input>

父组件的form.controls.position始终返回null。但改用如下方式集成:

<app-positions-input #positionComponent></app-positions-input>

通过@ViewChild获取的positionComponent.control.value能正常返回选中值,可这种方式无法将组件纳入响应式表单体系,没法做表单验证。

组件代码如下:

TypeScript代码

import { Component, EventEmitter, OnDestroy, OnInit, Output, forwardRef } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { PositionService } from '../position.service';
import { MaterialModule } from '@myapp/client/material';
import { AsyncPipe } from '@angular/common';
import { PositionGetDto as GetDto } from '@myapp/dtos';

@Component({
  selector: 'app-positions-input',
  standalone: true,
  imports: [ReactiveFormsModule, MaterialModule, AsyncPipe],
  templateUrl: './positions.component.html',
  styleUrl: './positions.component.scss',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => PositionsComponent),
      multi: true,
    }
  ]
})

export class PositionsComponent implements OnInit, OnDestroy, ControlValueAccessor {
  @Output() selected = new EventEmitter<GetDto>();

  control = new FormControl<GetDto | undefined>(undefined);
  items: GetDto[] = [];
  filteredItems: Observable<GetDto[]>;

  private ngUnsubscribe = new Subject<void>();

  private onChangeCallback: (_: GetDto | undefined) => void = () => {};
  private onTouchedCallback: () => void = () => {};

  constructor(private service: PositionService) {
    this.filteredItems = this.control.valueChanges
      .pipe(
        // tap((term) => { console.log("Term: %s", term); }),
        startWith(''),
        map(item => (item ? this._filter(item) : this.items.slice()))
      );
  }

  ngOnInit() {
    this.service.getAll().subscribe((items) => {
      this.items = items;
    });
  }

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }
  
  writeValue(item: GetDto | undefined): void {
    this.control.setValue(item);
  }

  registerOnChange(fn: any): void {
    this.onChangeCallback = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouchedCallback = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    //this.disabled = isDisabled;
  }

  private _filter(term: any): GetDto[] {
    let filteredItems: GetDto[];

    if (typeof term === 'object' && 'id' in term && 'name' in term) {
      filteredItems = this.items;
      return filteredItems;
    } else if (typeof term === 'string') {
      const lowerCaseTerm = term.toLowerCase();
      filteredItems = this.items.filter((item) =>
        item.name.toLowerCase().includes(lowerCaseTerm)
      );
      return filteredItems;
    }
    return this.items;
  }

  display(item: GetDto): string {
    return item ? item.name : '';
  }

  setValue(item: GetDto | undefined) {
    this.selected.emit(item);
    this.control.setValue(item);
  }
}

HTML模板

<mat-form-field>
  <mat-label>Position: </mat-label>
  <input matInput aria-label="Position" placeholder="Please put a position" [formControl]="control"
    [matAutocomplete]="auto">
  <mat-autocomplete #auto="matAutocomplete" [displayWith]="display">
    @for (item of filteredItems | async; track item) {
    <mat-option [value]="item">{{item.name}}</mat-option>
    }
  </mat-autocomplete>
</mat-form-field>

想请教下我哪里实现有误?我没找到合适的可复用Autocomplete示例。


问题原因与修复方案

核心问题是未正确实现ControlValueAccessor的双向同步逻辑——内部表单控件的值变化时,没有通知外部绑定的父表单控件,导致父控件无法感知选中值的变化。

具体错误点

  1. 内部control的值发生变化时,未调用onChangeCallback(ControlValueAccessor要求的外部同步入口)
  2. setValue方法更新内部控件值后,未触发onChangeCallback
  3. 缺少控件touched状态的同步,未在输入框失焦时调用onTouchedCallback
  4. writeValue方法未避免循环触发值变化事件
  5. setDisabledState方法未实现禁用状态同步

修复步骤

1. 同步内部值变化到外部表单

在构造函数中,订阅内部控件的valueChanges,触发外部同步:

constructor(private service: PositionService) {
  this.filteredItems = this.control.valueChanges
    .pipe(
      startWith(''),
      map(item => (item ? this._filter(item) : this.items.slice()))
    );

  // 新增:同步内部值变化到外部表单
  this.control.valueChanges.subscribe(value => {
    this.onChangeCallback(value);
  });
}

2. 完善setValue方法

更新内部控件后,同步值到外部:

setValue(item: GetDto | undefined) {
  this.selected.emit(item);
  this.control.setValue(item);
  this.onChangeCallback(item); // 新增
}

3. 同步touched状态

在模板输入框添加blur事件,标记控件为已触碰:

<input matInput aria-label="Position" placeholder="Please put a position" [formControl]="control"
  [matAutocomplete]="auto" (blur)="onTouchedCallback()">

4. 优化writeValue方法

设置内部控件时禁用事件触发,避免循环同步:

writeValue(item: GetDto | undefined): void {
  this.control.setValue(item, { emitEvent: false });
}

5. 实现禁用状态同步

完善setDisabledState方法,同步外部表单的禁用状态:

setDisabledState(isDisabled: boolean): void {
  isDisabled ? this.control.disable() : this.control.enable();
}

修复后逻辑说明

  • 外部表单传值时,内部控件更新且不会触发值变化事件,避免循环
  • 内部控件值变化(用户选择或输入)时,自动同步到外部表单控件
  • 输入框失焦时标记外部控件为touched,满足表单验证的触发条件
  • 禁用状态双向同步,确保组件与外部表单状态一致

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 10:47:10