Hanami 1.3 > Hanami 2.2 : tips & notes about my journey

As a long time Hanami user (8 years) for my SaaS company, I’m currently working on the migration from 1.3 to 2.2. Thanks to the team for this amazing piece of software :hugs:

With a quite big codebase and many things that work quite well, my first step is to keep most of the things in place, while embrassing some new concepts.

I’m making good progress, and I take notes along the way. I think that they may interest some fellow migrators like me, so here it is, I’ll add stuff in this thread.

6 Likes

Use case #1: compiling scss files

I like using scss files, useful for class nesting, variables, and few other things. Hanami 1 did that out of the box IIRC, so I wanted to be able to do it in Hanami 2, while keeping the official assets pipeline

After a few search in the guide + forum + a bit of ChatGPT, here is what works for me:

  • Add the dedicated plugin for esbuild (this will update package.json)

npm install --save-dev esbuild-sass-plugin sass

  • Load the plugin for asset compilation in app/config/assets.js (or slices/your_slice/config/assets.js if you want to configure one specific slice only)
import * as assets from "hanami-assets";
import { sassPlugin } from "esbuild-sass-plugin";

await assets.run({
  esbuildOptionsFn: (args, esbuildOptions) => {
    // Modify the esbuild options to include the sass plugin
    esbuildOptions.plugins = [
      ...(esbuildOptions.plugins || []), // preserve any existing plugins
      sassPlugin() // add sass plugin
    ];
    return esbuildOptions;
  }
});
  • Import the scss file in the app/assets/js/app.js file (or slices/your_slice/assets/js/app.js if you’re in a specific slice)
import "../css/app.css";
import "../css/your_file.scss";

This part was the most counter-intuitive for me as I was handling some stylesheet files, but I understand that it’s part of the compilation pipeline

With that in place, your main app.css compiled file, that you can load in your layout with <%= stylesheet_tag "app" %> will have the content of both app.css and your_file.scss files :tada:

@timriley would it make sense to enrich the official doc with concrete example like that? just let me know

Use case #2: slice based application, only start one slice at the time

I like monoliths when it comes to code base sharing, but I prefer spawing individual services (app / api / backoffice / pdf renderer… etc) for the run (dev or prod, doesn’t matter)

With Hanami 1.3 I’ve been doing that from the beginning by using the different apps (subfolder of apps directory)

With Hanami 2, obviously the slices are made for that. But I’ve came to the point where my app is almost empty, and I want to only start individual slices.

Here is how I did that (a bit simplified for the sake of example)

# config/routes.rb
# frozen_string_literal: true

module MyApp
  class Routes < Hanami::Routes
    
    if ENV["HANAMI_SLICES"] == "api"
      slice :api, at: "/" 
    elsif ENV["HANAMI_SLICES"] == "backoffice"
      slice :backoffice, at: "/"
    end
    
  end
end
# slices/api/config/routes.rb
# frozen_string_literal: true
require_relative "../middlewares/rack_global_error_handler" # an example of a custom middleware

module API
  class Routes < Hanami::Routes

    # Here is how I inject my custom middleware in the stack
    use API::Middlewares::RackGlobalErrorHandler

    # We are interested in the body of the request
    use :body_parser, :json

    scope 'api' do
      scope 'v1' do
        get  "/status", to: "v1.utils.status", as: :utils_status # pointing to an action in my slice
      end
    end
    
  end
end

Then we need to tweak Procfile a little bit by defining specific targets

#Procfile.dev
api: HANAMI_PORT=2303 bundle exec hanami server
backoffice: HANAMI_PORT=2308 bundle exec hanami server
assets: bundle exec hanami assets watch

And finally we adjust foreman launching to adapt to the slice we want to launch:

# bin/dev
#!/usr/bin/env sh

if ! gem list foreman -i --silent; then
  echo "Installing foreman..."
  gem install foreman
fi

# API slice does not need assets watching
if [ "$HANAMI_SLICES" = "api" ]; then
  FOREMAN_ASSETS=""
else
  FOREMAN_ASSETS=",assets=1"
fi

FOREMAN_M="$HANAMI_SLICES=1$FOREMAN_ASSETS"

exec foreman start -f Procfile.dev "$@" -m $FOREMAN_M

You can then launch the api slice alone :tada:

