Your problem
- You have 2 arrays of items of the same type
- You want to render a form array for each array
- You want to be able to move items between the arrays
- You want to be able to bind the form values back to the items on submit
The design
As with pretty much every component-based problem in Angular, you should think about model first. Your model is king, and everything else is built around it.
In your demo, you are moving items between lists. You are updating your model and binding your HTML to that. Nothing wrong there - it's the right approach and works.
The added challenge here is that you also want to move form groups around as well. We still need to think model-first, but also think about moving related state at the same time we move items between lists.
In abstract terms, you currently have
list1: [];
selected1: [];
list2: [];
selected2: [];
If you want to move items from 1 -> 2, your move method will remove any selected items from items1
to items2
. Simple.
Once you add forms, you will have a structure like this:
list1: [];
selected1: [];
form1: FormGroup;
formArray1: FormArray;
list2: [];
selected2: [];
form2: FormGroup;
formArray2: FormArray;
And when you move items from 1 -> 2, you will continue to move items from list1
to list2
, but now you will also need to remove the related item from formArray1
and add it to formArray2
.
I am storing a reference to the form arrays to make them easier to work with later.
Building the form
Working with form arrays is arguably the most complex part of this answer. The key with form arrays is that the HTML structure mimics the structure of the FormGroup
object you build.
We can use FormBuilder
to build a form from an array and bind to it like this:
component.ts
buildForm() {
this.model = [
{ value: 'a' }, { value: 'b' }, { value: 'c' }
];
// create a form group for each item in the model
const formGroups = this.model.map(x => this.formBuilder.group({
value: this.formBuilder.control(x.value)
}));
// create a form array for the groups
const formArray = this.formBuilder.array(formGroups);
// create the top-level form
this.form = this.formBuilder.group({
array: formArray
});
}
And bind to it in the HTML like this:
<form [formGroup]="form1" (submit)="onSubmit()">
<div formArrayName="array">
<div *ngFor="let item of list1; let i = index" [formGroupName]="i">
<input formControlName="value" />
</div>
</div>
<button>Submit</button>
</form>
This will generate an input for each item in the array, containing the values "a", "b", "c" respectively.
Moving items
Moving items between arrays is a simple javascript problem. We need to splice
the source array and push
to the destination array.
To move items from list 1 to list 2:
// move selected items from model 1
this.selected1.forEach(item => {
const index = this.list1.indexOf(item);
this.list1.splice(index, 1);
this.list2.push(item);
});
this.selected1.length = 0;
This will loop through each selected item in list 1, splice it from the list, push it to list 2. It will then clear the selected items;
Moving form groups
You will move form groups at the same time as you move items. It is similar in concept - you remove from one and add to the other. You built your form array from your model, so you know your indexes match.
// move selected items from model 1
this.selected1.forEach(item => {
const index = this.list1.indexOf(item);
const formGroup: FormGroup = this.formArray1.controls[index] as FormGroup;
this.list1.splice(index, 1);
// move between form arrays
this.formArray1.removeAt(index);
this.formArray2.push(formGroup);
this.list2.push(item);
});
Notice here how there are 2 lines to move between form arrays that look similar to the 2 lines used to move between regular arrays.
formArray.removeAt(index)
and formArray.push(formGroup)
are doing the moving. The difference with the form array is that we need to get a reference to it first using this.formArray1.controls[index] as FormGroup;
.
DEMO: https://stackblitz.com/edit/angular-3cwnsv
Caution
In this design the order in which you remove from and add to the arrays and forms is important. You are binding your HTML to both your arrays and your form. You are creating the array of inputs by looping over your array, and binding each item to the i
th group in the form array. If you remove from the form first, you will now have n
items in the array and n - 1
items in your form array. You will have an error when trying to bind to an undefined n
th form group.
You are now performing a transaction with multiple operations.
Aside from ensuring you remove and add in the correct order, one way around this is to use OnPush
change detection. Read up on this, as it's a good strategy for all but the simplest apps.
Next steps
I kept my demo simple for the purposes of the answer. It isn't particularly scalable or reusable as it stands. There is a lot of repeated code and bad naming to try and avoid getting distracted by nested components and property names that relate to your application.
In reality you would probably want to create a child component that is responsible for a lot of the code that I have duplicated. The design of that is definitely out of scope for this question.