The letter A styled as Alchemists logo. lchemists
Published September 1, 2023 Updated July 11, 2026
Cover
Hanami Views

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.

Overview

From a high level, this is what the architecture looks like.

Cover

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 several years old now.

Setup

You might find the Hanamismith gem useful since it automates generating a Hanami application for you with Hanami View. I’ll be referring to Hanamismith throughout this article since this gem helps get you started on the right foot.

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

Then, using a tool like Eza, you can print a tree view of the home slice as shown here:

Home slice

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 Hanami Actions Show class, 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:

Home 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

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:

  1. Uses a show.html.erb template that renders the exposed label provided by the view.

  2. Uses a Show view which is configured to use the current working directory to look for the associated show.html.erb template.

  3. Uses a label dependency which defaults to "Demo" and is exposed for rendering within the show.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.

Prior to Hanami 3.x.x, exposures would be automatically decorated with parts. With 3.x.x and higher, you must specify which exposures you want decorated with parts by passing decorate: true to your exposure.

The following sections detail how you can use exposures.

Positionals

An exposure can depend on another by making the dependent exposure a positional argument. For example, see the status exposure below:

class Show < Hanami::View
  expose :playlist
  expose :current
  expose(:index) { |playlist:, current:| playlist.screens.index current }
  expose(:total) { |playlist:| playlist.screens.size }
  expose(:status) { |index, total| "Slide #{index} of #{total}" }
end

Use positional parameters when injected dependencies have positional/keyword parameters themselves. This is why index and total are positional parameters to status because they both have keyword arguments. In contrast, both the index and total exposures use keyword parameters since the playlist and current exposures require no parameters.

Keywords

Use keyword parameters to either require or optionally provide default values. Example:

# Find a task by required ID.
class Show < Hanami::View
  include Deps[repository: "repositories.task"]

  expose(:task) { |id:| repository.find id }
end

# 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

# Splat additional attributes.
class Show < Hanami::View
  expose(:comment) { |**attributes| "#{attributes[:label]}\n\n#{attributes[:content]}" }
end

Blocks

Use blocks for lazy evaluation. Example:

class Index < Hanami::View
  include Deps[:routes]

  expose(:tasks_path) { routes.path :tasks }
end

With the above, routes.path will only be evaluated once tasks_path is messaged.

Defaults

Exposures can have explicit defaults. Example:

class Index < Hanami::View
  include Deps[:routes]

  expose :tasks_path, default: "/tasks"
end

Unfortunately, you can’t use a block for lazy evaluation. Only literals are allowed.

As

Specify a part via the :as keyword. Example:

class Index < Hanami::View
  expose :form, as: Parts::Forms::Registration, decorate: true
end

The above ensures form is an instance of the Parts::Forms::Registration instead of Parts::Form which would be the implicit type.

The annoying aspect of custom parts is that you must pass decorate: true because the fact that you are specifying a custom part is not enough to automatically use it.

Decoration

To decorate your exposure with a part, use the decorate method. Example:

class Show < Hanami::View
  decorate :article
end

You can use expose with decorate: true but the above is preferred since you avoid the Boolean Parameter code smell:

class Show < Hanami::View
  expose :article, decorate: true
end

Context

You can access the context by passing it is via a keyword parameter:

class Show < Hanami::View
  expose(:article) { |context:| article.with user: context.current_user }
end

💡 For more on contexts, see the Contexts section below.

Layouts

By default, exposures are only available to the template but can be made available to the layout by using layout: true. Example:

class Index < Hanami::View
  expose :users, layout: true
end

Privates

Exposures can be private which prevents them from being visible to the template. This is handy when you need building blocks for your public exposures. Example:

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

With the above, user is a building block to the user_name exposure without ever exposing the user itself.

Actions

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

Inspection

You can obtain a list of exposures at both the class and instance level using either the .exposures or #exposures methods respectively. Annoyingly, you’ll need to use .exposures.exposures or #exposures.exposures to iterate through them, though.

Rendering

Every view has a rendering which is especially useful when you want access to the context since the context isn’t directly available by default. Example:

class View < Hanami::View
  private

  def context = rendering.context
end

Use of private is important because you don’t want to expose your rendering to your templates as this information should be used to only enhance your exposures. Once you have access to the context, then you can make use of routes, settings, and everything documented in the Contexts section.

The rendering is also useful 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, overrides the defaults as provided via your view:

MyView.new.rendering format: "json", context: MyContext.new

With the above, a custom format and context is supplied. Knowing this can come in handy when needing 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.

Call

All views can be called with the following keyword attributes:

  • format: The format which is usually :html. Default: config.default_format.

  • context: The context which is either the default Hanami context or your own custom context. Default: config.default_context.

  • layout: The layout for your templates which is either the default Hanami layout or your own custom context. Default: config.layout.

  • **input: These are the objects to be exposed as defined by your exposures.

Knowing the above means you can modify default behavior. Example:

class View < Hanami::View
  def call(format: config.default_format, **)
    format == :json ? super(**, format:, layout: nil) : super
  end
end

The above allows your views to never use a layout when the format is JSON because you don’t need layouts when rendering JSON output. Otherwise, you can fallback to default behavior.

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 by inspecting 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 exposure decoration behavior with associated parts.
  # Default: false
  config.decorate_exposures, default: true

  # 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 the form_for helper directly messaging yield.

  • 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:

Tests

When using your own dependencies, contexts are quick to test since you can setup your specs to inject spies as you like. The situation gets more complicated when wanting to simulate an HTTP request for use within your actions. Example:

RSpec.describe Demo::Views::Context do
  subject(:a_context) { described_class.new request: }

  let :request do
    Hanami::Action::Request.new env: {"HTTP_HX_REQUEST" => "true"},
                                params: {}
  end

  describe "#request" do
    it "answers htmx request" do
      expect(a_context.request).to eq(request)
    end
  end
end

Alternatively, you can use Rack to create a mock environment object if you need additional support. Example:

let :request do
  Hanami::Action::Request.new env: {"HTTP_HX_REQUEST" => "true"},
                              params: {}
end

let(:environment) { Rack::MockRequest.env_for "https://test.io", {"HTTP_HX_REQUEST" => "true"} }

If you don’t provide a request object, you’ll get the following error:

Hanami::ComponentLoadError: Request not available. Only views rendered from Hanami::Action instances have a request.

This stems from the fact that Hanami will mutate the view layer upon boot via the hanami/extensions/view/context.rb extension. This is hard to simulate when needing isolated specs for testing purposes. This becomes even more complicated when using the view layer outside of Hanami and later wanting to include your custom views, via a gem, into your Hanami app as noted in this issue.

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.

Guidelines

When implementing parts, adhere to the following:

  • Use a part to augment and enhance the value object that it wraps for presentation purposes.

  • Use (add) methods that don’t override the value’s methods for which your part wraps. This is important because your part will delegate to the value’s methods when not implemented on the part giving you the best of both worlds.

  • Avoid mixing helper logic within your parts. Instead, keep your parts solely focused on providing additional behavior you can use within your templates and partials. This also ensures behavior is not restricted to a particular format and can be used with multiple templates/formats (i.e. HTML, JSON, etc.) which makes your part much more reusable.

  • Avoid rendering partials within your parts via the render method even though the method is available to you. Again, parts are purely for presentation which provides behavior your can use within your templates and partials. You don’t want to be rendering them within your part as this makes your parts much harder to test and introduces too much responsibility.

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 to your partial. Example: render "relative/path/to/partial". Also, as mentioned, in the Guidelines, you should avoid rendering partials as they make maintaining your parts much more complicated.

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

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:

  1. I’m using the #span method on the tag 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.

  2. When initializing the part (subject) in my spec, I must pass in Tasks::View.new.rendering for rendering purposes, otherwise you’ll end up a NoMethodError: 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 is nil when not associated with a view from your slice. This is why an instance of Tasks::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.

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.

Guidelines

When implementing scopes, adhere to the following:

  • Use scopes to encapsulate highly focused behavior around a tiny UI component. Examples include form fields, page headers, cards, panels, asides, and so forth.

  • Use scopes to extract conditional logic from your templates and partials in order to reduce the testing burden as scopes are much easier to test than an entire template or partial because you’ve isolated behavior down to a single responsibility.

  • Ensure your scopes directory is a sub-directory of views (i.e. views/scopes) in order 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 scope 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 all the caveats of inheritance so keep your behavior well encapsulated.

  • When 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.

Locals

When building a scope, you have immediate access to the locals. For example, consider the following keyword arguments that are passed into this demo scope:

scope :demo, label: "Demo", name: "demo"

Within your scope, you have immediate access to the label and name variables. You can also fetch the values by from the locals object itself:

locals.fetch :label, "Unknown"
locals.fetch :name, "unknown"

Use of locals gives you a powerful way in which to provide a fallback value when keys are missing. This also allows the keys passed to your scope to be optional.

Context

Scopes have access to the context via the context object. This means you can access all objects when first discussing Contexts.

There are times, when implementing a scope, when you need access to the current request which. The problem is that Hanami doesn’t make this automatically available to you for testing purposes. Instead, you’ll get the following error in your tests:

Hanami::ComponentLoadError: Request not available. Only views rendered from Hanami::Action instances have a request.

Using RSpec, here’s how to setup your scope so it has a request for testing purposes:

RSpec.describe Demo::Views::Scopes::MenuItem do
  subject(:scope) { described_class.new locals:, rendering: Demo::View.new.rendering(context:) }

  let(:locals) { {label: "Tasks", path: "/tasks"} }
  let(:context) { Demo::Views::Context.new(request:) }

  let :request do
    Hanami::Action::Request.new env: Rack::MockRequest.env_for("/tasks"), params: {}
  end
end

The difference with this setup, versus what is described in Tests, is you need to:

  1. Build a fake request so you can quickly alter your path.

  2. Build a context which uses your fake request.

  3. Build a rendering which uses your modified context so you have access to the request.

This allows you to avoid the Hanami::ComponentLoadError error and test your scope as used by your action.

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 ever 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 to render a default partial, you can do this by specifying the default path in your render method then sending up to super. Example:

class Form < Hanami::View::Scope
  def render(path = "shared/form") = super
end

The above will default to templates/shared/_form.html.erb unless you pass 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:

<!-- templates/books/_form.html.erb -->
<section>
  <%= yield %>
</section>

<!-- 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. You also can’t render a partial using HTML helper methods outside of Hanami (i.e. standalone) or you’ll get a dasherize-related stack dump. To avoid this, stick with basic HTML tags.

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 used within a Hanami application:

Implementation

# frozen_string_literal: true

require "refinements/array"

module Demo
  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

require "hanami_helper"

RSpec.describe Demo::Views::Scopes::Description do
  subject(:scope) { described_class.new locals:, rendering: Demo::View.new.rendering}

  let(:locals) { {content: "Test", error: {description: ["Danger!"]}} }

  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

To simplify spec setup, use the view rendering of your main view. This means if your Hanami application is called Demo, then you can use Demo::View.new.rendering as your rendering which will allow your scope instance to have access to assets, view helpers, and default context.

Standalone

When used outside of Hanami, the default folder structure is not auto-detected so you must be explicit with 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 due to causing the following exception:

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

To workaround this issue, you can inject the helpers you need as a dependency instead. See Helpers: Standalone section for details.

When writing specs, you’ll want to provide an anonymous view along with a context (if needed). Example:

RSpec.describe Demo::Views::Scopes::Logo do
  subject(:scope) { described_class.new locals: {}, rendering: view.new.rendering(context:) }

  let :view do
    Class.new Hanami::View do
      config.paths = [Bundler.root.join("lib/milestoner/templates")]
      config.template = "n/a"
    end
  end

  let(:context) { Demo::Views::Context.new settings: }
  let(:settings) { {label: "Test"} }
end

The context is optional and can be removed if you are not using the context but the above shows how to wire all of this together if needed.

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:

Forgery Protection

An important helper to be aware of is the csrf_token helper which is used to prevent Cross-Site Request Forgery (CSRF) attacks when you have sessions enabled for your Hanami Actions. Once enabled, ensure all of your forms are protected by using a hidden input field with the CSRF token being the value. You can use either of the following to protect yourself:

<!-- With tag helper -->
<%= tag.input name: :_csrf_token, type: :hidden, value: csrf_token %>

<!-- With form object -->
<%= form.hidden_field name: :_csrf_token, value: csrf_token %>

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" %>

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 added, 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.

Context

The context can be accessed via context. For example, you could obtain the flash by asking for it: context.flash.

Scopes

Scopes can be 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

Assets

Accessing assets via your helpers within views, parts, and scopes will lead to surprising behavior. At the moment, usage is quite buggy, so tread with caution. For example, you might be tempted to use either of the following (both accomplish the same thing):

helpers.asset_path "demo.svg"
context.assets["demo.svg"]

The problem is this will work in all environments except the test environment due to the following error popping up:

"Hanami::ComponentLoadError: Assets not available. Have you put files into your assets directory?"

The work around is to use the following solution by importing your assets and using them directly:

include Deps[:assets]

assets["setup.svg"].path

This has issues too as you’ll have to write your specs with regular expressions to test the path because, when running locally, you’ll end up with paths without cache busting suffixes (i.e. -6M3WXAC3.svg) but on CI the cache busting suffixes are included which can lead to flakey specs.

There’s another caveat to importing assets like this (i.e. include Deps[:assets]) which is you can’t use asset related helpers in your templates/partials once imported. Example:

<%= image_tag playlist.current_screen_uri,
              alt: "Screen",
              class: :pulsing_border,
              loading: :lazy,
              width: 200,
              height: 120 %>

You must use HTML tags instead:

<img src="<%= playlist.current_screen_uri %>"
     alt="Screen"
     class="pulsing_border"
     loading="lazy"
     width="200"
     height="120">

The reason is because the asset path won’t be computed when using the tag helpers (image_tag in this case).

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 %>

Locals

The keyword arguments you pass to your template and/or partial are available as a locals hash. For example, let’s say you passed in name and email to your person partial as follows:

<%= render :person, label: "Jane Doe", email: "jdoe@demo.io" %>

This means, within your _person.html.erb partial, you’d have access to the label and name either directly as variables or via the locals hash:

<!-- Direct Access: "Jane Doe (jdoe@demo.io)" -->
<%= "#{label} (#(email))" %>

<!-- Locals Access: "Jane Doe (jdoe@demo.io)" -->
<%= "#{locals[:label]} (#{locals[:email]})" %>

In most cases, you’ll want to directly access the local variables but knowing you have a locals hash means you can provide safe fallbacks when some keywords are missing or optional. Example:

<!-- Fallback -->
<%= locals.fetch :email, "unknown@demo.io" %>

This makes for an elegant way to provide a fallback (default) value when keys are missing.

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.

Errors

Errors can be difficult to debug since Hanami doesn’t always have the best error handling. One situation that can pop is when you forget to add a closing quote when rendering a partial. Example:

<!-- Bad: Notice the closing quote is missing. -->
<%= render "popovers/label %>

<!-- Good: Notice the partial properly quoted. -->
<%= render "popovers/label" %>

If you miss a closing (or starting) quote, like this, then you can end up with the following syntax error:

syntax errors found: locals = locals[:locals]
m... \n".freeze
unexpected backslash, ignoring it

While quite confusing, the stack dump should point you to the line in your code where there error is. So ignore the error message, fix the line, and you’ll be good-to-go.

Slices

When using slices, it’s handy for your slice to fallback to your application’s templates when your slice doesn’t have the necessary template. To enable, you only need to configure your slice’s view to add the relative path to the application’s templates like so:

module Authentication
  # The slice view.
  class View < Demo::View
    config.paths += ["app/templates"]
  end
end

The above will allow you to reference templates and partials in your application which your slice doesn’t have in order to reduce duplication. This isn’t full proof because if your application’s templates use assets, your slice won’t be able to find them so your mileage may vary.

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. For an additional example, I’d recommend checking out the Milestoner gem for release notes and deployment automation gem which is built upon the Hanami View gem.