HANAMI_SLICES=api bundle exec hanami dev

Or the backoffice slice, which will include assets watching, with:

HANAMI_SLICES=backoffice bundle exec hanami dev
1 Like

Use case #3: load boostrap icons

More generally speaking, this example could help someone trying to integrate external css files from node modules

I’ve been a long time user of bootstrap glyphicons since v3, which can be used as a font and which I find very easy to use. But they abandonned them at some point. Now they are back, in a dedicated package. So I took the time to integrate them properly in my fresh Hanami 2 project

  • First, add the module with npm i bootstrap-icons (this will update package.json)

  • Load the plugin for asset compilation in app/config/app.js (or slices/your_slice/config/app.js if you want to configure one specific slice only)

// Import boostrap icons only
import "bootstrap-icons/font/bootstrap-icons";

import "../css/app.css";

The css plugin will then be included in the built asset file app.css that should already be included in your layout thanks to the stylesheet_tag helper

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <%= favicon_tag %>
    <%= stylesheet_tag "app" %>
  </head>
[...]

The project link: https://icons.getbootstrap.com/

1 Like

That is very cool of you to document the challenges with the migration. Something I saw/heard a lot of people complain about was the lack of official guidelines when migrating from 1.3 to 2.x I think it would be really valuable to add those as some more official notes to the docs

1 Like

Thank you for your feedback. I know that it is very specific to my experience, which is one way to use Hanami, but if it can help some people, it would be amazing. For the moment I just document along the way and we’ll see where we can go. But I definitely think that part of this info could go into a documentation, or a cookbook

Use case #4: use Hanami interactors

I’ve always loved this “interactor” pattern brought by Hanami / Lucas. I know it has some flaws, and I wish I could change a few things, but I have to chose my battles. And I must say that using interactors helped me build my tool the way it is now.

I have 300 of them currently in production, so my goal while migrating to Hanami 2 is to keep them like that for the moment. I’ll consider how to go forward once mi migration is done.

Now that the lib is not part of the Hanami core any more, I started by using the drop-in replacement proposed by Nerian.
Here is a way to do it:

Add the gem to your Gemfile

# Gemfile
gem 'simple-interactors'

Then launch bundle install
The class can be used with a simple require 'hanami/interactor' anywhere you want

But I wanted to be a bit wiser when using them, and tried to use the providers, in what could be seen as an exotic way, but that I love !

Here is my provider:

# config/providers/interactors.rb
Hanami.app.register_provider(:interactors) do
  prepare do
    require 'hanami/interactor'
  end

  start do
    total_loaded = 0
    Dir["./lib/myapp/interactors/*.rb"].each do |file|
      require file
      total_loaded += 1
    end

    register "interactors", true

    target["logger"].info "🟢 Interactors loaded (#{total_loaded} files) ✓"
  end
end

As they are, by nature, shared accross slices, I mark them as shared in my app class

# config/app.rb
# frozen_string_literal: true

require "hanami"

module Myapp
  class App < Hanami::App

    # Some path can be excluded from auto registration into container:
    # config.no_auto_register_paths << "structs"

    # Expose some components in every slices:
    config.shared_app_component_keys += [
      "interactors",
    ]

#[...]

I can finally load them in a slice with:

module API
  class Action < Myapp::Action

    include Deps[
      "interactors",
    ]

#[...]

I still need to instantiate them by hand when I need them, but at least they are correctly lazily loaded :tada:

Thanks for sharing this approach with the interactors, @inouire!

This is interesting for me to see. I know you’re doing everything you do to streamline the upgrade path, so I understand you have to make compromises here, but I do want to ask you a couple of questions :slight_smile:

On providers: you should only need a provider if you have to do a special one-off configuration somewhere, or you otherwise want to register useful objects in the container.

Your provider doesn’t look like it does either. And to boot, it looks like your interactors are in /lib/. Classes in /lib/ won’t be auto-registered into the app container, which for your interactors is good, because their design is not compatible with auto-registration¹. However, classes in /lib/ are auto-loaded via Zeitwerk, so you should just be able to type their class name in a file and they’ll be loaded automatically. To me, this seems like this should already be enough? What exactly was going on such that you needed this provider?


¹ Looking at the Hanami 1.x interactor design via the SimpleInteractors README, these old-style interactor objects are not designed in a way that will allow container registration. They receive their per-request arguments via #initialize, whereas our container expects auto-registered objects to be initialized via a single call to .new, no arguments. It also expects those objects to be able to be used multiple times after initialization, whereas the old-style Hanami interactors are one-time-only objects.

Thanks Tim for the hint about auto loading. Maybe I lost myself while working on the migration, so I’ll try it with Zeitwerk auto-loading and let you know how it goes!

1 Like

@timriley ok, now I remember why I did that. The interactors are copied as-they-are from my legacy project, allowing me to smoothly prepare my migration (v2), while still modifying things in production (v1.3). So the interactor classes don’t have the expected path for auto loading, unless I tweak each one of them a bit, which I don’t want to do right now.

So my “provider” (which is not a real one, I agree) is a sort of poor man’s autoloading, allowing to bootstrap my legacy code into the new one.

Leveraging the true power of the container will be one of the first projects, after I migrated (I have 4 projects to migrate, of of them being really huge, so it could take time)

Use case #5: import js libraries (like HTMX or Alpine)

In my Hanami 1.3 app I imported a few js libs with a plain old <script> tag referencing the js files placed manually in the assets.

With a great assets pipeline in place in Hanami 2, I wanted to import js libraries in a clean way. Nothing fancy, and it’s already covered in the main doc, but I thought it was worth explaining the few steps needed.

First you need to add the libraries yout want as npm dependencies in packages.json, for example:

{
  "name": "myapp",
  "private": true,
  "type": "module",
  "dependencies": {
    "hanami-assets": "^2.2.1",
    "htmx.org": "^2.0.6",
    "alpinejs": "^3.14.9"
  },
  "devDependencies": {
    "esbuild": "^0.25.4",
  }
}

You can then download the packages with npm install

The libraries are now ready to be included in your main js file, for example for a specific slice slices/backend/assets/js/app.js:

import "htmx.org/dist/htmx.js"

import Alpine from 'alpinejs'
window.Alpine = Alpine
Alpine.start()

This main js file itself has to be included in your layout slices/backend/templates/layouts/app.html.erb

<%= javascript_tag "app" %>

The js libraries are now loaded with each page!

Note: I also needed to tweak the CSP directives for the slice in order to make the libraries work, I’ll treat that in my next use case

I am also doing this, and I solved it differently with a monkeypatch: How are slice routes intended to interact with HANAMI_SLICES?

1 Like

Use case #6: tweaking CSP directives to make AlpineJS work

In my previous use case, I included AlpineJS into my project; but it didn’t work at first try. Indeed, this library needs a few “unsafe” settings in order to work. CSP headers are used to notify the browser that you allow this kind of manipulation.

I didn’t find an official way to modify CSP per slice in the framework, so for the moment I decided to write my own “middleware” to do that. (Note: I know it’s not a real Rack middleware, just an included module with a before hook, but it’s the spirit is the same so that’s why I named it that way. You could definitely do that will a real Rack middleware BTW)

Here is the code of my middleware:

# slices/my_slice/middlewares/set_csp_directives.rb

# auto_register: false

module MySlice
  module Middlewares
    module SetCSPDirectives
     
      def self.included(action_class)
        action_class.before :set_custom_csp_directives
      end

      def set_custom_csp_directives(request, response)
        custom_directives = DEFAULT_CSP_DIRECTIVES.merge(
          "script-src"     => "'self' 'unsafe-eval'",
          "style-src-elem" => "'self' 'unsafe-inline'",
        )

        response.headers["Content-Security-Policy"] = custom_directives.map do |key, value|
          "#{key} #{value}"
        end.join(";")
      end

      # In my real app this constant is defined for my whole project, putting it here for the example
      DEFAULT_CSP_DIRECTIVES = {
        "base-uri"        => "'self'",
        "child-src"       => "'self'",
        "connect-src"     => "'self'",
        "default-src"     => "'none'",
        "font-src"        => "'self'",
        "form-action"     => "'self'",
        "frame-ancestors" => "'self'",
        "frame-src"       => "'self'",
        "img-src"         => "'self' https: data:",
        "media-src"       => "'self'",
        "object-src"      => "'none'",
        "script-src"      => "'self'",
        "style-src"       => "'self' 'unsafe-inline' https:",
        "style-src-elem"  => "'self'",
      }

    end
  end
end

I can then include it in my stack, like this:

module MySlice
  class Action < MyApp::Action

    include MySlice::Middlewares::SetCSPDirectives

    include Deps[
      "logger",
    ]

  end
end

The browser will then receive these patched CSP on each request, allowing AlpineJS to work properly. :tada:

Use case #7: defining the way we would use parts

One of the aspects I expected the most out of Hanami2 was the improvements about the views & templates.

More modular, more Ruby (less ERB), no code duplication: I knew that we would greatly benefit from more decoupling. Once it was out, we had to find our new methodology and to pick our preferred way of working in all the possibilities offered by the view layer. It took me some time, but I think I’ve found a way that suits us, I’ll try to explain it here.

(Disclaimer: it’s just one way to use the framework, optimized for our daily use with productivity and longevity in mind. I’m sure there are many more ways to do. Feel free to share !)

PARTS: here is our rule: if you need to render a chunk of html related to a core object: use a part instead of a raw partial

A part is some ruby code wrapping (aka decorating) another object of yours, allowing you to attach presentation related stuff to it, without the original object being aware of it.

We decided to define 3 kinds of methods allowed on a part:

  • render methods, the entry points to render a part, starting with ‘render_’
  • presentation methods, called by html partials, starting with ‘_’
  • internal methods, called by information methods, regular methods

It would look like this

# auto_register: false

module MySlice
  module Views
    module Parts
      class Product < MySlice::Views::Part

        # Rendering functions, should start with render (Deck convention proposal)

        def render_product_card(is_small: false, is_addable_to_selection: false)
          render("partials/product/product_card",
            is_small: is_small,
            is_interact_droppable: false,
            is_addable_to_selection: is_addable_to_selection,
          )
        end

        def render_interact_droppable_product_card
          render("partials/product/product_card",
            is_small: true,
            is_interact_droppable: true,
            is_addable_to_selection: false,
          )
        end

        def render_product_main_picture(is_small: false)
          render("partials/product/product_main_picture",
            is_small: is_small,
          )
        end

        def render_sample_product_title
          helpers.tag.li(displayed_name)
        end

        # Presentation functions, should start with _ (Deck convention proposal)

        def _truncated_displayed_name(limit = 50)
          if displayed_name.size > limit
            displayed_name(raw_value: true)[0..limit] + "[..]"
          else
            displayed_name(raw_value: true)
          end
        end

        def _img_manufacturer_logo
          helpers.manufacturer_logo(value)
        end

        def _img_main_picture(size: 300)
          helpers.product_image(value, size: size)
        end

      end
    end
  end
end
<!-- partials/product/_product_main_picture.html.erb -->
<div class="" style="<%= is_small ? 'width: 80px' : 'width: 300px' %>;margin: 2px;position: relative;">
  <%= product._img_main_picture(size: is_small ? 80 : 300) %>
</div>

And would be called like this in any partial, assuming we have declared expose :products, as: :product in the view

<% products.each do |product| %>
    <%= product.render_product_main_picture %>
<% end %> 

The idea behind this is to use one of the (potentially many) render methods as soon as we want to display a core object. It’s main responsibility is to target the correct partial and give i the correct locals.

And to put any display logic that the erb partial would need, in the presentation methods. Making them start with “_” has been chosen to avoid the confusion with native methods on the objects, that can be quite complicated on their own.

Next episode, using SCOPES !
Thank you for reading

Use case #8: defining the way we would use scopes

Scopes in Hanami 2 are a new concept, to provide methods available from a template

It took me some time to figure out what they were doing exactly, maybe because of the generic wording. After a few tests, I use them as “components” in my mind, a bit like parts, but not around a specific object, more a zone of the screen.

SCOPES: here is our rule: if you need to render a chunk of html that is not totally simple, and not related to a core object but more to a component: use a scope instead of a raw partial

It’s like augmenting your html partial, by providing ruby functions, so that they don’t end up in the html template. You can handle better default values on locals, too.

As per our strategy on parts, we defined 3 kinds of methods:

  • render methods, the entry points to render a scope, starting with ‘render_’
  • presentation methods, called by html partials, starting with ‘_’
  • internal methods, called by information methods, regular methods

Here is how it looks:

# auto_register: false

