Skip to content

Commit

Permalink
Bootstrap4 radio & checkbox (#897)
Browse files Browse the repository at this point in the history
* Added BS4 is-valid class on validation for checkbox, radio, etc.

* Make additional customization optional, applying it to BS4 customization of radio and checkbox

* Update radio error message layout
Changed error message for inline radios. This follows
the style for checkbox

* Fixing copy/paste error

* fixing inline radio rendering when use_custom_control = False

* explaining the presence of the input
  • Loading branch information
smithdc1 authored and carltongibson committed Oct 4, 2019
1 parent 5b2a93c commit 109eecf
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 26 deletions.
2 changes: 2 additions & 0 deletions crispy_forms/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ def helper(self):
template = None
field_template = None
disable_csrf = False
use_custom_control = True
label_class = ''
field_class = ''
include_media = True
Expand Down Expand Up @@ -364,6 +365,7 @@ def get_attributes(self, template_pack=TEMPLATE_PACK):
'html5_required': self.html5_required,
'form_show_labels': self.form_show_labels,
'disable_csrf': self.disable_csrf,
'use_custom_control': self.use_custom_control,
'label_class': self.label_class,
'field_class': self.field_class,
'include_media': self.include_media
Expand Down
8 changes: 6 additions & 2 deletions crispy_forms/templates/bootstrap4/field.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<div class="{% for offset in bootstrap_checkbox_offsets %}{{ offset }} {% endfor %}{{ field_class }}">
{% endif %}
{% endif %}
<{% if tag %}{{ tag }}{% else %}div{% endif %} id="div_{{ field.auto_id }}" class="{% if not field|is_checkbox %}form-group{% if 'form-horizontal' in form_class %} row{% endif %}{% else %}form-check{% endif %}{% if wrapper_class %} {{ wrapper_class }}{% endif %}{% if field.css_classes %} {{ field.css_classes }}{% endif %}">
<{% if tag %}{{ tag }}{% else %}div{% endif %} id="div_{{ field.auto_id }}" class="{% if not field|is_checkbox %}form-group{% if 'form-horizontal' in form_class %} row{% endif %}{% else %}{%if use_custom_control%}custom-control custom-checkbox{% else %}form-check{% endif %}{% endif %}{% if wrapper_class %} {{ wrapper_class }}{% endif %}{% if field.css_classes %} {{ field.css_classes }}{% endif %}">
{% if field.label and not field|is_checkbox and form_show_labels %}
<label for="{{ field.id_for_label }}" class="{% if 'form-horizontal' in form_class %}col-form-label {% endif %}{{ label_class }}{% if field.field.required %} requiredField{% endif %}">
{{ field.label|safe }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}
Expand All @@ -26,8 +26,12 @@

{% if not field|is_checkboxselectmultiple and not field|is_radioselect %}
{% if field|is_checkbox and form_show_labels %}
<label for="{{ field.id_for_label }}" class="form-check-label{% if field.field.required %} requiredField{% endif %}">
{%if use_custom_control%}
{% crispy_field field 'class' 'custom-control-input' %}
{% else %}
{% crispy_field field 'class' 'form-check-input' %}
{% endif %}
<label for="{{ field.id_for_label }}" class="{%if use_custom_control%}custom-control-label{% else %}form-check-label{% endif %}{% if field.field.required %} requiredField{% endif %}">
{{ field.label|safe }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}
</label>
{% include 'bootstrap4/layout/help_text_and_errors.html' %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
{% load crispy_forms_filters %}
{% load l10n %}

<div class="{% if inline_class %}form-check{% endif %}{% if field_class %} {{ field_class }}{% endif %}"{% if flat_attrs %} {{ flat_attrs|safe }}{% endif %}>
<div class="{% if field_class %} {{ field_class }}{% endif %}"{% if flat_attrs %} {{ flat_attrs|safe }}{% endif %}>

{% for choice in field.field.choices %}
{% if not inline_class %}<div class="form-check">{% endif %}
<input type="checkbox" class="form-check-input{%if is_bound %} is-{% if field.errors %}in{%endif%}valid{% endif %}"{% if choice.0 in field.value or choice.0|stringformat:"s" in field.value or choice.0|stringformat:"s" == field.value|default_if_none:""|stringformat:"s" %} checked="checked"{% endif %} name="{{ field.html_name }}" id="id_{{ field.html_name }}_{{ forloop.counter }}" value="{{ choice.0|unlocalize }}" {{ field.field.widget.attrs|flatatt }}>
<label id="id_{{ field.id_for_label }}_{{ forloop.counter }}" class="form-check-{% if inline_class %}{{ inline_class }}{% else %}label{% endif %}" for="id_{{ field.html_name }}_{{ forloop.counter }}">
<div class="{%if use_custom_control%}custom-control custom-checkbox{% if inline_class %} custom-control-inline{% endif %}{% else %}form-check{% if inline_class %} form-check-inline{% endif %}{% endif %}">
<input type="checkbox" class="{%if use_custom_control%}custom-control-input{% else %}form-check-input{% endif %}{%if is_bound %} is-{% if field.errors %}in{%endif%}valid{% endif %}"{% if choice.0 in field.value or choice.0|stringformat:"s" in field.value or choice.0|stringformat:"s" == field.value|default_if_none:""|stringformat:"s" %} checked="checked"{% endif %} name="{{ field.html_name }}" id="id_{{ field.html_name }}_{{ forloop.counter }}" value="{{ choice.0|unlocalize }}" {{ field.field.widget.attrs|flatatt }}>
<label id="id_{{ field.id_for_label }}_{{ forloop.counter }}" class="{%if use_custom_control%}custom-control-label{% else %}form-check-label{% endif %}" for="id_{{ field.html_name }}_{{ forloop.counter }}">
{{ choice.1|unlocalize }}
</label>
{% if field.errors and forloop.last %}
{% if field.errors and forloop.last and not inline_class %}
{% include 'bootstrap4/layout/field_errors_block.html' %}
{% endif %}
{% if not inline_class %}</div>{% endif %}
{% endfor %}
{% endif %}
</div>
{% endfor %}
{% if field.errors and inline_class %}
<div class="w-100 {%if use_custom_control%}custom-control custom-checkbox{% if inline_class %} custom-control-inline{% endif %}{% else %}form-check{% if inline_class %} form-check-inline{% endif %}{% endif %}">
{# the following input is only meant to allow boostrap to render the error message as it has to be after an invalid input. As the input has no name, no data will be sent. #}
<input type="checkbox" class="custom-control-input {% if field.errors %}is-invalid{%endif%}">
{% include 'bootstrap4/layout/field_errors_block.html' %}
</div>
{% endif %}

{% include 'bootstrap4/layout/help_text.html' %}
</div>
20 changes: 14 additions & 6 deletions crispy_forms/templates/bootstrap4/layout/radioselect.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
{% load crispy_forms_filters %}
{% load l10n %}

<div class="{% if inline_class %}form-check{% endif %}{% if field_class %} {{ field_class }}{% endif %}"{% if flat_attrs %} {{ flat_attrs|safe }}{% endif %}>
<div class="{% if field_class %} {{ field_class }}{% endif %}"{% if flat_attrs %} {{ flat_attrs|safe }}{% endif %}>

{% for choice in field.field.choices %}
{% if not inline_class %}<div class="form-check">{% endif %}
<input type="radio" class="form-check-input{%if is_bound %} is-{% if field.errors %}in{%endif%}valid{% endif %}"{% if choice.0|stringformat:"s" == field.value|default_if_none:""|stringformat:"s" %} checked="checked"{% endif %} name="{{ field.html_name }}" id="id_{{ field.id_for_label }}_{{ forloop.counter }}" value="{{ choice.0|unlocalize }}" {{ field.field.widget.attrs|flatatt }}>
<label for="id_{{ field.html_name }}_{{ forloop.counter }}" class="form-check-{% if inline_class %}{{ inline_class }}{% else %}label{% endif %}">
<div class="{%if use_custom_control%}custom-control custom-radio{% if inline_class %} custom-control-inline{% endif %}{% else %}form-check{% if inline_class %} form-check-inline{% endif %}{% endif %}">
<input type="radio" class="{%if use_custom_control%}custom-control-input{% else %}form-check-input{% endif %}{%if is_bound %} is-{% if field.errors %}in{%endif%}valid{% endif %}"{% if choice.0|stringformat:"s" == field.value|default_if_none:""|stringformat:"s" %} checked="checked"{% endif %} name="{{ field.html_name }}" id="id_{{ field.id_for_label }}_{{ forloop.counter }}" value="{{ choice.0|unlocalize }}" {{ field.field.widget.attrs|flatatt }}>
<label for="id_{{ field.id_for_label }}_{{ forloop.counter }}" class="{%if use_custom_control%}custom-control-label{% else %}form-check-label{% endif %}">
{{ choice.1|unlocalize }}
</label>
{% if field.errors and forloop.last %}
{% if field.errors and forloop.last and not inline_class %}
{% include 'bootstrap4/layout/field_errors_block.html' %}
{% endif %}
{% if not inline_class %}</div>{% endif %}
</div>
{% endfor %}
{% if field.errors and inline_class %}
<div class="w-100 {%if use_custom_control%}custom-control custom-radio{% if inline_class %} custom-control-inline{% endif %}{% else %}form-check{% if inline_class %} form-check-inline{% endif %}{% endif %}">
{# the following input is only meant to allow boostrap to render the error message as it has to be after an invalid input. As the input has no name, no data will be sent. #}
<input type="checkbox" class="custom-control-input {% if field.errors %}is-invalid{%endif%}">
{% include 'bootstrap4/layout/field_errors_block.html' %}
</div>
{% endif %}

{% include 'bootstrap4/layout/help_text.html' %}
</div>

4 changes: 2 additions & 2 deletions crispy_forms/templatetags/crispy_forms_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ def render(self, context):

if (
template_pack == 'bootstrap4'
and not is_checkbox(field)
and not is_multivalue(field)
):
css_class += ' form-control'
if not is_checkbox(field):
css_class += ' form-control'
if field.errors:
css_class += ' is-invalid'

Expand Down
6 changes: 4 additions & 2 deletions crispy_forms/tests/test_form_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ def test_inputs(settings):
assert 'class="btn"' in html
assert 'btn btn-primary' in html
assert 'btn btn-inverse' in html
assert len(re.findall(r'<input[^>]+> <', html)) == 8

if settings.CRISPY_TEMPLATE_PACK == 'bootstrap4':
assert len(re.findall(r'<input[^>]+> <', html)) == 9
else:
assert len(re.findall(r'<input[^>]+> <', html)) == 8

def test_invalid_form_method():
form_helper = FormHelper()
Expand Down
46 changes: 45 additions & 1 deletion crispy_forms/tests/test_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,51 @@ def test_keepcontext_context_manager(settings):
elif settings.CRISPY_TEMPLATE_PACK == 'bootstrap3':
assert response.content.count(b'checkbox-inline') == 3
elif settings.CRISPY_TEMPLATE_PACK == 'bootstrap4':
assert response.content.count(b'form-check-inline') == 3
assert response.content.count(b'custom-control-inline') == 3
assert response.content.count(b'custom-checkbox') > 0


@only_bootstrap4
def test_use_custom_control_is_used():
form = CheckboxesSampleForm()
form.helper = FormHelper()
form.helper.layout = Layout(
'checkboxes',
InlineCheckboxes('alphacheckboxes'),
'numeric_multiple_checkboxes'
)
# form.helper.use_custom_control take default value which is True

response = render(
request=None,
template_name='crispy_render_template.html',
context={'form': form}
)
assert response.content.count(b'custom-control-inline') == 3
assert response.content.count(b'custom-checkbox') == 9

form.helper.use_custom_control = True

response = render(
request=None,
template_name='crispy_render_template.html',
context={'form': form}
)
assert response.content.count(b'custom-control-inline') == 3
assert response.content.count(b'custom-checkbox') == 9

form.helper.use_custom_control = False

response = render(
request=None,
template_name='crispy_render_template.html',
context={'form': form}
)

assert response.content.count(b'custom-control-inline') == 0
assert response.content.count(b'form-check-inline') == 3
assert response.content.count(b'form-check') > 0
assert response.content.count(b'custom-checkbox') == 0


@only_bootstrap3
Expand Down
10 changes: 5 additions & 5 deletions crispy_forms/tests/test_layout_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ class CustomCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
form.helper = FormHelper()
form.helper.layout = Layout('inline_radios')

html = render_crispy_form(form)
html = render_crispy_form(form)
if settings.CRISPY_TEMPLATE_PACK == 'bootstrap4':
assert 'class="form-check"' in html
assert 'class="custom-control-input"' in html
else:
assert 'class="radio"' in html

Expand All @@ -161,7 +161,7 @@ class CustomCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
form.helper.layout = Layout('checkboxes')
html = render_crispy_form(form)
if settings.CRISPY_TEMPLATE_PACK == 'bootstrap4':
assert 'class="form-check"' in html
assert 'class="custom-control-input"' in html
else:
assert 'class="checkbox"' in html

Expand Down Expand Up @@ -225,7 +225,7 @@ def test_inline_radios(self, settings):
elif settings.CRISPY_TEMPLATE_PACK == 'bootstrap3':
assert html.count('radio-inline"') == 2
elif settings.CRISPY_TEMPLATE_PACK == 'bootstrap4':
assert html.count('form-check-inline"') == 2
assert html.count('custom-control-inline"') == 2

def test_accordion_and_accordiongroup(self, settings):
test_form = SampleForm()
Expand Down Expand Up @@ -487,7 +487,7 @@ def test_multiplecheckboxes(self, settings):
if settings.CRISPY_TEMPLATE_PACK == 'bootstrap3':
assert html.count('checkbox-inline"') == 3
elif settings.CRISPY_TEMPLATE_PACK == 'bootstrap4':
assert html.count('form-check-inline"') == 3
assert html.count('custom-control-inline"') == 3

def test_multiple_checkboxes_unique_ids(self):
test_form = CheckboxesSampleForm()
Expand Down
3 changes: 3 additions & 0 deletions docs/form_helper.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ Helper attributes you can set
**disable_csrf = False**
Disable CSRF token, when done, crispy-forms won't use ``{% csrf_token %}`` tag. This is useful when rendering several forms using ``{% crispy %}`` tag and ``form_tag = False`` csrf_token gets rendered several times.

**use_custom_control = True**
It indicate whether the radio and checkbox button should use the optional UI customization of the template pack or not. Useful when you already have customization based on the default interpretation of the template pack. When enabled crispy-forms will render elements such as checkbox and radio with optional additional UI customization, when available. Defaults to ``True``.

**form_error_title**
If you are rendering a form using ``{% crispy %}`` tag and it has ``non_field_errors`` to display, they are rendered in a div. You can set the title of the div with this attribute. Example: “Form Errors”.

Expand Down

0 comments on commit 109eecf

Please sign in to comment.