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

Hyphenate attributes with certain prefixes #737

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ The ``BOOTSTRAP5`` dict variable contains these settings and defaults:
# Field class used for horizontal fields withut a label.
'horizontal_field_offset_class': 'offset-sm-2',

# HTML attributes with any of these prefixes will have underscores converted to hyphens.
"hyphenate_attribute_prefixes": ["data"],

# Set placeholder attributes to label if no placeholder is provided.
'set_placeholder': True,

Expand Down
2 changes: 1 addition & 1 deletion src/django_bootstrap5/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from django.conf import settings

BOOTSTRAP5 = {"foo": "bar"}
BOOTSTRAP5_DEFAULTS = {
"css_url": {
"url": "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
Expand Down Expand Up @@ -34,6 +33,7 @@
"field_renderers": {
"default": "django_bootstrap5.renderers.FieldRenderer",
},
"hyphenate_attribute_prefixes": ["data"],
}


Expand Down
19 changes: 19 additions & 0 deletions src/django_bootstrap5/html.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from copy import copy

from django.forms.utils import flatatt
from django.utils.html import format_html

from django_bootstrap5.core import get_bootstrap_setting
from django_bootstrap5.text import text_value
from django_bootstrap5.utils import get_url_attrs

Expand All @@ -17,8 +20,24 @@ def render_link_tag(url):
return render_tag("link", attrs=attrs, close=False)


def has_prefix(name, prefixes):
"""Return whether the name has one of the given prefixes."""
return name.startswith(tuple(f"{prefix}_" for prefix in prefixes))


def hyphenate(attr_name):
"""Return the hyphenated version of the attribute name."""
return attr_name.replace("_", "-")


def render_tag(tag, attrs=None, content=None, close=True):
"""Render an HTML tag."""
prefixes = get_bootstrap_setting("hyphenate_attribute_prefixes") or []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not specifying should result in the default value being used.

Suggested change
prefixes = get_bootstrap_setting("hyphenate_attribute_prefixes") or []
prefixes = get_bootstrap_setting("hyphenate_attribute_prefixes") or ["data"]

if attrs:
for attr_name, attr_value in copy(attrs).items():
if has_prefix(attr_name, prefixes):
attrs[hyphenate(attr_name)] = attr_value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe error out or warn if the hyphenate key already exists?

del attrs[attr_name]
attrs_string = flatatt(attrs) if attrs else ""
builder = "<{tag}{attrs}>{content}"
content_string = text_value(content)
Expand Down
53 changes: 53 additions & 0 deletions tests/test_bootstrap_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,56 @@ def test_button_type_link(self):

with self.assertRaises(ValueError):
self.render("{% bootstrap_button 'button' button_type='button' href='#' %}")

def test_button_hyphenate_attributes(self):
# No attributes to hyphenate
self.assertHTMLEqual(
self.render("{% bootstrap_button 'button' href='#' %}"),
'<a class="btn btn-primary" href="#" role="button">button</a>',
)

# Default prefix: data
self.assertHTMLEqual(
self.render("{% bootstrap_button 'button' href='#' data_foo='bar' %}"),
'<a class="btn btn-primary" href="#" role="button" data-foo="bar">button</a>',
)

# Default prefix: no hx
self.assertHTMLEqual(
self.render("{% bootstrap_button 'button' href='#' data_foo='bar' hx_xyz='abc' %}"),
'<a class="btn btn-primary" href="#" role="button" data-foo="bar" hx_xyz="abc">button</a>',
)

# Custom prefix: data and hx
with self.settings(BOOTSTRAP5={"hyphenate_attribute_prefixes": ["data", "hx"]}):
self.assertHTMLEqual(
self.render("{% bootstrap_button 'button' href='#' data_foo='bar' hx_xyz='abc' %}"),
'<a class="btn btn-primary" href="#" role="button" data-foo="bar" hx-xyz="abc">button</a>',
)

# Custom prefix: hx only
with self.settings(BOOTSTRAP5={"hyphenate_attribute_prefixes": ["hx"]}):
self.assertHTMLEqual(
self.render("{% bootstrap_button 'button' href='#' data_foo='bar' hx_xyz='abc' %}"),
'<a class="btn btn-primary" href="#" role="button" data_foo="bar" hx-xyz="abc">button</a>',
)

# Custom prefix: empty list
with self.settings(BOOTSTRAP5={"hyphenate_attribute_prefixes": []}):
self.assertHTMLEqual(
self.render("{% bootstrap_button 'button' href='#' data_foo='bar' hx_xyz='abc' %}"),
'<a class="btn btn-primary" href="#" role="button" data_foo="bar" hx_xyz="abc">button</a>',
)

# Custom prefix: None
with self.settings(BOOTSTRAP5={"hyphenate_attribute_prefixes": None}):
self.assertHTMLEqual(
self.render("{% bootstrap_button 'button' href='#' data_foo='bar' hx_xyz='abc' %}"),
'<a class="btn btn-primary" href="#" role="button" data_foo="bar" hx_xyz="abc">button</a>',
)

# Edge case: attribute name starts with prefix but prefix is not followed by underscore
self.assertHTMLEqual(
self.render("{% bootstrap_button 'button' href='#' databar_foo='bar' %}"),
'<a class="btn btn-primary" href="#" role="button" databar_foo="bar">button</a>',
)
2 changes: 0 additions & 2 deletions tests/test_bootstrap_field_radio_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class DisabledSelectTestForm(forms.Form):


class RadioSelectWithDisabledOptions(forms.RadioSelect):

def __init__(self, attrs=None, choices=(), *, disabled_values=()):
super().__init__(attrs)
self.choices = choices
Expand All @@ -48,7 +47,6 @@ class DisabledOptionSelectTestForm(forms.Form):
)



class BootstrapFieldSelectTestCase(BootstrapTestCase):
def test_select(self):
"""Test field with select widget."""
Expand Down
3 changes: 0 additions & 3 deletions tests/test_bootstrap_field_radio_select_button_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class DisabledSelectTestForm(forms.Form):


class RadioSelectButtonGroupWithDisabledOptions(RadioSelectButtonGroup):

def __init__(self, attrs=None, choices=(), *, disabled_values=()):
super().__init__(attrs)
self.choices = choices
Expand All @@ -50,8 +49,6 @@ class DisabledOptionSelectTestForm(forms.Form):
)




class BootstrapFieldSelectTestCase(BootstrapTestCase):
def test_select(self):
"""Test field with select widget."""
Expand Down
Loading
Loading