refactor: pass slot data and slot default to slot render fn and rename LazySlot to SlotRef

This commit is contained in:
Juro Oravec 2024-06-11 14:34:04 +02:00
parent 40f4476993
commit 5c89d4dbeb
4 changed files with 62 additions and 44 deletions

View file

@ -37,8 +37,17 @@ from django_components.context import (
from django_components.expression import safe_resolve_dict, safe_resolve_list
from django_components.logger import logger, trace_msg
from django_components.middleware import is_dependency_middleware_active
from django_components.node import RenderedContent, nodelist_to_render_func
from django_components.slots import DEFAULT_SLOT_KEY, FillContent, FillNode, SlotContent, SlotName, resolve_slots
from django_components.slots import (
DEFAULT_SLOT_KEY,
FillContent,
FillNode,
SlotContent,
SlotName,
SlotRef,
SlotRenderedContent,
_nodelist_to_slot_render_func,
resolve_slots,
)
from django_components.template_parser import process_aggregate_kwargs
from django_components.utils import gen_id, search
@ -495,13 +504,13 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
slot_fills = {}
for slot_name, content in slots_data.items():
if isinstance(content, (str, SafeString)):
content_func = nodelist_to_render_func(
content_func = _nodelist_to_slot_render_func(
NodeList([TextNode(escape(content) if escape_content else content)])
)
else:
def content_func(ctx: Context) -> RenderedContent:
rendered = content(ctx)
def content_func(ctx: Context, kwargs: Dict[str, Any], slot_ref: SlotRef) -> SlotRenderedContent:
rendered = content(ctx, kwargs, slot_ref)
return escape(rendered) if escape_content else rendered
slot_fills[slot_name] = FillContent(
@ -555,7 +564,7 @@ class ComponentNode(Node):
if is_default_slot:
fill_content: Dict[str, FillContent] = {
DEFAULT_SLOT_KEY: FillContent(
content_func=nodelist_to_render_func(self.fill_nodes[0].nodelist),
content_func=_nodelist_to_slot_render_func(self.fill_nodes[0].nodelist),
slot_data_var=None,
slot_default_var=None,
),
@ -575,7 +584,7 @@ class ComponentNode(Node):
resolved_slot_default_var = fill_node.resolve_slot_default(context, resolved_component_name)
resolved_slot_data_var = fill_node.resolve_slot_data(context, resolved_component_name)
fill_content[resolved_name] = FillContent(
content_func=nodelist_to_render_func(fill_node.nodelist),
content_func=_nodelist_to_slot_render_func(fill_node.nodelist),
slot_default_var=resolved_slot_default_var,
slot_data_var=resolved_slot_data_var,
)

View file

@ -1,20 +1,9 @@
from typing import Callable, List, NamedTuple, Optional, Union
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
from django.utils.safestring import SafeText
RenderedContent = Union[str, SafeText]
RenderFunc = Callable[[Context], RenderedContent]
def nodelist_to_render_func(nodelist: NodeList) -> RenderFunc:
def render_func(ctx: Context) -> RenderedContent:
return nodelist.render(ctx)
return render_func
def nodelist_has_content(nodelist: NodeList) -> bool:

View file

@ -2,7 +2,7 @@ import difflib
import json
import re
from collections import deque
from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, Type, Union
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Set, Tuple, Type, Union
from django.template import Context, Template
from django.template.base import FilterExpression, Node, NodeList, Parser, TextNode
@ -14,25 +14,22 @@ from django_components.app_settings import ContextBehavior, app_settings
from django_components.context import _FILLED_SLOTS_CONTENT_CONTEXT_KEY, _ROOT_CTX_CONTEXT_KEY
from django_components.expression import resolve_expression_as_identifier, safe_resolve_dict
from django_components.logger import trace_msg
from django_components.node import (
NodeTraverse,
RenderFunc,
nodelist_has_content,
nodelist_to_render_func,
walk_nodelist,
)
from django_components.node import NodeTraverse, nodelist_has_content, walk_nodelist
from django_components.template_parser import process_aggregate_kwargs
from django_components.utils import gen_id
DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
SlotRenderedContent = Union[str, SafeString]
SlotRenderFunc = Callable[[Context, Dict[str, Any], "SlotRef"], SlotRenderedContent]
# Type aliases
SlotId = str
SlotName = str
SlotDefaultName = str
SlotDataName = str
SlotContent = Union[str, SafeString, RenderFunc]
SlotContent = Union[str, SafeString, SlotRenderFunc]
class FillContent(NamedTuple):
@ -50,7 +47,7 @@ class FillContent(NamedTuple):
```
"""
content_func: RenderFunc
content_func: SlotRenderFunc
slot_default_var: Optional[SlotDefaultName]
slot_data_var: Optional[SlotDataName]
@ -85,18 +82,18 @@ class SlotFill(NamedTuple):
name: str
escaped_name: str
is_filled: bool
content_func: RenderFunc
content_func: SlotRenderFunc
context_data: Dict
slot_default_var: Optional[SlotDefaultName]
slot_data_var: Optional[SlotDataName]
class LazySlot:
class SlotRef:
"""
Bound a slot and a context, so we can move these around as a single variable,
and lazily render the slot with given context only once coerced to string.
SlotRef allows to treat a slot as a variable. The slot is rendered only once
the instance is coerced to string.
This is used to access slots as variables inside the templates. When a LazySlot
This is used to access slots as variables inside the templates. When a SlotRef
is rendered in the template with `{{ my_lazy_slot }}`, it will output the contents
of the slot.
"""
@ -105,7 +102,7 @@ class LazySlot:
self._slot = slot
self._context = context
# Render the slot when the template coerces LazySlot to string
# Render the slot when the template coerces SlotRef to string
def __str__(self) -> str:
return mark_safe(self._slot.nodelist.render(self._context))
@ -149,25 +146,26 @@ class SlotNode(Node):
# If slot fill is using `{% fill "myslot" default="abc" %}`, then set the "abc" to
# the context, so users can refer to the default slot from within the fill content.
slot_ref = SlotRef(self, context)
default_var = slot_fill.slot_default_var
if default_var:
if not default_var.isidentifier():
raise TemplateSyntaxError(
f"Slot default alias in fill '{self.name}' must be a valid identifier. Got '{default_var}'"
)
extra_context[default_var] = LazySlot(self, context)
extra_context[default_var] = slot_ref
# Expose the kwargs that were passed to the `{% slot %}` tag. These kwargs
# are made available through a variable name that was set on the `{% fill %}`
# tag.
slot_kwargs = safe_resolve_dict(self.slot_kwargs, context)
slot_kwargs = process_aggregate_kwargs(slot_kwargs)
data_var = slot_fill.slot_data_var
if data_var:
if not data_var.isidentifier():
raise TemplateSyntaxError(
f"Slot data alias in fill '{self.name}' must be a valid identifier. Got '{data_var}'"
)
slot_kwargs = safe_resolve_dict(self.slot_kwargs, context)
slot_kwargs = process_aggregate_kwargs(slot_kwargs)
extra_context[data_var] = slot_kwargs
# For the user-provided slot fill, we want to use the context of where the slot
@ -175,7 +173,9 @@ class SlotNode(Node):
used_ctx = self._resolve_slot_context(context, slot_fill)
with used_ctx.update(extra_context):
# Render slot as a function
output = slot_fill.content_func(used_ctx)
# NOTE: While `{% fill %}` tag has to opt in for the `default` and `data` variables,
# the render function ALWAYS receives them.
output = slot_fill.content_func(used_ctx, slot_kwargs, slot_ref)
trace_msg("RENDR", "SLOT", self.name, self.node_id, msg="...Done!")
return output
@ -465,7 +465,7 @@ def resolve_slots(
name=slot.name,
escaped_name=_escape_slot_name(slot.name),
is_filled=False,
content_func=nodelist_to_render_func(slot.nodelist),
content_func=_nodelist_to_slot_render_func(slot.nodelist),
context_data=context_data,
slot_default_var=None,
slot_data_var=None,
@ -595,3 +595,10 @@ def _escape_slot_name(name: str) -> str:
# we leave this obligation to the user.
escaped_name = name_escape_re.sub("_", name)
return escaped_name
def _nodelist_to_slot_render_func(nodelist: NodeList) -> SlotRenderFunc:
def render_func(ctx: Context, slot_kwargs: Dict[str, Any], slot_ref: SlotRef) -> SlotRenderedContent:
return nodelist.render(ctx)
return render_func

View file

@ -3,6 +3,8 @@ Tests focusing on the Component class.
For tests focusing on the `component` tag, see `test_templatetags_component.py`
"""
from typing import Dict
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponse
from django.template import Context, Template, TemplateSyntaxError
@ -14,6 +16,7 @@ from .testutils import BaseTestCase, parametrize_context_behavior
# isort: on
from django_components import component, types
from django_components.slots import SlotRef
class ComponentTest(BaseTestCase):
@ -360,7 +363,8 @@ class ComponentRenderTest(BaseTestCase):
class SimpleComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
{% slot "first" required %}
{% slot "first" required data1="abc" data2:hello="world" data2:one=123 %}
SLOT_DEFAULT
{% endslot %}
"""
@ -371,7 +375,7 @@ class ComponentRenderTest(BaseTestCase):
"kwargs": kwargs,
}
def first_slot(ctx: Context):
def first_slot(ctx: Context, slot_data: Dict, slot_ref: SlotRef):
self.assertIsInstance(ctx, Context)
# NOTE: Since the slot has access to the Context object, it should behave
# the same way as it does in templates - when in "isolated" mode, then the
@ -388,7 +392,16 @@ class ComponentRenderTest(BaseTestCase):
self.assertEqual(ctx["kwargs"], {})
self.assertEqual(ctx["abc"], "def")
return "FROM_INSIDE_FIRST_SLOT"
slot_data_expected = {
"data1": "abc",
"data2": {"hello": "world", "one": 123},
}
self.assertDictEqual(slot_data_expected, slot_data)
self.assertIsInstance(slot_ref, SlotRef)
self.assertEqual("SLOT_DEFAULT", str(slot_ref).strip())
return f"FROM_INSIDE_FIRST_SLOT | {slot_ref}"
rendered = SimpleComponent.render(
context={"abc": "def"},
@ -398,7 +411,7 @@ class ComponentRenderTest(BaseTestCase):
)
self.assertHTMLEqual(
rendered,
"FROM_INSIDE_FIRST_SLOT",
"FROM_INSIDE_FIRST_SLOT | SLOT_DEFAULT",
)
@parametrize_context_behavior(["django", "isolated"])