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

Angular自定义下拉搜索Value Accessor设置FormControl值不选中问题

解决Angular Material自定义下拉搜索控件无法选中初始值的问题

核心问题分析

  1. 值同步不完整:父组件设置FormControl值时,writeValue仅更新了selectedValue,没把对应选项的文本同步到输入框,也没关联MatAutocomplete的选中状态。
  2. 值传递不统一:模板里mat-option的[value]绑的是选项文本(option.Value),但选择时传的是option.Id,父组件设置的也是Id,导致值类型不匹配,无法关联正确选项。
  3. FormControl与ngModel混用冲突:输入框同时绑定[formControl]和[(ngModel)],Angular中这两种绑定不能同时用,会导致值同步混乱。
  4. ngDoCheck操作不当:频繁触发的ngDoCheck里直接修改searchText,可能覆盖初始值的同步逻辑。

具体修复步骤

1. 完善writeValue的同步逻辑

当父组件传入值时,不仅要更新selectedValue,还要匹配对应的选项文本,同步到输入框和内部FormControl:

writeValue(value: any): void {
  this.selectedValue = value;
  // 根据Id找到对应选项,同步文本到输入框
  const selectedItem = this.itemSource.find(item => item.Id === value);
  if (selectedItem) {
    this.searchText = selectedItem.Value;
    this.searchTextSubject.next(selectedItem.Value);
    // 同步到内部FormControl,避免事件循环
    if (this.formControl) {
      this.formControl.setValue(selectedItem.Value, { emitEvent: false });
    }
  } else {
    this.searchText = '';
    this.searchTextSubject.next('');
    if (this.formControl) {
      this.formControl.setValue('', { emitEvent: false });
    }
  }
}

2. 修正模板的绑定逻辑

把mat-option的[value]改为绑定option.Id,添加displayWith函数处理选中值的显示,同时移除ngModel避免冲突:

<mat-form-field appearance="outline" class="form-field">
  <mat-label>Employee</mat-label>
  <input
    matInput
    [placeholder]="'Search and Select...'"
    [formControl]="formControl"
    [value]="searchText"
    [matAutocomplete]="auto"
    [class.invalid]="formControl.invalid && formControl.touched"
    (input)="onSearchTextChange($event.target.value)"
  />
  <mat-autocomplete #auto="matAutocomplete" [displayWith]="displayWith">
    <mat-option
      *ngFor="let option of filteredOptions$ | async"
      [value]="option.Id"
      (click)="selectOption(option)"
    >
      {{ option.Value }}
    </mat-option>
  </mat-autocomplete>
  <mat-error *ngIf="formControl.hasError('required')">Required</mat-error>
</mat-form-field>

3. 添加displayWith函数

在组件中定义displayWith,让MatAutocomplete根据选中的Id显示对应的文本:

displayWith = (id: number) => {
  const item = this.itemSource.find(item => item.Id === id);
  return item ? item.Value : '';
}

4. 调整selectOption方法

确保选中选项时同步更新所有状态:

selectOption(option: any): void {
  this.selectedValue = option.Id;
  this.searchText = option.Value;
  this.onChange(option.Id);
  this.onTouched();
  this.autocomplete.closePanel();
  if (this.formControl) {
    this.formControl.setValue(option.Value, { emitEvent: false });
  }
}

5. 优化ngDoCheck逻辑

避免频繁修改输入值,只在选中值为null时清空文本:

ngDoCheck(): void {
  if (this.selectedValue == null && this.searchText !== '') {
    this.searchText = '';
    this.searchTextSubject.next('');
  }
}

6. 同步初始值到内部FormControl

在setFormControl方法中,同步父组件的初始值到内部控件:

setFormControl() {
  const ngControl = this.injector.get(NgControl);
  if (ngControl instanceof FormControlName) {
    this.formControl = this.injector
      .get(FormGroupDirective)
      .getControl(ngControl);
  } else {
    this.formControl = (ngControl as FormControlDirective)
      .form as FormControl;
  }
  // 同步父组件的初始值
  if (this.formControl.value != null) {
    this.writeValue(this.formControl.value);
  }
}

修复后关键代码片段(组件)

@Component({
  selector: 'app-dropdown-search',
  templateUrl: './dropdown-search.component.html',
  styleUrls: ['./dropdown-search.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: DropdownSearchComponent,
      multi: true,
    },
  ],
})
export class DropdownSearchComponent
  implements OnInit, ControlValueAccessor, DoCheck
{
  @Input() itemSource: any[] = [
    { Id: 1, Value: 'Tom Cruise' },
    { Id: 2, Value: 'Maria Sharapova' },
    { Id: 3, Value: 'James Bond' },
  ];

  selectedValue: any;
  searchText: string = '';
  isDisabled: boolean = false;
  onChange: any = () => {};
  onTouched: any = () => {};

  @ViewChild(MatAutocompleteTrigger) autocomplete!: MatAutocompleteTrigger;

  filteredOptions$!: Observable<any[]>;
  private searchTextSubject = new BehaviorSubject<string>('');

  formControl!: FormControl;

  constructor(
    private injector: Injector,
    private errorStateMatcher: ErrorStateMatcher
  ) {}

  ngOnInit(): void {
    this.setFormControl();
    this.filteredOptions$ = this.searchTextSubject.pipe(
      debounceTime(100),
      distinctUntilChanged(),
      map((text) => this.filterOptions(text))
    );
  }

  ngDoCheck(): void {
    if (this.selectedValue == null && this.searchText !== '') {
      this.searchText = '';
      this.searchTextSubject.next('');
    }
  }

  setFormControl() {
    const ngControl = this.injector.get(NgControl);
    if (ngControl instanceof FormControlName) {
      this.formControl = this.injector
        .get(FormGroupDirective)
        .getControl(ngControl);
    } else {
      this.formControl = (ngControl as FormControlDirective)
        .form as FormControl;
    }
    if (this.formControl.value != null) {
      this.writeValue(this.formControl.value);
    }
  }

  writeValue(value: any): void {
    this.selectedValue = value;
    const selectedItem = this.itemSource.find(item => item.Id === value);
    if (selectedItem) {
      this.searchText = selectedItem.Value;
      this.searchTextSubject.next(selectedItem.Value);
      if (this.formControl) {
        this.formControl.setValue(selectedItem.Value, { emitEvent: false });
      }
    } else {
      this.searchText = '';
      this.searchTextSubject.next('');
      if (this.formControl) {
        this.formControl.setValue('', { emitEvent: false });
      }
    }
  }

  selectOption(option: any): void {
    this.selectedValue = option.Id;
    this.searchText = option.Value;
    this.onChange(option.Id);
    this.onTouched();
    this.autocomplete.closePanel();
    if (this.formControl) {
      this.formControl.setValue(option.Value, { emitEvent: false });
    }
  }

  onSearchTextChange(text: string): void {
    this.searchText = text;
    this.searchTextSubject.next(text);
  }

  filterOptions(text: string): any[] {
    if (text.trim() === '') {
      return [];
    }
    return this.itemSource.filter((item) =>
      item.Value.toLowerCase().includes(text.toLowerCase())
    );
  }

  displayWith = (id: number) => {
    const item = this.itemSource.find(item => item.Id === id);
    return item ? item.Value : '';
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 11:34:57