This article assume you have familiarity with Hanami and want to delve deeper into the Hanami View gem which is powerful on it’s own. If you get lost, you can refer to the Hanami Guides to get up to speed.
Overview
From a high level, this is what the architecture looks like.

We’ll be exploring each aspect of the above in greater detail.
History
The Hanami View gem is an evolution of work originally started with the Dry View gem. You want to use Hanami View instead of Dry View since the latter has been eclipsed by the former because the Dry View gem hasn’t been actively worked on for some time. In fact, the last release is a couple years old now.
Setup
You might find the Hanamismith gem useful since it automates generating a Hanami application for you with Hanami View included despite being in a prerelease state. For example, Hanamismith configures your Gemfile
to point to Hanami View main
branch:
gem "hanami-view", github: "hanami/view", branch: "main"
Once Hanami 2.1.0 is released, Hanamismith will be updated to use the 2.1.0 version of all related gems. I’ll be referring to Hanamismith throughout this article since this gem gives you a professional setup for local use.
Quick Start
A quick way to get started is to use Hanamismith to build a Hanami application for you:
gem install hanamismith
hanamismith build --name demo
The above provides a great entry point for learning the basics of Hanami View by starting from the outside and working deeper into the view layer. We’ll start with the routes (the only file not shown above):
# demo/config/routes.rb
module Demo
class Routes < Hanami::Routes
slice(:home, at: "/") { root to: "show" }
end
end
Notice that any request made to /
directs traffic to the show
action of our home slice. If we look at the Show
action, we see:
# demo/slices/home/actions/show.rb
module Home
module Actions
class Show < Home::Action
end
end
end
By default, an action will immediately render the associated view of the same name as shown here:
# demo/slices/home/views/show.rb
module Home
module Views
class Show < Home::View
expose :ruby_version, default: RUBY_VERSION
expose :hanami_version, default: Hanami::VERSION
end
end
end
You’ll notice the Show
view exposes data for rendering within the template of the same name which is shown here but truncated for brevity:
<!-- demo/slices/home/templates/show.html.erb -->
<footer class="footer">
<ul class="group">
<li>Ruby <%= ruby_version %></li>
<li>Hanami <%= hanami_version %></li>
</ul>
</footer>
When we launch the demo application and view the home page you can see the route, action, view, and template render the following page:
That’s quite nice for minimal code and, look, the Ruby and Hanami version information that was exposed via our Show
view can be seen in the page footer!
The rest of this article will delve into the full Hanami View feature set so, without further ado, let’s dive in!
Views
Views are your top level domain objects which provide a complete standalone rendering system that can be composed of several components for greater separation of concerns:
-
Configuration
-
Contexts
-
Parts
-
Helpers
-
Templates and Partials
-
Scopes
We’ll dive into each of the above but, first, I want to demonstrate the power of Hanami View when used in isolation. To begin, each view must inherit from Hanami::View
and have an associated template. Here’s a standalone example — a plain old Ruby object leveraging the Command Pattern — where a default label is injected and exposed for rendering within a template:
#! /usr/bin/env ruby
# frozen_string_literal: true
# Save as `demo`, then `chmod 755 demo`, and run as `./demo`.
require "bundler/inline"
gemfile true do
source "https://rubygems.org"
gem "hanami-view", github: "hanami/view", branch: "main"
end
require "pathname"
Pathname("show.html.erb").write("<h1><%= label %></h1>")
module Demo
module Views
class Show < Hanami::View
config.paths = Pathname.pwd
config.template = "show"
def initialize(label: "Demo", **)
@label = label
super(**)
end
expose(:label) { label }
private
attr_reader :label
end
end
end
puts Demo::Views::Show.new.call
# <h1>Demo</h1>
When unpacking the above, you’ll notice I provide a minimal implementation which:
-
Uses a
show.html.erb
template that renders the exposedlabel
provided by the view. -
Uses a
Show
view which is configured to use the current working directory to look for the associatedshow.html.erb
template. -
Uses a
label
dependency which defaults to"Demo"
and is exposed for rendering within theshow.html.erb
template.
When the view is initialized and called, we see <h1>Demo</h1>
is rendered. This makes views extremely versatile, simple to test in isolation, and great for use outside of a Hanami application. 🚀
Exposures
Exposures allow the passing of values — either via injected dependencies or inputs upon construction — from the view to the templates. Exposures are meant for exposing data only, not behavior. For example, routes can be exposed as follows (using a block as default):
class Index < Hanami::View
include Deps[:routes]
expose(:tasks_path) { routes.path :tasks }
end
Exposures can have explicit defaults (but use a block if you need dynamic content):
class Index < Hanami::View
include Deps[:routes]
expose :tasks_path, default: "/tasks"
end
Defaults can be taken a step further by defining keyword parameters:
# Index pagination that defaults to the first page with a 20 record limit.
class Index < Hanami::View
include Deps[repository: "repositories.task"]
expose(:tasks) { |page: 1, limit: 20| repository.paginate page, limit: }
end
# Show a task by ID.
class Show < Hanami::View
include Deps[repository: "repositories.task"]
expose(:task) { |id:| repository.find id }
end
Exposures can use a specific part via the :as
keyword:
class Index < Hanami::View
expose(:form, as: Parts::Forms::Registration) do |values: {}, errors: {}|
{values:, errors:}
end
end
Exposures can be used within your actions so you can supply custom data as keyword arguments to your view when rendered by the action:
class Show < Hanami::Action
def handle request, response
# Implicit, default behavior:
# response.render view
# ...but you can do this instead:
# response[:tasks_path] = "/my/tasks/path"
# ...or this:
# response.render view, tasks_path: "/my/tasks/path"
# The latter two examples are equivalent.
end
end
An exposure can depend on another by making the dependent exposure a positional argument:
class Show < Hanami::View
include Deps["repositories.books", "repositories.authors"]
expose(:book) { |id:| books.find id }
expose(:author) { |book| authors.find book.author_id }
end
Exposures can be private which is handy when you need building blocks — not needed by the template — which are great for use in complex exposures:
class Show < Hanami::View
include Deps[repository: "repositories.task"]
expose(:user_name) { |user, id:| user(id:).name }
private_expose(:user) { |id:| repository.find id }
end
By default, exposures are only available to the template but can be made available to the layout as well by using layout: true
:
class Index < Hanami::View
expose :users, default: [], layout: true
end
Exposures are decorated by parts but this can be disabled, via decorate: false
, since this allows you to use a primitive which has no extra behavior:
class Show < Hanami::View
expose :label, default: "Demo", decorate: false
end
Renderings
Every view has a rendering, which is not something you’d use much in your application, but very useful when used for testing purposes especially when testing parts, scopes, and other objects. Here’s how to obtain a rendering:
rendering = MyView.new.rendering
A rendering can take additional arguments which, when supplied, override the defaults as provided via your view:
MyView.new.rendering format: "json", context: MyContext.new
With the above, I supplied a custom format and context. Knowing this can come in handy when needing to supply different formats and contexts when testing your parts, scopes, and other objects. Keep this in mind when learning more about renderings later in this article.
Debugging
Views are convenient to use outside of HTTP requests/responses which simplifies debugging and testing. For example — since every view is a registered dependency — you can access a view instance via the application container as follows:
Home::Slice["views.show"].call
The above would render the following output (truncated for brevity):
<html>
<body>
<h1>Demo</h1>
</body>
</html>
You can take this a step further. In this case, I inspect the view’s locals:
Home::Slice["views.show"].call.locals
# {
# ruby_version: #<Home::Views::Part name=:ruby_version value="3.2.2">,
# hanami_version: #<Home::Views::Part name=:hanami_version value="2.1.0.beta1">
# }
Testing
RSpec.describe Home::Views::Show do
subject(:view) { described_class.new }
describe "#call" do
it "renders heading" do
expect(view.call.to_s).to include("<h1>Demo</h1>")
end
end
end
Configuration
Views are highly customizable — most of which are preconfigured when used within a Hanami application — but you can use a custom configuration instead of the defaults. Example:
class Show < Hanami::View
# Defines custom paths.
# Default: None (required).
config.paths = Hanami.app.root.join "alternate/templates"
# Defines custom template.
# Default: None (required).
config.template = "alternate"
# Defines custom base path to remove when inferring template's class name.
# For Hanami applications only.
# Default: nil
config.template_inference_base = "custom/path"
# Defines a custom path to where all layouts are located.
# Default: "layouts"
config.layouts_dir = "alternate"
# Defines a custom layout.
# Default: false
config.layout = "alternate"
# Defines a custom format.
# Default: :html
config.default_format = "text"
# Defines the context to be used within views, parts, scopes, templates, and partials.
# Default: Hanami::View::Context
config.default_context = AlternateContext.new
# Defines a custom part namespace.
# Default: nil
config.part_namespace = AlternateParts
# Defines a custom part builder.
# Default: Hanami::View::PartBuilder
config.part_builder = AlternatePartBuilder.new
# Defines a custom part class.
# Default: Hanami::View::Part
config.part_class = AlternatePart
# Defines a custom scope namespace.
# Default: nil
config.scope_namespace = AlternateScopes
# Defines a custom scope namespace.
# Default: Hanami::View::ScopeBuilder
config.scope_builder = AlternateScopeBuilder.new
# Defines a custom scope class.
# Default: Hanami::View::Scope
config.scope_class = AlternateScope.new
# Defines a custom scope class but unknown if this works or not.
# Seems to duplicate `scope_class` behavior. Use with caution or stick with `scope_class` instead.
# Default: nil
config.scope = AlternateScope.new
# Defines custom inflector for resolving single or plural strings.
# Default: Dry::Inflector.new
config.inflector = AlternateInflector.new
# Defines a custom rendering engine for a format.
# Default: {}
config.renderer_engine_mapping = {liquid: ::Tilt::LiquidTemplate}
# Defines custom options for the rendering engine.
# Default: {default_encoding: "utf-8"}
config.renderer_options = {trim: true}
end
In addition to the above, you can access — but not modify — the configuration once initialized. Example:
view = Show.new
view.config # Dry::Configurable::Config
view.config.layout # "alternate"
view.config.paths # [#<Hanami::View::Path>]
# ...etc...
Use of a custom configuration emphasizes my earlier example where I show how you can use Hanami View outside of a Hanami application. This is so powerful that I’m using Hanami View within Milestoner for building customizable release notes. Even better, this works extremely well with an XDG configuration especially when dealing with template paths.
Before wrapping up discussing a view’s configuration, I’ve elaborated on a few aspects of the configuration with additional details that might be of interest.
Paths
Use of config.paths
, for the moment, has set and forget behavior because you can’t append paths without extra effort. To do this, you need to create a Hanami::View::Path
object which can be appended to your path. Like so:
config.paths.append Hanami::View::Path[Pathname(__dir__).join("templates").expand_path]
The above is necessary because only view paths are allowed at the moment. I point this out because knowing how to append to an existing path — especially when using a slice — allows you to provide fallback behavior for missing templates, parts, etc.
Rendering Engine Mapping
Template engine detection happens automatically based on format (i.e. .erb
, etc.) so you only need to change this if you need a custom engine. That said, if you don’t customize config.renderer_engine_mapping
, you’ll end up with Hanami::View::ERB::Template
as your engine which provides the following functionality:
-
Automatic escaping of any non-
html_safe?
values when using<%=
and escaping disabled when using<%==
. -
Implicitly capturing and outputting block content without use of special helpers. This allows helpers like
<%= form_for(:post) do %>
to be used, with theform_for
helper directly messagingyield
. -
No advanced trimming or further customization.
Should Hanami::View::ERB::Template
not be desired, you could use config.renderer_engine_mapping = {erb: ::Tilt::ERBTemplate}
which gives you basic Tilt ERB functionality.
Lastly, multiple custom engines can be defined depending on your needs. In the original configuration example, I use {liquid: ::Tilt::LiquidTemplate}
where Liquid is the format and Tilt::LiquidTemplate
is the engine. All custom engines can be mapped using Tilt Template documentation.
Renderer Options
By default, the renderer options are: {default_encoding: "utf-8"}
. Anything you define, in addition to the defaults, will be merged with the defaults.
Resources
For more on the above, I’ll refer you to the Dry View Configuration documentation.
Contexts
Contexts are meant for passing common data between views, parts, scopes, helpers, and templates. For example: current user, asset paths, etc. Each context must inherit from Hanami::View::Context
and doesn’t need to be auto-registered because Hanami will look for the Context
class instead. Each context has access to the following objects:
-
routes
-
request
-
assets
-
content_for
-
inflector
-
settings
-
csrf_token
-
session
-
flash
Here’s a simple example with no customization:
# demo/views/context.rb
# auto_register: false
module Demo
module Views
class Context < Hanami::View::Context
end
end
end
Contexts are designed to be injected with dependencies. Example:
class DemoContext < Hanami::View::Context
def initialize(title:, description:, **)
@title = title
@description = description
super(**)
end
end
Contexts can be decorated with parts:
class DemoContext < Hanami::View::Context
decorate :navigation
attr_reader :navigation
def initialize(navigation:, **)
@navigation = navigation
super(**)
end
end
You can pass the same options to decorate
as you do with exposures, for example:
class DemoContext < Hanami::View::Context
decorate :navigation, as: :menu
end
Finally, contexts can be passed in when calling the view:
view.call context:
Parts
Parts are the presentation layer and should be familiar territory for those used to the Presenter Pattern. Parts are automatically initialized and wrapped around what you expose via your view. This means if you expose a :user
via your view, then the corresponding part would be Parts::User
. Otherwise, a generic Hanami::View::Part
will be used instead.
By default, parts need to be a sub-directory within the views directory (i.e. views/parts
) and will be auto-loaded for you. You can define a root part via views/part.rb
which inherits from your slice. This allows you to customize your part for the slice much like how view.rb
and action.rb
work at the slice root.
Dependency Injection
You can inject dependencies, like you would with any Ruby object, by using a custom constructor. Example:
class MyPart < Hanami::View::Part
def initialize(my_dependency: MyDependency.new, **)
@my_dependency = my_dependency
super(**)
end
end
The double splats (**
) are important because you need to pass all keyword arguments up to the superclass to complete initialization. Otherwise, you are free to define additional parameters for your method signature as needed.
Render
You can render partials within a part by using the #render
method but, unlike when working within a template, you can’t use symbols to identify your partial. This means you must pass the full relative path of the template. Example: render "my/relative/path/to/partial"
.
Decoration
Use the decorate
macro to memoize expensive operations and ensure they only run once when rendered. This is because a part instance only lives for the duration of a single rendering. For example, the following decorates a collection of commits associated with a tag:
class Tag < Hanami::View::Part
decorate :commits
end
Use of decorate
serves another purpose as it also allows you to decorate associations. In the above example, the Tag
has a collection of Commit
objects. Due to using decorate :commits
, this means you can use the following in your templates/partials:
<% tag.commits.each do |commit| %>
<%= commit.render :commit %>
<% end %>
⚠️ Decorations are only automatically applied for your templates and partials. When writing specs, you’ll need to manually decorate any/all records you need for testing purposes. This is as simple as mapping over your records and turning them into a collection of parts you can test. Example:
parts = records.map { |record| MyPart.new value: record }
Tests
Using the Hemo application, for example, here is a task part which provides additional information when rendered by the view:
module Tasks
module Views
module Parts
class Task < Hanami::View::Part
def assignee = user.name
def checked = ("checked" if completed_at)
def css_class = completed_at ? "task task-completed" : "task"
end
end
end
end
The corresponding spec is:
# frozen_string_literal: true
require "hanami_helper"
RSpec.describe Tasks::Views::Parts::Task do
subject(:part) { described_class.new value: task }
let(:task) { Test::Factory.structs[:task, id: 1, user:] }
let(:user) { Test::Factory.structs[:user, name: "Jane Doe"] }
describe "#assignee" do
it "answers user" do
expect(part.assignee).to eq("Jane Doe")
end
end
describe "#checked" do
it "answers checked when completed" do
allow(task).to receive(:completed_at).and_return(Time.now.utc)
expect(part.checked).to eq("checked")
end
it "answers nil when not completed" do
expect(part.checked).to be(nil)
end
end
describe "#css_class" do
it "answers completed when completed" do
allow(task).to receive(:completed_at).and_return(Time.now.utc)
expect(part.css_class).to eq("task task-completed")
end
it "answers default class when not completed" do
expect(part.css_class).to eq("task")
end
end
end
Other than a tiny bit of setup where I use a user
and task
in-memory ROM factory, I then pass the task
record as a value
to the part and that’s it.
While the above is straightforward, you’ll need to pass in your view as the rendering
argument to your part when using helpers. Here’s a simplified example of the above with implementation and spec:
# Implementation
module Tasks
module Views
module Parts
class Task < Hanami::View::Part
def assignee = tag.span user.name
end
end
end
end
# Specification
RSpec.describe Tasks::Views::Parts::Task do
subject(:part) { described_class.new value: task, rendering: Tasks::View.new.rendering }
let(:task) { Test::Factory.structs[:task, id: 1, user:] }
let(:user) { Test::Factory.structs[:user, name: "Jane Doe"] }
describe "#assignee" do
it "answers user" do
expect(part.assignee).to eq("<span>Jane Doe</span>")
end
end
end
Notice two important changes are applied in the above:
-
I’m using the
#span
method on thetag
helper to generation an HTML<span></span>
tag. I didn’t have to include any helpers since the default Hanami Helpers are included by default. -
When initializing the part (subject) in my spec, I must pass in
Tasks::View.new.rendering
for rendering purposes, otherwise you’ll end up aNoMethodError: undefined method `dasherize' for nil:NilClass
error. This is due to the fact that each part has an inflector (i.e.part._context.inflector
) which isnil
when not associated with a view from your slice. This is why an instance ofTasks::View
is used for part rendering since this is how the part behaves when used within your slice.
There are a couple of ways to setup your RSpec specs when testing parts. As shown above, when testing parts within a Hanami application, this is as simple as defining your subject and supplying the value
(in this case a task
) that your part wraps:
RSpec.describe Tasks::Views::Parts::Task do
subject(:part) { described_class.new value: task }
let(:task) { Test::Factory.structs[:task, id: 1, user:] }
end
If working within a slice, you’ll need a few lines of extra code to setup your view. Example:
RSpec.describe Demo do
subject(:part) { described_class.new value:, rendering: view.new.rendering }
let :view do
Class.new Hanami::View do
configure_for_slice Hanami.app
config.template = "n/a"
end
end
let(:value) { "For demonstration purposes." }
end
All the above is doing is defining an anonymous view, for testing purposes, so we can create our part with the view’s rendering.
When working with parts outside of a Hanami application, like in a pure Ruby project or a gem, you’ll need a similar setup when working in slices. Here’s an example from the Milestoner gem:
RSpec.describe Milestoner::Views::Parts::Commit do
subject(:part) { described_class.new value: commit, rendering: view.new.rendering }
let :view do
Class.new Hanami::View do
config.paths = [Bundler.root.join("lib/milestoner/templates/releases")]
config.template = "n/a"
end
end
let(:commit) { Milestoner::Models::Commit.new }
end
The above isn’t much different than working in a Hanami application, only that we need to construct our own view for testing purposes that uses a valid path to where your templates are located. Then, when creating an instance of our part, we pass in the view’s rendering like when working in a slice.
Guidelines
When using parts, be conscious of the following:
-
Use method names that don’t override the default
value
method provided for you so you can access the original object injected into your part. -
Avoid mixing template/helper logic within your parts if you can. Instead, keep your parts solely focused on rendering the data so they can be used with multiple templates/formats (i.e. HTML, JSON, etc.) as desired.
Scopes
Standard scopes provide access to the following:
-
Injected dependencies.
-
Locals.
-
Context.
-
Helpers.
-
Partials (when rendered).
You can also implement custom scopes which will be explained shortly. Using standard and/or custom scopes allows you to extract complicated logic out of your partials for reduced complexity and improved testability. A few notes:
-
By default, scopes need to be a sub-directory of views (i.e.
views/scopes
) to be auto-loaded for you. -
Scopes can be namespaced (i.e.
config.scope_namespace = View::Scopes::Demo
) by defining the namespace and then adding scopes to that namespace (i.e.views/scopes/demo/example.rb
). -
Only one namespace can be associated with a view at a time since multiple namespaces are not supported. In order to share functionality, you have to subclass your scopes which carries the same caveats as with standard inheritance so keep your behavior well encapsulated.
-
When you are inside a template,
self
is an instance of a scope. -
Scopes can be used within templates and partials via the
scope
method. This method can take a symbol (i.e.:avatar
) or a constant (i.e.Scopes::Avatar
) as the first argument followed by keyword arguments.
Dependency Injection
You can inject dependencies, like you would with any Ruby object, by using a custom constructor. Example:
class MyScope < Hanami::View::Scope
def initialize(my_dependency: MyDependency.new, **)
@my_dependency = my_dependency
super(**)
end
end
The double splats (**
) are important because you need to pass all keyword arguments up to the superclass to complete initialization. Otherwise, you are free to define additional parameters for your method signature as needed.
Context
Scopes have access to the context via the context
object. This means you can access objects like the inflector: context.inflector.humanize "A demo"
.
Render
Rendering a scope has a lot of uses. By default, whenever your message #render
, the scope will look for the partial of the same name. Example (assuming ERB is configured as your template engine):
-
Form
(class) equates to_form.html.erb
. -
FormField
(class) equates to_form_field.html.erb
.
Even better, your scope will dynamically load the partial of the same name relative to where the scope is used. For example, let’s say you have the following structure:
templates
/books
/_form.html.erb
/show.html.erb
templates
/authors
/show.html.erb
/_form.html.erb
If you message scope(:form).render
within the show
template of both books and authors, the corresponding _form
partial relative to the show
template will used. This gives you a lot of reusability since the same scope can be used with different partials of the same name.
The #render
method takes a path argument should you never need to explicitly render a partial. Example:
# Equates to: templates/shared/_book.html.erb
scope(:form).render "shared/book"
In some cases, when you’d like a custom default, you can do this by specifying the default path in your render method and send to super
. Example:
class Form < Hanami::View::Scope
def render(path = "shared/form") = super
end
The above will always default to templates/shared/_form.html.erb
unless you pass as an argument to #render
. Super handy for shared partials or default behavior in general.
Blocks are supported which means you can pass a block of content to #render
when wanting to yield the result via your partial. Example:
# Partial (templates/books/_form.html.erb)
<section>
<%= yield %>
</section>
# Template (templates/books/show.html.erb)
<%= scope(:form).render do %>
<h1>Demo</h1>
<% end %>
# Output
<section>
<h1>Demo</h1>
</section>
The above allows you to wrap your scope around a chunk of content that might have helpers or other dynamic content you wish to yield to your partial.
⚠️ You might be tempted to message your scope’s methods while inside the block of your scope when used within a template but this isn’t possible and will result in a method error.
Call
You can render partials within your scopes via the #render
method but, in general, you should shy away from heavy use by preferring to be explicit instead. Example:
scope(:my_scope).render "my/partial"
Being explicit improves readability because you can see the scope being used and the corresponding partial being scoped. That said, there is an interesting use case where rendering within your scope is valuable by using the #call
method to deal with logic that requires you to render a partial based on whether you have or don’t have content. Here’s an example pulled from the Milestoner gem:
class Users < Hanami::View::Scope
def call = users.any? ? render("milestones/users", users:) : render("milestones/none")
end
What’s nice about the above is the logic for determining if you have an array of users or none at all is nicely encapsulated in the scope so you don’t have to deal with any logic in your partial. Instead, you can use the scope as follows:
scope(:users, users: commit.collaborators).call
This simple one-liner will render a list of users or a paragraph stating there are no users. Now you can reuse this logic across multiple partials that need to render different kinds of users. Finally, this allows you to encapsulate and easily test this logic in one place.
Tests
Here’s an example of an implementation and corresponding spec:
Implementation
# frozen_string_literal: true
require "refinements/array"
module Main
module Views
module Scopes
# Encapsulates the rendering of a task description input with possible error.
class Description < Hanami::View::Scope
using Refinements::Array
def value = content
def message = (error[:description].to_sentence if error.key? :description)
end
end
end
end
Spec
# frozen_string_literal: true
require "hanami_helper"
RSpec.describe Main::Views::Scopes::Description do
subject(:scope) { described_class.new locals:, rendering: view.new.rendering }
let(:locals) { {content: "Test", error: {description: ["Danger!"]}} }
let :view do
Class.new Hanami::View do
config.paths = SPEC_ROOT
config.template = "n/a"
end
end
describe "#value" do
it "answers value" do
expect(scope.value).to eq("Test")
end
end
describe "#message" do
it "answers error message when error description exists" do
expect(scope.message).to eq("Danger!")
end
it "answers nil when error description doesn't exist" do
locals[:error] = {}
expect(scope.message).to be(nil)
end
end
end
Standalone
When used outside of Hanami, the default folder structure is not auto-detected so you must be explicit which namespace to use when configuring your view:
class Show < Demo::View
# Equates to `views/scopes`.
config.scope_namespace = Demo::Views::Scopes
end
You won’t be able to include helpers directly within your scope. Example:
class MyScope < Hanami::View::Scope
include Hanami::View::Helpers::TagHelper
end
# hanami-view-2.1.0/lib/hanami/view/helpers/tag_helper.rb:199:in `tag_builder_inflector': undefined method `inflector' for an instance of Milestoner::Views::Context (NoMethodError)
#
# return _context.inflector
As you can see, the above will result in an exception. To workaround this bug, you can inject the helpers you need as dependency instead. See Helpers: Standalone section for details.
Helpers
Helpers allow you to define custom tags for use within views, parts, scopes, templates, and partials.
Default
Hanami comes with several default helpers but documentation is spotty. Some documentation can be found via the official guides and others can be found in the repository. Here’s the full list:
-
Assets (Guide)
-
HTML (Guide)
-
String Escaping (Guide)
-
Number Formatting (Guide)
-
Forms (Source)
Custom
⚠️ Due to helpers being available everywhere — and this includes built-in Hanami helpers like the asset helpers — you’ll need to be careful to not define a helper which is the same name as a reserved word within the Hanami View ecosystem. You can workaround this limitation by using locals[:key]
to pluck out the value you need but will make your code tedious to maintain.
Example:
module Demo
module Views
module Helpers
def warning
tag.span class: :warning do
# Implicit yielding allows you to captures content from the block for use in the template.
yield
end
end
def error(message) = tag.span message, class: :errr
end
end
end
While helpers are available everywhere, including them within your views, parts, scopes, etc is not a good idea because mixing helpers within your views and parts prevents them from being reused in different formats (i.e. html, rss, json, etc.) — as emphasized earlier — and you don’t want to miss out on that flexibility.
By default, any slices/<name>/views/helper.rb
will be automatically included for use. Example:
module Tasks
module Views
module Helpers
end
end
end
To add additional helpers, you could use this structure within your slice:
slices/<slice>/views/helper.rb
slices/<slice>/views/helpers/one.rb
slices/<slice>/views/helpers/two.rb
For each new helper module you add, you’ll need to manually include them as you would any Ruby module. There is nothing special about helpers, they are normal Ruby modules so, once included, ensure you don’t have method name conflicts with other modules.
⚠️ Helpers have access to the routes
method so ensure you don’t override this.
Content For
Use of content_for
is is not a helper but deserves attention because it acts like a helper by allowing you to define content in a template for use in another template later. This is powerful when combined with your layout which allows your template to reach up into the layout and customize as necessary. This behavior is very similar to behavior found in other frameworks. Example:
# slices/home/templates/layouts/app.html.erb
<%= content_for :title %>
# slices/home/templates/show.html.erb
<% content_for :title, "Demo" %>
Context
The context can be accessed via context
. For example, you could obtain the flash by asking for it: context.flash
.
Scopes
Scopes can used via the scope
method. For example, the following builds and renders a scope:
scope(Shared::Flash, payload: context.flash).render "shared/flash", message: "Demo", kind: :alert
With the above, we are able to use the Shared::Flash
scope, provide a custom message
and kind
, in order to render the _flash
partial.
Rendering
Helpers have the ability to render partials via the render
method. Example:
module DemoHelper
def flash_notice
render "templates/flash", message: context.flash, kind: :notice
end
end
Standalone
When used outside of Hanami, you’ll not have automatic access to any of the helpers. One way to get around this is to create the following module for injection within any object that needs them. Example:
require "hanami/view"
module Demo
module Views
# Provides access to all Hanami View helpers.
module Helpers
extend Hanami::View::Helpers::TagHelper
extend Hanami::View::Helpers::EscapeHelper
extend Hanami::View::Helpers::NumberFormattingHelper
end
end
end
The above allows you to inject the default helpers while also providing a namespace for them. Example:
class MyView
def initialize helpers: Helpers
@helpers = Helpers
end
end
Now, when using the above, you can access all helpers via the helpers
object which is similar in nature to how you would use helpers within a native Hanami application. This also gives you a convenient way in which to add custom helpers to the Demo::Views::Helpers
object if desired.
Layouts
Layouts provide a global structure for all templates to be embedded within and are enabled by default. In situations where you don’t need a layout, you can pass layout: false
when rendering the view. For example, here’s a show action which renders a response where the layout is disabled for the view:
class Show < Home::Action
def handle(*, response) = response.render view, layout: false
end
Any layout information you pass to the view will take precedence over default configuration. This gives you a lot of flexibility in terms of dynamic behavior when rendering views for different layouts. For example, the following demonstrates messaging your view directly with default and custom layouts:
view.call # Renders HTML (default).
view.call layout: "json" # Renders structured JSON.
view.call layout: "text" # Renders plain text.
view.call layout: false # Renders without any layout.
At the moment, layouts can only be defined per slice. This is good for separation of concerns but means there isn’t a great way to have a shared theme or common set of functionality that could be inherited and overwritten by each slice. A workaround is to do this:
# demo/slices/home/view.rb
module Home
class View < Demo::View
config.paths = [
# Provides global application fallbacks.
Hanami.app.root.join("app/templates"),
# Use slice specific templates when found, otherwise use application fallbacks.
Pathname(__dir__).join("templates").expand_path
]
end
end
As mentioned earlier, you can’t append to config.paths
unless you use a Hanami::View::Path
object.
Templates and Partials
Templates allow you to consume data exposed via your view for rendering via your template engine (i.e. ERB). They can be broken down further into partials for greater reusability. A template can render a partial in multiple ways. The following demonstrates usage:
# Preferred. Uses `task` as the partial object.
<% tasks.each do |task| %>
<%= task.render :task %>
<% end %>
# Uses the `task` string as the partial object (this can be a relative path).
<% tasks.each do |task| %>
<%= task.render "task" %>
<% end %>
# Renames `task` as `next_action` for use in the partial.
<% tasks.each do |task| %>
<%= task.render :task, as: :next_action %>
<% end %>
# Passes `label` as additional data to the partial.
<% tasks.each do |task| %>
<%= task.render :task, label: "Test" %>
<% end %>
Escaping
You can escape multiple ways:
<%== demo %>
<%== demo.html_safe %>
<%= raw demo %>
None of the above are recommended because of security vulnerabilities and general Ruby Antipatterns. Instead, you should use the Sanitize and/or Loofah gems to ensure all output is properly sanitized.
Guidelines
-
Avoid using complicated logic, conditionals, etc. You can keep your templates and partials simple by moving all logic into your views, parts, scopes, helpers, etc.
Conclusion
I hope you’ve enjoyed this look at the capabilities of the Hanami View gem. If you’d like additional examples to further your learning, I’d recommend the following:
-
Hemo: A Hanami/htmx demonstration application which was initially generated using the Hanamismith gem.
-
Milestoner: A release notes and deployment automation gem which is built upon the Hanami View gem.