Reputation: 333
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.
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
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
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
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>
Upvotes: 0