Forms

HQ is largely a collection of forms.

Overview

Forms in HQ are a mix of bespoke HTML and Crispy Forms. Different parts of HQ use these two approaches, and you should always consider existing context when deciding whether to use Crispy Forms or HTML.

The benefit of Crispy Forms, and why you should opt for using it over bespoke HTML whenever possible, is that the HTML for each form component is controlled by templates. From HTML is often affected during a front-end migration (like Bootstrap). If a bespoke HTML form was used, that HTML needs to be re-examined everywhere. However, for forms using Crispy Forms, the relevant HTML only has to be changed once in the templates, which makes the overall migration faster and easier.

Crispy Forms

Crispy Forms generates HTML for forms based on form layouts defined in python. Use of this library contributes to consistency in design and reduces boilerplate HTML writing. It also helps reduce HTML changes required during a front-end migration.

Crispy Forms does not control the form's logic, processing, validation, or anything having to do with form data. It is used to specify the layout and hooks for the display logic, for instance a data-bind for Knockout. Django Forms still controls the remaining data-processing.

A Simple Example

Below is a very simple crispy forms example. The point where Crispy Forms becomes a part of the form is when self.helper is set. The layout of the form is then defined in self.helper.layout.

To include this form in a template, {% load crispy_forms_tags %} must be included at the top of the template, and {% crispy form %} should be placed where the form should appear — the variable form (or other variable name) is set in the template context.

Some additional notes:

  • It's best practice to set self.helper to either HQFormHelper or HQModalFormHelper, both defined in hqwebapp.crispy. These helpers help standardize the label_class and field_class css classes, as well as the form_class css class.

  • self.helper.form_action is where you can set the url for the form to post to.

  • It is possible to override self.helper.field_class and self.helper.form_class, but please do this sparingly. See Bootstrap's Column Documentation for more information about how to use these classes.

Basic Information
Advanced Information
Cancel
Python
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy

from crispy_forms import bootstrap as twbscrispy
from crispy_forms import layout as crispy

from corehq.apps.hqwebapp import crispy as hqcrispy
from corehq.apps.hqwebapp.widgets import BootstrapCheckboxInput


class BasicCrispyExampleForm(forms.Form):
    """
    This is a basic example form that demonstrates
    the use of Crispy Forms in HQ.
    """
    full_name = forms.CharField(
        label=gettext_lazy("Full Name"),
    )
    message = forms.CharField(
        label=gettext_lazy("Message"),
        widget=forms.Textarea(attrs={"class": "vertical-resize"}),
    )
    forward_message = forms.BooleanField(
        label=gettext_lazy("Forward Message"),
        required=False,
        widget=BootstrapCheckboxInput(
            inline_label=gettext_lazy(
                "Yes, forward this message to me."
            ),
        ),
    )
    language = forms.ChoiceField(
        label=gettext_lazy("Language"),
        choices=(
            ('en', gettext_lazy("English")),
            ('fr', gettext_lazy("French")),
            ('es', gettext_lazy("Spanish")),
            ('de', gettext_lazy("German")),
        ),
        required=False,
    )
    language_test_status = forms.BooleanField(
        label=gettext_lazy("Include me in language tests"),
        required=False,
    )

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

        # Here's what makes the form a Crispy Form
        self.helper = hqcrispy.HQFormHelper()

        # This is the layout of the form where we can explicitly specify the
        # order of fields and group fields into fieldsets:
        self.helper.layout = crispy.Layout(

            crispy.Fieldset(
                # This is the title for the group of fields that follows:
                _("Basic Information"),

                # By default, specifying a string with the field's slug
                # invokes crispy.Field as the default display component
                'full_name',

                # This line is effectively the same as the line above
                # and useful for adding attributes:
                crispy.Field('message'),

                # This is a special component that is best to use
                # in combination with BootstrapCheckboxInput on a
                # BooleanField (see Molecules > Checkboxes)
                hqcrispy.CheckboxField('forward_message'),
            ),
            crispy.Fieldset(
                _("Advanced Information"),
                'language',
                'language_test_status',
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Send Message"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                hqcrispy.LinkButton(  # can also be a StrictButton
                    _("Cancel"),
                    '#',
                    css_class="btn btn-outline-primary",
                ),
            ),
        )

Using Knockout with Crispy Forms

The example below demonstrates various ways Knockout Bindings can be applied within a Crispy Form. Please review the comments in python code for further details.

Report an Issue
JS
$(function () {
    'use strict';

    let ExampleFormModel = function () {
        let self = {};
        self.fullName = ko.observable();
        self.areas = ko.observableArray([
            gettext('Forms'), gettext('Cases'), gettext('Reports'), gettext('App Builder'),
        ]);
        self.area = ko.observable();
        self.includeMessage = ko.observable(false);
        self.message = ko.observable();
        self.alertText = ko.observable();

        self.onFormSubmit = function () {
            // an ajax call would likely happen here in the real world

            self.alertText(gettext("Thank you, " + self.fullName() + ", for your submission!"));
            self._resetForm();
        };

        self.cancelSubmission = function () {
            self.alertText(gettext("Submission has been cancelled."));
            self._resetForm();
        };

        self._resetForm = function () {
            self.fullName('');
            self.area(undefined);
            self.includeMessage(false);
            self.message('');

            // clear alert text after 2 sec
            setTimeout(function () {
                self.alertText('');
            }, 2000);
        };
        return self;
    };
    $("#ko-example-crispy-form").koApplyBindings(ExampleFormModel());
});
Python
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy

from crispy_forms import bootstrap as twbscrispy
from crispy_forms import layout as crispy

from corehq.apps.hqwebapp import crispy as hqcrispy
from corehq.apps.hqwebapp.widgets import BootstrapSwitchInput


class KnockoutCrispyExampleForm(forms.Form):
    """
    This is an example form that demonstrates the use
    of Crispy Forms in HQ with Knockout JS
    """
    full_name = forms.CharField(
        label=gettext_lazy("Full Name"),
    )
    area = forms.ChoiceField(
        label=gettext_lazy("Area"),
        required=False,
    )
    include_message = forms.BooleanField(
        label=gettext_lazy("Options"),
        widget=BootstrapSwitchInput(
            inline_label=gettext_lazy(
                "include message"
            ),
            # note that some widgets prefer to set data-bind attributes
            # this way, otherwise the formatting looks off:
            attrs={"data-bind": "checked: includeMessage"},
        ),
        required=False,
    )
    message = forms.CharField(
        label=gettext_lazy("Message"),
        widget=forms.Textarea(attrs={"class": "vertical-resize"}),
        required=False,
    )

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

        self.helper = hqcrispy.HQFormHelper()

        self.helper.form_id = "ko-example-crispy-form"
        self.helper.attrs.update({
            # we can capture the submit action with a data-bind here:
            "data-bind": "submit: onFormSubmit",
        })

        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Report an Issue"),
                crispy.Div(
                    # It's also possible to use crispy.HTML
                    # instead of crispy.Div, but make sure any HTMl
                    # inserted here is safe
                    crispy.Div(
                        css_class="alert alert-info",
                        # data-bind to display alertText
                        data_bind="text: alertText",
                    ),
                    # data-bind to toggle visibility
                    data_bind="visible: alertText()"
                ),
                crispy.Field(
                    'full_name',
                    # data-bind applying value of input to fullName
                    data_bind="value: fullName"
                ),
                crispy.Field(
                    'area',
                    # data-bind creating select2 (see Molecules > Selections)
                    data_bind="select2: areas, value: area"
                ),
                hqcrispy.CheckboxField('include_message'),
                crispy.Div(
                    crispy.Field('message', data_bind="value: message"),
                    # we apply a data-bind on the visibility to a wrapper
                    # crispy.Div, otherwise only the textarea visibility
                    # is toggled, while the label remains
                    data_bind="visible: includeMessage",
                ),

            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Submit Report"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                twbscrispy.StrictButton(
                    _("Cancel"),
                    css_class="btn btn-outline-primary",
                    # data-bind on the click event of the Cancel button
                    data_bind="click: cancelSubmission",
                ),
            ),
        )

HTML Forms

HQ uses styles provided by Bootstrap 5 Forms.

Notes on the example below:

  • Forms 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. See comments in the HTML example below if usage isn't clear.

  • The dropdown here (and throughout this section) should use a select2, as discussed in Molecules > Selections.

  • The textarea uses the vertical-resize CSS 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="off" attribute on the inputs controls the browser's form autofill. Most forms in HQ are unique to HQ and should always 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.

