6
votes

I'm creating a custom angular component which shows an error tooltip when my FormControl (Reactive Forms) is invalid. But I don't know how I can access the FormControl inside my custom component to check whether or not it is marked as valid.

What I want to accomplish

<div [formGroup]="form">
     <input formControlName="name" type="text" />
     <custom-validation-message formControlName="name">My special error message!</custom-validation-message>
  </div>

Already encountered stuff

ERROR Error: No value accessor for form control with name: 'surveyType'

Fixed this by implementing ControlValueAccessor with NG_VALUE_ACCESSOR even though I don't want to alter the value. I also added an injector to access the NgControl.

import { Component, OnInit, Injector } from '@angular/core';
import { NgControl, ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
    selector: 'custom-validation-message',
    templateUrl: './validation-message.component.html',
    providers: [{
        provide: NG_VALUE_ACCESSOR, multi: true, useExisting: ValidationMessageComponent
    }]
})
export class ValidationMessageComponent implements ControlValueAccessor, OnInit {
    public formControl: any;

    constructor(private injector: Injector) {
        super();
    }

    public ngOnInit(): void {
        const model = this.injector.get(NgControl);
        this.formControl = model.control;
    }

    public writeValue(obj: any): void {
    }
    public registerOnChange(fn: any): void {
    }
    public registerOnTouched(fn: any): void {
    }
    public setDisabledState?(isDisabled: boolean): void {
    }
}

Current Problem The model.control is undefined. After inspecting the model I found out that the model is as good as empty only the _parent is a full representation of my form. The model._parent.controls does contain all the controls of my form. But still I don't know the current control.

4

4 Answers

13
votes

as I get you point you just want to make a componnet for display form control validation message the other answer explane why ControlValueAccessor is not the case here ,you just want to pass a control form reference to the component then check the validation state , Thomas Schneiter answer is a correct why but I face the case and it 's hard to keep get refrance by get method and sometime we are in sub group and form array so I idea is to just pass the name of the form control as string then get the control reference.

CustomValidationMessageComponent

@Component({
  selector: "custom-validation-message",
  templateUrl: "./custom-validation-message.component.html",
  styleUrls: ["./custom-validation-message.component.css"]
})
export class CustomValidationMessageComponent {
  @Input()
  public controlName: string;

  constructor(@Optional() private controlContainer: ControlContainer) {} 

  get form(): FormGroup {
    return this.controlContainer.control as FormGroup;
  }

  get control(): FormControl {
    return this.form.get(this.controlName) as FormControl;
  }
}

template

<ng-container *ngIf="control && control?.invalid && control?.touched">
  <ul>
    <li *ngIf="control.hasError('required')">
      this is required field
    </li>
    <li *ngIf="control.hasError('pattern')">
      pattern is invalid 
    </li>
    <li *ngIf="control.hasError('maxlength')">
      the length is over the max limit
    </li>
     <!-- <li *ngFor="let err of control.errors | keyvalue">
       {{err.key}}
     </li> -->
  </ul>

</ng-container>

and you can use it like this

<form [formGroup]="form">
 <label>
   First Name <input type="text" formControlName="firstName" />
   <div>
       <custom-validation-message controlName="firstName"></custom-validation-message>
   </div>
 </label>

 ...

</form>

demo 🚀🚀

you can check this angular library ngx-valdemort created by JB Nizet where it solve this problem perfectly 👌.

3
votes

If i understand you correctly, the <custom-validation-message> should just display validation errors of a reactive forms input.

A ControlValueAccessor is used to create a custom input. What you want to do is to create a simple component with an Abstract control as input. The component could look like this:

ts:

@Input() public control: AbstractControl;
...

With this, you can access the formControls properties like invalid, touched and errors inside of the custom component.
html:

<ng-container *ngIf="control?.invalid && control?.touched">
  <ul>
    <li class="validation-message" *ngFor="let error of control.errors">
      {{error}}
    </li>
  </ul>
</ng-container>

then add the control that should display errors as an input

<custom-validation-message [control]="form.get('name')"></custom-validation-message>
2
votes

Here is how you can access the FormControl of a custom FormControl component (ControlValueAccessor). Tested with Angular 8.

<my-text-input formControlName="name"></my-text-input>
@Component({
  selector: 'my-text-input',
  template: '<input
    type="text"
    [value]="value"
  />'
})
export class TextInputComponent implements AfterContentInit, ControlValueAccessor {

  @Input('value') value = '';

  // There are things missing here to correctly implement ControlValueAccessor, 
  // but it's all standard.

  constructor(@Optional() @Self() public ngControl: NgControl) {
    if (ngControl != null) {
      ngControl.valueAccessor = this;
    }
  }


  // It's important which lifecycle hook you try to access it.
  // I recommend AfterContentInit, control is already available and you can still
  // change things on template without getting 'change after checked' errors.
  ngAfterContentInit(): void {
    if (this.ngControl && this.ngControl.control) {
      // this.ngControl.control is component FormControl
    }
  }
}
1
votes

Didn't check your approach. A CustomControlValueAccessor should only be used for real form controls. It's a creative approach, it may works somehow, but I wouldn't go for it.

There are other ways than injection to access the FormControl inside your validation component:

1) Define the FormGroup without the FormBuilder so that you'll have access to the form controls directly:

  firstName: new FormControl('');
  lastName: new FormControl('');

  profileForm = new FormGroup({
    firstName,
    lastName
  });

Then in your html you can pass the form control to the custom-validation-message:

<custom-validation-message [control]="firstName">My special error message!</custom-validation-message>

2) Use the FormBuilder nevertheless, but with a getter function:

// component

get firstName() {
    return this.profileForm.get('firstName') as FormControl;
}
<custom-validation-message [control]="firstName">My special error message!</custom-validation-message>

3) or as Thomas Schneiter wrote: access the control in the template with:

<form [formGroup]="form">
   <input formControlName="name" type="text" />
   <custom-validation-message [control]="form.get('firstName)">My special error message!</custom-validation-message>
</form>