May was a breakthrough month in terms of the integration of the standalone components into Hanami 2. Let’s dig right in.
Last month I wrote about my first pass at integrating Hanami view classes with application they exist within. It looked like this:
module Main
# "Base" view class for `main` slice
class View < Hanami::View[:main]
end
module Views
class Articles
# Class for a specific view, inheriting from base
class Index < Main::View
end
end
end
end
In this approach, inheriting from Hanami::View[:main]
would tell the subclass to apply its configuration using details from the main
slice. It worked fine, but there’s still some redundancy there:
module Main # <- We're clearly in the `main` slice
class View < Hanami::View[:main] # <- So why do we have to repeat it here?
end
end
In trying to get this stuff done for the next alpha release, Luca has definitely been encourating a pragmatic approach to getting things in place (“perfect is the enemy of good,” or in this case “shipped”). It’s the right way to go, but even still, this nagged at me, especially given our goal of reducing boilerplate as much as possible.
After a while I realised that the application itself could provide a facility to help us out in this situation. Given a class like Main::View
, and given that each slice ”owns” a specific namespace, there is enough information in the class name alone to infer which slice it belongs to. So now we have this:
Hanami.application.component_provider(Main::View)
#=> #<Hanami::Slice:0x00007fc2ae074568
# @application=Soundeck::Application,
# @booted=true,
# @container=Main::Container,
# @name=:main,
# @namespace=Main,
# @namespace_path="main",
# @root=#<Pathname:/Users/tim/Source/hanami/soundeck/slices/main>>
Pass in a class or instance, and get its slice. Easy. With this in place, we can use it from the .inherited
hook of Hanami::View
to get all the information we need for truly seamless integration:
module Main
class View < Hanami::View
end
end
Look, zero boilerplate!
But this is only half of the seamless view integration story: now that we can infer the slice that provides a given view, how do we add the slice-specific behaviour, especially given we’re still inheriting directly from Hanami::View
, which still needs to be able to provide the behaviour for standalone (non-integrated) views?
Our original approach for this was also pragmatic: when we need a subclass of a given Hanami component (like Hanami::View
or Hanami::Action
) to behave differently within an Hanami application versus when used standalone, we would just monkey patch the application-specific behaviour. Now, anyone who knows me would know this isn’t approach I would not tolerate for long. 😉 Even still, I was willing to do it for the sake of expedience. You can see the approach (and all my misgivings about it) in lib/hanami/action/extensions/application_action.rb
in my proof of concept action integration PR.
But as is the theme of this section, it nagged at me. What we really needed here was for the patched methods providing the integration specialisations to be able to call super
to get to the standalone behaviour wherever they needed. With a monkey patch, this isn’t possible because you end up completely replacing the methods (or having to resort to hacky “alias method chain”-style approaches).
One way to solve this would be to have a deeper inheritance chain (Hanami::ApplicationView < Hanami::View
) and using different superclasses for integrating views versus standalone views, but that bifurcates view usage in an unfriendly way, and more likely than not would make one of those two use cases more awkward than the other.
At this point I realised Ruby gives us another option for this: modules! What we needed here were two different modules in the ancestor chain for a given view class, with the “nearest” one providing the application integration behaviour (e.g. [Main::Views::Articles::Index, Main::View, ApplicationView, StandaloneView]
), and the next one back providing the standard standalone behaviour. This way, the application integration module can add only the specialisations it requires, and can call super
whenever it needs.
The final piece to this puzzle is to make it so that the ApplicationView
module can provide behaviour that’s specific to a given slice. This is where the module builder pattern comes in. Instead of this ApplicationView
module being a plain old static module, we can initialize it with the slice object that we get when we’re subclassing Hanami::View
in the first place.
So with this in place, here’s what Hanami::View
and its .inherited
roughly look like:
require_relative "view/application_view"
require_relative "view/standalone_view"
# ...
module Hanami
class View
include StandaloneView
# ...
def self.inherited(subclass)
super
# If inheriting directly from Hanami::View within an Hanami app, configure
# the view for the application
if subclass.superclass == View && (provider = application_provider(subclass))
subclass.include ApplicationView.new(provider)
end
end
def self.application_provider(subclass)
if Hanami.respond_to?(:application?) && Hanami.application?
Hanami.application.component_provider(subclass)
end
end
private_class_method :application_provider
end
end
And the resulting ancestors for an actual view class:
Main::Views::Articles::Index.ancestors
# => [
# Main::Views::Home::Index,
# Main::View,
# #<Hanami::View::ApplicationView[main]>,
# Hanami::View,
# Hanami::View::StandaloneView::InstanceMethods,
# Hanami::View::StandaloneView,
# # ...
# ]
These are exactly the number of different places we need to neatly slot in all the behaviour for our truly seamless view integration!
The resulting arrangement has some other nice benefits, too, because the integration logic has now moved out from the hanami gem and over into the hanami-view gem itself:
I was chuffed with how this all worked out, and I’m much, much happier with the overall arrangement now. Hats off to Ruby for being such a flexible language! Check out the full hanai-view PR for this new integration approach, as well as the corresponding integration hooks (and reduction in view integration code!) inside the hanami gem.
Hanami.application?
checkA small subtle thing you might have noticed above was that check for Hanami.application?
. This is another hook I added to make it easier for components to integrate (or not) with an Hanami application. Because many of the Hanami components can be used on their own (hanami-view, hanami-router, and hanami-controller in particular), the Hanami
namespace will definitely exist, but not necessarily a full Hanami.application
. This Hanami.application?
check provides a safe way to determine if an application has been defined before activating any integration code.
Right now this is defined directly on the Hanami
module by the hanami gem, but we’ll also be adding it to hanami-utils so you can safely use it without having to require the full application gem.
All the polishing of the view integration was a warm-up for the main game this month: properly implementing the integration of view rendering into Hanami actions.
This was the approach we agreed upon after our experiments last month:
class Index < Main::Action
include Deps[view: "views.articles.index"]
def handle(req, res)
# Views are rendered by the response
res.render view
end
end
And as of a few days ago, the work is complete!
That single res.render view
belies a lot of underlying logic. What we do with this integration is provide the view with all the request-specific data that it might need to render itself, things like the current session, flash messages, CSRF token, etc. This is all set up automatically for you as soon as you inherit from Hanami::Action
within an existing Hanami application.
Sound familiar? That’s because we follow the exact same integration approach for actions as we do for views. Hanami::Action now has a StandaloneAction
module providing the basic functionality, and an ApplicationAction
module that is initialized with the action’s slice, so it can pick up whatever details it needs from the slice or application to provide the view rendering integration.
The crux of the integration is the action setting up a view context object with the request/response pair created when the action is called. This view context is automatically passed to the view when res.render
is called. Having the request/response pair available to the context means that the context object can provide methods to make those details like the flash
available for use within the view templates, scopes, and parts (these are links to dry-view documentation, since at this point, hanami-view unstable and dry-view 0.7 are effectively the same).
This integration is eminently flexible. There are multiple points at which an application author can customise it. The first would be to add their own methods to the view context class within their application (I’ll show examples of this this next month). The next would be to override any of these methods within their base action class:
#view_options
, to pass additional options to every view as it is rendered#view_context_options
, to pass additional options to the view context#build_response
, to customize the response object that is prepared for passing to the action’s #handle
method (it’s unlikely this will need to be customised, but it’s nice to keep all options open)I think these methods are a perfect example of the approach we’re taking with Hanami 2 development: conveniences by default, but every possible measure available to adjust things when you need to diverge from the defaults.
This month was all about laying the proper groundwork for action and view integration. With this done, my plan for June is to roll through all these steps to round it out:
Hanami::Action
configuration class-based, like we have for view configuration, so we can auto-configure actions based on their sliceActions::Articles::Index
) so you don’t need to explicitly auto-inject them#handle
for basic render-only actionsHanami::View::ApplicationContext
context class with a default set of helpful methods for use within all Hanami views, including those that require access to the request (as described above)With Hanami 2, while we’re building upon many years worth of open source efforts in terms of the existing libraries we’re pulling together, it’s become very clear to me through this process that doing good integration work is just as much effort all over again. Thanks for your patience while we work through this as best we can.
Earlier this month I announced that I joined GitHub sponsors, and since then, two more kind people began sponsoring me. Thank you so much Samuel Williams and Thomas Klemm!
And a big shout out to Benjamin Klotz for your continuing support 😄
This post turned out to be a big one! The fact that I had so much to say speaks to just how pivotal a month this was. I’m looking forward to the next few weeks of rolling downhill from here and collecting a bunch of quick wins. See you all again at the end of June! 👋🏼