Programistically choose a layout for Hanami::View

Is there a way, to conditionally change the layout for a particular view? Or to render in in a layout/without a layout based on… request or whatever logic comes before the rendering I guess.

Here is an example:

module Main
  module Views
    module Register
      class New < Main::View
      end
    end
  end
end
module Main
  module Actions
    module Register
      class New < Main::Action
        def handle(request, response)
        end
      end
    end
  end
end

The view is rendered from this action. No extra logic. It is normally rendered when entering /register endpoint. However I also have a point in my / path, where user can click “REGISTER” not in the navbar, but inside some hero section. Then HTMX makes a request to /register and replaces part of the page content with the rendered template. However it is rendered with the whole layout. So I need to change the layout before rendering, instead of at config level. HTMX sends some extra headers that I could use to determine whether I should use the layout or not.
Right now I see no way of doing this. Searched the documentation and checked the view object config, but there is not way of changing it (without interfering with the internals in meta way).

Is there an option of doing it? Or do I just need two views? Decide which view to render based on a header, and the only difference between them would be config.layout = false in one of them.

I like to have explicit classes for both.

For example, in my Hanami Mastery app I made the base view fir htmx layout-free stuff

I checked a while ago and reading the information about layout from request headers was not yet supported, I can follow up next week or so.

1 Like

Thanks for asking this question, @krzykamil! This is definitely possible.

To answer the question, let’s start by looking at how Hanami views themselves can have their layout specified. This is typically done via config.layout = "standard_layout" in a view class body, but you can also change it at call time, too: my_view.call(layout: "different_layout").

Next, we can look at how views are called from actions. Views are rendered on the action’s response, like so:

response.render(view, **options)

Under the hood, rendering on a response will call the view object and pass those options (among others) as arguments:

def render(view, **input)
  view_input = {
    **view_options.call(request, self),
    **exposures,
    **input
  }

  self.body = view.call(**view_input).to_str
end

This already gives us one approach for changing the layout when an action is called: update our handle method to render the view and specify the layout as required:

def handle(request, response)
  # `view` is automatically available if we have a matching view class
  response.render(view, layout: "different_layout")
end

In the above approach, you can add whatever logic you need to change "different_layout" to the right value for your circumstances. I presume you want to inspect headers and change the layout if the request is made by HTMX, or similar.

This approach is fine, but it’s probably not the cleanest if you wanted to apply this behaviour to a large number of actions. For this, the code snippet above from hanami-controller points us to another approach, which is its use of the #view_options action method.

We could overload this method to set our view layout for our needs:

def view_options(request, response)
  # Layouts can be `nil` (for no layout) as well as strings
  options = {}
  options[:layout] = nil if request.headers["HX-Request"] == "true"

  {**super, **options}
end

This will automatically set the layout name without requiring you to do so explicitly inside your #handle methods. If you put this in a base action class, then it will work for all its subclasses!

Hopefully this gets you on the right path. Please give this a try and let me know how you go!

1 Like

Thanks for the in-depth answer @timriley, that is really helpful, I did got it working thanks to you.

    def view_options(request, response)
      options = {}
      options[:layout] = nil if request.get_header("HTTP_HX_REQUEST") == "true"

      {**super, **options}
    end

Now the same views can be reused to serve HTMX more easily.