Basic Information
Cancel
HTML
<form action="#someUrl"
      method="post">
  <!-- the {% csrf_token %} template tag should be included here -->
  <fieldset>
    <legend>Basic Information</legend>
    <div class="row mb-3">
      <!-- Generally, it is best practice to use the {% css_label_class %}
           template tag below instead of the col-* classes -->
      <label for="id_first_name"
             class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
        First Name
      </label>
      <!-- Generally, it is best practice to use the {% css_field_class %}
           template tag below instead of the col-* classes -->
      <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
        <input type="text"
               name="first_name"
               class="form-control"
               id="id_first_name"
               autocomplete="off" />
      </div>
    </div>
    <div class="row mb-3">
      <label for="id_favorite_color"
             class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
        Favorite Color
      </label>
      <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
        <select name="favorite_color"
                class="form-select hqwebapp-select2"
                id="id_favorite_color">
          <option value="red">Red</option>
          <option value="green">Green</option>
          <option value="blue">Blue</option>
        </select>
      </div>
    </div>
    <div class="row mb-3">
      <label for="id_hopes"
             class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
        Hopes and Dreams
      </label>
      <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
        <textarea name="hopes"
                  class="form-control vertical-resize"
                  id="id_hopes"></textarea>
      </div>
    </div>
  </fieldset>
  <div class="form-actions row">
    <!-- Generally, it is best practice to use the {% css_action_class %}
         template tag below instead of offset-* and col-* -->
    <div class="offset-xs-12 offset-sm-4 offset-md-4 offset-lg-3 col-xs-12 col-sm-8 col-md-8 col-lg-9">
      <button class="btn btn btn-primary" type="submit">Save</button>
      <a href="#" class="btn btn-outline-primary">Cancel</a>
    </div>
  </div>
</form>

Form Validation

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.

Showing Field Errors

Errors in forms should be displayed near the relevant field / input using the is-invalid class directly on the errored form-control or form-select. The corresponding feedback message can be provided beneath the relevant field inside of a <span class="invalid-feedback"> element. This element should come before the form-text (help text) element. See the example below, as well as Bootstrap5's Validation docs for more options.

Please enter your full name. Thank you!
This is the space for help text for a field.
Please select a favorite color.
Please enter your response to "Hopes and Dreams".
HTML
<div class="row mb-3">
  <label for="id_first_name"
         class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
    Full Name
  </label>
  <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">

    <!-- this is how an invalid input is marked with the is-invalid class -->
    <input class="form-control is-invalid"
           type="text"
           name="full_name"
           id="id_first_name"
           autocomplete="off" />

    <!-- Note that span.invalid-feedback element comes before .form-text -->
    <span class="invalid-feedback">
      Please enter your full name. Thank you!
    </span>

    <div class="form-text">
      This is the space for help text for a field.
    </div>
  </div>
</div>
<div class="row mb-3">
  <label for="id_favorite_color"
         class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
    Favorite Color
  </label>
  <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
    <select name="favorite_color"
            class="form-select hqwebapp-select2 is-invalid"
            id="id_favorite_color">
      <option value="red">Red</option>
      <option value="green">Green</option>
      <option value="blue">Blue</option>
    </select>
    <span class="invalid-feedback">
      Please select a favorite color.
    </span>
  </div>
</div>
<div class="row mb-3">
  <label for="id_hopes"
         class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
    Hopes and Dreams
  </label>
  <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
    <textarea name="hopes"
              class="form-control vertical-resize is-invalid"
              id="id_hopes"></textarea>
    <span class="invalid-feedback">
      Please enter your response to "Hopes and Dreams".
    </span>
  </div>
</div>

Showing Form Errors

Sometimes we encounter a situation where a general error was encountered when creating a form that can't be pinpointed to a specific field. In this case, we should use the Django Messages framework to raise an error from the view to the page. This might look something like:

messages.error(request, gettext("This is an error"))

In javascript, alert_user.js provides the same functionality.

Showing Errors in Crispy Forms

Crispy Forms automatically adds the is-invalid and valid-feedback markup when a field throws a ValidationError when calling is_valid() on the attached Django Form. The example below shows how we can throw a field-level error.

We can also throw form-level ValidationErrors in the main clean() method of the form. However, it is preferred that we use the Django Messages framework (as explained above) for raising these general errors.

Error State Test
This is a field-level error for the field 'note'.
This field will always error.
Python
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy

from crispy_forms import bootstrap as twbscrispy
from crispy_forms import layout as crispy

from corehq.apps.hqwebapp import crispy as hqcrispy


class ErrorsCrispyExampleForm(forms.Form):
    full_name = forms.CharField(
        label=gettext_lazy("Full Name"),
    )
    note = forms.CharField(
        label=gettext_lazy("Note"),
        required=False,
        help_text=gettext_lazy("This field will always error.")
    )

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

        self.helper = hqcrispy.HQFormHelper()
        self.helper.form_action = "#form-errors"

        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Error State Test"),
                'full_name',
                'note',
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Submit Form"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
            ),
        )

    def clean_note(self):
        # we can validate the value of note and throw ValidationErrors here
        # FYI this part is standard Django Forms functionality and unrelated to Crispy Forms
        raise forms.ValidationError(_("This is a field-level error for the field 'note'."))

Marking Fields as Valid

Valid fields be displayed near the relevant field / input using the is-valid class directly on the valid form-control or form-select. The corresponding feedback message can be provided beneath the relevant field inside of a <span class="valid-feedback"> element. This element should come before the form-text (help text) element.

Looks good. Thank you!
This is the space for help text for a field.
Great! Thanks for providing us your preference.
Fantastic. We love to hear it!
HTML
<div class="row mb-3">
  <label for="id_first_name"
         class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
    Full Name
  </label>
  <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">

    <!-- this is how a valid input is marked with the is-valid class -->
    <input class="form-control is-valid"
           type="text"
           name="full_name"
           value="Jon Jackson"
           id="id_first_name"
           autocomplete="off" />

    <!-- Note that span.valid-feedback element comes before .form-text -->
    <span class="valid-feedback">
      Looks good. Thank you!
    </span>

    <div class="form-text">This is the space for help text for a field.</div>
  </div>
</div>
<div class="row mb-3">
  <label for="id_favorite_color"
         class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
    Favorite Color
  </label>
  <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
    <select name="favorite_color"
            class="form-select hqwebapp-select2 is-valid"
            id="id_favorite_color">
      <option value="red">Red</option>
      <option value="green" selected>Green</option>
      <option value="blue">Blue</option>
    </select>
    <span class="valid-feedback">
      Great! Thanks for providing us your preference.
    </span>
  </div>
</div>
<div class="row mb-3">
  <label for="id_hopes"
         class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
    Hopes and Dreams
  </label>
  <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
    <textarea name="hopes"
              class="form-control vertical-resize is-valid"
              id="id_hopes">Traveling to the stars...</textarea>
    <span class="valid-feedback">
      Fantastic. We love to hear it!
    </span>
  </div>
</div>

Valid Feedback in Crispy Forms

At the moment, Django Forms doesn't propagate valid feedback up to the form, so Crispy Forms has no way to display this information automatically. In the real world, marking fields as valid will likely come from client-side validation with Knockout Validation.

Knockout Validation

Knockout Validation is an extension of Knockout that we use to do client-side validation of form fields.

We have several custom validators that can be used as well as the built-in ones that ship with Knockout Validation:

  • validators.ko.js contains several custom validators. Use of these validators is demonstrated in the example below.

    password_validators.ko.js contains validators related to password checks. minimumPasswordLength is demonstrated below, but zxcvbnPassword needs to be used inside an AMD module setup.

The example below makes use of most of our custom knockout validators (in various states) as well as some built-in validators to demonstrate how we might use Knockout Validation to do client-side validation on a user creation form. Please review the comments in the source code for additional guidance.

