mirror of
https://github.com/django-components/django-components.git
synced 2025-09-26 23:49:07 +00:00
feat: refactor render fn and allow slots as functions
This commit is contained in:
parent
3a7d5355cf
commit
fee26ec1d8
5 changed files with 243 additions and 140 deletions
|
@ -9,7 +9,7 @@ class Greeting(component.Component):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
slots = {"message": "Hello, world!"}
|
slots = {"message": "Hello, world!"}
|
||||||
context = {"name": request.GET.get("name", "")}
|
context = {"name": request.GET.get("name", "")}
|
||||||
return self.render_to_response(context, slots)
|
return self.render_to_response(context=context, slots=slots)
|
||||||
|
|
||||||
def get_context_data(self, name, *args, **kwargs) -> Dict[str, Any]:
|
def get_context_data(self, name, *args, **kwargs) -> Dict[str, Any]:
|
||||||
return {"name": name}
|
return {"name": name}
|
||||||
|
|
|
@ -37,7 +37,8 @@ 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.slots import DEFAULT_SLOT_KEY, FillContent, FillNode, SlotName, resolve_slots
|
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.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
|
||||||
|
|
||||||
|
@ -180,14 +181,27 @@ def _get_dir_path_from_component_path(
|
||||||
class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
# Either template_name or template must be set on subclass OR subclass must implement get_template() with
|
# Either template_name or template must be set on subclass OR subclass must implement get_template() with
|
||||||
# non-null return.
|
# non-null return.
|
||||||
class_hash: ClassVar[int]
|
_class_hash: ClassVar[int]
|
||||||
|
|
||||||
template_name: ClassVar[Optional[str]] = None
|
template_name: ClassVar[Optional[str]] = None
|
||||||
|
"""Relative filepath to the Django template associated with this component."""
|
||||||
template: Optional[str] = None
|
template: Optional[str] = None
|
||||||
|
"""Inlined Django template associated with this component."""
|
||||||
js: Optional[str] = None
|
js: Optional[str] = None
|
||||||
|
"""Inlined JS associated with this component."""
|
||||||
css: Optional[str] = None
|
css: Optional[str] = None
|
||||||
|
"""Inlined CSS associated with this component."""
|
||||||
media: Media
|
media: Media
|
||||||
|
"""
|
||||||
|
Normalized definition of JS and CSS media files associated with this component.
|
||||||
|
|
||||||
|
NOTE: This field is generated from Component.Media class.
|
||||||
|
"""
|
||||||
|
response_class = HttpResponse
|
||||||
|
"""This allows to configure what class is used to generate response from `render_to_response`"""
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
|
"""Defines JS and CSS media files associated with this component."""
|
||||||
css: Optional[Union[str, List[str], Dict[str, str], Dict[str, List[str]]]] = None
|
css: Optional[Union[str, List[str], Dict[str, str], Dict[str, List[str]]]] = None
|
||||||
js: Optional[Union[str, List[str]]] = None
|
js: Optional[Union[str, List[str]]] = None
|
||||||
|
|
||||||
|
@ -205,17 +219,39 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
self._context: Optional[Context] = None
|
self._context: Optional[Context] = None
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||||
cls.class_hash = hash(inspect.getfile(cls) + cls.__name__)
|
cls._class_hash = hash(inspect.getfile(cls) + cls.__name__)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.registered_name or self.__class__.__name__
|
||||||
|
|
||||||
def get_context_data(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
def get_context_data(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_template_name(self, context: Mapping) -> Optional[str]:
|
def get_template_name(self, context: Context) -> Optional[str]:
|
||||||
return self.template_name
|
return self.template_name
|
||||||
|
|
||||||
def get_template_string(self, context: Mapping) -> Optional[str]:
|
def get_template_string(self, context: Context) -> Optional[str]:
|
||||||
return self.template
|
return self.template
|
||||||
|
|
||||||
|
# NOTE: When the template is taken from a file (AKA specified via `template_name`),
|
||||||
|
# then we leverage Django's template caching. This means that the same instance
|
||||||
|
# of Template is reused. This is important to keep in mind, because the implication
|
||||||
|
# is that we should treat Templates AND their nodelists as IMMUTABLE.
|
||||||
|
def get_template(self, context: Context) -> Template:
|
||||||
|
template_string = self.get_template_string(context)
|
||||||
|
if template_string is not None:
|
||||||
|
return Template(template_string)
|
||||||
|
|
||||||
|
template_name = self.get_template_name(context)
|
||||||
|
if template_name is not None:
|
||||||
|
return get_template(template_name).template
|
||||||
|
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
f"Either 'template_name' or 'template' must be set for Component {type(self).__name__}."
|
||||||
|
f"Note: this attribute is not required if you are overriding the class's `get_template*()` methods."
|
||||||
|
)
|
||||||
|
|
||||||
def render_dependencies(self) -> SafeString:
|
def render_dependencies(self) -> SafeString:
|
||||||
"""Helper function to render all dependencies for a component."""
|
"""Helper function to render all dependencies for a component."""
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
@ -242,26 +278,6 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
return mark_safe(f"<script>{self.js}</script>")
|
return mark_safe(f"<script>{self.js}</script>")
|
||||||
return mark_safe("\n".join(self.media.render_js()))
|
return mark_safe("\n".join(self.media.render_js()))
|
||||||
|
|
||||||
# NOTE: When the template is taken from a file (AKA
|
|
||||||
# specified via `template_name`), then we leverage
|
|
||||||
# Django's template caching. This means that the same
|
|
||||||
# instance of Template is reused. This is important to keep
|
|
||||||
# in mind, because the implication is that we should
|
|
||||||
# treat Templates AND their nodelists as IMMUTABLE.
|
|
||||||
def get_template(self, context: Mapping) -> Template:
|
|
||||||
template_string = self.get_template_string(context)
|
|
||||||
if template_string is not None:
|
|
||||||
return Template(template_string)
|
|
||||||
|
|
||||||
template_name = self.get_template_name(context)
|
|
||||||
if template_name is not None:
|
|
||||||
return get_template(template_name).template
|
|
||||||
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
f"Either 'template_name' or 'template' must be set for Component {type(self).__name__}."
|
|
||||||
f"Note: this attribute is not required if you are overriding the class's `get_template*()` methods."
|
|
||||||
)
|
|
||||||
|
|
||||||
def inject(self, key: str, default: Optional[Any] = None) -> Any:
|
def inject(self, key: str, default: Optional[Any] = None) -> Any:
|
||||||
"""
|
"""
|
||||||
Use this method to retrieve the data that was passed to a `{% provide %}` tag
|
Use this method to retrieve the data that was passed to a `{% provide %}` tag
|
||||||
|
@ -303,20 +319,102 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
|
|
||||||
As the `{{ data.hello }}` is taken from the "provider".
|
As the `{{ data.hello }}` is taken from the "provider".
|
||||||
"""
|
"""
|
||||||
comp_name = self.registered_name or self.__class__.__name__
|
|
||||||
if self._context is None:
|
if self._context is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Method 'inject()' of component '{comp_name}' was called outside of 'get_context_data()'"
|
f"Method 'inject()' of component '{self.name}' was called outside of 'get_context_data()'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return get_injected_context_var(comp_name, self._context, key, default)
|
return get_injected_context_var(self.name, self._context, key, default)
|
||||||
|
|
||||||
def render_from_input(
|
@classmethod
|
||||||
self,
|
def render_to_response(
|
||||||
context: Context,
|
cls,
|
||||||
args: Union[List, Tuple],
|
context: Union[Dict[str, Any], Context] = None,
|
||||||
kwargs: Dict[str, Any],
|
slots: Optional[Mapping[SlotName, SlotContent]] = None,
|
||||||
|
escape_slots_content: bool = True,
|
||||||
|
args: Optional[Union[List, Tuple]] = None,
|
||||||
|
kwargs: Optional[Dict[str, Any]] = None,
|
||||||
|
*response_args: Any,
|
||||||
|
**response_kwargs: Any,
|
||||||
|
) -> HttpResponse:
|
||||||
|
"""
|
||||||
|
This is the interface for the `django.views.View` class which allows us to
|
||||||
|
use components as Django views with `component.as_view()`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```py
|
||||||
|
MyComponent.render_to_response(
|
||||||
|
args=[1, "two", {}],
|
||||||
|
kwargs={
|
||||||
|
"key"=123,
|
||||||
|
},
|
||||||
|
slots={
|
||||||
|
header='STATIC TEXT HERE',
|
||||||
|
},
|
||||||
|
escape_slots_content=False,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
content = cls.render(
|
||||||
|
args=args,
|
||||||
|
kwargs=kwargs,
|
||||||
|
context=context,
|
||||||
|
slots=slots,
|
||||||
|
escape_slots_content=escape_slots_content,
|
||||||
|
)
|
||||||
|
return cls.response_class(content, *response_args, **response_kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def render(
|
||||||
|
cls,
|
||||||
|
context: Optional[Union[Dict[str, Any], Context]] = None,
|
||||||
|
args: Optional[Union[List, Tuple]] = None,
|
||||||
|
kwargs: Optional[Dict[str, Any]] = None,
|
||||||
|
slots: Optional[Mapping[SlotName, SlotContent]] = None,
|
||||||
|
escape_slots_content: bool = True,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
"""
|
||||||
|
Example:
|
||||||
|
```py
|
||||||
|
MyComponent.render(
|
||||||
|
args=[1, "two", {}],
|
||||||
|
kwargs={
|
||||||
|
"key"=123,
|
||||||
|
},
|
||||||
|
slots={
|
||||||
|
header='STATIC TEXT HERE',
|
||||||
|
},
|
||||||
|
escape_slots_content=False,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
comp = cls()
|
||||||
|
return comp._render(context, args, kwargs, slots, escape_slots_content)
|
||||||
|
|
||||||
|
def _render(
|
||||||
|
self,
|
||||||
|
context: Union[Dict[str, Any], Context] = None,
|
||||||
|
args: Optional[Union[List, Tuple]] = None,
|
||||||
|
kwargs: Optional[Dict[str, Any]] = None,
|
||||||
|
slots: Optional[Mapping[SlotName, SlotContent]] = None,
|
||||||
|
escape_slots_content: bool = True,
|
||||||
|
) -> str:
|
||||||
|
# Allow to provide no args/kwargs
|
||||||
|
args = args or []
|
||||||
|
kwargs = kwargs or {}
|
||||||
|
|
||||||
|
# Allow to provide no Context, so we can render component just with args + kwargs
|
||||||
|
context_was_given = True
|
||||||
|
if context is None:
|
||||||
|
context = Context()
|
||||||
|
context_was_given = False
|
||||||
|
|
||||||
|
# Allow to provide a dict instead of Context
|
||||||
|
# NOTE: This if/else is important to avoid nested Contexts,
|
||||||
|
# See https://github.com/EmilStenstrom/django-components/issues/414
|
||||||
|
context = context if isinstance(context, Context) else Context(context)
|
||||||
|
prepare_context(context, self.component_id)
|
||||||
|
|
||||||
# Temporarily populate _context so user can call `self.inject()` from
|
# Temporarily populate _context so user can call `self.inject()` from
|
||||||
# within `get_context_data()`
|
# within `get_context_data()`
|
||||||
self._context = context
|
self._context = context
|
||||||
|
@ -324,114 +422,93 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
self._context = None
|
self._context = None
|
||||||
|
|
||||||
with context.update(context_data):
|
with context.update(context_data):
|
||||||
rendered_component = self.render(
|
template = self.get_template(context)
|
||||||
context=context,
|
_monkeypatch_template(template)
|
||||||
context_data=context_data,
|
if not context_was_given:
|
||||||
|
# Associate the newly-created Context with a Template, otherwise we get
|
||||||
|
# an error when we try to use `{% include %}` tag inside the template?
|
||||||
|
context.template = template
|
||||||
|
context.template_name = template.name
|
||||||
|
|
||||||
|
# Set `Template._is_component_nested` based on whether we're currently INSIDE
|
||||||
|
# the `{% extends %}` tag.
|
||||||
|
# Part of fix for https://github.com/EmilStenstrom/django-components/issues/508
|
||||||
|
template._is_component_nested = bool(context.render_context.get(BLOCK_CONTEXT_KEY))
|
||||||
|
|
||||||
|
# Support passing slots explicitly to `render` method
|
||||||
|
if slots:
|
||||||
|
fill_content = self._fills_from_slots_data(slots, escape_slots_content)
|
||||||
|
else:
|
||||||
|
fill_content = self.fill_content
|
||||||
|
|
||||||
|
# If this is top-level component and it has no parent, use outer context instead
|
||||||
|
slot_context_data = context_data
|
||||||
|
if not context[_PARENT_COMP_CONTEXT_KEY]:
|
||||||
|
slot_context_data = self.outer_context.flatten()
|
||||||
|
|
||||||
|
slots, resolved_fills = resolve_slots(
|
||||||
|
context,
|
||||||
|
template,
|
||||||
|
component_name=self.name,
|
||||||
|
context_data=slot_context_data,
|
||||||
|
fill_content=fill_content,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Available slot fills - this is internal to us
|
||||||
|
updated_slots = {
|
||||||
|
**context.get(_FILLED_SLOTS_CONTENT_CONTEXT_KEY, {}),
|
||||||
|
**resolved_fills,
|
||||||
|
}
|
||||||
|
|
||||||
|
# For users, we expose boolean variables that they may check
|
||||||
|
# to see if given slot was filled, e.g.:
|
||||||
|
# `{% if variable > 8 and component_vars.is_filled.header %}`
|
||||||
|
slot_bools = {slot_fill.escaped_name: slot_fill.is_filled for slot_fill in resolved_fills.values()}
|
||||||
|
|
||||||
|
with context.update(
|
||||||
|
{
|
||||||
|
_ROOT_CTX_CONTEXT_KEY: self.outer_context,
|
||||||
|
_FILLED_SLOTS_CONTENT_CONTEXT_KEY: updated_slots,
|
||||||
|
# NOTE: Public API for variables accessible from within a component's template
|
||||||
|
# See https://github.com/EmilStenstrom/django-components/issues/280#issuecomment-2081180940
|
||||||
|
"component_vars": {
|
||||||
|
"is_filled": slot_bools,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
):
|
||||||
|
rendered_component = template.render(context)
|
||||||
|
|
||||||
if is_dependency_middleware_active():
|
if is_dependency_middleware_active():
|
||||||
output = RENDERED_COMMENT_TEMPLATE.format(name=self.registered_name) + rendered_component
|
output = RENDERED_COMMENT_TEMPLATE.format(name=self.name) + rendered_component
|
||||||
else:
|
else:
|
||||||
output = rendered_component
|
output = rendered_component
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def render(
|
|
||||||
self,
|
|
||||||
context: Union[Dict[str, Any], Context],
|
|
||||||
slots_data: Optional[Dict[SlotName, str]] = None,
|
|
||||||
escape_slots_content: bool = True,
|
|
||||||
context_data: Optional[Dict[str, Any]] = None,
|
|
||||||
) -> str:
|
|
||||||
# NOTE: This if/else is important to avoid nested Contexts,
|
|
||||||
# See https://github.com/EmilStenstrom/django-components/issues/414
|
|
||||||
context = context if isinstance(context, Context) else Context(context)
|
|
||||||
prepare_context(context, self.component_id)
|
|
||||||
|
|
||||||
template = self.get_template(context)
|
|
||||||
_monkeypatch_template(template)
|
|
||||||
|
|
||||||
# Set `Template._is_component_nested` based on whether we're currently INSIDE
|
|
||||||
# the `{% extends %}` tag.
|
|
||||||
# Part of fix for https://github.com/EmilStenstrom/django-components/issues/508
|
|
||||||
template._is_component_nested = bool(context.render_context.get(BLOCK_CONTEXT_KEY))
|
|
||||||
|
|
||||||
# Support passing slots explicitly to `render` method
|
|
||||||
if slots_data:
|
|
||||||
fill_content = self._fills_from_slots_data(slots_data, escape_slots_content)
|
|
||||||
else:
|
|
||||||
fill_content = self.fill_content
|
|
||||||
|
|
||||||
# If this is top-level component and it has no parent, use outer context instead
|
|
||||||
if not context[_PARENT_COMP_CONTEXT_KEY]:
|
|
||||||
context_data = self.outer_context.flatten()
|
|
||||||
if context_data is None:
|
|
||||||
context_data = {}
|
|
||||||
|
|
||||||
slots, resolved_fills = resolve_slots(
|
|
||||||
context,
|
|
||||||
template,
|
|
||||||
component_name=self.registered_name,
|
|
||||||
context_data=context_data,
|
|
||||||
fill_content=fill_content,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Available slot fills - this is internal to us
|
|
||||||
updated_slots = {
|
|
||||||
**context.get(_FILLED_SLOTS_CONTENT_CONTEXT_KEY, {}),
|
|
||||||
**resolved_fills,
|
|
||||||
}
|
|
||||||
|
|
||||||
# For users, we expose boolean variables that they may check
|
|
||||||
# to see if given slot was filled, e.g.:
|
|
||||||
# `{% if variable > 8 and component_vars.is_filled.header %}`
|
|
||||||
slot_bools = {slot_fill.escaped_name: slot_fill.is_filled for slot_fill in resolved_fills.values()}
|
|
||||||
|
|
||||||
with context.update(
|
|
||||||
{
|
|
||||||
_ROOT_CTX_CONTEXT_KEY: self.outer_context,
|
|
||||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY: updated_slots,
|
|
||||||
# NOTE: Public API for variables accessible from within a component's template
|
|
||||||
# See https://github.com/EmilStenstrom/django-components/issues/280#issuecomment-2081180940
|
|
||||||
"component_vars": {
|
|
||||||
"is_filled": slot_bools,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
):
|
|
||||||
return template.render(context)
|
|
||||||
|
|
||||||
def render_to_response(
|
|
||||||
self,
|
|
||||||
context_data: Union[Dict[str, Any], Context],
|
|
||||||
slots_data: Optional[Dict[SlotName, str]] = None,
|
|
||||||
escape_slots_content: bool = True,
|
|
||||||
*args: Any,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> HttpResponse:
|
|
||||||
"""
|
|
||||||
This is the interface for the `django.views.View` class which allows us to
|
|
||||||
use components as Django views with `component.as_view()`.
|
|
||||||
"""
|
|
||||||
return HttpResponse(
|
|
||||||
self.render(context_data, slots_data, escape_slots_content),
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _fills_from_slots_data(
|
def _fills_from_slots_data(
|
||||||
self,
|
self,
|
||||||
slots_data: Dict[SlotName, str],
|
slots_data: Mapping[SlotName, SlotContent],
|
||||||
escape_content: bool = True,
|
escape_content: bool = True,
|
||||||
) -> Dict[SlotName, FillContent]:
|
) -> Dict[SlotName, FillContent]:
|
||||||
"""Fill component slots outside of template rendering."""
|
"""Fill component slots outside of template rendering."""
|
||||||
slot_fills = {
|
slot_fills = {}
|
||||||
slot_name: FillContent(
|
for (slot_name, content) in slots_data.items():
|
||||||
nodes=NodeList([TextNode(escape(content) if escape_content else content)]),
|
if isinstance(content, (str, SafeString)):
|
||||||
|
content_func = nodelist_to_render_func(
|
||||||
|
NodeList([
|
||||||
|
TextNode(escape(content) if escape_content else content)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
def content_func(ctx: Context) -> RenderedContent:
|
||||||
|
rendered = content(ctx)
|
||||||
|
return escape(rendered) if escape_content else rendered
|
||||||
|
|
||||||
|
slot_fills[slot_name] = FillContent(
|
||||||
|
content_func=content_func,
|
||||||
slot_default_var=None,
|
slot_default_var=None,
|
||||||
slot_data_var=None,
|
slot_data_var=None,
|
||||||
)
|
)
|
||||||
for (slot_name, content) in slots_data.items()
|
|
||||||
}
|
|
||||||
return slot_fills
|
return slot_fills
|
||||||
|
|
||||||
|
|
||||||
|
@ -477,7 +554,11 @@ class ComponentNode(Node):
|
||||||
is_default_slot = len(self.fill_nodes) == 1 and self.fill_nodes[0].is_implicit
|
is_default_slot = len(self.fill_nodes) == 1 and self.fill_nodes[0].is_implicit
|
||||||
if is_default_slot:
|
if is_default_slot:
|
||||||
fill_content: Dict[str, FillContent] = {
|
fill_content: Dict[str, FillContent] = {
|
||||||
DEFAULT_SLOT_KEY: FillContent(self.fill_nodes[0].nodelist, None, None),
|
DEFAULT_SLOT_KEY: FillContent(
|
||||||
|
content_func=nodelist_to_render_func(self.fill_nodes[0].nodelist),
|
||||||
|
slot_data_var=None,
|
||||||
|
slot_default_var=None,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
fill_content = {}
|
fill_content = {}
|
||||||
|
@ -494,7 +575,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(
|
||||||
nodes=fill_node.nodelist,
|
content_func=nodelist_to_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,
|
||||||
)
|
)
|
||||||
|
@ -510,7 +591,11 @@ class ComponentNode(Node):
|
||||||
if self.isolated_context:
|
if self.isolated_context:
|
||||||
context = make_isolated_context_copy(context)
|
context = make_isolated_context_copy(context)
|
||||||
|
|
||||||
output = component.render_from_input(context, resolved_context_args, resolved_context_kwargs)
|
output = component._render(
|
||||||
|
context=context,
|
||||||
|
args=resolved_context_args,
|
||||||
|
kwargs=resolved_context_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
trace_msg("RENDR", "COMP", self.name_fexp, self.component_id, "...Done!")
|
trace_msg("RENDR", "COMP", self.name_fexp, self.component_id, "...Done!")
|
||||||
return output
|
return output
|
||||||
|
|
|
@ -20,7 +20,7 @@ class ComponentRegistry:
|
||||||
|
|
||||||
def register(self, name: str, component: Type["component.Component"]) -> None:
|
def register(self, name: str, component: Type["component.Component"]) -> None:
|
||||||
existing_component = self._registry.get(name)
|
existing_component = self._registry.get(name)
|
||||||
if existing_component and existing_component.class_hash != component.class_hash:
|
if existing_component and existing_component._class_hash != component._class_hash:
|
||||||
raise AlreadyRegistered('The component "%s" has already been registered' % name)
|
raise AlreadyRegistered('The component "%s" has already been registered' % name)
|
||||||
self._registry[name] = component
|
self._registry[name] = component
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
from typing import Callable, List, NamedTuple, Optional
|
from typing import Callable, List, NamedTuple, Optional, Union
|
||||||
|
|
||||||
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
|
from typing import Any, 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,7 +14,13 @@ 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 NodeTraverse, nodelist_has_content, walk_nodelist
|
from django_components.node import (
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -26,6 +32,7 @@ SlotId = str
|
||||||
SlotName = str
|
SlotName = str
|
||||||
SlotDefaultName = str
|
SlotDefaultName = str
|
||||||
SlotDataName = str
|
SlotDataName = str
|
||||||
|
SlotContent = Union[str, SafeString, RenderFunc]
|
||||||
|
|
||||||
|
|
||||||
class FillContent(NamedTuple):
|
class FillContent(NamedTuple):
|
||||||
|
@ -43,7 +50,7 @@ class FillContent(NamedTuple):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
nodes: NodeList
|
content_func: RenderFunc
|
||||||
slot_default_var: Optional[SlotDefaultName]
|
slot_default_var: Optional[SlotDefaultName]
|
||||||
slot_data_var: Optional[SlotDataName]
|
slot_data_var: Optional[SlotDataName]
|
||||||
|
|
||||||
|
@ -78,7 +85,7 @@ class SlotFill(NamedTuple):
|
||||||
name: str
|
name: str
|
||||||
escaped_name: str
|
escaped_name: str
|
||||||
is_filled: bool
|
is_filled: bool
|
||||||
nodelist: NodeList
|
content_func: RenderFunc
|
||||||
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]
|
||||||
|
@ -134,7 +141,7 @@ class SlotNode(Node):
|
||||||
|
|
||||||
def render(self, context: Context) -> SafeString:
|
def render(self, context: Context) -> SafeString:
|
||||||
trace_msg("RENDR", "SLOT", self.name, self.node_id)
|
trace_msg("RENDR", "SLOT", self.name, self.node_id)
|
||||||
slots: dict[SlotId, "SlotFill"] = context[_FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
slots: Dict[SlotId, "SlotFill"] = context[_FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
||||||
# NOTE: Slot entry MUST be present. If it's missing, there was an issue upstream.
|
# NOTE: Slot entry MUST be present. If it's missing, there was an issue upstream.
|
||||||
slot_fill = slots[self.node_id]
|
slot_fill = slots[self.node_id]
|
||||||
|
|
||||||
|
@ -167,7 +174,8 @@ class SlotNode(Node):
|
||||||
# came from (or current context if configured so)
|
# came from (or current context if configured so)
|
||||||
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):
|
||||||
output = slot_fill.nodelist.render(used_ctx)
|
# Render slot as a function
|
||||||
|
output = slot_fill.content_func(used_ctx)
|
||||||
|
|
||||||
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
|
||||||
|
@ -368,7 +376,7 @@ def resolve_slots(
|
||||||
name=name,
|
name=name,
|
||||||
escaped_name=_escape_slot_name(name),
|
escaped_name=_escape_slot_name(name),
|
||||||
is_filled=True,
|
is_filled=True,
|
||||||
nodelist=fill.nodes,
|
content_func=fill.content_func,
|
||||||
context_data=context_data,
|
context_data=context_data,
|
||||||
slot_default_var=fill.slot_default_var,
|
slot_default_var=fill.slot_default_var,
|
||||||
slot_data_var=fill.slot_data_var,
|
slot_data_var=fill.slot_data_var,
|
||||||
|
@ -457,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,
|
||||||
nodelist=slot.nodelist,
|
content_func=nodelist_to_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,
|
||||||
|
@ -505,7 +513,7 @@ def _resolve_default_slot(
|
||||||
# `NamedTuple._replace`, because `_replace` is not typed.
|
# `NamedTuple._replace`, because `_replace` is not typed.
|
||||||
named_fills[slot.name] = SlotFill(
|
named_fills[slot.name] = SlotFill(
|
||||||
is_filled=default_fill.is_filled,
|
is_filled=default_fill.is_filled,
|
||||||
nodelist=default_fill.nodelist,
|
content_func=default_fill.content_func,
|
||||||
context_data=default_fill.context_data,
|
context_data=default_fill.context_data,
|
||||||
slot_default_var=default_fill.slot_default_var,
|
slot_default_var=default_fill.slot_default_var,
|
||||||
slot_data_var=default_fill.slot_data_var,
|
slot_data_var=default_fill.slot_data_var,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue