Angular 7 : Need help understanding issues I face when building a form with multilevel nested field groups

745 views Asked by At

As you can see on the attached image below, I've built a form in Angular 7. Form contains multiple levels of nested field groups. When pressing "Add Track" button, outer field group is pushed to the dom. Then inside you can press on "+" to add fields inside of each of the outer field groups.

Problem 1: I keep on receiving the following error and "Add Track" only pushes complete field group into index 1, passed that it is missing fields with "+" buttons that are populated by the nested *ngFor loops in HTML.

Error:

ERROR Error: Cannot find control with path: 'albumTracks -> 1 -> audioSources -> 1'
    at _throwError (forms.js:1775)
    at setUpFormContainer (forms.js:1757)
    at FormGroupDirective.push../node_modules/@angular/forms/fesm5/forms.js.FormGroupDirective.addFormGroup (forms.js:4541)
    at FormGroupName.push../node_modules/@angular/forms/fesm5/forms.js.AbstractFormGroupDirective.ngOnInit (forms.js:1887)
    at checkAndUpdateDirectiveInline (core.js:9243)
    at checkAndUpdateNodeInline (core.js:10507)
    at checkAndUpdateNode (core.js:10469)
    at debugCheckAndUpdateNode (core.js:11102)
    at debugCheckDirectivesFn (core.js:11062)
    at Object.eval [as updateDirectives] (AddAlbumsDinamicComponent.html:103)

Problem 2: When I fill out form and click "Save" button that then executes onSubmit() method and gets all entered form values by: "this.albumForm.value", I am missing bunch of values that I have entered into fields, which were pushed dynamically by pressing "Add Track" and "+" buttons.

Tried to find answers online, but could not find anything similar to what I am trying to do. All online examples are for simple forms, not for multilevel nested form groups. Also new to Angular, need help. S.O.S, thank you so much for your help.

enter image description here

component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray } from '@angular/forms';
import { PostRequestService } from '../post-request.service';

@Component({
  selector: 'app-add-albums-dinamic',
  templateUrl: './add-albums-dinamic.component.html',
  styleUrls: ['./add-albums-dinamic.component.css']
})

export class AddAlbumsDinamicComponent implements OnInit {

  albumForm: FormGroup;
  items: FormArray;
  arrayElement: FormArray;
  submitted = false;

  constructor(private formBuilder: FormBuilder, private postRequestService: PostRequestService) {}

  ngOnInit() {
    this.albumForm = this.formBuilder.group({
      albumTitle: '',
      albumCoverImage: '',
      datePublished: '',
      albumPurchaseSources: this.formBuilder.array([ this.createAlbumPurchaseSource() ]),
      albumTracks: this.formBuilder.array([ this.albumTracksDetails() ])
    });
  }

  createAlbumPurchaseSource(): FormGroup {
    return this.formBuilder.group({
      albumPurchaseSourceName: '',
      albumPurchaseURL: ''
    });
  }

  createAudioSource(): FormGroup {
    return this.formBuilder.group({
      audioSourceName: '',
      audioSourceURL: ''
    });
  }

  createVideoSource(): FormGroup {
    return this.formBuilder.group({
      videoSourceName: '',
      videoSourceURL: ''
    });
  }

  createTrackPurchaseSource(): FormGroup {
    return this.formBuilder.group({
      trackPurchaseSourceName: '',
      trackPurchaseSourceURL: ''
    });
  }

  albumTracksDetails(): FormGroup {
    return this.formBuilder.group({
      trackTitle: '',
      trackGenre: '',
      audioSources: this.formBuilder.array([ this.createAudioSource() ]),
      videoSources: this.formBuilder.array([ this.createVideoSource() ]),
      trackPurchaseSources: this.formBuilder.array([ this.createTrackPurchaseSource() ]),
      downloadURL: ''
    });
  }


  addAudioSource(i: number): void {
    const control = (<FormArray>this.albumForm.controls['albumTracks']).at(i).get('audioSources') as FormArray;
    control.push(this.createAudioSource());
  }

  addVideoSource(i: number): void {
    const control = (<FormArray>this.albumForm.controls['albumTracks']).at(i).get('videoSources') as FormArray;
    control.push(this.createVideoSource());
  }

