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的双向同步逻辑——内部表单控件的值变化时,没有通知外部绑定的父表单控件,导致父控件无法感知选中值的变化。
具体错误点
- 内部
control的值发生变化时,未调用onChangeCallback(ControlValueAccessor要求的外部同步入口) setValue方法更新内部控件值后,未触发onChangeCallback- 缺少控件
touched状态的同步,未在输入框失焦时调用onTouchedCallback writeValue方法未避免循环触发值变化事件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
相关产品推荐
相关产品推荐

