Inter-portlet Drag and Drop

Table of Contents
  Name Size Creator (Last Modifier) Creation Date Last Mod Date Comment  
ZIP Archive portlet-dnd-tutorial.zip 39 kB Arturo Zambrano May 14, 2013 May 14, 2013  

Introduction

Drag and Drop has always been a great user interface functionality that is easy and intuitive for the user for performing a number of actions in all sorts of applications. ICEfaces has always provided Drag and Drop support in regular Java applications and in portlet applications as well. However, before ICEfaces 3.3, the Drag and Drop functionality in portlets was constrained to working only within one portlet. Fortunately, a new feature in ICEfaces 3.3 allows developers to implement Drag and Drop functionality across different portlets on a page. This opens a world of possibilities in portlet applications. It is now possible to share the exact data object or POJO via Drag and Drop with different portlets, which can operate on the same object in different ways. All of this is now possible without having to make big changes to existing portlets or pages, and their beans, to accomplish this. This tutorial illustrates an easy technique for inter-portlet Drag and Drop functionality.

Getting Started

This tutorial was developed using Liferay 6. However, the technique illustrated here can be used in any portal container that supports JSF. It is assumed that the reader is familiar with basic portlet development.

In this tutorial we will create 3 different portlets: one that contains draggable items, and two similar portlets that will be used as drop zones. The first portlet will simply contain a one-column table with a few rows. Each of those rows will be draggable. Rather than rows, we will view them as items to drag. The other two portlets will be almost identical. Each of them will contain a one-column table, initially empty, that will be populated with the items that we drop on them. Thus, the items we drop will be removed from the first portlet and will be added to the portlet where we dropped them.

The technique is based on the new dragStartListener and datasource attributes supported by ace:draggable. We will create three different beans and one simple data object. The technique uses a window-scoped bean as a managed property in two view-scoped beans to allow communication between them. In other words, the window-scoped bean is shared among portlets on the same page. Each portlet in this tutorial uses a view-scoped bean, whose spatial scope is the portlet itself and nothing outside of it. While it would be possible to accomplish inter-portlet Drag and Drop with a window-scoped bean alone, portlets are more likely to be designed with view-scoped beans, so this technique shows how to easily add inter-portlet communication to existing portlets. At the same time, working with view-scoped beans will allow us to reuse those beans either on the same page or in a different page, allowing us to leverage the full potential or portlet development, rather than having a page-design mentality, where we could end up with less flexibility and less reusability.

We will now describe the different facelets and beans that we will use and explain their purposes. We will just highlight the core parts of the code. The full source code is available in a zip file with this tutorial, where you can also see the file structure. At the end, we'll go through the required XML configuration to register these portlets in Liferay.

Draggables Portlet

This portlet will simply contain a one-column table with five rows. Each of those rows will be draggable. We will create a file named drag.xhtml, whose body will contain the following markup:

drag.xhtml
<h:body>
	<f:view contentType="text/html">
		<h:form>
			<ace:panel header="Draggables">
				<ace:dataTable id="tutorialTable" value="#{draggableBean.items}" var="item">
					<ace:column headerText="Draggable items">
						<h:outputText value="#{item.name}" id="item" style="display:inline-block;width:100%;"/>
						<ace:draggable revert="true" helper="clone" opacity="0.7" for="item"
							dragStartListener="#{draggableBean.handleDrag}" datasource="tutorialTable" scope="tutorial"/>
					</ace:column>
				</ace:dataTable>
			</ace:panel>
		</h:form>
	</f:view>
</h:body>

The key point in the markup above is the use of a dragStartListener handler method and the reference to the table in the datasource attribute of ace:draggable. This will cause that the DragDropEvent object of the handler method contain a reference to the same data object corresponding to the "row" being dragged. The for atribute specifies the id of the component that will receive the draggable functionality, in this case, an h:outputText component. The scope attribute specifies a string for the draggable that must match the same scope string of the droppable component in order to be a valid drop action. In this case we simply use the string 'tutorial' for our scope. The other attributes simply have to do with the way things look. When revert is true, the draggable item will return to its initial position if it's not dropped on a valid drop target. When using helper set to 'clone', a new HTML node, based on the original one, will be created and dragged, instead of the original. The opacity attribute simply specifies the opacity of this helper node to distinguish it from the original one. The bean for this portlet will be named DraggableBean.java. It will be view-scoped, and initially it will look like this:

DraggableBean.java
public class DraggableBean implements java.io.Serializable {

