slots - Django-Components" > slots - Django-Components" >
Skip to content

slots ¤

FillContent dataclass ¤

FillContent(content_func: SlotFunc[TSlotData], slot_default_var: Optional[SlotDefaultName], slot_data_var: Optional[SlotDataName])

Bases: Generic[TSlotData]

This represents content set with the {% fill %} tag, e.g.:

{% component "my_comp" %}
    {% fill "first_slot" %} <--- This
        hi
        {{ my_var }}
        hello
    {% endfill %}
{% endcomponent %}

FillNode ¤

FillNode(nodelist: NodeList, kwargs: RuntimeKwargs, trace_id: str, node_id: Optional[str] = None, is_implicit: bool = False)

Bases: BaseNode

Set when a component tag pair is passed template content that excludes fill tags. Nodes of this type contribute their nodelists to slots marked as 'default'.

Source code in src/django_components/slots.py
def __init__(
    self,
    nodelist: NodeList,
    kwargs: RuntimeKwargs,
    trace_id: str,
    node_id: Optional[str] = None,
    is_implicit: bool = False,
):
    super().__init__(nodelist=nodelist, args=None, kwargs=kwargs, node_id=node_id)

    self.is_implicit = is_implicit
    self.trace_id = trace_id
    self.component_id: Optional[str] = None

Slot ¤

Bases: NamedTuple

This represents content set with the {% slot %} tag, e.g.:

{% slot "my_comp" default %} <--- This
    hi
    {{ my_var }}
    hello
{% endslot %}

SlotFill dataclass ¤

SlotFill(
    name: str,
    escaped_name: str,
    is_filled: bool,
    content_func: SlotFunc[TSlotData],
    slot_default_var: Optional[SlotDefaultName],
    slot_data_var: Optional[SlotDataName],
)

Bases: Generic[TSlotData]

SlotFill describes what WILL be rendered.

It is a Slot that has been resolved against FillContents passed to a Component.

SlotNode ¤

SlotNode(
    nodelist: NodeList,
    trace_id: str,
    node_id: Optional[str] = None,
    kwargs: Optional[RuntimeKwargs] = None,
    is_required: bool = False,
    is_default: bool = False,
)

Bases: BaseNode

Source code in src/django_components/slots.py
def __init__(
    self,
    nodelist: NodeList,
    trace_id: str,
    node_id: Optional[str] = None,
    kwargs: Optional[RuntimeKwargs] = None,
    is_required: bool = False,
    is_default: bool = False,
):
    super().__init__(nodelist=nodelist, args=None, kwargs=kwargs, node_id=node_id)

    self.is_required = is_required
    self.is_default = is_default
    self.trace_id = trace_id

SlotRef ¤

SlotRef(slot: SlotNode, context: Context)

SlotRef allows to treat a slot as a variable. The slot is rendered only once the instance is coerced to string.

This is used to access slots as variables inside the templates. When a SlotRef is rendered in the template with {{ my_lazy_slot }}, it will output the contents of the slot.

Source code in src/django_components/slots.py
def __init__(self, slot: "SlotNode", context: Context):
    self._slot = slot
    self._context = context

parse_slot_fill_nodes_from_component_nodelist ¤

parse_slot_fill_nodes_from_component_nodelist(nodes: Tuple[Node, ...], ignored_nodes: Tuple[Type[Node]]) -> List[FillNode]

Given a component body (django.template.NodeList), find all slot fills, whether defined explicitly with {% fill %} or implicitly.

So if we have a component body:

{% component "mycomponent" %}
    {% fill "first_fill" %}
        Hello!
    {% endfill %}
    {% fill "second_fill" %}
        Hello too!
    {% endfill %}
{% endcomponent %}
Then this function returns the nodes (django.template.Node) for fill "first_fill" and fill "second_fill".

Source code in src/django_components/slots.py
@lazy_cache(lambda: lru_cache(maxsize=app_settings.TEMPLATE_CACHE_SIZE))
def parse_slot_fill_nodes_from_component_nodelist(
    nodes: Tuple[Node, ...],
    ignored_nodes: Tuple[Type[Node]],
) -> List[FillNode]:
    """
    Given a component body (`django.template.NodeList`), find all slot fills,
    whether defined explicitly with `{% fill %}` or implicitly.

    So if we have a component body:
    ```django
    {% component "mycomponent" %}
        {% fill "first_fill" %}
            Hello!
        {% endfill %}
        {% fill "second_fill" %}
            Hello too!
        {% endfill %}
    {% endcomponent %}
    ```
    Then this function returns the nodes (`django.template.Node`) for `fill "first_fill"`
    and `fill "second_fill"`.
    """
    fill_nodes: List[FillNode] = []
    if nodelist_has_content(nodes):
        for parse_fn in (
            _try_parse_as_default_fill,
            _try_parse_as_named_fill_tag_set,
        ):
            curr_fill_nodes = parse_fn(nodes, ignored_nodes)
            if curr_fill_nodes:
                fill_nodes = curr_fill_nodes
                break
        else:
            raise TemplateSyntaxError(
                "Illegal content passed to 'component' tag pair. "
                "Possible causes: 1) Explicit 'fill' tags cannot occur alongside other "
                "tags except comment tags; 2) Default (default slot-targeting) content "
                "is mixed with explict 'fill' tags."
            )
    return fill_nodes

resolve_slots ¤

resolve_slots(
    context: Context,
    template: Template,
    component_name: Optional[str],
    fill_content: Dict[SlotName, FillContent],
    is_dynamic_component: bool = False,
) -> Tuple[Dict[SlotId, Slot], Dict[SlotId, SlotFill]]

Search the template for all SlotNodes, and associate the slots with the given fills.

Returns tuple of: - Slots defined in the component's Template with {% slot %} tag - SlotFills (AKA slots matched with fills) describing what will be rendered for each slot.

Source code in src/django_components/slots.py
def resolve_slots(
    context: Context,
    template: Template,
    component_name: Optional[str],
    fill_content: Dict[SlotName, FillContent],
    is_dynamic_component: bool = False,
) -> Tuple[Dict[SlotId, Slot], Dict[SlotId, SlotFill]]:
    """
    Search the template for all SlotNodes, and associate the slots
    with the given fills.

    Returns tuple of:
    - Slots defined in the component's Template with `{% slot %}` tag
    - SlotFills (AKA slots matched with fills) describing what will be rendered for each slot.
    """
    slot_fills = {
        name: SlotFill(
            name=name,
            escaped_name=_escape_slot_name(name),
            is_filled=True,
            content_func=fill.content_func,
            slot_default_var=fill.slot_default_var,
            slot_data_var=fill.slot_data_var,
        )
        for name, fill in fill_content.items()
    }

    slots: Dict[SlotId, Slot] = {}
    # This holds info on which slot (key) has which slots nested in it (value list)
    slot_children: Dict[SlotId, List[SlotId]] = {}
    all_nested_slots: Set[SlotId] = set()

    def on_node(entry: NodeTraverse) -> None:
        node = entry.node
        if not isinstance(node, SlotNode):
            return

        slot_name, _ = node.resolve_kwargs(context, component_name)

        # 1. Collect slots
        # Basically we take all the important info form the SlotNode, so the logic is
        # less coupled to Django's Template/Node. Plain tuples should also help with
        # troubleshooting.
        slot = Slot(
            id=node.node_id,
            name=slot_name,
            nodelist=node.nodelist,
            is_default=node.is_default,
            is_required=node.is_required,
        )
        slots[node.node_id] = slot

        # 2. Figure out which Slots are nested in other Slots, so we can render
        # them from outside-inwards, so we can skip inner Slots if fills are provided.
        # We should end up with a graph-like data like:
        # - 0001: [0002]
        # - 0002: []
        # - 0003: [0004]
        # In other words, the data tells us that slot ID 0001 is PARENT of slot 0002.
        parent_slot_entry = entry.parent
        while parent_slot_entry is not None:
            if not isinstance(parent_slot_entry.node, SlotNode):
                parent_slot_entry = parent_slot_entry.parent
                continue

            parent_slot_id = parent_slot_entry.node.node_id
            if parent_slot_id not in slot_children:
                slot_children[parent_slot_id] = []
            slot_children[parent_slot_id].append(node.node_id)
            all_nested_slots.add(node.node_id)
            break

    walk_nodelist(template.nodelist, on_node, context)

    # 3. Figure out which slot the default/implicit fill belongs to
    slot_fills = _resolve_default_slot(
        template_name=template.name,
        component_name=component_name,
        slots=slots,
        slot_fills=slot_fills,
        is_dynamic_component=is_dynamic_component,
    )

    # 4. Detect any errors with slots/fills
    # NOTE: We ignore errors for the dynamic component, as the underlying component
    # will deal with it
    if not is_dynamic_component:
        _report_slot_errors(slots, slot_fills, component_name)

    # 5. Find roots of the slot relationships
    top_level_slot_ids: List[SlotId] = [node_id for node_id in slots.keys() if node_id not in all_nested_slots]

    # 6. Walk from out-most slots inwards, and decide whether and how
    # we will render each slot.
    resolved_slots: Dict[SlotId, SlotFill] = {}
    slot_ids_queue = deque([*top_level_slot_ids])
    while len(slot_ids_queue):
        slot_id = slot_ids_queue.pop()
        slot = slots[slot_id]

        # Check if there is a slot fill for given slot name
        if slot.name in slot_fills:
            # If yes, we remember which slot we want to replace with already-rendered fills
            resolved_slots[slot_id] = slot_fills[slot.name]
            # Since the fill cannot include other slots, we can leave this path
            continue
        else:
            # If no, then the slot is NOT filled, and we will render the slot's default (what's
            # between the slot tags)
            resolved_slots[slot_id] = SlotFill(
                name=slot.name,
                escaped_name=_escape_slot_name(slot.name),
                is_filled=False,
                content_func=_nodelist_to_slot_render_func(slot.nodelist),
                slot_default_var=None,
                slot_data_var=None,
            )
            # Since the slot's default CAN include other slots (because it's defined in
            # the same template), we need to enqueue the slot's children
            if slot_id in slot_children and slot_children[slot_id]:
                slot_ids_queue.extend(slot_children[slot_id])

    # By the time we get here, we should know, for each slot, how it will be rendered
    # -> Whether it will be replaced with a fill, or whether we render slot's defaults.
    return slots, resolved_slots