18

I'm trying to implement an edit form for a model with nested attributes (FormArray). I'm having trouble with the syntax and I'm uncertain whether I'm on the right track. The attributes for the main form work, it's the nested form I'm having problems with. Here's what I have so far.

Here I initiate the form group:

private initForm() {
  this.subscription = this.expenseService.getExpense(this.id)
    .subscribe(
      expense => {
        this.expense = expense;
        this.patchForm();
      }
    );
  this.expenseEditForm = this.fb.group({
    date: '',
    amount: '',
    check_number: '',
    debit: '',
    payee_id: '',
    notes: '',
    expense_expense_categories_attributes:[]
  });
}

Here I patch the form to set values from an object retrieved from my backend API.

private patchForm() {
  this.expenseEditForm.setValue({
    date: '',
    amount: this.expense.amount_cents,
    check_number: this.expense.check_number,
    debit: this.expense.debit,
    payee_id: '',
    notes: this.expense.notes,
    expense_expense_categories_attributes:  this.fb.array([
      this.setExpenseCategories(),
    ])
  });
}

This is where I'm stuck. How do I push onto a FormArray. If I try to push, I get an error stating that push it doesn't exist on FormArray.

private setExpenseCategories() {
  for ( let expenseCategories of this.expense.expense_expense_categories){
    this.fb.array.push([
       this.fb.group({
        expense_category_id: [expenseCategories.expense_category_id, Validators.required],
        amount: [expenseCategories.amount_cents]
      ])
    });
  }
}

Just in case it's needed. Here's my html.

<div
  *ngFor="let expensecategoriesCtl of expenseEditForm.controls.expense_expense_categories_attributes.controls let i = index"
  [formGroupName]="i"
  style="margin-top: 10px;">

  <md-card>
    <md-select class="full-width-input"
               placeholder="Expense Category"
               id="expense_category_id"
               formControlName="expense_category_id"
    >

      <md-option *ngFor="let expenseCategory of expenseCategories" value="{{expenseCategory.id}}">
        {{expenseCategory.category}}
      </md-option>
    </md-select>

    <md-input-container class="full-width-input">
      <input
        mdInput placeholder="Amount"
        type="number"
        formControlName="amount">
    </md-input-container>
  </md-card>
</div>
ctilley79
  • 2,151
  • 3
  • 31
  • 64

2 Answers2

22

Some changes to DeborahK's answer, since expense.expense_expense_categories doesn't contain a primitive types, but objects. Therefore we cannot assign the values as is, but each object needs to be wrapped in a FormGroup, just like you have attempted.

Here I have a shortened version of your code:

Build the form:

ngOnInit() {
  this.expenseEditForm = this.fb.group({
    notes: [''],
    // notice below change, we need to mark it as an formArray
    expense_expense_categories_attributes: this.fb.array([])
})

Then we call patchForm in the callback, just like you have. That function would look like this, notice, we call this.setExpenseCategories outside:

patchForm() {
  this.expenseEditForm.patchValue({
    notes: this.expense.notes,
  })
  this.setExpenseCategories()
}

Then comes the biggest change from your existing code, where we first assign the FormArray to the variable control and then we iterate your array received from backend, create a FormGroup for each object and push the object to each FormGroup:

setExpenseCategories(){
  let control = <FormArray>this.expenseEditForm.controls.expense_expense_categories_attributes;
  this.expense.expense_expense_categories.forEach(x => {
    control.push(this.fb.group(x));
  })
}

Then to the template, this example is without Angular Material:

<form [formGroup]="expenseEditForm">
  <label>Notes: </label>
  <input formControlName="notes" /><br>
  <!-- Declare formArrayName -->
  <div formArrayName="expense_expense_categories_attributes">
    <!-- iterate formArray -->
    <div *ngFor="let d of expenseEditForm.get('expense_expense_categories_attributes').controls; let i=index"> 
      <!-- Use the index for each formGroup inside the formArray -->
      <div [formGroupName]="i">
      <label>Amount: </label>
        <input formControlName="amount" />
      </div>
    </div>
  </div>
</form>

Finally a

Demo

AT82
  • 71,416
  • 24
  • 140
  • 167
  • Which approach is the best? Is the APM-Updated demo that Deborah linked a better solution? Should I convert the object and instantiate an Expense Category class, or is the approach I was attempting better? Your demo doesn't populate the amount fields with values. Can you update the demo to illustrate this? – ctilley79 Apr 29 '17 at 02:07
  • @Alex I've followed this solution and found that the FormGroup in FormArray gets pushed every time resulting in the display of more FormGroup than required. – 20B2 Mar 20 '18 at 18:34
  • @20B2 It's been a while since answering this question :D But I don't see why this solution would cause such a problem. Could you perhaps provide a stackblitz with your issue and I'd be happy to take a look :) – AT82 Mar 20 '18 at 18:38
  • @Alex I've used your solution in modal. The first time I open the modal, the result is fine. But as I close the modal and reopen the same modal or open another modal, the FormGroup gets pushed to the previously pushed array resulting in the more FormGroups. I've solved this by passing boolean parameter. Whenever the modal is closed, I passed false as parameter and clear the control array. Currently I'm looking for better solution, but it is working fine for now. – 20B2 Mar 20 '18 at 18:44
  • 1
    @20B2 You have some kind of different setup going on, since for this case and the question at hand this solution solves the issue. But you are using a modal, so it's a different scenario, and seems that it does push just new formgroup to the existing form array like youy say, and that would be expected behavior. So yes, I guess you need to clear it. Hard to say anything else since there is no code to look at :) – AT82 Mar 20 '18 at 18:50
  • Thank you very much @AJT82, This is working for me. :) – sachinsuthariya Jun 02 '20 at 16:08
3

If I understand your question correctly, you may need something like this:

    // Update the data on the form
    this.productForm.patchValue({
        productName: this.product.productName,
        productCode: this.product.productCode,
        starRating: this.product.starRating,
        description: this.product.description
    });
    this.productForm.setControl('tags', this.fb.array(this.product.tags || []));

You can see the complete example here: https://github.com/DeborahK/Angular2-ReactiveForms in the APM - Updated folder.

DeborahK
  • 57,520
  • 12
  • 104
  • 129