1

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:

  • I changed [(ngModel)] two way binding and (ngModelChanges) listener to just a simple [value] and (input) listener but that did not help.
  • I registered the onValidationChange method but that did not work.

Is there anything I am doing wrong, or something that I forgot to add, or is this an actual angular bug?

1 Answer 1

1

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();
}

Full Code:

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 '';
  }
}

Stackblitz Demo

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you. This worked. Quick question, what is the purpose of the Validator interface then? Is it for controls that provide their own validation logic?
@Dominic It is for creating new validations that are embedded inside the custom control Angular Custom Form Controls: Complete Guide

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.