Slots

Slots are placeholders declared by a component that you can fill up with custom content.

In order to declare a slot, you must use the slot function:

slot name, options

Where:

  • name - is the name of the slot.
  • options - a keyword list of options for additional customization.

Supported options

  • required - declares the slot as required. Default is false.
  • props - the list of custom properties that should be passed to the associated slotable content.

Rendering content with <slot>

Slots are similar to properties as they are exposed as part of the component's public API. The main difference is that while properties are passed as attributes, slots are injected inside the component's body.


defmodule Hero do
use Surface.Component

def render(assigns) do
~H"""
<section class="hero is-info">
<div class="hero-body">
<slot/>
</div>
</section>
"""
end
end

Fallback content

Sometimes it’s useful to specify a fallback content that should be rendered when no content is provided for a slot.


<section class="hero is-info">
<div class="hero-body">
<slot>
No content defined!
</slot>
</div>
</section>

By defining any children inside <slot>...</slot>, that content becomes the default content.

No content defined!

defmodule Example do
use Surface.Component

def render(assigns) do
~H"""
<Hero/>
"""
end
end

Declaring slots

Slots defined using <slot/> are automatically registered into the component's metadata. That means you don't have to explicitly declare them using the slot function. However, since a slot is also part of the public API, it's advisable to declare it so you can add proper documentation to it. Additionally, if you want Surface to statically validate required slots and slot props, you also need to declare them.

The following updated version of the Hero component explicitly declares the default slot.


defmodule Hero do
use Surface.Component

@doc "The content of the Hero"
slot default, required: true

def render(assigns) do
~H"""
<section class="hero is-info">
<div class="hero-body">
<slot/>
</div>
</section>
"""
end
end

Now, if the user tries to use a Hero without defining any content, a proper missing required slot "default" error will be raised at compile-time.

Missing required slot

Named slots

In the previous example, we defined a component with a single default slot. But what if you need to define multiple slots? A classical example of such requirement is the Card component. A card usually has three distinct areas, a header, a footer and the main content.

In order to create a component that can represent a card, we need to use named slots. Let's take a look at how it works.

A simple card component

This example demonstrates how to create components with multiple slots. It defines a default slot to hold the card's content and two named slots: header and footer.

defmodule Example do
use Surface.Component

def render(assigns) do
~H"""
<Card>
<template slot="header">
A simple card component
</template>

This example demonstrates how to create components with multiple slots.
It defines a <strong>default</strong> slot to hold the card's content
and two <strong>named slots</strong>: header and footer.

<template slot="footer">
<a href="#" class="card-footer-item">Footer Item 1</a>
<a href="#" class="card-footer-item">Footer Item 2</a>
</template>
</Card>
"""
end
end

And finally our Card component defining all three slots:


defmodule Card do
use Surface.Component

@doc "The header"
slot header

@doc "The footer"
slot footer

@doc "The main content"
slot default

def render(assigns) do
~H"""
<div class="card">
<header class="card-header" style="background-color: #f5f5f5">
<p class="card-header-title">
<slot name="header"/>
</p>
</header>
<div class="card-content">
<div class="content">
<slot/>
</div>
</div>
<footer class="card-footer" style="background-color: #f5f5f5">
<slot name="footer"/>
</footer>
</div>
"""
end
end

Pay attention that defining a <slot/> without a name is the same as defining it as <slot name="default"/>.

Typed slotables

Instead of using <template slot="...">, you might want to define a custom component to hold the slot's content. In our case, we can define a <Footer> and a <Header> component, setting the :slot option as the name of the slot in the parent card.


defmodule Header do
use Surface.Component, slot: "header"
end

defmodule Footer do
use Surface.Component, slot: "footer"
end

To use them, we don't have to change anything in the Card component. We just need to replace each <template> with the appropriate Footer or Header component.

A simple card component

This is the same Card component but now we're using typed slotables instead of templates.

<Card>
<Header>
A simple card component
</Header>

This is the same Card component but now we're using
<strong>typed slotables</strong> instead of <strong>templates</strong>.

<Footer>
<a href="#" class="card-footer-item">Footer Item 1</a>
<a href="#" class="card-footer-item">Footer Item 2</a>
</Footer>
</Card>

Slot props

There are cases when it's necessary to pass information from the child's scope to the corresponding slot content that is being injected by the parent. Using slot props, Surface gives you an extra layer of encapsulation as it allows you to expose only the pieces of data that the parent needs, keeping everything else in the child's scope private to the parent.

Imagine you're developing a new component that you need to show some ratings. It should provide predefined buttons to increment/decrement its value but you want to make the rendering of the value itself customizable so you can, let's say, show it as a number in one page and as a list of stars in another. You also want to define a property for the max value.

Let's see the code:


defmodule Rating do
use Surface.LiveComponent

@doc "The maximum value"
prop max, :integer, default: 5

@doc "The content"
slot default, props: [:value, :max]

data value, :integer, default: 1

def render(assigns) do
~H"""
<div>
<p>
<slot :props={{ value: @value, max: @max }}/>
</p>
<div style="padding-top: 10px;">
<button class="button is-info" :on-click="dec" disabled={{ @value == 1 }}>
-
</button>
<button class="button is-info" :on-click="inc" disabled={{ @value == @max }}>
+
</button>
</div>
</div>
"""
end

def handle_event("inc", _, socket) do
{:noreply, update(socket, :value, & &1 + 1)}
end

def handle_event("dec", _, socket) do
{:noreply, update(socket, :value, & &1 - 1)}
end
end

