diff --git a/src/django_components/component.py b/src/django_components/component.py index fdcc80df..9dc2c7c8 100644 --- a/src/django_components/component.py +++ b/src/django_components/component.py @@ -2,7 +2,7 @@ import inspect import os import sys from pathlib import Path -from typing import Any, ClassVar, Dict, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Type, Union +from typing import Any, ClassVar, Dict, List, Mapping, MutableMapping, Optional, Sequence, Tuple, Type, Union from django.core.exceptions import ImproperlyConfigured from django.forms.widgets import Media, MediaDefiningClass @@ -23,12 +23,12 @@ from django_components.component_registry import AlreadyRegistered, ComponentReg from django_components.logger import logger from django_components.middleware import is_dependency_middleware_active from django_components.slots import ( - DefaultFillContent, ImplicitFillNode, - NamedFillContent, NamedFillNode, + FillContent, SlotName, render_component_template_with_slots, + DEFAULT_SLOT_KEY, ) from django_components.utils import search @@ -186,7 +186,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass): self, registered_name: Optional[str] = None, outer_context: Optional[Context] = None, - fill_content: Union[DefaultFillContent, Iterable[NamedFillContent]] = (), # type: ignore + fill_content: dict[str, FillContent] = {}, # type: ignore ): self.registered_name: Optional[str] = registered_name self.outer_context: Context = outer_context or Context() @@ -280,14 +280,13 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass): escape_content: bool = True, ) -> None: """Fill component slots outside of template rendering.""" - self.fill_content = [ - ( - slot_name, + self.fill_content = { + slot_name: FillContent( TextNode(escape(content) if escape_content else content), None, ) for (slot_name, content) in slots_data.items() - ] + } class ComponentNode(Node): @@ -299,20 +298,14 @@ class ComponentNode(Node): context_args: List[FilterExpression], context_kwargs: Mapping[str, FilterExpression], isolated_context: bool = False, - fill_nodes: Union[ImplicitFillNode, Iterable[NamedFillNode]] = (), + fill_nodes: Sequence[Union[ImplicitFillNode, NamedFillNode]] = (), ) -> None: self.name_fexp = name_fexp self.context_args = context_args or [] self.context_kwargs = context_kwargs or {} self.isolated_context = isolated_context self.fill_nodes = fill_nodes - self.nodelist = self._create_nodelist(fill_nodes) - - def _create_nodelist(self, fill_nodes: Union[Iterable[Node], ImplicitFillNode]) -> NodeList: - if isinstance(fill_nodes, ImplicitFillNode): - return NodeList([fill_nodes]) - else: - return NodeList(fill_nodes) + self.nodelist = NodeList(fill_nodes) def __repr__(self) -> str: return "".format( @@ -330,16 +323,18 @@ class ComponentNode(Node): 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()} - if isinstance(self.fill_nodes, ImplicitFillNode): - fill_content = self.fill_nodes.nodelist + if len(self.fill_nodes) == 1 and isinstance(self.fill_nodes[0], ImplicitFillNode): + fill_content: Dict[str, FillContent] = { + DEFAULT_SLOT_KEY: FillContent(self.fill_nodes[0].nodelist, None) + } else: - fill_content = [] + fill_content = {} for fill_node in self.fill_nodes: # Note that outer component context is used to resolve variables in # fill tag. resolved_name = fill_node.name_fexp.resolve(context) resolved_fill_alias = fill_node.resolve_alias(context, resolved_component_name) - fill_content.append((resolved_name, fill_node.nodelist, resolved_fill_alias)) + fill_content[resolved_name] = FillContent(fill_node.nodelist, resolved_fill_alias) component: Component = component_cls( registered_name=resolved_component_name, diff --git a/src/django_components/slots.py b/src/django_components/slots.py index 3a7a3485..c5d4a3ad 100644 --- a/src/django_components/slots.py +++ b/src/django_components/slots.py @@ -1,6 +1,6 @@ import difflib import sys -from typing import Dict, Iterable, List, Optional, Set, Tuple, Type, Union +from typing import Dict, List, NamedTuple, Optional, Sequence, Set, Tuple, Type, Union if sys.version_info[:2] < (3, 9): from typing import ChainMap @@ -19,16 +19,20 @@ from django.template.exceptions import TemplateSyntaxError from django.utils.safestring import SafeString, mark_safe FILLED_SLOTS_CONTENT_CONTEXT_KEY = "_DJANGO_COMPONENTS_FILLED_SLOTS" +DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT" # Type aliases SlotName = str AliasName = str -DefaultFillContent: TypeAlias = NodeList -NamedFillContent = Tuple[SlotName, NodeList, Optional[AliasName]] -FillContent = Tuple[NodeList, Optional[AliasName]] +class FillContent(NamedTuple): + """Data passed from component to slot to render that slot""" + nodes: NodeList + alias: Optional[AliasName] + + FilledSlotsContext = ChainMap[Tuple[SlotName, Template], FillContent] @@ -103,7 +107,7 @@ class SlotNode(Node, TemplateAwareNodeMixin): extra_context = {} try: - slot_fill_content: FillContent = filled_slots_map[(self.name, self.template)] + slot_fill_content = filled_slots_map[(self.name, self.template)] except KeyError: if self.is_required: raise TemplateSyntaxError( @@ -244,7 +248,7 @@ class IfSlotFilledNode(Node): def parse_slot_fill_nodes_from_component_nodelist( component_nodelist: NodeList, ComponentNodeCls: Type[Node], -) -> Union[Iterable[NamedFillNode], ImplicitFillNode]: +) -> Sequence[Union[NamedFillNode, ImplicitFillNode]]: """ Given a component body (`django.template.NodeList`), find all slot fills, whether defined explicitly with `{% fill %}` or implicitly. @@ -263,7 +267,7 @@ def parse_slot_fill_nodes_from_component_nodelist( Then this function returns the nodes (`django.template.Node`) for `fill "first_fill"` and `fill "second_fill"`. """ - fill_nodes: Union[Iterable[NamedFillNode], ImplicitFillNode] = [] + fill_nodes: Sequence[Union[NamedFillNode, ImplicitFillNode]] = [] if _block_has_content(component_nodelist): for parse_fn in ( _try_parse_as_default_fill, @@ -340,7 +344,7 @@ def _block_has_content(nodelist: NodeList) -> bool: def render_component_template_with_slots( template: Template, context: Context, - fill_content: Union[DefaultFillContent, Iterable[NamedFillContent]], + fill_content: Dict[str, FillContent], registered_name: Optional[str], ) -> str: """ @@ -363,16 +367,16 @@ def render_component_template_with_slots( def _prepare_component_template_filled_slot_context( template: Template, - fill_content: Union[DefaultFillContent, Iterable[NamedFillContent]], + fill_content: Dict[str, FillContent], slots_context: Optional[FilledSlotsContext], registered_name: Optional[str], ) -> FilledSlotsContext: - if isinstance(fill_content, NodeList): - default_fill_content = (fill_content, None) - named_fills_content = {} + if DEFAULT_SLOT_KEY in fill_content: + named_fills_content = fill_content.copy() + default_fill_content = named_fills_content.pop(DEFAULT_SLOT_KEY) else: + named_fills_content = fill_content default_fill_content = None - named_fills_content = {name: (nodelist, alias) for name, nodelist, alias in list(fill_content)} # If value is `None`, then slot is unfilled. slot_name2fill_content: Dict[SlotName, Optional[FillContent]] = {}