Angular自定义Select组件表单验证的正确实现方法问询
自定义Select组件表单验证问题
我实现了一个基于ControlValueAccessor的自定义select组件,代码和模板如下。使用时遇到两个验证问题:
- 给组件设置
required属性后,未选择选项时内部原生select会被标记为invalid,但自定义组件本身在表单中显示为valid; - 通过表单控件的
control.setErrors()方法标记组件为invalid时,内部的原生select仍保持valid状态。
请问需要补充哪些逻辑才能正确实现该组件的表单验证?
组件代码
import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ExtendedSelectOption } from '@app/core/models/common/common.model'; import { Is } from '@app/shared/utils/it'; @Component({ selector: 'app-custom-select', templateUrl: './custom-select.component.html', styleUrls: ['./custom-select.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomSelectComponent), multi: true, }, ], }) export class CustomSelectComponent implements ControlValueAccessor { @Input() inputId: string; @Input() set ngModel(model: unknown) { // only update ngModel and selected option on changed value if (this.selectedOption?.value !== model && !( Is.nullOrUndefined(this.selectedOption) && Is.nullOrUndefined(model))) { this.selectedOption = this.options.find(option => option.value === model) || null; this._ngModel = model; } } get ngModel(): unknown { return this._ngModel; } @Input() options: ExtendedSelectOption[] = []; @Input() inputClass: string; @Input() disabled: boolean = false; @Input() required: boolean = false; @Input() showLabel: boolean = false; @Input() labelText: string = null; @Input() inputName: string = null; @Output() triggerFocus = new EventEmitter<FocusEvent>(); @Output() triggerClick = new EventEmitter<MouseEvent>(); @Output() triggerBlur = new EventEmitter<FocusEvent>(); @Output() ngModelChange = new EventEmitter<unknown>(); private _ngModel: unknown = null; selectedOption: ExtendedSelectOption = null; onOptionChange(option: ExtendedSelectOption) { this._ngModel = option.value; this.ngModelChange.emit(this._ngModel); } // this fulfills NG_VALUE_ACCESSOR interface onChange: (value: unknown) => void = (value: unknown) => {}; onTouched: () => void = () => {}; writeValue(value: boolean): void { this.ngModel = value; } registerOnChange(fn: (value: unknown) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; } }
模板代码
<mat-icon class="inactive_select_icon" [title]="'Inactive ' + inputName" *ngIf="selectedOption?.disabled" >info_outline</mat-icon > <select [attr.id]="inputId || null" [class]="inputClass" [class.inactive-select]="selectedOption?.disabled" [disabled]="disabled" [required]="required" [name]="inputName" (click)="triggerClick.emit()" (focus)="triggerFocus.emit()" (blur)="triggerBlur.emit()" [(ngModel)]="selectedOption" (ngModelChange)="onOptionChange($event);"> <option *ngFor="let option of options" [ngValue]="option" [disabled]="option.disabled"> {{option.label}} </option> </select> <label *ngIf="showLabel" class="u-ml--tiny" [for]="inputId || null">{{ labelText }}</label>
解决方案
要让自定义组件和内部原生select的验证状态完全同步,需要补充验证器集成和状态双向同步的逻辑,具体步骤如下:
1. 实现Validator接口并注册验证器提供商
自定义组件需要实现Angular表单的Validator接口,同时在组件提供商中注册NG_VALIDATORS,这样表单系统才能识别组件的验证规则和状态。
修改组件代码:
// 新增导入 import { Validator, AbstractControl, NG_VALIDATORS, NgControl } from '@angular/forms'; import { Inject, Optional } from '@angular/core'; // 组件类实现Validator接口 export class CustomSelectComponent implements ControlValueAccessor, Validator { // 注入NgControl,用于关联外部表单控件状态 constructor(@Optional() @Inject(NgControl) public ngControl: NgControl) { if (this.ngControl) { this.ngControl.valueAccessor = this; } } // ... 原有代码 // 实现Validator接口的验证方法 validate(control: AbstractControl): { [key: string]: any } | null { // 处理required验证规则 if (this.required && (this._ngModel === null || this._ngModel === undefined)) { return { required: true }; } // 可添加其他自定义验证逻辑 return null; } }
更新组件提供商:
providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomSelectComponent), multi: true, }, // 新增验证器提供商 { provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomSelectComponent), multi: true, }, ],
2. 同步内部原生select的验证状态
将外部表单控件的验证状态(比如invalid、touched)绑定到内部原生select上,确保视觉状态一致。
修改模板中的select元素:
<select [attr.id]="inputId || null" [class]="inputClass" [class.inactive-select]="selectedOption?.disabled" <!-- 绑定错误样式类 --> [class.is-invalid]="ngControl?.invalid && ngControl?.touched" <!-- 绑定无障碍属性 --> [attr.aria-invalid]="ngControl?.invalid && ngControl?.touched" [disabled]="disabled" [required]="required" [name]="inputName" (click)="triggerClick.emit()" (focus)="triggerFocus.emit()" <!-- 触发onTouched,标记控件为已触碰 --> (blur)="triggerBlur.emit(); onTouched()" [(ngModel)]="selectedOption" (ngModelChange)="onOptionChange($event);" >
3. 修正onChange和onTouched的调用逻辑
原组件中没有正确调用ControlValueAccessor的onChange方法,导致表单控件无法感知值的变化;同时blur事件没有触发onTouched,导致验证状态不会在失去焦点后更新。
修改onOptionChange方法:
onOptionChange(option: ExtendedSelectOption) { this._ngModel = option?.value; this.ngModelChange.emit(this._ngModel); // 同步值到外部表单控件 this.onChange(this._ngModel); }
4. 处理初始值的验证同步
在writeValue方法中,确保初始值设置后触发验证状态更新:
writeValue(value: unknown): void { this.ngModel = value; // 手动触发外部控件的验证更新 if (this.ngControl?.control) { this.ngControl.control.markAsDirty(); this.ngControl.control.updateValueAndValidity(); } }
内容的提问来源于stack exchange,提问作者karlitos
相关产品推荐
相关产品推荐

