codeslower.com Savor Your Code.

Using the draggable property in ExtJS

ExtJS, from Sencha, Inc., is an open-source JavaScript framework for creating client-side MVC-structured applications with rich and interactive user interfaces. Recently I prototyped a UI which included docked panels that the user could re-arrange by dragging. I struggled quite a bit to figure out how ExtJS "wanted" me to implement this feature and I hope that, by sharing what I learned, it will be easier for anyone else who tries to implement something similar.

Example

I've created an example at http://draggable.codeslower.com showing three draggable panels that can be dropped into another panel. In this example, panels are no longer draggable after being dropped. To reset the panels, refresh the page. The source code is available on GitHub. The version used when this article was published is tagged v07182011. The example's logic resides entirely in the file draggable.js.

ExtJS Components

ExtJS ships with a UI library based on "Components," much like many other object-oriented UI frameworks. Every UI element is sub-classed from the AbstractComponent class, inheriting a number of common properties and behaviors. We are interested in the draggable property.

DragSource and draggable

draggable is a configuration property that makes any component a draggable object. In particular, when used with the Panel component, you can drag the panel around using its title bar. In draggable.js, each of the panels titled "Drag This Panel" is made draggable using the config "draggable: true."

When configured as draggable, the Panel component also exposes a dd property. From the ExtJS documentation::

If this Panel is configured draggable, this property will contain an instance of Ext.dd.DragSource which handles dragging the Panel.

So draggable will allow a Panel to be moved using the mouse, and also causes a dd property to appear on the Panel at run-time.

ExtJS calls the draggable panels a "drag source" and the drop area the "drop target." The DragSource instance accessed through the dd property on the Panel provides a default implementation of the drag part of "drag and drop." What's missing is how to implement a "drop target" so the panel can actually be dropped somewhere.

Implementing a Drop Target

The DropTarget class provides a default implementation that makes any component a "drop target" or "drop zone." To be used, the DropTarget object must be attached to an Element (not the same as a Component, but every Component has one). The DropTarget instance must implement four methods: notifyEnter, notifyOver, notifyOut, and notifyDrop.

In draggable.js, the panel titled "Drop a Panel Here" will be associated with our DropTarget instance. However, the DropTarget object can't be created until the component's element exists. Therefore we configure the panel to call the "initializeDropTarget" function when the render event fires. We attach our initializeDropTarget method using the listeners configuration property:

 xtype: 'panel',
title: 'Drop a Panel Here',
listeners: { render: initializeDropTarget },
cls: 'x-dd-drop-ok'

Additionally, we set the CSS class of the drop target to "x-dd-drop-ok". The DragSource instance associated with our draggable panels looks for this CSS class to determine if the drop target is valid or not.

Implementing initializeDropTarget

When the render event fires, ExtJS will pass initializeDropTarget the Panel (called targetPanel below) associated with our drop target. The first thing we do is create a DropTarget instance, giving it the Element associated with our Panel:

 targetPanel.dropTarget = Ext.create('Ext.dd.DropTarget', targetPanel.el);

We assign the DropTarget instance to the dropTarget property so it does not get garbage-collected when the initializeDropTarget function finishes. We could have also attached it to a local variable defined during our Ext.onReady handler, but here we follow the conventions seen in the ExtJS drag-and-drop examples (in particular, the "Custom Drag and Drop" example).

Once the object is created, we implement notifyDrop, notifyEnter, notifyOut and notifyOver by defining them on the dropTarget instance. notifyDrop is called when the component is dropped on our target. notifyEnter, notifyOut and notifyOver are called when a draggable component enters, leaves or hovers over the drop target area, respectively.

At this point, we have put together all the pieces necessary to implement drag-and-drop using the draggable property and the DropTarget class. All we have to do is implement logic that modifies the UI based on user actions. For this example, we only need special behavior in the notifyDrop method.

Implementing notifyDrop

notifyDrop is called with three arguments: the dropped component (source), the mouse event associated with the drop (evt) and custom data provided by the dropped component (data):

    targetPanel.dropTarget.notifyDrop = function(source, evt, data) {
...
};

source is not the actual Panel created for our Viewport in our Ext.onReady handler, nor is it the actual Element associated with the Panel. Instead, it is a proxy object created by the DragSource instance. However, the id property on the source object is the same as the dropped Panel, so we can always get to the original using Ext.getCmp. The evt argument is an Ext.EventObject that can be used to get information about the mouse-up event, but we don't use it here. The data argument, in this case, is provided by the DragSource instance, but in a custom drag-and-drop solution it can be anything. We don't use it here either.

Recall that we want to move the panel being drug into the panel we dropped it on. This requirement complicates our implementation of notifyDrop because it is not safe to modify the component being drug during the drag-and-drop operation. I only discovered this through trial-and-error; some operations are safe, but moving a component to another container is certainly not.

Fortunately, the DragSource instance exposed by the dd property on our Panel will call the method "afterValidDrop" when the Panel is dropped on a valid drop target. Unfortunately, this method is is only visible in the source code for Ext.dd.DragSource (it has the wrong method name associated with it in the documentation, and I've reported the bug as well).

Therefore, in the implementation of notifyDrop, I first get a reference to the dropped Panel (i.e., the Panel just drug) using Ext.getCmp:

    var droppedPanel = Ext.getCmp(source.id);

I then attach an implementation of the "afterValidDrop" method to the dd property of the dropped Panel:

    droppedPanel.dd.afterValidDrop = function() {
targetPanel.add(droppedPanel.cloneConfig({
draggable: false,
title: "Can't Drag This Panel."
}));

droppedPanel.destroy();
};

Notice I clone the dropped Panel before adding it to the target Panel. This isn't strictly necessary, but made it it a little easier to make sure the new Panel is not draggable. I could have added droppedPanel directly to targetPanel, but then I would have had to deal with disabling the DragSource instance associated with droppedPanel.

Our implementation of notifyDrop ends by returning true. This tells the DragSource instance that the drop was valid. If we did not return true, onValidDrop would never be called.

Reflection

Once the pieces are in place, it's fairly straightforward to implement notifyDrop and the other handlers required by DropTarget. It's unfortunate that there isn't a better way to modify the component you are dragging inside the notifyDrop method.

Given the draggable property, it would make sense that a droppable property also exist. Maybe someone can write a plugin?

Remember, the code for this example is available on GitHub. The v07182011 tag points to the version used while this article was written. Please fork the master branch if you do contribute.

Comments and pull requests are welcome!

Category: None

Please login to comment.