Selections

HQ has different interactions for selecting data from a list.

Overview

A good rule of thumb when deciding what selection component to use is the following:

  • Select-Toggles are good for selecting between a small list of items (2-5 items).
  • Select2s are good for selective from a very large set of items, as it supports pagination.
  • Multiselects are useful when the action involves moving items between two lists.

For any user-defined data, it's difficult to be certain how many items will be in the list. Even data sets that we might expect to be small—such as the number of forms in a module—might be large for certain projects. It's better to assume that a list will grow large and display it as some kind of dropdown where the options are a click away, rather than to assume it'll stay small and display all options on the page.

Select-Toggle

On the occasions when a list is guaranteed to be short (2-5 items), consider displaying it as a toggle. This shows all options and only takes one click to select.

We use a custom toggle widget <select-toggle> that uses Knockout Components and is conceptually the same as a single-select dropdown. See select_toggle.js for full documentation.

There's also a SelectToggle widget for use with Forms, defined in hqwebapp's widgets.py.

HTML
<select-toggle
  params="
    options: [
      'peaceful',
      'easy',
      'feeling'
    ]
  "
></select-toggle>
A few places in HQ use the same look and feel as the select toggle widget but have unique implementations. They generally use bootstrap's button groups and also often include our own btn-group-separated class, which separates the buttons, making them easier to read and wrap better.

Usage in Crispy Forms

In crispy forms, we can use the SelectToggle widget on a ChoiceField. If the crispy form is not being included inside a knockout model, apply_bindings=True must be specified in the widget's arguments. Otherwise, you can specify ko_value, as well as other options in attrs.

Color Chooser
What is your favorite primary color
Python
from django import forms
from django.utils.translation import gettext_lazy, gettext as _
from crispy_forms import bootstrap as twbscrispy, layout as crispy
from corehq.apps.hqwebapp import crispy as hqcrispy
from corehq.apps.hqwebapp.widgets import SelectToggle

COLOR_CHOICES = [
    ('r', gettext_lazy('Red')),
    ('g', gettext_lazy('Green')),
    ('b', gettext_lazy('Blue'))
]


class SelectToggleDemoForm(forms.Form):
    color = forms.ChoiceField(
        label=gettext_lazy("Color"),
        required=False,
        choices=COLOR_CHOICES,  # we need to specify this twice for form validation
        widget=SelectToggle(
            choices=COLOR_CHOICES,
            apply_bindings=True,
            attrs={},  # you can specify ko_value or other select-toggle parameters here
        ),
        help_text=gettext_lazy("What is your favorite primary color"),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.helper = hqcrispy.HQFormHelper()
        self.helper.form_method = 'POST'
        self.helper.form_action = '#'
        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Color Chooser"),
                'color',
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Save"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                hqcrispy.LinkButton(
                    _("Cancel"),
                    '#',
                    css_class="btn btn-outline-primary",
                ),
            ),
        )

Select2

For most lists, select2 is the way to go. It adds behavior to a normal <select> element. It supports either hard-coded static options or dynamic options fetched via ajax. It can support free text options, acting like an autocomplete, or can restrict users to a specific list of options. Beyond these major options, select2 supports many more specific features and styling options; see the full documentation for details.

We instantiate select2 in a number of different ways: manually with javascript, via knockout bindings, and by using CSS classes that certain javascript modules search for.

We also have a select2 options for common behaviors like validating email addresses and displaying form questions. Before you add a new custom set of select2 options, please look around and ask around for other parts of HQ that have similar behavior.

Manual Setup

This is the most straightforward way of initializing a select2 element.

HTML
<select id="js-manual-select2" class="form-select basic">
  <option>one</option>
  <option>two</option>
  <option>three</option>
</select>
JS
import $ from 'jquery';
import 'select2/dist/js/select2.full.min';

$(function () {
    $("#js-manual-select2").select2();
});

A clearable version of that select2 element:

HTML
<select id="js-manual-select2-clear" class="form-select basic">
  <option value="1">one</option>
  <option value="2">two</option>
  <option value="3">three</option>
</select>
JS
import $ from 'jquery';
import 'select2/dist/js/select2.full.min';

$(function () {
    $("#js-manual-select2-clear").select2({
        allowClear: true,
        placeholder: "Select an option...",
    });
});

Manual Setup with Crispy Forms

This is similar to the manual setup above, but using crispy forms to provide the form HTML.

City Adventure Planner
Select the location you would like to see recommendations for.
Select the experiences you would like to have
JS
import $ from 'jquery';
import 'select2/dist/js/select2.full.min';

$(function () {
    $("#id_location").select2();
    $("#id_experiences").select2();
});
Python
from django import forms
from django.utils.translation import gettext_lazy, gettext as _
from crispy_forms import bootstrap as twbscrispy, layout as crispy
from corehq.apps.hqwebapp import crispy as hqcrispy


class Select2ManualDemoForm(forms.Form):
    location = forms.ChoiceField(
        label=gettext_lazy("Location"),
        required=False,
        help_text=gettext_lazy("Select the location you would like to see recommendations for."),
    )
    experiences = forms.MultipleChoiceField(
        label=gettext_lazy("Experiences"),
        required=False,
        help_text=gettext_lazy("Select the experiences you would like to have"),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # in theory, you can populate this from a parameter passed in args or kwargs
        self.fields['location'].choices = [
            (city, city) for city in [
                "Berlin", "Amsterdam", "Frankfurt", "Paris", "Stockholm", "Reykjavik", "Geneva", "The Hague",
                "Rome", "Oslo", "London", "Hamburg", "Copenhagen", "Cape Town", "New York", "Atlanta",
            ]
        ]
        self.fields['experiences'].choices = [
            (experience, experience) for experience in [
                "Food", "Museum", "Music", "Street Art", "Street Food", "Underground", "Buildings", "Monuments",
            ]
        ]

        self.helper = hqcrispy.HQFormHelper()
        self.helper.form_method = 'POST'
        self.helper.form_action = '#'
        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("City Adventure Planner"),
                'location',
                'experiences',
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Plan"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                hqcrispy.LinkButton(
                    _("Cancel"),
                    '#',
                    css_class="btn btn-outline-primary",
                ),
            ),
        )

Referencing a CSS Class

We can automatically initialize a select2 using the css class hqwebapp-select2, as long as the hqwebapp/js/bootstrap5/widgets module is present.

HTML
<select class="form-select hqwebapp-select2">
  <option>uno</option>
  <option>dos</option>
  <option>tres</option>
</select>

This also applies to <select multiple> elements:

HTML
<select multiple class="form-select hqwebapp-select2">
  <option>moja</option>
  <option>mbili</option>
  <option>tatu</option>
</select>

Referencing the CSS Class in Crispy Forms

This is similar to the HTML-based setups above, but adding the hqwebapp-select2 to crispy.Field's css_class argument instead.

Activity Planner
Select a lake you would like to visit
Select the activities you would like to do
Cancel
Python
from django import forms
from django.utils.translation import gettext_lazy, gettext as _
from crispy_forms import bootstrap as twbscrispy, layout as crispy
from corehq.apps.hqwebapp import crispy as hqcrispy


class Select2CssClassDemoForm(forms.Form):
    lake = forms.ChoiceField(
        label=gettext_lazy("Lake"),
        required=False,
        help_text=gettext_lazy("Select a lake you would like to visit"),
    )
    activities = forms.MultipleChoiceField(
        label=gettext_lazy("Activities"),
        choices=(
            ("kay", gettext_lazy("Kayak")),
            ("sup", gettext_lazy("SUP")),
            ("can", gettext_lazy("Canoe")),
            ("bot", gettext_lazy("Boat")),
            ("swi", gettext_lazy("Swim")),
        ),
        required=False,
        help_text=gettext_lazy("Select the activities you would like to do"),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # in theory, you can populate this from a parameter passed in args or kwargs
        self.fields['lake'].choices = [
            (lake, lake) for lake in [
                "Veluwemeer", "IJesselmeer", "Markermeer", "Gooimeer", "Westeinderplassen", "Kralingen",
                "Berkendonk", "Oldambtmeer", "Loosdrechste Plassen", "Zevenhuizplas", "Burgurmer Mar",
            ]
        ]

        self.helper = hqcrispy.HQFormHelper()
        self.helper.form_method = 'POST'
        self.helper.form_action = '#'
        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Activity Planner"),
                crispy.Field('lake', css_class="hqwebapp-select2"),
                crispy.Field('activities', css_class="hqwebapp-select2"),
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Create Schedule"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                hqcrispy.LinkButton(
                    _("Cancel"),
                    '#',
                    css_class="btn btn-outline-primary",
                ),
            ),
        )