Now let's create two instances of our Rating component, each one rendering its value differently.

Rating: 1



defmodule Example do
use Surface.Component

def render(assigns) do
~H"""
<div>
<Rating :let={{ value: value }} id="rating_1">
<h1 class="title is-marginless">
Rating: {{ value }}
</h1>
</Rating>
<hr>
<Rating :let={{ value: value, max: max }} id="rating_2">
<div>
<span :for={{ i <- 1..max }} class={{ :icon, "has-text-warning": i <= value }}>
<i class="fas fa-star"></i>
</span>
</div>
</Rating>
</div>
"""
end
end

Renderless components

There are cases when you don't need to render any of the children of a specific component. You just want to use them as a list of values that can be retrieved so you can provide a more declarative way to configure that component.

Imagine you want to define a Grid component but instead of defining a property to pass the columns definitions, you want to extract that information directly from the component's body. In order to achieve that, you can define a Column component and use the :slot option to inform that any instance will be bound to a parent slot.

By doing that, the component will no longer be rendered automatically. The list of children belonging to the same slot will be grouped and become available to the parent as an assign. The parent then decides what should be done with each individual group (slot).

Here's an example:

Name Artist Released
The Dark Side of the Moon Pink Floyd March 1, 1973
OK Computer Radiohead June 16, 1997
Disraeli Gears Cream November 2, 1967
Physical Graffiti Led Zeppelin February 24, 1975

defmodule Example do
use Surface.LiveComponent

data albums, :list, default: []

def mount(socket) do
{:ok, assign(socket, albums: [
%{name: "The Dark Side of the Moon", artist: "Pink Floyd", released: "March 1, 1973"},
%{name: "OK Computer", artist: "Radiohead", released: "June 16, 1997"},
%{name: "Disraeli Gears", artist: "Cream", released: "November 2, 1967", selected: true},
%{name: "Physical Graffiti", artist: "Led Zeppelin", released: "February 24, 1975"}
])}
end

def render(assigns) do
~H"""
<div>
<Grid items={{ @albums }}>
<Column field="name" />
<Column field="artist" />
<Column field="released" />
</Grid>
</div>
"""
end
end

Here are the Grid and Column components:


defmodule Column do
use Surface.Component, slot: "cols"

@doc "The field to be rendered"
prop field, :string
end

defmodule Grid do
use Surface.Component

@doc "The list of items to be rendered"
prop items, :list, required: true

@doc "The list of columns defining the Grid"
slot cols

def render(assigns) do
~H"""
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th :for={{ col <- @cols }}>
{{ Phoenix.Naming.humanize(col.field) }}
</th>
</tr>
</thead>
<tbody>
<tr :for={{ item <- @items }} class={{ "has-text-warning": item[:selected] }}>
<td :for={{ col <- @cols, field = String.to_atom(col.field) }}>
{{ item[field] }}
</td>
</tr>
</tbody>
</table>
"""
end
end

By defining a named slot cols, we instruct Surface to create a new assign named @cols that will hold a list containing all children that belong to the slot cols.

Note: As you can see, the Column component does not render anything. It just holds the provided values for its properties. All the rendering is done by the parent Grid.

Binding slot props to generators

Imagine that instead of passing the field related to the column, you want to define some markup that should be rendered for each column. This would give us much more flexibility to render the items. Here's an example of what we could do.

Title Artist
The Dark Side of the Moon (Released: March 1, 1973) Pink Floyd
OK Computer (Released: June 16, 1997) Radiohead
Disraeli Gears (Released: November 2, 1967) Cream
Physical Graffiti (Released: February 24, 1975) Led Zeppelin

def render(assigns) do
~H"""
<div>
<Grid items={{ album <- @albums }}>
<Column title="Title">
{{ album.name}} (Released: <strong>{{ album.released }}</strong>)
</Column>
<Column title="Artist">
<a href="#">{{ album.artist }}</a>
</Column>
</Grid>
</div>
"""
end

Notice that we're not passing a regular list to the property items anymore, instead, we are passing a generator that defines a variable called album. That variable will hold the value of each item in the list that will be passed back to the column's scope by the parent Grid.

Note: Currently, Surface only support generators defining a single variable. Optional filter expressions are also supported. The concept of generators and filters is the same used by comprehensions in Elixir. For more information, see the section Generators and filters in the Elixir official documentation.

Here's the updated version of the Column and Grid components:


defmodule Column do
use Surface.Component, slot: "cols"

@doc "The title of the column"
prop title, :string, required: true
end

defmodule Grid do
use Surface.Component

@doc "The list of items to be rendered"
prop items, :list, required: true

@doc "The list of columns defining the Grid"
slot cols, props: [item: ^items]

def render(assigns) do
~H"""
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th :for={{ col <- @cols }}>
{{ col.title }}
</th>
</tr>
</thead>
<tbody>
<tr :for={{ item <- @items }} class={{ "is-selected": item[:selected] }}>
<td :for.index={{ @cols }}>
<slot name="cols" index={{ index }} :props={{ item: item }}/>
</td>
</tr>
</tbody>
</table>
"""
end
end

Let's take a closer look at two important changes we made in our Grid:

  1. slot cols, props: [item: ^items] - The cols slot now declares a slot prop item but instead of just defining the name of the prop (as we did for our Rating component), we bound the value of that prop to each value (item) produced by the generator items.

  2. <slot name="cols" index={{ index }} :props={{ item: item }}/> - Here, we use <slot> to render each column's content passing the current item.