diff --git a/django_cotton/cotton_loader.py b/django_cotton/cotton_loader.py index 62f112f..c29c6e7 100755 --- a/django_cotton/cotton_loader.py +++ b/django_cotton/cotton_loader.py @@ -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 @@ -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() diff --git a/django_cotton/templatetags/_component.py b/django_cotton/templatetags/_component.py index 23c11e9..28ca965 100644 --- a/django_cotton/templatetags/_component.py +++ b/django_cotton/templatetags/_component.py @@ -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): diff --git a/django_cotton/templatetags/_vars_frame.py b/django_cotton/templatetags/_vars_frame.py index a4ad16d..bd03258 100644 --- a/django_cotton/templatetags/_vars_frame.py +++ b/django_cotton/templatetags/_vars_frame.py @@ -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 @@ -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() @@ -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) diff --git a/django_cotton/tests/inline_test_case.py b/django_cotton/tests/inline_test_case.py index 3f9d70f..5965412 100644 --- a/django_cotton/tests/inline_test_case.py +++ b/django_cotton/tests/inline_test_case.py @@ -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 @@ -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 diff --git a/django_cotton/tests/test_cotton.py b/django_cotton/tests/test_cotton.py index d8e369f..00f37b4 100644 --- a/django_cotton/tests/test_cotton.py +++ b/django_cotton/tests/test_cotton.py @@ -8,24 +8,16 @@ class InlineTestCase(CottonInlineTestCase): def test_component_is_rendered(self): self.create_template( "cotton/component.html", - """ -