Create New User
Hint: 'jon' is taken. Try typing that in to trigger an error.
Hint: 'jon@dimagi.com' is taken. Try typing that in to trigger an error.
JS
$(function () {
    'use strict';

    let UserModel = function () {
        let self = {},
            _rateLimit = {
                rateLimit: {
                    method: "notifyWhenChangesStop",
                    timeout: 400,
                },
            },
            // This line below would be part of an hqDefine import
            initialPageData = hqImport("hqwebapp/js/initial_page_data");

        self.username = ko.observable()
            .extend(_rateLimit)
            .extend({
                // It's possible to stack validators like this:
                required: {
                    message: gettext("Please specify a username."),
                    params: true,
                },
                minLength: {
                    message: gettext("Username must be at least three characters long."),
                    params: 3,
                },
            })
            .extend({
                validation: {
                    async: true,
                    validator: function (val, params, callback) {
                        // Order matters when specifying validators. This check only uses the previous two validators to calculate isValid()
                        if (self.username.isValid()) {
                            $.post(initialPageData.reverse('styleguide_validate_ko_demo'), {
                                username: self.username(),
                            }, function (result) {
                                callback({
                                    isValid: result.isValid,
                                    message: result.message,
                                });
                            });
                        }
                    },
                },
            });

        self.password = ko.observable()
            .extend(_rateLimit)
            .extend({
                required: {
                    message: gettext("Please specify a password."),
                    params: true,
                },
                minimumPasswordLength: {
                    params: 6,
                    message: gettext("Your password must be at least 6 characters long"),
                },
            });

        self.email = ko.observable()
            .extend({
                required: {
                    message: gettext("Please specify an email."),
                    params: true,
                },
                emailRFC2822: true,
            });

        // The async validation for email is decoupled in the emailDelayed here. Notice the difference in response between validating email vs username.
        // Being able to rate limit server-side calls is **extremely important** in a production environment to prevent unnecessary calls to the server.
        self.emailDelayed = ko.pureComputed(self.email)
            .extend(_rateLimit)
            .extend({
                validation: {
                    async: true,
                    validator: function (val, params, callback) {
                        if (self.email.isValid()) {
                            $.post(initialPageData.reverse('styleguide_validate_ko_demo'), {
                                email: self.email(),
                            }, function (result) {
                                callback({
                                    isValid: result.isValid,
                                    message: result.message,
                                });
                            });
                        }
                    },
                },
            });

        return self;
    };

    let ExampleFormModel = function () {
        let self = {};

        // newUser exists as a separate model so that it's easier to reset validation in _resetForm() below
        self.newUser = ko.observable(UserModel());

        self.isFormValid = ko.computed(function () {
            // When performing form validation ensure that async validators are not in isValidating states. If using delayed validators, ensure their states are also checked.
            return  (self.newUser().username.isValid()
                && !self.newUser().username.isValidating()
                && self.newUser().password.isValid()
                && self.newUser().email.isValid()
                && self.newUser().emailDelayed.isValid()
                && !self.newUser().emailDelayed.isValidating());
        });

        self.disableSubmit = ko.computed(function () {
            return !self.isFormValid();
        });

        self.alertText = ko.observable();

        self.onFormSubmit = function () {
            // an ajax call would likely happen here in the real world

            self.alertText("Thank you! '" + self.newUser().username() + "' has been created.");
            self._resetForm();
        };

        self.cancelSubmission = function () {
            self.alertText(gettext("Resetting form..."));
            self._resetForm();
        };

        self._resetForm = function () {
            self.newUser(UserModel());

            // clear alert text after 2 sec
            setTimeout(function () {
                self.alertText('');
            }, 2000);
        };
        return self;
    };
    $("#ko-validation-example").koApplyBindings(ExampleFormModel());
});
Python
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy

from crispy_forms import bootstrap as twbscrispy
from crispy_forms import layout as crispy

from corehq.apps.hqwebapp import crispy as hqcrispy


class KnockoutValidationCrispyExampleForm(forms.Form):
    """
    This is an example form that demonstrates the use
    of Crispy Forms in HQ with Knockout Validation
    """
    username = forms.CharField(
        label=gettext_lazy("Username"),
        help_text=gettext_lazy("Hint: 'jon' is taken. Try typing that in to trigger an error."),
    )
    password = forms.CharField(
        label=gettext_lazy("Password"),
        widget=forms.PasswordInput,
    )
    email = forms.CharField(
        label=gettext_lazy("Email"),
        help_text=gettext_lazy("Hint: 'jon@dimagi.com' is taken. Try typing that in to trigger an error."),
    )

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

        self.helper = hqcrispy.HQFormHelper()

        self.helper.form_id = "ko-validation-example"
        self.helper.attrs.update({
            "data-bind": "submit: onFormSubmit",
        })

        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Create New User"),
                crispy.Div(
                    crispy.Div(
                        css_class="alert alert-info",
                        data_bind="text: alertText",
                    ),
                    data_bind="visible: alertText()"
                ),
                crispy.Div(
                    crispy.Field(
                        "username",
                        # koValidationStateFeedback is a custom binding
                        # handler we created add success messages
                        # and additional options asynchronous validation
                        data_bind="textInput: username,"
                                  "koValidationStateFeedback: { "
                                  "   validator: username,"
                                  "   successMessage: gettext('This username is available.'),"
                                  "   checkingMessage: gettext('Checking if username is available...'),"
                                  "}",
                    ),
                    crispy.Field(
                        "password",
                        autocomplete="off",
                        # FYI textInput is a special binding that updates
                        # the value of the observable on keyUp.
                        data_bind="textInput: password,"
                                  "koValidationStateFeedback: { "
                                  "   validator: password,"
                                  "   successMessage: gettext('Perfect!'),"
                                  "}",
                    ),
                    crispy.Field(
                        "email",
                        autocomplete="off",
                        # This usage of koValidationStateFeedback
                        # demonstrates how to couple standard validators
                        # with a rate-limited async validator
                        # and have all the state messages
                        # appear gracefully in the same place.
                        data_bind="textInput: email,"
                                  "koValidationStateFeedback: { "
                                  "   validator: email,"
                                  "   delayedValidator: emailDelayed,"
                                  "   successMessage: gettext('This email is available.'),"
                                  "   checkingMessage: gettext('Checking if email is available...'),"
                                  "}",
                    ),
                    # We need the wrapper crispy.Div to apply the with
                    # binding to these fields.
                    # Calling newUser().username() works for
                    # the first instance of newUser, but not
                    # after it is re-initialized in _resetForm()
                    data_bind="with: newUser",
                ),
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Create User"),
                    type="submit",
                    css_class="btn btn-primary",
                    data_bind="disable: disableSubmit"
                ),
                twbscrispy.StrictButton(
                    _("Cancel"),
                    css_class="btn btn-outline-primary",
                    data_bind="click: cancelSubmission",
                ),
            ),
        )

Field States

In addition to validation states described above, there are other field states available.

Disabled & Readonly / Plain Text Fields

Disabling a field gives it a grayed out appearance, removes pointer events, and prevents focusing. This can be done by adding the disabled attribute to the field.

Additionally, we can mark a field as readonly and plain text, which removes the input styling and prevents the field from being editing, while also displaying the value as plain text. To do this, add the form-control-plaintext class to the field AND the readonly attribute. Note that this only works for input and textarea elements.

Generally it's best to use disabled on fields that are not editable due to permissions or form logic, but would otherwise be editable. If a field cannot be editable by any means and the text is intended to always be read-only, please use the form-control-plaintext class alongside the readonly attribute.

HTML Example

Examples of Disabled Fields
Examples of Readonly Fields
HTML
<fieldset>
  <legend>
    Examples of Disabled Fields
  </legend>
  <div class="row mb-3">
    <label for="id_first_name_dis"
           class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
      First Name
    </label>
    <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
      <input type="text"
             name="first_name_dis"
             value="Jon"
             class="form-control"
             id="id_first_name_dis"
             autocomplete="off"
             disabled />
      <!-- ^^ notice the disabled attribute above -->
    </div>
  </div>
  <div class="row mb-3">
    <label for="id_favorite_color_dis"
           class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
      Favorite Color
    </label>
    <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
      <select name="favorite_color_dis"
              class="form-select hqwebapp-select2"
              id="id_favorite_color_dis"
              disabled>
        <!-- ^^ notice the disabled attribute above -->
        <option value="red">Red</option>
        <option value="green">Green</option>
        <option value="blue">Blue</option>
      </select>
    </div>
  </div>
  <div class="row mb-3">
    <label for="id_hopes_dis"
           class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
      Hopes and Dreams
    </label>
    <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
      <textarea name="hopes_dis"
                class="form-control vertical-resize"
                id="id_hopes_dis"
                disabled>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce in facilisis lectus. Cras accumsan ante vel massa sagittis faucibus.</textarea>
      <!-- ^^ notice the disabled attribute above -->
    </div>
  </div>
</fieldset>
<fieldset>
  <legend>
    Examples of Readonly Fields
  </legend>
  <div class="row mb-3">
    <label for="id_first_name_ro"
           class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
      First Name
    </label>
    <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
      <input type="text"
             name="first_name_ro"
             value="Jon"
             id="id_first_name_ro"
             autocomplete="off"
             class="form-control-plaintext"
             readonly />
      <!-- ^^ notice the readonly attribute and form-control-plaintext css class above -->
    </div>
  </div>
  <div class="row mb-3">
    <label for="id_hopes_ro"
           class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
      Hopes and Dreams
    </label>
    <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
      <textarea name="hopes_ro"
                id="id_hopes_ro"
                class="form-control-plaintext"
                readonly>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce in facilisis lectus. Cras accumsan ante vel massa sagittis faucibus.</textarea>
      <!-- ^^ notice the readonly attribute and form-control-plaintext css class above -->
    </div>
  </div>
</fieldset>

Crispy Forms Example

Examples of Disabled Fields
Examples of Readonly Fields
Python
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy

from crispy_forms import layout as crispy

from corehq.apps.hqwebapp import crispy as hqcrispy


class DisabledFieldsExampleForm(forms.Form):
    """
    This is example demonstrates the use of
    disabled and readonly plaintext fields
    in Crispy Forms.
    """
    # NOTE the _dis and _ro in the field slugs are just to differentiate similar fields and not part of convention
    full_name_dis = forms.CharField(
        label=gettext_lazy("Full Name"),
    )
    message_dis = forms.CharField(
        label=gettext_lazy("Message"),
        widget=forms.Textarea(),
    )
    full_name_ro = forms.CharField(
        label=gettext_lazy("Full Name"),
    )
    message_ro = forms.CharField(
        label=gettext_lazy("Message"),
        widget=forms.Textarea(),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Sets up initial data. You may also pass a dictionary to the initial kwarg when initializing the form.
        self.fields['full_name_dis'].initial = "Jon Jackson"
        self.fields['message_dis'].initial = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " \
                                             "Fusce in facilisis lectus. Cras accumsan ante vel massa " \
                                             "sagittis faucibus."
        self.fields['full_name_ro'].initial = "Jon Jackson"
        self.fields['message_ro'].initial = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " \
                                            "Fusce in facilisis lectus. Cras accumsan ante vel massa " \
                                            "sagittis faucibus."

        self.helper = hqcrispy.HQFormHelper()
        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Examples of Disabled Fields"),
                # note the disabled attribute
                crispy.Field('full_name_dis', disabled=""),
                crispy.Field('message_dis', disabled=""),
            ),
            crispy.Fieldset(
                _("Examples of Readonly Fields"),
                # note the disabled attribute and
                # form-control-plaintext css_class
                crispy.Field(
                    'full_name_ro',
                    readonly="",
                    css_class="form-control-plaintext"
                ),
                crispy.Field(
                    'message_ro',
                    readonly="",
                    css_class="form-control-plaintext"
                ),
            ),
        )

Placeholders & Help Text

Placeholders and help text are a great way to give the user guidance when filling out a form. Placeholders insert "hint text" directly in a field that gets replaced by the inputted value, while help text is text that appears beneath the field.

Placeholders are useful for providing example formatting for the expected input.

Help text is useful for providing detailed guidance or comments related to the field.

HTML Example

We'll never share your email with anyone else.
HTML
<div class="row mb-3">
  <label for="id_email"
         class="col-form-label col-xs-12 col-sm-4 col-md-4 col-lg-3">
    Email
  </label>
  <div class="col-xs-12 col-sm-8 col-md-8 col-lg-9">
    <!-- note the placeholder attribute has a suggested value filled in that will disappear when you type -->
    <input placeholder="name@example.com"
           type="email"
           name="email"
           class="form-control"
           id="id_email"
           autocomplete="off"
           aria-labelledby="emailHelp" />
    <!-- The form text below will always remain below the field. Note the usage of aria-labelledby attribute on the input applied to the id of the form-text element. This is best practice for accessibility. -->
    <div class="form-text" id="emailHelp">
      We'll never share your email with anyone else.
    </div>
  </div>
</div>

Crispy Forms Example

We'll never share your email with anyone else.
Python
from django import forms
from django.utils.translation import gettext_lazy

from crispy_forms import layout as crispy

from corehq.apps.hqwebapp import crispy as hqcrispy


class PlaceholderHelpTextExampleForm(forms.Form):
    """
    This example demonstrates the use of placeholders
    and help text in Crispy Forms
    """
    email = forms.EmailField(
        label=gettext_lazy("Email"),
        # note that the help_text is set here
        help_text=gettext_lazy("We'll never share your email with anyone else."),
    )

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

        self.helper = hqcrispy.HQFormHelper()
        self.helper.layout = crispy.Layout(
            crispy.Field('email', placeholder="name@example.com"),
        )