Skip to content

Slots

django-components has the most extensive slot system of all the popular Python templating engines.

The slot system is based on Vue, and works across both Django templates and Python code.

What are slots?ยค

Components support something called 'slots'.

When you write a component, you define its template. The template will always be the same each time you render the component.

However, sometimes you may want to customize the component slightly to change the content of the component. This is where slots come in.

Slots allow you to insert parts of HTML into the component. This makes components more reusable and composable.

<div class="calendar-component">
    <div class="header">
        {# This is where the component will insert the content #}
        {% slot "header" / %}
    </div>
</div>

Slot anatomyยค

Slots consists of two parts:

  1. {% slot %} tag - Inside your component you decide where you want to insert the content.
  2. {% fill %} tag - In the parent template (outside the component) you decide what content to insert into the slot. It "fills" the slot with the specified content.

Let's look at an example:

First, we define the component template. This component contains two slots, header and body.

<!-- calendar.html -->
<div class="calendar-component">
    <div class="header">
        {% slot "header" %}
            Calendar header
        {% endslot %}
    </div>
    <div class="body">
        {% slot "body" %}
            Today's date is <span>{{ date }}</span>
        {% endslot %}
    </div>
</div>

Next, when using the component, we can insert our own content into the slots. It looks like this:

{% component "calendar" date="2020-06-06" %}
    {% fill "body" %}
        Can you believe it's already
        <span>{{ date }}</span>??
    {% endfill %}
{% endcomponent %}

Since the 'header' fill is unspecified, it's default value is used.

When rendered, notice that:

  • The body is filled with the content we specified,
  • The header is still the default value we defined in the component template.
<div class="calendar-component">
    <div class="header">
        Calendar header
    </div>
    <div class="body">
        Can you believe it's already <span>2020-06-06</span>??
    </div>
</div>

Slots overviewยค

Slot definitionยค

Slots are defined with the {% slot %} tag:

{% slot "name" %}
    Default content
{% endslot %}

Single component can have multiple slots:

{% slot "name" %}
    Default content
{% endslot %}

{% slot "other_name" / %}

And you can even define the same slot in multiple places:

<div>
    {% slot "name" %}
        First content
    {% endslot %}
</div>
<div>
    {% slot "name" %}
        Second content
    {% endslot %}
</div>

Info

If you define the same slot in multiple places, you must mark each slot individually when setting default or required flags, e.g.:

<div class="calendar-component">
    <div class="header">
        {% slot "image" default required %}Image here{% endslot %}
    </div>
    <div class="body">
        {% slot "image" default required %}Image here{% endslot %}
    </div>
</div>

Slot fillingยค

Fill can be defined with the {% fill %} tag:

{% component "calendar" %}
    {% fill "name" %}
        Filled content
    {% endfill %}
    {% fill "other_name" %}
        Filled content
    {% endfill %}
{% endcomponent %}

Or in Python with the slots argument:

Calendar.render(
    slots={
        "name": "Filled content",
        "other_name": "Filled content",
    },
)

Default slotยค

You can make the syntax shorter by marking the slot as default:

{% slot "name" default %}
    Default content
{% endslot %}

This allows you to fill the slot directly in the {% component %} tag, omitting the {% fill %} tag:

{% component "calendar" %}
    Filled content
{% endcomponent %}

To target the default slot in Python, you can use the "default" slot name:

Calendar.render(
    slots={"default": "Filled content"},
)

Accessing default slot in Python

Since the default slot is stored under the slot name default, you can access the default slot in Python under the "default" key:

class MyTable(Component):
    def get_template_data(self, args, kwargs, slots, context):
        default_slot = slots["default"]
        return {
            "default_slot": default_slot,
        }

Warning

Only one {% slot %} can be marked as default. But you can have multiple slots with the same name all marked as default.

If you define multiple different slots as default, this will raise an error.

โŒ Don't do this

{% slot "name" default %}
    Default content
{% endslot %}
{% slot "other_name" default %}
    Default content
{% endslot %}

โœ… Do this instead

{% slot "name" default %}
    Default content
{% endslot %}
{% slot "name" default %}
    Default content
{% endslot %}

Warning

Do NOT combine default fills with explicit named {% fill %} tags.

The following component template will raise an error when rendered:

โŒ Don't do this

{% component "calendar" date="2020-06-06" %}
    {% fill "header" %}Totally new header!{% endfill %}
    Can you believe it's already <span>{{ date }}</span>??
{% endcomponent %}

โœ… Do this instead

{% component "calendar" date="2020-06-06" %}
    {% fill "header" %}Totally new header!{% endfill %}
    {% fill "default" %}
        Can you believe it's already <span>{{ date }}</span>??
    {% endfill %}
{% endcomponent %}

Warning

You cannot double-fill a slot.

That is, if both {% fill "default" %} and {% fill "header" %} point to the same slot, this will raise an error when rendered.

Required slotยค

You can make the slot required by adding the required keyword:

{% slot "name" required %}
    Default content
{% endslot %}

This will raise an error if the slot is not filled.

Access fillsยค

You can access the fills with the {{ component_vars.slots.<name> }} template variable:

{% if component_vars.slots.my_slot %}
    <div>
        {% fill "my_slot" %}
            Filled content
        {% endfill %}
    </div>
{% endif %}

And in Python with the Component.slots property:

class Calendar(Component):
    # `get_template_data` receives the `slots` argument directly
    def get_template_data(self, args, kwargs, slots, context):
        if "my_slot" in slots:
            content = "Filled content"
        else:
            content = "Default content"

        return {
            "my_slot": content,
        }

    # In other methods you can still access the slots with `Component.slots`
    def on_render_before(self, context, template):
        if "my_slot" in self.slots:
            # Do something

Dynamic fillsยค

The slot and fill names can be set as variables. This way you can fill slots dynamically:

{% with "body" as slot_name %}
    {% component "calendar" %}
        {% fill slot_name %}
            Filled content
        {% endfill %}
    {% endcomponent %}
{% endwith %}

You can even use {% if %} and {% for %} tags inside the {% component %} tag to fill slots with more control:

{% component "calendar" %}
    {% if condition %}
        {% fill "name" %}
            Filled content
        {% endfill %}
    {% endif %}

    {% for item in items %}
        {% fill item.name %}
            Item: {{ item.value }}
        {% endfill %}
    {% endfor %}
{% endcomponent %}

You can also use {% with %} or even custom tags to generate the fills dynamically:

{% component "calendar" %}
    {% with item.name as name %}
        {% fill name %}
            Item: {{ item.value }}
        {% endfill %}
    {% endwith %}
{% endcomponent %}

Warning

If you dynamically generate {% fill %} tags, be careful to render text only inside the {% fill %} tags.

Any text rendered outside {% fill %} tags will be considered a default fill and will raise an error if combined with explicit fills. (See Default slot)

Slot dataยค

Sometimes the slots need to access data from the component. Imagine an HTML table component which has a slot to configure how to render the rows. Each row has a different data, so you need to pass the data to the slot.

Similarly to Vue's scoped slots, you can pass data to the slot, and then access it in the fill.

This consists of two steps:

  1. Passing data to {% slot %} tag
  2. Accessing data in {% fill %} tag

The data is passed to the slot as extra keyword arguments. Below we set two extra arguments: first_name and job.

{# Pass data to the slot #}
{% slot "name" first_name="John" job="Developer" %}
    {# Fallback implementation #}
    Name: {{ first_name }}
    Job: {{ job }}
{% endslot %}

Note

name kwarg is already used for slot name, so you cannot pass it as slot data.

To access the slot's data in the fill, use the data keyword. This sets the name of the variable that holds the data in the fill:

{# Access data in the fill #}
{% component "profile" %}
    {% fill "name" data="d" %}
        Hello, my name is <h1>{{ d.first_name }}</h1>
        and I'm a <h2>{{ d.job }}</h2>
    {% endfill %}
{% endcomponent %}

To access the slot data in Python, use the data attribute in slot functions.

def my_slot(ctx):
    return f"""
        Hello, my name is <h1>{ctx.data["first_name"]}</h1>
        and I'm a <h2>{ctx.data["job"]}</h2>
    """

Profile.render(
    slots={
        "name": my_slot,
    },
)

Slot data can be set also when rendering a slot in Python:

slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")

# Render the slot
html = slot({"name": "John"})

Info

To access slot data on a default slot, you have to explictly define the {% fill %} tags with name "default".

{% component "my_comp" %}
    {% fill "default" data="slot_data" %}
        {{ slot_data.input }}
    {% endfill %}
{% endcomponent %}

Warning

You cannot set the data attribute and fallback attribute to the same name. This raises an error:

{% component "my_comp" %}
    {% fill "content" data="slot_var" fallback="slot_var" %}
        {{ slot_var.input }}
    {% endfill %}
{% endcomponent %}

Slot fallbackยค

The content between the {% slot %}..{% endslot %} tags is the fallback content that will be rendered if no fill is given for the slot.

{% slot "name" %}
    Hello, my name is {{ name }}  <!-- Fallback content -->
{% endslot %}

Sometimes you may want to keep the fallback content, but only wrap it in some other content.

To do so, you can access the fallback content via the fallback kwarg. This sets the name of the variable that holds the fallback content in the fill:

{% component "profile" %}
    {% fill "name" fallback="fb" %}
        Original content:
        <div>
            {{ fb }}  <!-- fb = 'Hello, my name...' -->
        </div>
    {% endfill %}
{% endcomponent %}

To access the fallback content in Python, use the fallback attribute in slot functions.

The fallback value is rendered lazily. Coerce the fallback to a string to render it.

def my_slot(ctx):
    # Coerce the fallback to a string
    fallback = str(ctx.fallback)
    return f"Original content: " + fallback

Profile.render(
    slots={
        "name": my_slot,
    },
)

Fallback can be set also when rendering a slot in Python:

slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")

# Render the slot
html = slot({"name": "John"}, fallback="Hello, world!")

Info

To access slot fallback on a default slot, you have to explictly define the {% fill %} tags with name "default".

{% component "my_comp" %}
    {% fill "default" fallback="fallback" %}
        {{ fallback }}
    {% endfill %}
{% endcomponent %}

Warning

You cannot set the data attribute and fallback attribute to the same name. This raises an error:

{% component "my_comp" %}
    {% fill "content" data="slot_var" fallback="slot_var" %}
        {{ slot_var.input }}
    {% endfill %}
{% endcomponent %}

Slot functionsยค

In Python code, slot fills can be defined as strings, functions, or Slot instances that wrap the two. Slot functions have access to slot data, fallback, and context.

def row_slot(ctx):
    if ctx.data["disabled"]:
        return ctx.fallback

    item = ctx.data["item"]
    if ctx.data["type"] == "table":
        return f"<tr><td>{item}</td></tr>"
    else:
        return f"<li>{item}</li>"

Table.render(
    slots={
        "prepend": "Ice cream selection:",
        "append": Slot("ยฉ 2025"),
        "row": row_slot,
        "column_title": Slot(lambda ctx: f"<th>{ctx.data['name']}</th>"),
    },
)

Inside the component, these will all be normalized to Slot instances:

class Table(Component):
    def get_template_data(self, args, kwargs, slots, context):
        assert isinstance(slots["prepend"], Slot)
        assert isinstance(slots["row"], Slot)
        assert isinstance(slots["header"], Slot)
        assert isinstance(slots["footer"], Slot)

You can render Slot instances by simply calling them with data:

class Table(Component):
    def get_template_data(self, args, kwargs, slots, context):
        prepend_slot = slots["prepend"]
        return {
            "prepend": prepend_slot({"item": "ice cream"}),
        }

Filling slots with functionsยค

You can "fill" slots by passing a string or Slot instance directly to the {% fill %} tag:

class Table(Component):
    def get_template_data(self, args, kwargs, slots, context):
        def my_fill(ctx):
            return f"Hello, {ctx.data['name']}!"

        return {
            "my_fill": Slot(my_fill),
        }
{% component "table" %}
    {% fill "name" body=my_fill / %}
{% endcomponent %}

Note

Django automatically executes functions when it comes across them in templates.

Because of this you MUST wrap the function in Slot instance to prevent it from being called.

Read more about Django's do_not_call_in_templates.

Slot classยค

The Slot class is a wrapper around a function that can be used to fill a slot.

from django_components import Component, Slot

def footer(ctx):
    return f"Hello, {ctx.data['name']}!"

Table.render(
    slots={
        "footer": Slot(footer),
    },
)

Slot class can be instantiated with a function or a string:

slot1 = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
slot2 = Slot("Hello, world!")

Warning

Passing a Slot instance to the Slot constructor results in an error:

slot = Slot("Hello")

# Raises an error
slot2 = Slot(slot)

Rendering slotsยค

Python

You can render a Slot instance by simply calling it with data:

slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")

# Render the slot with data
html = slot({"name": "John"})

Optionally you can pass the fallback value to the slot. Fallback should be a string.

html = slot({"name": "John"}, fallback="Hello, world!")

Template

Alternatively, you can pass the Slot instance to the {% fill %} tag:

{% fill "name" body=slot / %}

Slot contextยค

If a slot function is rendered by the {% slot %} tag, you can access the current Context using the context attribute.

class Table(Component):
    template = """
        {% with "abc" as my_var %}
            {% slot "name" %}
                Hello!
            {% endslot %}
        {% endwith %}
    """

def slot_func(ctx):
    return f"Hello, {ctx.context['my_var']}!"

slot = Slot(slot_func)
html = slot()

Warning

While available, try to avoid using the context attribute in slot functions.

Instead, prefer using the data and fallback attributes.

Access to context may be removed in future versions (v2, v3).

Slot metadataยค

When accessing slots from within Component methods, the Slot instances are populated with extra metadata:

These are populated the first time a slot is passed to a component.

So if you pass the same slot through multiple nested components, the metadata will still point to the first component that received the slot.

You can use these for debugging, such as printing out the slot's component name and slot name.

Fill node

Components or extensions can use Slot.fill_node to handle slots differently based on whether the slot was defined in the template with {% fill %} and {% component %} tags, or in the component's Python code.

If the slot was created from a {% fill %} tag, this will be the FillNode instance.

If the slot was a default slot created from a {% component %} tag, this will be the ComponentNode instance.

You can use this to find the Component in whose template the {% fill %} tag was defined:

class MyTable(Component):
    def get_template_data(self, args, kwargs, slots, context):
        footer_slot = slots.get("footer")
        if footer_slot is not None and footer_slot.fill_node is not None:
            owner_component = footer_slot.fill_node.template_component
            # ...

Extra

You can also pass any additional data along with the slot by setting it in Slot.extra:

slot = Slot(
    lambda ctx: f"Hello, {ctx.data['name']}!",
    extra={"foo": "bar"},
)

When you create a slot, you can set any of these fields too:

# Either at slot creation
slot = Slot(
    lambda ctx: f"Hello, {ctx.data['name']}!",
    # Optional
    component_name="table",
    slot_name="name",
    extra={},
)

# Or later
slot.component_name = "table"
slot.slot_name = "name"
slot.extra["foo"] = "bar"

Read more in Pass slot metadata.

Slot contentsยค

Whether you create a slot from a function, a string, or from the {% fill %} tags, the Slot class normalizes its contents to a function.

Use Slot.contents to access the original value that was passed to the Slot constructor.

slot = Slot("Hello!")
print(slot.contents)  # "Hello!"

If the slot was created from a string or from the {% fill %} tags, the contents will be accessible also as a Nodelist under Slot.nodelist.

slot = Slot("Hello!")
print(slot.nodelist)  # <django.template.Nodelist: ['Hello!']>

Escaping slots contentยค

Slots content are automatically escaped by default to prevent XSS attacks.

In other words, it's as if you would be using Django's escape() on the slot contents / result:

from django.utils.html import escape

class Calendar(Component):
    template = """
        <div>
            {% slot "date" default date=date / %}
        </div>
    """

Calendar.render(
    slots={
        "date": escape("<b>Hello</b>"),
    }
)

To disable escaping, you can wrap the slot string or slot result in Django's mark_safe():

Calendar.render(
    slots={
        # string
        "date": mark_safe("<b>Hello</b>"),

        # function
        "date": lambda ctx: mark_safe("<b>Hello</b>"),
    }
)

Info

Read more about Django's format_html and mark_safe.

Examplesยค

Pass through all the slotsยค

You can dynamically pass all slots to a child component. This is similar to passing all slots in Vue:

class MyTable(Component):
    template = """
        <div>
          {% component "child" %}
            {% for slot_name, slot in component_vars.slots.items %}
              {% fill name=slot_name body=slot / %}
            {% endfor %}
          {% endcomponent %}
        </div>
    """

Required and default slotsยค

Since each {% slot %} is tagged with required and default individually, you can have multiple slots with the same name but different conditions.

In this example, we have a component that renders a user avatar - a small circular image with a profile picture or name initials.

If the component is given image_src or name_initials variables, the image slot is optional.

But if neither of those are provided, you MUST fill the image slot.

<div class="avatar">
    {# Image given, so slot is optional #}
    {% if image_src %}
        {% slot "image" default %}
            <img src="{{ image_src }}" />
        {% endslot %}

    {# Image not given, but we can make image from initials, so slot is optional #}    
    {% elif name_initials %}
        {% slot "image" default %}
            <div style="
                border-radius: 25px;
                width: 50px;
                height: 50px;
                background: blue;
            ">
                {{ name_initials }}
            </div>
        {% endslot %}

    {# Neither image nor initials given, so slot is required #}
    {% else %}
        {% slot "image" default required / %}
    {% endif %}
</div>

Dynamic slots in table componentยค

Sometimes you may want to generate slots based on the given input. One example of this is Vuetify's table component, which creates a header and an item slots for each user-defined column.

So if you pass columns named name and age to the table component:

[
    {"key": "name", "title": "Name"},
    {"key": "age", "title": "Age"},
]

Then the component will accept fills named header-name and header-age (among others):

{% fill "header-name" data="data" %}
    <b>{{ data.value }}</b>
{% endfill %}

{% fill "header-age" data="data" %}
    <b>{{ data.value }}</b>
{% endfill %}

In django-components you can achieve the same, simply by using a variable or a template expression instead of a string literal:

<table>
  <tr>
    {% for header in headers %}
      <th>
        {% slot "header-{{ header.key }}" value=header.title %}
          {{ header.title }}
        {% endslot %}
      </th>
    {% endfor %}
  </tr>
</table>

When using the component, you can either set the fill explicitly:

{% component "table" headers=headers items=items %}
    {% fill "header-name" data="data" %}
        <b>{{ data.value }}</b>
    {% endfill %}
{% endcomponent %}

Or also use a variable:

{% component "table" headers=headers items=items %}
    {% fill "header-{{ active_header_name }}" data="data" %}
        <b>{{ data.value }}</b>
    {% endfill %}
{% endcomponent %}

Note

It's better to use literal slot names whenever possible for clarity. The dynamic slot names should be reserved for advanced use only.

Spread operatorยค

Lastly, you can also pass the slot name through the spread operator.

When you define a slot name, it's actually a shortcut for a name keyword argument.

So this:

{% slot "content" / %}

is the same as:

{% slot name="content" / %}

So it's possible to define a name key on a dictionary, and then spread that onto the slot tag:

{# slot_props = {"name": "content"} #}
{% slot ...slot_props / %}

Full example:

class MyTable(Component):
    template = """
        {% slot ...slot_props / %}
    """

    def get_template_data(self, args, kwargs, slots, context):
        return {
            "slot_props": {"name": "content", "extra_field": 123},
        }

Info

This applies for both {% slot %} and {% fill %} tags.

Legacy conditional slotsยค

Since version 0.70, you could check if a slot was filled with

{{ component_vars.is_filled.<name> }}

Since version 0.140, this has been deprecated and superseded with

{% component_vars.slots.<name> %}

The component_vars.is_filled variable is still available, but will be removed in v1.0.

NOTE: component_vars.slots no longer escapes special characters in slot names.

You can use {{ component_vars.is_filled.<name> }} together with Django's {% if / elif / else / endif %} tags to define a block whose contents will be rendered only if the component slot with the corresponding 'name' is filled.

This is what our example looks like with component_vars.is_filled.

<div class="frontmatter-component">
    <div class="title">
        {% slot "title" %}
            Title
        {% endslot %}
    </div>
    {% if component_vars.is_filled.subtitle %}
        <div class="subtitle">
            {% slot "subtitle" %}
                {# Optional subtitle #}
            {% endslot %}
        </div>
    {% elif component_vars.is_filled.title %}
        ...
    {% elif component_vars.is_filled.<name> %}
        ...
    {% endif %}
</div>

Accessing is_filled of slot names with special charactersยค

To be able to access a slot name via component_vars.is_filled, the slot name needs to be composed of only alphanumeric characters and underscores (e.g. this__isvalid_123).

However, you can still define slots with other special characters. In such case, the slot name in component_vars.is_filled is modified to replace all invalid characters into _.

So a slot named "my super-slot :)" will be available as component_vars.is_filled.my_super_slot___.

Same applies when you are accessing is_filled from within the Python, e.g.:

class MyTable(Component):
    def on_render_before(self, context, template) -> None:
        # โœ… Works
        if self.is_filled["my_super_slot___"]:
            # Do something

        # โŒ Does not work
        if self.is_filled["my super-slot :)"]:
            # Do something