Forms
HQ is largely a collection of forms.
On this page
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 eitherHQFormHelper
orHQModalFormHelper
, both defined inhqwebapp.crispy
. These helpers help standardize thelabel_class
andfield_class
css classes, as well as theform_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
andself.helper.form_class
, but please do this sparingly. See Bootstrap's Column Documentation for more information about how to use these classes.
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.
$(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()); });
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.
<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.
<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 ValidationError
s 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.
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.
<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, butzxcvbnPassword
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.
$(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()); });
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
<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
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
<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
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"), )