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()