Ext JS: Drag + Drop Grid Rows

2021-01-17 10 min read Code Example David Poxon

In this article I discuss adding functionality to ‘drag and drop’ rows in an Ext JS grid. Specifically, this article will cover an approach that does not rely on any Ext JS plugins, e.g., the canonical gridviewdragdrop, and assumes Ext JS version 3.4.

I recently found myself needing to add a ‘custom ordering’ feature to items in a list, i.e., through the UI the user can change the order of the items in the list to suit their liking. The UI for displaying the list of items already existed: a pretty complex implementation of an Ext JS EditorGridPanel. ‘Drag and drop’ was not my initial strategy, but after a few hours of coding the code was a mess and I knew I needed to reconsider my approach. It was then I had the idea to implement a ‘drag and drop’ interaction for ordering the items in the list.

I am certainly no expert in Ext JS, and even less so in what ‘drag and drop’ features it exposes, so I fired up my search engine to see what documentation I could find on the topic of Ext JS ‘drag and drop’. There is a bevy of information and examples out there, and none of them worked for me 😞. A large number of the examples I came across pitched use of the gridviewdragdrop plugin. Unfortunately this plugin does not exist for Ext JS 3.4 and, even so, I was not keen on introducing a plugin to solve this UI interaction. Sencha, the makers of Ext JS, do provide an example for implementing ‘drag and drop’ for Ext JS 3.4 but, sadly, it doesn’t work.

So, after viewing what seems to be an endless number of examples, I was able to cobble together enough code to get Ext JS to provide me with the foundations I need. I then filled in the gaps for a “robust” ‘drag and drop’ feature to reorder rows in a grid. In the remainder of this article, I will describe and document my approach. A playable example of what is described is below.

To begin we will start with building a simple grid view and populating it with data. In the code we first create a JsonStore, our data repository, named store. The store contains five records that represent types of fruit. Each record has the following properties:

  • id, the unique identifier for the record; and
  • name, the name of the fruit.

Having created the data store, we create a grid for displaying our data: a GridPanel named grid. The grid defines a single column labeled Fruit, which is configured to display the name property of each record. We also set the autoheight property so that the grid dynamically grows in response to the amount of data in the data store. Finally, we configure the grid to be rendered to the document body.

var store = new Ext.data.JsonStore({
    fields: [{
        name: 'id'
    }, {
        name: 'name'
    }],
    data: [{
        id: 1,
        name: 'Apple'
    }, {
        id: 2,
        name: 'Banana'
    }, {
        id: 3,
        name: 'Cherry'
    }, {
        id: 4,
        name: 'Dragon Fruit'
    }, {
        id: 5,
        name: 'Elderberry'
    }, ]
});

var grid = new Ext.grid.GridPanel({
    store: store,
    columns: [{
        id: 'name',
        header: 'Fruit',
        dataIndex: 'name',
    }],
    autoHeight: true,
    renderTo: Ext.getBody()
});

The above code will give present a UI that looks something like this:

A visualised Ext JS grid with five fruit items in alphabetical order: Apple, Banana, Cherry, Dragon Fruit, Elderberry

Now that we have defined our grid, we’re going to configure it so that we can ‘drag and drop’ its rows to customise the order in which they appear. There are three steps to the process:

  1. enable ‘drag’ by configuring the grid to include a ‘drag zone’;
  2. enable ‘drop’ by configuring the the grid with a ‘drop zone’; and
  3. implement our reordering feature.

Enabling ‘Drag’

Enabling the ability to drag rows around our grid requires turning on a single property of the grid: enableDragDrop. Turning on this property will, on creation of the grid, cause the grid to create a drag zone for itself. This will allow the user to drag individual rows around the grid, along with applying some nice styling to give the user appropriate visual cues. The updated code for grid is below.

var grid = new Ext.grid.GridPanel({
    store: store,
    columns: [{
        id: 'name',
        header: 'Fruit',
        dataIndex: 'name',
    }],
    autoHeight: true,
    enableDragDrop: true, // add 'drag' interactions to this grid
    renderTo: Ext.getBody()
});

By setting this property we are now able to drag items, as the below shows. Note the ‘prohibited’ symbol on the left side of the dragged item, this indicates the item cannot be dropped on the grid. In the next step we will configure the grid so we can drop the dragged rows onto the grid.

