40

I have a custom form control component (it is a glorified input). The reason for it being a custom component is for ease of UI changes - i.e. if we change the way we style our input controls fundamentally it will be easy to propagate change across the whole application.

Currently we are using Material Design in Angular https://material.angular.io

which styles controls very nicely when they are invalid.

We have implemented ControlValueAccessor in order to allow us to pass a formControlName to our custom component, which works perfectly; the form is valid/invalid when the custom control is valid/invalid and the application functions as expected.

However, the issue is that we need to style the UI inside the custom component based on whether it is invalid or not, which we don't seem to be able to do - the input that actually needs to be styled is never validated, it simply passes data to and from the parent component.

COMPONENT.ts

import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
    AbstractControl,
    ControlValueAccessor,
    NG_VALIDATORS,
    NG_VALUE_ACCESSOR,
    ValidationErrors,
    Validator,
} from '@angular/forms';

@Component({
  selector: 'app-input',
  templateUrl: './input.component.html',
  styleUrls: ['./input.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputComponent),
      multi: true
    }
  ]
})
export class InputComponent implements OnInit, ControlValueAccessor {
  writeValue(obj: any): void {
    this._value = obj;
  }
  registerOnChange(fn: any): void {
    this.onChanged = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  get value() {
    return this._value;
  }

  set value(value: any) {
    if (this._value !== value) {
      this._value = value;
      this.onChanged(value);
    }
  }

  @Input() type: string;

  onBlur() {
    this.onTouched();
  }

  private onTouched = () => {};
  private onChanged = (_: any) => {};
  disabled: boolean;

  private _value: any;

  constructor() { }

  ngOnInit() {
  }

}

COMPONENT.html

<ng-container [ngSwitch]="type">
  <md-input-container class="full-width" *ngSwitchCase="'text'">
    <span mdPrefix><md-icon>lock_outline</md-icon> &nbsp; </span>
    <input mdInput placeholder="Password" type="text" [(ngModel)]="value" (blur)="onBlur()" />
  </md-input-container>
</ng-container>

example use on page:

HTML:

<app-input type="text" formControlName="foo"></app-input>

TS:

this.form = this.fb.group({
        foo: [null, Validators.required]
    });
matthall74
  • 451
  • 1
  • 4
  • 6
  • Perhaps this is what you've been looking for? https://stackoverflow.com/questions/48573931/angular-5-glue-logic-of-components-dynamically-added-to-form – Guillermo Prandi Feb 04 '18 at 19:53

3 Answers3

38

You can get access of the NgControl through DI. NgControl has all the information about validation status. To retrieve NgControl you should not provide your component through NG_VALUE_ACCESSOR instead you should set the accessor in the constructor.

@Component({
  selector: 'custom-form-comp',
  templateUrl: '..',
  styleUrls: ...
})
export class CustomComponent implements ControlValueAccessor {

   constructor(@Self() @Optional() private control: NgControl) {
     this.control.valueAccessor = this;
   }

   // ControlValueAccessor methods and others

   public get invalid(): boolean {
     return this.control ? this.control.invalid : false;
   }

   public get showError(): boolean {
      if (!this.control) {
       return false;
      }

      const { dirty, touched } = this.control;

      return this.invalid ? (dirty || touched) : false;
   }
}

Please go through this article to know the complete information.

VJAI
  • 32,167
  • 23
  • 102
  • 164
  • 2
    So we retrieve the fact that the formControlName="foo" is valid or not through the injected NgControl, correct? And we may use the get invalid() boolean variable in the template of the custom-form-comp to style its inner elements, correct? Is there a way (and does it make sense) to forward the classes ng-touched, ng-dirty, ng-invalid, etc from the component to its internal elements? – user2010955 Mar 20 '19 at 22:39
  • 4
    for Angular 8 I can simplify the constructor to `constructor(private control: NgControl) { this.control.valueAccessor = this; }` – mariszo Jun 11 '19 at 06:54
  • 1
    hi @VJAI , I am trying to take your answer, and apply it to this question, which is somewhat different, do you have a more optimal answer? https://stackoverflow.com/questions/59086347/controlvalueaccessor-with-error-validation-in-angular-material thanks –  Nov 30 '19 at 22:04
  • 6
    This was very helpful, though I had to re-read a few times to notice that I needed to *stop* providing through `NG_VALUE_ACCESSOR` and use `.valueAccessor` instead. Thanks! – Coderer May 21 '20 at 09:42
  • 2
    @Coderer thanks to your comment I was able to make it work, I was getting a circular dependency error.. didn't notice in the answer that the providers array has been omitted.. nice one! – Божидар Йовчев Oct 14 '21 at 00:27
  • 1
    While this works now I have the opposite problem of not being able to validate the input from the inside, because providing `NG_VALIDATORS` also results in a circular dependency. Is it possible to be able to do internal validation (for instance against a regex) **and** external validation (using `Validators.required` on the form where the component is used)? – Yasammez Aug 30 '22 at 14:34
5

Answer found here:

Get access to FormControl from the custom form component in Angular

Not sure this is the best way to do it, and I'd love someone to find a prettier way, but binding the child input to the form control obtained in this manner solved our issues

matthall74
  • 451
  • 1
  • 4
  • 6
  • I am having the exact same issue. How did you mange to reflect the validation down to the input after you fetched the controller? – Erex Aug 22 '17 at 10:55
  • nice pointing in the right directions, but indeed, how did you do this? – SomeOne_1 Aug 27 '17 at 18:18
  • thanks for taking me in the right direction, the answer on that page that actually worked for me was [this one](https://stackoverflow.com/a/51126965/602447) – Alberto Rechy Aug 12 '18 at 19:30
-1

In addition: Might be considered dirty, but it does the trick for me:

  1. let your component implement the Validator interface. 2 In the validate function you use the controlcontainer to get to the outer formcontrol of your component.
  2. Track the status of the parent form control (VALID/INVALID) by using a variable.
  3. check for touched. and perform validation actions on your fields only when touched is true and the status has changed.
SomeOne_1
  • 808
  • 10
  • 10