我在使用自定义输入组件时遇到一些奇怪的行为。
首先,我构建了一个简单的抽象类,它具有组件的主要“功能”和方法,然后,输入组件的代码很少:
// 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
,您可以尝试使用指令或仅使用反应式表单方法手动禁用它。它可能会导致难以解决的错误,因此建议以编程方式禁用它。
您没有在正确的位置检查验证错误,验证方法是为您插入自定义验证而设计的,该自定义验证会针对您的控件的值进行验证,大多数情况下不会涉及检查其他错误(未正确显示)。
当您在该位置检查控件的错误时,它显示的是之前的状态,所以我猜其他验证没有更新。所以请在
validate
函数内进行验证,不要检查其他错误。
您还可以导入
ControlContainer
,获取formControlName
并获取实际的表单控件,您可以用它来检查Validators.required
是否已添加。虽然这并不是万无一失,但它是访问自定义表单元素内的表单控件的一个很好的起点。
constructor(public inj: Injector) {}
ngOnInit() {
const controlContainer = this.inj.get(ControlContainer);
this.control = controlContainer!.control!.get(this.formControlName);
this.required$.next(this.control!.hasValidator(Validators.required));
}
import {
Component,
OnDestroy,
AfterViewInit,
Input,
Output,
EventEmitter,
ChangeDetectionStrategy,
forwardRef,
Directive,
inject,
Injector,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import {
ReactiveFormsModule,
ControlValueAccessor,
Validator,
FormGroup,
FormControl,
AbstractControl,
ValidationErrors,
Validators,
NG_VALUE_ACCESSOR,
NG_VALIDATORS,
NgControl,
ControlContainer,
} from '@angular/forms';
import { BehaviorSubject, Subscription } from 'rxjs';
import { CommonModule } from '@angular/common';
@Directive()
export abstract class BaseFormInput<T>
implements ControlValueAccessor, Validator, AfterViewInit, OnDestroy
{
@Input() label!: string;
@Input() formControlName!: 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);
control!: AbstractControl<any, any> | null;
ngOnDestroy() {
this.changeSub.unsubscribe();
}
constructor(public inj: Injector) {}
ngOnInit() {
const controlContainer = this.inj.get(ControlContainer);
this.control = controlContainer!.control!.get(this.formControlName);
this.required$.next(this.control!.hasValidator(Validators.required));
}
ngAfterViewInit() {
this.changeSub = this.input.valueChanges.subscribe((v: any) => {
if (!this.disabled$.getValue()) {
this.onChange.emit(v);
this.changeInternal(v);
}
});
}
writeValue = (obj: any) => 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 | null {
// THIS LINE HAS WEIRD BEHAVIOR
// console.log(
// control,
// control.getError('required'),
// control.errors,
// control.value
// );
return null;
}
public get isDisabled$() {
return this.disabled$.asObservable();
}
public get isRequired$() {
return this.required$.asObservable();
}
}
@Component({
selector: 'ec-input-text',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `<div class="form-control">
<label *ngIf="label">
{{ label }}
<span *ngIf="isRequired$ | async">*</span>
</label>
<input *ngIf="type !== 'textarea'" [type]="type" [formControl]="input" />
<textarea *ngIf="type === 'textarea'" [formControl]="input" ></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;
}
@Component({
selector: 'app-root',
standalone: true,
imports: [ReactiveFormsModule, CommonModule, InputTextComponent],
template: `
<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><br/>
{{form.errors | json}}
<br/>
{{form.controls.firstName.errors | json}}
`,
})
export class App {
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),
});
}
bootstrapApplication(App);