DEV Community

Cover image for Guide to Slots in Rails' ViewComponent
Rails Designer
Rails Designer

Posted on • Updated on • Originally published at railsdesigner.com

Guide to Slots in Rails' ViewComponent

This article was previously published at Rails Designer.


ViewComponent, started at GitHub, is a popular open-source project. Since its release, it has garnered significant community support, with hundreds of contributors and over a thousand commits. 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?

Slots are a powerful feature of ViewComponent 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 UserProfileComponent). renders_many can render many slots (think related links in an ArticleComponent). All 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
Enter fullscreen mode Exit fullscreen mode
<%# user_profile_component.html.erb %>
<header>
  <%= avatar %>
  <h1><%= @user.name %></h1>
</header>
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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 slightly different set up.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# avatar_component.rb
class AvatarComponent < ViewComponent::Base
  def call
    tag.img src: "path/to/avatar/of/sorts.jpg", alt: ""
  end
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then when rendering it:

<%= render UserProfileComponent.new do |component| %>
  <⁠% component.with_avatar(src: "path/to/avatar/of/sorts.jpg", alt: "My Profile picture") %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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 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>
Enter fullscreen mode Exit fullscreen mode

And that's all you need 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.

Top comments (0)