Sunday, February 08, 2009

ExtJS Tip : Sortable Grid Rows via Drag and Drop

One particular challenge on a recent project is to have the ability to sort grid rows using drag and drop. With lots of help from the ExtJS forums, here's how I ended up doing it.

The code snippet below shows an Ext gridpanel with the added ability to allow users to sort rows using drag and drop. This is acheived by (1) setting the enableDragDrop configuration to true to allow dragging and dropping of rows and (2) creating a drop target that handles the drop event when a row is dropped.

Update 031609 : An anonymous comment mentioned having problems with sorting multiple rows. This tip only works with single select rows. I have added the code change below that forces the grid to use a single select row selection model.



var grid = new Ext.grid.GridPanel({
id: 'mygrid',
title: 'My Grid',
store: store, // define the data store in a separate variable
loadMask: true,
ddGroup:'mygridDD',
enableDragDrop: true, // enable drag and drop of grid rows
viewConfig: {
emptyText: 'No pages found',
sm: new Ext.grid.RowSelectionModel({singleSelect:true}),
forceFit: true
}, columns: gridcolumns, // define grid columns in a separate variable
listeners: {
"render": {
scope: this,
fn: function(grid) {

// Enable sorting Rows via Drag & Drop
// this drop target listens for a row drop
// and handles rearranging the rows

var ddrow = new Ext.dd.DropTarget(grid.container, {
ddGroup : 'mygridDD',
copy:false,
notifyDrop : function(dd, e, data){

var ds = grid.store;

// NOTE:
// you may need to make an ajax call here
// to send the new order
// and then reload the store



// alternatively, you can handle the changes
// in the order of the row as demonstrated below

// ***************************************

var sm = grid.getSelectionModel();
var rows = sm.getSelections();
if(dd.getDragData(e)) {
var cindex=dd.getDragData(e).rowIndex;
if(typeof(cindex) != "undefined") {
for(i = 0; i < rows.length; i++) {
ds.remove(ds.getById(rows[i].id));

}
ds.insert(cindex,data.selections);
sm.clearSelections();
}
}

// ************************************
}
})

// load the grid store
// after the grid has been rendered
store.load();
}

}
}
})

13 comments:

  1. there's an error in the code


    for(i = 0; i <>

    ReplyDelete
  2. Fixed. Thanks for pointing it out.

    ReplyDelete
  3. i've got a problem...
    try to drop more than 1 records on the LAST record...

    some records will fade away and the last will become unselectable -_-

    ReplyDelete
  4. Hmmm, I'm afraid that this won't work with a multi-select grid. You will need to add

    sm: new Ext.grid.RowSelectionModel({singleSelect:true})

    to the grid definition to force the gridpanel to use a singleselect row selectionmodel. Sorry if this wasn't part of the above code.

    ReplyDelete
  5. thx for wonderful code... it really helped me out....

    Maven
    raj_zero1@hotmail.com

    ReplyDelete
  6. Hey it works ! Thanks Ham for posting !

    ReplyDelete
  7. Hey, thank you for your code, it is just was i need!

    I've been working with it but i need an CheckboxSelectionModel. If you use this SelectionModel, if you DD in CheckBox column, the rows will be duplicated. To solve that problem, i've make this changes:
    notifyDrop: function( dd, e, data ) {
    var sm = taulacategories.getSelectionModel();
    var rows = sm.getSelections();
    if( rows.length > 0 ) {
    var ds = taulacategories.store;
    if( dd.getDragData( e ) ) {
    var cindex = dd.getDragData( e ).rowIndex;
    if( typeof( cindex ) != 'undefined' ) {
    for( i = 0; i < rows.length; i++ ) ds.remove( ds.getById( rows[i].id ) );
    ds.insert( cindex, data.selections );
    sm.clearSelections();
    }
    }
    }
    }

    Thank you again for your work.

    Carlos

    ReplyDelete
  8. Carlos,

    You are very welcome and thank you for posting your code.

    ReplyDelete
  9. This is the type of blog entries that are crucial as an addition to official APIs or manuals for filling in the usability gaps.

    Thank you very much for a useful code.

    I do have a problem, however. Is it possible to do this without having to use ds.remove? This creates 2 critical issues.
    1) No way to differentiate between records removed by user, say, clicking delete button in grid and records removed by ds.remove() so backend server will remove unexpected entries.
    2) If using an automated storeWriter the script crashes if a delete API is not set. If I do set it, thats going to be extra traffic requesting delete operation + deletion of good records.

    Is there a way to unmark the record as deleted after the DropTarget handler is completed?
    Technically, I could add a boolean field to every moved record so the backend ignores moved=true from its delete operation. But this looks really ugly and does not solve the problem of wasted traffic for requests that will be ignored anyway.

    Any ideas?

    ReplyDelete
  10. Hi again, I came up with a solution that does not rely on order of records in store but on the hidden (from grid's point of view) order column:
    fn: function(grid) {
    var ddrow = new Ext.dd.DropTarget(grid.container, {
    ddGroup : 'mygridDD',
    copy : false,
    notifyDrop : function(dd, e, data) { // thing being dragged, event, data from draged source
    var ds = grid.store;
    var sm = grid.getSelectionModel();
    rows = sm.getSelections();

    // order of ext events:
    // JsonStore getRecord() -> change Record() -> JsonStore save dirty records -> JsonStore order -> GridView refresh
    if(dd.getDragData(e)) {
    var cindex = dd.getDragData(e).rowIndex;

    var sourceOriginalOrder = rows[0].data.order;
    var destinationOriginalOrder = dd.getDragData(e).selections[0].data.order;
    if(sourceOriginalOrder != destinationOriginalOrder) {
    var destinationOriginalIndex = cindex;

    if(typeof(cindex) != "undefined") {
    ds.getById(rows[0].id).set('order', destinationOriginalOrder);
    ds.getById(rows[0].id).markDirty();

    ds.getAt(destinationOriginalIndex).set('order', sourceOriginalOrder);
    ds.getAt(destinationOriginalIndex).markDirty();

    ds.save();
    ds.sort('order', 'ASC');
    sm.clearSelections();
    }
    }
    }
    }
    });

    For it to work, 2 properties also need to be set for the store:
    remoteSort : false,
    sortInfo : {
    field : 'order',
    direction : 'ASC'
    }

    ReplyDelete
  11. Hi,
    I was able to tweak your code to make it support multiple selections, and also to maintain the order of the selected rows even after dragging them in an upward direction.

    Here's my code:

    render: function(grid) {
    var ddrow = new Ext.dd.DropTarget(grid.getView().mainBody, {
    ddGroup: 'myGridDD',
    copy: false,
    notifyDrop: function(dd, e, data) {
    var ds = grid.store;
    var sm = grid.getSelectionModel();
    var rows = sm.getSelections();
    if (dd.getDragData(e)) {
    var cindex = dd.getDragData(e).rowIndex;

    if (typeof(cindex) != "undefined") {
    for(var i = 0; i < rows.length; i++) {
    var srcIndex = ds.indexOfId(rows[i].id);
    ds.remove(ds.getById(rows[i].id));
    if (i > 0 && cindex < srcIndex) {
    cindex++;
    }
    ds.insert(cindex, rows[i]);
    }

    sm.selectRecords(rows);
    }
    }
    }
    });

    }

    ReplyDelete
  12. @Tsai Xing Wei

    Thank you very much !

    ReplyDelete
  13. For me this does not work

    I had to replace:
    var ddrow = new Ext.dd.DropTarget(grid.container, {

    with
    var ddrow = new Ext.dd.DropTarget(grid.getView().scroller.dom, {

    Hope to help someone cause I spent lot of time on this ;)

    ReplyDelete