Skip to content

Adding slots

Our calendar component's looking great! But we just got a new assignment from our colleague - The calendar date needs to be shown on 3 different pages:

  1. On one page, it needs to be shown as is
  2. On the second, the date needs to be bold
  3. On the third, the date needs to be in italics

As a reminder, this is what the component's template looks like:

<div class="calendar">
  Today's date is <span>{{ date }}</span>
</div>

There's many ways we could approach this:

  • Expose the date in a slot
  • Style .calendar > span differently on different pages
  • Pass a variable to the component that decides how the date is rendered
  • Create a new component

First two options are more flexible, because the custom styling is not baked into a component's implementation. And for the sake of demonstration, we'll solve this challenge with slots.

1. What are slots¤

Components support something called Slots.

When a component is used inside another template, slots allow the parent template to override specific parts of the child component by passing in different content.

This mechanism makes components more reusable and composable.

This behavior is similar to slots in Vue.

In the example below we introduce two tags that work hand in hand to make this work. These are...

  • {% slot <name> %}/{% endslot %}: Declares a new slot in the component template.
  • {% fill <name> %}/{% endfill %}: (Used inside a {% component %} tag pair.) Fills a declared slot with the specified content.

2. Add a slot tag¤

Let's update our calendar component to support more customization. We'll add {% slot %} tag to the template:

<div class="calendar">
  Today's date is
  {% slot "date" default %}  {# <--- new #}
    <span>{{ date }}</span>
  {% endslot %}
</div>

Notice that:

  1. We named the slot date - so we can fill this slot by using {% fill "date" %}

  2. We also made it the default slot.

  3. We placed our original implementation inside the {% slot %} tag - this is what will be rendered when the slot is NOT overriden.

3. Add fill tag¤

Now we can use {% fill %} tags inside the {% component %} tags to override the date slot to generate the bold and italics variants:

{# Default #}
{% component "calendar" date="2024-12-13" / %}

{# Bold #}
{% component "calendar" date="2024-12-13" %}
  <b> 2024-12-13 </b>
{% endcomponent %}

{# Italics #}
{% component "calendar" date="2024-12-13" %}
  <i> 2024-12-13 </i>
{% endcomponent %}

Which will render as:

<!-- Default -->
<div class="calendar">
  Today's date is <span>2024-12-13</span>
</div>

<!-- Bold -->
<div class="calendar">
  Today's date is <b>2024-12-13</b>
</div>

<!-- Italics -->
<div class="calendar">
  Today's date is <i>2024-12-13</i>
</div>

Info

Since we used the default flag on {% slot "date" %} inside our calendar component, we can target the date component in multiple ways:

  1. Explicitly by it's name

    {% component "calendar" date="2024-12-13" %}
      {% fill "date" %}
        <i> 2024-12-13 </i>
      {% endfill %}
    {% endcomponent %}
    

  2. Implicitly as the default slot (Omitting the {% fill %} tag)

    {% component "calendar" date="2024-12-13" %}
      <i> 2024-12-13 </i>
    {% endcomponent %}
    

  3. Explicitly as the default slot (Setting fill name to default)

    {% component "calendar" date="2024-12-13" %}
      {% fill "default" %}
        <i> 2024-12-13 </i>
      {% endfill %}
    {% endcomponent %}
    

5. Wait, there's a bug¤

There is a mistake in our code! 2024-12-13 is Friday, so that's fine. But if we updated the to 2024-12-14, which is Saturday, our template from previous step would render this:

<!-- Default -->
<div class="calendar">
  Today's date is <span>2024-12-16</span>
</div>

<!-- Bold -->
<div class="calendar">
  Today's date is <b>2024-12-14</b>
</div>

<!-- Italics -->
<div class="calendar">
  Today's date is <i>2024-12-14</i>
</div>

The first instance rendered 2024-12-16, while the rest rendered 2024-12-14!

Why? Remember that in the get_context_data() method of our Calendar component, we pre-process the date. If the date falls on Saturday or Sunday, we shift it to next Monday:

[project root]/components/calendar/calendar.py
from datetime import date

from django_components import Component, register

# If date is Sat or Sun, shift it to next Mon, so the date is always workweek.
def to_workweek_date(d: date):
    ...

@register("calendar")
class Calendar(Component):
    template_name = "calendar.html"
    ...
    def get_context_data(self, date: date, extra_class: str | None = None):
        workweek_date = to_workweek_date(date)
        return {
            "date": workweek_date,
            "extra_class": extra_class,
        }

And the issue is that in our template, we used the date value that we used as input, which is NOT the same as the date variable used inside Calendar's template.

5. Adding data to slots¤

We want to use the same date variable that's used inside Calendar's template.

Luckily, django-components allows passing data to the slot, also known as Scoped slots.

This consists of two steps:

  1. Pass the date variable to the {% slot %} tag
  2. Access the date variable in the {% fill %} tag by using the special data kwarg

Let's update the Calendar's template:

<div class="calendar">
  Today's date is
  {% slot "date" default date=date %}  {# <--- changed #}
    <span>{{ date }}</span>
  {% endslot %}
</div>

Info

The {% slot %} tag has one special kwarg, name. When you write

{% slot "date" / %}

It's the same as:

{% slot name="date" / %}

Other than the name kwarg, you can pass any extra kwargs to the {% slot %} tag, and these will be exposed as the slot's data.

{% slot name="date" kwarg1=123 kwarg2="text" kwarg3=my_var / %}

6. Accessing slot data in fills¤

Now, on the {% fill %} tags, we can use the data kwarg to specify the variable under which the slot data will be available.

The variable from the data kwarg contains all the extra kwargs passed to the {% slot %} tag.

So if we set data="slot_data", then we can access the date variable under slot_data.date:

{# Default #}
{% component "calendar" date="2024-12-13" / %}

{# Bold #}
{% component "calendar" date="2024-12-13" %}
  {% fill "date" data="slot_data" %}
    <b> {{ slot_data.date }} </b>
  {% endfill %}
{% endcomponent %}

{# Italics #}
{% component "calendar" date="2024-12-13" %}
  {% fill "date" data="slot_data" %}
    <i> {{ slot_data.date }} </i>
  {% endfill %}
{% endcomponent %}

By using the date variable from the slot, we'll render the correct date each time:

<!-- Default -->
<div class="calendar">
  Today's date is <span>2024-12-16</span>
</div>

<!-- Bold -->
<div class="calendar">
  Today's date is <b>2024-12-16</b>
</div>

<!-- Italics -->
<div class="calendar">
  Today's date is <i>2024-12-16</i>
</div>

Info

When to use slots vs variables?

Generally, slots are more flexible - you can access the slot data, even the original slot content. Thus, slots behave more like functions that render content based on their context.

On the other hand, variables are static - the variable you pass to a component is what will be used.

Moreover, slots are treated as part of the template - for example the CSS scoping (work in progress) is applied to the slot content too.