Link Search Menu Expand Document

Slots

In addition to the content accessor, ViewComponents can accept content through slots. Think of slots as a way to render multiple blocks of content, including other components.

Slots are defined with renders_one and renders_many:

  • renders_one defines a slot that will be rendered at most once per component: renders_one :header
  • renders_many defines a slot that can be rendered multiple times per-component: renders_many :posts

If a second argument isn’t provided to these methods, a passthrough slot is registered. Any content passed through can be rendered inside these slots without restriction.

For example:

# blog_component.rb
class BlogComponent < ViewComponent::Base
  renders_one :header
  renders_many :posts
end

To render a renders_one slot, call the name of the slot.

To render a renders_many slot, iterate over the name of the slot:

<%# blog_component.html.erb %>
<h1><%= header %></h1>

<% posts.each do |post| %>
  <%= post %>
<% end %>
<%# index.html.erb %>
<%= render BlogComponent.new do |c| %>
  <% c.header do %>
    <%= link_to "My blog", root_path %>
  <% end %>

  <% BlogPost.all.each do |blog_post| %>
    <% c.post do %>
      <%= link_to blog_post.name, blog_post.url %>
    <% end %>
  <% end %>
<% end %>

Returning:

<h1><a href="/">My blog</a></h1>

<a href="/blog/first-post">First post</a>
<a href="/blog/second-post">Second post</a>

Component slots

Slots can also render other components. Pass the name of a component as the second argument to define a component slot.

Arguments passed when calling a component slot will be used to initialize the component and render it. A block can also be passed to set the component’s content.

# blog_component.rb
class BlogComponent < ViewComponent::Base
  # Since `HeaderComponent` is nested inside of this component, we've to
  # reference it as a string instead of a class name.
  renders_one :header, "HeaderComponent"

  # `PostComponent` is defined in another file, so we can refer to it by class name.
  renders_many :posts, PostComponent

  class HeaderComponent < ViewComponent::Base
    attr_reader :classes

    def initialize(classes:)
      @classes = classes
    end

    def call
      content_tag :h1, content, { class: classes }
    end
  end
end
<%# blog_component.html.erb %>
<%= header %>

<% posts.each do |post| %>
  <%= post %>
<% end %>
<%# index.html.erb %>
<%= render BlogComponent.new do |c| %>
  <% c.header(classes: "") do %>
    <%= link_to "My Site", root_path %>
  <% end %>

  <% c.post(title: "My blog post") do %>
    Really interesting stuff.
  <% end %>

  <% c.post(title: "Another post!") do %>
    Blog every day.
  <% end %>
<% end %>

Lambda slots

It’s also possible to define a slot as a lambda that returns content to be rendered (either a string or a ViewComponent instance). Lambda slots are useful in cases where writing another component may be unnecessary, such as working with helpers like content_tag or as wrappers for another ViewComponent with specific default values:

class BlogComponent < ViewComponent::Base
  renders_one :header, -> (classes:) do
    # This isn't complex enough to be its own component yet, so we'll use a
    # lambda slot. If it gets much bigger, it should be extracted out to a
    # ViewComponent and rendered here with a component slot.
    content_tag :h1 do
      link_to title, root_path, { class: classes }
    end
  end

  # It's also possible to return another ViewComponent with preset default values:
  renders_many :posts, -> (title:, classes:) do
    PostComponent.new(title: title, classes: "my-default-class " + classes)
  end
end

Lambda slots are able to access state from the parent ViewComponent:

class TableComponent < ViewComponent::Base
  renders_one :header, -> do
    HeaderComponent.new(selectable: @selectable)
  end

  def initialize(selectable: false)
    @selectable = selectable
  end
end

Rendering collections

renders_many slots can also be passed a collection, using the plural setter (links in this example):

# navigation_component.rb
class NavigationComponent < ViewComponent::Base
  renders_many :links, "LinkComponent"

  class LinkComponent < ViewComponent::Base
    def initialize(name:, href:)
      @name = name
      @href = href
    end
  end
end
<%# navigation_component.html.erb %>
<% links.each do |link| %>
  <%= link %>
<% end %>
<%# index.html.erb %>
<%= render(NavigationComponent.new) do |c| %>
  <% c.links([
    { name: "Home", href: "/" },
    { name: "Pricing", href: "/pricing" },
    { name: "Sign Up", href: "/sign-up" },
  ]) %>
<% end %>

#with_content

Slot content can also be set using #with_content:

<%= render BlogComponent.new do |c| %>
  <% c.header(classes: "title").with_content("My blog") %>
<% end %>

To view documentation for content_areas (deprecated) and the original implementation of Slots (deprecated), see /content_areas and /slots_v1.

Polymorphic slots (Experimental)

Polymorphic slots can render one of several possible slots. To use this experimental feature, include ViewComponent::PolymorphicSlots.

For example, consider this list item component that can be rendered with either an icon or an avatar visual. The visual slot is passed a hash mapping types to slot definitions:

class ListItemComponent < ViewComponent::Base
  include ViewComponent::PolymorphicSlots

  renders_one :visual, types: {
    icon: IconComponent,
    avatar: lambda { |**system_arguments|
      AvatarComponent.new(size: 16, **system_arguments)
    }
  }
end

Note: the types hash’s values can be any valid slot definition, including a component class, string, or lambda.

Filling in the visual slot is done by calling the appropriate slot method:

<%= render ListItemComponent.new do |c| %>
  <% c.visual_avatar(src: "http://some-site.com/my_avatar.jpg", alt: "username") %>
    Profile
  <% end >
<% end %>
<%= render ListItemComponent.new do |c| %>
  <% c.visual_icon(icon: :key) %>
    Security Settings
  <% end >
<% end %>