	private List<Item> items;
	
	public DraggableBean() {
		items = new ArrayList<Item>();
		items.add(new Item("Item1"));
		items.add(new Item("Item2"));
		items.add(new Item("Item3"));
		items.add(new Item("Item4"));
		items.add(new Item("Item5"));
	}

	public void handleDrag(DragDropEvent e) {
		Item item = (Item) e.getData();
		// we will add more code here...
	}
	
	public List<Item> getItems() {
		return items;
	}
	
	public void setItems(List<Item> items) {
		this.items = items;
	}
}

We will add more to it later. At this point, it is only necessary to know that this bean simply contains a list of items that serves as a model for our data table. It also contains the handler method that will be invoked when we start to drag an item. Notice that in this handler, we are retrieving the actual data object, corresponding to the item, from the event object.

The entry for this bean in faces-config.xml would look like this:

    <managed-bean>
        <description>
            Backing bean for draggables portlet.
        </description>
        <managed-bean-name>draggableBean</managed-bean-name>
        <managed-bean-class>
            org.icefaces.tutorial.portletdndtutorial.DraggableBean
        </managed-bean-class>
        <managed-bean-scope>view</managed-bean-scope>
    </managed-bean>

Drop Target Portlet

We will create two of these portlets. These portlets will simply contain a panel that will serve as a drop zone and a table inside of it, initially empty, that will display the items that have been dropped on it. We will create a file named drop1.xhtml and a file named drop2.xhtml. Their bodies will contain the following markup:

drop1.xhtml
<h:body>
	<f:view contentType="text/html">
		<h:form>
			<ace:panel header="Drop Target 1">
				<ace:panel id="droppable" header="Drop zone" style="height:200px;">
					<ace:droppable tolerance="touch" dropListener="#{droppableBean.handleDrop}" scope="tutorial">
						<ace:ajax />
					</ace:droppable>
					<ace:dataTable id="droppableTable" value="#{droppableBean.items}" var="item">
						<ace:column headerText="Dropped items">
							<h:outputText value="#{item.name}" style="display:inline-block;width:100%;"/>
						</ace:column>
					</ace:dataTable>
				</ace:panel>
			</ace:panel>
		</h:form>
	</f:view>
</h:body>

The only difference between drop1.xhtml and drop2.xhtml is that drop2.xhtml will have a different header label in the container panel, for the sole purpose of distinguishing between both of them (i.e. <ace:panel header="Drop Target 2">).

In this case, by nesting the ace:droppable component inside of the ace:panel component, we make the panel a drop target on which we can drop our draggables. The dropListener attribute will be fired whenever a valid drop action occurs. Again, we're using the scope attribute with the 'tutorial' string to only allow draggables with the same scope string to be dropped in this drop target. The tolerance attribute simply determines whether the draggable must be completely contained in the drop target in order to be dropped or if it is enough to be touching it.

Both of these portlets will use the same view-scoped bean. We only need to write one. We don't have to make any modification for it to work with a different portlet. This bean will be named DroppableBean.java, and its initial contents will be the following:

DroppableBean.java
public class DroppableBean implements java.io.Serializable {

	private List<Item> items;
	
	public DroppableBean() {
		items = new ArrayList<Item>();
	}
	
	public void handleDrop(DragDropEvent e) {
		// we will add this code later...
	}
	
	public List<Item> getItems() {
		return items;
	}
	
	public void setItems(List<Item> items) {
		this.items = items;
	}
}

We will also add more to this bean later. We will explain in the next section how the magic happens and how the drop handler will eventually work to accomplish what we described at the beginning of the tutorial.

The entry for this bean in faces-config.xml would look like this:

    <managed-bean>
        <description>
            Backing bean for drop target portlet.
        </description>
        <managed-bean-name>droppableBean</managed-bean-name>
        <managed-bean-class>
            org.icefaces.tutorial.portletdndtutorial.DroppableBean
        </managed-bean-class>
        <managed-bean-scope>view</managed-bean-scope>
    </managed-bean>

WindowScopedBean

This is the most important part of the technique. We will create a window-scoped bean, named WindowScopedBean.java, that will allow our two view-scoped beans described above to share objects. The bean itself is quite simple and looks like this:

WindowScopedBean.java
public class WindowScopedBean implements java.io.Serializable {

	private Item draggedItem;
	private List<Item> originalList;
	
	public Item getDraggedItem() {
		return draggedItem;
	}
	
	public void setDraggedItem(Item draggedItem) {
		this.draggedItem = draggedItem;
	}

