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.
- python
>=3.8.1,<4.0 - Django
>=3.2 - django-crispy-forms
>=2.1,<3.0 - wagtail
>=2.15if usingWagtailBaseForm - sass
>=1.33.0if building the sass yourself
Note
govuk-frontend will not, and does not need to, be installed to use this package.
All form-related styles from govuk-frontend==5.4.1 have been copied into this project with the prepended "govuk-" replaced with "tbxforms-", e.g. .govuk-button to .tbxforms-button and @mixin govuk-clearfix to @mixin tbxforms-clearfix.
For non-government projects, installing the complete GOV.UK Frontend package unnecessarily increases the bundle size as we only need form-related styles.
For government projects, this increases the bundle size as both tbxforms and govuk-frontend must be installed. However, these projects are less common, so they are not prioritised.
You must install both the Python package and the NPM package.
Install using pip:
pip install tbxformsAdd 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 = ["tbxforms"] CRISPY_TEMPLATE_PACK = "tbxforms"Install using NPM:
npm install tbxformsNote: This package uses the Element.closest, NodeList.forEach, and Array.includes APIs. You will need to install and configure polyfills for legacy browser support if you need to.
Instantiate your forms:
import TbxForms from 'tbxforms'; document.addEventListener('DOMContentLoaded', () => { for (const form of document.querySelectorAll(TbxForms.selector())) { new TbxForms(form); } });Import the styles into your project...
...Either as CSS without any customisations:
@use 'node_modules/tbxforms/dist/style.css';...Or as Sass to customise variables:
@use 'node_modules/tbxforms/tbxforms.scss' with ( $tbxforms-text-colour: #000, $tbxforms-error-colour: #f00, );tbxforms provides out-of-the-box GOV.UK Design System styles for everything except buttons, as styles for these probably exist within 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 can be used for coded Django forms and editor-controlled Wagtail forms.
All forms must inherit the TbxFormsMixin mixin, as well as specifying a Django base form class (e.g. forms.Form or forms.ModelForm)
from django import forms from tbxforms.forms import TbxFormsMixin class ExampleForm(TbxFormsMixin, forms.Form): ... class ExampleModelForm(TbxFormsMixin, forms.ModelForm): ...Wagtail forms must inherit from TbxFormsMixin and WagtailBaseForm.
from wagtail.contrib.forms.forms import BaseForm as WagtailBaseForm from tbxforms.forms import TbxFormsMixin class ExampleWagtailForm(TbxFormsMixin, WagtailBaseForm): ...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_sexy_form %} </body> </html>A FormHelper allows you to alter the rendering behaviour of forms.
Every form that inherits from TbxFormsMixin (i.e. every form within tbxforms) will have a FormHelper with the following default attributes:
highlight_required_fields: see later section on highlighting required fieldshtml5_required = Truelabel_size = Size.MEDIUMlegend_size = Size.MEDIUMform_error_title = _("There is a problem with your submission")- Plus everything from django-crispy-forms' default attributes.
These can be changed during instantiation or on the go - examples below.
Submit buttons are not automatically added to forms. To add one, you can extend the form.helper.layout (examples below).
Extend during instantiation:
from django import forms from tbxforms.forms import TbxFormsMixin from tbxforms.layout import Button class YourSexyForm(TbxFormsMixin, forms.Form): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper.layout.extend([ Button.primary( name="submit", type="submit", value="Submit", ) ])Or afterwards:
from tbxforms.layout import Button form = YourSexyForm() form.helper.layout.extend([ Button.primary( name="submit", type="submit", value="Submit", ) ])tbxforms can show/hide parts of the layout depending on a given value. For example, you could show 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.
Field example:
from django import forms from django.core.exceptions import ValidationError from tbxforms.choices import Choice from tbxforms.forms import TbxFormsMixin from tbxforms.layout import Field, Layout class ExampleForm(TbxFormsMixin, 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) ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper.layout = Layout( # Add our newsletter sign-up field. Field.text("newsletter_signup"), # Add our email field and define the conditional logic. Field.text( "email", data_conditional={ "field_name": "newsletter_signup", # Field to inspect. "values": ["yes"], # Value(s) to cause this field to show. }, ), )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 Layout( Div( HTML("<p>Some relevant text.</p>"), Field.text("some_other_field"), Field.text("email"), data_conditional={ "field_name": "newsletter_signup", "values": ["yes"], }, ), )Conditional fields must be optional (required=False) as they are not always visible, but it can be useful to show them as required to the user.
To do this, use the conditional_fields_to_show_as_required() method:
from django import forms from django.core.exceptions import ValidationError from tbxforms.choices import Choice from tbxforms.forms import TbxFormsMixin from tbxforms.layout import Field, Layout class ExampleForm(TbxFormsMixin, 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) ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper.layout = Layout( # Add our newsletter sign-up field. Field.text("newsletter_signup"), # Add our email field and define the conditional logic. Field.text( "email", data_conditional={ "field_name": "newsletter_signup", # Field to inspect. "values": ["yes"], # Value(s) to cause this field to show. }, ), ) @staticmethod def conditional_fields_to_show_as_required() -> [str]: # Non-required fields that should show as required to the user. return [ "email", ] 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 marked as required. Therefore, 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.", } ) return cleaned_dataIf TBXFORMS_HIGHLIGHT_REQUIRED_FIELDS=False (or unset), optional fields will have "(optional)" appended to their labels. This is the default behaviour and recommended by GDS.
If TBXFORMS_HIGHLIGHT_REQUIRED_FIELDS=True, required fields will have an asterisk appended to their labels and optional fields will not be highlighted.
This setting can be changed on a per-form basis by setting the form helper's highlight_required_fields attribute:
from django import forms from tbxforms.forms import TbxFormsMixin class ExampleForm(TbxFormsMixin, forms.Form): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Highlight required fields regardless of settings.TBXFORMS_HIGHLIGHT_REQUIRED_FIELDS self.helper.highlight_required_fields = TrueYou can also style these markers by targeting these CSS classes:
.tbxforms-field_marker--required.tbxforms-field_marker--optional
Label and legend sizes can be changed through the form's helper, e.g.:
from tbxforms.layout import Size class ExampleForm(...): ... def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper.label_size = Size.LARGE self.helper.legend_size = Size.LARGEPossible values for the label_size and legend_size:
SMALLMEDIUM(default)LARGEEXTRA_LARGE
You can disable the error summary by setting show_error_summary=False in the form's helper, e.g.:
class ExampleForm(...): ... def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper.show_error_summary = FalseThis is useful if your form has a complex setup and you need complete control over the error summary - e.g. if the form also contains a formset. In this instance, you would want to create your own error summary template and include it in your template.
- 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