Skip to content

Instantly share code, notes, and snippets.

@MatMoore
Last active October 2, 2025 09:25
Show Gist options
  • Select an option

  • Save MatMoore/3cc92470d46145fe443c475f48657fe0 to your computer and use it in GitHub Desktop.

Select an option

Save MatMoore/3cc92470d46145fe443c475f48657fe0 to your computer and use it in GitHub Desktop.
Form builder: conditional field patterns
"""
Possible DSL for building conditional fields
It should take care of
- conditional `required` validation
- passing the `conditional` html to the frontend component
- preserving ordering of errors
- blanking values if the matching option is deselected
"""
class MyForm(FormWithConditionalFields):
# foo = ...
# bar = ...
def __init__(self):
with self.conditional_field_builder() as builder:
builder['foo'].each_option.reveals(lambda _value: CharField(required=False, label='abc'), name_suffix='details'))
builder['bar'][CHOICE1].reveals(CharField(required=True, label='def')

Checkbox reveal

  • when the checkbox is selected, another field is revealed
  • the field is required if and only if the checkbox is selected
  • if the checkbox is not selected, any value from the field should be ignored
    investigated = yes_no_field(
        label="Has this been investigated?",
        error_messages={
            "required": "Select whether the lump has been investigated or not"
        },
    )
    investigation_details = CharField(
        required=False,
        label="Provide details",
        hint="Include where, when and the outcome",
        error_messages={"required": "Enter details of any investigations"},
        classes="nhsuk-u-width-two-thirds",
    )

Dynamic details

  • for each choice in the choice field, there is a field for providing more details
  • the fields are basically all the same
    • but these need to be unique fields on the django forms, since all are rendered to the page, only the visibility changes when you select different options
  • when dynamically adding fields, it's important to preserve the correct field order, so that errors are reported in form order
  • the value could be persisted to a single model field, since the fields are mutually exclusive
    • but then you need to map back the appropriate field when passing initial
  • the conditional field's value is either required in all cases where the parent field is set, or it depends on the particular value
  • values from the conditional fields that are not visible should be ignored

Initialisation

        for choice_value, _ in self.STOPPED_REASON_CHOICES:
            details_field_name = f"{choice_value}_details"
            self.fields[details_field_name] = CharField(
                required=False, label="Provide details"
            )

Clean

        if (
            "stopped_reasons" in cleaned_data
            and "other" in cleaned_data["stopped_reasons"]
        ):
            if not cleaned_data.get("other_details"):
                self.add_error(
                    "other_details", "Explain why this appointment cannot proceed"
                )

Save

        for field_name, value in self.cleaned_data.items():
            if field_name.endswith("_details") and value:
                reasons_json[field_name] = value

Template

{% for choice_value, _ in form.STOPPED_REASON_CHOICES %}
  {% do form.stopped_reasons.add_conditional_html(choice_value, form[choice_value + "_details"].as_field_group()) %}
{% endfor %}

Specified other

  • the choice field has an "other" value
  • when other is selected, a "details" field is revealed
  • the details field is required if and only if the other value is selected
  • any details value should be ignored if "other" is selected
    area = ChoiceField(
        choices=RightLeftOtherChoices,
        label="Where is the swelling or shape change located?",
        error_messages={
            "required": "Select the location of the swelling or shape change"
        },
    )
    area_description = CharField(
        required=False,
        label="Describe the specific area",
        hint="For example, the left armpit",
        error_messages={
            "required": "Describe the specific area where the swelling or shape change is located"
        },
        classes="nhsuk-u-width-two-thirds",
    )

Mixed reveals

  • specific options reveal extra fields
  • the conditional fields are not necessarily the same thing
  • each conditional field is required if it's parent option is selected
  • values from conditional fields that are not visible should be ignored
      {% do form.when_taken.add_conditional_html(form.WhenTaken.EXACT, form.exact_date.as_field_group()) %}
      {% do form.when_taken.add_conditional_html(form.WhenTaken.APPROX, form.approx_date.as_field_group()) %}
      {{ form.when_taken.as_field_group() }}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment