Selectize.js is an HTML widget that replaces your normal Select
elements. We just integrated selectize.js
into our Django project &
found that it helped improve page load speeds and provide a better user
experience.
We were using formsets with Select
elements that had over 500 options in one
of our Django projects. A 20-row formset would cause the generation of 10,000
Option
elements with 9,500 of them being duplicates. Load times for new items
were around 2.5 seconds per page. We used Selectize.js to load the
options of Select
element from an array of JSON
objects. This reduced our
edit page sizes from 1400KB to 260KB and our load times from 4.99s to 0.50s.
We were also able to add additional context for our users. Our original
Select
element only showed the names of all Account
instances. Using
selectize
, we are able to show and filter based on any part of the name or
the description.
We then used AJAX to fetch options as the user types, this adds any options that were added after the page was loaded.
You can see the commit that inspired this blog post on our bug tracker or on github.
To actually reduce the amount of HTML generated, we have to do two things -
create a JSON
array of possible options and prevent the Django fields from
rendering Option
elements.
Since a Select
appears in the navigation of every page, we opted to create a
context manager to inserted the JSON
array into the context
of every page
instead of simply including it as a context
variable in our view. We started
by creating a context_managers.py
in our core
app:
# accounts/context_manager.py import json from accounts.models import (Account) def all_accounts(request): accounts = Account.objects.order_by('name') values = [{'text': account.name, 'description': account.description, 'value': account.id} for account in accounts] return {'accounts_json': json.dumps(values)}
We create a list of dictionaries from Account
instances. Selectize will use
the text
property as the options label and the value
as the value to send
in the request. We will use the description
to add more information to the
dropdowns. We can access this array using the accounts_json
variable in any
of our templates.
We'll include this in our TEMPLATE_CONTEXT_PROCESSORS
setting:
# core/settings/base.py TEMPLATE_CONTEXT_PROCESSORS = ( # ... "django.contrib.messages.context_processors.messages", "constance.context_processors.config", "accounts.context_processors.all_accounts", )
We set the JSON
array as a variable in our base.html
template:
<!-- core/templates/base.html --> <script type="text/javascript"> // A global variable containing accounts for ajax selects accounts_array = {{ accounts_json|safe }}; </script>
Now that we've got a single list of Accounts
to use for our Selects, we can
get rid of all the Option
elements Django creates. However, we can't remove
all the Options - if one is already selected(i.e., we are editing an existing
entry that has Accounts
set) we'll need to keep it so that Selectize can use
it to initialize it's widget. We can do this by modifying the __init__
method
of the Form
we use to generate our Formset
:
class TransactionForm(forms.ModelForm): class Meta: # ... widgets = { 'account': forms.Select(attrs={'class': 'autocomplete'}) } def __init__(self, *args, **kwargs): super(TransactionForm, self).__init__(*args, **kwargs) if self.is_bound: return if None not in (self.instance, self.instance.account_id): self.fields['account'].queryset = Account.objects.filter( id=self.instance.account_id) else: self.fields['account'].queryset = Account.objects.none()
We first checked to see if the form is bound - if it is, we don't modify the
queryset so that they can be used in form validation(the fields could be
removed later by an overidden is_valid
method...). We've also added the
autocomplete
class to our account
field so we can easily hook it up to
Selectize - which we do in our base template:
<!-- core/templates/base.html --> <script type="text/javascript"> $(document).ready(function() { // Turn Select Widgets for Accounts into AJAX Autocompletes $('.autocomplete').each(function() { $(this).selectize({ options: accounts_array, }); }); }); </script>
Now every form in our formset should be using the values from accounts_array
& be displayed using Selectize. If you inspect your HTML, there should only be
a single Option
element under each Select
.
Now that we've got a fast loading page, we can use that description
property
we included in our JSON
objects to provide extra context to users. This is
easily done using Selectize's options. We'll use the
render
option to specify a custom rendering function for the dropdown
options:
/* core/templates/base.html */ // ... $(this).selectize({ options: accounts_array, render: { option: function(data, escape) { return '<div class="option">' + '<span class="text">' + escape(data.text) + '</span>' + '<span class="description">' + escape(data.description) + '</span>' + '</div>'; }, }, // ...
We added some CSS to split up the text & description and to de-emphasize the description:
.selectize-control .option .text { display: block; } .selectize-control .option .description { font-size: 10px; display: block; font-style: italic; }
This provides a much nicer interface than the default HTML Select
elements.
The only downside is that a refresh is required if an Account
is added after
the page was loaded. To counteract this, we will asynchronously load the latest
options.
The first step is to provide a URL and view to return values based on a users query. This is super simple if you're using Django-AJAX:
# accounts/views.py from django_ajax.decorators import ajax @ajax def accounts_query(request): if 'q' in request.GET: q = request.GET['q'] accounts = Account.objects.filter( Q(name__icontains=q) | Q(description__icontains=q)) else: accounts = Account.objects.all() return [{'text': account.name, 'description': account.description, 'value': account.id } for account in accounts] # accounts/urls.py urlpatterns = patterns('accounts.views', # ... url(r'^ajax/accounts/$', 'accounts_query', name='accounts_query') )
Now we just hook Selectize up to the view by using the load
option:
/* core/templates/base.html */ // ... $(this).selectize({ options: accounts_array, load: function(query, callback) { if (!query.length) return callback(); $.ajax({ url: '{% url accounts_query %}', type: 'GET', data: {q: query}, error: function() { callback(); }, success: function(res) { callback(res.content); } }); } // ...
Now Selectize will use the accounts_array
to provide initial options for the
user & will perform an AJAX request to retrieve options when the user starts
typing into the widget.
Comments
There are currently no comments
New Comment