mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 06:18:17 +00:00
refactor: fix context vars missing in isolated slot (#455)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
e4e787b29d
commit
ae22eff8af
3 changed files with 94 additions and 11 deletions
|
@ -263,7 +263,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
# 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, outer_context=self.outer_context or Context())
|
||||
prepare_context(context, self.outer_context or Context(), component_id=self.component_id)
|
||||
template = self.get_template(context)
|
||||
|
||||
# Associate the slots with this component for this context
|
||||
|
|
|
@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Optional
|
|||
|
||||
from django.template import Context
|
||||
|
||||
from django_components.app_settings import SlotContextBehavior, app_settings
|
||||
from django_components.logger import trace_msg
|
||||
from django_components.utils import find_last_index
|
||||
|
||||
|
@ -19,11 +20,17 @@ if TYPE_CHECKING:
|
|||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY = "_DJANGO_COMPONENTS_FILLED_SLOTS"
|
||||
_OUTER_ROOT_CTX_CONTEXT_KEY = "_DJANGO_COMPONENTS_OUTER_ROOT_CTX"
|
||||
_SLOT_COMPONENT_ASSOC_KEY = "_DJANGO_COMPONENTS_SLOT_COMP_ASSOC"
|
||||
_PARENT_COMP_KEY = "_DJANGO_COMPONENTS_PARENT_COMP"
|
||||
_CURRENT_COMP_KEY = "_DJANGO_COMPONENTS_CURRENT_COMP"
|
||||
|
||||
|
||||
def prepare_context(context: Context, outer_context: Optional[Context]) -> None:
|
||||
def prepare_context(
|
||||
context: Context,
|
||||
outer_context: Optional[Context],
|
||||
component_id: str,
|
||||
) -> None:
|
||||
"""Initialize the internal context state."""
|
||||
# This is supposed to run ALWAYS at Component.render
|
||||
# This is supposed to run ALWAYS at `Component.render()`
|
||||
if outer_context is not None:
|
||||
set_outer_root_context(context, outer_context)
|
||||
|
||||
|
@ -47,6 +54,8 @@ def prepare_context(context: Context, outer_context: Optional[Context]) -> None:
|
|||
if "forloop" in context:
|
||||
context.dicts[-1][_SLOT_COMPONENT_ASSOC_KEY] = context[_SLOT_COMPONENT_ASSOC_KEY].copy()
|
||||
|
||||
set_component_id(context, component_id)
|
||||
|
||||
|
||||
def make_isolated_context_copy(context: Context) -> Context:
|
||||
# Even if contexts are isolated, we still need to pass down the
|
||||
|
@ -61,9 +70,23 @@ def make_isolated_context_copy(context: Context) -> Context:
|
|||
set_outer_root_context(context_copy, root_ctx)
|
||||
copy_forloop_context(context, context_copy)
|
||||
|
||||
context_copy[_CURRENT_COMP_KEY] = context.get(_CURRENT_COMP_KEY, None)
|
||||
context_copy[_PARENT_COMP_KEY] = context.get(_PARENT_COMP_KEY, None)
|
||||
|
||||
return context_copy
|
||||
|
||||
|
||||
def set_component_id(context: Context, component_id: str) -> None:
|
||||
"""
|
||||
We use the Context object to pass down info on inside of which component
|
||||
we are currently rendering.
|
||||
"""
|
||||
# Store the previous component so we can detect if the current component
|
||||
# is the top-most or not. If it is, then "_parent_component_id" is None
|
||||
context[_PARENT_COMP_KEY] = context.get(_CURRENT_COMP_KEY, None)
|
||||
context[_CURRENT_COMP_KEY] = component_id
|
||||
|
||||
|
||||
def get_slot_fill(context: Context, component_id: str, slot_name: str) -> Optional["FillContent"]:
|
||||
"""
|
||||
Use this function to obtain a slot fill from the current context.
|
||||
|
@ -110,15 +133,31 @@ def set_outer_root_context(context: Context, outer_ctx: Optional[Context]) -> No
|
|||
We pass through this context to allow to configure how slot fills should be
|
||||
rendered using the `SLOT_CONTEXT_BEHAVIOR` setting.
|
||||
"""
|
||||
if outer_ctx and len(outer_ctx.dicts) > 1:
|
||||
outer_root_context: Context = outer_ctx.new()
|
||||
# NOTE_1:
|
||||
# - Index 0 are the defaults set in BaseContext
|
||||
# - Index 1 is the context generated by `Component.get_context_data`
|
||||
# of the parent's component
|
||||
# - All later indices (2, 3, ...) are extra layers added by the rendering
|
||||
# logic (each Node usually adds it's own context layer)
|
||||
# Special case for handling outer context of top-level components when
|
||||
# slots are isolated. In such case, the entire outer context is to be the
|
||||
# outer root ctx.
|
||||
if (
|
||||
outer_ctx
|
||||
and not context.get(_PARENT_COMP_KEY)
|
||||
and app_settings.SLOT_CONTEXT_BEHAVIOR == SlotContextBehavior.ISOLATED
|
||||
and _OUTER_ROOT_CTX_CONTEXT_KEY in context # <-- Added to avoid breaking tests
|
||||
):
|
||||
outer_root_context = outer_ctx.new()
|
||||
outer_root_context.push(outer_ctx.flatten())
|
||||
|
||||
# In nested components, the context generated from `get_context_data`
|
||||
# is found at index 1.
|
||||
# NOTE:
|
||||
# - Index 0 are the defaults set in BaseContext
|
||||
# - Index 1 is the context generated by `Component.get_context_data`
|
||||
# of the parent's component
|
||||
# - All later indices (2, 3, ...) are extra layers added by the rendering
|
||||
# logic (each Node usually adds it's own context layer)
|
||||
elif outer_ctx and len(outer_ctx.dicts) > 1:
|
||||
outer_root_context = outer_ctx.new()
|
||||
outer_root_context.push(outer_ctx.dicts[1])
|
||||
|
||||
# Fallback
|
||||
else:
|
||||
outer_root_context = Context()
|
||||
|
||||
|
|
|
@ -259,6 +259,50 @@ class ComponentTest(BaseTestCase):
|
|||
""",
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
COMPONENTS={
|
||||
"context_behavior": "isolated",
|
||||
"slot_context_behavior": "isolated",
|
||||
},
|
||||
)
|
||||
def test_slots_of_top_level_comps_can_access_full_outer_ctx(self):
|
||||
class SlottedComponent(component.Component):
|
||||
template_name = "template_with_default_slot.html"
|
||||
|
||||
def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": name,
|
||||
}
|
||||
|
||||
component.registry.register("test", SlottedComponent)
|
||||
|
||||
self.template = Template(
|
||||
"""
|
||||
{% load component_tags %}
|
||||
<body>
|
||||
{% component "test" %}
|
||||
ABC: {{ name }}
|
||||
{% endcomponent %}
|
||||
</body>
|
||||
"""
|
||||
)
|
||||
|
||||
nested_ctx = Context()
|
||||
nested_ctx.push({"some": "var"}) # <-- Nested comp's take data only from this layer
|
||||
nested_ctx.push({"name": "carl"}) # <-- But for top-level comp, it should access this layer too
|
||||
rendered = self.template.render(nested_ctx)
|
||||
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<body>
|
||||
<div>
|
||||
<main> ABC: carl </main>
|
||||
</div>
|
||||
</body>
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
class InlineComponentTest(BaseTestCase):
|
||||
def test_inline_html_component(self):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue