Skip to content

Add support for slot arguments#2540

Open
CyberAP wants to merge 1 commit intoViewComponent:mainfrom
CyberAP:slots-with-arguments
Open

Add support for slot arguments#2540
CyberAP wants to merge 1 commit intoViewComponent:mainfrom
CyberAP:slots-with-arguments

Conversation

@CyberAP
Copy link

@CyberAP CyberAP commented Jan 31, 2026

What are you trying to accomplish?

This PR adds support for slot arguments.

# greeting_component.rb
class GreetingComponent < ViewComponent::Base
  renders_one :message
end
<%# greeting_component.html.erb %>
<div class="greeting">
  <%= message("Hello", "World") %>
</div>
<%# index.html.erb %>
<%= render GreetingComponent.new do |component| %>
  <% component.with_message do |greeting, name| %>
    <%= greeting %>, <%= name %>!
  <% end %>
<% end %>

Returning:

<div class="greeting">
  Hello, World!
</div>

What approach did you choose and why?

Slot arguments provide 2 major benefits to a component approach:

  1. Isolation – parent components no longer need to access private component instance to grab necessary data for rendering
  2. Configuration – slots could be used in more sophisticated ways previously not possible

Right now ViewComponent only supports basic slots without arguments. This means you have to follow patterns like this in order to get the data you need for rendering:

<%# index.html.erb %>
<%= render GreetingComponent.new do |component| %>
  <% component.with_message %>
    <%= component.greeting %>, <%= component.name %>!
  <% end %>
<% end %>

This breaks one of the key component principles: Encapsulation. If the parent component knows about internal state of the child component they become coupled. That leads to unreliable refactorings and degraded component's reusability.

A common pattern of customizing a slot would be to use lambda slots:

class BlogComponent < ViewComponent::Base
  renders_one :header, ->(classes:, &block) do
    content_tag :h1, class: classes, &block
  end
end

Unfortunately this approach does not allow us to pass data to the block directly, meaning it's actually the inverse of what we want: it passes data to a child component, not the child component passing data to the parent.

Another problem with this approach is that we can't customize what exactly we render inside the lambda.

Slot arguments allow us to solve these problems with elegance. Instead of component instance we access slot arguments passed to the block:

<%# index.html.erb %>
<%= render GreetingComponent.new do |component| %>
  <% component.with_message do |greeting, name| %>
    <%= greeting %>, <%= name %>!
  <% end %>
<% end %>

This means we are in full control of what's rendered inside the slot and also never touch component internals, allowing for safer refactorings.

With slot arguments we can now support more advanced cases of using slots:

<%# greeting_component.html.erb %>
<div class="greetings">
  <% greetings.each_with_index do |greeting, index| %>
    <%= message(greeting, index) %>
  <% end %>
</div>
<%# index.html.erb %>
<%= render GreetingComponent.new do |component| %>
  <% component.with_message do |greeting, index| %>
    <%= index % 2 == 0 ? greeting : '' %>!
  <% end %>
<% end %>

Anything you want to highlight for special attention from reviewers?

This feature has been previously discussed but there still is no working alternative to it.

This approach is not new and is inspired by Scoped Slots in Vue and Child Component as Function pattern in React.

Copy link
Member

@joelhawksley joelhawksley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CyberAP thank you for taking the time to write and document your proposal here. I think it's an interesting approach and I have some thoughts about what I'd want to see change before considering it for inclusion in the framework.

Real-world use-case

Can you provide an example of how you're looking to use this feature? Specifically, I'm looking for enough context to know what is being blocked by this PR so that I can understand if this proposed solution is the best option.

Explicit argument definition

The proposed implementation does not define what arguments can be passed to a slot, instead allowing callers to pass whatever they want! One of ViewComponent's key advantages is clearly communicating what context is needed to render views. What might it look like to define parameters when registering a slot?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants