Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested components proposal #265

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",}}
Expand Down
19 changes: 11 additions & 8 deletions django_unicorn/components/unicorn_template_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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} }}"
Expand Down
14 changes: 14 additions & 0 deletions example/unicorn/components/nested/actions.py
Original file line number Diff line number Diff line change
@@ -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
23 changes: 19 additions & 4 deletions example/unicorn/components/nested/row.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions example/unicorn/templates/unicorn/nested/actions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<td>
{% if is_editing %}
<!-- on_save etc in the ActionsView context will be bound methods of the RowView instance -->
<button unicorn:click="on_save">Save</button>
<button unicorn:click.discard="on_cancel">Cancel</button>
{% else %}
<button unicorn:click="on_edit">Edit</button>
{% endif %}
</td>
19 changes: 11 additions & 8 deletions example/unicorn/templates/unicorn/nested/row.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{% load unicorn %}

<tr>
<td>
{% if is_editing %}
Expand All @@ -22,12 +24,13 @@
n/a
{% endif %}
</td>
<td>
{% if is_editing %}
<button unicorn:click="save">Save</button>
<button unicorn:click.discard="cancel" unicorn:partial.key="{{ model.pk }}">Cancel</button>
{% else %}
<button unicorn:click="edit" unicorn:partial.key="{{ model.pk }}">Edit</button>
{% endif %}
</td>

{% unicorn 'nested.actions' parent=view key=model.id is_editing=is_editing on_edit=on_edit on_cancel=on_cancel on_save=on_save %}

<!-- there could be a short form if the kwarg has the same name as the attribute?
since tags cannot span multiple lines

{% unicorn 'nested.actions' parent=view key=model.id is_editing on_edit on_cancel on_save %}
-->

</tr>
4 changes: 3 additions & 1 deletion tests/templates/test_component_parent.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{% load unicorn %}

<div>
parent
{% unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view }
{% unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view %}
</div>
6 changes: 6 additions & 0 deletions tests/templates/test_component_parent_nested.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% load unicorn %}

<div>
parent nested (3 layers)
{% unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParent' parent=view %}
</div>
16 changes: 16 additions & 0 deletions tests/templatetags/test_unicorn_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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