A Torchbox-flavoured template pack for django-crispy-forms, adapted from crispy-forms-gds.
Out of the box, forms created with tbxforms
will look like the
GOV.UK Design System, though many
variables can be customised.
You must install both the Python package and the NPM package.
pip install tbxforms
Add django-crispy-forms
and tbxforms
to your installed apps:
INSTALLED_APPS = [
...
'crispy_forms', # django-crispy-forms
'tbxforms',
]
Now add the following settings to tell django-crispy-forms
to use tbxforms
:
CRISPY_ALLOWED_TEMPLATE_PACKS = ["tbx"]
CRISPY_TEMPLATE_PACK = "tbx"
There are two optional settings which will control whether HTML is rendered for
a field's label
and help_text
. The defaults are set to False
(which escapes the HTML and prevents it from being rendered):
TBXFORMS_ALLOW_HTML_LABEL = False
TBXFORMS_ALLOW_HTML_HELP_TEXT = False
TBXFORMS_ALLOW_HTML_BUTTON = False
npm install tbxforms
This package uses the Element.closest
, NodeList.forEach
, and Array.includes
APIs. You will additionally need to install and configure polyfills for legacy browser support.
import TbxForms from 'tbxforms';
document.addEventListener('DOMContentLoaded', () => {
for (const form of document.querySelectorAll(TbxForms.selector())) {
new TbxForms(form);
}
});
...Either as CSS without any customisations:
@use 'node_modules/tbxforms/style.css';
...Or as Sass to customise variables:
@use 'node_modules/tbxforms/tbxforms.scss' with (
$tbxforms-error-colour: #f00,
$tbxforms-text-colour: #000,
);
Alternatively, variables can be defined in a centralised variables SCSS such as tbxforms/static/sass/abstracts/_variables.scss.
tbxforms
provides out-of-the-box GOV.UK Design System styles for everything
except buttons, as styles for these probably exist in your project.
You will need to write button styles for the following classes:
.tbxforms-button
.tbxforms-button.tbxforms-button--primary
.tbxforms-button.tbxforms-button--secondary
.tbxforms-button.tbxforms-button--warning
tbxforms
supports Django (>=2.2,<=4.0
) and Wagtail (>=2.15
) forms.
django>=2.2,<=4.0
is supported.
All forms must inherit from TbxFormsBaseForm
and whichever Django base form class.
from django import forms
from tbxforms.forms import BaseForm as TbxFormsBaseForm
class ExampleForm(TbxFormsBaseForm, forms.Form):
# < Your field definitions and helper property >
class ExampleModelForm(TbxFormsBaseForm, forms.ModelForm):
# < Your field definitions, ModelForm config, and helper property >
wagtail>=2.15
is supported.
Wagtail forms must inheirt from TbxFormsBaseForm
and WagtailBaseForm
.
from wagtail.contrib.forms.forms import BaseForm as WagtailBaseForm
from tbxforms.forms import BaseForm as TbxFormsBaseForm
class ExampleWagtailForm(TbxFormsBaseForm, WagtailBaseForm):
# < Your helper property >
In your form definitions (e.g. forms.py):
from tbxforms.forms import BaseWagtailFormBuilder as TbxFormsBaseWagtailFormBuilder
from path.to.your.forms import ExampleWagtailForm
class WagtailFormBuilder(TbxFormsBaseWagtailFormBuilder):
def get_form_class(self):
return type(str("WagtailForm"), (ExampleWagtailForm,), self.formfields)
And in your form page models (e.g. models.py):
from path.to.your.forms import WagtailFormBuilder
class ExampleFormPage(...):
...
form_builder = WagtailFormBuilder
...
Just like Django Crispy Forms, you need to pass your form object to the
{% crispy ... %}
template tag, e.g.:
{% load crispy_forms_tags %}
<html>
<body>
{% crispy your_form %}
</body>
</html>
Submit buttons are not automatically added - you will need to do this by
extending the form helper's layout
(example below).
Every form that inherits from TbxFormsBaseForm
will have the following
attributes set:
html5_required = True
label_size = Size.MEDIUM
legend_size = Size.MEDIUM
form_error_title = _("There is a problem with your submission")
- Plus everything from django-crispy-forms' default attributes.
These can be overridden (and/or additional attributes from the above list defined) just like you would do with any other inherited class, e.g.:
from django import forms
from wagtail.contrib.forms.forms import BaseForm as WagtailBaseForm
from tbxforms.forms import BaseForm as TbxFormsBaseForm
from tbxforms.layout import Button, Size
class YourSexyForm(TbxFormsBaseForm, forms.Form):
@property
def helper(self):
fh = super().helper
# Override some settings
fh.html5_required = False
fh.label_size = Size.SMALL
fh.form_error_title = _("Something's wrong, yo.")
# Add a submit button
fh.layout.extend([
Button.primary(
name="submit",
type="submit",
value="Submit",
)
])
return fh
Possible values for the label_size
and legend_size
:
SMALL
MEDIUM
(default)LARGE
EXTRA_LARGE
tbxforms
can show/hide parts of the layout
depending on a given value. For
example, you could show (and require) an email address field only when the user
chooses to sign up to a newsletter (examples below).
You can apply this logic to field
, div
, and fieldset
elements.
Note: any field names included within the
conditional_fields_to_show_as_required()
method will appear on the frontend
as required, though will technically be required=False
.
Field example:
from django import forms
from tbxforms.choices import Choice
from tbxforms.forms import BaseForm as TbxFormsBaseForm
from tbxforms.layout import Field, Layout
class ExampleForm(TbxFormsBaseForm, forms.Form):
NEWSLETTER_CHOICES = (
Choice("yes", "Yes please", hint="Receive occasional email newsletters."),
Choice("no", "No thanks"),
)
newsletter_signup = forms.ChoiceField(
choices=NEWSLETTER_CHOICES
)
email = forms.EmailField(
widget=forms.EmailInput(required=False)
)
@staticmethod
def conditional_fields_to_show_as_required() -> [str]:
# Non-required fields that should show as required to the user.
return [
"email",
]
@property
def helper(self):
fh = super().helper
# Override what is rendered for this form.
fh.layout = Layout(
# Add our newsletter sign-up field.
Field("newsletter_signup"),
# Add our email field and define the conditional logic.
Field(
"email",
data_conditional={
"field_name": "newsletter_signup", # Field to inspect.
"values": ["yes"], # Value(s) to cause this field to show.
},
),
)
return fh
def clean(self):
cleaned_data = super().clean()
newsletter_signup = cleaned_data.get("newsletter_signup")
email = cleaned_data.get("email")
# Fields included within `conditional_fields_to_show_as_required()` will
# be shown as required but not enforced - i.e. they will not have the
# HTML5 `required` attribute set.
# Thus we need to write our own check to enforce the value exists.
if newsletter_signup == "yes" and not email:
raise ValidationError(
{
"email": _("This field is required."),
}
)
# The tbxforms JS will attempt to clear any redundant data upon submission,
# though it is recommended to also handle this in your clean() method.
elif newsletter_signup == "no" and email:
del cleaned_data['email']
return cleaned_data
Container example:
When you have multiple fields/elements that you want to show/hide together, you
can use the exact same data_conditional
definition as above but on a div
or
fieldset
element, e.g.:
from tbxforms.layout import HTML, Div, Field
Layout(
Div(
HTML("<p>Some relevant text.</p>"),
Field("some_other_field"),
Field("email"),
data_conditional={
"field_name": "newsletter_signup",
"values": ["yes"],
},
),
)
- Download the PyPI package
- Download the NPM package
- Learn more about Django Crispy Forms
- Learn more about Crispy Forms GDS
- Learn more about GOV.UK Design System