ViewComponent, started at GitHub, is a popular open-source project. The goal for it is to break down complex views and improve performance in Rails applications
Slots are one of the most powerful features. From a simple text-output to rendering (multiple other) ViewComponent’s. Slots can do it! Let’s go over how they work.
What are slots in ViewComponent?
ViewComponent Slots are a powerful feature designed to make components more flexible and reusable by allowing them to accept and render nested content or even other components. Introduced as an improvement in the “Slots V2” API, slots are now a default feature of ViewComponent and one I highly recommend you start to use (if you haven’t already). Slots are used extensively in Rails Designer too.
Let’s go over the various types of slots. Yes, slots are versatile. From rendering just some content, to rendering other components to even polymorphic slots.
Slots can be defined in two ways:
renders_one
renders_many
And they do exactly what you think they do. renders_one
allows you to render (and define) only one slot (think an avatar in a UserProfileComponent
). renders_many
can render many slots (think related links in an ArticleComponent
). All the different kinds of slots, described below, work for both renders-one and -many.
Render content
The most basic version of defining a slot is as follows:
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar
end
<%# user_profile_component.html.erb %>
<header>
<%= avatar %>
<h1><%= @user.name %></h1>
</header>
Then when you render the component in a view you can define the avatar slot as follows:
<%= render UserProfileComponent.new do |component| %>
<% component.with_avatar do %>
<%= image_tag("path/to/avatar/of/sorts.jpg", alt: "") %>
<% end %>
<% end %>
Little aside: it takes any block, so you could also write <% component.with_avatar { image_tag("path/to/avatar/of/sorts.jpg", alt: "") } %>
.
Render another component
There are two ways to render another component through a slot: inside of the component class or as another file. The way they work are similar, but are set up slightly different.
Render another component inline
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar, "AvatarComponent"
class AvatarComponent < ViewComponent::Base
def call
tag.img src: "path/to/avatar/of/sorts.jpg", alt: ""
end
end
end
Notice how the quotes around AvatarComponent
? Adding the component as a string is how ViewComponent assumes the other component is nested inside the component.
Render another component from another file
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar, AvatarComponent
end
# avatar_component.rb
class AvatarComponent < ViewComponent::Base
def call
tag.img src: "path/to/avatar/of/sorts.jpg", alt: ""
end
end
Here the class name (AvatarComponent
) is referenced, letting ViewComponent to look for the class in another file.
Render lambda slot
Alright, now this sounds scary! But a lambda is nothing more like a mini-function that you can create on-the-fly without giving it a name. Still afraid? An example probably helps:
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar, ->(src: nil, alt: alt, css: "w-6 h-6 rounded-full") do
content_tag(:img, src: src, alt: alt, class: css)
end
end
Then when rendering it:
<%= render UserProfileComponent.new do |component| %>
<% component.with_avatar(src: "path/to/avatar/of/sorts.jpg", alt: "My Profile picture") %>
<% end %>
For simple elements like this one, this will work just fine. But you can also call another component. Using both inline and external components as described above.
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar, ->(src: nil, alt: nil, css: "w-6 h-6 rounded-full") do
AvatarComponent.new(src: src, alt: alt, css: css)
end
end
This example assumes a AvatarComponent
to be defined that takes three attributes src
, alt
and an optional css
.
Render polymorphic components
Polymorphic slots has been a fairly new addition to ViewComponent (since 2.12.0). I personally haven’t found too much use for them, but still grabbed for them a few times. Let’s look at a, bit of contrived, example:
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar, types: {
icon: ->(css: "w-4 h-4 rounded-full") do
tag.span @user.first.name, class: css
end,
image: ->(css: "w-4 h-4 rounded-full") do
image_tag @user.avatar, class: css
end
}
end
In your view you can then do this:
<%= render UserProfileComponent.new(user: Current.user) do |component| %>
<% if Current.user.avatar.attached? %>
<% component.with_image_avatar %>
<% else %>
<% component.with_icon_avatar %>
<% end %>
<% end %>
The polymorphic slots are called with the type
name in them, eg. with_image_avatar
and with_icon_avatar
.
Predicate slot_name?
methods
Say what? Another fancy term, I know. But all it is, is the slot name appended with a question mark. Assume the same user profile component from above.
Let’s also assume you need to wrap the user’s avatar in a parent element. And maybe the avatar slot is optional.
You can then, in your template, wrap the avatar HTML like so to only display it if it’s defined:
<%# user_profile_component.html.erb %>
<header>
<% if avatar? %>
<div class="mr-4">
<%= avatar %>
</div>
<% end %>
<h1><%= @user.name %></h1>
</header>
And that’s all there is to know about slots in ViewComponent. The many options might make it sometimes tricky to know which kind of slot to go for, but experimenting—quickly trying an option and move to the next—is the fastest way to get comfortable with the many options around slots.
Have questions around if, when or how to use for a specific example? Feel free to reach out.