Skip to content

Commit 27d5119

Browse files
Move slot rendering to BoundComponent and remove Slots abstraction (#138)
1 parent 5edddba commit 27d5119

File tree

7 files changed

+65
-80
lines changed

7 files changed

+65
-80
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Changed
22+
23+
- **Internal**: Refactored slot handling logic by moving slot processing from `BirdNode` to `BoundComponent`.
24+
- **Internal**: Simplified component context management in `BirdNode` by offloading context prep to `BoundComponent`.
25+
26+
### Removed
27+
28+
- **Internal**: Removed standalone `Slots` dataclass abstraction in favor of handling in `BoundComponent`.
29+
30+
### Fixed
31+
32+
- Fixed default slot content handling when using `only` keyword for component context isolation.
33+
2134
## [0.12.0]
2235

2336
### Added

src/django_bird/components.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
from hashlib import md5
66
from pathlib import Path
77
from threading import Lock
8+
from typing import TYPE_CHECKING
89

910
from cachetools import LRUCache
1011
from django.apps import apps
1112
from django.conf import settings
1213
from django.template.backends.django import Template as DjangoTemplate
14+
from django.template.base import Node
15+
from django.template.base import NodeList
16+
from django.template.base import TextNode
1317
from django.template.context import Context
1418
from django.template.engine import Engine
1519
from django.template.exceptions import TemplateDoesNotExist
@@ -19,11 +23,16 @@
1923
from django_bird.params import Params
2024
from django_bird.params import Value
2125
from django_bird.staticfiles import Asset
26+
from django_bird.templatetags.tags.slot import DEFAULT_SLOT
27+
from django_bird.templatetags.tags.slot import SlotNode
2228

2329
from .conf import app_settings
2430
from .staticfiles import AssetType
2531
from .templates import get_template_names
2632

33+
if TYPE_CHECKING:
34+
from django_bird.templatetags.tags.bird import BirdNode
35+
2736

2837
@dataclass(frozen=True, slots=True)
2938
class Component:
@@ -37,9 +46,9 @@ def get_asset(self, asset_filename: str) -> Asset | None:
3746
return asset
3847
return None
3948

40-
def get_bound_component(self, attrs: list[Param]):
41-
params = Params.with_attrs(attrs)
42-
return BoundComponent(component=self, params=params)
49+
def get_bound_component(self, node: BirdNode):
50+
params = Params.with_attrs(node.attrs)
51+
return BoundComponent(component=self, params=params, nodelist=node.nodelist)
4352

4453
@property
4554
def data_attribute_name(self):
@@ -108,6 +117,7 @@ def next(self, component: Component) -> int:
108117
class BoundComponent:
109118
component: Component
110119
params: Params
120+
nodelist: NodeList | None
111121
_sequence: SequenceGenerator = field(default_factory=SequenceGenerator)
112122

113123
def render(self, context: Context):
@@ -123,15 +133,46 @@ def render(self, context: Context):
123133

124134
props = self.params.render_props(self.component.nodelist, context)
125135
attrs = self.params.render_attrs(context)
136+
slots = self.fill_slots(context)
126137

127138
with context.push(
128139
**{
129140
"attrs": attrs,
130141
"props": props,
142+
"slot": slots.get(DEFAULT_SLOT),
143+
"slots": slots,
131144
}
132145
):
133146
return self.component.template.template.render(context)
134147

148+
def fill_slots(self, context: Context):
149+
if self.nodelist is None:
150+
return {
151+
DEFAULT_SLOT: None,
152+
}
153+
154+
slot_nodes = {
155+
node.name: node for node in self.nodelist if isinstance(node, SlotNode)
156+
}
157+
default_nodes = NodeList(
158+
[node for node in self.nodelist if not isinstance(node, SlotNode)]
159+
)
160+
161+
slots: dict[str, Node | NodeList] = {
162+
DEFAULT_SLOT: default_nodes,
163+
**slot_nodes,
164+
}
165+
166+
if context.get("slots"):
167+
for name, content in context["slots"].items():
168+
if name not in slots or not slots.get(name):
169+
slots[name] = TextNode(str(content))
170+
171+
if not slots[DEFAULT_SLOT] and "slot" in context:
172+
slots[DEFAULT_SLOT] = TextNode(context["slot"])
173+
174+
return {name: node.render(context) for name, node in slots.items() if node}
175+
135176
@property
136177
def id(self):
137178
return str(self._sequence.next(self.component))

src/django_bird/slots.py

Lines changed: 0 additions & 46 deletions
This file was deleted.

src/django_bird/templatetags/tags/bird.py

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# pyright: reportAny=false
22
from __future__ import annotations
33

4-
from typing import Any
5-
64
from django import template
75
from django.template.base import NodeList
86
from django.template.base import Parser
@@ -11,11 +9,8 @@
119

1210
from django_bird._typing import TagBits
1311
from django_bird._typing import override
14-
from django_bird.components import Component
1512
from django_bird.components import components
1613
from django_bird.params import Param
17-
from django_bird.slots import DEFAULT_SLOT
18-
from django_bird.slots import Slots
1914

2015
TAG = "bird"
2116
END_TAG = "endbird"
@@ -73,12 +68,11 @@ def __init__(
7368
def render(self, context: Context) -> str:
7469
component_name = self.get_component_name(context)
7570
component = components.get_component(component_name)
76-
component_context = self.get_component_context_data(component, context)
77-
bound_component = component.get_bound_component(attrs=self.attrs)
71+
bound_component = component.get_bound_component(node=self)
7872

7973
if self.isolated_context:
80-
return bound_component.render(context.new(component_context))
81-
with context.push(**component_context):
74+
return bound_component.render(context.new())
75+
else:
8276
return bound_component.render(context)
8377

8478
def get_component_name(self, context: Context) -> str:
@@ -87,14 +81,3 @@ def get_component_name(self, context: Context) -> str:
8781
except template.VariableDoesNotExist:
8882
name = self.name
8983
return name
90-
91-
def get_component_context_data(
92-
self, component: Component, context: Context
93-
) -> dict[str, Any]:
94-
slots = Slots.collect(self.nodelist, context).render()
95-
default_slot = slots.get(DEFAULT_SLOT) or context.get("slot")
96-
97-
return {
98-
"slot": default_slot,
99-
"slots": slots,
100-
}

src/django_bird/templatetags/tags/slot.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
TAG = "bird:slot"
1818
END_TAG = "endbird:slot"
1919

20+
DEFAULT_SLOT = "default"
21+
2022

2123
def do_slot(parser: Parser, token: Token) -> SlotNode:
2224
bits = token.split_contents()
@@ -27,7 +29,7 @@ def do_slot(parser: Parser, token: Token) -> SlotNode:
2729

2830
def parse_slot_name(bits: TagBits) -> str:
2931
if len(bits) == 1:
30-
return "default"
32+
return DEFAULT_SLOT
3133
elif len(bits) == 2:
3234
name = bits[1]
3335
if name.startswith("name="):

tests/templatetags/test_bird.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,11 +1374,11 @@ def test_parent_context_access(test_case, templates_dir, normalize_whitespace):
13741374
),
13751375
template_content="""
13761376
{% bird button only %}
1377-
{% bird:slot prefix %}{{ user.role }}{% endbird:slot %}
1377+
{% bird:slot prefix %}{{ user.role|default:"User" }}{% endbird:slot %}
13781378
{% endbird %}
13791379
""",
13801380
template_context={"user": {"name": "John", "role": "Admin"}},
1381-
expected="<button>Admin Anonymous</button>",
1381+
expected="<button>User Anonymous</button>",
13821382
),
13831383
TestComponentCase(
13841384
description="Only flag with self-closing tag",

tests/test_components.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,6 @@ def test_from_name_basic(self, templates_dir):
3636
assert comp.name == "button"
3737
assert isinstance(comp.template, DjangoTemplate)
3838

39-
bound = comp.get_bound_component([])
40-
41-
assert bound.render(Context({})) == "<button>Click me</button>"
42-
4339
def test_from_name_with_assets(self, templates_dir):
4440
button = TestComponent(
4541
name="button", content="<button>Click me</button>"
@@ -109,10 +105,6 @@ def test_from_name_custom_component_dir(self, templates_dir, override_app_settin
109105
assert comp.name == "button"
110106
assert isinstance(comp.template, DjangoTemplate)
111107

112-
bound = comp.get_bound_component([])
113-
114-
assert bound.render(Context({})) == "<button>Click me</button>"
115-
116108
def test_id_is_consistent(self, templates_dir):
117109
button = TestComponent(
118110
name="button", content="<button>Click me</button>"

0 commit comments

Comments
 (0)