Search code examples
angulartypescriptangular2-formbuilder

Angular 2/4 Edit Form Populate FormArray Controls


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>

Solution

  • 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