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

1
2
  Key
 value

Row Entry

1
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

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

Row Entry

1
2
3
4
<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:

1
2
3
4
5
6
7
8
9
<%= 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

1
2
3
4
5
6
7
<%= render('entry',
           key: 'Key', value: 'value',
           props: {
             entry: { class: %[text-center my-0] },
             key: {},
             value: {},
           }) %>

Row Entry

1
2
3
4
5
6
7
<%= 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:

1
2
3
4
5
6
7
8
9
10
<%
  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:

1
2
3
4
5
6
7
8
9
10
11
<% 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<% 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

1
2
3
4
5
6
7
<%= render('entry',
           key: 'Key', value: 'value',
           props: {
             entry: { class: %[text-center my-0] },
             key: {},
             value: {},
           }) %>

outputs

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

Row Entry

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

outputs

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