Repository: zostera/django-bootstrap5

Issue:
wrapper_class does not override mb-3
In django-bootstrap4, `bootstrap_field field form_group_class='foo'` will use a form group class of `foo`.

In django-bootstrap5, `bootstrap_field field wrapper_class='foo'` will use a wrapper class of `foo mb-3`.

This breaks cases where you need to remove the default margin.

A possible workaround is to use `layout='inline'`, but that adds unrelated styling which might cause other issues.


Discussion:

Resolving patch:
diff --git a/src/django_bootstrap5/core.py b/src/django_bootstrap5/core.py
--- a/src/django_bootstrap5/core.py
+++ b/src/django_bootstrap5/core.py
@@ -16,6 +16,8 @@
     },
     "theme_url": None,
     "javascript_in_head": False,
+    "wrapper_class": "mb-3",
+    "inline_wrapper_class": "",
     "horizontal_label_class": "col-sm-2",
     "horizontal_field_class": "col-sm-10",
     "horizontal_field_offset_class": "offset-sm-2",
diff --git a/src/django_bootstrap5/forms.py b/src/django_bootstrap5/forms.py
--- a/src/django_bootstrap5/forms.py
+++ b/src/django_bootstrap5/forms.py
@@ -1,9 +1,6 @@
 from .core import get_field_renderer, get_form_renderer, get_formset_renderer
 from .html import render_tag
 
-WRAPPER_CLASS = ""
-WRAPPER_TAG = "div"
-
 
 def render_formset(formset, **kwargs):
     """Render a formset to a Bootstrap layout."""
diff --git a/src/django_bootstrap5/renderers.py b/src/django_bootstrap5/renderers.py
--- a/src/django_bootstrap5/renderers.py
+++ b/src/django_bootstrap5/renderers.py
@@ -15,7 +15,7 @@
 
 from .core import get_bootstrap_setting
 from .css import merge_css_classes
-from .forms import WRAPPER_CLASS, WRAPPER_TAG, render_field, render_form, render_label
+from .forms import render_field, render_form, render_label
 from .size import DEFAULT_SIZE, SIZE_MD, get_size_class, parse_size
 from .text import text_value
 from .utils import render_template_file
@@ -27,7 +27,8 @@ class BaseRenderer(object):
 
     def __init__(self, *args, **kwargs):
         self.layout = kwargs.get("layout", "")
-        self.wrapper_class = kwargs.get("wrapper_class", WRAPPER_CLASS)
+        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", "")
         self.label_class = kwargs.get("label_class", "")
         self.show_help = kwargs.get("show_help", True)
@@ -433,7 +434,15 @@ def get_checkbox_classes(self):
 
     def get_wrapper_classes(self):
         """Return classes for wrapper."""
-        wrapper_classes = [self.wrapper_class]
+        wrapper_classes = []
+
+        if self.is_inline:
+            wrapper_classes.append(self.get_inline_field_class())
+            wrapper_classes.append(self.inline_wrapper_class)
+        else:
+            if self.is_horizontal:
+                wrapper_classes.append("row")
+            wrapper_classes.append(self.wrapper_class)
 
         if self.is_floating:
             wrapper_classes.append("form-floating")
@@ -447,13 +456,6 @@ def get_wrapper_classes(self):
         if self.field.field.required:
             wrapper_classes.append(self.required_css_class)
 
-        if self.is_inline:
-            wrapper_classes.append(self.get_inline_field_class())
-        else:
-            if self.is_horizontal:
-                wrapper_classes.append("row")
-            wrapper_classes.append("mb-3")
-
         return merge_css_classes(*wrapper_classes)
 
     def field_before_label(self):
