An Example of an ERB Component

In a previous post I discussed my need for a flexible function for generating values to pass into the HTML attribute options hash of the content_tag helper. In this post, I want to discuss one particular context in which I needed the meld method.

For one particular work project I found myself building a few “ERB components”, that is, ERB partials that had some flexibility around their HTML output via params passed on render. In one specific example, I was building a simple partial for rendering a key-value pair. I was using the description list HTML element (dl), but I wanted the flexibility to have the entry render either as a “column” or as a “row”, e.g.:

Column Entry

Key
value

Row Entry

Key value

So, I wanted one ERB partial where I could get either output based on params passed to the render call of that partial.

Using Bootstrap 4, I know how I wanted the HTML for each entry to look:

Column Entry

<dl class="text-center my-0">
<dt class="">Key</dt>
<dd class="">value</dd>
</dl>

Row Entry

<dl class="d-flex my-0">
<dt class="col-4">Key</dt>
<dd class="col">value</dd>
</dl>

So, I could write some aspirational ERB for how I would like the partial to work:

<%= content_tag(:dl, **props_for(:entry)) do %>
<%= content_tag(:dt, **props_for(:key)) do %>
<%= value_for(:key) %>
<% end %>
 
<%= content_tag(:dd, **props_for(:value)) do %>
<%= value_for(:value) %>
<% end %>
<% end %>

I then could also write some aspirational render calls:

Column Entry

<%= render('entry',
key: 'Key', value: 'value',
props: {
entry: { class: %[text-center my-0] },
key: {},
value: {},
}) %>

Row Entry

<%= render('entry',
key: 'Key', value: 'value',
props: {
entry: { class: %[d-flex my-0] },
key: { class: %[col-4] },
value: { class: %[col] },
}) %>

Now, I just needed to write the props_for and value_for methods for the partial. The first thing I need is to access the params passed into the partial. With ERB partials, you can get the full set of params passed into a partial via the local_assigns variable. local_assigns references a hash of the params. So, I wrote my methods like so:

<%
def props_for(key)
local_assigns.dig(:props, key)
end
%>
<%
def value_for(*keys)
keys.reduce(local_assigns) { |hash, key| hash.try(:dig, key) }
end
%>

NOTE: The value_for method here is precisely the same as the access method I discussed in this past article.

While simple and elegant, these methods have two problems. First, local_assigns is not accessible from any scope except the outer partial scope; you will get a undefined local variable or method 'local_assigns' error when you try to run these methods in the partial. Second, these methods won’t handle params passed using string keys. Let’s refactor and fix both of these issues:

<% instructions = local_assigns.deep_symbolize_keys || {} %>
<%
def props_for(k, instructions)
instructions.dig(:props, k.to_sym)
end
%>
<%
def value_for(*keys, instructions)
keys.map(&:to_sym).reduce(instructions) { |hash, key| hash.try(:dig, key) }
end
%>

Now, we can simply pass the symbolized hash of local_assigns into the methods as a param, and we ensure that we are always working with symbols. Our final ERB partial-as-component looks like so:

<% instructions = local_assigns.deep_symbolize_keys || {} %>
<%
def props_for(k, instructions)
instructions.dig(:props, k.to_sym)
end
%>
<%
def value_for(*keys, instructions)
keys.map(&:to_sym).reduce(instructions) { |hash, key| hash.try(:dig, key) }
end
%>
 
<%= content_tag(:dl, **props_for(:entry, instructions)) do %>
<%= content_tag(:dt, **props_for(:key, instructions)) do %>
<%= value_for(:key, instructions) %>
<% end %>
 
<%= content_tag(:dd, **props_for(:value, instructions)) do %>
<%= value_for(:value, instructions) %>
<% end %>
<% end %>

This will output the HTML we desire given the render calls outlined above.

Column Entry

<%= render('entry',
key: 'Key', value: 'value',
props: {
entry: { class: %[text-center my-0] },
key: {},
value: {},
}) %>

outputs

<dl class="text-center my-0">
<dt class="">Key</dt>
<dd class="">value</dd>
</dl>

Row Entry

<%= render('entry',
key: 'Key', value: 'value',
props: {
entry: { class: %[d-flex my-0] },
key: { class: %[col-4] },
value: { class: %[col] },
}) %>

outputs

<dl class="d-flex my-0">
<dt class="col-4">Key</dt>
<dd class="col">value</dd>
</dl>