ViewComponent Guide
This project uses ViewComponent for building reusable, testable UI components.
Overview
Section titled “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
Section titled “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
Section titled “Base Classes”ApplicationComponent
Section titled “ApplicationComponent”All custom components should inherit from ApplicationComponent:
class MyComponent < ApplicationComponent def initialize(title:) @title = title endendApplicationComponent 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
Section titled “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
Section titled “⚠️ CRITICAL: Block-Capturing Helpers”Do NOT rely on ApplicationComponent delegation for helpers that accept blocks!
The Problem
Section titled “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
Section titled “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 %>endSafe vs Unsafe Helpers
Section titled “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
Section titled “Creating a New Component”1. Generate the component
Section titled “1. Generate the component”bin/rails generate component Www::MyFeature titleThis creates:
app/components/www/my_feature_component.rbapp/components/www/my_feature_component.html.erb
2. Define the Ruby class
Section titled “2. Define the Ruby class”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, :descriptionend3. Create the template
Section titled “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
Section titled “4. Render the component”<%# In any view %><%= render Www::MyFeatureComponent.new(title: "Hello") do %> <p>This is the block content</p><% end %>Common Patterns
Section titled “Common Patterns”Using Slots
Section titled “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 endend<%# 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
Section titled “Conditional Rendering”Use render? to conditionally render a component:
class Www::AlertComponent < ApplicationComponent def initialize(message:) @message = message end
def render? @message.present? endendAccessing Helpers
Section titled “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 endendTesting Components
Section titled “Testing Components”Components can be tested in isolation:
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" endendReferences
Section titled “References”- ViewComponent Documentation
- ViewComponent GitHub
- Tips for Using ViewComponents in Rails
- Base class:
app/components/application_component.rb