@@ -507,8 +509,7 @@ def render(self):
             )
 
         return format_html(
-            '<{tag} class="{wrapper_classes}">{label}{field_with_errors_and_help}</{tag}>',
-            tag=WRAPPER_TAG,
+            '<div class="{wrapper_classes}">{label}{field_with_errors_and_help}</div>',
             wrapper_classes=self.get_wrapper_classes(),
             label=label,
             field_with_errors_and_help=field_with_errors_and_help,


Test patch:
diff --git a/tests/test_bootstrap_field_parameters.py b/tests/test_bootstrap_field_parameters.py
new file mode 100644
--- /dev/null
+++ b/tests/test_bootstrap_field_parameters.py
@@ -0,0 +1,93 @@
+from django import forms
+
+from tests.base import BootstrapTestCase
+
+
+class CharFieldTestForm(forms.Form):
+    test = forms.CharField()
+
+
+class BootstrapFieldParameterTestCase(BootstrapTestCase):
+    """Test `bootstrap_field` parameters`."""
+
+    def test_wrapper_class(self):
+        """Test field with default CharField widget."""
+
+        form = CharFieldTestForm()
+
+        self.assertHTMLEqual(
+            self.render("{% bootstrap_field form.test %}", context={"form": form}),
+            (
+                '<div class="django_bootstrap5-req mb-3">'
+                '<label for="id_test" class="form-label">Test</label>'
+                '<input class="form-control" id="id_test" name="test" placeholder="Test" required type="text">'
+                "</div>"
+            ),
+        )
+
+        self.assertHTMLEqual(
+            self.render("{% bootstrap_field form.test inline_wrapper_class='foo' %}", context={"form": form}),
+            (
+                '<div class="django_bootstrap5-req mb-3">'
+                '<label for="id_test" class="form-label">Test</label>'
+                '<input class="form-control" id="id_test" name="test" placeholder="Test" required type="text">'
+                "</div>"
+            ),
+        )
+
+        self.assertHTMLEqual(
+            self.render("{% bootstrap_field form.test wrapper_class='foo' %}", context={"form": form}),
+            (
+                '<div class="django_bootstrap5-req foo">'
+                '<label for="id_test" class="form-label">Test</label>'
+                '<input class="form-control" id="id_test" name="test" placeholder="Test" required type="text">'
+                "</div>"
+            ),
+        )
+
+        self.assertHTMLEqual(
+            self.render("{% bootstrap_field form.test wrapper_class=None %}", context={"form": form}),
+            (
+                '<div class="django_bootstrap5-req">'
+                '<label for="id_test" class="form-label">Test</label>'
+                '<input class="form-control" id="id_test" name="test" placeholder="Test" required type="text">'
+                "</div>"
+            ),
+        )
+
+    def test_inline_wrapper_class(self):
+        """Test field with default CharField widget."""
+
+        form = CharFieldTestForm()
+
+        self.assertHTMLEqual(
+            self.render("{% bootstrap_field form.test layout='inline' %}", context={"form": form}),
+            (
+                '<div class="col-12 django_bootstrap5-req">'
+                '<label class="visually-hidden" for="id_test">Test</label>'
+                '<input type="text" name="test" class="form-control" placeholder="Test" required id="id_test">'
+                "</div>"
+            ),
+        )
+
+        self.assertHTMLEqual(
+            self.render("{% bootstrap_field form.test layout='inline' wrapper_class='foo' %}", context={"form": form}),
+            (
+                '<div class="col-12 django_bootstrap5-req">'
+                '<label class="visually-hidden" for="id_test">Test</label>'
+                '<input type="text" name="test" class="form-control" placeholder="Test" required id="id_test">'
+                "</div>"
+            ),
+        )
+
+        self.assertHTMLEqual(
+            self.render(
+                "{% bootstrap_field form.test layout='inline' inline_wrapper_class='foo' %}", context={"form": form}
+            ),
+            (
+                '<div class="col-12 django_bootstrap5-req foo">'
+                '<label class="visually-hidden" for="id_test">Test</label>'
+                '<input type="text" name="test" class="form-control" placeholder="Test" required id="id_test">'
+                "</div>"
+            ),
+        )