Package: tbxforms
Torchbox FormsA Torchbox-flavoured template pack fordjango-crispy-forms, adapted fromcrispy-forms-gds.Out of the box, forms created withtbxformswill look like theGOV.UK Design System, though many
variables can be customised.Requirementspython>=3.8.1,<4.0Django>=2.2django-crispy-forms>=1.13.0,<2.0wagtail>=2.15if usingWagtailBaseFormsass>=1.33.0if building the sass yourselfInstallationYou must install both the Python package and the NPM package.Install the Python packageInstall using pip:pipinstalltbxformsAdddjango-crispy-formsandtbxformsto your installed apps:INSTALLED_APPS=[# ...'crispy_forms',# django-crispy-forms'tbxforms',]Now add the following settings to telldjango-crispy-formsto usetbxforms:CRISPY_ALLOWED_TEMPLATE_PACKS=["tbx"]CRISPY_TEMPLATE_PACK="tbx"Install the NPM packageInstall using NPM:npminstalltbxformsNote: This package uses theElement.closest,NodeList.forEach, andArray.includesAPIs. You will need to install and configure polyfills for
legacy browser support if you need to.Instantiate your forms:importTbxFormsfrom'tbxforms';document.addEventListener('DOMContentLoaded',()=>{for(constformofdocument.querySelectorAll(TbxForms.selector())){newTbxForms(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-error-colour:#f00,$tbxforms-text-colour:#000,);Alternatively, variables can be defined in a centralised variables SCSS
such astbxforms/static/sass/abstracts/_variables.scss.Add button stylestbxformsprovides 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--warningUsagetbxformscan be used for coded Django forms and editor-controlled Wagtail forms.Django formsAll forms must inherit theTbxFormsMixinmixin, as well as specifying a Django base form class (e.g.forms.Formorforms.ModelForm)fromdjangoimportformsfromtbxforms.formsimportTbxFormsMixinclassExampleForm(TbxFormsMixin,forms.Form):...classExampleModelForm(TbxFormsMixin,forms.ModelForm):...Wagtail formsCreate or update a Wagtail formWagtail forms must inherit fromTbxFormsMixinandWagtailBaseForm.fromwagtail.contrib.forms.formsimportBaseFormasWagtailBaseFormfromtbxforms.formsimportTbxFormsMixinclassExampleWagtailForm(TbxFormsMixin,WagtailBaseForm):...Instruct a Wagtail Page model to use your formIn your form definitions(e.g. forms.py):fromtbxforms.formsimportBaseWagtailFormBuilderasTbxFormsBaseWagtailFormBuilderfrompath.to.your.formsimportExampleWagtailFormclassWagtailFormBuilder(TbxFormsBaseWagtailFormBuilder):defget_form_class(self):returntype(str("WagtailForm"),(ExampleWagtailForm,),self.formfields)And in your form page models(e.g. models.py):frompath.to.your.formsimportWagtailFormBuilderclassExampleFormPage(...):...form_builder=WagtailFormBuilder...Render a formJust 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>FormHelpersAFormHelperallows you to alter the rendering behaviour of forms.Every form that inherits fromTbxFormsMixin(i.e. every form withintbxforms)
will have aFormHelperwith the following default attributes:html5_required = Truelabel_size = Size.MEDIUMlegend_size = Size.MEDIUMform_error_title = _("There is a problem with your submission")Plus everything fromdjango-crispy-forms' default attributes.These can be changed during instantiation oron the go- examples below.Add a submit buttonSubmit buttons are not automatically added to forms. To add one, you can extend
theform.helper.layout(examples below).Extend during instantiation:fromdjangoimportformsfromtbxforms.formsimportTbxFormsMixinfromtbxforms.layoutimportButtonclassYourSexyForm(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:fromtbxforms.layoutimportButtonform=YourSexyForm()form.helper.layout.extend([Button.primary(name="submit",type="submit",value="Submit",)])Conditionally-required fieldstbxformscan show/hide parts of thelayoutdepending 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 tofield,div, andfieldsetelements.Note: any field names included within theconditional_fields_to_show_as_required()method will appear on the frontend
as required, though will technically berequired=False.Field example:fromdjangoimportformsfromdjango.core.exceptionsimportValidationErrorfromtbxforms.choicesimportChoicefromtbxforms.formsimportTbxFormsMixinfromtbxforms.layoutimportField,LayoutclassExampleForm(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.},),)@staticmethoddefconditional_fields_to_show_as_required()->[str]:# Non-required fields that should show as required to the user.return["email",]defclean(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.ifnewsletter_signup=="yes"andnotemail:raiseValidationError({"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.elifnewsletter_signup=="no"andemail:delcleaned_data['email']returncleaned_dataContainer example:When you have multiple fields/elements that you want to show/hide together, you
can use the exact samedata_conditionaldefinition as above but on adivorfieldsetelement, e.g.:fromtbxforms.layoutimportHTML,Div,Field,LayoutLayout(Div(HTML("<p>Some relevant text.</p>"),Field.text("some_other_field"),Field.text("email"),data_conditional={"field_name":"newsletter_signup","values":["yes"],},),)Customising behaviourChange the default label and legend classesPossible values for thelabel_sizeandlegend_size:SMALLMEDIUM(default)LARGEEXTRA_LARGEFurther readingDownload thePyPI packageDownload theNPM packageLearn more aboutDjango Crispy FormsLearn more aboutCrispy Forms GDSLearn more aboutGOV.UK Design System