How to implement Drag and Drop lists with Django and Dojo

     

Our application manages news items which can be assigned to any number of topics. The sequencing of the news items within each topic can be set independently of the sequencing of items in other topics. We wanted a simple drag-and-drop interface that would allow us to set the order of items in a topic.

The items are displayed in a simple unordered list that gets an id of "mylist", and its list items have ids that represent their primary keys in the database:

<ul id="mylist">
  <li id=4>list item 1</li>
  <li id=7>list item 2</li>
  <li id=1>list item 3</li>
  <li id=9>list item 4</li>
</ul>

Dojo makes use of two objects DragSource and DropTarget. In order to make a list dragable, we include:

<script type="text/javascript" src="/media/js/dojo/dojo.js"> </script>
<script type="text/javascript">
  dojo.require("dojo.dnd.*");
  dojo.require("dojo.event.*");
  dojo.require("dojo.lang");
</script>

The next step is to initialize drag and drop sources.

function init(){
  var dl = byId("mylist");
  new dojo.dnd.HtmlDropTarget(dl, ["li2"]);
  var lis = dl.getElementsByTagName("li");
  for(var x=0; x<lis .length; x++){
    new dojo.dnd.HtmlDragSource(lis[x], "li2");
}

Then we register this event with Dojo so this function is executed every time a page is loaded:

dojo.event.connect(dojo, "loaded", "init");

At this point we should have a dragable list, which is both a Dragable element and a Drop target. Our initial task is to have a list that writes to a database when a drag is finished. We use it to change the sequence of news articles. In order to do that, we need to make XMLHttpRequest everytime a drag occurs. Dojo allows you to extend the default functions so I decided to override the onDragEnd function. We change our init() so it looks like this:

function init(){

                dojo.lang.extend(dojo.dnd.HtmlDragSource, {
                onDragEnd: function(e){
                switch(e.dragStatus){

                        case "dropSuccess":
                                dojo.dom.removeNode(this.dragClone);
                                this.dragClone = null;
                               
                                var mylist = byId("mylist");
                                var list = newslist.getElementsByTagName("li");
                                var orderstr = "";
                                for(i=0;i<list .length;i++)
                                {
                                  orderstr += list[i].id;
                                  if(i != list.length-1)
                                  orderstr += ",";
                                }
                               
                                sendreq(orderstr);
                                break;

                        case "dropFailure": // slide back to the start
                                …(copy the original code here)
                                break;
                }

                // shortly the browser will fire an onClick() event,
                // but since this was really a drag, just squelch it
                dojo.event.connect(this.domNode, "onclick", this, "squelchOnClick");

                dojo.event.topic.publish('dragEnd', { source: this } );
                },});
               
                var dl = byId("newslist");
                new dojo.dnd.HtmlDropTarget(dl, ["li2"]);
                var lis = dl.getElementsByTagName("li");
                for(var x=0; x<lis.length; x++){
                        new dojo.dnd.HtmlDragSource(lis[x], "li2");
                }
               
               
                }//end init()
 

In the dropSuccess section, we "collect" the ID's of the list items and put them in a string (orderstr). Then we call a custom function called 'sendreq' and pass the string along.

note:we do not need to override the dropFailure case, but since we are extending the OnDragEnd function, we have to include both cases, so go ahead and copy and paste the original code from the dojo libraries.

Our custom sendReq function calls dojo.io.bind which is provided by Dojo and takes care of the xmlHttpRequest:

function sendreq(str)
{
  dojo.io.bind({
       url: '/project/app/view/reorder/?orderstr='+str,
       handler: callBack
  });
}

Dojo's bind() function takes a url parameter and a handler parameter.

  • URL specifies the location of the file that is going to accept the HttpRequest and update the database.
  • Handler specifies the callback function that is going to accept the HttpResponse from the server.
  • In our case, we are not returning any data from the server, so the callback function is quite simple:

    function callBack(type, data, evt)
    {
       if (type == 'error')
         alert('Error when retrieving data from the server!');     
    }

    Having implemented the client side portion, it is time to handle the HttpRequest on the server. Our request is sent to:

    '/project/app/view/reorder/?orderstr='+str

    * str is a comma separated list of id's in the desired order

    The reorder() function takes the orderstr from the 'request.GET' object, :

    def reorder(request):
            if request.GET['orderstr']:
                    orderstr = request.GET['orderstr']
                    orderstr = orderstr.split(',')
                    for i in range(len(orderstr)):
                            n = NewsSequence.objects.get(pk=orderstr[i])
                            n.sequence = i   
                            n.save()
                    return HttpResponse(orderstr)

    It updates the database and returns the string of IDs back to the client so we can indicate success.

7 Responses to “How to implement Drag and Drop lists with Django and Dojo”

  1. Jacob Kaplan-Moss Says:

    Nice trick!

    Do you find the performance of an XHR request for each drop acceptable, or are you thinking you'll need to extend this to cache changed objects and submit 'em all at once?

  2. Ivan Manolov Says:

    The XHR works great with a broadband connection but I havent tried it on a dial-up yet. I dont know if any caching could be used since each one of the drops is done independently and there is no form submission, but I am open to suggestions.
    Thank you.

  3. David Robinson Says:

    Jacob, since this resequencing is just an administrative function not exposed to the public, the volume of resequencing transactions is likely to be small enough that I'm not expecting performance to be a concern. That, and we haven't really had a chance to dive into Django's cacheing yet :-)

  4. Chris Dary Says:

    So, could there be caching implemented if the XHR would be sent via onUnload? That way there would only be one request sent, and still no 'form submit' sort of user activity.

    I'm fairly sure it's not really necessary in our context, but I thought I'd throw it out there in case anyone else was looking for a way.

  5. arthur debert Says:

    Jakob,
    I've implemented something similar to this, and because it's also on administrative interface the performance penalty for each drop is small.
    The issue is more of an usability one. After each drop, I display a progress indicator and 'lock' the drag and drop until the server responds. That's to avoid an inconsistent state between the view and the database (in case of one request getting lost after multiple drops).

    As for the url, I am using something like:
    ‘/project/app/model/reorder/?orderstr=’+str

    and then on the view function I get the model from the url.
    On the model itself, I have a function that retrieves the model's field to use on the reordering:
    def get_order_field(self):
    return 'sequence'

    and the view function granbs the model name from the url, and checks to see if the model allows reordering…

    cheers
    arthur

  6. lasizoillo Says:

    Great, but a bit buggy (but less that my english ;-) )

    I see that your initial target for this widget is doing a list of news.
    s/newslist/mylist/g ;-)

    "byId" not exists, but "dojo.byId" yes.

    Is not necessary remove the clone. You are extending HTMLDragSource, not HtmlDragObject. You can remove the following lines:
    dojo.dom.removeNode(this.dragClone);
    this.dragClone = null;

    The trick is great, thanks for it.

  7. snott Says:

    Dude, you rock. I needed to implement this and it's a major pita if you're just learning dojo.
    Many thanks!

Leave a Reply