Angular 2/4 Edit Form Populate FormArray Controls
Asked Answered
T

2

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>
Tobin answered 28/4, 2017 at 0:41 Comment(0)
H
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

Hygrophilous answered 28/4, 2017 at 8:3 Comment(6)
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?Tobin
@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.Dvina
@Dvina 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 :)Hygrophilous
@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.Dvina
@Dvina 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 :)Hygrophilous
Thank you very much @AJT82, This is working for me. :)Darkness
K
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.

Kiaochow answered 28/4, 2017 at 0:48 Comment(2)
Cool. I looked at the sample app and i'll try to implement it tomorrow. CRUD operations are 95% of the programming I do.Tobin
Try this: this.tags.at(index).patchValue(text);Gerber

© 2022 - 2024 — McMap. All rights reserved.