	public List<Item> getOriginalList() {
		return originalList;
	}
	
	public void setOriginalList(List<Item> originalList) {
		this.originalList = originalList;
	}
}

For more information about the window scope offered by ICEfaces, please visit this wiki page. The entry for this bean in faces-config.xml would look like this:

    <managed-bean>
        <description>
            Backing bean for window-scoped bean.
        </description>
        <managed-bean-name>windowScopedBean</managed-bean-name>
        <managed-bean-class>
            org.icefaces.tutorial.portletdndtutorial.WindowScopedBean
        </managed-bean-class>
        <managed-bean-scope>#{window}</managed-bean-scope>
    </managed-bean>

The important part is how the view-scoped beans are going to use this bean. So, we will add the following code to both DraggableBean.java and DroppableBean.java:

	private WindowScopedBean windowScopedBean;
	
	public void setWindowScopedBean(WindowScopedBean windowScopedBean) {
		this.windowScopedBean = windowScopedBean;
	}

We will make this bean a managed property of DraggableBean.java and DroppableBean.java. Thus, it is important to write the setter method for this property, as we did above, so that the framework can actually add to our view-scoped beans a reference to this window-scoped bean. To make this bean a managed property of our view-scoped beans, just add the following markup inside their managed-bean entries in faces-config.xml:

        <managed-property>
            <property-name>windowScopedBean</property-name>
            <property-class>org.icefaces.tutorial.portletdndtutorial.WindowScopedBean</property-class>
            <value>#{windowScopedBean}</value>
        </managed-property>

Now, we will add two more lines to our drag-start handler in DraggableBean.java. These lines will save the object, corresponding to the item being dragged, in the window-scoped bean, which will be later retrieved by the drop handler in DroppableBean.java. We will also add a reference to the original list of this item, so it can be removed after the drop operation. At the end, this method will look like this:

	public void handleDrag(DragDropEvent e) {
		Item item = (Item) e.getData();
		windowScopedBean.setDraggedItem(item);
		windowScopedBean.setOriginalList(items);
	}

Now, we will finally add some code to our drop handler in DroppableBean.java. The first line will retrieve the object we just saved in the window-scoped bean. Then, we check if the object is not null, and if it is not, we add it to this portlet's list and remove it from the draggables portlet list. At the end, this method will look like this:

	public void handleDrop(DragDropEvent e) {
		Item item = windowScopedBean.getDraggedItem();
		if (item != null) {
			items.add(item);
			List<Item> originalList = windowScopedBean.getOriginalList();
			if (originalList != null) {
				originalList.remove(item);
			}
		}
	}

The sequence of events should be clearer now. First, when a draggable item from the draggables portlet starts being dragged, the handleDrag method is fired, saving references for the item object and a the original list in the window-scoped bean. Then, when the item is dropped on a drop target portlet, the handleDrop method is fired, retrieving the objects that were set in the window-scoped bean by handleDrag and moving the item object from one original list to the drop portlet list.

It might seem like it is unnecessary to save a reference to the original list every time an item starts being dragged, since there's only one draggables portlet. However, this is done to illustrate that there could be many different draggables portlets, and, by using this technique, it would be easy to move items from one portlet to another.

Item Object

We have been using a very simple data object, just to avoid directing the attention of the reader away from the main point of this tutorial, but of course, more complex data objects can be used. The code for this class is the following:

Item.java
public class Item implements java.io.Serializable {
    
	private String name;
	
	public Item (String name) {
		this.name = name;
	}
	
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
}

Ajax Push

At this point, we have already accomplished our goal of facilitating inter-portlet Drag and Drop functionality. However, from the user perspective, the effects might not be very visible, since when we add an item to a drop target, the table of that portlet is updated to reflect the fact that it now contains a new item, but that same item doesn't seem to disappear from the draggables portlet. We can't use an <ace:ajax> tag to cause an ajax update in a different portlet. However, we can use ICEpush to accomplish this, and fortunately, it is very easy to do.

We will import the class org.icefaces.application.PushRenderer to our two view-scoped beans. Then, we will add the following line at the end of the constructor in DraggableBean.java: PushRenderer.addCurrentView("tutorial");. Finally, we will add the following line at the end of the drop handler in DroppableBean.java: PushRenderer.render("tutorial");. That's all we need to dynamically update our draggables portlet after a drop event in our drop target portlets.

This is a very basic ICEpush illustration, since ICEpush is out of the scope of this tutorial. For a more complete introduction to ICEpush, please look for the tutorial named Easy Ajax Push.

XML Configuration

Portlet development basics are also out of the scope of this tutorial. More information can be found on this wiki by searching for the 'portlet' keyword. However, we will show how the Liferay XML configuration files look like for this tutorial. Again, the full source code needed for this tutorial, including all the XML configuration can be found in the zip file on this page.

portlet.xml
<portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
             version="2.0"
	         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	         xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd">

    <portlet>
        <portlet-name>icefacesTutorialDragPortlet</portlet-name>
        <display-name>ICEfaces Tutorial Drag Portlet</display-name>
        <portlet-class>org.portletfaces.bridge.GenericFacesPortlet</portlet-class>
        <init-param>
            <name>javax.portlet.faces.defaultViewId.view</name>
            <value>/drag.xhtml</value>
        </init-param>
        <supports>
            <mime-type>text/html</mime-type>
            <portlet-mode>view</portlet-mode>
        </supports>
        <portlet-info>
            <title>ICEfaces Tutorial Drag Portlet</title>
            <keywords>icefaces tutorial drag</keywords>
        </portlet-info>
    </portlet>
	
    <portlet>
        <portlet-name>icefacesTutorialDropPortlet1</portlet-name>
        <display-name>ICEfaces Tutorial Drop Portlet 1</display-name>
        <portlet-class>org.portletfaces.bridge.GenericFacesPortlet</portlet-class>
        <init-param>
            <name>javax.portlet.faces.defaultViewId.view</name>
            <value>/drop1.xhtml</value>
        </init-param>
        <supports>
            <mime-type>text/html</mime-type>
            <portlet-mode>view</portlet-mode>
        </supports>
        <portlet-info>
            <title>ICEfaces Tutorial Drop Portlet 1</title>
            <keywords>icefaces tutorial drop</keywords>
        </portlet-info>
    </portlet>
	
    <portlet>
        <portlet-name>icefacesTutorialDropPortlet2</portlet-name>
        <display-name>ICEfaces Tutorial Drop Portlet 2</display-name>
        <portlet-class>org.portletfaces.bridge.GenericFacesPortlet</portlet-class>
        <init-param>
            <name>javax.portlet.faces.defaultViewId.view</name>
            <value>/drop2.xhtml</value>
        </init-param>
        <supports>
            <mime-type>text/html</mime-type>
            <portlet-mode>view</portlet-mode>
        </supports>
        <portlet-info>
            <title>ICEfaces Tutorial Drop Portlet 2</title>
            <keywords>icefaces tutorial drop</keywords>
        </portlet-info>
    </portlet>
</portlet-app>
liferay-portlet.xml
<?xml version="1.0"?>
<liferay-portlet-app>

    <portlet>
        <portlet-name>icefacesTutorialDragPortlet</portlet-name>
        <instanceable>false</instanceable>
        <ajaxable>false</ajaxable>
    </portlet>
    <portlet>
        <portlet-name>icefacesTutorialDropPortlet1</portlet-name>
        <instanceable>false</instanceable>
        <ajaxable>false</ajaxable>
    </portlet>
    <portlet>
        <portlet-name>icefacesTutorialDropPortlet2</portlet-name>
        <instanceable>false</instanceable>
        <ajaxable>false</ajaxable>
    </portlet>
    <role-mapper>
        <role-name>administrator</role-name>
        <role-link>Administrator</role-link>
    </role-mapper>
    <role-mapper>
        <role-name>guest</role-name>
        <role-link>Guest</role-link>
    </role-mapper>
    <role-mapper>
        <role-name>power-user</role-name>
        <role-link>Power User</role-link>
    </role-mapper>
    <role-mapper>
        <role-name>user</role-name>
        <role-link>User</role-link>
    </role-mapper>
</liferay-portlet-app>
liferay-display.xml
<display>
	<category name="ICEfaces Tutorial">
		<portlet id="icefacesTutorialDragPortlet"/>
		<portlet id="icefacesTutorialDropPortlet1"/>
		<portlet id="icefacesTutorialDropPortlet2"/>
	</category>
</display>

Final Notes

Once everything is finished, just compile the project to produce a war file. Then, in Liferay, install these portlets by uploading this war file in the administrator interface. Finally, create a new page in Liferay and add all the three portlets you just installed to that page and see for yourself how inter-portlet drag and drop is now possible.

Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.

© Copyright 2018 ICEsoft Technologies Canada Corp.