diff --git a/CHANGELOG.md b/CHANGELOG.md index cadc7cdc..e69223a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Fix `show_label='visually-hidden'` being ignored by `bootstrap_field`, `bootstrap_form` and `bootstrap_formset`, which rendered an ordinary visible label instead of one hidden from sight (#526). The string is truthy, so the check that adds the `visually-hidden` class only ever caught `False`, even though both values have always been documented as equivalent. - Add `show_label` to `bootstrap_label`, accepting the same values as on `bootstrap_field`: `True`, `False`/`'visually-hidden'` to hide the label from sight but keep it for screen readers, and `'skip'` to render nothing (#526). A hidden label gets `visually-hidden` in place of the default `form-label`, as `bootstrap_field` and Bootstrap's own examples do; a `label_class` you pass yourself is kept either way. +- 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. ## 26.4 (2026-10-11) diff --git a/docs/forms.rst b/docs/forms.rst index 0fa47795..99bd2db5 100644 --- a/docs/forms.rst +++ b/docs/forms.rst @@ -50,6 +50,53 @@ 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``. 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. + +Each input still needs an accessible name. A single ``
" ), ) + + +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()}), + ( + '' + '' + ), + )