with one click
with one click
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | hanami-views |
| description | Expert guidance on building, configuring, and testing Hanami Views |
This skill provides expert guidance on building, configuring, and testing Hanami Views (v2.x). It covers dependencies, input and exposures, templates, helpers, parts, scopes, context, error rendering, and testing strategies.
Include dependencies using Deps mixin
module Bookshelf
module Views
module Books
class Show < Bookshelf::View
include Deps["repos.book_repo"]
expose :book do |id:|
book_repo.get!(id)
end
end
end
end
end
Define exposures for template data
expose method with symbol nameSpecify input defaults for optional parameters
expose :books do |page: 1, per_page: 20|
book_repo.listing(page: page, per_page: per_page)
end
Create private exposures (not passed to template)
private_expose :author do |author_id:|
author_repo.get!(author_id)
end
expose :author_name do |author|
author.name
end
Make exposure available to layout
expose :recommended_books, layout: true do
book_repo.recommended_listing
end
Disable part decoration for exposures
expose :page_number, decorate: false
Receive input from actions
response.render# Action
response.render(view, id: request.params[:id])
# View
expose :book do |id:|
book_repo.get!(id)
end
Use response object for input
# Action sets properties on response
response[:page] = request.params[:page]
response[:per_page] = request.params[:per_page]
# View accesses as keyword arguments
expose :books do |page:, per_page:|
book_repo.listing(page: page, per_page: per_page)
end
Expose input directly to template
expose :query # query parameter made available in template
Depend on other exposures
expose :book do |id:|
book_repo.get!(id)
end
expose :author do |book| # depends on book exposure
author_repo.get!(book.author_id)
end
Access context from exposures
expose :books do |context:|
book_repo.books_for_user(context.current_user)
end
Use ERB templates by default
.html.erb<name>.<format>.<engine>Configure template engines
# app/view.rb
require "slim" # for .html.slim templates
module Bookshelf
class View < Hanami::View
end
end
Create layouts
templates/layouts/app.html.erb<%= yield %> for template content# app/templates/layouts/app.html.erb
<html>
<body>
<%= yield %>
</body>
</html>
Configure layout per view
class Index < Bookshelf::View
config.layout = "app" # or false/nil for no layout
end
Make exposure available to layouts
expose :page_title, layout: true
Use standard helpers in templates
format_number, asset helpers, HTML helpers<p><%= format_number(1234) %></p>
Access helpers in parts
helpers objectclass Book < Bookshelf::Views::Part
def word_count
helpers.format_number(body_text.split)
end
end
Access helpers in scopes
class MediaPlayer < Bookshelf::Views::Scope
def display_count
format_number(items.count)
end
end
Write custom helpers
app/views/helpers.rbmodule MyApp
module Views
module Helpers
def current_path?(path)
_context.request.fullpath == path
end
end
end
end
Organize helpers in nested modules
module MyApp
module Views
module Helpers
include FormattingHelper # from formatting_helper.rb
include UrlHelper # from url_helper.rb
end
end
end
Define part classes
Views::Parts namespace# app/views/parts/book.rb
module Bookshelf
module Views
module Parts
class Book < Bookshelf::Views::Part
end
end
end
end
# app/views/part.rb (base class)
module Bookshelf
module Views
class Part < Hanami::View::Part
end
end
end
Associate parts with exposures
:book → Views::Parts::Bookas: optionexpose :current_user, as: :user # Uses Views::Parts::User
expose :books, as: [:item_collection, :item]
Access decorated value in parts
#value or #_valueclass Book < Bookshelf::Views::Part
def title
value.title.upcase
end
def display_name
"#{title} (#{publication_date})"
end
end
Render partials from parts
class Book < Bookshelf::Views::Part
def info_box
render("books/info_box") # book available as local
end
end
Access helpers and context in parts
helpers.method_namecontext.method_name or _context aliasclass Book < Bookshelf::Views::Part
def cover_url
value.cover_url || helpers.asset_url("default.png")
end
def path
context.routes.path(:book, id)
end
end
Decorate part attributes
class Book < Bookshelf::Views::Part
decorate :author # Returns Views::Parts::Author
decorate :reviews, as: :review_collection
end
Build scopes from parts
class Book < Bookshelf::Views::Part
def info_box
scope(:info_box, size: :large).render
end
end
Define scope classes
Views::Scopes namespace# app/views/scopes/media_player.rb
module Bookshelf
module Views
module Scopes
class MediaPlayer < Bookshelf::Views::Scope
end
end
end
end
# app/views/scope.rb (base class)
module Bookshelf
module Views
class Scope < Hanami::View::Scope
end
end
end
Build scopes in templates
scope(:media_player, item: audio_file)
Access scope locals
#locals hash with defaultsclass MediaPlayer < Bookshelf::Views::Scope
def show_artwork?
locals.fetch(:show_artwork, true)
end
def display_title
"#{item.title} (#{item.duration})" # item is local
end
end
Render partials via scopes
class MediaPlayer < Bookshelf::Views::Scope
def render_player
render("media/audio_player")
end
end
Access context from scopes
context.method_name or _context aliasclass MediaPlayer < Bookshelf::Views::Scope
def item_url
routes.path(:item, item.id) # delegates to context#routes
end
end
Use standard context methods
context or _context access# Template
<%= inflector.pluralize("koala") %>
<%= routes.path(:books) %>
# Part
context.inflector.pluralize("koala")
Access request details in views from actions
#request - current HTTP request#session - session for current request#flash - flash messages from previous request#csrf_token - CSRF token helperCustomize context class
# app/views/context.rb
module Bookshelf
module Views
class Context < Hanami::View::Context
def current_path?(path)
request.fullpath == path
end
include Deps["repos.user_repo"]
def current_user
return nil unless session["user_id"]
@current_user ||= user_repo.get(session["user_id"])
end
end
end
end
Decorate context attributes
class Context < Hanami::View::Context
decorate :current_user, as: :user
end
Provide alternative context object
# Direct rendering
my_view.call(context: my_alternative_context)
# From action
response.render(view, context: my_alternative_context)
Customize error views
public/404.html and public/500.htmlconfig.render_errorsConfigure exception to response mapping
# config/app.rb
class App < Hanami::App
config.render_error_responses.merge!(
"ROM::TupleCountMismatchError" => :not_found
)
end
Automatic view rendering (default)
Pages::Contact → View Pages::ContactExplicit view rendering
def handle(request, response)
response.render(view, page: params[:page])
end
Explicit view dependency
class Contact < Bookshelf::Action
include Deps[view: "views.pages.contact"]
def handle(request, response)
# uses specified view dependency
end
end
Conditional view rendering
class Show < Bookshelf::Action
include Deps[
view: "views.home.show",
alternative_view: "views.alternative"
]
def handle(request, response)
if some_condition
response.render(alternative_view)
else
response.render(view)
end
end
end
RESTful view dependencies
Create action → New viewUpdate action → Edit viewDisable automatic rendering
def auto_render?(response)
false
end
Test views directly
RSpec.describe Views::Profile::Show do
subject(:view) { described_class.new(users_repo:) }
let(:user_repo) { double(:user_repo) }
let(:current_user) { double(name: "Amy", id: 1) }
it "renders user's own profile" do
allow(user_repo).to receive(:by_id).with(1).and_return(current_user)
output = view.call(current_user:, id: 1).to_s
expect(output).to include("This is your profile")
end
end
Test exposures
describe "exposures" do
subject(:rendered) { view.call(current_user:) }
it "exposes current_user" do
expect(rendered[:current_user].name).to eq("Amy")
end
end
Test parts
RSpec.describe Views::Parts::User do
subject(:part) { described_class.new(value: user) }
let(:user) { double(name: "Amy", email: "amy@example.com") }
it "displays name and email" do
expect(part.display_name).to eq("Amy (amy@example.com)")
end
end
Test part methods with helpers and partials
describe "#title_tag" do
it "includes name in h1 tag" do
expect(part.title_tag).to eq("<h1>Amy (amy@example.com)</h1>")
end
end
describe "#info_card" do
it "renders info card partial" do
expect(part.info_card).to start_with('<div class="user-info-card">')
end
end
When assisting with Hanami Views tasks, follow this workflow:
Identify the view requirements
Design exposure strategy
Configure dependencies
Choose appropriate part strategy
Organize helpers and context
Implement testing strategy
Review and refine
When detailed information is needed about specific topics, consult the Hanami Views documentation:
layout: true sparingly for layout-specific exposures#locals hash for defaults_context alias in case of naming conflictsinitialize_copy methodhtml_safe)