V2.2: Overview


Hanami provides a complete view system for rendering HTML, JSON and other formats.

In addition to helpers, templates and partials, features like exposures, parts and contexts provide a comprehensive toolset for well-architected view code.

For a guided walk-through of Hanami with some example use of views, be sure to check out the Getting Started Guide.

Introduction

A typical Hanami view has a view class and a template. Both are created for you when you generate an action:

$ bundle exec hanami generate action home.show

Created app/actions/home/show.rb
Created app/views/home/show.rb              <- view
Created app/templates/home/show.html.erb    <- template

You can also generate a view independently of an action. This is useful for standalone views that render emails, instant messages or other background rendered content:

$ bundle exec hanami generate view emails.welcome

Created app/views/emails/welcome.rb
Created app/templates/emails/welcome.html.erb

Rendering a home page

Here’s the view class generated for the home show action above:

# app/views/home/show.rb

module Bookshelf
  module Views
    module Home
      class Show < Bookshelf::View
      end
    end
  end
end

And the corresponding template at app/templates/home/show.html.erb:

<h1>Bookshelf::Views::Home::Show</h1>

Let’s change this template to say “Welcome to Bookshelf”, then configure a route to render the home show action for requests to the root.

<h1>Welcome to Bookshelf</h1>

The route:

# config/routes.rb

module Bookshelf
  class Routes < Hanami::Routes
    root to: "home.show"
  end
end

The home show action:

# app/actions/home/show.rb

module Bookshelf
  module Actions
    module Home
      class Show < Bookshelf::Action
        def handle(*, response)
        end
      end
    end
  end
end

Requests for the root will now render a home page:

Welcome to Bookshelf

Understanding views

A Hanami view has a few important responsibilities. These include deciding what template to render, as well as what data to expose to that template and how.

When it comes to data, views can either accept data as input, fetch it themselves, or a mix of the two.

Let’s say the Bookshelf application needs to render a list of books on a books index page. An ERB template for that might look like:

<h1>What's on the Bookshelf</h1>
<ul>
  <% books.each do |book| %>
    <li><%= book.title %></li>
  <% end %>
</ul>

To provide the books this template needs, the view can fetch them from a book repository (or any source of its choosing), then expose them to the template by creating a books exposure using the expose method:

# app/views/books/index.rb

module Bookshelf
  module Views
    module Books
      class Index < Bookshelf::View
        include Deps["repos.book_repo"]

        expose :books do
          book_repo.listing
        end
      end
    end
  end
end

The result of the block passed to the exposure is now available in the template as books.

If the book repo also offered a list of best selling titles, the view could expose those through a second exposure:

# app/views/books/index.rb

module Bookshelf
  module Views
    module Books
      class Index < Bookshelf::View
        include Deps["repos.book_repo"]

        expose :books do
          book_repo.listing
        end

        expose :best_sellers do
          book_repo.best_sellers
        end
      end
    end
  end
end

Where they are available via best_sellers:

<h1>What's on the Bookshelf</h1>
<ul>
  <% books.each do |book| %>
    <li><%= book.title %></li>
  <% end %>
</ul>

<h2>Don't miss these best selling titles</h2>
<ul>
  <% best_sellers.each do |book| %>
    <li><%= book.title %></li>
  <% end %>
</ul>

Exposures can also decorate the objects they provide to the view via Parts, which allow view specific behaviours to be added to objects.

Views can also accept input (for example, they might take an :id parameter from an action), and renderer different formats (i.e. the same view can render both HTML and JSON).

They can also be configured to use a different templates and layout to their defaults:

# app/views/books/index.rb

module Bookshelf
  module Views
    module Books
      class Index < Bookshelf::View
        config.template = "books/index" # configure which template to render
        config.layout = "app" # configure which layout to use. Set to false or nil to use no layout
      end
    end
  end
end

See more in configuration.

Stand-alone rendering

One of the most useful properties of views is that they can be used outside of the HTTP request cycle. This means they can be used to render emails, instant messages or other content in background processes. It also means they’re readily testable.

We can see this in action in the Hanami console:

bundle exec hanami console

bookshelf[development]> Hanami.app["views.home.show"].call.to_s
=> "<html><body><h1>Welcome to Bookshelf</h1></body></html>"

And in an RSpec example:

# spec/views/home/show_spec.rb

RSpec.describe Bookshelf::Views::Home::Show do
  subject(:view) { described_class.new }

  describe "#call" do
    subject(:output) { view.call }

    it "renders a welcome heading" do
      expect(output.to_s).to match "<h1>Welcome to Bookshelf</h1>"
    end
  end
end