28

I have Angular form that is built with help of FormBuilder.

Form contains a FormArray which has as many fields as user wants. I've set validator for fields

this.fb.array([this.fb.control('', Validators.required)])

and for each new push validator is the same.

The problem is that I don't know how to access a specific field's isValid property since they are bound with FormControl via [formControlName]="index".

I've tried to do it that way, but it doesn't seem to work

<div *ngIf="array.at(index).invalid" class="alert alert-danger p-2">
</div>

Where array is a formArray.controls passed from a parent.

Update #1

There is a case https://stackblitz.com/edit/angular-7ppkoh

2
  • Please consider creating a stackblitz. Commented Aug 31, 2018 at 21:22
  • @SiddharthAjmera check it out Commented Sep 1, 2018 at 6:47

7 Answers 7

31

I don't really think this would be possible completely on the template. That's because to access the FormArray's control's state in your template, you'll have to access this.formGroup.get('features').controls[i].invalid. But since get returns an instance of type AbstractControl, you won't have access to the controls array on it. For that, you'll have to typecast whatever is returned from this.formGroup.get('features') into a FormArray. And I don't really think that would be possible on the template.

You'll have to create a method in your class that would return the validity of the control based on the index.

So if we continue to refer to your stackblitz eg, here's how:

<form [formGroup]="formGroup">
  <div formArrayName="features">
    <div 
      class="row no-gutters form-group" 
      *ngFor="let feature of features.controls; let index = index; let last = last">
      <input 
        type="text" 
        class="form-control px-2 col" 
        [formControlName]="index" 
        title="feature" 
        required>

        IS Invalid - {{ getValidity(index) }}

      <div class="col-3 col-md-2 row no-gutters">
        <button 
          class="col btn btn-outline-danger" 
          (click)="removeFeature(index)">
          -
        </button>
        <button 
          class="col btn btn-success" 
          *ngIf="last" 
          (click)="addFeature()">
          +
        </button>
      </div>
    </div>
  </div>
</form>

And in your class:

import { Component } from '@angular/core';
import { FormArray, FormBuilder, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  constructor(
    private fb: FormBuilder,
  ) {}

  formGroup = this.fb.group({
    features: this.fb.array([this.fb.control('', Validators.required)])
  });

  get features(): FormArray {
    return this.formGroup.get('features') as FormArray;
  }

  addFeature(): void {
    this.features.push(this.fb.control('', Validators.required));
  }

  getValidity(i) {
    return (<FormArray>this.formGroup.get('features')).controls[i].invalid;
  }

  removeFeature(index): void {
    this.features.removeAt(index);
    console.log(this.features);
  }

}

UPDATE

A few months back I realized that calling a method in one of the data-binding syntaxes(i.e. String Interpolation - {{ ... }}, Property Binding - [propertyName]="methodName()", or Attribute Binding - [class.class-name]="methodName()" OR [style.styleName]="methodName()") is extremely costly as far as performance is concerned.

So, you should do it using:

{{ formGroup.controls['features'].controls[index].invalid }}

Instead of:

{{ getValidity(index) }}

Here's an Updated Working Sample StackBlitz for your ref.

If you wanna know more about it, I highly recommend you to check this thread out:

Angular Performance: DOM Event causes unnecessary function calls

Hope this helps :)

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

6 Comments

Thank you. I did it that way get featureControl() { return (this.group.get('features') as FormArray).controls[this.index]; } and that worked!
How do you intend of passing this.index in the template though? Just curious.
I've shown you an adapted version of what I have. Actually, features is an extra component which consumes index from parent. So there won't be any problems with index
Can you elaborate or provide some documentation on the performance penalties of calling a method with data-binding?
@ColbyBoren, I wrote an answer about it here that points to all the different answers that I've given and also a Medium Article that I wrote. Hope this helps :)
|
12

I have this example in angular 8.

In your template when you do this.

<ng-container formArrayName="calibers">
        <ng-container *ngFor="let item of qualityForm.get('calibers')['controls']; let index = index" [formGroupName]="index.toString()">
          <ion-item>
            <ion-label position="floating">{{ getCaliberName(item) }}</ion-label>
            <ion-input formControlName="percentage" placeholder="Input Percentage" type="number" clearInput></ion-input>
            <ng-container *ngIf="item.get('percentage').hasError('required')">
              <span class="errorMsg">Input Percentage</span>
            </ng-container>
            <ng-container *ngIf="item.get('percentage').hasError('max')">
              <span class="errorMsg">Percentage cannot be greater than 100</span>
            </ng-container>
          </ion-item>
        </ng-container>
      </ng-container>

That item object in the ngFor will give you access to the form control. all you need to do to get the array form errors is item.get('percentage').hasError('required')

Comments

6

In the ngFor statement, you are defining the variable "feature" to use for each control of the form array.

*ngFor="let feature of features.controls; let index = index; let last = last"

You can use that variable to get the invalid status of that control.

feature.invalid 

Here is the Stackblitz


Additionally

You don't need the required attribute in the HTML when you are using reactive forms.

So this

<input type="text" class="form-control px-2 col" [formControlName]="index" title="feature" required>

Can be

<input type="text" class="form-control px-2 col" [formControlName]="index" title="feature">

Comments

1

You Should use form controls to achieve that sharply.

<div *ngIf="formGroup.get('features').controls[i].controls.index.invalid && (formGroup.get('features').controls[i].controls.index.dirty || formGroup.get('features').controls[i].controls.index.touched)"                                          class="text-center error">
<p [hidden]="!formGroup.get('features').controls[i].controls.index.errors.required">Index is required.</p>
</div>

Comments

1

As above answer using

feature.invalid from features.controls

let you validate all elements inside that controlat once.

However if you want to validate a specific element can you following code:

> feature.controls.controlname.invalid

Note: I'm using feature not features

1 Comment

This is a very useful distinction if you have a more complex FormGroup with multiple controls
0

The FormGroup class has a get method which returns the abstractControl for the given key. The one you use in formControlName.

Here the links for both Api docs:
AbstractControl
FormGroup

<form [formGroup]="form">
  <input formControlName="name" type="text" />
  <div *ngIf="form.get('name').invalid">
    <p><Message you like to show/p>
  </div>
</form>

Comments

0

creating form with formArray.

createForm() {
  this.studentForm = this.fb.group({
    name: [],
    id: [],
    hobbies: this.fb.array([])
  })
}

addHobbies() {
 const control = new FormControl(null, [Validators.required]);
 (<FormArray>this.studentForm.get('hobbies')).push(control);
}

Printing error message to user

<form [formGroup]="studentForm" (submit)="saveStudent()">
  <input type="text" id="id" name="id" formControlName="id">
  <input type="text" id="name" name="name" formControlName="name">
  <div formArrayName="hobbies">
      <label>Hobbies</label>
      <button type="button" (click)="addHobbies()">Add Hobbies</button>
      <div *ngFor="let formControl of studentForm.get('hobbies')['controls']; let i = index">
          <input type="text" [formControlName]="i"> 
          <div *ngIf="!studentForm.get('hobbies')['controls'][i].valid && 
                      studentForm.get('hobbies')['controls'][i].errors?.required">
         <p>This field is required</p>
     </div>
      </div>
  </div>
  <button type="submit">Submit Student</button>
</form>

1 Comment

Please don't post only code as answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.