diff --git a/src/django_components/slots.py b/src/django_components/slots.py index f75ae263..8b989333 100644 --- a/src/django_components/slots.py +++ b/src/django_components/slots.py @@ -87,18 +87,20 @@ class ComponentIdMixin: self._component_id = value -class SlotNode(Node, ComponentIdMixin): +class SlotNode(Node): def __init__( self, name: str, nodelist: NodeList, is_required: bool = False, is_default: bool = False, + node_id: Optional[str] = None, ): self.name = name self.nodelist = nodelist self.is_required = is_required self.is_default = is_default + self.node_id = node_id or gen_id() @property def active_flags(self) -> List[str]: @@ -167,7 +169,9 @@ class FillNode(Node, ComponentIdMixin): name_fexp: FilterExpression, alias_fexp: Optional[FilterExpression] = None, is_implicit: bool = False, + node_id: Optional[str] = None, ): + self.node_id = node_id or gen_id() self.nodelist = nodelist self.name_fexp = name_fexp self.alias_fexp = alias_fexp @@ -214,9 +218,11 @@ class IfSlotFilledConditionBranchNode(_IfSlotFilledBranchNode, ComponentIdMixin) slot_name: str, nodelist: NodeList, is_positive: Union[bool, None] = True, + node_id: Optional[str] = None, ) -> None: self.slot_name = slot_name self.is_positive: Optional[bool] = is_positive + self.node_id = node_id or gen_id() super().__init__(nodelist) def evaluate(self, context: Context) -> bool: diff --git a/src/django_components/templatetags/component_tags.py b/src/django_components/templatetags/component_tags.py index d28621b9..aeb9d729 100644 --- a/src/django_components/templatetags/component_tags.py +++ b/src/django_components/templatetags/component_tags.py @@ -146,15 +146,22 @@ def do_slot(parser: Parser, token: Token) -> SlotNode: "Order of options is free." ) + # Use a unique ID to be able to tie the fill nodes with components and slots + # NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering + slot_id = gen_id() + nodelist = parser.parse(parse_until=["endslot"]) parser.delete_first_token() - return SlotNode( + slot_node = SlotNode( slot_name, nodelist, is_required=is_required, is_default=is_default, + node_id=slot_id, ) + return slot_node + @register.tag("fill") def do_fill(parser: Parser, token: Token) -> FillNode: @@ -180,15 +187,23 @@ def do_fill(parser: Parser, token: Token) -> FillNode: alias_fexp = FilterExpression(alias, parser) else: raise TemplateSyntaxError(f"'{tag}' tag takes either 1 or 3 arguments: Received {len(args)}.") + + # Use a unique ID to be able to tie the fill nodes with components and slots + # NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering + fill_id = gen_id() + nodelist = parser.parse(parse_until=["endfill"]) parser.delete_first_token() - return FillNode( + fill_node = FillNode( nodelist, name_fexp=FilterExpression(tgt_slot_name, tag), alias_fexp=alias_fexp, + node_id=fill_id, ) + return fill_node + @register.tag(name="component") def do_component(parser: Parser, token: Token) -> ComponentNode: @@ -208,14 +223,15 @@ def do_component(parser: Parser, token: Token) -> ComponentNode: bits = token.split_contents() bits, isolated_context = check_for_isolated_context_keyword(bits) component_name, context_args, context_kwargs = parse_component_with_args(parser, bits, "component") + + # Use a unique ID to be able to tie the fill nodes with components and slots + # NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering + component_id = gen_id() + body: NodeList = parser.parse(parse_until=["endcomponent"]) parser.delete_first_token() fill_nodes = parse_slot_fill_nodes_from_component_nodelist(body, ComponentNode) - # Use a unique ID to be able to tie the fill nodes with this specific component - # and its slots - component_id = gen_id() - # Tag all fill nodes as children of this particular component instance for node in fill_nodes: node.component_id = component_id