Each view in a Hanami application starts with a view class. In the same way that actions inherit from a base action class, views inherit from a base view class at (defined in app/view.rb).
One of the key responsibilities of a view is to source any data required by its template. To know what data to surface, a view might need some input such as the id of an item to render, or what page of results from a collection to display.
Imagine the following ERB template for showing a book, located at app/templates/books/show.html.erb:
<h1><%= book.title %></h1>
<p><%= book.description %></p>
To render, this template requires a book object. A view can provide that book object to the template using an exposure.
Exposures
Exposures are the mechanism that allow values to be passed from views to templates. They are defined using the #expose method, which accepts a symbol specifying the exposure’s name. Here, as an example, the exposed book is a struct with a title and description:
# app/views/books/show.rb
= Struct.new(:title, :description, keyword_init: true)
expose :book do
Book.new(title: "Pride and Prejudice", description: "The 1813 Jane Austen classic.")
end
end
end
end
end
When called from an action, the books show view will now render:
$ curl http://localhost:2300/books/1
Pride and Prejudice
The 1813 Jane Austen classic.
View input
To render a specific book from a data store, the view needs to know what book to render. A specific id or a slug can be passed to the view as view input.
For example, assuming the books show action services a route like GET /books/:id, the requested book id can be passed from the action below to view as an argument to the view:
# app/actions/books/show.rb
response.render view, id: request.params[:id]
end
end
end
end
end
Within the view, inputs are available as keyword arguments to exposure blocks:
# app/views/books/show.rb
include Deps["repos.book_repo"]
expose :book do
book_repo.get!(id)
end
end
end
end
end
Now that id is provided as input, the view can expose the requested book to its template using a book repository that fetches from the database.
Using the response object to provide input
An alternative way to provide view input is to set properties on the response object. The following two actions are equivalent:
# app/actions/books/index.rb
response.render view, page: request.params[:page], per_page: request.params[:per_page]
end
end
end
end
end
# app/actions/books/index.rb
response[:page] = request.params[:page]
response[:per_page] = request.params[:per_page]
end
end
end
end
end
Specifying input defaults
For optional input data, you can provide a default values (either nil or something more meaningful). A books index view might have defaults for page and per_page:
# app/views/books/index.rb
include Deps["repos.book_repo"]
expose :books do
book_repo.listing(page: page, per_page: per_page)
end
end
end
end
end
Exposing input to the template
View inputs can be exposed to templates directly:
# app/views/books/search.rb
expose :query
end
end
end
end
<p>You are searching for <%= query %></p>
Depending on other exposures
Sometimes one exposure will depend on value of another. You can depend on another exposure by naming it as a positional argument in your exposure block.
Below, the author exposure depends on the book exposure, allowing the author to be fetched based on the book’s author_id.
# app/views/books/show.rb
include Deps[
"repos.book_repo",
"repos.author_repo"
]
expose :book do
book_repo.get!(id)
end
expose :author do
author_repo.get!(book.author_id)
end
end
end
end
end
Accessing the context
To access the context object from an exposure, include a context: keyword parameter:
expose :books do
book_repo.books_for_user(context.current_user)
end
Decorating exposures
Normally exposures are passed to templates “as is”. However, you can optionally decorate them with parts. Parts let you attach view-specific logic (such as formatting, presentation helpers, and markup) directly to the object it concerns, rather than scattering it across templates. This logic can then be reused across views and tested independently.
To decorate an exposure, either use the decorate method, or pass decorate: true to expose.
decorate :book do
book_repo.get(id)
end
# Equivalent to the above
expose :book, decorate: true do
book_repo.get(id)
end
# Decorate values passed through directly
decorate :genre, :publisher
Warning
Before Hanami 3.0, all exposures were decorated by default. To restore the earlier behavior, set config.decorate_exposures = true in your view class.
Private exposures
You can create private exposures that are not passed to the template. This is helpful if you have an exposure that other exposures will depend on, but is not otherwise needed in the template.
Here only the author’s name is exposed:
private_expose :author do
author_repo.get!(author_id)
end
expose :author_name do
author.name
end
Layout exposures
Exposure values are made available only to the template by default. To make an exposure also available to the layout, use the layout: true option:
expose :recommended_books, layout: true do
book_repo.recommended_listing
end