Can't get Angular Dynamic Nested FormArrays to work

24 views Asked by At

I'm trying to make an array of books where each book has multiple authors that I can add or remove

I'm trying to learn angular and I've come across a need to have an array nested within another array.

I'm trying to male an array of books where each book would have an array of authors. I keep getting the error:

Cannot find control with path: 'books -> 0 -> authors -> 0

here is my ts code :

import { Component, Inject, OnInit } from '@angular/core';
import {FormArray, FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';



import {STEPPER_GLOBAL_OPTIONS} from '@angular/cdk/stepper';

/**
 * @title Stepper that displays errors in the steps
 */
@Component({
  selector: 'app-home',
  templateUrl: 'home.component.html',
  styleUrls: ['home.component.scss'],
  providers: [
    {
      provide: STEPPER_GLOBAL_OPTIONS,
      useValue: {showError: true},
    },
  ],
})
export class HomeComponent implements OnInit {

  bookForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.bookForm = this.fb.group({
      books: this.fb.array([])  // Create an empty FormArray for books
    });
  }

  ngOnInit(): void {
      this.createBook()
  }

  get books() {
    return this.bookForm.get('books') as FormArray;
  }

  addBook() {
    this.books.push(this.createBook());
  }

  removeBook(index: number) {
    this.books.removeAt(index);
  }

  createBook() {
    return this.fb.group({
      title: ['', Validators.required],
      authors: this.fb.array([this.createAuthor()])  // Initialize with an empty author control
    });
  }
  
  createAuthor() {
    return this.fb.control('', Validators.required);
  }
  
  

  getAuthors(bookIndex: number) {
    return this.books.at(bookIndex).get('authors') as FormArray;
  }

  addAuthor(bookIndex: number) {
    const book = this.books.at(bookIndex) as FormGroup;
    const authors = book.get('authors') as FormArray;
    authors.push(this.createAuthor());
  }
  
  removeAuthor(bookIndex: number, authorIndex: number) {
    const book = this.books.at(bookIndex) as FormGroup;
    const authors = book.get('authors') as FormArray;
    authors.removeAt(authorIndex);
  }
  

  

  onSubmit() {
    if (this.bookForm.valid) {
      // Handle form submission
      console.log(this.bookForm.value);
    }
  }
 
}

And here is my HTML code:

<form [formGroup]="bookForm" (ngSubmit)="onSubmit()">
    <div formArrayName="books">
      <div *ngFor="let book of books.controls; let bookIndex = index">
        <div [formGroupName]="bookIndex">
          <div>
            <label>
              Book Title:
              <input formControlName="title">
            </label>
            <button type="button" (click)="removeBook(bookIndex)">Remove Book</button>
          </div>
          <div formArrayName="authors">
            <div *ngFor="let author of getAuthors(bookIndex).controls; let authorIndex = index">
              <div [formGroupName]="authorIndex">
                <label>
                  Author:
                  <input formControlName="authorName">
                </label>
                <button type="button" (click)="removeAuthor(bookIndex, authorIndex)">Remove Author</button>
              </div>
            </div>
            <button type="button" (click)="addAuthor(bookIndex)">Add Author</button>
          </div>
        </div>
      </div>
      <button type="button" (click)="addBook()">Add Book</button>
    </div>
    <button type="submit">Submit</button>
  </form>
  
1

There are 1 answers

0
Eliseo On

A FormArray can be a FormArray of FormGroup (store an array of objects), a FormArray of FormControls (store an array of string, numbers, Dates, boolean..) or a FormArray of FormArrays (store an array of an array)

1.- If your authors are a FormArray of FormControls (only store the name) you need change the .html

    <div formArrayName="authors">
        <div *ngFor="let author of getAuthors(bookIndex).controls; let authorIndex = index">
            <!--you not use <div [formGroupName]="authorIndex">-->
            <label>
              Author:
              <!-see that you use [formControlName]="authorIndex"-->
              <input [formControlName]="authorIndex">
            </label>
            <button type="button" 
                    (click)="removeAuthor(bookIndex, authorIndex)">
                  Remove Author
            </button>
        </div>
        <button type="button" (click)="addAuthor(bookIndex)">Add Author</button>
      </div>

2.- If your authors are a formArray of formGroups, your function create author should be like

  createAuthor() {
    return this.fb.group({
      authorName:this.fb.control('', Validators.required)
    })
  }

NOTE: you can, to check the value or your form, write at the end of your component.html

<pre>
{{bookForm.value|json}}
</pre>