Dynamic Knockout Binding

We can also use the knockout binding select2 to initialize a select2, with options provided by Knockout.

HTML
<div id="js-ko-model-dynamic">
  <select
    class="form-select"
    data-bind="
      select2: letters,
      value: value
    "
  ></select>
</div>
JS
import $ from 'jquery';
import ko from 'knockout';

$(function () {
    $("#js-ko-model-dynamic").koApplyBindings(function () {
        return {
            letters: ['eins', 'zwei', 'drei'],
            value: ko.observable('eins'),
        };
    });
});

Dynamic Knockout Binding with Crispy Forms

This is similar to the HTML-based setups above, except that an external script applies koApplyBindings to the <form> id (<form> can also be wrapped with a <div> with the same id). In order to apply the binding to the ChoiceField in crispy forms, the data_bind argument is used inside crispy.Field.

Playlist Creator
Select the electronic music genre you want to generate a playlist
Cancel
JS
import $ from 'jquery';
import ko from 'knockout';

$(function () {
    $("#ko-playlist-generator").koApplyBindings(function () {
        return {
            genres: [
                "techno", "house", "acid", "electro", "bass", "drum & bass",
                "jungle", "club", "detroit techno", "detroit house", "chicago house",
                "soulful house", "west coast electro", "rave", "acid techno",
                "percussive techno", "hypnotic techno", "breaks", "deep house",
                "deep techno", "jack",
            ],
            value: ko.observable("acid"),
        };
    });
});
Python
from django import forms
from django.utils.translation import gettext_lazy, gettext as _
from crispy_forms import bootstrap as twbscrispy, layout as crispy
from corehq.apps.hqwebapp import crispy as hqcrispy


class Select2DynamicKoForm(forms.Form):
    genre = forms.ChoiceField(
        label=gettext_lazy("Genre"),
        required=False,
        help_text=gettext_lazy("Select the electronic music genre you want to generate a playlist"),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.helper = hqcrispy.HQFormHelper()
        self.helper.form_method = 'POST'
        self.helper.form_action = '#'
        self.helper.form_id = "ko-playlist-generator"
        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Playlist Creator"),
                crispy.Field('genre', data_bind="select2: genres, value: value"),
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Create Playlist"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                hqcrispy.LinkButton(
                    _("Cancel"),
                    '#',
                    css_class="btn btn-outline-primary",
                ),
            ),
        )

Static Knockout Binding

We can also initialize a select2 with the staticSelect2 Knockout Binding, where the options are pulled from HTML instead of Knockout. This is useful for select2s that have non-varying options but don't work with the hqwebapp-select2 CSS class because they're inside a Knockout-controlled UI, so they aren't guaranteed to exist on page render.

HTML
<div id="js-ko-model-static">
  <select
    class="form-select"
    data-bind="staticSelect2: {}"
  >
    <option>un</option>
    <option>deux</option>
    <option>trois</option>
  </select>
</div>
JS
import $ from 'jquery';

$(function () {
    $("#js-ko-model-static").koApplyBindings();
});

Static Knockout Binding with Crispy Forms

This is similar to the HTML-based setups above, except that an external script applies koApplyBindings to the <form> id (<form> can also be wrapped with a <div> with the same id).

Menu Generator
Select pastry names you would like ChatGPT to create an enticing menu for
Cancel
JS
import $ from 'jquery';

$(function () {
    $("#ko-menu-generator").koApplyBindings();
});
Python
from django import forms
from django.utils.translation import gettext_lazy, gettext as _
from crispy_forms import bootstrap as twbscrispy, layout as crispy
from corehq.apps.hqwebapp import crispy as hqcrispy


class Select2StaticKoForm(forms.Form):
    pastries = forms.MultipleChoiceField(
        label=gettext_lazy("Pastries"),
        required=False,
        help_text=gettext_lazy("Select pastry names you would like ChatGPT to create an enticing menu for"),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['pastries'].choices = [
            (pastry, pastry) for pastry in [
                "Mille-feuille", "Croissant", "Eclair", "Choux", "Cinnamon roll", "Tart", "Profiteroles",
                "Bear claw", "Croquembouche", "Baklava", "Cannoli", "Strudel", "Canele",
            ]
        ]

        self.helper = hqcrispy.HQFormHelper()
        self.helper.form_method = 'POST'
        self.helper.form_action = '#'
        self.helper.form_id = "ko-menu-generator"
        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Menu Generator"),
                crispy.Field('pastries', data_bind="staticSelect2: {}"),
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Create Menu"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                hqcrispy.LinkButton(
                    _("Cancel"),
                    '#',
                    css_class="btn btn-outline-primary",
                ),
            ),
        )

Autocomplete Knockout Binding

To initialize a select2 to autocomplete select suggestions, use the autocompleteSelect2 Knockout binding. The difference between this binding and the original select2, is the ability to enter free text.

HTML
<div id="js-ko-model-autocomplete">
  <select
    class="form-select"
    data-bind="
      autocompleteSelect2: dishes,
      value: value
    "
  >
  </select>
</div>
JS
import $ from 'jquery';
import ko from 'knockout';

$(function () {
    $("#js-ko-model-autocomplete").koApplyBindings(function () {
        return {
            dishes: [
                'Gorgonzola salad with star anise dressing',
                'Swede and kohlrabi soup',
                'Gorgonzola and marjoram risotto',
                'Tuna tart with gorgonzola sauce',
                'Potato salad with bergamot dressing',
                'Marrow salad with orange dressing',
                'Orange and plumcot cake',
                'Orange and strawberry muffins',
            ],
            value: ko.observable(''),
        };
    });
});

Autocomplete Knockout Binding with Crispy Forms

This is similar to the HTML-based setups above, except that an external script applies koApplyBindings to the <form> id (<form> can also be wrapped with a <div> with the same id).

Veggie Recipes
Select your favorite vegetable you want recipe suggestions for
Cancel
JS
import $ from 'jquery';
import ko from 'knockout';

$(function () {
    $("#ko-veggie-suggestions").koApplyBindings(function () {
        return {
            veggies: [
                "kale", "broccoli", "radish", "bell pepper", "sweet potato", "spinach", "cabbage",
            ],
            value: ko.observable(''),
        };
    });
});
Python
from django import forms
from django.utils.translation import gettext_lazy, gettext as _
from crispy_forms import bootstrap as twbscrispy, layout as crispy
from corehq.apps.hqwebapp import crispy as hqcrispy


class Select2AutocompleteKoForm(forms.Form):
    vegetable = forms.ChoiceField(
        label=gettext_lazy("Vegetable"),
        required=False,
        help_text=gettext_lazy("Select your favorite vegetable you want recipe suggestions for"),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.helper = hqcrispy.HQFormHelper()
        self.helper.form_method = 'POST'
        self.helper.form_action = '#'
        self.helper.form_id = "ko-veggie-suggestions"
        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Veggie Recipes"),
                crispy.Field('vegetable', data_bind="autocompleteSelect2: veggies, value: value"),
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Suggest Recipes"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                hqcrispy.LinkButton(
                    _("Cancel"),
                    '#',
                    css_class="btn btn-outline-primary",
                ),
            ),
        )

Crispy Forms Select2Ajax Widget

It's also possible to use select2 that fetches its options asynchronously in crispy forms using the Select2Ajax widget. This is extremely useful for selecting from a large data set, like Mobile Workers or Locations. A very simplified example is below.

Note that you will need a view to POST queries to with this widget. It's recommended to explore how Select2Ajax is currently being used in HQ.
Choose Filters
Select the users you want to view data for
Cancel
Python
from django import forms
from django.urls import reverse
from django.utils.translation import gettext_lazy, gettext as _
from crispy_forms import bootstrap as twbscrispy, layout as crispy
from corehq.apps.hqwebapp import crispy as hqcrispy
from corehq.apps.hqwebapp.widgets import Select2Ajax


class Select2AjaxDemoForm(forms.Form):
    user_filter = forms.Field(
        label=gettext_lazy("Users(s)"),
        required=False,
        widget=Select2Ajax(multiple=True),
        help_text=gettext_lazy("Select the users you want to view data for"),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['user_filter'].widget.set_url(
            reverse("styleguide_data_select2_ajax_demo")
        )

        self.helper = hqcrispy.HQFormHelper()
        self.helper.form_method = 'POST'
        self.helper.form_action = '#'
        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Choose Filters"),
                'user_filter',
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Search"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                hqcrispy.LinkButton(
                    _("Cancel"),
                    '#',
                    css_class="btn btn-outline-primary",
                ),
            ),
        )

Multiselect

This is a custom widget we built. It can be useful in situations where the user is adding and removing items from one list to another and wants to be able to see both the included and excluded items.

Be cautious adding it to new pages! Be sure that the visual weight and potential learning curve is worthwhile for the workflow you're creating. It's more complicated than a dropdown and takes up much more space.
HTML
<select multiple id="js-example-multiselect">
  <option>alpha</option>
  <option>beta</option>
  <option>gamma</option>
  <option>delta</option>
  <option>epsilon</option>
</select>
JS
import $ from 'jquery';
import multiselectUtils from 'hqwebapp/js/multiselect_utils';

let listener = function () {
    console.log("Triggered willSelectAllListener");
};

$(function () {
    multiselectUtils.createFullMultiselectWidget('js-example-multiselect', {
        selectableHeaderTitle: gettext("Available Letters"),
        selectedHeaderTitle: gettext("Letters Selected"),
        searchItemTitle: gettext("Search Letters..."),
        disableModifyAllActions: false,
        willSelectAllListener: listener,
    });
});

Optional Properties

In addition to the optional title properties, the following properties can be useful in situations where more control is needed.

disableModifyAllActions—defaults to false, useful when the preferred workflow is to disable the ability to select and remove all items at once.
willSelectAllListener—provides an opportunity to execute code prior to all items being selected

Use in Crispy Forms

This is similar to the manual setup above, but using crispy forms to provide the form HTML.

Team Builder
Make your team selection
Cancel
JS
import $ from 'jquery';
import multiselectUtils from 'hqwebapp/js/multiselect_utils';

let teamListener = function () {
    console.log("Triggered willSelectAllListener");
};

$(function () {
    multiselectUtils.createFullMultiselectWidget('id_team', {
        selectableHeaderTitle: gettext("Benched"),
        selectedHeaderTitle: gettext("Playing"),
        searchItemTitle: gettext("Search Team..."),
        disableModifyAllActions: false,
        willSelectAllListener: teamListener,
    });
});
Python
from django import forms
from django.utils.translation import gettext_lazy, gettext as _
from crispy_forms import bootstrap as twbscrispy, layout as crispy
from corehq.apps.hqwebapp import crispy as hqcrispy


class MultiselectDemoForm(forms.Form):
    team = forms.MultipleChoiceField(
        label=gettext_lazy("Team"),
        required=False,
        help_text=gettext_lazy("Make your team selection"),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # in theory, you can populate this from a parameter passed in args or kwargs
        self.fields['team'].choices = [
            (name, name) for name in [
                "Stephen Curry", "Nikola Jokic", "Joel Embiid", "Damian Lillard", "Kevin Durant", "Anthony Davis",
                "Jayson Tatum", "Paul George", "Zach LaVine", "Desmond Bane", "Bam Adebayo", "Karl-Anthony Towns"
            ]
        ]
        self.fields['team'].initial = [
            "Stephen Curry", "Jayson Tatum",
        ]

        self.helper = hqcrispy.HQFormHelper()
        self.helper.form_method = 'POST'
        self.helper.form_action = '#'
        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Team Builder"),
                'team',
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Save Team"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                hqcrispy.LinkButton(
                    _("Cancel"),
                    '#',
                    css_class="btn btn-outline-primary",
                ),
            ),
        )

Other Selection Interactions

There are several other interactions used on HQ to select items from a list, however use of these options should be limited compared to the options above.

Standard Select Elements

There's nothing inherently wrong with standard <select> elements, but since so many dropdowns use select2, <select> elements without this styling create visual inconsistency. It should typically be trivial to turn a standard <select> element into a select2.

Long Lists: Standard <select> elements do interfere with usability when their list of options gets long. For instance, with 15+ items, it becomes difficult to find and select an item. Therefore, long lists in particular should be switched to select2.

Lists of Checkboxes

Like standard HTML select elements, there's nothing inherently wrong with these, but because we don't use them often, they're bad for a consistent user experience.

At.js

At.js is a library for mentioning people in comments. It's the basis for easy references in Form Builder, used in the xpath expression builder. Form builder also uses it for autocomplete behavior (form builder doesn't have select2 available).

Case List Explorer use At.js for creating the advanced search query by allowing the user to search and reference case properties as they build a query.

Both Form Builder and Case List Explorer use At.js in a powerful way: to build queries or expressions with advanced syntax. At.js should not be used for simple autocomplete or select behavior.