Search code examples
ruby-on-railsactionview

a custom Rails *_for helper?


I find myself using Rails' form_for in the show view to take advantage of translations and other methods on AR objects to create non-interactive forms to display the object. There's got to be a better way to do this though with a custom builder. Is there anything out there (gem or diy-tutorial-wise) for this kind of functionality? My search skills are poor for this one.

For example, it be great if I could write something like this:

<%= dl_for(@cog) do |dl| %>
  <%= dl.dt_dd(:name) %>
  <%= dl.dt_dd(:colors) { |colors| colors.to_sentence } %>
  <%= dl.dt_dd(:size, { class: @cog.size }) %>
<% end %>

And get:

<dl>
  <dt>My Name Translation</dt>
  <dd>Cog 1</dd>

  <dt>My Colors Translation</dt>
  <dd>Red, Green and Blue</dd>

  <dt class="Small">My Size Translation</dt>
  <dd class="Small">Small</dd>
</dl>

Solution

  • You can use a variation of the presenter pattern to create your own element builders:

    class DefinitionListBuilder
      attr_reader :object
      # context is the view context that we can call the rails helper
      # method on
      def initialize(object, context, **options)
        @object = object
        @context = context
        @options = options
        @i18n_key = object.model_name.i18n_key
      end
    
      def h
        @context
      end
    
      def dl(&block)
        @context.content_tag :dl, @options do
          yield self
        end
      end
    
      def dt_dd(attribute, **options)
        h.capture do
          h.concat(h.content_tag :dt, translate_attribute(attribute), options)
          h.concat(h.content_tag :dd, object.send(attribute), options)
        end
      end
    
      private
      def translate_attribute(attribute)
        key = "activerecord.attributes.#{@i18n_key}.#{attribute}"
        h.t(key)
      end
    end
    
    

    This plain old ruby object is the equivalent to a FormBuilder. Which really just is a object that wraps a model instance and provides helpers scoped to that instance. You then create a helper which creates instances of the element builder:

    module DefinitionListHelper
      def dl_for(record, **options, &block)
        builder = DefinitionListBuilder.new(record, self, options)
        builder.dl(&block)
      end
    end
    

    This is the equivalent to ActionView::Helpers::FormHelper which provides form_for.

    For the sake of brevity this is simplified and #dd_dt does not take a block.

    Example:

    # config/locales/se.yml
    se:
      activerecord:
        attributes:
          cog:
            name: 'Namn'
            size: 'Storlek'
            color: 'Färg'
    
    <%= dl_for(Cog.new(name: 'Cogsworth', size: 'biggish', color: 'red')) do |builder| %>
      <%= builder.dt_dd :name %>
      <%= builder.dt_dd :size %>
      <%= builder.dt_dd :color %>
    <% end %>
    

    HTML output:

    <dl>
      <dt>Namn</dt><dd>Cogsworth</dd>
      <dt>Storlek</dt><dd>biggish</dd>
      <dt>Färg</dt><dd>red</dd>
    </dl>