I’m struggling with some strange behavior while using my custom input component.
First of all, I built a simple abstract class that has the main "features" and methods of the component, then, the input-component which has very few code:
// 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()
}
}
The input component is simply designed like this:
@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
}
Finally, I created a register-component, which uses the input.
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>
The TS of the register-component has a public property like this:
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)
})
Now, the issue is the following: in the validate method of the abstract class (where I put a comment), I tried to log the control errors, and I get a strange behavior: when logging the formControl, I can see in the console that the property errors is null, but if I log control.errors it logs:
{ required: true }
Even though the control is valid and I typed the value (in fact, control.value has a value and results valid).
So if i do:
console.log(control)
And I expand it, errors is null (expected behavior, correct!)
But if I do:
console.log(control.errors)
It is valorized (not correct, the control is valid!)
How can I figure this out? Thanks in advance!
2
Answers
Do not use
attr.disabled
ordisabled
in reactive forms, you can try a directive or just manually disabling it using reactive form methods. It can lead to difficult to solve bugs, so recommending disabling it programmatically.Disabling Form Controls When Working With Reactive Forms in Angular
You are not checking for the validation errors at the right place, the validate method is designed for you to insert custom validation that does validation specific to your control’s value, mostly won’t involve checking the other errors (not showing correctly).
When you check the errors of the control at this location, it is showing the previous state, so I guess the other validations are not updated. So please perform validation inside the
validate
function and do not check the other errors.Also you can import the
ControlContainer
, get theformControlName
and get the actual form control, you can use it to check theValidators.required
is added or not. Although this is not fool proof it’s a good starting point to access the form control inside the custom form element.Full Code:
Stackblitz Demo
If you’ve ever set up custom form controls in Angular with ControlValueAccessor and BehaviorSubjects, you might’ve noticed that checking control.errors in the validate method doesn’t always show the latest state. It’s like, you know the control should be valid, but Angular hasn’t fully caught up yet. This happens because Angular’s validation can run a bit asynchronously, so it might not update control.errors right when you think it should.
A quick fix? Just wrap console.log(control.errors) in a setTimeout with a zero delay:
That tiny delay lets Angular finish its validation cycle and change detection, so by the time console.log runs, control.errors should be totally up-to-date. It’s a simple workaround that just gives Angular a little time to catch its breath before you check the errors!