diff --git a/docs/source/dom_id.md b/docs/source/dom_helper.md
similarity index 60%
rename from docs/source/dom_id.md
rename to docs/source/dom_helper.md
index 5955e15..18e75a5 100644
--- a/docs/source/dom_id.md
+++ b/docs/source/dom_helper.md
@@ -1,4 +1,6 @@
-# dom_id
+# DOM Helper
+
+## dom_id
`dom_id` is a helper method that returns a unique DOM ID based on the object's class name and ID
@@ -22,3 +24,17 @@ from turbo_helper import dom_id
target = dom_id(instance, "detail_container")
```
+
+## class_names
+
+Inspired by JS [classnames](https://www.npmjs.com/package/classnames) and Rails `class_names`
+
+`class_names` can help conditionally render css classes
+
+```javascript
+
+
+''
+```
+
+It can also work well with TailwindCSS's some special css char such as `/` and `:`
diff --git a/docs/source/index.rst b/docs/source/index.rst
index bb88707..e9b5d31 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -18,7 +18,7 @@ Topics
install.md
form-submission.md
- dom_id.md
+ dom_helper.md
turbo_frame.md
turbo_stream.md
real-time-updates.md
diff --git a/src/turbo_helper/templatetags/turbo_helper.py b/src/turbo_helper/templatetags/turbo_helper.py
index be239c8..0423ad9 100644
--- a/src/turbo_helper/templatetags/turbo_helper.py
+++ b/src/turbo_helper/templatetags/turbo_helper.py
@@ -1,3 +1,4 @@
+import re
from typing import Any, Optional
from django import template
@@ -60,6 +61,87 @@ def dom_id(instance: Any, prefix: Optional[str] = "") -> str:
return identifier
+ATTRIBUTE_RE = re.compile(
+ r"""
+ (?P
+ [@\w:_\.\/-]+
+ )
+ (?P
+ \+?=
+ )
+ (?P
+ ['"]? # start quote
+ [^"']*
+ ['"]? # end quote
+ )
+""",
+ re.VERBOSE | re.UNICODE,
+)
+
+
+VALUE_RE = re.compile(
+ r"""
+ ['"] # start quote (required)
+ (?P
+ [^"']* # match any character except quotes
+ )
+ ['"] # end quote (required)
+ """,
+ re.VERBOSE | re.UNICODE,
+)
+
+
+@register.tag
+def class_names(parser, token):
+ error_msg = f"{token.split_contents()[0]!r} tag requires " "a list of css classes"
+ try:
+ bits = token.split_contents()
+ tag_name = bits[0] # noqa
+ attr_list = bits[1:]
+ except ValueError as exc:
+ raise TemplateSyntaxError(error_msg) from exc
+
+ css_ls = []
+ css_dict = {}
+ for pair in attr_list:
+ attribute_match = ATTRIBUTE_RE.match(pair) or VALUE_RE.match(pair)
+
+ if attribute_match:
+ dct = attribute_match.groupdict()
+ attr = dct.get("attr", None)
+ # sign = dct.get("sign", None)
+ value = parser.compile_filter(dct["value"])
+ if attr:
+ css_dict[attr] = value
+ else:
+ css_ls.append(value)
+ else:
+ raise TemplateSyntaxError("class_names found supported token: " + f"{pair}")
+
+ return ClassNamesNode(css_ls=css_ls, css_dict=css_dict)
+
+
+class ClassNamesNode(Node):
+ def __init__(self, css_ls, css_dict):
+ self.css_ls = css_ls
+ self.css_dict = css_dict
+
+ def render(self, context):
+ final_css = []
+
+ # for common css classes
+ for value in self.css_ls:
+ final_css.append(value.token)
+
+ # for conditionals
+ for attr, expression in self.css_dict.items():
+ real_value = expression.resolve(context)
+ if real_value:
+ final_css.append(attr)
+
+ return " ".join(final_css)
+
+
class TurboFrameTagNode(Node):
def __init__(self, frame_id, nodelist, extra_context=None):
self.frame_id = frame_id
diff --git a/tests/test_tags.py b/tests/test_tags.py
index 447912c..4ead006 100644
--- a/tests/test_tags.py
+++ b/tests/test_tags.py
@@ -2,6 +2,7 @@
from django.template import Context, Template
from tests.testapp.models import TodoItem
+from tests.utils import assert_dom_equal
from turbo_helper.templatetags.turbo_helper import dom_id
pytestmark = pytest.mark.django_db
@@ -32,6 +33,44 @@ def test_prefix(self, todo):
result = dom_id(todo, "test")
assert "test_todoitem_1" == result
+ def test_value_override(self):
+ template = """
+ {% load turbo_helper %}
+
+ {% dom_id first as dom_id %}
+
+
+ {% dom_id second as dom_id %}
+
+
+
+ """
+ output = render(
+ template,
+ {
+ "first": "first",
+ "second": "second",
+ },
+ ).strip()
+ assert_dom_equal(
+ output,
+ ' ',
+ )
+
+
+class TestClassNames:
+ def test_logic(self):
+ template = """
+ {% load turbo_helper %}
+
+
+ """
+ output = render(template, {}).strip()
+ assert_dom_equal(
+ output,
+ '',
+ )
+
class TestFrame:
def test_string(self):
diff --git a/tests/utils.py b/tests/utils.py
index 9c62038..168ffd8 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,10 +1,24 @@
from bs4 import BeautifulSoup
+def normalize_classes(soup):
+ """Normalize the order of CSS classes in the BeautifulSoup object."""
+ for tag in soup.find_all(class_=True):
+ classes = tag.get("class", [])
+ sorted_classes = sorted(classes)
+ tag["class"] = " ".join(sorted_classes)
+ return soup
+
+
def assert_dom_equal(expected_html, actual_html):
+ """Assert that two HTML strings are equal, ignoring differences in class order."""
expected_soup = BeautifulSoup(expected_html, "html.parser")
actual_soup = BeautifulSoup(actual_html, "html.parser")
+ # Normalize the class attribute order
+ expected_soup = normalize_classes(expected_soup)
+ actual_soup = normalize_classes(actual_soup)
+
expected_str = expected_soup.prettify()
actual_str = actual_soup.prettify()