module MySlice
  module Views
    module Scopes
      class Header < MySlice::View::Scope

        def render_header
          render("partials/navigation/header", 
            current_language: current_language,
            available_languages: available_languages,
          )
        end

        def _choosable_languages
          available_languages.except(current_language)
        end

        def _is_on_index_page?
          @is_on_index_page ||= current_path?("/#{current_language.code_iso}")
        end

      end
    end
  end
end
<!-- slices/my_slice/templates/partials/navigation/_header.html.erb -->
<div class="header">
  <div class="header__content <%= 'header__content--limited' if _is_on_index_page? %>">
    <div class="header__logo">  
      <%= image_tag(logo-white.png")%>
    </div>

    <div class="header__block">
      <div class="language-switcher" aria-label="Menu" x-data="{ open: false }">
        <div class="language-switcher__selector" @click="open = ! open"  @click.outside="open = false">
          <img src="<%= current_language.flag_url %>"/>
          <span style="margin-right: 5px">
            <%= current_language.code_iso %>
          </span>
          <i class="bi bi-caret-down-fill" x-show="!open"></i>
          <i class="bi bi-caret-up-fill" style="display:none" x-show="open"></i>
        </div>

        <div class="language-switcher__dropdown" style="display:none" x-show="open">
          <% _choosable_languages.each do |language|%>
            <div class="language-switcher__dropdown_item">
              <img src="<%= language.flag_url %>"/>
              <span><%= language.code_iso %></span>
              
              <!-- I realise that this logic could be in the scope as well :D. I'll do that-->
              <% if _is_on_index_page? %>
                <%= link_to("", routes.path(:index, :lang => language.code_iso)) %>
              <% else %>
                <%= link_to("", routes.path(:change_language, :lang => language.code_iso, :path => request.fullpath.sub('/', ''))) %>
              <% end %>
            </div>
          <% end %>
        </div>
      </div>    

      <div class="header__selection">
        <%= link_to("sélection", routes.path(:project_new)) %>
      </div>
    </div>
  </div>
</div>

And this is how it would be called from a template (the layout, in that example):

<!-- slices/my_slice/tempates/layout/app.html.erb -->

<!DOCTYPE html>
<html>
  <body>
    <%= scope(:header, 
      current_language: current_language,
      available_languages: available_languages
    ).render_header %>
    
    <%= yield %>
  </body>
</html>

With that way of working with scopes, we can think in a similar manner while working our global page, distinguishing between:

  • parts for objects
  • scopes for components

and leveraging ruby code for presentation logic.

Let me know what you think especially if this approach rings a bell, or shocks you in any way!
Thanks for reading, see you on next episode.

Use case #9: delegating the work to another action

We had this use case of an url called from several use cases from the js code (drag & drop stuff). We didn’t want to make the js aware of the different use cases, but still we wanted to split the code in different actions.

This was pretty simple thanks to a basic delegation pattern, because everything is modular.
Here is an example:

# slices/my_slice/config/routes.rb

module MySlice
  class Routes < Hanami::Routes
    get "/drag-and-drop",  to: "project.generic_drag_and_drop"
  end
end
# slices/my_slice/actions/project/generic_drag_and_drop.rb
# frozen_string_literal: true

module MySlice
  module Actions
    module Project
      class GenericDragAndDrop < CatalogueStudio::Action
        
        # Import another actions that will do the work
        # explicitely naming them is not mandatory but I prefere like it
        include Deps[
          drop_product_action: "actions.project.drop_product",
          drop_asset_action: "actions.project.drop_asset",
        ]

        def handle(request, response)
          if request.params[:product_id]
            drop_product_action.handle(request, response)
          elsif request.params[:asset_id]
            drop_asset_action.handle(request, response)
          end
        end
      end
    end
  end
end
# slices/my_slice/actions/project/drop_product.rb
# frozen_string_literal: true

module MySlice
  module Actions
    module Project
      class DropProduct < CatalogueStudio::Action
       
        def handle(request, response)
          # doing the real work about product drop!
        end
      end
    end
  end
end

That’s’ it. Everything will behave like the action doing the work was called directly

If you want to do parameter checking in the parent action before delegation, see my post Subordinate Actions, or: Dynamic Dispatch

1 Like

Thanks @alassek , I’ll take a look!