decorate-with-parts
Use this skill when creating View Parts for decorator-style logic in Hanami 2.x.
Core principle: Parts wrap data with presentation methods. They keep Views and templates free of complex formatting logic.
Quick Reference
| Scenario | Approach |
|---|---|
| Create a Part | Class inherits from Hanami::View::Part |
| Expose as a Part | expose :user, as: :user_part |
| Define Part methods | Add methods to the Part class for presentation logic |
| Access wrapped data | value method returns the raw underlying object |
| Delegate to wrapped data | delegate :name, :email, to: :value |
| Use in template | <%= user_part.display_name %> |
| Format data | def formatted_date; value.created_at.strftime("%B %d, %Y"); end |
| Generate HTML | Keep HTML generation out of Parts. Use helpers or template logic. |
Core Rules
Create the Part class:
# app/views/parts/user.rb # frozen_string_literal: true module MyApp module Views module Parts class User < Hanami::View::Part delegate :name, :email, to: :value def display_name "#{value.first_name} #{value.last_name}" end def member_since value.created_at.strftime("%B %Y") end def admin? value.role == "admin" end end end end endExpose data as a Part in the View:
class Show < MyApp::View expose :user, as: :user_part endUse Part methods in templates:
<h1><%= user_part.display_name %></h1> <p>Member since <%= user_part.member_since %></p> <% if user_part.admin? %> <span class="badge">Admin</span> <% end %>Keep Parts focused on presentation. No database queries, no business rules:
# GOOD: formatting and simple predicates def display_name "#{value.first_name} #{value.last_name}" end # BAD: business logic def can_delete?(resource) value.role == "admin" && resource.owner_id == value.id endDelegate common methods to the wrapped value:
delegate :id, :name, :email, :created_at, to: :valueAccess the raw value with the
valuemethod when needed:def raw_attributes value.to_h endDo not generate HTML in Parts. Parts return strings or booleans. Templates handle HTML:
# GOOD def status_label value.active? ? "Active" : "Inactive" end # BAD def status_badge "<span class='badge'>#{status_label}</span>" end
Common Mistakes
- No database queries in Parts. Parts wrap already-fetched data; queries belong in repositories or actions.
- No HTML generation in Parts. Parts return plain strings or booleans; templates produce HTML markup.
- No business logic in Parts. Predicates based on simple attribute values are fine; authorization rules and domain decisions belong in interactors or service objects.
Integration
| Related Skill | When to chain |
|---|---|
| create-view | Parts are used within Views. Master View structure first. |
| create-action | Actions pass data to Views, which wrap them in Parts. |
| define-entity | Parts often wrap Entity objects returned by Repositories. |