18. Selectize Widget

Rendering choice fields using a <select>-element becomes quite impractical when there are too many options to select from.

For this purpose, the Django admin backend offers so-called auto complete fields, which loads a filtered set of options from the server as soon as the user starts typing into the input field. This widget is based on the Select2 plugin, which itself depends upon jQuery, and hence it is not suitable for us. Since django-formset aims to be JavaScript framework agnostic, it uses an alternative widget written in pure JavaScript.

18.1. Usage with fixed Number of Choices

Assume, we have an address form defining a ChoiceField to choose one of the past European Django conferences. If this number exceeds say 25, then we should consider to render the select box using the special widget formset.widgets.Selectize:

from django.forms import fields, forms, widgets
from formset.widgets import Selectize

class ConferenceForm(forms.Form):
    venue = fields.ChoiceField(
        choices=[
            (2009, "Praha"),
            (2010, "Berlin"),
            (2011, "Amsterdam"),
            (2012, "Zürich"),
            (2013, "Warszawa"),
            (2014, "Île des Embiez"),
            (2015, "Cardiff"),
            (2016, "Budapest"),
            (2017, "Firenze"),
            (2018, "Heidelberg"),
            (2019, "København"),
            (2020, "Virtual"),
            (2022, "Porto"),
            (2023, "Edinburgh"),
            (2024, "Vigo"),
        ],
        widget=Selectize,
    )

This widget waits for the user to type some characters into the input field for “venue”. If the entered string matches the name of one or more cities (even partially), then a list of options is generated containing the matching cities. By adding more characters to the input field, that list will shrink to only a few or eventually no entry. This makes the selection simple and comfortable.

18.2. Usage with dynamic Number of Choices

Often we can’t handle the choices using a static list. This happens for instance, when we store them in a Django model. We then point a foreign key onto the chosen entry of that model. The above example then can be rewritten by replacing the ChoiceField against a ModelChoiceField. Instead of choices this field then requires an argument queryset. For the form we defined above, we use a Django model named County with name as identifier. All counties we can select from, are now stored in a database table.

from django.forms import fields, forms, models, widgets
from formset.widgets import Selectize
from testapp.models import County

class CountyForm(forms.Form):
    county = models.ModelChoiceField(
        queryset=County.objects.all(),
        widget=Selectize(
            search_lookup='name__icontains',
            placeholder="Select a county",
        ),
    )

Here we instantiate the widget formset.widgets.Selectize using the following arguments:

  • search_lookup: A Django lookup expression. For choice fields with more than 50 options, this instructs the django-formset-library on how to look for other entries in the database.

  • group_field_name in combination with option groups. This field is used to determine the group name. See below.

  • filter_by is a dictionary to filter options based on the value of other field(s). See below.

  • placeholder: The empty label shown in the select field, when no option is selected.

  • attrs: A Python dictionary of extra attributes to be added to the rendered <select> element.

18.2.1. Grouping Select Options

Sometimes it may be desirable to group options the user may select from.

In the United States there are 3143 counties, many of them sharing the same name. When rendering them inside a select box, it would be rather unclear which county belongs to which state. For this purpose, HTML provides the element <optgroup>. Other than visually grouping options to select from, this element has no other effect. Fortunately our Selectize widget mimicks that feature and so we can group all counties by state by rewriting our form as:

class GroupedCountyForm(forms.Form):
    county = models.ModelChoiceField(
        label="County",
        queryset=County.objects.all(),
        widget=Selectize(
            search_lookup='name__icontains',
            group_field_name='state',
            placeholder="Select a county"
        ),
        required=True,
    )

Here we grouped the counties by state. To achieve this, we have to change the widget in the field county and configure how to group them. By using the attribute group_field_name, the Selectize-widget uses the named field from the model specified by the queryset for grouping.

When rendered, the <option> elements then are grouped inside <optgroup>-s using the state’s name as their label:

18.2.2. Filtering Select Options

As we have seen in the previous example, even grouping too many options might not be a user-friendly solution. This is because the user has to type a string, at least partially. So the user already must know what he’s looking for. This approach is not always practical. Many of the counties share the same name. For instance, there are 34 counties named “Washington”, 26 named “Franklin” and 24 named “Lincoln”. Using an auto-select field, would just show a long list of eponymous county names.

In many use cases, the user usually knows in which state the desired county is located. So it would be practical if the selection field offers a reduced set of options, namely the counties of just that state. Therefore let’s create a form with adjacent fields for preselecting options:

from testapp.models import State

