ruby-on-railsactionview

Class based view helper does not output captured HTML as expected


I am trying to create a set of view helpers for a project I am working in and as they share a lot of common code, for better or worse, I am trying to take an object oriented approach. However, I am having problems when I try to capture and render HTML.

Instead of rendering the HTML within the tag, it will render it before the tag and also render it as a string within the tag.

I want to understand why there are differences in the behaviour and what I can do to fix it (if it is even possible to)

Example 1: working functional approach

I have the following helper:

# application_helper.rb

def my_helper(summary_text, &block)
  tag.details do
    concat(tag.summary(tag.span(summary_text)))
    concat(tag.div(&block))
  end
end

In my html.erb file I have:

<%= my_helper('Summary text') do %>
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
  </ul>
<% end %>

This will render:

<details>
  <summary>
    <span>
      Summary text
    </span>
  </summary>
  <div>
    <ul>
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </ul>
  </div>
</details>

Example 2: non-working Object Oriented approach

In my helper file I have defined the class:

# helpers/my_helper.rb

require 'action_view'

class MyHelper
  include ActionView::Context
  include ActionView::Helpers

  attr_reader :summary_text

  def initialize(summary_text)
    @summary_text = summary_text
  end

  def render(&block)
    tag.details do
      concat(tag.summary(tag.span(summary_text)))
      concat(tag.div(&block))
    end
  end
end

And in my application helper I have:

# application_helper.rb

def my_helper(summary_text, &block)
  MyHelper.new(summary_text).render(&block)
end

In my html.erb file I have:

<%= my_helper('Summary text') do %>
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
  </ul>
<% end %>

This will render:

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>
<details>
  <summary>
    <span>
      Summary text
    </span>
  </summary>
  <div>
    &lt;ul&gt;&lt;li&gt;Item 1&lt;/li&gt;&lt;li&gt;Item 2&lt;/li&gt;&lt;li&gt;Item 3&lt;/li&gt;&lt;/ul&gt;
  </div>
</details>

I would expect the Object Oriented approach to have the same result as the functional based approach.

I've had a try at debugging this in the Rails code and if I were to have a guess I think it's something to do with the output_buffer and the OO helper not using the same one as the view, but I am not sure.


Solution

  • What you want to do here is pass the view context into the builder/decorator/presenter/view compont/thingamajigger and call the methods on it instead:

    # do not use require - the Rails component are already loaded through railties
    # do not call this "Helper" - that will just make things more confusing.
    class ListBuilder
      attr_reader :summary_text
      attr_reader :context
      delegate :tag, :concat, to: :context
    
      def initialize(summary_text, context:)
        @summary_text = summary_text
        @context = context
      end
    
      def render(&block)
        tag.details do
          concat(tag.summary(tag.span(summary_text)))
          concat(tag.div(&block))
        end
      end
    end
    
    def list_with_summary(summary_text, &block)
      ListBuilder.new(summary_text, context: self).render(&block)
    end