Skip to content

Commit

Permalink
Add support for horizontal forms (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
dyve committed Apr 18, 2021
1 parent 49057cb commit c539819
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [0.3.0] - In development

- Add support for horizontal forms.
- Add support for `checkbox_type="switch"`.
- Set PyPI Development Status to 4 - Beta.
- Remove use_i18n setting because it duplicates standard Django functionality.
Expand Down
15 changes: 15 additions & 0 deletions docs/forms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,18 @@ Floating labels are supported for widgets that can use `form-control`, with the

The `Select` widget is supported by Bootstrap 5 (with restrictions), but not yet supported by this package.


Horizontal forms
----------------

Reference: https://getbootstrap.com/docs/5.0/forms/layout/#horizontal-form

This behavior can be triggered by setting `layout="horizontal"`.

In a horizontal layout, labels and field will receive different styling, resulting in a horizontal layout on supported viewports.

These parameters contain the classes for labels and fields:

- `horizontal_label_class` The class for the label
- `horizontal_field_class` The class for the section with field, help text and errors
- `horizontal_field_offset_class` The offset for fields that have no label, or that use the label as part of their field function (such as checkbox)
5 changes: 3 additions & 2 deletions src/django_bootstrap5/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
},
"theme_url": None,
"javascript_in_head": False,
"horizontal_label_class": "col-md-3",
"horizontal_field_class": "col-md-9",
"horizontal_label_class": "col-sm-2",
"horizontal_field_class": "col-sm-10",
"horizontal_field_offset_class": "offset-sm-2",
"set_placeholder": True,
"checkbox_layout": None,
"checkbox_style": None,
Expand Down
68 changes: 45 additions & 23 deletions src/django_bootstrap5/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def __init__(self, *args, **kwargs):
)
self.checkbox_layout = kwargs.get("checkbox_layout", get_bootstrap_setting("checkbox_layout"))
self.checkbox_style = kwargs.get("checkbox_style", get_bootstrap_setting("checkbox_style"))
self.horizontal_field_offset_class = kwargs.get(
"horizontal_field_offset_class", get_bootstrap_setting("horizontal_field_offset_class")
)
self.inline_field_class = kwargs.get("inline_field_class", get_bootstrap_setting("inline_field_class"))
self.error_css_class = kwargs.get("error_css_class", None)
self.required_css_class = kwargs.get("required_css_class", None)
Expand All @@ -55,6 +58,11 @@ def is_floating(self):
"""Return whether to render `form-control` widgets as floating."""
return self.layout == "floating"

@property
def is_horizontal(self):
"""Return whether to render form horizontally."""
return self.layout == "horizontal"

@property
def is_inline(self):
"""Return whether to render widgets with inline layout."""
Expand Down Expand Up @@ -215,17 +223,7 @@ def __init__(self, field, *args, **kwargs):
self.help_text = text_value(field.help_text) if self.show_help and field.help_text else ""
self.field_errors = [conditional_escape(text_value(error)) for error in field.errors]

if "placeholder" in kwargs:
# Find the placeholder in kwargs, even if it's empty
self.placeholder = kwargs["placeholder"]
elif get_bootstrap_setting("set_placeholder"):
# If not found, see if we set the label
self.placeholder = field.label
else:
# Or just set it to empty
self.placeholder = ""
if self.placeholder:
self.placeholder = text_value(self.placeholder)
self.placeholder = text_value(kwargs.get("placeholder", self.default_placeholder))

self.addon_before = kwargs.get("addon_before", self.widget.attrs.pop("addon_before", ""))
self.addon_after = kwargs.get("addon_after", self.widget.attrs.pop("addon_after", ""))
Expand Down Expand Up @@ -264,6 +262,11 @@ def __init__(self, field, *args, **kwargs):
def is_floating(self):
return super().is_floating and self.can_widget_float(self.widget) and self.is_widget_form_control(self.widget)

@property
def default_placeholder(self):
"""Return default placeholder for field."""
return self.field.label if get_bootstrap_setting("set_placeholder") else ""

def restore_widget_attrs(self):
self.widget.attrs = self.initial_attrs.copy()

Expand Down Expand Up @@ -341,7 +344,7 @@ def add_widget_attrs(self):
elif isinstance(widget, ClearableFileInput):
widget.template_name = "django_bootstrap5/widgets/clearable_file_input.html"

def get_label_class(self):
def get_label_class(self, horizontal=False):
"""Return CSS class for label."""
label_classes = [text_value(self.label_class)]
if not self.show_label:
Expand All @@ -351,6 +354,8 @@ def get_label_class(self):
widget_label_class = "form-check-label"
elif self.is_inline:
widget_label_class = "visually-hidden"
elif horizontal:
widget_label_class = merge_css_classes(self.horizontal_label_class, "col-form-label")
else:
widget_label_class = "form-label"
label_classes = [widget_label_class] + label_classes
Expand All @@ -363,11 +368,15 @@ def get_field_html(self):
self.restore_widget_attrs()
return field_html

def get_label_html(self):
def get_label_html(self, horizontal=False):
"""Return value for label."""
label_html = "" if self.show_label == "skip" else self.field.label
if label_html:
label_html = render_label(label_html, label_for=self.field.id_for_label, label_class=self.get_label_class())
label_html = render_label(
label_html,
label_for=self.field.id_for_label,
label_class=self.get_label_class(horizontal=horizontal),
)
return label_html