Our grid showing the UI for dragging an item - the dragged item reads “1 selected row”, and to the left of the test is a ‘prohibited’ symbol indicating the item cannot be dropped

Enabling ‘Drop’

Now that we can drag rows around, we want to be able to drop them. This is a little more involved then enabling drag: unlike the drag zone, which is automatically created for us, we have to manually create the drop zone and add it to the grid.

In the updated code for grid below, we include the minimum code we need to introduce the drop zone. This includes initialising the DropZone instance and wiring it into our grid. When we initialise the DropZone instance, we pass the grid’s scroller to its constructor and, by doing so, define the scroller area as the area within which items can be dopped. The semantics of the DropZone members, and our implementation of them, are as follows:

  • getTargetFromEvent, from the event return the intended drop target, e.g., the node entered, exited, hovered over, or dropped on - we use Sencha’s documented implementation;
  • onNodeEnter, what to do when a drop target is ‘entered’ - we add CSS class grid-highlight-drop-location, which adds a red bottom border, to the target;
  • onNodeOut, what to do when a drop target is ‘exited’ - we remove the grid-highlight-drop-location CSS class;
  • onNodeOver, which CSS class should be applied to the dragged element when it hovers over a drop target - we use a built-in class which styles the dragged item with a ‘green tick’ to indicate it can be dropped here; and
  • onNodeDrop, what to do when a dragged item is ‘dropped’ on a target - for now, we do nothing but indicate the drop was valid.

Note that we add the drop zone after the grid has been rendered (by setting a listener on the render event); we do this because the documentation tells us the drag zone will not be available until this point, and we need to tell the drop zone which drag zone it will be cooperating with.

var grid = new Ext.grid.GridPanel({
    store: store,
    columns: [{
        id: 'name',
        header: 'Fruit',
        dataIndex: 'name',
    }],
    autoHeight: true,
    enableDragDrop: true,
    renderTo: Ext.getBody(),
    listeners: {
        'render': function () {
            var gridView = this.getView();
            this.dropZone = new Ext.dd.DropZone(gridView.scroller, {
                getTargetFromEvent: function (e) {
                    return e.getTarget(gridView.rowSelector);
                },

                onNodeEnter: function (target, dd, e, data) {
                    Ext.fly(target)
                        .addClass('grid-highlight-drop-location');
                },

                onNodeOut: function (target, dd, e, data) {
                    Ext.fly(target)
                        .removeClass('grid-highlight-drop-location');
                },

                onNodeOver: function (target, dd, e, data) {
                    return Ext.dd.DropZone.prototype.dropAllowed;
                },

                onNodeDrop: function (target, dd, e, data) {
                    return true;
                },

                // tell the drop zone which drag zone it's working with
                ddGroup: gridView.dragZone.ddGroup
            });
        }
    }
});

Our style rules for the grid-highlight-drop-location CSS class:

.grid-highlight-drop-location {
    border-bottom-style: solid;
    border-bottom-color: red;
    border-bottom-width: medium;
}

Using the above code we can now drag rows around our grid and indicate their drop location, as shown below. Note that instead of a ‘prohibited’ symbol the dragged item shows a green ‘tick’ which indicates this is a valid location for the dragged item to be dropped.

Our grid showing the UI for dragging an item - the dragged item reads “1 selected row”, and to the left of the test is a green tick it indicating the item can be dropped

Custom Ordering

Now that we can drag and drop rows, we’re going to add logic to leverage this to customise the order of the rows in our grid. Achieving this goal requires a couple of key steps:

  1. update the data store so that each record can have its order stored against it; and
  2. update the the recorded order of items based on user interaction, i.e, the ‘drag and drop’.

Updating the Data Store

Updating the data store simply requires updating the definition of the record to include a field which allows us to store its sort order. In our example, we call this field sortOrder. The updated definition for store is found in the code below. Not only do we update the definition, but we have given each record a default value for sortOrder based on the alphabetical order of their respective names. We also add a default sort to the store, which is used to sort records in ascending (ASC) order based on their sortOrder property.

var store = new Ext.data.JsonStore({
    fields: [{
        name: 'id'
    }, {
        name: 'name'
    }, {
        name: 'sortOrder' // our new sorting field
    }],
    data: [{
        id: 1,
        name: 'Apple',
        sortOrder: 1
    }, {
        id: 2,
        name: 'Banana',
        sortOrder: 2
    }, {
        id: 3,
        name: 'Cherry',
        sortOrder: 3
    }, {
        id: 4,
        name: 'Dragon Fruit',
        sortOrder: 4
    }, {
        id: 5,
        name: 'Elderberry',
        sortOrder: 5
    }, ],
    sortInfo: {
        field: 'sortOrder',
        direction: 'ASC'
    }
});

Updating the Recorded Order

When we drag and drop a row on the grid, we are demonstrating that we want this to be its new position, or its new order in relation to the rest of the items, in the grid. The following are the logical steps we follow to accomplish our stated goal.

  1. Using the drop target, find the index of the row we dropped on.
  2. Using the index of the dropped and dragged rows(data provides information about the dragged row), exit early if the row we dropped on is the same row we dragged - effectively signalling no change.
  3. Using data, find the record we dragged.
  4. Using the index of the dropped record, find the record we dropped on.
  5. Build up a representation of the rows and their current order. We’re going to use this representation as the source-of-truth regarding the order of the items. We will manipulate this representation based on the user interaction, and then model our post-interaction sort order based on the order of the items in the representation.
  6. From this representation, remove the item we dragged (we’re going to add it back in later at the location it was dropped).
  7. To the representation, add the item we dragged to its dropped location, i.e., directly below the row we dropped on.
  8. Based on our sorted representation, update the stored sort values for each record.
  9. Sort the data store based on our sort scheme (ascending based on sortOrder), and commit our changes to the data store.
var grid = new Ext.grid.GridPanel({
    store: store,
    columns: [{
        id: 'name',
        header: 'Fruit',
        dataIndex: 'name',
    }],
    autoHeight: true,
    enableDragDrop: true,
    renderTo: Ext.getBody(),
    listeners: {
        'render': function () {
            var gridView = this.getView();
            var gridStore = this.store;
            this.dropZone = new Ext.dd.DropZone(gridView.scroller, {
                getTargetFromEvent: function (e) {
                    return e.getTarget(gridView.rowSelector);
                },

                onNodeEnter: function (target, dd, e, data) {
                    Ext.fly(target)
                        .addClass('grid-highlight-drop-location');
                },

                onNodeOut: function (target, dd, e, data) {
                    Ext.fly(target)
                        .removeClass('grid-highlight-drop-location');
                },

                onNodeOver: function (target, dd, e, data) {
                    return Ext.dd.DropZone.prototype.dropAllowed;
                },

                onNodeDrop: function (target, dd, e, data) {
                    // Step 1
                    var droppedOnIdx = gridView
                        .findRowIndex(target);

                    // Step 2
                    if (droppedOnIdx == data.rowIndex) return true;

                    // Step 3
                    var draggedRec = gridStore
                        .getAt(data.rowIndex);

                    // Step 4
                    var droppedOnRec = gridStore
                        .getAt(droppedOnIdx);

                    // Step 5
                    var items = [];
                    gridStore.each((record) => {
                        var id = record.get('id');
                        items.push(id);
                    });

                    // Step 6
                    var draggedRecId = draggedRec.get('id');
                    var idxToRemove = items.indexOf(draggedRecId);
                    var removedItem = items.splice(idxToRemove, 1)[0];

                    // Step 7
                    var droppedOnRecId = droppedOnRec.get('id');
                    var droppedOnRecIdx = items
                        .indexOf(droppedOnRecId);
                    items.splice(droppedOnRecIdx + 1, 0, removedItem);

                    // Step 8
                    gridStore.each((record) => {
                        var recordId = record.get('id');
                        var newOrder = items.indexOf(recordId);
                        record.set('sortOrder', newOrder);
                    });

                    // Step 9
                    gridStore.sort('sortOrder', 'ASC');
                    gridStore.commitChanges();

                    return true;
                },

                // we want the drag and drop zones to be the same place
                ddGroup: gridView.dragZone.ddGroup
            });
        }
    }
});

Summary

You should now have a working feature that allows you to use ‘drag and drop’ to reorder rows in a Ext JS grid, all without using a single Ext JS plugin.

You can find a working Sencha fiddle here.

Thoughts, comments, or feedback? Drop them below 👇🏼

comments powered by Disqus