I built a shared input component in Angular using Control Value Accessor interface. The input itself works but validation doesn't get updated when I change the value within the input. Here is the input component I created.
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormsModule,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '@angular/forms';
@Component({
selector: 'app-input',
standalone: true,
imports: [FormsModule],
templateUrl: './input.component.html',
styleUrl: './input.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => InputComponent),
multi: true,
},
],
})
export class InputComponent implements ControlValueAccessor, Validator {
@Input() value: string = '';
@Input() type: string = 'text';
@Input() placeholder: string = '';
@Input() disabled: boolean = false;
@Input() label: string = '';
// default to a random id unless one is provided.
@Input() id: string = '';
isInvalid: boolean = false;
touched: boolean = false;
onChange = (value: string) => {};
onTouched = () => {};
onValidationChange = () => {};
errorMessage: string = '';
writeValue(value: string): void {
this.value = value;
}
registerOnChange(onChange: any): void {
this.onChange = onChange;
}
registerOnTouched(onTouched: any): void {
this.onTouched = onTouched;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onInputChange($event: Event) {
const input = $event.target as HTMLInputElement;
this.value = input.value;
this.onChange(this.value);
this.markAsTouched();
this.onValidationChange();
}
markAsTouched() {
if (!this.touched) {
this.onTouched();
this.touched = true;
}
}
validate(control: AbstractControl): ValidationErrors | null {
this.errorMessage = this.getErrorMessage(control.errors);
return control.errors;
}
registerOnValidatorChange(onValidationChange: () => void): void {
this.onValidationChange = onValidationChange;
}
private getErrorMessage(errors: ValidationErrors | null): string {
if (!this.touched || !errors) {
return '';
}
if (errors['required']) {
return 'This field is required';
}
if (errors['minLength']) {
return `Minimum length is ${errors['minLength'].requiredLength}.`;
}
return '';
}
}
When I log the control.errors within the validate method, it still shows that the required validation still has an error.
I have a sample angular project showing this issue here:
https://stackblitz.com/edit/stackblitz-starters-fy4zue?file=src%2Finput%2Finput.component.ts
Things I have tried:
Is there anything I am doing wrong, or something that I forgot to add, or is this an actual angular bug?
You do not need validator for this scenario, use it, when you want to embed a validation inside the custom component.
Angular Custom Form Controls: Complete Guide
For your scenario, all you need to do is call the function on ngOnInit
and onChange
and make sure you fetch the formControl
object using the below method.
constructor(
private controlContainer: ControlContainer,
private elementRef: ElementRef
) {}
ngOnInit() {
const formControlName =
this.elementRef.nativeElement.getAttribute('formcontrolname');
console.log(this.controlContainer, formControlName);
this.control = this.controlContainer?.control?.get(formControlName) || null;
console.log(this.control);
this.errorMessage = this.getErrorMessage(this.control.errors);
}
Then onChange, we call the getErrorMessage
again.
onInputChange($event: Event) {
const input = $event.target as HTMLInputElement;
this.value = input.value;
console.log('asdf', this.value);
this.onChange(this.value);
this.errorMessage = this.getErrorMessage(this.control.errors);
this.markAsTouched();
// this.onValidationChange();
}
import {
Component,
ElementRef,
forwardRef,
Host,
inject,
Input,
OnInit,
Optional,
SkipSelf,
} from '@angular/core';
import {
AbstractControl,
ControlContainer,
ControlValueAccessor,
FormsModule,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
FormControl,
NgControl,
} from '@angular/forms';
@Component({
selector: 'app-input',
standalone: true,
imports: [FormsModule],
templateUrl: './input.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true,
},
],
})
export class InputComponent implements ControlValueAccessor {
value: string = '';
@Input() type: string = 'text';
@Input() placeholder: string = '';
@Input() disabled: boolean = false;
@Input() label: string = '';
// default to a random id unless one is provided.
@Input() id: string = '';
isInvalid: boolean = false;
touched: boolean = false;
onChange = (value: string) => {};
onTouched = () => {};
errorMessage: string = '';
control!: any;
constructor(
private controlContainer: ControlContainer,
private elementRef: ElementRef
) {}
ngOnInit() {
const formControlName =
this.elementRef.nativeElement.getAttribute('formcontrolname');
console.log(this.controlContainer, formControlName);
this.control = this.controlContainer?.control?.get(formControlName) || null;
console.log(this.control);
this.errorMessage = this.getErrorMessage(this.control.errors);
}
writeValue(value: string): void {
console.log(value);
this.value = value;
}
registerOnChange(onChange: any): void {
this.onChange = onChange;
}
registerOnTouched(onTouched: any): void {
this.onTouched = onTouched;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onInputChange($event: Event) {
const input = $event.target as HTMLInputElement;
this.value = input.value;
console.log('asdf', this.value);
this.onChange(this.value);
this.errorMessage = this.getErrorMessage(this.control.errors);
this.markAsTouched();
// this.onValidationChange();
}
markAsTouched() {
if (!this.touched) {
this.onTouched();
this.touched = true;
}
}
private getErrorMessage(errors: ValidationErrors | null): string {
if (errors?.['required']) {
return 'This field is required';
}
if (errors?.['minLength']) {
return `Minimum length is ${errors['minLength'].requiredLength}.`;
}
return '';
}
}