When building web applications, sooner or later, you will need to build either a drag-and-drop feature to allow intuitive interactions or a grid system UI to allow arbitrary positioning of elements in the layout. There are libraries for either of them in Angular 2, but there are no libraries for Angular 2 Drag-And-Drop Grid System as a whole.
Although this post is written at the time of Angular 2, it is also applicable for Angular 4 and future versions of Angular, according to the official Angular branding guideline.
This is where this blog post comes in as I will talk about how to implement such a system.
Update on 20 Oct: How to avoid issues with relative positioning of item and use absolute positioning
Drag-And-Drop Solutions
First and foremost, I need to clarify the meaning of drag-and-drop here. Normally it can mean one of the following two things, or both:
- Drag and drop an external resources (image, file) from other websites or local disk to the page
- Drag and drop items from one section of the UI to another, on the same web page
Here I am focusing on the second type of drag-and-drop.
For Angular 2, there are a few choices to implement this kind of drag-and-drop.
ng2-Dragula
The most popular one appears to the Angular 2 wrapper of Dragula library. It has a nice demo, good support for Angular 2 specific syntax, and some event listeners. However, there are a few issues with it that makes it an automatic solution to our use case:
Firstly, it does not support arbitrary positioning in a grid system. Its primary mechanism appears to be sorting , i.e. sorting a linear list in one dimensional ul
element, and moving items from one ul
to another ul
. Hence, it would be hard to implement it together with a grid system in which the positioning of items are not one-dimensional.
Secondly, its event listeners are not powerful enough. It does not support listening to events while dragging, makes it hard to do real-time dynamic changes to the items based on their current position while being dragged. Also, the drop event synchronizes the model before being called, but the DOM element is not updated yet with the dropped item when the event is called. For example, if we have the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
constructor(private dragulaService: DragulaService) { dragulaService.dropModel.subscribe((value: any) => { this.onDrop(value.slice(1)); }); } private onDrop(args: [HTMLElement, HTMLElement]) { let [e, el] = args; //... console.log('items in DOM:'); console.log(el.children.length); console.log('items in model:'); console.log(this.model.length); //... } |
We get the following output from the Chrome console:
Hence, it is only possible to get the position of the dropped item via the model, not the DOM element. Not a big deal, but kind of strange.
Old jQuery Style
With the new Zone.js, it is technically easier to use jQuery for DOM manipulation in Angular 2, as compared to Angular 1. In fact, when I first Googled about drag-and-drop Angular 2, I chanced upon this amazing project Trello Clone. It uses jQuery UI sortable and draggable together with standard Angular 2 components to implement such feature.
The appeal of the approach is that it is very mature, being widely used way before Angular 1 was created. However, it does not fit into the Angular’s model of data binding, so it will not be an idiomatic Angular 2 approach.
Grid System Solution
There is only one library that I found to be popular for implementing grid system in Angular 2. That is angular2-grid. It also comes with support for drag-and-drop, but only for within the grid container. I needed something that allows drag-and-drop across different containers, so this library is also not a drop-in solution.
Solution to Angular 2 Drag-And-Drop Grid System
Since there are no direct solution to this problem. We are left with either using some of the libraries as basis for extending, or writing our own library. I usually prefer the first approach so I went ahead and experimented with some of them.
It turned out that there is a trick to mimic grid-system in ng2-Dragula, by using placeholder items. ng2-Dragula uses a sorting based approach, so if we are able to transform the arbitrary positioning problem into a sorting problem, then we can use ng2-Dragula.
The key here is placeholder item. With placeholder items each occupying a fixed dimension, we can populate the list with real item and invisible placeholder items, such that it looks like the real items are being absolutely positioned.
Here’s a screenshot of the trick:
As you can see, the items in each vertical list appear to be arbitrarily positioned along the y-axis. But what is really happening is that there are placeholder items occupying the void spaces to create the illusion. These placeholder items appear in both model and DOM, so you can add any arbitrary logic to them to suit your need.
This approach also has an added benefit of easy manipulation of individual items’ positions. Since we know how many placeholders and other items are placed in front of the item, we can calculate its expected position anytime by querying the DOM. Also, when we place any items into an arbitrary position, we can get the position by querying the DOM as well.
Turn Relative Positioning Into Absolute Positioning
After experimenting further, it turned out that the approach described above has one major issue. The items in the list are relatively positioned, i.e. when the item on top gets moved somewhere else, the items below it also moves.
Luckily, there is a relatively simple solution to that as well, by turning placeholders into containers.
This means, instead of populating the column with placeholders, we populate them with containers, which contains the actual items. In this way, we can still use ng2-Dragula and apply the directive to each container in the columns, instead of the entire column. Since only the items get moved, all the containers remain stationary, and moving one item does not affect the others.
With additional help from the accepts
option from ng2-Dragula, we can also validate if dragging is allowed. This is helpful if we want to do collision detection between items since this approach does not avoid collision of items by default.
So there you go, a complete Angular 2 Drag-And-Drop Grid System built using some hack.
Really? Why Use a Hack?
Life does not always give you exactly what you want. Sometimes you need to hack a bit to get it. Of course I’d rather use a good idiomatic Angular 2 approach. But until such libraries come out, I will continue hacking my own solutions.