class FilteredCountyForm(forms.Form):
    state = models.ModelChoiceField(
        label="State",
        queryset=State.objects.all(),
        widget=Selectize(
            search_lookup='name__icontains',
            placeholder="First, select a state"
        ),
        required=False,
    )
    county = models.ModelChoiceField(
        label="County",
        queryset=County.objects.all(),
        widget=Selectize(
            search_lookup=['name__icontains'],
            filter_by={'state': 'state__id'},
            placeholder="Then, select a county"
        ),
        required=True,
    )

This form shows the usage of two adjacent fields, where the first field’s value is used to filter the options for the next field. Here with the field state, the user can make a preselection of the state. When the state is changed, the other field county gets filled with all counties belonging to that selected state.

To enable this feature, the widget Selectize accepts the optional argument filter_by which contains a dictionary such as {'state': 'state__id'} defining the lookup expression on the given queryset. Here each key maps to an adjacent field and its value contains a lookup expression.

Setting up forms using filters, can improve the user experience, because it reduces the available options to choose from. This might be a more friendly alternative rather than using option groups.

18.3. Selectize Multiple Widget

If the form field for “county” shall accept more than one selection, in Django we replace it by a django.forms.fields.MultipleChoiceField. The widget then used to handle such an input field also must be replaced. For this purpose django-formset offers the special widget formset.widgets.SelectizeMultiple to handle more than one option to select from. From a functional point of view, this behaves similar to the Selectize widget described before. But instead of replacing a chosen option by another one, selected options are lined up to build a set of options. Again, we can group and filter the given options, as shown in the two previous examples. This example rewrites the grouped options with a SelectizeMultiple widget:

from formset.widgets import SelectizeMultiple

class GroupedCountiesForm(forms.Form):
    county = models.ModelMultipleChoiceField(
        label="County",
        queryset=County.objects.all(),
        widget=SelectizeMultiple(
            search_lookup='name__icontains',
            group_field_name='state',
            placeholder="Select up to 5 counties"
        ),
        required=True,
    )

By default a SelectizeMultiple widget can accept up to 5 different options. This limit can be adjusted by increasing the argument of max_items. This value however shall not exceed more than say 15 items, otherwise the input field might become unmanageable. If you need a multiple select field able to accept hundreds of items, consider using the Dual Selector Widget widget.

18.4. Handling ForeignKey and ManyToManyField

If we create a form out of a Django model, we explicitly have to tell it to either use the Selectize or the SelectizeMultiple widget. Otherwise Django will use the default HTML <select> or <select multiple> fields, which are not user friendly for big datasets.

Say that we have an address model using a foreign key to existing cities:

from django.db import models

class AddressModel(models.Model):
    # other fields

    city = models.ForeignKey(
        CityModel,
        verbose_name="City",
        on_delete=models.CASCADE,
    )

then when creating the corresponding Django form, we must replace the default widget Select against our special widget Selectize:

from django.forms import models
from formset.widgets import Selectize

class AddressForm(models.ModelForm):
    class Meta:
        model = AddressModel
        fields = '__all__'
        widgets = {
            # other fields
            'city': Selectize(search_lookup='label__icontains'),
        }

The argument search_lookup is used to build the search query.

If we want to allow the user to select more than one city, we have to replace the ForeignKey against a ManyToManyField – and conveniently rename “city” to “cities”. Then in the above example, we’d have to replace the Selectize widget against SelectizeMultiple:

from django.forms import models
from formset.widgets import SelectizeMultiple

class AddressForm(models.ModelForm):
    class Meta:
        model = AddressModel
        fields = '__all__'
        widgets = {
            # other fields
            'cities': SelectizeMultiple(search_lookup='label__icontains'),
        }

18.5. Endpoint for Dynamic Queries

Remember that all views connecting forms using the Selectize or SelectizeMultiple widget must inherit from formset.views.IncompleteSelectResponseMixin. This mixin handles the endpoint for our lookups.

In comparison to other libraries offering autocomplete fields, such as Django-Select2, django-formset does not require developers to add an explicit endpoint to the URL routing. Instead it shares the same endpoint for form submission as for querying for extra options out of the database. This means that the form containing a field using the Selectize widget must be controlled by a view inheriting from formset.views.IncompleteSelectResponseMixin.

Note

The default view offered by django-formset, formset.views.FormView already inherits from IncompleteSelectResponseMixin.

18.6. Implementation Details

The client part of the Selectize widget relies on Tom-Select which itself is a fork of the popular Selectize.js-library, but rewritten in pure TypeScript and without any other external dependencies. This made it suitable for the client part of django-formset, which itself is a self-contained JavaScript library compiled out of TypeScript.