Skip to content

Commit

Permalink
Move slot rendering to BoundComponent and remove Slots abstraction (
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuadavidthomas authored Jan 24, 2025
1 parent 5edddba commit 27d5119
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 80 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/

## [Unreleased]

### Changed

- **Internal**: Refactored slot handling logic by moving slot processing from `BirdNode` to `BoundComponent`.
- **Internal**: Simplified component context management in `BirdNode` by offloading context prep to `BoundComponent`.

### Removed

- **Internal**: Removed standalone `Slots` dataclass abstraction in favor of handling in `BoundComponent`.

### Fixed

- Fixed default slot content handling when using `only` keyword for component context isolation.

## [0.12.0]

### Added
Expand Down
47 changes: 44 additions & 3 deletions src/django_bird/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
from hashlib import md5
from pathlib import Path
from threading import Lock
from typing import TYPE_CHECKING

from cachetools import LRUCache
from django.apps import apps
from django.conf import settings
from django.template.backends.django import Template as DjangoTemplate
from django.template.base import Node
from django.template.base import NodeList
from django.template.base import TextNode
from django.template.context import Context
from django.template.engine import Engine
from django.template.exceptions import TemplateDoesNotExist
Expand All @@ -19,11 +23,16 @@
from django_bird.params import Params
from django_bird.params import Value
from django_bird.staticfiles import Asset
from django_bird.templatetags.tags.slot import DEFAULT_SLOT
from django_bird.templatetags.tags.slot import SlotNode

from .conf import app_settings
from .staticfiles import AssetType
from .templates import get_template_names

if TYPE_CHECKING:
from django_bird.templatetags.tags.bird import BirdNode


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

def get_bound_component(self, attrs: list[Param]):
params = Params.with_attrs(attrs)
return BoundComponent(component=self, params=params)
def get_bound_component(self, node: BirdNode):
params = Params.with_attrs(node.attrs)
return BoundComponent(component=self, params=params, nodelist=node.nodelist)

@property
def data_attribute_name(self):
Expand Down Expand Up @@ -108,6 +117,7 @@ def next(self, component: Component) -> int:
class BoundComponent:
component: Component
params: Params
nodelist: NodeList | None
_sequence: SequenceGenerator = field(default_factory=SequenceGenerator)

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

props = self.params.render_props(self.component.nodelist, context)
attrs = self.params.render_attrs(context)
slots = self.fill_slots(context)

with context.push(
**{
"attrs": attrs,
"props": props,
"slot": slots.get(DEFAULT_SLOT),
"slots": slots,
}
):
return self.component.template.template.render(context)

def fill_slots(self, context: Context):
if self.nodelist is None:
return {
DEFAULT_SLOT: None,
}

slot_nodes = {
node.name: node for node in self.nodelist if isinstance(node, SlotNode)
}
default_nodes = NodeList(
[node for node in self.nodelist if not isinstance(node, SlotNode)]
)

slots: dict[str, Node | NodeList] = {
DEFAULT_SLOT: default_nodes,
**slot_nodes,
}

if context.get("slots"):
for name, content in context["slots"].items():
if name not in slots or not slots.get(name):
slots[name] = TextNode(str(content))

if not slots[DEFAULT_SLOT] and "slot" in context:
slots[DEFAULT_SLOT] = TextNode(context["slot"])

return {name: node.render(context) for name, node in slots.items() if node}

@property
def id(self):
return str(self._sequence.next(self.component))
Expand Down
46 changes: 0 additions & 46 deletions src/django_bird/slots.py

This file was deleted.

23 changes: 3 additions & 20 deletions src/django_bird/templatetags/tags/bird.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# pyright: reportAny=false
from __future__ import annotations

from typing import Any

from django import template
from django.template.base import NodeList
from django.template.base import Parser
Expand All @@ -11,11 +9,8 @@

from django_bird._typing import TagBits
from django_bird._typing import override
from django_bird.components import Component
from django_bird.components import components
from django_bird.params import Param
from django_bird.slots import DEFAULT_SLOT
from django_bird.slots import Slots

TAG = "bird"
END_TAG = "endbird"
Expand Down Expand Up @@ -73,12 +68,11 @@ def __init__(
def render(self, context: Context) -> str:
component_name = self.get_component_name(context)
component = components.get_component(component_name)
component_context = self.get_component_context_data(component, context)
bound_component = component.get_bound_component(attrs=self.attrs)
bound_component = component.get_bound_component(node=self)

if self.isolated_context:
return bound_component.render(context.new(component_context))
with context.push(**component_context):
return bound_component.render(context.new())
else:
return bound_component.render(context)

def get_component_name(self, context: Context) -> str:
Expand All @@ -87,14 +81,3 @@ def get_component_name(self, context: Context) -> str:
except template.VariableDoesNotExist:
name = self.name
return name

def get_component_context_data(
self, component: Component, context: Context
) -> dict[str, Any]:
slots = Slots.collect(self.nodelist, context).render()
default_slot = slots.get(DEFAULT_SLOT) or context.get("slot")

return {
"slot": default_slot,
"slots": slots,
}
4 changes: 3 additions & 1 deletion src/django_bird/templatetags/tags/slot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
TAG = "bird:slot"
END_TAG = "endbird:slot"

DEFAULT_SLOT = "default"


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

def parse_slot_name(bits: TagBits) -> str:
if len(bits) == 1:
return "default"
return DEFAULT_SLOT
elif len(bits) == 2:
name = bits[1]
if name.startswith("name="):
Expand Down
4 changes: 2 additions & 2 deletions tests/templatetags/test_bird.py
Original file line number Diff line number Diff line change
Expand Up @@ -1374,11 +1374,11 @@ def test_parent_context_access(test_case, templates_dir, normalize_whitespace):
),
template_content="""
{% bird button only %}
{% bird:slot prefix %}{{ user.role }}{% endbird:slot %}
{% bird:slot prefix %}{{ user.role|default:"User" }}{% endbird:slot %}
{% endbird %}
""",
template_context={"user": {"name": "John", "role": "Admin"}},
expected="<button>Admin Anonymous</button>",
expected="<button>User Anonymous</button>",
),
TestComponentCase(
description="Only flag with self-closing tag",
Expand Down
8 changes: 0 additions & 8 deletions tests/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ def test_from_name_basic(self, templates_dir):
assert comp.name == "button"
assert isinstance(comp.template, DjangoTemplate)

bound = comp.get_bound_component([])

assert bound.render(Context({})) == "<button>Click me</button>"

def test_from_name_with_assets(self, templates_dir):
button = TestComponent(
name="button", content="<button>Click me</button>"
Expand Down Expand Up @@ -109,10 +105,6 @@ def test_from_name_custom_component_dir(self, templates_dir, override_app_settin
assert comp.name == "button"
assert isinstance(comp.template, DjangoTemplate)

bound = comp.get_bound_component([])

assert bound.render(Context({})) == "<button>Click me</button>"

def test_id_is_consistent(self, templates_dir):
button = TestComponent(
name="button", content="<button>Click me</button>"
Expand Down

0 comments on commit 27d5119

Please sign in to comment.