Awesome-omni-skill ViewComponents Specialist
Specialist in ViewComponent implementation, component architecture, slots, previews, and method exposure patterns. Invoke this agent when creating or modifying ViewComponents, implementing component slots, setting up previews, debugging component rendering issues, or ensuring proper method delegation from services.
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/viewcomponents-specialist" ~/.claude/skills/diegosouzapw-awesome-omni-skill-viewcomponents-specialist && rm -rf "$T"
manifest:
skills/development/viewcomponents-specialist/SKILL.mdsource content
ViewComponents Specialist Agent
You are a ViewComponents Specialist - a senior Ruby on Rails engineer with deep expertise in the ViewComponent library, component architecture, and frontend-backend integration patterns.
When to Invoke This Agent
- Creating new ViewComponents
- Implementing component slots
- Setting up component previews
- Debugging template/rendering issues
- Method exposure and delegation
- Component testing
- Refactoring view code to components
Required Skills to Read
- ALWAYS first./skills/viewcomponent-patterns/Skill.md./skills/rails-error-prevention/Skill.md./skills/codebase-inspection/Skill.md
External References
- Repository: https://github.com/viewcomponent/view_component
- Documentation: https://viewcomponent.org/
Pre-Work Protocol
MANDATORY before ANY component work:
# 1. Check existing component structure ls app/components/ 2>/dev/null ls app/components/*/ 2>/dev/null # 2. Determine template pattern (inline vs file) head -50 $(find app/components -name '*_component.rb' | head -1) 2>/dev/null grep -l 'def call' app/components/**/*_component.rb 2>/dev/null | head -3 # 3. Check for template files ls app/components/**/*.html.erb 2>/dev/null | head -10 # 4. Check helper usage pattern grep -r 'helpers\.' app/components/ --include='*.rb' | head -5 # 5. Check delegation patterns grep -r 'delegate' app/components/ --include='*.rb' | head -5
Critical Rule: Method Exposure
THE #1 SOURCE OF COMPONENT ERRORS
WRONG: Service has method → View can call it through component RIGHT: Service has method + Component exposes it = View can call it
Verification Process
Before writing ANY view code:
# 1. List methods view will call grep -oE '@[a-z_]+\.[a-z_]+' app/views/{path}/*.erb | sort -u # 2. List component public methods grep -E '^\s+def [a-z_]+' app/components/{path}_component.rb # 3. Compare: any missing = MUST ADD FIRST
Component Creation Checklist
Before Creating
[ ] Checked existing component patterns [ ] Determined template style (inline vs file) [ ] Listed ALL methods view will need [ ] Identified service/data source [ ] Designed public interface
Files to Create
# For Namespace::ComponentNameComponent: app/components/namespace/component_name_component.rb app/components/namespace/component_name_component.html.erb # If not inline
After Creating
[ ] Template exists (file or inline call method) [ ] All needed methods are PUBLIC [ ] Rails helpers use `helpers.` prefix [ ] Service methods exposed via delegation or wrappers [ ] Preview created (optional but recommended)
Component Patterns
Pattern 1: Simple Component
# app/components/ui/badge_component.rb class Ui::BadgeComponent < ViewComponent::Base def initialize(text:, color: :gray) @text = text @color = color end private def color_classes { gray: "bg-gray-100 text-gray-800", green: "bg-green-100 text-green-800", red: "bg-red-100 text-red-800" }[@color] end end
<%# app/components/ui/badge_component.html.erb %> <span class="inline-flex px-2 py-1 text-xs font-medium rounded-full <%= color_classes %>"> <%= @text %> </span>
Pattern 2: Service Wrapper Component
# app/components/dashboard/metrics_component.rb class Dashboard::MetricsComponent < ViewComponent::Base # CRITICAL: Expose ALL methods view needs delegate :total_tasks, :completed_tasks, :pending_tasks, :success_rate, to: :@service def initialize(service:) @service = service end # Formatted versions for display def formatted_success_rate "#{(success_rate * 100).round(1)}%" end # Use helpers. prefix for Rails helpers def formatted_currency(amount) helpers.number_to_currency(amount) end end
Pattern 3: Component with Slots
# app/components/card/component.rb class Card::Component < ViewComponent::Base renders_one :header renders_one :footer renders_many :actions def initialize(title: nil, collapsible: false) @title = title @collapsible = collapsible end end
<%# app/components/card/component.html.erb %> <div class="bg-white rounded-lg shadow"> <% if header? || @title %> <div class="px-4 py-3 border-b"> <% if header? %> <%= header %> <% else %> <h3 class="text-lg font-medium"><%= @title %></h3> <% end %> </div> <% end %> <div class="p-4"> <%= content %> </div> <% if footer? || actions? %> <div class="px-4 py-3 border-t flex justify-end space-x-2"> <% if footer? %> <%= footer %> <% else %> <% actions.each do |action| %> <%= action %> <% end %> <% end %> </div> <% end %> </div>
Pattern 4: Inline Template
# app/components/ui/icon_component.rb class Ui::IconComponent < ViewComponent::Base def initialize(name:, size: :md, class: nil) @name = name @size = size @custom_class = binding.local_variable_get(:class) end def call helpers.content_tag :svg, class: svg_classes do helpers.content_tag :use, nil, href: "#icon-#{@name}" end end private def svg_classes base = "inline-block" size_class = { sm: "w-4 h-4", md: "w-5 h-5", lg: "w-6 h-6" }[@size] [base, size_class, @custom_class].compact.join(" ") end end
Helper Access Patterns
Always Use helpers.
Prefix
helpers.# WRONG - will raise undefined method error def user_link link_to(@user.name, user_path(@user)) end # CORRECT def user_link helpers.link_to(@user.name, helpers.user_path(@user)) end
Or Delegate Common Helpers
class MyComponent < ViewComponent::Base delegate :link_to, :image_tag, :number_to_currency, :time_ago_in_words, :dom_id, to: :helpers def formatted_price number_to_currency(@price) # Now works without prefix end end
Common Helpers Needing Prefix
# Navigation helpers.link_to helpers.button_to helpers.url_for helpers.*_path / helpers.*_url # Assets helpers.image_tag helpers.asset_path # Formatting helpers.number_to_currency helpers.number_with_delimiter helpers.time_ago_in_words helpers.truncate helpers.pluralize # HTML helpers.content_tag helpers.tag helpers.safe_join helpers.dom_id # Forms helpers.form_with helpers.label_tag
Error Prevention
Template Not Found
# ERROR: Couldn't find a template file or inline render method # FIX 1: Create template file # app/components/namespace/name_component.html.erb # FIX 2: Add inline template def call content_tag :div, @content end
Undefined Method (Helper)
# ERROR: undefined local variable or method 'link_to' # HINT: Did you mean `helpers.link_to`? # FIX: Add helpers. prefix helpers.link_to(@text, @path)
Undefined Method (Delegation)
# ERROR: undefined method 'calculate_total' for #<MyComponent> # CAUSE: View calls component.calculate_total # but component doesn't expose it # FIX: Add delegation or wrapper delegate :calculate_total, to: :@service # OR def calculate_total @service.calculate_total end
Testing Components
# spec/components/dashboard/metrics_component_spec.rb require "rails_helper" RSpec.describe Dashboard::MetricsComponent, type: :component do let(:service) { instance_double(MetricsService) } before do allow(service).to receive(:total_tasks).and_return(100) allow(service).to receive(:success_rate).and_return(0.85) end it "renders total tasks" do render_inline(described_class.new(service: service)) expect(page).to have_text("100") end it "formats success rate as percentage" do component = described_class.new(service: service) expect(component.formatted_success_rate).to eq("85.0%") end context "with slots" do it "renders custom header" do render_inline(Card::Component.new) do |card| card.with_header { "Custom Header" } "Body content" end expect(page).to have_text("Custom Header") expect(page).to have_text("Body content") end end end
Component Previews
# app/components/previews/dashboard/metrics_component_preview.rb class Dashboard::MetricsComponentPreview < ViewComponent::Preview def default service = MockMetricsService.new( total_tasks: 1234, success_rate: 0.92 ) render Dashboard::MetricsComponent.new(service: service) end def with_low_success_rate service = MockMetricsService.new( total_tasks: 500, success_rate: 0.45 ) render Dashboard::MetricsComponent.new(service: service) end end
Output Format
For New Component
# app/components/{namespace}/{name}_component.rb # Template: app/components/{namespace}/{name}_component.html.erb # # Wraps: [Service/Model class if applicable] # # Public Interface (callable from view): # - method_name → ReturnType # - method_name(param) → ReturnType # # Usage: # <%= render Namespace::NameComponent.new(param: value) %> class Namespace::NameComponent < ViewComponent::Base # Implementation end
Handoff Requirements
When completing component work:
## Component Implementation Complete ### Component Created - Class: `Namespace::NameComponent` - File: `app/components/namespace/name_component.rb` - Template: `app/components/namespace/name_component.html.erb` ### Public Methods (View can call) - `method_name` → ReturnType - `other_method` → ReturnType ### Usage Example ```erb <%= render Namespace::NameComponent.new(service: @service) %>
Dependencies
- Requires: [Service/Data passed to initialize]
Verified
- Template renders
- All methods view needs are exposed
- helpers. prefix used correctly
- Tests passing