2019: A draggable forms odyssey

Hi, I am HAL9000. Please call me HAL. I hope you had a nice trip in deep sleep. Thankfully you woke up, since there is a lack in our angular material 7 knowledge which I can't fix by myself. Let me take over the ship controls and I will guide you through these angular material cdk and reactive forms instructions.
You can trust me.
Since the release of Angular 7 in October 18, the framework includes new exciting features. One of them is drag and drop in Angular Materials Component Dev Kit (CDK).
Considering you have an existing Angular 7 project, add @angular/material to your dependencies. Also this article requires basic experience with angular material.

The template

At first we will create our html template including the form. To bind the reactive form of type FormGroup created in the component class to the template, use the formGroup attribute provided by Angulars forms module and pass in the form instance. The forms direct child has to be a FormArray container, which also will act as the container, where we will be able to drop our elements. Note the cdkDropList, which makes the element a drop container and the cdkDropListDropped attribute, with which we are able to catch all the drop events.
<form [formGroup]="form"> <div formArrayName="sections" cdkDropList (cdkDropListDropped)="drop($event)"> <div *ngFor="let section of sections?.controls; let i = index" [formGroupName]="i" cdkDrag> <button type="button" mat-icon-button cdkDragHandle> <mat-icon>unfold_more</mat-icon> </button> <input matInput formControlName="text" placeholder="Text"> </div> </div> </form>
Listing: form-drag.component.html - The reactive forms template
The next part is to have a look at are the FormArray elements created with the ngFor loop. It loops through all elements in the form array and also provides an index for usage in the template. Each of these elements will be attributed with cdkDrag to let angular know that these items are draggable. Since we do not want to drag the elements when pressing the mouse at the input, it would be nice to have a drag handle. This is the button for, given the attribute cdkDragHandle.

The component class

import { Component } from '@angular/core'; import { FormGroup, FormControl, FormArray } from '@angular/forms'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; @Component({ selector: 'app-form-drag', templateUrl: './form-drag.component.html' }) export class FormDragComponent {}
Listing: form-drag-component.ts
The next step is adding a public FormGroup member called form to the class.
form: FormGroup;
Listing: Public FormGroup member of our component
You could also define your members with the public or private keywords. No keyword automatically makes it public, which we really need since we access it in the template. Instantiate the item in the components constructor, since this is the first place to execute any code in a class.
constructor() { this.form = new FormGroup({ items: new FormArray([]) }); }
Listing: Class constructor - create the form instance in here
To reduce template logic create a getter function which just returns a part of the form, the items.
get items(): FormArray { return this.form.get('items') as FormArray; }
Listing: Getter function to receive items
The last thing to do is to implement the drop() function for catching the drop events and move the items inside the array.

Move an item in a group

Imagine a group of k items. These items represent the items in the form array.
[i1,i2,...,in1,in,...,ik][i_1,i_2, ... , i_{n-1}, i_{n},..., i_{k}]
We now want to move the item from the first position to the n-1'th position. After moving the List above will then look like
[i2,...,in1,i1,in,...ik][i_2, ..., i_{n-1}, i_1, i_{n}, ... i_{k}]
And this should also work when moving items in the other direction from the n-1'th position to the second position for example
[i2,i1,i3,...,in1,in,...ik][i_2, i_1, i_3, ..., i_{n-1}, i_{n}, ... i_{k}]
Okay let's create the drop function now. At first we will determine the direction of the move, from an higher position to a lower position and vice versa. The elements position which gets dragged before dragging will be the from index and the position where it is dropped will be the to index. Lets store the element at from in a temporary variable, so that we can overwrite this position without loosing its data.
In the for loop we now move from our from position to the to position. This is why we needed the direction first. Imagine a drag and drop from from = 2 to to = 5. Then in the first step of the loop it is going to pick the element at index 3 and replaces it with the element at index 2. The next step is picking index 4 and store it in index 3 and so on. After looping, the to and the to - 1 element will be equal, but we want the item at from, which we stored in a temp variable, to be located at position to.
drop(event: CdkDragDrop<FormGroup[]>) { const dir = event.currentIndex > event.previousIndex ? 1 : -1; const from = event.previousIndex; const to = event.currentIndex; const temp = this.sections.at(from); for (let i = from; i * dir < to * dir; i = i + dir) { const current = this.sections.at(i + dir); this.sections.setControl(i, current); } this.sections.setControl(to, temp); }
Listing: drop function catches drop event and moves items

Concluding words

Hopefully you learned how to implement drag and drop in combination with angulars reactive forms. Since these to not look very smooth without any animations, follow the examples in the angular material documentation to style and animate them properly.
I've also have made a feature and pull request in the official angular repository to provide a member function which simply lets you do the following
drop(event: CdkDragDrop<FormGroup[]>) { this.items.moveItem(event.previousIndex, event.currentIndex); }
Listing: Pull request in angular library
If you like this approach, take a look at my code and like my pull request and comments to increase the relevance. You do not want to miss any of my articles? Follow me on twitter.

References

HAL image: https://de.wikipedia.org/wiki/2001:_Odyssee_im_Weltraum

Comments

Any questions? Want to join the discussion?

Sign in or sign up