Django: How to let user re-order/sort table of content with drag and drop
Tags: django, javascript, webdev, python
Published: 2020-02-24 19:05:10

I recently needed to implement a way to let admin users on site order groups freely. Turns out this is quite involved proces relative to other common tasks in Django. 

I cannot guarantee that my way shown below is the best but I believe it works quite well and be implemented in a few steps. I am going to be using *Group* in the example but of course it can be anything. 

When starting with bigger pieces of functionality I always try to separate the task into smaller pieces. For example with the ordering, let’s start with the drag and drop of table rows and don’t worry about saving the order just yet.

I started by researching available JavaScript libraries for the re-ordering of elements and after a while decided to use [SortableJS](https://github.com/SortableJS/Sortable). It is fairly robust with tons of options but for the basics can be up and running pretty quickly.

### Step One: Add SortableJS to page

```django
{% load static %}
<script src="{% static 'js/external/Sortable.min.js' %}"></script>
```

Okay, that is done. I always prefer to keep libraries in my project instead of relying on CDN so I have control over it being available and the version I need.

## Step Two: Configure `<table>` for ordering

To configure HTML `<table>` for SortableJS, we need for it to have `<tbody>` , ideally with ID to easily access it with JavaScript. For example for the groups: `<tbody id="groups">`

### Step Three: Initialize SortableJS

Now we can initialize SortableJS for our table like so:

```javascript
const groups = document.getElementById('groups');
   let sortable = Sortable.create(groups, {
});
```

You should be able to **drag and drop table rows and change their positions**.

### Step Four (optional): Add drag handle

I think it is more intuitive to have handle for dragging than dragging whole rows. So I added new first table cell with `<span>` element to create handle:

```html
<td>
<span class="handle"></span>
</td>
```

You can of course use regular image or SVG icon, but if you create your drag handle with CSS it can work for light and dark versions of your site without issues.

Here is the CSS for `.handle`:

```css
.handle {
    display: inline-block;
    border-top: 3px solid var(--dark);
    width: 30px;
    padding-top: 5px;
    cursor: grab;
}
.handle:after, .handle:before {
    display: block;
    content: "";
    padding-top: 5px;
    border-top: 3px solid var(--dark);
}
.handle:hover, .handle:hover:before, .handle:hover:after {
    border-color: var(--primary);
}
.handle:active, .handle:active:before, .handle:active:after {
    cursor: grabbing;
}
```

I found initial version on CodePen and then modified it to my liking. Mostly changing sizes and using Bootstrap 4 colors.

Now we just need to tell SortableJS that we have handle it can use:

```javascript
let sortable = Sortable.create(groups, {
    handle: '.handle',
});
```

Done!

We are almost done with the front-end stuff.

**Configuration tips**

You can configure Sortable to add classes to the row that is being dragged. These are specified via additional options like so:

```javascript
let sortable = Sortable.create(groups, {
    handle: '.handle',
    dragClass: 'dragged',
    chosenClass: 'sortableChosen',
});
```

For example you can change background of the `dragClass` so the user can see it more clearly when dragging. I also lowered the opacity of `sortableChosen` class.

### Step Five: Prepare your Django model

We need to save the ordering which means our Django model needs to have `order` field to save order so we can order by it later. I used `IntegerField` like so:

```python
class Group(models.Model):
    lookup_id = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
    order = models.IntegerField(blank=False, default=100_000)
```

The rest of `Group` class is omitted. I am using `lookup_id` as public-facing identifier for my objects and it has index for fast look-up.

`order` has big default value so the newly added will naturally appear last in the table. You could try something more sophisticated but I think this is good enough.

Our Django model is ready.

### Step Six: How to transfer HTML `<table>` ordering to database?

We have table with drag handle so user can re-order it as they please. We have also modified our model so it can store the order and we can `order_by` the results by `order` property.

Ideally we want to monitor changes in the table order and then sending the new order to Django so it can update `order` for all groups in the table.
You could possibly save after each change or do it periodically say every 10 seconds. I opted for dedicated “Save ordering” button so user can choose when to save or decide not to if they change their mind.

**How do we know the order of groups in our table?** Well, we know the order of `<tr>` elements inside the `<tbody>`. So we can add data attribute to each `<tr>` and then query the table. 

The order of row elements will represent the new order of groups.
Let’s modify template and add the attribute like so:

```django
<tr data-lookup="{{ folder.lookup_id }}">
```

To properly send data to our Django application we are going to need a form:

```django
<form id="orderingForm" method="post">
{% csrf_token %}
<input type="hidden" id="orderingInput" name="ordering">
</form>
```

And also the button to manually save the new order:

```html
<button id="saveOrdering" class="btn btn-outline-primary btn-lg float-right mr-2">Save ordering</button>
```

Now our structure is ready and we can move back to JavaScript to react to “Save ordering” click/tap and get the new order from table.

### Step Seven: Use JavaScript to submit new ordering via form
We have form ready and we know how to get the new ordering of our groups.
First we will create constants for HTML elements we want to work with:

```javascript
const saveOrderingButton = document.getElementById('saveOrdering');
const orderingForm = document.getElementById('orderingForm');
const formInput = orderingForm.querySelector('#orderingInput');
```

Next let’s create function that will fill the hidden form input with new ordering and submits it:

```javascript
function saveOrdering() {
    const rows = document.getElementById("groups").querySelectorAll('tr');
    let ids = [];
    for (let row of rows) {
        ids.push(row.dataset.lookup);
    }
    formInput.value = ids.join(',');
    orderingForm.submit();
}
```

We need to query the rows inside the function to get current ordering, next we loop over of all rows and extract the lookup-ids into array. Next we will join them with `,` and lastly submit the form.

### Step Eight: React to button click

We are almost done with our JavaScript. However we still need to connect our button with our function.

```javascript
saveOrderingButton.addEventListener('click', saveOrdering);
```

As an _improvement_, you could have the button disabled and only allow it after the user changes order of the items:

```javascript
let sortable = Sortable.create(groups, {
    handle: '.handle',
    dragClass: 'dragged',
    chosenClass: 'sortable-chosen',
    onChange: () => {
        saveOrderingButton.disabled = false;
    }
});
```

Also the submit could be handled with AJAX but I don’t want to distract from the important parts here 🙂

### Step Nine: Prepare Django view and form

Our front-end is basically ready. Now we need Django logic to extract the new ordering from submitted form and update `order` properties of our models.

Let’s start by defining simple form in `forms.py` like so:

```python
class OrderingForm(forms.Form):
    ordering = forms.CharField()
```

Now we can move to our views file and define the view responsible for saving new ordering:

```python
@require_POST
def save_new_ordering(request):
	pass
```

Let’s replace `pass` with full implementation and then I will explain what is going on:

```python
form = OrderingForm(request.POST)

if form.is_valid():
    ordered_ids = form.cleaned_data["ordering"].split(',')

    with transaction.atomic():
        current_order = 1
        for lookup_id in ordered_ids:
            group = Group.objects.get(lookup_id__exact=lookup_id)
            group.order = current_order
            group.save()
            current_order += 1

return redirect('group-list')
```

First we create new `OrderingForm` from the `request.POST` data and then check if it is valid.

If we have valid forms, we create list of ordered lookup-ids from the form field. Next using `transaction.atomic()` we loop over all the ids, get `Group` object and then update its `order` property. 

When all is finished we will `redirect` the user back to the list of groups.

### Step Ten: Define url path for new view

I promise, we are almost there. We just need to define url path for our newly created view and update the HTML form definition.

```python
urlpatterns = [
..
path('save-group-ordering', save_new_ordering, name='save-group-oldering'),
..
]
```

And _finally_ let’s go back to our template and add `action` to our `<form>`:

```django
<form id="orderingForm" method="post" action="{% url 'save-group-oldering' %}">
```
**And that is our ordering finished.**

I am by no means a Django expert so there may be better solution or easier Python code. Feel free to suggest improvements :-)

Thanks for reading!

_Is anything not clear? Do you want more information? Ask in the comments and I will do my best to help you._

Also published on my [site](https://nemecek.be/blog/4/django-how-to-let-user-re-ordersort-table-of-content-with-drag-and-drop)