Rails is on a path to re-implementing Ruby methods in an attempt to make partials easier for developers to reason through. Here’s a tweet from Rails on May 16, 2024.
Just for fun I took a swing at moving partials into Ruby helpers so a plain ‘ol Ruby method signature could be used to enforce the boundary between the caller and the template.
Here’s what a partial looks like that sits between a helper method and any other part of Rails.
module ApplicationHelper
def contact_info(first_name, last_name = "Brad", email:, phone: nil)
phone ||= "911"
render {
<<~ERB
<p>Hi <%= first_name %> <%= last_name %>,</p>
<p>Here's your contact info:</p>
<dl>
<dt>Email:</dt> <dd><%= email %></dd>
<dt>Phone:</dt> <dd><%= phone %></dd>
</dl>
<p>That's it, thanks for playing!</p>
<p>Served from <%= request.url %> at <%= @time %></p>
ERB
}
end
end
This partial can be called from anywhere in the Rails app as follows:
<h1>Let's look at a minimum viable way to improve partials</h1>
<%= contact_info("Brad", "Gessler") %>
Now let’s render the thing.
Oops! I forgot to pass the email
keyword argument. That’s the feature: we used Ruby’s method definitions to enforce what data is passed into the template. Let’s try that again.
Ahh, that’s better. Now let’s see what makes this possible.
The implementation of this helper ended up being around 10 lines of code, mostly to deal with the gymnastics of passing the variable bindings from the helper function into the Rails renders’ locals.
module ApplicationHelper
def render_partial(*, locals: {}, **, &block)
binding = block.binding
block_locals = binding.local_variables.each_with_object(Hash.new) do |variable, locals|
locals[variable] = binding.local_variable_get variable
end
render *, inline: block.call, locals: block_locals.merge(locals), **
end
def render(...)
block_given? ? render_partial(...) : super(...)
end
end
If it looks confusing, that’s because it kind of is! The challenge for this whole problem is getting the signature from the helper method:
def contact_info(first_name, last_name = "Brad", email:, phone: nil)
into the locals that’s passed into Rails renderer. The only way of accomplishing that without explicitly passing the helper functions binding into render
is to put an ERB string (or whatever template) into the render {}
block.
This works with any Rails templating language. To make this work with Slim, you might do something like this:
module ApplicationHelper
def more_info(name:)
render(type: :slim) {'''
h1 Hello there!
p This is for #{name}
'''}
end
end
Of course you could just use Phlex 💪 and not have to deal with these global scoping issues at all.