The letter A styled as Alchemists logo. lchemists
Published September 1, 2023 Updated April 3, 2025
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. 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.

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

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

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

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:

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

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:

  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.

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:

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: