Selections
HQ has different interactions for selecting data from a list.
On this page
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.
<select-toggle params=" options: [ 'peaceful', 'easy', 'feeling' ] " ></select-toggle>
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
.
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.
<select id="js-manual-select2" class="form-select basic"> <option>one</option> <option>two</option> <option>three</option> </select>
import $ from 'jquery'; import 'select2/dist/js/select2.full.min'; $(function () { $("#js-manual-select2").select2(); });
A clearable version of that select2
element:
<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>
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.
import $ from 'jquery'; import 'select2/dist/js/select2.full.min'; $(function () { $("#id_location").select2(); $("#id_experiences").select2(); });
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.
<select class="form-select hqwebapp-select2"> <option>uno</option> <option>dos</option> <option>tres</option> </select>
This also applies to <select multiple>
elements:
<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.
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.
<div id="js-ko-model-dynamic"> <select class="form-select" data-bind=" select2: letters, value: value " ></select> </div>
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
.
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"), }; }); });
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.
<div id="js-ko-model-static"> <select class="form-select" data-bind="staticSelect2: {}" > <option>un</option> <option>deux</option> <option>trois</option> </select> </div>
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
).
import $ from 'jquery'; $(function () { $("#ko-menu-generator").koApplyBindings(); });
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.
<div id="js-ko-model-autocomplete"> <select class="form-select" data-bind=" autocompleteSelect2: dishes, value: value " > </select> </div>
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
).
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(''), }; }); });
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.
POST
queries to with this widget. It's recommended to explore
how Select2Ajax
is currently being used in HQ.
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.
<select multiple id="js-example-multiselect"> <option>alpha</option> <option>beta</option> <option>gamma</option> <option>delta</option> <option>epsilon</option> </select>
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.
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, }); });
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
.
<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.