0

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

3 Answers 3

1

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())   
Sign up to request clarification or add additional context in comments.

Comments

0

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

1 Comment

Thanks, I think maybe I wasn't clear. There will be an array of customSongProperties and the only field that needs to be rendered for user input is the 'value' property of each customSongProperty.
0

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;  

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.