mirror of
https://github.com/django-components/django-components.git
synced 2025-08-18 13:10:13 +00:00
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
c0013c0fe4
commit
ff70be35e4
4 changed files with 198 additions and 47 deletions
|
@ -40,7 +40,6 @@ from django_components.component_registry import ComponentRegistry
|
||||||
from django_components.component_registry import registry as registry_
|
from django_components.component_registry import registry as registry_
|
||||||
from django_components.context import (
|
from django_components.context import (
|
||||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY,
|
_FILLED_SLOTS_CONTENT_CONTEXT_KEY,
|
||||||
_PARENT_COMP_CONTEXT_KEY,
|
|
||||||
_REGISTRY_CONTEXT_KEY,
|
_REGISTRY_CONTEXT_KEY,
|
||||||
_ROOT_CTX_CONTEXT_KEY,
|
_ROOT_CTX_CONTEXT_KEY,
|
||||||
get_injected_context_var,
|
get_injected_context_var,
|
||||||
|
@ -630,16 +629,10 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
||||||
else:
|
else:
|
||||||
fill_content = self.fill_content
|
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()
|
|
||||||
|
|
||||||
_, resolved_fills = resolve_slots(
|
_, resolved_fills = resolve_slots(
|
||||||
context,
|
context,
|
||||||
template,
|
template,
|
||||||
component_name=self.name,
|
component_name=self.name,
|
||||||
context_data=slot_context_data,
|
|
||||||
fill_content=fill_content,
|
fill_content=fill_content,
|
||||||
# Dynamic component has a special mark do it doesn't raise certain errors
|
# Dynamic component has a special mark do it doesn't raise certain errors
|
||||||
is_dynamic_component=getattr(self, "_is_dynamic_component", False),
|
is_dynamic_component=getattr(self, "_is_dynamic_component", False),
|
||||||
|
|
|
@ -15,7 +15,6 @@ from django_components.utils import find_last_index
|
||||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY = "_DJANGO_COMPONENTS_FILLED_SLOTS"
|
_FILLED_SLOTS_CONTENT_CONTEXT_KEY = "_DJANGO_COMPONENTS_FILLED_SLOTS"
|
||||||
_ROOT_CTX_CONTEXT_KEY = "_DJANGO_COMPONENTS_ROOT_CTX"
|
_ROOT_CTX_CONTEXT_KEY = "_DJANGO_COMPONENTS_ROOT_CTX"
|
||||||
_REGISTRY_CONTEXT_KEY = "_DJANGO_COMPONENTS_REGISTRY"
|
_REGISTRY_CONTEXT_KEY = "_DJANGO_COMPONENTS_REGISTRY"
|
||||||
_PARENT_COMP_CONTEXT_KEY = "_DJANGO_COMPONENTS_PARENT_COMP"
|
|
||||||
_CURRENT_COMP_CONTEXT_KEY = "_DJANGO_COMPONENTS_CURRENT_COMP"
|
_CURRENT_COMP_CONTEXT_KEY = "_DJANGO_COMPONENTS_CURRENT_COMP"
|
||||||
_INJECT_CONTEXT_KEY_PREFIX = "_DJANGO_COMPONENTS_INJECT__"
|
_INJECT_CONTEXT_KEY_PREFIX = "_DJANGO_COMPONENTS_INJECT__"
|
||||||
|
|
||||||
|
@ -57,9 +56,6 @@ 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 use the Context object to pass down info on inside of which component
|
||||||
we are currently rendering.
|
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_CONTEXT_KEY] = context.get(_CURRENT_COMP_CONTEXT_KEY, None)
|
|
||||||
context[_CURRENT_COMP_CONTEXT_KEY] = component_id
|
context[_CURRENT_COMP_CONTEXT_KEY] = component_id
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,6 @@ class SlotFill(Generic[TSlotData]):
|
||||||
escaped_name: str
|
escaped_name: str
|
||||||
is_filled: bool
|
is_filled: bool
|
||||||
content_func: SlotFunc[TSlotData]
|
content_func: SlotFunc[TSlotData]
|
||||||
context_data: Mapping
|
|
||||||
slot_default_var: Optional[SlotDefaultName]
|
slot_default_var: Optional[SlotDefaultName]
|
||||||
slot_data_var: Optional[SlotDataName]
|
slot_data_var: Optional[SlotDataName]
|
||||||
|
|
||||||
|
@ -479,7 +478,6 @@ def resolve_slots(
|
||||||
context: Context,
|
context: Context,
|
||||||
template: Template,
|
template: Template,
|
||||||
component_name: Optional[str],
|
component_name: Optional[str],
|
||||||
context_data: Mapping[str, Any],
|
|
||||||
fill_content: Dict[SlotName, FillContent],
|
fill_content: Dict[SlotName, FillContent],
|
||||||
is_dynamic_component: bool = False,
|
is_dynamic_component: bool = False,
|
||||||
) -> Tuple[Dict[SlotId, Slot], Dict[SlotId, SlotFill]]:
|
) -> Tuple[Dict[SlotId, Slot], Dict[SlotId, SlotFill]]:
|
||||||
|
@ -497,7 +495,6 @@ def resolve_slots(
|
||||||
escaped_name=_escape_slot_name(name),
|
escaped_name=_escape_slot_name(name),
|
||||||
is_filled=True,
|
is_filled=True,
|
||||||
content_func=fill.content_func,
|
content_func=fill.content_func,
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
@ -507,6 +504,7 @@ def resolve_slots(
|
||||||
slots: Dict[SlotId, Slot] = {}
|
slots: Dict[SlotId, Slot] = {}
|
||||||
# This holds info on which slot (key) has which slots nested in it (value list)
|
# This holds info on which slot (key) has which slots nested in it (value list)
|
||||||
slot_children: Dict[SlotId, List[SlotId]] = {}
|
slot_children: Dict[SlotId, List[SlotId]] = {}
|
||||||
|
all_nested_slots: Set[SlotId] = set()
|
||||||
|
|
||||||
def on_node(entry: NodeTraverse) -> None:
|
def on_node(entry: NodeTraverse) -> None:
|
||||||
node = entry.node
|
node = entry.node
|
||||||
|
@ -535,16 +533,17 @@ def resolve_slots(
|
||||||
# - 0002: []
|
# - 0002: []
|
||||||
# - 0003: [0004]
|
# - 0003: [0004]
|
||||||
# In other words, the data tells us that slot ID 0001 is PARENT of slot 0002.
|
# In other words, the data tells us that slot ID 0001 is PARENT of slot 0002.
|
||||||
curr_entry = entry.parent
|
parent_slot_entry = entry.parent
|
||||||
while curr_entry and curr_entry.parent is not None:
|
while parent_slot_entry is not None:
|
||||||
if not isinstance(curr_entry.node, SlotNode):
|
if not isinstance(parent_slot_entry.node, SlotNode):
|
||||||
curr_entry = curr_entry.parent
|
parent_slot_entry = parent_slot_entry.parent
|
||||||
continue
|
continue
|
||||||
|
|
||||||
parent_slot_id = curr_entry.node.node_id
|
parent_slot_id = parent_slot_entry.node.node_id
|
||||||
if parent_slot_id not in slot_children:
|
if parent_slot_id not in slot_children:
|
||||||
slot_children[parent_slot_id] = []
|
slot_children[parent_slot_id] = []
|
||||||
slot_children[parent_slot_id].append(node.node_id)
|
slot_children[parent_slot_id].append(node.node_id)
|
||||||
|
all_nested_slots.add(node.node_id)
|
||||||
break
|
break
|
||||||
|
|
||||||
walk_nodelist(template.nodelist, on_node, context)
|
walk_nodelist(template.nodelist, on_node, context)
|
||||||
|
@ -565,10 +564,7 @@ def resolve_slots(
|
||||||
_report_slot_errors(slots, slot_fills, component_name)
|
_report_slot_errors(slots, slot_fills, component_name)
|
||||||
|
|
||||||
# 5. Find roots of the slot relationships
|
# 5. Find roots of the slot relationships
|
||||||
top_level_slot_ids: List[SlotId] = []
|
top_level_slot_ids: List[SlotId] = [node_id for node_id in slots.keys() if node_id not in all_nested_slots]
|
||||||
for node_id, slot in slots.items():
|
|
||||||
if node_id not in slot_children or not slot_children[node_id]:
|
|
||||||
top_level_slot_ids.append(node_id)
|
|
||||||
|
|
||||||
# 6. Walk from out-most slots inwards, and decide whether and how
|
# 6. Walk from out-most slots inwards, and decide whether and how
|
||||||
# we will render each slot.
|
# we will render each slot.
|
||||||
|
@ -592,7 +588,6 @@ def resolve_slots(
|
||||||
escaped_name=_escape_slot_name(slot.name),
|
escaped_name=_escape_slot_name(slot.name),
|
||||||
is_filled=False,
|
is_filled=False,
|
||||||
content_func=_nodelist_to_slot_render_func(slot.nodelist),
|
content_func=_nodelist_to_slot_render_func(slot.nodelist),
|
||||||
context_data=context_data,
|
|
||||||
slot_default_var=None,
|
slot_default_var=None,
|
||||||
slot_data_var=None,
|
slot_data_var=None,
|
||||||
)
|
)
|
||||||
|
@ -625,29 +620,30 @@ def _resolve_default_slot(
|
||||||
|
|
||||||
# Check for errors
|
# Check for errors
|
||||||
for slot in slots.values():
|
for slot in slots.values():
|
||||||
if slot.is_default:
|
if not slot.is_default:
|
||||||
if default_slot_encountered:
|
continue
|
||||||
raise TemplateSyntaxError(
|
|
||||||
"Only one component slot may be marked as 'default'. "
|
|
||||||
f"To fix, check template '{template_name}' "
|
|
||||||
f"of component '{component_name}'."
|
|
||||||
)
|
|
||||||
default_slot_encountered = True
|
|
||||||
|
|
||||||
# Here we've identified which slot the default/implicit fill belongs to
|
if default_slot_encountered:
|
||||||
if default_fill:
|
raise TemplateSyntaxError(
|
||||||
# NOTE: We recreate new instance, passing all fields, instead of using
|
"Only one component slot may be marked as 'default'. "
|
||||||
# `NamedTuple._replace`, because `_replace` is not typed.
|
f"To fix, check template '{template_name}' "
|
||||||
named_fills[slot.name] = SlotFill(
|
f"of component '{component_name}'."
|
||||||
is_filled=default_fill.is_filled,
|
)
|
||||||
content_func=default_fill.content_func,
|
default_slot_encountered = True
|
||||||
context_data=default_fill.context_data,
|
|
||||||
slot_default_var=default_fill.slot_default_var,
|
# Here we've identified which slot the default/implicit fill belongs to
|
||||||
slot_data_var=default_fill.slot_data_var,
|
if default_fill:
|
||||||
# Updated fields
|
# NOTE: We recreate new instance, passing all fields, instead of using
|
||||||
name=slot.name,
|
# `NamedTuple._replace`, because `_replace` is not typed.
|
||||||
escaped_name=_escape_slot_name(slot.name),
|
named_fills[slot.name] = SlotFill(
|
||||||
)
|
is_filled=default_fill.is_filled,
|
||||||
|
content_func=default_fill.content_func,
|
||||||
|
slot_default_var=default_fill.slot_default_var,
|
||||||
|
slot_data_var=default_fill.slot_data_var,
|
||||||
|
# Updated fields
|
||||||
|
name=slot.name,
|
||||||
|
escaped_name=_escape_slot_name(slot.name),
|
||||||
|
)
|
||||||
|
|
||||||
# Check: Only component templates that include a 'default' slot
|
# Check: Only component templates that include a 'default' slot
|
||||||
# can be invoked with implicit filling.
|
# can be invoked with implicit filling.
|
||||||
|
@ -725,7 +721,7 @@ def _escape_slot_name(name: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def _nodelist_to_slot_render_func(nodelist: NodeList) -> SlotFunc:
|
def _nodelist_to_slot_render_func(nodelist: NodeList) -> SlotFunc:
|
||||||
def render_func(ctx: Context, kwargs: Dict[str, Any], slot_ref: SlotRef) -> SlotResult:
|
def render_func(ctx: Context, slot_data: Dict[str, Any], slot_ref: SlotRef) -> SlotResult:
|
||||||
return nodelist.render(ctx)
|
return nodelist.render(ctx)
|
||||||
|
|
||||||
return render_func # type: ignore[return-value]
|
return render_func # type: ignore[return-value]
|
||||||
|
|
|
@ -522,6 +522,172 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# See https://github.com/EmilStenstrom/django-components/issues/698
|
||||||
|
class NestedSlotsTests(BaseTestCase):
|
||||||
|
class NestedSlots(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% slot 'wrapper' %}
|
||||||
|
<div>
|
||||||
|
Wrapper Default
|
||||||
|
{% slot 'parent1' %}
|
||||||
|
<div>
|
||||||
|
Parent1 Default
|
||||||
|
{% slot 'child1' %}
|
||||||
|
<div>
|
||||||
|
Child 1 Default
|
||||||
|
</div>
|
||||||
|
{% endslot %}
|
||||||
|
</div>
|
||||||
|
{% endslot %}
|
||||||
|
{% slot 'parent2' %}
|
||||||
|
<div>
|
||||||
|
Parent2 Default
|
||||||
|
</div>
|
||||||
|
{% endslot %}
|
||||||
|
</div>
|
||||||
|
{% endslot %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
registry.register("example", self.NestedSlots)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_empty(self):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'example' %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
rendered = Template(template_str).render(Context())
|
||||||
|
expected = """
|
||||||
|
<div>
|
||||||
|
Wrapper Default
|
||||||
|
<div>
|
||||||
|
Parent1 Default
|
||||||
|
<div>
|
||||||
|
Child 1 Default
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Parent2 Default
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_override_outer(self):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'example' %}
|
||||||
|
{% fill 'wrapper' %}
|
||||||
|
<div>
|
||||||
|
Entire Wrapper Replaced
|
||||||
|
</div>
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
rendered = Template(template_str).render(Context())
|
||||||
|
expected = """
|
||||||
|
<div>
|
||||||
|
Entire Wrapper Replaced
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_override_middle(self):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'example' %}
|
||||||
|
{% fill 'parent1' %}
|
||||||
|
<div>
|
||||||
|
Parent1 Replaced
|
||||||
|
</div>
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
rendered = Template(template_str).render(Context())
|
||||||
|
expected = """
|
||||||
|
<div>
|
||||||
|
Wrapper Default
|
||||||
|
<div>
|
||||||
|
Parent1 Replaced
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Parent2 Default
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_override_inner(self):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'example' %}
|
||||||
|
{% fill 'child1' %}
|
||||||
|
<div>
|
||||||
|
Child1 Replaced
|
||||||
|
</div>
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
rendered = Template(template_str).render(Context())
|
||||||
|
expected = """
|
||||||
|
<div>
|
||||||
|
Wrapper Default
|
||||||
|
<div>
|
||||||
|
Parent1 Default
|
||||||
|
<div>
|
||||||
|
Child1 Replaced
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Parent2 Default
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_override_all(self):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'example' %}
|
||||||
|
{% fill 'child1' %}
|
||||||
|
<div>
|
||||||
|
Child1 Replaced
|
||||||
|
</div>
|
||||||
|
{% endfill %}
|
||||||
|
{% fill 'parent1' %}
|
||||||
|
<div>
|
||||||
|
Parent1 Replaced
|
||||||
|
</div>
|
||||||
|
{% endfill %}
|
||||||
|
{% fill 'wrapper' %}
|
||||||
|
<div>
|
||||||
|
Entire Wrapper Replaced
|
||||||
|
</div>
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
rendered = Template(template_str).render(Context())
|
||||||
|
expected = """
|
||||||
|
<div>
|
||||||
|
Entire Wrapper Replaced
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
|
||||||
class SlottedTemplateRegressionTests(BaseTestCase):
|
class SlottedTemplateRegressionTests(BaseTestCase):
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_slotted_template_that_uses_missing_variable(self):
|
def test_slotted_template_that_uses_missing_variable(self):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue