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

fix: loader tags compatibility #468

1 change: 1 addition & 0 deletions src/django_components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ def render(
context_data = {}

slots, resolved_fills = resolve_slots(
context,
template,
component_name=self.registered_name,
context_data=context_data,
Expand Down
61 changes: 58 additions & 3 deletions src/django_components/node.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from typing import Callable, List, NamedTuple, Optional

from django.template import Context, Template
from django.template.base import Node, NodeList, TextNode
from django.template.defaulttags import CommentNode
from django.template.loader_tags import ExtendsNode, IncludeNode, construct_relative_path


def nodelist_has_content(nodelist: NodeList) -> bool:
Expand All @@ -20,26 +22,79 @@ class NodeTraverse(NamedTuple):
parent: Optional["NodeTraverse"]


def walk_nodelist(nodes: NodeList, callback: Callable[[Node], Optional[str]]) -> None:
def walk_nodelist(
nodes: NodeList,
callback: Callable[[Node], Optional[str]],
context: Optional[Context] = None,
) -> None:
"""Recursively walk a NodeList, calling `callback` for each Node."""
node_queue: List[NodeTraverse] = [NodeTraverse(node=node, parent=None) for node in nodes]
while len(node_queue):
traverse = node_queue.pop()
callback(traverse)
child_nodes = get_node_children(traverse.node)
child_nodes = get_node_children(traverse.node, context)
child_traverses = [NodeTraverse(node=child_node, parent=traverse) for child_node in child_nodes]
node_queue.extend(child_traverses)


def get_node_children(node: Node) -> NodeList:
def get_node_children(node: Node, context: Optional[Context] = None) -> NodeList:
"""
Get child Nodes from Node's nodelist atribute.

This function is taken from `get_nodes_by_type` method of `django.template.base.Node`.
"""
# Special case - {% extends %} tag - Load the template and go deeper
if isinstance(node, ExtendsNode):
# NOTE: When {% extends %} node is being parsed, it collects all remaining template
# under node.nodelist.
# Hence, when we come across ExtendsNode in the template, we:
# 1. Go over all nodes in the template using `node.nodelist`
# 2. Go over all nodes in the "parent" template, via `node.get_parent`
nodes = NodeList()
nodes.extend(node.nodelist)
template = node.get_parent(context)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To resolve the parent Template referenced via {% extends %} tag, we needed to pass it a Context. That's why much of the non-test changes is adding a context argument to functions

nodes.extend(template.nodelist)
return nodes

# Special case - {% include %} tag - Load the template and go deeper
elif isinstance(node, IncludeNode):
template = get_template_for_include_node(node, context)
return template.nodelist

nodes = NodeList()
for attr in node.child_nodelists:
nodelist = getattr(node, attr, [])
if nodelist:
nodes.extend(nodelist)
return nodes


def get_template_for_include_node(include_node: IncludeNode, context: Context) -> Template:
"""
This snippet is taken directly from `IncludeNode.render()`. Unfortunately the
render logic doesn't separate out template loading logic from rendering, so we
have to copy the method.
"""
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it says above, this function is a copy from Django's codebase

template = include_node.template.resolve(context)
# Does this quack like a Template?
if not callable(getattr(template, "render", None)):
# If not, try the cache and select_template().
template_name = template or ()
if isinstance(template_name, str):
template_name = (
construct_relative_path(
include_node.origin.template_name,
template_name,
),
)
else:
template_name = tuple(template_name)
cache = context.render_context.dicts[0].setdefault(include_node, {})
template = cache.get(template_name)
if template is None:
template = context.template.engine.select_template(template_name)
cache[template_name] = template
# Use the base.Template of a backends.django.Template.
elif hasattr(template, "template"):
template = template.template
return template
3 changes: 2 additions & 1 deletion src/django_components/slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ def _try_parse_as_default_fill(


def resolve_slots(
context: Context,
template: Template,
component_name: Optional[str],
context_data: Dict[str, Any],
Expand Down Expand Up @@ -374,7 +375,7 @@ def on_node(entry: NodeTraverse) -> None:
slot_children[parent_slot_id].append(node.node_id)
break

walk_nodelist(template.nodelist, on_node)
walk_nodelist(template.nodelist, on_node, context)

# 3. Figure out which slot the default/implicit fill belongs to
slot_fills = _resolve_default_slot(
Expand Down
17 changes: 17 additions & 0 deletions tests/templates/block_in_slot_in_component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% load component_tags %}
<!DOCTYPE html>
<html lang="en">
<body>
{% component "slotted_component" %}
{% fill "header" %}{% endfill %}
{% fill "main" %}
{% slot "body" %}
Helloodiddoo
{% block inner %}
Default inner
{% endblock %}
{% endslot %}
{% endfill %}
{% endcomponent %}
</body>
</html>
13 changes: 13 additions & 0 deletions tests/templates/block_inside_component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% load component_tags %}
<!DOCTYPE html>
<html lang="en">
<body>
{% component "slotted_component" %}
{% fill "header" %}{% endfill %}
{% fill "main" %}
{% block body %}
{% endblock %}
{% endfill %}
{% endcomponent %}
</body>
</html>
18 changes: 9 additions & 9 deletions tests/templates/extendable_template_with_blocks.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{% load component_tags %}
<!DOCTYPE html>
<html lang="en">
<body>
<main role="main">
<div class='container main-container'>
{% block body %}
{% endblock %}
</div>
</main>
</body>
</html>
<body>
<main role="main">
<div class='container main-container'>
{% block body %}
{% endblock %}
</div>
</main>
</body>
</html>
17 changes: 17 additions & 0 deletions tests/templates/slot_inside_block.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% load component_tags %}
<!DOCTYPE html>
<html lang="en">
<body>
{% component "slotted_component" %}
{% fill "header" %}{% endfill %}
{% fill "main" %}
Helloodiddoo
{% block inner %}
{% slot "body" %}
Default inner
{% endslot %}
{% endblock %}
{% endfill %}
{% endcomponent %}
</body>
</html>
1 change: 1 addition & 0 deletions tests/templates/slot_inside_extends.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% extends "block_in_slot_in_component.html" %}
1 change: 1 addition & 0 deletions tests/templates/slot_inside_include.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% include "block_in_slot_in_component.html" %}
Loading
Loading