ViewComponent Guide
This project uses ViewComponent for building reusable, testable UI components.
Overview
ViewComponent is a framework for building view components in Rails. Components are Ruby objects that encapsulate view logic and templates, making them easier to test and reuse than partials.
Version: 3.22.0
Directory Structure
app/components/
├── application_component.rb # Base class for all components
├── railsboot/ # Railsboot UI component library
│ └── component.rb
└── www/ # Website-specific components
├── product_card_component.rb
├── product_card_component.html.erb
└── ...
Base Classes
ApplicationComponent
All custom components should inherit from ApplicationComponent:
class MyComponent < ApplicationComponent
def initialize(title:)
@title = title
end
end
ApplicationComponent provides:
- Delegated helpers:
fa_icon,image_tag,image_asset_tag,number_to_currency,number_with_delimiter,cms_link,post_url,post_path,strip_tags - Utility method:
fetch_or_fallback(argument, constant, fallback)
Railsboot::Component
Components from the Railsboot UI library inherit from Railsboot::Component. These are pre-built UI components with consistent styling.
⚠️ CRITICAL: Block-Capturing Helpers
Do NOT rely on ApplicationComponent delegation for helpers that accept blocks!
The Problem
When you delegate a block-accepting helper (like link_to, content_tag, tag), it routes through the helpers proxy which breaks ViewComponent's block capture mechanism.
Symptoms of this bug:
- Block content renders BEFORE the wrapper element
- Raw HTML tags (
</div>,</span>) appear as visible text - Escaped HTML inside elements
The Solution
Components that need link_to, content_tag, tag, or other block-accepting helpers must include the ActionView helper modules directly:
class Www::MyComponent < ApplicationComponent
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
# Now link_to with blocks works correctly in the template:
# <%= link_to some_path, class: 'btn' do %>
# Click me <%= fa_icon('arrow-right') %>
# <% end %>
end
Safe vs Unsafe Helpers
| Helper | Can Delegate? | Reason |
|---|---|---|
fa_icon |
✅ Yes | No block support |
image_tag |
✅ Yes | No block support |
number_to_currency |
✅ Yes | No block support |
cms_link |
✅ Yes | No block support |
strip_tags |
✅ Yes | No block support |
link_to |
❌ No | Accepts blocks |
content_tag |
❌ No | Accepts blocks |
tag |
❌ No | Accepts blocks (Rails 5+) |
form_with |
❌ No | Accepts blocks |
Creating a New Component
1. Generate the component
bin/rails generate component Www::MyFeature title
This creates:
app/components/www/my_feature_component.rbapp/components/www/my_feature_component.html.erb
2. Define the Ruby class
# app/components/www/my_feature_component.rb
class Www::MyFeatureComponent < ApplicationComponent
# Include these if you need link_to or content_tag with blocks
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
def initialize(title:, description: nil)
@title = title
@description = description
end
private
attr_reader :title, :description
end
3. Create the template
<%# app/components/www/my_feature_component.html.erb %>
<div class="my-feature">
<h2><%= title %></h2>
<% if description.present? %>
<p><%= description %></p>
<% end %>
<%= content %> <%# Renders block content passed to the component %>
</div>
4. Render the component
<%# In any view %>
<%= render Www::MyFeatureComponent.new(title: "Hello") do %>
<p>This is the block content</p>
<% end %>
Common Patterns
Using Slots
Slots allow you to pass multiple content blocks to a component:
class Www::CardComponent < ApplicationComponent
renders_one :header
renders_many :actions
def initialize(title:)
@title = title
end
end
<%# Template %>
<div class="card">
<div class="card-header">
<%= header %>
</div>
<div class="card-body">
<%= content %>
</div>
<% if actions? %>
<div class="card-footer">
<% actions.each do |action| %>
<%= action %>
<% end %>
</div>
<% end %>
</div>
<%# Usage %>
<%= render Www::CardComponent.new(title: "My Card") do |c| %>
<% c.with_header do %>
<h3>Custom Header</h3>
<% end %>
<p>Card body content</p>
<% c.with_action do %>
<%= link_to "Edit", edit_path %>
<% end %>
<% end %>
Conditional Rendering
Use render? to conditionally render a component:
class Www::AlertComponent < ApplicationComponent
def initialize(message:)
@message =
end
def render?
@message.present?
end
end
Accessing Helpers
class Www::MyComponent < ApplicationComponent
def formatted_price
# Delegated helpers can be called directly
number_to_currency(@price)
end
def some_link
# For non-delegated helpers, use the helpers proxy
helpers.some_custom_helper
end
end
Testing Components
Components can be tested in isolation:
# test/components/www/my_feature_component_test.rb
require "test_helper"
class Www::MyFeatureComponentTest < ViewComponent::TestCase
def test_renders_title
render_inline(Www::MyFeatureComponent.new(title: "Hello"))
assert_selector "h2", text: "Hello"
end
def test_renders_block_content
render_inline(Www::MyFeatureComponent.new(title: "Hello")) do
"Block content"
end
assert_text "Block content"
end
end
References
- ViewComponent Documentation
- ViewComponent GitHub
- Tips for Using ViewComponents in Rails
- Base class:
app/components/application_component.rb