feat: merge context settings, replace if_filled tag with var

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2024-05-01 20:55:09 +02:00 committed by GitHub
parent 0f3491850b
commit 3fc90e4956
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1394 additions and 838 deletions

View file

@ -20,18 +20,16 @@ from django.views import View
# way the two modules depend on one another.
from django_components.component_registry import registry # NOQA
from django_components.component_registry import AlreadyRegistered, ComponentRegistry, NotRegistered, register # NOQA
from django_components.context import make_isolated_context_copy, prepare_context, set_slot_component_association
from django_components.context import (
_FILLED_SLOTS_CONTENT_CONTEXT_KEY,
_PARENT_COMP_CONTEXT_KEY,
_ROOT_CTX_CONTEXT_KEY,
make_isolated_context_copy,
prepare_context,
)
from django_components.logger import logger, trace_msg
from django_components.middleware import is_dependency_middleware_active
from django_components.node import walk_nodelist
from django_components.slots import (
DEFAULT_SLOT_KEY,
FillContent,
FillNode,
SlotName,
SlotNode,
render_component_template_with_slots,
)
from django_components.slots import DEFAULT_SLOT_KEY, FillContent, FillNode, SlotName, resolve_slots
from django_components.utils import gen_id, search
RENDERED_COMMENT_TEMPLATE = "<!-- _RENDERED {name} -->"
@ -189,11 +187,11 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
registered_name: Optional[str] = None,
component_id: Optional[str] = None,
outer_context: Optional[Context] = None,
fill_content: Dict[str, FillContent] = {},
fill_content: Optional[Dict[str, FillContent]] = None,
):
self.registered_name: Optional[str] = registered_name
self.outer_context: Context = outer_context or Context()
self.fill_content = fill_content
self.fill_content = fill_content or {}
self.component_id = component_id or gen_id()
def __init_subclass__(cls, **kwargs: Any) -> None:
@ -254,34 +252,75 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
f"Note: this attribute is not required if you are overriding the class's `get_template*()` methods."
)
def render_from_input(self, context: Context, args: Union[List, Tuple], kwargs: Dict) -> str:
component_context: dict = self.get_context_data(*args, **kwargs)
with context.update(component_context):
rendered_component = self.render(context, context_data=component_context)
if is_dependency_middleware_active():
output = RENDERED_COMMENT_TEMPLATE.format(name=self.registered_name) + rendered_component
else:
output = rendered_component
return output
def render(
self,
context_data: Union[Dict[str, Any], Context],
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_data if isinstance(context_data, Context) else Context(context_data)
prepare_context(context, component_id=self.component_id, outer_context=self.outer_context or Context())
context = context if isinstance(context, Context) else Context(context)
prepare_context(context, self.component_id)
template = self.get_template(context)
# Associate the slots with this component for this context
# This allows us to look up component-specific slot fills.
def on_node(node: Node) -> None:
if isinstance(node, SlotNode):
trace_msg("ASSOC", "SLOT", node.name, node.node_id, component_id=self.component_id)
set_slot_component_association(context, node.node_id, self.component_id)
walk_nodelist(template.nodelist, on_node)
# Support passing slots explicitly to `render` method
if slots_data:
self._fill_slots(slots_data, escape_slots_content)
fill_content = self._fills_from_slots_data(slots_data, escape_slots_content)
else:
fill_content = self.fill_content
return render_component_template_with_slots(
self.component_id, template, context, self.fill_content, self.registered_name
# 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(
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],
@ -290,25 +329,30 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
*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 _fill_slots(
def _fills_from_slots_data(
self,
slots_data: Dict[SlotName, str],
escape_content: bool = True,
) -> None:
) -> Dict[SlotName, FillContent]:
"""Fill component slots outside of template rendering."""
self.fill_content = {
slot_fills = {
slot_name: FillContent(
nodes=NodeList([TextNode(escape(content) if escape_content else content)]),
alias=None,
)
for (slot_name, content) in slots_data.items()
}
return slot_fills
class ComponentNode(Node):
@ -346,8 +390,8 @@ class ComponentNode(Node):
# Resolve FilterExpressions and Variables that were passed as args to the
# component, then call component's context method
# to get values to insert into the context
resolved_context_args = [safe_resolve(arg, context) for arg in self.context_args]
resolved_context_kwargs = {key: safe_resolve(kwarg, context) for key, kwarg in self.context_kwargs.items()}
resolved_context_args = safe_resolve_list(self.context_args, context)
resolved_context_kwargs = safe_resolve_dict(self.context_kwargs, context)
is_default_slot = len(self.fill_nodes) == 1 and self.fill_nodes[0].is_implicit
if is_default_slot:
@ -368,24 +412,27 @@ class ComponentNode(Node):
component_id=self.component_id,
)
component_context: dict = component.get_context_data(*resolved_context_args, **resolved_context_kwargs)
# Prevent outer context from leaking into the template of the component
if self.isolated_context:
context = make_isolated_context_copy(context)
with context.update(component_context):
rendered_component = component.render(context)
if is_dependency_middleware_active():
output = RENDERED_COMMENT_TEMPLATE.format(name=resolved_component_name) + rendered_component
else:
output = rendered_component
output = component.render_from_input(context, resolved_context_args, resolved_context_kwargs)
trace_msg("RENDR", "COMP", self.name_fexp, self.component_id, "...Done!")
return output
def safe_resolve_list(args: List[FilterExpression], context: Context) -> List:
return [safe_resolve(arg, context) for arg in args]
def safe_resolve_dict(
kwargs: Union[Mapping[str, FilterExpression], Dict[str, FilterExpression]],
context: Context,
) -> Dict:
return {key: safe_resolve(kwarg, context) for key, kwarg in kwargs.items()}
def safe_resolve(context_item: FilterExpression, context: Context) -> Any:
"""Resolve FilterExpressions and Variables in context if possible. Return other items unchanged."""