Jason Smith
Jason Smith

Reputation: 333

Create and Fill Reactive FormArray Angular 10

Update: I think I'm getting closer. This is what I have now:

  songEditForm = this.fb.group({
    title: [null, [Validators.required, Validators.maxLength(128)]],
    projectId: [null, [Validators.required]],
    artist: [null, [Validators.required, Validators.maxLength(128)]],
    album: [null, [Validators.maxLength(128)]],
    minutes: [null, [Validators.min(0), Validators.max(99)]],
    seconds: [null, [, Validators.min(0), Validators.max(59)]],
    songParts: [null, [Validators.maxLength(4000)]],
    timeSignature: [null, [Validators.maxLength(10)]],
    songKey: [null, [Validators.maxLength(10)]],
    bpm: [null, [, Validators.min(0), Validators.max(320)]],
    rating: [null, [, Validators.min(0), Validators.max(5)]],
    comfortLevel: [null, [, Validators.min(0), Validators.max(5)]],
    energyLevel: [null, [, Validators.min(0), Validators.max(11)]],
    notes: [null, [Validators.maxLength(512)]],
    genre: [null],
    isPublic: [null],
    isFavorite: [null],
    customSongProperties: this.fb.array([])
  });

  get customSongProperties() {
    return this.songEditForm.get('customSongProperties') as FormArray;
  }


      <mat-card formArrayName="customSongProperties" *ngFor="let customSongProperty of customSongProperties.controls; let i=index">
        <mat-form-field>
          <mat-label>{{customSongProperty.value.label}}</mat-label>
          <input matInput type="text" [formControlName]="i" name="i">
        </mat-form-field>
      </mat-card>

But I can't seem to bind the values from my array into the form array.

ts with data array shown

html render


ORIGINAL POST BELOW THIS LINE

I need to loop through an object/array and create zero or more input fields with labels. The object I want to bind the Reactive form array to has label and value properties (amongst others). I feel like I am close but I am getting this error message:

ERROR Error: Cannot find control with path: 'customSongProperties -> 0 -> value'

<ng-container formArrayName="customSongProperties">
  <mat-card *ngFor="let _ of customSongProperties.controls; index as i">
    <ng-container [formGroupName]="i">
      <input matInput formControlName="value.value" name="index" placeholder="value.label" maxlength="50" />
    </ng-container>
  </mat-card>
</ng-container>

This is how I am trying to fill the form array:

this.data.customSongProperties.forEach(customSongProperty => {
  this.customSongProperties.push(new FormControl(customSongProperty));
});

This is the object I am binding to and trying to build form fields from:

export class CustomSongProperty {
  id: number;
  userId: number;
  songPropertyDataTypeId: number;
  songPropertyDataTypeName: string | null;
  label: string | null;
  songId: number;
  value: string | null;
}

This seems right to me, but clearly is not. I was following this tutorial: Reactive Form Array Tutorial But my comprehension kind of fell apart at the end. Any help is appreciated.

Thank you

Upvotes: 0

Views: 3168

Answers (3)

Eliseo
Eliseo

Reputation: 57939

Jason, you can create a FormArray of FromControls or a FormArray of FormGroups (if the elements of the form array has an unique property or they are objects). e.g.

//e.g. you need a FormArray of FormControls if your json object is like
title:'my title'
customSongProperties:[ 'one','two','three']

//e.g. you need a FormArray of FormGroups if your json object is like
title:'my title'
customSongProperties:[ {value:'one'},{value:'two'},{value:'three'}]

With a FormArray of FormControls you use

<div formArraName="customSongProperties">
    <mat-card *ngFor="let customSongProperty of customSongProperties.controls; 
       let i=index" >
        <mat-form-field>
          <mat-label>{{customSongProperty.value.label}}</mat-label>
           <!--you use [formControlName]="i" for the 
             uniq FormControl in the formArray-->
          <input matInput type="text" [formControlName]="i" >
        </mat-form-field>
     </mat-card>
</div>

But in your case you has a FormArray of FormGroups, so the .html must be

<div formArraName="customSongProperties">
     <!--see that you indicate [formGroupName]="i"-->
    <mat-card *ngFor="let customSongProperty of customSongProperties.controls; 
       let i=index" [formGroupName]="i">
        <mat-form-field>
          <mat-label>{{customSongProperty.value.label}}</mat-label>
           <!--you use formControlName="nameOfProperty"
               remember that you can has severals FormsControls in the
               FormGroup
             -->
          <input matInput type="text" formControlName="value" >
        </mat-form-field>
     </mat-card>
</div>

About how create a FormGroup, always is interesting use a function that return a FormGroup and recived as data an object or null. As our FormArray is a FormArray of FormGroup we can do

getCustomSongPropertiesFormGroup(data:any=null)
{
   //if data is null we create an object by defect
   data=data || {id:0,userId:0...}
   return this.fb.group({
     id: [data.id],
     userId: [data.userId],
     ...
   })
 }

And to create the formGroup songEditForm

getSongFormGroup(data:any=null)
{
   //if data is null we create an object by defect
   data=data || {title:null,projectId:null...,customSongProperties:null}
   return this.fb.group({
     title: [data.title, [Validators.required, Validators.maxLength(128)]],
     projectId: [data.projectId, [Validators.required]],
     ...
     customSongProperties:data.customSongProperties?
                          fb.array(data.customSongProperties
                            .map(x=>this.getCustomSongPropertiesFormGroup(x)):
                          []
   })
}

Try explain a few the "map", if you has in data.customSongProperties an array of objects, you transform this array of object in an array of formGroup using map map(x=>this.getCustomSongPropertiesFormGroup(x) this is the array with we create the formArray.

Now you can use,e.g.

   //to create the form songEditForm
   this.songEditForm=this.getSongFormGroup()

   //to add a new element of the formArray
   this.customSongProperties.push(this.getCustomSongPropertiesFormGroup())   

Upvotes: 1

Jason Smith
Jason Smith

Reputation: 333

in ts class definition:

  songEditForm = this.fb.group({
    title: [null, [Validators.required, Validators.maxLength(128)]],
    customSongProperties: this.fb.array([
      this.fb.group({
        id: [null],
        songId: [null],
        label: [null],
        value: [null]
      })
    ])
  });
  
  get customSongProperties() {
    return this.songEditForm.get('customSongProperties') as FormArray;
  }

 setExistingCustomSongProperties(customSongProperties: CustomSongProperty[]): FormArray
  {
    const formArray = new FormArray([]);
    customSongProperties.forEach(customSongProperty => {
      formArray.push(
        this.fb.group({
          id: customSongProperty.id,
          songId: customSongProperty.songId,
          label: customSongProperty.label,
          value: customSongProperty.value
        }));
    });
    return formArray;
  }

in ngOnInit:

 this.songEditForm.setControl('customSongProperties', this.setExistingCustomSongProperties(this.data.customSongProperties));

in component html:

  <div formArrayName="customSongProperties" class="available-properties">
    <mat-card *ngFor="let customSongProperty of customSongProperties.controls; let i=index" [formGroupName]="i">
      <mat-form-field>
        <mat-label>{{customSongProperty.value.label}}</mat-label>
        <input matInput type="text" formControlName="value" [name]="i">
      </mat-form-field>
    </mat-card>
  </div>  

in onSubmit:

this.data.customSongProperties = this.songEditForm.value.customSongProperties;  

Upvotes: 0

Slawa Eremin
Slawa Eremin

Reputation: 5415

I think, it is better to use FormGroup in this case, you can use field names for generating controls and creating FormGroup, and in the template you can just go through array and show inputs:

Component:

export class AppComponent implements OnInit  {
  fieldNames: string[] = [];
  form: FormGroup;
  
  constructor(
  ) {}

  ngOnInit(): void {
    const controls: Record<string, FormControl> =  Object
      .keys(customSongProperty)
      .reduce( (res, fieldName) => {
        this.fieldNames.push(fieldName);
        res[fieldName] = new FormControl(customSongProperty[fieldName]);
        return res;
      }, {});

      this.form = new FormGroup(controls); 
   }
}

template:

<form [formGroup]="form">
  <ng-container *ngFor="let fieldName of fieldNames">
    <label>
      {{ fieldName}} : 
      <input [formControlName]="fieldName" maxlength="50" />
    </label>
  </ng-container>


  <div class="result">
    {{ form.value | json  }}
  </div>
</form>

code of the example

Upvotes: 0

Related Questions