Custom control component implementing both ControlValueAccessor and Validator interface.
This custom control is unable to display error properly from the consumer component's validators.
@Component({
selector: 'custom-input',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => CustomInput),
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => CustomInput),
},
],
template: `
<article style="border: 1px solid green; padding: .5rem ">
<form [formGroup]="inputForm">
<input formControlName="telInput" placeholder="Telephone" (blur)="onFocusOut($event);">
</form>
<section class="code">
inner errors: {{ inputForm.get('telInput')?.errors | json }}
</section >
</article>
`,
})
export class CustomInput implements ControlValueAccessor, Validator {
inputForm!: FormGroup;
formCallbacks = {
onChange: (anyVal: any) => {},
onTouched: () => {},
onValidatorChange: () => {},
};
writeValue(obj: any): void {
this.inputForm.setValue({ telInput: obj });
}
registerOnChange(fn: any): void {
this.formCallbacks.onChange = fn;
}
registerOnTouched(fn: any): void {
this.formCallbacks.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
isDisabled ? this.inputForm.disable() : this.inputForm.enable();
}
validate(control: AbstractControl): ValidationErrors | null {
console.log('[inner] validate', control.errors);
return control.errors;
}
registerOnValidatorChange?(fn: () => void): void {
this.formCallbacks.onValidatorChange = fn;
}
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.inputForm = this.fb.group({
telInput: [''],
});
this.inputForm.valueChanges.subscribe((val) => {
this.formCallbacks.onChange(val.telInput);
});
}
onFocusOut(event: any) {
this.formCallbacks.onTouched();
}
}
However when this custom control is used with a reactive form and a validator, the errors are not syncing properly between the component form and custom input.
function someValidator(ac: AbstractControl): ValidationErrors | null {
if (ac.value === null) {
return null;
}
const inputVal = ac.value;
const noErrorsOk = inputVal.length > 2;
const err = inputVal !== null && noErrorsOk ? null : { dataFormat: true };
console.log('[outer] validator err', err);
return err;
}
@Component({
selector: 'app-root',
standalone: true,
imports: [CustomInput, ReactiveFormsModule, CommonModule],
template: `
<article>
<form [formGroup]="mainForm">
<custom-input formControlName="joesNumber"></custom-input>
</form>
<section class="code">
outer error: {{ mainForm.get('joesNumber')?.errors | json }}
</section>
<section>
<p>Notes: length > 2 is valid. inner error is always null and outer error is alwasy dataFormat error. </p>
</section>
</article>
`,
})
export class App {
mainForm!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.mainForm = this.fb.group({
joesNumber: ['', [someValidator]],
});
this.mainForm.statusChanges.subscribe((val) => {
console.log('[outer] statusChange', val);
});
}
}
playground
When we create a custom form control with Validator is to make a function validator that it's not the same that the errors we pass when create the formControl. you can not return the control.errors
If you want to give a class to your input if the formControl is invalid you can declare a variable control
And give value in the validate function
So write some like
Update
There're another way to "reach" the "outher control", we can get the control using injector and get it in ngAfterViewInit
The setTimeout is to not get the error "ExpressionChangedAfterItHasBeenCheckedError"
In this way, using a ErrorStateMatcher like this a bit old SO, we can manage the mat-input and the mat-error in the mat-form-field
Rewrite the code with a few changes
So we can use a mat-form-field
See how we can show one or another error using the "outerControl"
Note also that the inner FormControl not declare with any validator, you use the
validatefunctiona stackblitz
Well, this work in several cases, but we can also not create a custom form control else a custom form field control like show the guide Custom form field control
So, our component implements