Profile

Validation Error Tracker for Form Fields


Angular provides form control APIs to collect the validation errors within a form group. However, it is tricky to gather validation errors across multiple form groups in a general way. What we do is to create a global registry service. Each new form element component will be registered under the registry service. This approach makes it for us to track all validation errors on one single page.

@Injectable()
export class FormElementRegistryService {

  private readonly registry: Array<AbstractFormElementComponent> = new Set< AbstractFormElementComponent>();

  public register(formElementComponent: AbstractFormElementComponent): void {
    this.registry.add(formElementComponent);
  }

  public unregister(formElementComponent: AbstractFormElementComponent): void {
    this.registry.remove(formElementComponent);
  }

  public getFormElementErrors(): Array<FormElementError> {
    return Array.from(this.registry.values())
      .reduce((formComponentErrors, formElementComponent) => {
        const isValidationError = !!formElementComponent.control?.invalid;

        if (isValidationError) {
          const controlErrors = formElementComponent.control.errors ?? {};
          const validationError = {
            fieldName: formElementComponent.fieldName,
            errors: Object.keys(controlErrors).map(key => ({ [key]: controlErrors[key] })),
          };
          return [...formComponentErrors, validationError];
        } else {
          return formComponentErrors;
        }
      }, []);
  }
}
@Directive()
export abstract class AbstractFormElementComponent implements ControlValueAccessor, OnInit, OnDestroy {

  public control: AbstractControl = new FormControl();
 
  constructor(
    protected cdr: ChangeDetectorRef,
    @Optional() private readonly formElementRegistryService: FormElementRegistryService,
    @Optional() @Self() protected ngControl: NgControl,
  ) {
    if (ngControl) {
      ngControl.valueAccessor = this;
    }
  }

  public ngOnInit(): void {
      this.control = this.ngControl.control;

    // register the component in initialization
      if (this.formElementRegistryService) {
        this.formElementRegistryService.register(this);
      }
    }

  public ngOnDestroy(): void {
      this.control = this.ngControl.control;

      if (this.formElementRegistryService) {
        this.formElementRegistryService.unregister(this);
      }
    }
}