Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bootstrap4 radio & checkbox #897

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,25 @@
{% 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 %}">
<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>
19 changes: 13 additions & 6 deletions crispy_forms/templates/bootstrap4/layout/radioselect.html
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="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 %}">
<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