Repository: django-crispy-forms/django-crispy-forms

Issue:
checkboxes inside a MultiWidget have class "form-control" added (bootstrap3 only)
I've hit a problem with rendering of MultiValueField with a MultiWidget.  The class `form-control` is added to every field, and this causes the rendering to go awry.  So I'd like to prevent it, but I've found the code that adds this class and it's not immediately obvious how to improve it.

The code is in `crispy_forms_field.py` in the `render()` method of
the `CrispyFieldNode` class - [link](https://github.com/maraujop/django-crispy-forms/blob/dev/crispy_forms/templatetags/crispy_forms_field.py#L122)

``` python
        if (
            template_pack == 'bootstrap3'
            and not is_checkbox(field)
            and not is_file(field)
        ):
            css_class += ' form-control'

        widget.attrs['class'] = css_class
```

The field has a number of widgets inside it, so it may be appropriate to
add the `form-control` class to some, but not to all.  But the code
above applies it to all or none.

I've tried using [layout.MultiWidgetField](https://django-crispy-forms.readthedocs.org/en/latest/api_layout.html#layout.MultiWidgetField)
but that just adds CSS classes - it doesn't override the classes already
added by crispy.

One workaround I have found is to set the class of the field in the
`__init__` method of the form:

``` python
    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)
        self.fields['my_multifield'].widget.attrs['class'] = 'my-new-class'
```

That replaces the classes set by crispy.  But it's not a very elegant
workaround.

I guess the least invasive change would be to add a function
`is_multivaluefield()` and change the if statement above to:

``` python
        if (
            template_pack == 'bootstrap3'
            and not is_checkbox(field)
            and not is_file(field)
            and not is_multivaluefield(field)
        ):
            css_class += ' form-control'
```

Then you have full control of any special rendering at the cost of
always having to manually add the classes yourself using
layout.MultiValueField or your own custom code.

An alternative could be to detect the multivalue field, and then have a
separate loop that goes through each widget of the multivalue field and
applies the above code to that.

I'll work on a pull request for the simple change, but please let me
know on this issue if you see a better way of doing this.



Discussion:

Resolving patch:
diff --git a/crispy_forms/templatetags/crispy_forms_field.py b/crispy_forms/templatetags/crispy_forms_field.py
--- a/crispy_forms/templatetags/crispy_forms_field.py
+++ b/crispy_forms/templatetags/crispy_forms_field.py
@@ -42,6 +42,11 @@ def is_file(field):
     return isinstance(field.field.widget, forms.ClearableFileInput)
 
 
+@register.filter
+def is_multivalue(field):
+    return isinstance(field.field.widget, forms.MultiWidget)
+
+
 @register.filter
 def classes(field):
     """
@@ -116,6 +121,7 @@ def render(self, context):
                 template_pack in ['bootstrap3', 'bootstrap4']
                 and not is_checkbox(field)
                 and not is_file(field)
+                and not is_multivalue(field)
             ):
                 css_class += ' form-control'
                 if field.errors:


Test patch:
diff --git a/crispy_forms/tests/forms.py b/crispy_forms/tests/forms.py
--- a/crispy_forms/tests/forms.py
+++ b/crispy_forms/tests/forms.py
@@ -119,3 +119,7 @@ class SampleFormWithMedia(forms.Form):
     class Media:
         css = {'all': ('test.css',)}
         js = ('test.js',)
+
+
+class SampleFormWithMultiValueField(forms.Form):
+    multi = forms.SplitDateTimeField()
diff --git a/crispy_forms/tests/test_form_helper.py b/crispy_forms/tests/test_form_helper.py
--- a/crispy_forms/tests/test_form_helper.py
+++ b/crispy_forms/tests/test_form_helper.py
@@ -28,7 +28,7 @@
 from .conftest import (
     only_bootstrap, only_bootstrap3, only_bootstrap4, only_uni_form,
 )
-from .forms import SampleForm, SampleFormWithMedia
+from .forms import SampleForm, SampleFormWithMedia, SampleFormWithMultiValueField
 
 try:
     from django.middleware.csrf import _get_new_csrf_key
@@ -821,3 +821,24 @@ def test_passthrough_context():
     html = render_crispy_form(form, helper=form.helper, context=c)
     assert "Got prefix: foo" in html
     assert "Got suffix: bar" in html
+
+
+
+
+
+@only_bootstrap3
+def test_bootstrap3_does_add_form_control_class_to_non_multivaluefield():
+    form = SampleForm()
+    form.helper = FormHelper()
+    form.helper.template_pack = 'bootstrap3'
+    html = render_crispy_form(form)
+    assert 'form-control' in html
+
+
+@only_bootstrap3
+def test_bootstrap3_does_not_add_form_control_class_to_multivaluefield():
+    form = SampleFormWithMultiValueField()
+    form.helper = FormHelper()
+    form.helper.template_pack = 'bootstrap3'
+    html = render_crispy_form(form)
+    assert 'form-control' not in html