We at LeanAgri stress a lot on making clean and user friendly interfaces. One of these obsessions helped us create an interesting extension: Django sortable multiselectfield
The interface we were building needed inputs as Comma separated values from users. Some interesting details about these comma separated values:
- These values could be selected only from a set of allowed values
- The order of the values is important
This is what we started with:
Uggh, ugly User experience.
Hunting for solutions
I found a library called Django Multiselect field which allows comma separated field inputs to be taken in from Checkboxes.
The library provided a starting point for us, and we went ahead and added functionality to allow sorting of values to create Django sortable multiselectfield to give our users the best experience.
Designing Sorting behavior
This is how the sorting behavior was finalized to work:
- All list of items are sortable. Inside a choice group, all items are sortable too.
- When the form is reloaded, all selected items show on top in sorted order, and unselected items go at bottom (in the same order they are defined).
- When sorting is changed, the form is marked as dirty and saved again.
Pretty neat eh?
That’s it. Our story of creating a sortable multiselect field, and our design decisions for the same which makes our users very happy with this small piece of addition.
Is this usable in other cases? Maybe in a M2M relation, we also would need to add a new field which stores the order of selected fields (which is how Sorted M2M works I believe)
The remaining article majorly talks about the implementation details, read on if that gets you interested.
Extending Django multiselect
I got to coding this and quickly extended Django multiselect into something workable.
First, I modified the template to allow sorting of fields. The code started with working with forms.CheckboxSelectMultiple
and added sortable-item-list
and sortable-item
selectors.
{% load i18n staticfiles %} {% with id=widget.attrs.id %} <ul {% if id %} id="{{ id }}" {% endif %} class="sortable-item-list" {% if widget.attrs.class %} class="{{ widget.attrs.class }}" {% endif %}> {% for group, options, index in widget.optgroups %} {% if group %} <li> {{ group }} <ul class="sortable-item-list" {% if id %} id="{{ id }}_{{ index }}" {% endif %} > {% endif %} {% for option in options %} <li class="sortable-item"> {% include option.template_name with widget=option %} </li> {% endfor %} {% if group %} </ul> </li> {% endif %} {% endfor %} </ul> {% endwith %}
This was followed by adding JQuery sortable properties to our lists:
if (typeof jQuery === 'undefined') { var jQuery = django.jQuery; } (function ($) { $(function () { $('.sortable-item-list').each(function () { $(this).sortable({ axis: 'y', cursor: 'move', items: "> .sortable-item" }); }); }); })(jQuery);
Apart from this, we need to ensure that changes in order are recorded as Form changes, which is done by simply modifying the has_changed
method.
def has_changed(self, initial, data): """Implementation same as MultipleChoiceField.hasChanged() except this checks the order of lists as well""" if self.disabled: return False if initial is None: initial = [] if data is None: data = [] if len(initial) != len(data): return True initial_list = [force_text(value) for value in initial] data_list = [force_text(value) for value in data] return initial_list != data_list
Here’s the diff of this feature addition, in a few lines we are able to extend this library into Django Sortable multiselect field allowing us to fill in ordered comma separated data into our databases.