mirror of
https://github.com/django-components/django-components.git
synced 2025-09-26 15:39:08 +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.expression import safe_resolve_dict, safe_resolve_list
|
||||||
from django_components.logger import logger, trace_msg
|
from django_components.logger import logger, trace_msg
|
||||||
from django_components.middleware import is_dependency_middleware_active
|
from django_components.middleware import is_dependency_middleware_active
|
||||||
from django_components.node import RenderedContent, nodelist_to_render_func
|
from django_components.slots import (
|
||||||
from django_components.slots import DEFAULT_SLOT_KEY, FillContent, FillNode, SlotContent, SlotName, resolve_slots
|
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.template_parser import process_aggregate_kwargs
|
||||||
from django_components.utils import gen_id, search
|
from django_components.utils import gen_id, search
|
||||||
|
|
||||||
|
@ -495,13 +504,13 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
slot_fills = {}
|
slot_fills = {}
|
||||||
for slot_name, content in slots_data.items():
|
for slot_name, content in slots_data.items():
|
||||||
if isinstance(content, (str, SafeString)):
|
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)])
|
NodeList([TextNode(escape(content) if escape_content else content)])
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def content_func(ctx: Context) -> RenderedContent:
|
def content_func(ctx: Context, kwargs: Dict[str, Any], slot_ref: SlotRef) -> SlotRenderedContent:
|
||||||
rendered = content(ctx)
|
rendered = content(ctx, kwargs, slot_ref)
|
||||||
return escape(rendered) if escape_content else rendered
|
return escape(rendered) if escape_content else rendered
|
||||||
|
|
||||||
slot_fills[slot_name] = FillContent(
|
slot_fills[slot_name] = FillContent(
|
||||||
|
@ -555,7 +564,7 @@ class ComponentNode(Node):
|
||||||
if is_default_slot:
|
if is_default_slot:
|
||||||
fill_content: Dict[str, FillContent] = {
|
fill_content: Dict[str, FillContent] = {
|
||||||
DEFAULT_SLOT_KEY: 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_data_var=None,
|
||||||
slot_default_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_default_var = fill_node.resolve_slot_default(context, resolved_component_name)
|
||||||
resolved_slot_data_var = fill_node.resolve_slot_data(context, resolved_component_name)
|
resolved_slot_data_var = fill_node.resolve_slot_data(context, resolved_component_name)
|
||||||
fill_content[resolved_name] = FillContent(
|
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_default_var=resolved_slot_default_var,
|
||||||
slot_data_var=resolved_slot_data_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 import Context, Template
|
||||||
from django.template.base import Node, NodeList, TextNode
|
from django.template.base import Node, NodeList, TextNode
|
||||||
from django.template.defaulttags import CommentNode
|
from django.template.defaulttags import CommentNode
|
||||||
from django.template.loader_tags import ExtendsNode, IncludeNode, construct_relative_path
|
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:
|
def nodelist_has_content(nodelist: NodeList) -> bool:
|
||||||
|
|
|
@ -2,7 +2,7 @@ import difflib
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from collections import deque
|
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 import Context, Template
|
||||||
from django.template.base import FilterExpression, Node, NodeList, Parser, TextNode
|
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.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.expression import resolve_expression_as_identifier, safe_resolve_dict
|
||||||
from django_components.logger import trace_msg
|
from django_components.logger import trace_msg
|
||||||
from django_components.node import (
|
from django_components.node import NodeTraverse, nodelist_has_content, walk_nodelist
|
||||||
NodeTraverse,
|
|
||||||
RenderFunc,
|
|
||||||
nodelist_has_content,
|
|
||||||
nodelist_to_render_func,
|
|
||||||
walk_nodelist,
|
|
||||||
)
|
|
||||||
from django_components.template_parser import process_aggregate_kwargs
|
from django_components.template_parser import process_aggregate_kwargs
|
||||||
from django_components.utils import gen_id
|
from django_components.utils import gen_id
|
||||||
|
|
||||||
DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
|
DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
|
||||||
|
|
||||||
|
SlotRenderedContent = Union[str, SafeString]
|
||||||
|
SlotRenderFunc = Callable[[Context, Dict[str, Any], "SlotRef"], SlotRenderedContent]
|
||||||
|
|
||||||
# Type aliases
|
# Type aliases
|
||||||
|
|
||||||
SlotId = str
|
SlotId = str
|
||||||
SlotName = str
|
SlotName = str
|
||||||
SlotDefaultName = str
|
SlotDefaultName = str
|
||||||
SlotDataName = str
|
SlotDataName = str
|
||||||
SlotContent = Union[str, SafeString, RenderFunc]
|
SlotContent = Union[str, SafeString, SlotRenderFunc]
|
||||||
|
|
||||||
|
|
||||||
class FillContent(NamedTuple):
|
class FillContent(NamedTuple):
|
||||||
|
@ -50,7 +47,7 @@ class FillContent(NamedTuple):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
content_func: RenderFunc
|
content_func: SlotRenderFunc
|
||||||
slot_default_var: Optional[SlotDefaultName]
|
slot_default_var: Optional[SlotDefaultName]
|
||||||
slot_data_var: Optional[SlotDataName]
|
slot_data_var: Optional[SlotDataName]
|
||||||
|
|
||||||
|
@ -85,18 +82,18 @@ class SlotFill(NamedTuple):
|
||||||
name: str
|
name: str
|
||||||
escaped_name: str
|
escaped_name: str
|
||||||
is_filled: bool
|
is_filled: bool
|
||||||
content_func: RenderFunc
|
content_func: SlotRenderFunc
|
||||||
context_data: Dict
|
context_data: Dict
|
||||||
slot_default_var: Optional[SlotDefaultName]
|
slot_default_var: Optional[SlotDefaultName]
|
||||||
slot_data_var: Optional[SlotDataName]
|
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,
|
SlotRef allows to treat a slot as a variable. The slot is rendered only once
|
||||||
and lazily render the slot with given context only once coerced to string.
|
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
|
is rendered in the template with `{{ my_lazy_slot }}`, it will output the contents
|
||||||
of the slot.
|
of the slot.
|
||||||
"""
|
"""
|
||||||
|
@ -105,7 +102,7 @@ class LazySlot:
|
||||||
self._slot = slot
|
self._slot = slot
|
||||||
self._context = context
|
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:
|
def __str__(self) -> str:
|
||||||
return mark_safe(self._slot.nodelist.render(self._context))
|
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
|
# 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.
|
# 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
|
default_var = slot_fill.slot_default_var
|
||||||
if default_var:
|
if default_var:
|
||||||
if not default_var.isidentifier():
|
if not default_var.isidentifier():
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"Slot default alias in fill '{self.name}' must be a valid identifier. Got '{default_var}'"
|
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
|
# 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 %}`
|
# are made available through a variable name that was set on the `{% fill %}`
|
||||||
# tag.
|
# tag.
|
||||||
|
slot_kwargs = safe_resolve_dict(self.slot_kwargs, context)
|
||||||
|
slot_kwargs = process_aggregate_kwargs(slot_kwargs)
|
||||||
data_var = slot_fill.slot_data_var
|
data_var = slot_fill.slot_data_var
|
||||||
if data_var:
|
if data_var:
|
||||||
if not data_var.isidentifier():
|
if not data_var.isidentifier():
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"Slot data alias in fill '{self.name}' must be a valid identifier. Got '{data_var}'"
|
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
|
extra_context[data_var] = slot_kwargs
|
||||||
|
|
||||||
# For the user-provided slot fill, we want to use the context of where the slot
|
# 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)
|
used_ctx = self._resolve_slot_context(context, slot_fill)
|
||||||
with used_ctx.update(extra_context):
|
with used_ctx.update(extra_context):
|
||||||
# Render slot as a function
|
# 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!")
|
trace_msg("RENDR", "SLOT", self.name, self.node_id, msg="...Done!")
|
||||||
return output
|
return output
|
||||||
|
@ -465,7 +465,7 @@ def resolve_slots(
|
||||||
name=slot.name,
|
name=slot.name,
|
||||||
escaped_name=_escape_slot_name(slot.name),
|
escaped_name=_escape_slot_name(slot.name),
|
||||||
is_filled=False,
|
is_filled=False,
|
||||||
content_func=nodelist_to_render_func(slot.nodelist),
|
content_func=_nodelist_to_slot_render_func(slot.nodelist),
|
||||||
context_data=context_data,
|
context_data=context_data,
|
||||||
slot_default_var=None,
|
slot_default_var=None,
|
||||||
slot_data_var=None,
|
slot_data_var=None,
|
||||||
|
@ -595,3 +595,10 @@ def _escape_slot_name(name: str) -> str:
|
||||||
# we leave this obligation to the user.
|
# we leave this obligation to the user.
|
||||||
escaped_name = name_escape_re.sub("_", name)
|
escaped_name = name_escape_re.sub("_", name)
|
||||||
return escaped_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`
|
For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
|
@ -14,6 +16,7 @@ from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
# isort: on
|
# isort: on
|
||||||
|
|
||||||
from django_components import component, types
|
from django_components import component, types
|
||||||
|
from django_components.slots import SlotRef
|
||||||
|
|
||||||
|
|
||||||
class ComponentTest(BaseTestCase):
|
class ComponentTest(BaseTestCase):
|
||||||
|
@ -360,7 +363,8 @@ class ComponentRenderTest(BaseTestCase):
|
||||||
class SimpleComponent(component.Component):
|
class SimpleComponent(component.Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% slot "first" required %}
|
{% slot "first" required data1="abc" data2:hello="world" data2:one=123 %}
|
||||||
|
SLOT_DEFAULT
|
||||||
{% endslot %}
|
{% endslot %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -371,7 +375,7 @@ class ComponentRenderTest(BaseTestCase):
|
||||||
"kwargs": kwargs,
|
"kwargs": kwargs,
|
||||||
}
|
}
|
||||||
|
|
||||||
def first_slot(ctx: Context):
|
def first_slot(ctx: Context, slot_data: Dict, slot_ref: SlotRef):
|
||||||
self.assertIsInstance(ctx, Context)
|
self.assertIsInstance(ctx, Context)
|
||||||
# NOTE: Since the slot has access to the Context object, it should behave
|
# 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
|
# 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["kwargs"], {})
|
||||||
self.assertEqual(ctx["abc"], "def")
|
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(
|
rendered = SimpleComponent.render(
|
||||||
context={"abc": "def"},
|
context={"abc": "def"},
|
||||||
|
@ -398,7 +411,7 @@ class ComponentRenderTest(BaseTestCase):
|
||||||
)
|
)
|
||||||
self.assertHTMLEqual(
|
self.assertHTMLEqual(
|
||||||
rendered,
|
rendered,
|
||||||
"FROM_INSIDE_FIRST_SLOT",
|
"FROM_INSIDE_FIRST_SLOT | SLOT_DEFAULT",
|
||||||
)
|
)
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue