mirror of
https://github.com/django-components/django-components.git
synced 2025-08-22 06:54:04 +00:00
refactor: pass slot data and slot default to slot render fn and rename LazySlot to SlotRef
This commit is contained in:
parent
40f4476993
commit
5c89d4dbeb
4 changed files with 62 additions and 44 deletions
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"])
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue