Organisms
Molecules connect into organisms: even larger building blocks, the kind that make up a substantial portion of the design of a new feature or significant update to an existing feature.
Forms on CommCare HQ
Forms in HQ are a mix of bespoke HTML and Crispy Forms. Different parts of HQ use these two different approaches; both are supported.
HTML forms
HQ uses styles provided
by Bootstrap 3 Forms,
with the majority of forms using the form-horizontal
style.
Notes on the example below:
-
Forms also need to include a
{% csrf_token %}
tag to protect against CSRF attacks. HQ will reject forms that do not contain this token. -
The sets of grid classes (
col-sm-*
, etc.) can be replaced by{% css_field_class %}
,{% css_label_class %}
, and{% css_action_class %}
, which will fill in HQ's standard form proportions. - The dropdown here (and throughout this section) should use a select2, as discussed in selections.
-
The textarea uses the
vertical-resize
class to allow for long input. Inputs that accept XPath expressions are especially likely to have very long input. The text area does not support horizontal resizing, which can allow the user to expand a textarea so that it overlaps with other elements or otherwise disrupts the page's layout. -
The
autocomplete
attribute controls the browser's form autofill. Most forms in HQ are unique to HQ and should set turn off autocomplete to prevent unexpected automatic input. Exceptions would be forms that include information like a user's name and address. - This example does not show translations, but all user-facing text should be translated.
Simple HTML form
<form action="#" class="form-horizontal" method="post"> <fieldset> <legend>Basic Information</legend> <div class="form-group"> <label for="id_first_name" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label"> First Name </label> <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls"> <input type="text" name="first_name" class="form-control" id="id_first_name" autocomplete="off" /> </div> </div> <div class="form-group"> <label for="id_favorite_color" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label"> Favorite Color </label> <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls"> <select name="favorite_color" class="form-control" id="id_favorite_color"> <option value="red">Red</option> <option value="green">Green</option> <option value="blue">Blue</option> </select> </div> </div> <div class="form-group"> <label for="id_hopes" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label"> Hopes and Dreams </label> <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls"> <textarea name="hopes" class="form-control vertical-resize" id="id_hopes"></textarea> </div> </div> </fieldset> <div class="form-actions"> <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 col-sm-offset-4 col-md-offset-4 col-lg-offset-2 controls"> <button class="btn btn btn-primary" type="submit">Save</button> <a href="#" class="btn btn-default">Cancel</a> </div> </div> </form>
Form states and error messaging
Good error messages are specific, actionable, visually near the affected input. They occur as soon as a problem is detected. They help the user figure out how to address the situation: "Sorry, this isn't supported. Try XXX." Without any cues, the user is stuck in the same frustrating situation.
Errors in forms should be displayed near the relevant input using the .has-error
class. Note
that .has-error
is applied to the input's parent container, while any error
message should have be marked with .help-block
. There are .has-warning
and
.has-success
classes similar to .has-error
, though we don't use them often.
For form-level or page-level errors, use django's standard messages framework:
messages.error
, etc. In javascript, our alert_user module provides similar functionality.
Form with errors
<form action="#" class="form-horizontal" method="post"> <fieldset> <legend>Basic Information</legend> <div class="form-group"> <label for="id_normal_input" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label"> Normal Input </label> <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls"> <input type="text" name="normal_input" class="form-control" id="id_normal_input" /> </div> </div> <div class="form-group"> <label for="id_hint_input" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label"> Hint Text </label> <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls"> <input type="text" name="hint_input" class="form-control" placeholder='This is a hint' id="id_hint_input" /> </div> </div> <div class="form-group"> <label for="id_disabled_input" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label text-muted"> Disabled Input </label> <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls"> <input type="text" name="disabled_input" class="form-control" disabled id="id_disabled_input" /> </div> </div> <div class="form-group has-error"> <label for="id_error_input" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label"> Error Input </label> <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls"> <input type="text" name="error_input" class="form-control" id="id_error_input" /> <span class='help-block'>This is an error message.</span> </div> </div> </fieldset> <div class="form-actions"> <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 col-sm-offset-4 col-md-offset-4 col-lg-offset-2 controls"> <button class="btn btn btn-primary" type="submit">Save</button> <a href="#" class="btn btn-default">Cancel</a> </div> </div> </form>
Glossary of HTML form elements and classes
- Fieldset
-
A grouping of form fields.
<fieldset> ... </fieldset>
- Fieldset Legend
-
A title that describes the group of fields succinctly.
Always present as the first tag inside a
<fieldset />
<fieldset> <legend>Basic Information</legend> ... </fieldset>
- Form Group
-
An item that provides one bit of functionality or collects one subset of
data for the form. It contains
controls
and itscontrol-label
and is present inside the<fieldset>
after the<legend>
. Sometimes the control group might have one label for a set of related fields (multi field) or a modifiable table of objects.<fieldset> <legend>Basic Information</legend> <div class="form-group"> <label for="emailInput" class="col-lg-2 control-label">Email</label> <div class="col-lg-10 controls"> <input id="emailInput" ...> </div> </div> ... </fieldset>
- Control Label
-
A descriptive label that is visually aligned to the left of its controls.
<div class="form-group"> <label for="emailInput" class="col-lg-2 control-label">Email</label> <div class="col-lg-10 controls"> <input id="emailInput" ...> </div> </div>
Note that thefor
attribute refers to theid
attribute of the input. Clicking on the label will focus on the input that has theid
specified infor
. - Controls
-
A functional unit / input in the form.
- Form Actions
-
A group of buttons that affect the processing or navigation for
the entire form and is present before the closing
</form>
tag.<div class="form-group form-actions"> ... </div>
- Submit Action
-
It is always the first button specified in Form Actions and appears as the left-most button. It has the following visual appearance, using the
btn-primary
class:It submits the form and triggers its processing.
- Secondary Action
-
In Form Actions, it is the button with the following visual appearance, using the
btn-default
class:Typically, this is the cancel button. It does some other action that causes you to navigate away from the form, but not save.
Crispy Forms
Example with annotated sourceCrispy Forms generates HTML for forms based on python form definitions. Using it contributes to consistency in design and reduces boilerplate HTML writing. Crispy does not control the form's logic, processing, validation, or anything having to do with form data. It can be used specify hooks for the display logic.
Notes on the example below:
-
The
form_method
andform_action
are the keys to how and where the form is submitted. -
self.helper
controls the label and field widths. Best practice is to inherit from a class likeHQFormHelper
orHQModalFormHelper
(both defined in hqwebapp.crispy) to get the standard form proportions. If necessary, custom classes can be set usingself.helper.label_class
andself.helper.field_class
. The form's.form-horizontal
is also automatically set byHQFormHelper
and similar classes, but it can be overridden withself.helper.form_class
-
Resize this window to see the form responsiveness in action.
HQFormHelper
uses.col-xs-12.col-sm-4.col-md-4.col-lg-2
for labels and.col-xs-12.col-sm-8.col-md-8.col-lg-6
for fields. On a laptop or monitor, you likely see 1:4 or 1:3 proportions, but if you shrink the browser window enough, you'll see the label and field eventually stack on top of each other. See bootstrap grid documentation for a more thorough understanding of the responsive grid system. -
All layout objects accept the attribute
css_class
to specify custom css-classes to the field, like so:hqcrispy.LinkButton( _("Cancel"), '#', css_class="btn btn-default", )
-
Other attributes, for example a
data-bind=""
, can be added like so:crispy.Field( 'first_name', data_bind="value: firstName", )
All underscores (_
) in the attributes in Python turn into hyphens (-
) in HTML, e.g.,data_bind="foo"
becomesdata-bind="foo"
.
Basic Crispy Form
Located in corehq.apps.styleguide.example_forms
from django import forms from django.utils.translation import gettext_lazy, gettext as _ from crispy_forms import layout as crispy from crispy_forms import bootstrap as twbscrispy from corehq.apps.hqwebapp import crispy as hqcrispy class BasicCrispyForm(forms.Form): first_name = forms.CharField( label=gettext_lazy("First Name"), ) favorite_color = forms.ChoiceField( label=gettext_lazy("Pick a Favorite Color"), choices=( ('red', gettext_lazy("Red")), ('green', gettext_lazy("Green")), ('blue', gettext_lazy("Blue")), ('purple', gettext_lazy("Purple")), ), ) def __init__(self, *args, **kwargs): super(BasicCrispyForm, self).__init__(*args, **kwargs) self.helper = hqcrispy.HQFormHelper() self.helper.form_method = 'POST' self.helper.form_action = '#' self.helper.layout = crispy.Layout( crispy.Fieldset( _("Basic Information"), crispy.Field('first_name'), crispy.Field('favorite_color'), ), hqcrispy.FormActions( twbscrispy.StrictButton( _("Save"), type="submit", css_class="btn btn-primary", ), hqcrispy.LinkButton( _("Cancel"), '#', css_class="btn btn-default", ), ), )
Tables
Keep tables scannable and think about how big they might get.
When adding a table, first consider the nature the information you're displaying. Tables are best suited to tabular data, so if that's not what you're working with, consider other design options.
To increase scannability:
- Remove uncessary design
- Don't stretch tables
- Align headings with data
- Align to the decimal point
- Use whitespace
- Keep small screens in mind, consider oblique headers (headers at a 45-degree angle)
- In languages that read left to right, left-align text and right-align numbers
Recommended reading: https://alistapart.com/article/web-typography-tables
Most of our tables use hand-crafted markup based on Bootstrap's styles. Some areas of CommCare, particularly reporting, use DataTables and are tightly integrated with the python code that generates the data. The remainder of this section is primarily relevant to Bootstrap tables.
Basic Tables
To control column spacing, use the .col-(xs|sm|mg|lg)-[0-9]
classes provided by
Bootstrap's grid system
.
Basic table
Case Type | Name | Owner | Status |
---|---|---|---|
patient | Arundhati | worker1 | open |
patient | Karan | worker4 | open |
patient | Salman | worker1 | open |
patient | Aravind | worker4 | closed |
patient | Katherine | worker3 | closed |
<table class="table table-striped table-hover"> <thead> <tr> <th class="col-sm-2">Case Type</th> <th class="col-sm-4">Name</th> <th class="col-sm-4">Owner</th> <th class="col-sm-2">Status</th> </tr> </thead> <tbody> <tr> <td>patient</td> <td>Arundhati</td> <td>worker1</td> <td>open</td> </tr> <tr> <td>patient</td> <td>Karan</td> <td>worker4</td> <td>open</td> </tr> <tr> <td>patient</td> <td>Salman</td> <td>worker1</td> <td>open</td> </tr> <tr> <td>patient</td> <td>Aravind</td> <td>worker4</td> <td>closed</td> </tr> <tr> <td>patient</td> <td>Katherine</td> <td>worker3</td> <td>closed</td> </tr> </tbody> </table>
Scaling Tables
Most tables of user-created data will, at least for some projects, grow to a point where they should support a summary, pagination, and possibly searching. Your table doesn't necessarily need all of these, but consider how users are likely to use your table and manipulate the data in it.
Table with total, pagination, and search
<div class="form-inline pull-right"> <div class="input-group"> <input type="text" class="form-control" placeholder="Filter Cases" /> <span class="input-group-btn"> <button type="button" class="btn btn-default"> <i class="fa fa-search"></i> </button> </span> </div> </div> <strong>5 of 23 cases</strong> <table class="table table-striped table-hover"> <thead> <tr> <th class="col-sm-2"> <i class="fa fa-unsorted text-muted"></i> Case Type </th> <th class="col-sm-3"> <i class="fa fa-unsorted text-muted"></i> Name </th> <th class="col-sm-3"> <i class="fa fa-unsorted text-muted"></i> Owner </th> <th class="col-sm-2"> <i class="fa fa-sort-amount-asc"></i> Status </th> <th class="col-sm-2">Action</th> </tr> </thead> <tbody> <tr> <td>patient</td> <td>Arundhati</td> <td>worker1</td> <td>open</td> <td><button class="btn btn-danger">Archive</button></td> </tr> <tr> <td>patient</td> <td>Karan</td> <td>worker4</td> <td>open</td> <td><button class="btn btn-danger">Archive</button></td> </tr> <tr> <td>patient</td> <td>Salman</td> <td>worker1</td> <td>open</td> <td><button class="btn btn-danger">Archive</button></td> </tr> <tr> <td>patient</td> <td>Aravind</td> <td>worker4</td> <td>closed</td> <td><button class="btn btn-danger">Unarchive</button></td> </tr> <tr> <td>patient</td> <td>Katherine</td> <td>worker3</td> <td>closed</td> <td><button class="btn btn-danger">Unarchive</button></td> </tr> </tbody> </table> <div class="row"> <div class="col-sm-5"> <div class="form-inline pagination-text"> <span>Showing 1 to 5 of 23 entries</span> <select class="form-control"> <option value="5" selected>5 per page</option> <option value="25">25 per page</option> <option value="50">50 per page</option> <option value="100">100 per page</option> </select> </div> </div> <div class="col-sm-7 text-right"> <ul class="pagination"> <li><a href="#">»</a></li> <li class="active"><a href="#">1</a></li> <li><a href="#">2</a></li> <li><a href="#">3</a></li> <li><a href="#">4</a></li> <li><a href="#">5</a></li> <li><a href="#">»</a></li> </ul> </div> </div>