7

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.

0

4 Answers 4

19

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.

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

3 Comments

I've just tried out your approach and it works great. I am updating my own framework and ngx-valdemort gave me another idea. Having one container with all the possible errors inside is way better than having to duplicate the custom-validation-message for each possible error.
How is that @Optional() private controlContainer: ControlContainer injected? Is that automatically happening inside of the formGroup?
@ThomasSchneiter the ControlContainer inject by angular and give us access to the formGroup instance from the parent check this and include a talk by Kara Ericksons thecodecampus.de/blog/nested-forms-in-angular
3

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>

1 Comment

hi 👋, control.errors is an object of key value not any array so this case you will get an error like this NgFor only supports binding to Iterables such as Arrays. you can fix this problem like this <li *ngFor="let err of control.errors | keyvalue"> {{err.key}} </li> 🤔
2

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

6 Comments

This causes Error: Circular dep for TextInputComponent
I don't see how this causes circular dep. It is likely to be something in your imports.
I notice that you didn't provide NG_VALUE_ACCESSOR providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: MyCustomComponent, multi: true }], is it the same as what you do in the Constructor and check if it's null ?
Yes, it is explained here material.angular.io/guide/…
That might be the reason why @GennadiyLitvinyuk had circular dep, that he was providing NG_VALUE_ACCESSOR and shouldn't in this case.
|
1

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>

1 Comment

This was my original aprouch. I wanted to change to get rid of the "complexity" (form.get(...)). Thats why I wanted something that looked similar to how you bind input controls.

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.