-
Notifications
You must be signed in to change notification settings - Fork 487
Description
Discussed in https://github.com/ViewComponent/view_component/discussions/2438
Originally posted by RubenSmit September 2, 2025
The viewcomponent best practices mention Application-specific ViewComponents that can be used to translate a domain object into general-purpose components. This is very usefull to make your code more DRY but it is currently not possible to pass inherited ViewComponents into slots.
Context
renders_one and renders_many slots in ViewComponent today only accept:
- initializer args for the specified component class, or
- a block for setting the component’s content.
They do not accept a pre-built component instance.
This creates friction when you want to extend a base component (e.g. BadgeComponent) into a semantic wrapper (e.g. StatusBadgeComponent) and then slot that wrapper into a parent component (e.g. CardComponent).
Right now, developers must either:
- Re-map wrapper logic into slot args, or
- Use polymorphic slots with boilerplate
with_badge_status,with_badge_plainmethods.
A more natural developer experience would allow a wrapper component to be passed directly into a slot.
Proposed solution
Allow renders_one and renders_many slots to accept either:
- the current form (args → new instance of slot class), or
- a pre-built
ViewComponent::Baseinstance (that inherits from the slot class).
Example
# app/components/design_system/badge_component.rb
class BadgeComponent < ViewComponent::Base
def initialize(text:, color:)
@text = text
@color = color
end
end
# app/components/status_badge_component.rb
class StatusBadgeComponent < BadgeComponent
def initialize(status:)
super(
text: status.text,
color: {
"pending" => :yellow,
"paid" => :green,
"cancelled" => :red
}.fetch(status.type)
)
end
end
# app/components/card_component.rb
class CardComponent < ViewComponent::Base
renders_one :badge, BadgeComponent
endUsage with current slots (works today)
<%= render CardComponent.new do |card| %>
<% card.with_badge(text: "Pending", color: :yellow) %>
<% end %>Desired usage with application specific component (proposed)
<%= render CardComponent.new do |card| %>
<% card.with_badge(StatusBadgeComponent.new(status: @order.status)) %>
<% end %>Under the hood, the badge slot would detect if the argument is a BadgeComponent instance:
- If yes → render that instance directly
- If not → treat args as initializer options for the declared slot class (current behavior)