Skip to content

Allow passing Application-specific ViewComponents into slots #2546

@joelhawksley

Description

@joelhawksley

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_plain methods.

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:

  1. the current form (args → new instance of slot class), or
  2. a pre-built ViewComponent::Base instance (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
end

Usage 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions