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 isfalse
. -
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.
Fallback content
Sometimes it’s useful to specify a fallback content that should be rendered when no content is provided for a slot.
By defining any children inside <slot>...</slot>
, that content becomes the default content.
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.
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.
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
And finally our Card
component defining all three slots:
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.
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
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:
Now let's create two instances of our Rating
component, each one rendering its
value differently.
Rating: 1
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 |
Here are the Grid
and Column
components:
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 parentGrid
.
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 |
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:
Let's take a closer look at two important changes we made in our Grid
:
-
slot cols, props: [item: ^items]
- Thecols
slot now declares a slot propitem
but instead of just defining the name of the prop (as we did for ourRating
component), we bound the value of that prop to each value (item) produced by the generatoritems
. -
<slot name="cols" index={{ index }} :props={{ item: item }}/>
- Here, we use<slot>
to render each column's content passing the current item.