What I am trying to do: I am building a reusable repeater of FormGroups (reactive forms) where the generic part of the repeater provides the add/remove controls and logic and all components that want to use it only have to provide the template that has to be repeated. I have checked
which shows how something like this can be done the template driven way.
Where I am stuck: the formControlName
directive that I attach to the controls within the <template>
is not picked up by the generic part of the repeater:
Error: Cannot find control with name: 'id'
at _throwError (https://unpkg.com/@angular/forms/bundles/forms.umd.js:1527:15)
at setUpControl (https://unpkg.com/@angular/forms/bundles/forms.umd.js:1465:13)
at FormGroupDirective.addControl (https://unpkg.com/@angular/forms/bundles/forms.umd.js:3889:13)
at FormControlName._setUpControl (https://unpkg.com/@angular/forms/bundles/forms.umd.js:4318:48)
at FormControlName.ngOnChanges (https://unpkg.com/@angular/forms/bundles/forms.umd.js:4264:22)
Code: (plunkr)
RepeaterComponent2
@Component({
selector: 'repeater2',
template: `
<div *ngIf="formArray">debug formArray: {{formArray.length}}</div>
<div [formGroup]="formGroup">
<input type="button" value="add" (click)="addRow()" noBootstrap>
<div formArrayName="values" class="repeater" *ngFor="let row of model; let i = index">
<div [formGroupName]="i" class="repeaterRow">
<input type="button" value="remove" (click)="removeRow(row, i)" noBootstrap>
<template [ngTemplateOutlet]="itemTemplate" [ngOutletContext]="{item: row}"></template>
</div>
</div>
</div>
`
})
export class RepeaterComponent2 implements OnInit {
/* initial model */
@Input() model: any[] = [];
/* function to create a new item */
@Input() newItem: (i) => {};
/* function to create a new FormGroup to fit the item */
@Input() newItemFormGroup: (fb: FormBuilder, model: any) => FormGroup;
/* root form group where the local FormArray is attached to */
@Input() formGroup: FormGroup;
@Output() repeaterArrayChanged: EventEmitter<any[]>;
@ContentChild(TemplateRef) itemTemplate: TemplateRef<any>;
formArray: FormArray;
constructor(private fb: FormBuilder) {
this.repeaterArrayChanged = new EventEmitter<any[]>();
}
ngOnInit() {
this.formArray = this.fb.array([]);
this.formGroup.addControl('values', this.formArray);
this.model.forEach((item: any) => this.addItemFormGroup(item))
}
addRow() {
let id = this.model.length + 1;
let obj = this.newItem(id);
this.model.push(obj);
this.addItemFormGroup(obj);
this.repeaterArrayChanged.emit(this.model);
}
addItemFormGroup(item: any) {
this.formArray.push(this.newItemFormGroup(this.fb, item));
}
removeItemFormGroup(i) {
this.formArray.removeAt(i);
}
removeRow(row, i) {
this.model = this.model.filter((x: any) => x !==row);
this.removeItemFormGroup(i);
this.repeaterArrayChanged.emit(this.model);
}
}
How it should be used:
@Component({
selector: 'my-app',
template: `
<div>
<div>{{debugForm | json}}</div>
<form [formGroup]="formGroup">
<!-- passing in:
- the root form group (the formArray of the repeater adds itself to it)
- the initial model items
- a callback that is used to create a new model item when "add" is clicked
- a callback that is used to create a "suitable" FormGroup for the passed model item
-->
<repeater2 [formGroup]="formGroup" [model]="model.items" [newItem]="newItem" [newItemFormGroup]="newItemFormGroup">
<template let-row="item">
<span>{{row | json}}</span>
<!-- !!! THIS FORM CONTROL IS NOT PICKED UP !!! -->
<input type="text" formControlName="id" noBootstrap>
</template>
</repeater2>
</form>
</div>
`
})
export class RepeaterTestComponent2 implements OnInit {
model: Model;
formGroup: FormGroup;
constructor(private fb: FormBuilder) {
this.model = new Model(1, [
new Item(1, 'one'),
new Item(2, 'two'),
new Item(3, 'three')
]);
}
ngOnInit() {
this.formGroup = this.fb.group({});
}
newItem(i): any {
return new Item(i, 'xxx');
}
newItemFormGroup(fb: FormBuilder, model: any): any {
return fb.group({
id: []
});
}
get debugForm() {
return {
value: this.formGroup.value
};
}
}