| name | viewcomponents |
| description | Conventions for creating and maintaining UI ViewComponents in the Ui:: namespace. Use when creating or modifying components in app/components/ — covers ApplicationComponent inheritance, Dry::Types parameters, CSS mapping constants, Stimulus integration, templates, and component tests. |
RubyVideo ViewComponents - UI Component Rules
UI Component Architecture
This document defines specific rules for creating and maintaining UI components in the Ui:: namespace within RubyVideo's ViewComponent architecture.
Component Structure & Inheritance
Base Component Pattern
All UI components MUST inherit from ApplicationComponent:
class Ui::ComponentNameComponent < ApplicationComponent
end
ApplicationComponent Features
The base ApplicationComponent provides:
Dry::Initializer integration for type-safe parameters
- Automatic
attributes handling for HTML attributes
display option for conditional rendering
render? method that respects the display option
UI Component Conventions
1. Component Naming
- File naming:
snake_case_component.rb (e.g., button_component.rb)
- Class naming:
Ui::PascalCaseComponent (e.g., Ui::ButtonComponent)
- Template naming:
snake_case_component.html.erb (e.g., button_component.html.erb)
2. Parameter Definition
Use Dry::Types for type-safe parameters with proper defaults:
class Ui::ButtonComponent < ApplicationComponent
param :text, default: proc {}
option :url, Dry::Types["coercible.string"], optional: true
option :kind, type: Dry::Types["coercible.symbol"].enum(:primary, :secondary), default: proc { :primary }
option :disabled, type: Dry::Types["strict.bool"], default: proc { false }
end
3. CSS Class Management
Mapping Constants Pattern
Define mapping constants for CSS class variants:
class Ui::ButtonComponent < ApplicationComponent
KIND_MAPPING = {
primary: "btn-primary",
secondary: "btn-secondary",
neutral: "btn-neutral btn-outline",
ghost: "btn-ghost"
}.freeze
SIZE_MAPPING = {
sm: "btn-sm",
md: "",
lg: "btn-lg"
}.freeze
end
Component Classes Method
Implement component_classes method for dynamic styling:
private
def component_classes
class_names(
"btn",
KIND_MAPPING[kind],
SIZE_MAPPING[size],
"btn-outline": outline,
"btn-disabled": disabled
)
end
Final Classes Method
Combine component classes with user-provided classes:
private
def classes
[component_classes, attributes[:class]].join(" ")
end
4. Content Handling
Content Method Pattern
Handle both parameter content and block content:
private
def content
text.presence || super
end
Call Method for Complex Rendering
Use call method for complex rendering logic:
def call
case button_kind
when :link
link_to(url, class: classes, **attributes.except(:class)) { content }
when :button
tag.button(type: type, class: classes, **attributes.except(:class)) { content }
end
end
5. Stimulus Integration
Before Render Hook
Use before_render for Stimulus controller setup:
def before_render
attributes[:data] = {
controller: "modal",
modal_open_value: open,
action: "keydown.esc->modal#close"
}.merge(attributes[:data] || {})
end
Data Attributes
Pass Stimulus values through data attributes:
attributes[:data] = {
controller: "dropdown",
dropdown_open_value: open,
action: "click->dropdown#toggle"
}
Common UI Component Patterns
Button Component
class Ui::ButtonComponent < ApplicationComponent
KIND_MAPPING = {
primary: "btn-primary",
secondary: "btn-secondary",
ghost: "btn-ghost"
}.freeze
SIZE_MAPPING = {
sm: "btn-sm",
md: "",
lg: "btn-lg"
}.freeze
param :text, default: proc {}
option :url, Dry::Types["coercible.string"], optional: true
option :kind, type: Dry::Types["coercible.symbol"].enum(*KIND_MAPPING.keys), default: proc { :primary }
option :size, type: Dry::Types["coercible.symbol"].enum(*SIZE_MAPPING.keys), default: proc { :md }
option :disabled, type: Dry::Types["strict.bool"], default: proc { false }
def call
if url.present?
link_to(url, class: classes, **attributes.except(:class)) { content }
else
tag.button(type: :button, class: classes, **attributes.except(:class)) { content }
end
end
private
def classes
[component_classes, attributes[:class]].join(" ")
end
def component_classes
class_names(
"btn",
KIND_MAPPING[kind],
SIZE_MAPPING[size],
"btn-disabled": disabled
)
end
def content
text.presence || super
end
end
Badge Component
class Ui::BadgeComponent < ApplicationComponent
KIND_MAPPING = {
primary: "badge-primary",
secondary: "badge-secondary",
neutral: "badge-neutral"
}.freeze
SIZE_MAPPING = {
xs: "badge-xs",
sm: "badge-sm",
md: "badge-md",
lg: "badge-lg"
}.freeze
param :text, optional: true
option :kind, type: Dry::Types["coercible.symbol"].enum(*KIND_MAPPING.keys), default: proc { :primary }
option :size, type: Dry::Types["coercible.symbol"].enum(*SIZE_MAPPING.keys), default: proc { :md }
def call
content_tag(:span, class: classes, **attributes.except(:class)) do
content
end
end
private
def classes
[component_classes, attributes[:class]].join(" ")
end
def component_classes
class_names(
"badge",
KIND_MAPPING[kind],
SIZE_MAPPING[size]
)
end
def content
text.presence || super
end
end
Modal Component
class Ui::ModalComponent < ApplicationComponent
POSITION_MAPPING = {
top: "modal-top",
middle: "modal-middle",
bottom: "modal-bottom",
responsive: "modal-bottom sm:modal-middle"
}.freeze
SIZE_MAPPING = {
md: "",
lg: "!max-w-[800px]"
}.freeze
option :open, type: Dry::Types["strict.bool"], default: proc { false }
option :position, type: Dry::Types["coercible.symbol"].enum(*POSITION_MAPPING.keys), default: proc { :responsive }
option :size, type: Dry::Types["coercible.symbol"].enum(*SIZE_MAPPING.keys), default: proc { :md }
def before_render
attributes[:data] = {
controller: "modal",
modal_open_value: open,
action: "keydown.esc->modal#close"
}.merge(attributes[:data] || {})
end
private
def classes
[component_classes, attributes.delete(:class)].compact_blank.join(" ")
end
def component_classes
class_names(
"modal",
POSITION_MAPPING[position]
)
end
def size_class
SIZE_MAPPING[size]
end
end
Template Patterns
Basic Template Structure
<!-- button_component.html.erb -->
<%= call %>
Complex Template with Slots
<!-- modal_component.html.erb -->
<div class="<%= classes %>" <%= tag.attributes(attributes) %>>
<div class="modal-box <%= size_class %>">
<% if close_button %>
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
</form>
<% end %>
<%= content %>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</div>
Testing UI Components
Component Test Structure
class Ui::ButtonComponentTest < ViewComponent::TestCase
test "renders primary button" do
render_inline(Ui::ButtonComponent.new("Click me", kind: :primary))
assert_selector("button.btn.btn-primary", text: "Click me")
end
test "renders as link when url provided" do
render_inline(Ui::ButtonComponent.new("Click me", url: "/test"))
assert_selector("a.btn", text: "Click me", href: "/test")
end
test "applies custom classes" do
render_inline(Ui::ButtonComponent.new("Click me", class: "custom-class"))
assert_selector("button.custom-class")
end
end
Best Practices
1. Type Safety
- Always use
Dry::Types for parameter validation
- Define enum constraints for variant options
- Use
optional: true for non-required parameters
2. CSS Architecture
- Use mapping constants for variant classes
- Implement
component_classes method for dynamic styling
- Support custom classes through
attributes[:class]
3. Content Flexibility
- Support both parameter content and block content
- Use
content method to handle both cases
- Implement
call method for complex rendering logic
4. Stimulus Integration
- Use
before_render for controller setup
- Pass values through data attributes
- Support custom actions and controllers
5. Accessibility
- Include proper ARIA attributes
- Support keyboard navigation
- Ensure proper semantic HTML
6. Performance
- Use
freeze on mapping constants
- Minimize method calls in templates
- Cache computed values when appropriate
Common Mistakes to Avoid
- Don't hardcode CSS classes in templates
- Don't forget to handle both parameter and block content
- Don't skip type validation for parameters
- Don't forget to merge custom attributes
- Don't ignore accessibility requirements
- Don't create components without proper mapping constants
Component Checklist
When creating a new UI component, ensure:
This ensures consistency across all UI components and maintains the design system's integrity.