From 0004da0793a0dc898fef52056fae2951de296399 Mon Sep 17 00:00:00 2001 From: Shreyansh Jain Date: Tue, 29 Sep 2026 18:25:19 +0530 Subject: [PATCH 1/2] Add wrapper=False to render a field without its wrapper --- CHANGELOG.md | 1 + docs/forms.rst | 29 ++++++ src/django_bootstrap5/renderers.py | 12 +++ .../templatetags/django_bootstrap5.py | 13 ++- tests/test_bootstrap_field_parameters.py | 95 +++++++++++++++++++ 5 files changed, 149 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ade90d9..7f04e511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Add a `wrapper` argument to `bootstrap_field`, `bootstrap_form` and `bootstrap_formset` (#438). With `wrapper=False` a field renders its label, widget, errors and help text without the wrapper `div`, so several fields can share one element of your own, such as an `input-group`. The classes the wrapper carried (`wrapper_class`, the horizontal `row`, and the required and validation indicator classes) are then not rendered, as there is no element to carry them; the `is-valid`/`is-invalid` classes on the widget are unaffected. `wrapper_class=""` keeps rendering `
` as before. - Test on Python 3.15 (release candidate) against Django 6.1 and `main`. The job is non-blocking and 3.15 is not yet advertised as supported. - Fail the CI gate when any job fails, not only the test matrix. A failing docs or packaging build could previously be merged. diff --git a/docs/forms.rst b/docs/forms.rst index 0fa47795..41c53fc2 100644 --- a/docs/forms.rst +++ b/docs/forms.rst @@ -50,6 +50,35 @@ Bootstrap 5 offers Input groups to combine fields and add-ons (both before and a Note: input-group needs has-validation https://github.com/twbs/bootstrap/blob/6b3254536bac263c39e3a536c3c13945210d91b2/site/content/docs/5.0/migration.md +To put more than one field in a single input group, render the fields without their wrappers by +passing ``wrapper=False``: + +.. code:: html + +
+ {% bootstrap_field form.field_a wrapper=False %} + {% bootstrap_field form.field_b wrapper=False %} +
+ +Each field then renders its label, widget, errors and help text, but no wrapper ``div``: + +.. code:: html + +
+ + + + +
+ +The classes the wrapper carried are not rendered anywhere, because the element that carried them is +gone: ``wrapper_class``, the ``row`` class of the horizontal layout, and the ``error_css_class``, +``success_css_class`` and ``required_css_class`` indicator classes. The element you wrap the fields in +is yours, so put what you need on it, including ``has-validation`` when the group contains a field that +can show validation feedback. The ``is-valid``/``is-invalid`` classes on the widgets themselves are +unaffected. With ``layout="floating"``, the widget and its label keep their own ``form-floating`` +element inside the group, as Bootstrap's floating labels require. + Floating labels --------------- diff --git a/src/django_bootstrap5/renderers.py b/src/django_bootstrap5/renderers.py index 8d565815..8f7afc9b 100644 --- a/src/django_bootstrap5/renderers.py +++ b/src/django_bootstrap5/renderers.py @@ -35,6 +35,7 @@ class BaseRenderer: def __init__(self, **kwargs): self.layout = kwargs.get("layout", get_bootstrap_setting("layout")) + self.wrapper = kwargs.get("wrapper", True) self.wrapper_class = kwargs.get("wrapper_class", get_bootstrap_setting("wrapper_class")) self.inline_wrapper_class = kwargs.get("inline_wrapper_class", get_bootstrap_setting("inline_wrapper_class")) self.field_class = kwargs.get("field_class", "") @@ -87,6 +88,7 @@ def get_kwargs(self): """Return kwargs to pass on to child renderers.""" context = { "layout": self.layout, + "wrapper": self.wrapper, "wrapper_class": self.wrapper_class, "field_class": self.field_class, "label_class": self.label_class, @@ -553,6 +555,11 @@ def render(self): field = format_html('
{}{}{}{}
', classes, addon_before, field, addon_after, errors) errors = "" + if not self.wrapper and self.is_floating and not self.has_addons: + # Without a wrapper there is nothing to carry `form-floating`, so the widget and its label get + # their own element, the same nesting Bootstrap uses for floating labels inside an input group. + field = format_html('
{}
', field) + if isinstance(self.widget, CheckboxInput): field = format_html('
{}{}{}
', self.get_checkbox_classes(), field, errors, help) errors = "" @@ -565,6 +572,11 @@ def render(self): '
{}
', horizontal_class, field_with_errors_and_help ) + if not self.wrapper: + # The label, widget, errors and help text are rendered as before, without an element around them. + # The classes the wrapper carried are dropped, since the element that carried them is gone. + return format_html("{}{}", label, field_with_errors_and_help) + return format_html( '
{label}{field_with_errors_and_help}
', wrapper_classes=self.get_wrapper_classes(), diff --git a/src/django_bootstrap5/templatetags/django_bootstrap5.py b/src/django_bootstrap5/templatetags/django_bootstrap5.py index 57dcaaf0..9ef9e388 100644 --- a/src/django_bootstrap5/templatetags/django_bootstrap5.py +++ b/src/django_bootstrap5/templatetags/django_bootstrap5.py @@ -371,8 +371,19 @@ def bootstrap_field(field, **kwargs): Layout set in ``'bootstrap_form'`` takes precedence over layout set in ``'bootstrap_formset'``. Layout set in ``'bootstrap_field'`` takes precedence over layout set in ``'bootstrap_form'``. + wrapper + Whether to render the ``div`` that wraps the field and label. Set this to ``False`` to render + the label, widget, errors and help text without a wrapper, so several fields can share an + element of your own, such as a Bootstrap ``input-group``. The classes the wrapper carried + (``wrapper_class``, ``row``, and the required and validation indicator classes) are then not + rendered, as there is no element to carry them; put them on your own element instead. The + ``is-valid``/``is-invalid`` classes on the widget itself are not affected. + + :default: ``True`` + wrapper_class - CSS class of the ``div`` that wraps the field and label. + CSS class of the ``div`` that wraps the field and label. An empty value still renders the + wrapper, with an empty ``class``; pass ``wrapper=False`` to leave it out altogether. :default: ``'mb-3'`` diff --git a/tests/test_bootstrap_field_parameters.py b/tests/test_bootstrap_field_parameters.py index 360e2f9c..b6be4774 100644 --- a/tests/test_bootstrap_field_parameters.py +++ b/tests/test_bootstrap_field_parameters.py @@ -89,3 +89,98 @@ def test_inline_wrapper_class(self): "
" ), ) + + +class InputGroupTestForm(forms.Form): + first = forms.CharField(help_text="First help") + second = forms.CharField(required=False) + + +class BootstrapFieldWrapperTestCase(BootstrapTestCase): + """Test the `wrapper` parameter of `bootstrap_field`.""" + + def test_wrapper_default_is_unchanged(self): + """Test that not passing `wrapper` renders the wrapper as before.""" + self.assertHTMLEqual( + self.render("{% bootstrap_field form.test %}", context={"form": CharFieldTestForm()}), + ( + '
' + '' + '' + "
" + ), + ) + + def test_wrapper_false(self): + """Test that `wrapper=False` renders label and field without a wrapper.""" + self.assertHTMLEqual( + self.render("{% bootstrap_field form.test wrapper=False %}", context={"form": CharFieldTestForm()}), + ( + '' + '' + ), + ) + + def test_wrapper_false_in_input_group(self): + """Test that two fields without wrappers can share one input group.""" + self.assertHTMLEqual( + self.render( + '
' + "{% bootstrap_field form.first wrapper=False %}" + "{% bootstrap_field form.second wrapper=False %}" + "
", + context={"form": InputGroupTestForm()}, + ), + ( + '
' + '' + '' + '
First help
' + '' + '' + "
" + ), + ) + + def test_wrapper_false_keeps_errors_and_help_text(self): + """Test that `wrapper=False` keeps the error block and the help text, and drops wrapper classes.""" + self.assertHTMLEqual( + self.render( + "{% bootstrap_field form.first wrapper=False %}", context={"form": InputGroupTestForm(data={})} + ), + ( + '' + '' + '
' + '
This field is required.
' + "
" + '
First help
' + ), + ) + + def test_wrapper_false_keeps_form_floating(self): + """Test that `wrapper=False` keeps a `form-floating` element for a floating label.""" + self.assertHTMLEqual( + self.render( + "{% bootstrap_field form.test layout='floating' wrapper=False %}", + context={"form": CharFieldTestForm()}, + ), + ( + '
' + '' + '' + "
" + ), + ) + + def test_wrapper_false_on_form(self): + """Test that `wrapper=False` on `bootstrap_form` reaches the fields.""" + self.assertHTMLEqual( + self.render("{% bootstrap_form form wrapper=False %}", context={"form": CharFieldTestForm()}), + ( + '' + '' + ), + ) From 8cf47ffb4d8c50523a50db17e7f889efdc3fabb7 Mon Sep 17 00:00:00 2001 From: Shreyansh Jain Date: Tue, 6 Oct 2026 17:19:03 +0530 Subject: [PATCH 2/2] Keep labels out of the input-group example Bootstrap lays out every child of an input group as a flex item, so a field's label is stretched into the row instead of sitting above it, and Bootstrap's own examples keep the label outside the group. The example now renders the fields with show_label='skip' and says the group takes a label of your own, outside it. --- docs/forms.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/forms.rst b/docs/forms.rst index 41c53fc2..484395db 100644 --- a/docs/forms.rst +++ b/docs/forms.rst @@ -51,23 +51,23 @@ Note: input-group needs has-validation https://github.com/twbs/bootstrap/blob/6b3254536bac263c39e3a536c3c13945210d91b2/site/content/docs/5.0/migration.md To put more than one field in a single input group, render the fields without their wrappers by -passing ``wrapper=False``: +passing ``wrapper=False``. Bootstrap lays out every child of an input group as a flex item, so a +field's own label would be stretched into the row instead of sitting above it. Render the fields +with ``show_label='skip'`` and give the group a single label of your own, outside it: .. code:: html
- {% bootstrap_field form.field_a wrapper=False %} - {% bootstrap_field form.field_b wrapper=False %} + {% bootstrap_field form.field_a wrapper=False show_label='skip' %} + {% bootstrap_field form.field_b wrapper=False show_label='skip' %}
-Each field then renders its label, widget, errors and help text, but no wrapper ``div``: +Each field then renders its widget, errors and help text, but no wrapper ``div`` and no label: .. code:: html
- -