Easy way to add Drag&Drop to your GWT based applications

I am programming a mid-office application with the GWT 2.5.0 front end. I wanted to improve the UX and came to the conclusion that drag & drop is the best way how to match two different data lists together (with different size, attributes, limitations, specifics and behavior). But GWT 2.5.0. does not support the drag & drop function.

So I decided to search for a library and extend some classes as little as possible, ideally with nice examples and documentation. I found a solution developed by Fred Sauer (all his publicly presented work and contributions are on GitHub) that has plenty of examples. On the other hand, there are 14 different solutions for specific situations. I used two examples with different logic and put them together. The goal of this blog is to show you how I made it work.  

To explain the process for the developers, I will describe what to do and what to avoid. I had quite limited time, as most developers do, so there still might be a nicer way. If you find one, please let me know! Thanks.

 

Step 1: What do I want?

The first and most crucial step was to decide what exactly I want to do. It sounds easy, right, I have an object that I want to drag from somewhere and drop it somewhere else, what’s the fuss about? But think about it, because I have underestimated this step and lost a lot of time by redesigning it over and over (and 3 times from the scratch). The best part is that 99 % of the solution is already explained in the provided examples and you just need to realize which one it is. 

These questions might help you to realize what you exactly want. Let’s suppose we have a list of unique objects and 2 different areas where you can drag & drop them:

  • Does object order within an area matter?
  • Is there a limitation on the number of objects in area?
  • Is the behavior of both areas the same?
  • Is an area a drag source, a drop target, or both?
  • Is there a condition, when objects cannot be dropped to an area?
  • Is there some restriction, where the objects cannot be dropped to an area?
  • Can you move the objects within an area?

I will present my problem from a different viewpoint to help you imagine the situation:

An organization has a predefined number of garages and a number of cars. The amount of cars can differ every time – it can be more (usual scenario) or less than the garages. Also, some garages might be closed for cleaning. As input you have the number and the position of garages, so you know, which remote to give to which driver. You also have defined if a car is in the garage already. And then you have a list of all cars – some of them are already parked in the garage. It must be decided, which car from the example above to park where. The rest of the cars that cannot be placed into garages do not matter. Also you can decide which garage to clean. If there is a car in it already, it must leave immediately.

 

 

What you can see in the picture is that we have 5 garages, 2 are being cleaned (X), one is taken () and 2 are still free (?). Obviously, the garages cannot be moved, only the cars in them can be. Just from this example, we can find answers from the previous questions (2 areas: parked cars as A1 and waiting cars as A2):

  • In A1 the order is important, we know Car Adam is in the 2nd garage.
  • The A1 has limited number of cars – exactly the number of usable garages.
  • The areas do NOT have the same behavior. The waiting cars are not in a queue, it does not matter which car comes first and last. You can move these cars (as draggable objects) within this area without any consequences.
  • In A1, you cannot drop a car to a garage, where a car is already, or when it is being cleaned. In A2 you can drop any car anywhere.
  • In A1 are strict places, where you can put cars, you cannot add a new “line” in the table, because there are no more garages.
  • In A1, if you drag a car and move it elsewhere in A1 (must be available), it changes the situation; the car must be parked in the other garage, and the previous one is empty, thus other car can be moved there.

Of course, for your situation there may be more questions, more areas for drag & drop and also more limitations.

 

Step 2: Develop it

When you are sure, what you want, the easy part comes - implement it! As a starter, add gwt-dnd dependency (this is the version we already had) in your pom.xml file. Already newer versions are available now, check the link here.

<dependency>
<groupId>com.allen-sauer.gwt.dnd</groupId>
<artifactId>gwt-dnd</artifactId>
<version>3.2.3</version>
</dependency>

Next, you have to specify what must be a DropTarget and a DragSource – from the name it should be clear, that from DragSource you can drag the object, and you can either put it back, or drop it somewhere. Dropping is possible only on DropTargets, which can be the same area as the DragSource, but not necessarily.

Here the CarWidget class, extends com.google.gwt.user.client.ui.HTML - this is the object I need to move, the id is needed for specifying which car is being moved.

public class CarWidget extends HTML {

    private String id;

    public CarWidget(Car car, DragController dragController) {
        this.id = car.getId();
        setVisible(true);
        setText(car.getName());
        setStyleName("dnd-car");
        dragController.makeDraggable(this);
    }
}

As we can see, the CarWidget must be provided with a dragController that will be from now on responsible for moving the instance. After you call the makeDraggable method, carWidget can be dragged. Also a style name is already added, because we want every car to look the same while moving/dragging/dropping.

The areas need just a bit more work. Let’s focus on the easier area – A2, waiting cars. We must realize, that this panel must accept to drop a Car object, but also to drag it from here. So it is both DropTarget and also DragSource.

After some reconsideration, I chose the Insert Panel solution. I have only one column, into which I put, remove or re-adjust cars. If it is empty, it’s no problem to add any car back there (there is an empty hidden widget always at the end that enables this). This solution dynamically changes the number of cars – there is no “empty place” visible, if you remove the car.

I used the presented VerticalPanelWithSpacer class. The example is perfect for our situation and nicely animated. If you add the car in between 2 cars, it is added in the middle and the car(s) below are moved down.

To use it, just create a Panel, into which you will add some heading and this VerticalPanelWithSpacer. On this you can add all the cars that are not already parked somewhere in a garage.

 

// initialize inner vertical panel to hold individual widgets
 allCarsPanel = new VerticalPanelWithSpacer();
 allCarsPanel.setSpacing(0);
 
 // initialize a widget drop controller for the current column
 carsWidgetDropController = new PanelDropController(allCarsPanel);
 widgetDragController.registerDropController(carsWidgetDropController);
 
 // Put together the column pieces
 Label heading = new Label("Cars waiting for garage");
 columnCompositePanel.add(heading);
 columnCompositePanel.add(allCarsPanel);
 if (cars != null) {
    for (Car car : cars) {
       if (!car.isParked()) {
          CarWidget widget = new CarWidget(car, widgetDragController);
          widget.addClickHandler(clickHandler);
          allCarsPanel.add(widget);
       }
    }
 }

 

As for the PanelDropController, it only extends VerticalPanelDropController, with some overriding onDrop() method to change some style and properties. For most solutions, this will not be necessary and VerticalPanelDropController is sufficient. We create 1 dropController for the entire panel. There is no reason to refuse a car, so the drop is always allowed.

It is important to notice the widgetDragController – it is an instance of PickupDragController and got here via constructor arguments. Everything that is dragged from the widgetDragController can be dropped on the allCarsPanel which is our A2 area. For this, you must call the registerDropController method. As you might already have guessed, this exact step is needed for all DropTargets.

The more complex part – A1 has a different behavior. It has a predefined number of places that cannot change. So we can imagine it as a table with fixed number of columns that can be filled. First big difference is that every cell has its own dropController. Then you can easily specify the condition when the place is already taken. The solution was to use the puzzle example with just 1 column and specified size. Again the dragController is passed as a constructor argument.

 

FlexTable flexTable = new FlexTable();
 
 // initialize horizontal panel to hold our columns
 HorizontalPanel horizontalPanel = new HorizontalPanel();
 horizontalPanel.add(flexTable);
 
 setWidget(horizontalPanel);
 int j = 0;
 
 Label header = new Label("Parked cars");
 simplePanel.add(header); // for each Garage object create a slot
 if (garages.size() > 0) {
    garageSlots = new ArrayList( garages.size());
    for (Garage garage : garages) {
 
       // create a simple panel drop target for the current cell
       simplePanel = new SimplePanel (j, garage.getKey());
       simplePanel.setPixelSize(WIDTH, HEIGHT);
       garageSlots.add(simplePanel);
       flexTable.setWidget(j++, 0, simplePanel);
       ... some logic for parked cars       if (!garage.isCleaned()) {
          simplePanel.setIgnored(true);
       }
 
       // instantiate a drop controller of the panel in the current cell
       TableDropController dropController = new TableDropController(simplePanel);
       widgetDragController.registerDropController(dropController);
    }
 }	

 

For every garage a new column in the flexTable is created with corresponding simplePanel(garageSlot). When we drop the CarWidget, we basically just add it to the corresponding garageSlot, and remove it from there when dragging away.

The TableDropController overrides onPreviewDrop method. We need to define situations (conditions) for which no object can be dropped here.

 

public class TableDropController extends SimpleDropController {
 
    private final SimplePanel dropTarget;
 
    ...
    @Override
    public void onDrop(DragContext context) {
       dropTarget.setWidget(context.draggable);
       if (context.draggable instanceof CarWidget) {      ... some logic, style updates             }
 
       super.onDrop(context);
    }
 
    @Override
    public void onPreviewDrop(DragContext context) throws VetoDragException {
       if (dropTarget.getWidget() != null) {
          throw new VetoDragException();
       }
       super.onPreviewDrop(context);
    }
 
 }

So when the VetoDragException is thrown, the controller will not drop the object in the table. This conditions checks, if the corresponding SimplePanel(garageSlot) contains a widget (the Car widget that we add there when dropping it).

The last step to have the basics is to provide 1 panel as a parent for both areas and add them there. I have used the Absolute panel, set some attributes and added the areas. Also, here we need to create the PickupDragController that we provide for A1 and A2. The construction parameters needed are: the parent panel and if we allow objects to be put outside the parent panel.

 

widgetDragController = new PickupDragController(this, false);
widgetDragController.setBehaviorDragStartSensitivity(1);

                  

The setBehaviorDragStartSensitivity method is interesting. If you don’t set it, or set it to 0, all click actions will be ignored. No click handlers make sense in this case. If you need action on click, you need to set it bigger than 0. I recommend to set it to 1. If a bigger number is there, an undesired selection of a half of the page is shown. If you do not need it, leave the default value (0), it displays the dragging process in the nicest way.

 

Step 3: Play with it!

And this is it! Of course, for extra features more classes and methods must be implemented, but with this you have a functional skeleton to build on. Some of the steps needed and implemented were:

  • Changing a style of a car widget. Since in A2 it is the “column” itself, it needs board, padding, margins and other styles. These are redundant in A1, because we set them for the static Simple panels
  • On some extra button action a car was moved from A1 to A2, because user decided to clean the garage (dualListBoxExample)

 

If I can point out one thing for the readers, I would stress out the preparation phase. First think and question the usage of drag & drop for your situation. The implementation itself is not difficult, it took me less than a week together with styling. Now, Drag & Drop is used at the project and the client is happy with the solution. I am happy too.

 

photo source_www.carbuzz.com

 

Publikované: 10. aug 2015 16:50 | Počet prečítaní: 2793
  • Jarmila Jančigová

    Java Developer

    Jarka pracuje v Davinci Software na pozícii Java developer. Preferuje prácu na backende a vyvíja veci test driven development spôsobom. Keď nepracuje, rada maká na Power joge, počúva hudbu a objavuje výmysly zahraničnej kuchyne.

We want you

Do you see yourself working with us? Check out our vacancies. Is your ideal vacancy not in the list? Please send an open application. We are interested in new talents, both young and experienced.

Join us