  addTrackPurchaseSource(i: number): void {
    const control = (<FormArray>this.albumForm.controls['albumTracks']).at(i).get('trackPurchaseSources') as FormArray;
    control.push(this.createTrackPurchaseSource());
  }

  addTracks(): void {
    this.items = this.albumForm.get('albumTracks') as FormArray;
    this.items.push(this.albumTracksDetails());
  }

  addAlbumPurchaseSources(): void {
    this.items = this.albumForm.get('albumPurchaseSources') as FormArray;
    this.items.push(this.createAlbumPurchaseSource());
  }


  onSubmit() {
      this.submitted = true;

      this.postRequestService.
      postRequest(this.albumForm.value)
      .subscribe(
          (val) => {
              console.log("POST call successful value returned in body",
                          val);
          },
          response => {
              console.log("POST call in error", response);
          },
          () => {
              console.log("The POST observable is now completed.");
          });
  }


}

component.html

<div class="container">
  <div id="outer-container">
    <div id="sidebar">
      <div class="mycontent-left">
        <app-artist-admin-sidebar></app-artist-admin-sidebar>
      </div>
    </div>
    <div id="content">
      <form [formGroup]="albumForm" (ngSubmit)="onSubmit()" novalidate>
        <div><font size="4">Album Details:</font></div>
        <br/>
        <div class="row">
          <div class="col">
            <div class="col-sm-2 control-label"><font size="3">Album Title:</font></div>
            <div class="col-sm-3 nopadding">
              <input type="text" class="form-control" id="album_title" name="album_title" formControlName="albumTitle"
                     placeholder="Album Title">
            </div>
          </div>
        </div>

        <br/>
        <div class="row">
          <div class="col">
            <div class="col-sm-3 control-label"><font size="3">Album Cover Image:</font></div>
            <div class="col-sm-3 nopadding">
              <input type="file" class="form-control" id="cover_image" name="cover_image"
                     formControlName="albumCoverImage" placeholder="Album Cover Image">
            </div>
          </div>
        </div>

        <br/>
        <div class="row">
          <div class="col">
            <div class="col-sm-3 control-label"><font size="3">Date Published:</font></div>
            <div class="col-sm-3 nopadding">
              <input type="date" class="form-control" id="date_published" name="date_published"
                     formControlName="datePublished" placeholder="Date Published">
            </div>
          </div>
        </div>

        <br/>
        <div formArrayName="albumPurchaseSources"
             *ngFor="let item of albumForm.get('albumPurchaseSources').controls; let i = index;">
          <div [formGroupName]="i">
            <div class="row">
              <div class="col">
                <div class="col-sm-2 control-label"><font size="3">Album Purchase Source:</font></div>
                <div class="col-sm-3 nopadding">
                  <input type="text" class="form-control" id="album_purchase_source_name"
                         name="album_purchase_source_name" formControlName="albumPurchaseSourceName"
                         placeholder="Source Name">
                </div>
                <div class="col-sm-3 nopadding">
                  <div class="form-group">
                    <div class="input-group">
                      <input type="text" class="form-control" id="album_purchase_url" name="album_purchase_url"
                             formControlName="albumPurchaseURL" placeholder="URL">
                      <div class="input-group-btn">
                        <button class="btn btn-success" type="button" (click)="addAlbumPurchaseSources();"><span
                          class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>

        <hr/>
        <br/>
        <div><font size="4">Album Tracks Details:</font></div>
        <br/>
        <div formArrayName="albumTracks" *ngFor="let item of albumForm.get('albumTracks').controls; let i = index;">
          <div [formGroupName]="i">

            <div class="row">
              <div class="col">
                <div class="col-sm-2 control-label"><font size="3">Track Title:</font></div>
                <div class="col-sm-3 nopadding">
                      <input type="text" class="form-control" id="track_title" name="track_title"
                             formControlName="trackTitle" placeholder="Track Title">
                </div>
              </div>
            </div>

            <br/>
            <div class="row">
              <div class="col">
                <div class="col-sm-2 control-label"><font size="3">Track Genre:</font></div>
                <div class="col-sm-3 nopadding">
                  <input type="text" class="form-control" id="genre" name="genre" formControlName="trackGenre"
                         placeholder="Genre">
                </div>
              </div>
            </div>

            <br/>
            <div formArrayName="audioSources" *ngFor="let audioItem of item.controls['audioSources'].controls; let audioi=index;">
              <div [formGroupName]="i">
                <div class="row">
                  <div class="col">
                    <div class="col-sm-2 control-label"><font size="3">Audio:</font></div>
                    <div class="col-sm-3 nopadding">

                      <input type="text" class="form-control" id="audio_source_name" name="audio_source_name" formControlName="audioSourceName" placeholder="Audio Source Name">

                    </div>
                    <div class="col-sm-3 nopadding">
                      <div class="form-group">
                        <div class="input-group">

                          <input type="text" class="form-control" id="audio_url" name="audio_url" formControlName="audioSourceURL" placeholder="Audio Source URL">

                          <div class="input-group-btn">
                            <button class="btn btn-success" type="button" (click)="addAudioSource(i);"><span
                              class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>


            <br/>
            <div formArrayName="videoSources" *ngFor="let videoItem of item.controls['videoSources'].controls; let videoi = index;">
              <div [formGroupName]="i">
                <div class="row">
                  <div class="col">
                    <div class="col-sm-2 control-label"><font size="3">Video:</font></div>
                    <div class="col-sm-3 nopadding">

                      <input type="text" class="form-control" id="video_source_name" name="video_source_name" formControlName="videoSourceName" placeholder="Video Source Name">

                    </div>
                    <div class="col-sm-3 nopadding">
                      <div class="form-group">
                        <div class="input-group">

                          <input type="text" class="form-control" id="video_url" name="video_url" formControlName="videoSourceURL" placeholder="Video Source URL">

                          <div class="input-group-btn">
                            <button class="btn btn-success" type="button" (click)="addVideoSource(i);"><span
                              class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>

            <br/>
            <div formArrayName="trackPurchaseSources" *ngFor="let trackPurchaseItem of item.controls['trackPurchaseSources'].controls; let purchasei = index;">
              <div [formGroupName]="i">
                <div class="row">
                  <div class="col">
                    <div class="col-sm-2 control-label"><font size="3">Track Purchase Source:</font></div>
                    <div class="col-sm-3 nopadding">

                      <input type="text" class="form-control" id="purchase_source_name" name="purchase_source_name" formControlName="trackPurchaseSourceName" placeholder="Purchase Source Name">

                    </div>
                    <div class="col-sm-3 nopadding">
                      <div class="form-group">
                        <div class="input-group">

                          <input type="text" class="form-control" id="purchase_url" name="purchase_url" formControlName="trackPurchaseSourceURL" placeholder="Purchase Source URL">

                          <div class="input-group-btn">
                            <button class="btn btn-success" type="button" (click)="addTrackPurchaseSource(i);"><span
                              class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>


            <br/>
            <div class="row">
              <div class="col">
                <div class="col-sm-2 control-label"><font size="3">Download:</font></div>
                <div class="col-sm-3 nopadding">
                  <input type="text" class="form-control" id="download_url" name="download_url"
                         formControlName="downloadURL" placeholder="Download URL">
                </div>
              </div>
            </div>
            <br/><hr><br/>
          </div>
        </div>
        <div class="form-group">
          <button class="btn btn-success" type="button" (click)="addTracks();">Add Track</button>
        </div>
        <div class="form-group">
          <button class="btn btn-primary">Save</button>
        </div>
      </form>
    </div>
  </div>
</div>
1

There are 1 answers

2
muradm On BEST ANSWER

Works here: https://stackblitz.com/edit/so-53246132

Problem #1:

        <div formArrayName="audioSources" *ngFor="let audioItem of item.controls['audioSources'].controls; let audioi=index;">
          <div [formGroupName]="i">

But should be:

        <div formArrayName="audioSources" *ngFor="let audioItem of item.controls['audioSources'].controls; let audioi=index;">
          <div [formGroupName]="audioi">

Same for videoi and purchasei.

Problem #2:

  addTracks(): void {
    (this.albumForm.get('albumTracks') as FormArray).push(this.albumTracksDetails());
    // why? what is items?
    // this.items = this.albumForm.get('albumTracks') as FormArray;
    // this.items.push(this.albumTracksDetails());
  }

Also subject to change is:

  addAlbumPurchaseSources(): void {
    this.items = this.albumForm.get('albumPurchaseSources') as FormArray;
    this.items.push(this.createAlbumPurchaseSource());
  }