ViewComponent Subtemplates

Gem Version Build Status

Adds support for sub-templates with typed arguments to ViewComponent, enabling modular, reusable component architectures.

Features

Template Arguments - Define typed arguments for sub-templates
Automatic Detection - Sub-templates discovered in component sidecar directories
Dynamic Methods - call_[name] helper methods generated automatically
ViewComponent Integration - Seamless integration with existing ViewComponent workflow

Installation

Add this line to your application's Gemfile:

gem 'view_component_subtemplates'

And then execute:

bundle install

Quick Start

1. Define a component

# app/components/table_component.rb
class TableComponent < ViewComponent::Base
  def initialize(users:, title:)
    @users = users
    @title = title
  end
end

2. Create your templates

<!-- app/components/table_component.html.erb -->
<div class="table-container">
  <%= call_header(title: @title, sortable: true) %>

  <tbody>
    <% @users.each_with_index do |user, index| %>
      <%= call_row(model: user, highlight: index.even?) %>
    <% end %>
  </tbody>

  <%= call_footer(total_count: @users.count) %>
</div>

3. File structure

app/components/
├── table_component.rb
├── table_component.html.erb
└── table_component/
    ├── header.html.erb
    ├── row.html.erb
    └── footer.html.erb

4. Sub-template files

<!-- app/components/table_component/header.html.erb -->
<%# locals: (title:, sortable:) -%>
<thead class="<%= 'sortable' if sortable %>">
  <tr>
    <th><%= title %></th>
    <th>Actions</th>
  </tr>
</thead>
<!-- app/components/table_component/row.html.erb -->
<%# locals: (model:, highlight:) -%>
<tr class="<%= 'highlighted' if highlight %>">
  <td><%= model.name %></td>
  <td><%= model.email %></td>
</tr>
<!-- app/components/table_component/footer.html.erb -->
<%# locals: (total_count:) -%>
<tfoot>
  <tr>
    <td colspan="2">Total: <%= total_count %> users</td>
  </tr>
</tfoot>

5. Use in your views

<%= render TableComponent.new(users: @users, title: "User List") %>

Rendering a subtemplate on its own

Sometimes you need to render just one subtemplate, outside the component's main template. A common case is a controller action that responds to an AJAX request with only an updated fragment.

Call render_subtemplate_in on a component instance, passing the current view context. The subtemplate must not declare locals: any per-render data is passed through the component's constructor, so the call site stays free of an untyped **locals boundary.

class UsersController < ApplicationController
  def row
    component = RowComponent.new(user: User.find(params[:id]), highlight: false)

    render html: component.render_subtemplate_in(view_context, :row), layout: false
  end
end
<%# app/components/row_component/row.html.erb -- no `locals:` line %>
<tr class="<%= 'highlighted' if @highlight %>"><td><%= @user.name %></td></tr>

render_subtemplate_in(view_context, name) renders the named subtemplate and returns its HTML as an html_safe string, without rendering the component's main template. It is the subtemplate-level counterpart of ViewComponent's render_in: render_in(view_context) is the external entry point for the main template (internally call), and render_subtemplate_in is the external entry point for a single subtemplate (internally call_<name>).

render_subtemplate_in raises a clear error if the named subtemplate does not exist, or if it declares locals (pass that data through the component's constructor instead).

The no-locals rule applies only to standalone rendering. Inside a component's templates, call_<name> keeps taking locals exactly as shown in the Quick Start; render_subtemplate_in is the only path that requires a no-locals subtemplate.

Requirements

  • Ruby >= 3.1.0
  • Rails >= 7.0.0
  • ViewComponent >= 4.2.0

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests.

bundle install
bundle exec rake test