Skip to content

Commit

Permalink
Merge pull request #41 from wrabit/hyphen_to_underscores_in_attribute…
Browse files Browse the repository at this point in the history
…_names

converts hyphen var names to uderscores for access
  • Loading branch information
wrabit authored Jul 6, 2024
2 parents 9df953e + d0b9f0d commit 16d55ca
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 45 deletions.
14 changes: 11 additions & 3 deletions django_cotton/cotton_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ def get_template_sources(self, template_name):


class UnsortedAttributes(HTMLFormatter):
"""This keeps BS4 from re-ordering attributes"""

def attributes(self, tag):
for k, v in tag.attrs.items():
yield k, v
Expand Down Expand Up @@ -181,16 +183,22 @@ def _wrap_with_cotton_vars_frame(self, soup, cvars_el):

vars_with_defaults = []
for var, value in cvars_el.attrs.items():
# Attributes in context at this point will already have been formatted in _component to be accessible, so in order to cascade match the style.
accessible_var = var.replace("-", "_")

if value is None:
vars_with_defaults.append(f"{var}={var}")
vars_with_defaults.append(f"{var}={accessible_var}")
elif var.startswith(":"):
# If ':' is present, the user wants to parse a literal string as the default value,
# i.e. "['a', 'b']", "{'a': 'b'}", "True", "False", "None" or "1".
var = var[1:] # Remove the ':' prefix
vars_with_defaults.append(f'{var}={var}|eval_default:"{value}"')
accessible_var = accessible_var[1:] # Remove the ':' prefix
vars_with_defaults.append(
f'{var}={accessible_var}|eval_default:"{value}"'
)
else:
# Assuming value is already a string that represents the default value
vars_with_defaults.append(f'{var}={var}|default:"{value}"')
vars_with_defaults.append(f'{var}={accessible_var}|default:"{value}"')

cvars_el.decompose()

Expand Down
20 changes: 11 additions & 9 deletions django_cotton/templatetags/_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,28 @@ def render(self, context):
all_slots = context.get("cotton_slots", {})
component_slots = all_slots.get(self.component_key, {})
local_context.update(component_slots)
local_context.update(attrs)

# We need to check if any dynamic attributes are present in the component slots and move them over to attrs
if "ctn_template_expression_attrs" in component_slots:
for expression_attr in component_slots["ctn_template_expression_attrs"]:
attrs[expression_attr] = component_slots[expression_attr]

# Make the attrs available in the context for the vars frame
local_context["attrs_dict"] = attrs

# Reset the component's slots in context to prevent bleeding into sibling components
all_slots[self.component_key] = {}

# Provide all of the attrs as a string to pass to the component
local_context.update(attrs)
# Build attrs string before formatting any '-' to '_' in attr names
attrs_string = " ".join(
f"{key}={ensure_quoted(value)}" for key, value in attrs.items()
)
local_context["attrs"] = mark_safe(attrs_string)

# Make the attrs available in the context for the vars frame, also before formatting the attr names
local_context["attrs_dict"] = attrs

# Store attr names in a callable format, i.e. 'x-init' will be accessible by {{ x_init }} when called explicitly and not in {{ attrs }}
attrs = {key.replace("-", "_"): value for key, value in attrs.items()}
local_context.update(attrs)

# Reset the component's slots in context to prevent bleeding into sibling components
all_slots[self.component_key] = {}

return render_to_string(self.template_path, local_context)

def process_dynamic_attribute(self, value, context):
Expand Down
24 changes: 14 additions & 10 deletions django_cotton/templatetags/_vars_frame.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django import template
from django.template.base import token_kwargs
from django.utils.safestring import mark_safe

from django_cotton.utils import ensure_quoted
Expand All @@ -16,7 +15,11 @@ def cotton_vars_frame(parser, token):
"""
bits = token.split_contents()[1:] # Skip the tag name

tag_kwargs = token_kwargs(bits, parser)
# We dont use token_kwargs because it doesn't allow for hyphens in key names, i.e. x-data=""
tag_kwargs = {}
for bit in bits:
key, value = bit.split("=")
tag_kwargs[key] = parser.compile_filter(value)

nodelist = parser.parse(("endcotton_vars_frame",))
parser.delete_first_token()
Expand Down Expand Up @@ -45,17 +48,18 @@ def render(self, context):

# Overwrite 'attrs' in the local context by excluding keys that are identified as vars
attrs_without_vars = {k: v for k, v in component_attrs.items() if k not in vars}
context["attrs_dict"] = attrs_without_vars

# Provide all of the attrs as a string to pass to the component
# Provide all of the attrs as a string to pass to the component before any '-' to '_' replacing
attrs = " ".join(
[
f"{key}={ensure_quoted(value)}"
for key, value in attrs_without_vars.items()
]
f"{key}={ensure_quoted(value)}" for key, value in attrs_without_vars.items()
)

context["attrs"] = mark_safe(attrs)
context.update(vars)

context["attrs_dict"] = attrs_without_vars

# Store attr names in a callable format, i.e. 'x-init' will be accessible by {{ x_init }} when called explicitly and not in {{ attrs }}
formatted_vars = {key.replace("-", "_"): value for key, value in vars.items()}

context.update(formatted_vars)

return self.nodelist.render(context)
19 changes: 15 additions & 4 deletions django_cotton/tests/inline_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import shutil
import tempfile

from django.core.cache import cache
from django.urls import path
from django.test import override_settings
from django.views.generic import TemplateView
Expand All @@ -22,39 +23,49 @@ class CottonInlineTestCase(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()

# Set tmp dir and register a url module for our tmp files
cls.temp_dir = tempfile.mkdtemp()
cls.url_module = DynamicURLModule()
cls.url_module_name = f"dynamic_urls_{cls.__name__}"
sys.modules[cls.url_module_name] = cls.url_module

# Create a new TEMPLATES setting
# Register our temp directory as a TEMPLATES path
cls.new_templates_setting = settings.TEMPLATES.copy()
cls.new_templates_setting[0]["DIRS"] = [
cls.temp_dir
] + cls.new_templates_setting[0]["DIRS"]

# Apply the new setting
# Apply the setting
cls.templates_override = override_settings(TEMPLATES=cls.new_templates_setting)
cls.templates_override.enable()

@classmethod
def tearDownClass(cls):
"""Remove temporary directory and clean up modules"""
cls.templates_override.disable()
shutil.rmtree(cls.temp_dir, ignore_errors=True)
del sys.modules[cls.url_module_name]
super().tearDownClass()

def tearDown(self):
"""Clear cache between tests so that we can use the same file names for simplicity"""
cache.clear()

def create_template(self, name, content):
"""Create a template file in the temporary directory and return the path"""
path = os.path.join(self.temp_dir, name)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
f.write(content)
return path

def create_view(self, template_name):
def make_view(self, template_name):
"""Make a view that renders the given template"""
return TemplateView.as_view(template_name=template_name)

def create_url(self, url, view):
def register_url(self, url, view):
"""Register a URL pattern and returns path"""
url_pattern = path(url, view)
self.url_module.urlpatterns.append(url_pattern)
return url_pattern
Expand Down
80 changes: 61 additions & 19 deletions django_cotton/tests/test_cotton.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,16 @@ class InlineTestCase(CottonInlineTestCase):
def test_component_is_rendered(self):
self.create_template(
"cotton/component.html",
"""
<div class="i-am-component">
{{ slot }}
</div>
""",
"""<div class="i-am-component">{{ slot }}</div>""",
)

self.create_template(
"view.html",
"""
<c-component>
Hello, World!
</c-component>
""",
"""<c-component>Hello, World!</c-component>""",
)

# Create URL
self.create_url("view/", self.create_view("view.html"))
# Register Url
self.register_url("view/", self.make_view("view.html"))

# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf()):
Expand All @@ -36,11 +28,7 @@ def test_component_is_rendered(self):
def test_new_lines_in_attributes_are_preserved(self):
self.create_template(
"cotton/component.html",
"""
<div {{ attrs }}>
{{ slot }}
</div>
""",
"""<div {{ attrs }}>{{ slot }}</div>""",
)

self.create_template(
Expand All @@ -56,8 +44,8 @@ def test_new_lines_in_attributes_are_preserved(self):
""",
)

# Create URL
self.create_url("view/", self.create_view("view.html"))
# Register Url
self.register_url("view/", self.make_view("view.html"))

# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf()):
Expand All @@ -74,6 +62,60 @@ def test_new_lines_in_attributes_are_preserved(self):
in response.content.decode()
)

def test_attribute_names_on_component_containing_hyphens_are_converted_to_underscores(
self,
):
self.create_template(
"cotton/component.html",
"""
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
""",
)

self.create_template(
"view.html",
"""
<c-component x-data="{}" x-init="do_something()" />
""",
)

# Register Url
self.register_url("view/", self.make_view("view.html"))

# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf()):
response = self.client.get("/view/")

self.assertContains(response, 'x-data="{}" x-init="do_something()"')

def test_attribute_names_on_cvars_containing_hyphens_are_converted_to_underscores(
self,
):
self.create_template(
"cotton/component.html",
"""
<c-vars x-data="{}" x-init="do_something()" />
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
""",
)

self.create_template(
"view.html",
"""
<c-component />
""",
)

# Register Url
self.register_url("view/", self.make_view("view.html"))

# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf()):
response = self.client.get("/view/")

self.assertContains(response, 'x-data="{}" x-init="do_something()"')


class CottonTestCase(TestCase):
def test_parent_component_is_rendered(self):
Expand Down

0 comments on commit 16d55ca

Please sign in to comment.