def get_help_html(self):
Expand Down Expand Up @@ -427,6 +436,8 @@ def get_wrapper_classes(self):
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)

Expand All @@ -441,21 +452,32 @@ def render(self):
return text_value(self.field)

field = self.get_field_html()
label = self.get_label_html()
field_with_label = field + label if self.field_before_label() else label + field
if self.field_before_label():
label = self.get_label_html()
field = field + label
label = mark_safe("")
horizontal_class = merge_css_classes(self.horizontal_field_class, self.horizontal_field_offset_class)
else:
label = self.get_label_html(horizontal=self.is_horizontal)
horizontal_class = self.horizontal_field_class

if isinstance(self.widget, CheckboxInput):
field_with_label = format_html(
'<div class="{form_check_class}">{field_with_label}</div>',
field = format_html(
'<div class="{form_check_class}">{field}</div>',
form_check_class=self.get_checkbox_classes(),
field_with_label=field_with_label,
field=field,
)

field_with_help_and_errors = format_html("{}{}{}", field, self.get_help_html(), self.get_errors_html())
if self.is_horizontal:
field_with_help_and_errors = format_html(
'<div class="{}">{}</div>', horizontal_class, field_with_help_and_errors
)

return format_html(
'<{tag} class="{wrapper_classes}">{field_with_label}{help}{errors}</{tag}>',
'<{tag} class="{wrapper_classes}">{label}{field_with_help_and_errors}</{tag}>',
tag=WRAPPER_TAG,
wrapper_classes=self.get_wrapper_classes(),
field_with_label=field_with_label,
help=self.get_help_html(),
errors=self.get_errors_html(),
label=label,
field_with_help_and_errors=field_with_help_and_errors,
)
16 changes: 8 additions & 8 deletions tests/test_bootstrap_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,19 @@ def test_bootstrap_field_text_floating(self):
"""Test field with text widget in floating layout."""

class TestForm(forms.Form):
test = forms.CharField()
test = forms.BooleanField()

test_form = TestForm()
html = render_template_with_bootstrap(
"{% bootstrap_field form.test layout='floating' %}", context={"form": test_form}
)
html = render_template_with_bootstrap("{% bootstrap_field form.test %}", context={"form": test_form})
self.assertHTMLEqual(
html,
(
'<div class="django_bootstrap5-req mb-3 form-floating">'
'<input class="form-control" id="id_test" name="test" placeholder="Test" required type="text">'
'<label for="id_test" class="form-label">Test</label>'
"</div>"
'<div class="django_bootstrap5-req mb-3">'
'<div class="form-check">'
'<input class="form-check-input" id="id_test" name="test" required type="checkbox">'
'<label class="form-check-label" for="id_test">'
"Test"
"</label>"
),
)

Expand Down
26 changes: 26 additions & 0 deletions tests/test_bootstrap_field_floating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django import forms
from django.test import TestCase

from .base import render_template_with_bootstrap


class BootstrapFieldTest(TestCase):
def test_bootstrap_field_text_floating(self):
"""Test field with text widget in floating layout."""

class TestForm(forms.Form):
test = forms.CharField()

test_form = TestForm()
html = render_template_with_bootstrap(
"{% bootstrap_field form.test layout='floating' %}", context={"form": test_form}
)
self.assertHTMLEqual(
html,
(
'<div class="django_bootstrap5-req mb-3 form-floating">'
'<input class="form-control" id="id_test" name="test" placeholder="Test" required type="text">'
'<label for="id_test" class="form-label">Test</label>'
"</div>"
),
)
53 changes: 53 additions & 0 deletions tests/test_bootstrap_field_horizontal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from django import forms
from django.test import TestCase

from .base import render_template_with_bootstrap


class BootstrapHorizontalFieldTest(TestCase):
def test_bootstrap_field_text_horizontal(self):
"""Test field with text widget."""

class TestForm(forms.Form):
test = forms.CharField()

test_form = TestForm()
html = render_template_with_bootstrap(
"{% bootstrap_field form.test layout='horizontal' %}", context={"form": test_form}
)
self.assertHTMLEqual(
html,
(
'<div class="django_bootstrap5-req mb-3 row">'
'<label class="col-form-label col-sm-2" for="id_test">'
"Test"
'</label><div class="col-sm-10">'
'<input class="form-control" id="id_test" name="test" placeholder="Test" required type="text">'
"</div>"
"</div>"
),
)

def test_bootstrap_field_checkbox_horizontal(self):
"""Test field with text widget."""

class TestForm(forms.Form):
test = forms.BooleanField()

test_form = TestForm()
html = render_template_with_bootstrap(
"{% bootstrap_field form.test layout='horizontal' %}", context={"form": test_form}
)
self.assertHTMLEqual(
html,
(
'<div class="django_bootstrap5-req mb-3 row">'
'<div class="col-sm-10 offset-sm-2">'
'<div class="form-check">'
'<input class="form-check-input" id="id_test" name="test" required type="checkbox">'
'<label class="form-check-label" for="id_test">'
"Test"
"</label>"
"</div>"
),
)

0 comments on commit c539819

Please sign in to comment.