ViewComponent Patterns
Build modern, component-based UIs with ViewComponent using Evil Martians' view_component-contrib patterns.
When to Use This Skill
-
Creating ViewComponent classes
-
Implementing slots and style variants
-
Building Lookbook previews
-
Testing components in isolation
-
Refactoring partials to components
Core Principle: Components Over Partials
Prefer ViewComponents over partials for reusable UI.
Why ViewComponents?
-
Better encapsulation than partials
-
Testable in isolation
-
Object-oriented approach with explicit contracts
-
IDE support and type safety
-
Performance benefits (compiled templates)
Setup
Gemfile
gem "view_component" gem "view_component-contrib" # Evil Martians patterns gem "dry-initializer" # Declarative initialization gem "lookbook" # Component previews gem "inline_svg" # SVG icons
Install with Rails template:
rails app:template LOCATION="https://railsbytes.com/script/zJosO5"
Base Classes
app/components/application_view_component.rb
class ApplicationViewComponent < ViewComponentContrib::Base extend Dry::Initializer end
spec/components/previews/application_view_component_preview.rb
class ApplicationViewComponentPreview < ViewComponentContrib::Preview::Base self.abstract_class = true end
Basic Component with dry-initializer
app/components/button_component.rb
class ButtonComponent < ApplicationViewComponent option :text option :variant, default: -> { :primary } option :size, default: -> { :md } end
<%# app/components/button_component.html.erb %> <button class="btn btn-<%= variant %> btn-<%= size %>"> <%= text %> </button>
Style Variants DSL
Replace manual VARIANTS hashes with the Style Variants DSL:
class ButtonComponent < ApplicationViewComponent include ViewComponentContrib::StyleVariants
option :text option :color, default: -> { :primary } option :size, default: -> { :md }
style do base { %w[font-medium rounded-full] }
variants {
color {
primary { %w[bg-blue-500 text-white] }
secondary { %w[bg-gray-500 text-white] }
danger { %w[bg-red-500 text-white] }
}
size {
sm { "text-sm px-2 py-1" }
md { "text-base px-4 py-2" }
lg { "text-lg px-6 py-3" }
}
}
# Apply when multiple conditions match
compound(size: :lg, color: :primary) { "uppercase" }
defaults { { color: :primary, size: :md } }
end end
<button class="<%= style(color:, size:) %>"> <%= text %> </button>
Component with Slots
class CardComponent < ApplicationViewComponent renders_one :header renders_one :footer renders_many :actions end
<%= render CardComponent.new do |card| %> <% card.with_header do %> <h3>Title</h3> <% end %>
<p>Body content</p>
<% card.with_action do %> <%= helpers.link_to "Edit", edit_path %> <% end %> <% end %>
Important Rules
- Prefix Rails helpers with helpers.
<%# CORRECT %> <%= helpers.link_to "Home", root_path %> <%= helpers.image_tag "logo.png" %> <%= helpers.inline_svg_tag "icons/user.svg" %>
<%# WRONG - will fail in component context %> <%= link_to "Home", root_path %>
Exception: t() i18n helper does NOT need prefix:
<%= t('.title') %>
- SVG Icons as Separate Files
Store SVGs in app/assets/images/icons/ and render with inline_svg gem:
<%= helpers.inline_svg_tag "icons/user.svg", class: "w-5 h-5" %>
Don't inline SVG markup in Ruby code - use separate files instead.
Conditional Rendering
class AlertComponent < ApplicationViewComponent option :message option :type, default: -> { :info } option :dismissible, default: -> { true }
Skip rendering if no message
def render? message.present? end end
Lookbook Previews
spec/components/previews/button_component_preview.rb
class ButtonComponentPreview < ApplicationViewComponentPreview def default render ButtonComponent.new(text: "Click me") end
def primary render ButtonComponent.new(text: "Primary", color: :primary) end
def all_sizes render_with(wrapper: :flex_row) do safe_join([ render(ButtonComponent.new(text: "Small", size: :sm)), render(ButtonComponent.new(text: "Medium", size: :md)), render(ButtonComponent.new(text: "Large", size: :lg)) ]) end end end
Access at: http://localhost:3000/lookbook
Testing Components
RSpec.describe ButtonComponent, type: :component do it "renders button text" do render_inline(ButtonComponent.new(text: "Click me")) expect(page).to have_button("Click me") end
it "applies style variant classes" do render_inline(ButtonComponent.new(text: "Save", color: :primary, size: :lg)) expect(page).to have_css("button.bg-blue-500.text-lg") end end
Detailed References
For advanced patterns and examples:
-
references/patterns.md
-
Slots, collections, polymorphic components, Turbo integration
-
references/style-variants.md
-
Full Style Variants DSL, compound variants, TailwindMerge