23

I failed to get an Angular 2 reactive form to work which has a FormGroup nested in a FormArray. Can somebody show me what is wrong in my setup.

Unrelated parts of the code has been omitted for the brevity.

Following is my component

orderForm: FormGroup = this.fb.group({
    id: [''],
    store: ['', Validators.required],

    //The part related to the error
    order_lines: this.fb.array([
        this.fb.group({
           id: [''],
           order_id: [],
           product_id: [],
           description: ['', Validators.required],
           unit_price: ['', Validators.required],
           discount: [0],
           units: [1, Validators.required],
           line_total: ['', Validators.required]
        })
    ])
});

constructor(private fb: FormBuilder) {  }

//Order instance is passed from elsewhere in the code
select(order: Order): void {
    this.orderForm.reset(order)
}

The Order instance passed to the select method is like this:

  {
        "id": 20,
        "store": "Some Store",
        "order_lines": [
            {
                "id": 6,
                "order_id": 20,
                "product_id": 1,
                "description": "TU001: Polka dots",
                "unit_price": "1000.00",
                "discount": "100.00",
                "units": 2,
                "line_total": "1900.00"
            },
            {
                "id": 7,
                "order_id": 20,
                "product_id": 2,
                "description": "TU002: Polka dots",
                "unit_price": "500.00",
                "discount": "0.00",
                "units": 1,
                "line_total": "500.00"
            }
        ]
    }

The view template is like below.

<form [formGroup]="orderForm">
   <input type="number" formControlName="id">
   <input type="text" formControlName="store">

   <div formArrayName="order_lines">
      <div *ngFor="let line of orderForm.get('order_lines'); let i=index">
          <div [formGroupName]="i">
               <input type="text" [formControlName]="product_id">
               <input type="text" [formControlName]="description">
               <input type="number" [formControlName]="units">
               <input type="number" [formControlName]="unit_price">
               <input type="number" [formControlName]="line_total">
          </div>
      </div>
   </div>
</form>

This setup gives me a console error **Cannot find control at order_lines -> 0 -> **. I'm wondering what I'm doing wrong.

I could get this to work with a simple FormControl inside the order_lines FormArray. But it fails with the given error when a FormGroup is used inside the FormArray.

Can you please help me to get this working.

4 Answers 4

26

Instead of using form.get better to go with controls like below -

<form [formGroup]="orderForm">
   <input type="number" formControlName="id">
   <input type="text" formControlName="store">

   <div formArrayName="order_lines">
      <div *ngFor="let line of orderForm.controls.order_lines.controls; let i=index">
          <div [formGroupName]="i">
               <input type="text" formControlName="product_id">
               <input type="text" formControlName="description">
               <input type="number" formControlName="units">
               <input type="number" formControlName="unit_price">
               <input type="number" formControlName="line_total">
          </div>
      </div>
   </div>
</form>

Working Example

What you are missing -

  • you are missing .control in last of *ngFor to iterate over the controls.
  • If you are using above approach you need to replace [formControlName] with formControlName
Sign up to request clarification or add additional context in comments.

8 Comments

It gives another error. Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.
@PardeepJain, I always use orderForm.get('order_lines).controls. For improve my knowledge, why is better use orderForm.controls.order_lines.controls? Have you a link or similar?
@PardeepJain Your code solve the problem a half. But it gives a strange behaviour. Though there are multiple order_ines, it shows only the first one. Please check it Stackblitz. Click Set Test Data button. You will see only first order_line is rendered.
@Johna seems you have attached the wrong link, there is no such button on given link. Please check
[formGroupName]="i" was the hardest thing to grasp! Thank you :)
|
7

@Johna, to complementary my answer:

You have two functions

  buildForm(data:any):FormGroup
  {
    return data?this.fb.group({
      id: [data.id?data.id:''],
      store: [data.store?data.store:'', Validators.required],
      order_lines:this.fb.array(this.buildArrayControl(data.order_lines?data.order_lines:null))
    })
    :
    this.fb.group({
      id: [''],
      store: ['', Validators.required],
      order_lines:this.fb.array(this.buildArrayControl(null))
    })

  }

  buildArrayControl(data:any[]|null):FormGroup[]
  {
    return data?
    data.map(x=>{
      return this.fb.group({
           id: [x.id?x.id:''],
           order_id: [x.order_id?x.order_id:''],
           product_id: [x.product_id?x.product_id:''],
           description: [x.description?x.description:'', Validators.required],
           unit_price: [x.unit_price?x.unit_price:'', Validators.required],
           discount: [x.discount?x.discount:0],
           units: [x.units?x.units:1, Validators.required],
           line_total: [x.line_total?x.line_total:'', Validators.required]
        }) 
    })
    :
    [this.fb.group({
           id: [''],
           order_id: [],
           product_id: [],
           description: ['', Validators.required],
           unit_price: ['', Validators.required],
           discount: [0],
           units: [1, Validators.required],
           line_total: ['', Validators.required]
        }) 
    ]
  }

then you can do, e.g.

this.orderForm = this.buildForm(
    {
      id:'112',
      store:'22655',
      order_lines:[{description:1222,....},{description:1455,...}]
    }
  )

 //or 
 this.orderForm=this.buildForm(null);

1 Comment

I was reluctant to try your solution since it seemed complex. Finally gave it a try and worked perfectly.
6

I know OP's question has been already answered, but i'd like to post the solution to my problem, which i solved thanks to OP's question and above answers.

I was asked to create a dynamic form(a questionnaire) but had some problems implementing nested Angular dynamic forms.

The resulting dynamic form can be prepopulated with a number of entries, or you can add more fields to an empty list, or both.

My use case creates a car check-up form and every item on the starting array is a 'TO-DO' task for the operator.

Angular 9 + Material + Bootstrap.

The following code will clarify better:

/* Component Typescript */

constructor( private formBuilder: FormBuilder ) {  }

arrayTitoli = [
    "TASK1",
    "TASK2",
    "TASK3",
    "TASK4",
    "TASK5",
  ]
  
  addTaskField(value) {
  
    this.array.push(
        
      this.formBuilder.group({
        titolo: value,
        checkbox: false,
        noteAggiuntive: ""
      })

    )
  }

  get titoliArray(){
    
    let array = [];

    for(let i = 0;i <this.arrayTitoli.length;i++){
      array.push(
        
        this.formBuilder.group({
          titolo: this.arrayTitoli[i],
          checkbox: false,
          noteAggiuntive: ""
        })

      )
    }

    return array;
  }

  angForm: FormGroup = this.formBuilder.group({
  
    array: this.formBuilder.array(this.titoliArray)

  });

  get array(): FormArray {
    return this.angForm.get('array') as FormArray;
  }

  
  onFormSubmit(): void {
    console.log(this.angForm.value)
    
  }
<!-- Component HTML -->

<div class="p-2">
<form [formGroup] = "angForm" (ngSubmit)="onFormSubmit()">
    <div>
        <button type="submit" class="btn btn-primary">Send</button>
      </div>
    <div formArrayName="array">
        <div *ngFor="let line of array.controls; index as idx;">
            <div [formGroupName]="idx">
            <mat-grid-list  style="text-align: center!important;" [cols]="3" rowHeight="13vh" (window:resize)="onResize($event)">
                
                <mat-grid-tile style="font-weight: bold;font-size: smaller;" class="col-md-4 col-xs-4" colspan="1">
                    {{line.controls['titolo'].value}} 
                 </mat-grid-tile>

                 <mat-grid-tile class="col-md-4" colspan="1">
                    <mat-checkbox formControlName="checkbox" #check color="primary">
                        <!--<button *ngIf="!mobile"  type="button" (click)="check.toggle()" class="btn btn-{{check.checked ? 'success' : 'danger'}} btn-sm" >SPUNTA</button>-->
                    </mat-checkbox>
                    
            
                    <mat-form-field  [style.maxWidth.%]="mobile ? '70' : '100'" style="margin-left: 10%;">
                        <mat-label *ngIf="!mobile">Note aggiuntive</mat-label>
                        <mat-label *ngIf="mobile">Note</mat-label>
                        <textarea formControlName="noteAggiuntive" matInput value='' cdkTextareaAutosize #autosize="cdkTextareaAutosize" cdkAutosizeMinRows="1"
                            cdkAutosizeMaxRows="5"></textarea>
                    </mat-form-field>
                </mat-grid-tile>

            </mat-grid-list>
            <hr>
        
        </div>
    </div>
    </div>
      
      
</form>
<div>
    <mat-form-field>
    <input type="text" #newTask matInput>
    </mat-form-field>
    <span class="px-2">
        <button type="button" class="btn btn-primary" (click)="addTaskField(newTask.value)">Create</button>
    </span>
  </div>
</div>

Hope it can be useful.

Comments

5

You can move formArrayName inline with *ngFor and include .controls in ngFor, as we are looping the form controls.

<div>
      <div formArrayName="order_lines" *ngFor="let line of orderForm.get('order_lines').controls; let i=index">
          <div [formGroupName]="i">
               <input type="text" [formControlName]="product_id">
               <input type="text" [formControlName]="description">
               <input type="number" [formControlName]="units">
               <input type="number" [formControlName]="unit_price">
               <input type="number" [formControlName]="line_total">
          </div>
      </div>
   </div>

Comments

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.