在使用自定义输入组件时,我遇到了一些奇怪的行为。
首先,我建立了一个简单的抽象类,它具有组件的主要“功能”和方法,然后是具有很少代码的输入组件:
// Abstract class
export abstract class BaseFormInput<T> implements ControlValueAccessor, Validator, AfterViewInit, OnDestroy {
@Input() label: string
@Output() onChange: EventEmitter<T> = new EventEmitter<T>()
private changeInternal: (obj: T) => void
private changeSub: Subscription
private disabled$ = new BehaviorSubject(false)
private required$ = new BehaviorSubject(false)
public input = new FormControl(null)
ngOnDestroy() {
this.changeSub.unsubscribe()
}
ngAfterViewInit() {
this.changeSub = this.input.valueChanges.subscribe(v => {
if (!this.disabled$.getValue()) {
this.onChange.emit(v)
this.changeInternal(v)
}
})
}
writeValue = (obj: T) => this.input.setValue(obj)
registerOnChange = (fn: (obj: T) => void) => this.changeInternal = fn
registerOnTouched = (_fn: (obj: any) => void) => {}
setDisabledState = (isDisabled: boolean) => this.disabled$.next(isDisabled)
validate(control: AbstractControl): ValidationErrors {
this.required$.next(control.hasValidator(Validators.required))
// THIS LINE HAS WEIRD BEHAVIOR
console.log(control, control.errors)
return null
}
public get isDisabled$(){
return this.disabled$.asObservable()
}
public get isRequired$(){
return this.required$.asObservable()
}
}
输入组件简单设计如下:
@Component({
selector: "ec-input-text",
template: `<div class="form-control">
<label *ngIf="label">
{{ label }}
<span *ngIf="isRequired$ | async">*</span>
</label>
<input *ngIf="type !== 'textarea'" [type]="type" [formControl]="input" [attr.disabled]="isDisabled$ | async" />
<textarea *ngIf="type === 'textarea'" [formControl]="input" [attr.disabled]="isDisabled$ | async"></textarea>
<ng-template></ng-template>
</div>`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputTextComponent), multi: true },
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputTextComponent), multi: true }
]
})
export class InputTextComponent extends BaseFormInput<string> {
@Input() type: "text" | "password" | "email" | "textarea" = "text"
@Input() maxLength: number
}
最后,我创建了一个使用输入的注册组件。
HTML:
<form [formGroup]="form">
<ec-input-text label="First name" formControlName="firstName" />
<ec-input-text label="Last name" formControlName="lastName" />
<ec-input-text label="E-mail" formControlName="email" type="email" />
<ec-input-text label="Password" formControlName="password" type="password" />
</form>
注册组件的 TS 具有如下公共属性:
public form = new FormGroup({
firstName: new FormControl(null, [Validators.required, Validators.maxLength(50)]),
lastName: new FormControl(null, [Validators.required, Validators.maxLength(50)]),
email: new FormControl(null, [Validators.required, Validators.maxLength(100)]),
password: new FormControl(null, Validators.required)
})
现在,问题如下:在抽象类的validate方法中(我在其中添加了注释),我尝试记录控制错误,但出现了奇怪的行为:记录formControl时,我可以在控制台中看到属性errors为空,但如果我记录control.errors,则会记录:
{ required: true }
即使控件有效,并且我输入了值(实际上,control.value有一个值并且结果有效)。因此,如果我这样做:
console.log(control)
我将其扩展,错误为空(预期行为,正确!)
但如果我这么做:
console.log(control.errors)
它被重视了(不正确,控制是有效的!)
我该如何解决这个问题?提前致谢!
不要在响应式表单中使用
attr.disabled
或disabled
,您可以尝试使用指令或使用响应式表单方法手动禁用它。这可能会导致难以解决的错误,因此建议以编程方式禁用它。在 Angular 中使用响应式表单时禁用表单控件
您没有在正确的位置检查验证错误,验证方法旨在让您插入自定义验证,该验证针对您的控件的值进行验证,大多不会涉及检查其他错误(未正确显示)。
当您在此位置检查控件的错误时,它显示之前的状态,所以我猜其他验证尚未更新。因此请在
validate
函数内部执行验证,不要检查其他错误。您还可以导入
ControlContainer
,获取formControlName
并获取实际的表单控件,您可以使用它来检查Validators.required
是否已添加。虽然这不是万无一失的,但它是访问自定义表单元素内的表单控件的一个很好的起点。完整代码:
Stackblitz 演示
如果您曾经使用 ControlValueAccessor 和 BehaviorSubjects 在 Angular 中设置自定义表单控件,您可能已经注意到,在验证方法中检查 control.errors 并不总是显示最新状态。就好像您知道控件应该是有效的,但 Angular 尚未完全跟上。发生这种情况的原因是 Angular 的验证可以异步运行,因此它可能无法在您认为应该更新 control.errors 时立即更新。
有什么快速解决方法吗?只需将 console.log(control.errors) 包装在 setTimeout 中,并设置零延迟即可:
这个微小的延迟让 Angular 完成其验证周期和变更检测,因此在 console.log 运行时,control.errors 应该完全是最新的。这是一个简单的解决方法,只是让 Angular 在您检查错误之前有一点时间来喘口气!
出现此问题是因为您在调用方法时尝试访问尚未初始化的对象的属性。
导致问题的错误字段填充在您的验证方法中,因此当您尝试访问它时它不会立即可用。因此,尝试在验证中直接读取此属性会导致问题。
一种解决方案是使用 setTimeout 延迟控制台日志。通过这样做,您可以允许验证完成执行并更新所有内部字段。当超时到期时,您的控制台日志将使用更新后的数据执行,让您可以安全地访问错误属性。这应该可以解决问题。