diff --git a/conftest.py b/conftest.py index 2646fdb3..103104a1 100644 --- a/conftest.py +++ b/conftest.py @@ -8,6 +8,9 @@ def pytest_configure(): { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": ["tests"], + "OPTIONS": { + "libraries": {"unicorn": "django_unicorn.templatetags.unicorn",} + }, } ] databases = {"default": {"ENGINE": "django.db.backends.sqlite3",}} diff --git a/django_unicorn/components/unicorn_template_response.py b/django_unicorn/components/unicorn_template_response.py index 09c342d4..5870abfa 100644 --- a/django_unicorn/components/unicorn_template_response.py +++ b/django_unicorn/components/unicorn_template_response.py @@ -95,17 +95,20 @@ def render(self): json_tag["id"] = json_element_id json_tag.string = sanitize_html(init) + # Include init script and json tags from child components + json_tags = [json_tag] + for child in self.component.children: + if hasattr(child, "_init_script"): + init_script = f"{init_script} {child._init_script}" + if hasattr(child, "_json_tags"): + json_tags.extend(child._json_tags) + + # Defer rendering the init script and json tag until the outermost + # component (without a parent) is rendered if self.component.parent: self.component._init_script = init_script - self.component._json_tag = json_tag + self.component._json_tags = json_tags else: - json_tags = [] - json_tags.append(json_tag) - - for child in self.component.children: - init_script = f"{init_script} {child._init_script}" - json_tags.append(child._json_tag) - script_tag = soup.new_tag("script") script_tag["type"] = "module" script_tag.string = f"if (typeof Unicorn === 'undefined') {{ console.error('Unicorn is missing. Do you need {{% load unicorn %}} or {{% unicorn_scripts %}}?') }} else {{ {init_script} }}" diff --git a/example/unicorn/components/nested/actions.py b/example/unicorn/components/nested/actions.py new file mode 100644 index 00000000..43fbcd2e --- /dev/null +++ b/example/unicorn/components/nested/actions.py @@ -0,0 +1,14 @@ +from django_unicorn.components import UnicornView +from typing import Callable + + +class ActionsView(UnicornView): + # In this example the ActionsView is completely controlled by the + # RowView and it doesn't really "own" these - its useful to put them + # on the class for type hints and/or default values, but we don't + # want to give the impression they are ActionsView's own state + + is_editing: bool + on_edit: Callable + on_cancel: Callable + on_save: Callable \ No newline at end of file diff --git a/example/unicorn/components/nested/row.py b/example/unicorn/components/nested/row.py index bf31a7c9..c119aa31 100644 --- a/example/unicorn/components/nested/row.py +++ b/example/unicorn/components/nested/row.py @@ -2,16 +2,31 @@ from example.coffee.models import Flavor +def callback(func): + """A decorator for callbacks passed as kwargs to a child component + + This allows the bound method itself to be resolved in the template. + Without it, the template variable resolver will automatically call + the method and use what is returned as the resolved value. + """ + func.do_not_call_in_templates = True + return func + + class RowView(UnicornView): model: Flavor = None is_editing = False - def edit(self): + @callback + def on_edit(self): + print("on_edit callback fired") self.is_editing = True - def cancel(self): + @callback + def on_cancel(self): self.is_editing = False - def save(self): + @callback + def on_save(self): self.model.save() - self.is_editing = False + self.is_editing = False \ No newline at end of file diff --git a/example/unicorn/templates/unicorn/nested/actions.html b/example/unicorn/templates/unicorn/nested/actions.html new file mode 100644 index 00000000..ca8e964c --- /dev/null +++ b/example/unicorn/templates/unicorn/nested/actions.html @@ -0,0 +1,9 @@ + + {% if is_editing %} + + + + {% else %} + + {% endif %} + \ No newline at end of file diff --git a/example/unicorn/templates/unicorn/nested/row.html b/example/unicorn/templates/unicorn/nested/row.html index 97cfe565..b65892c0 100644 --- a/example/unicorn/templates/unicorn/nested/row.html +++ b/example/unicorn/templates/unicorn/nested/row.html @@ -1,3 +1,5 @@ +{% load unicorn %} + {% if is_editing %} @@ -22,12 +24,13 @@ n/a {% endif %} - - {% if is_editing %} - - - {% else %} - - {% endif %} - + + {% unicorn 'nested.actions' parent=view key=model.id is_editing=is_editing on_edit=on_edit on_cancel=on_cancel on_save=on_save %} + + + \ No newline at end of file diff --git a/tests/templates/test_component_parent.html b/tests/templates/test_component_parent.html index e207cfed..853d0f3c 100644 --- a/tests/templates/test_component_parent.html +++ b/tests/templates/test_component_parent.html @@ -1,4 +1,6 @@ +{% load unicorn %} +
parent - {% unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view } + {% unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view %}
\ No newline at end of file diff --git a/tests/templates/test_component_parent_nested.html b/tests/templates/test_component_parent_nested.html new file mode 100644 index 00000000..b9a2cf7b --- /dev/null +++ b/tests/templates/test_component_parent_nested.html @@ -0,0 +1,6 @@ +{% load unicorn %} + +
+ parent nested (3 layers) + {% unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParent' parent=view %} +
\ No newline at end of file diff --git a/tests/templatetags/test_unicorn_render.py b/tests/templatetags/test_unicorn_render.py index f112cea3..ea76b698 100644 --- a/tests/templatetags/test_unicorn_render.py +++ b/tests/templatetags/test_unicorn_render.py @@ -46,6 +46,10 @@ def mount(self): self.call("testCall2", "hello") +class FakeComponentParentNested(UnicornView): + template_name = "templates/test_component_parent_nested.html" + + def test_unicorn_render_kwarg(): token = Token( TokenType.TEXT, @@ -303,3 +307,15 @@ def test_unicorn_render_hash(settings): rendered_content = html[:script_idx] expected_hash = generate_checksum(rendered_content) assert f'"hash":"{expected_hash}"' in html + + +def test_unicorn_render_parent_nested_multiple_layers(settings): + settings.DEBUG = True + token = Token( + TokenType.TEXT, + "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParentNested'", + ) + unicorn_node = unicorn(None, token) + context = {} + html = unicorn_node.render(context) + assert html.count("componentInit") == 3 \ No newline at end of file