Nested content_tag throws undefined method `output

2020-05-20 02:28发布

问题:

I'm trying to create a simple view helper but as soon as I try nest a couple of content tags it will throw

NoMethodError: undefined method `output_buffer=' for

def table_for(list, &proc)
  t = Table.new
  proc.call(t)
  t.render_column(list) 
end

class Table
  include ActionView::Helpers::TagHelper

  attr_accessor :columns, :block

  def initialize
    @columns = Array.new
  end

  def col(name)
    @columns << name
  end

  def render_column(list)
    content_tag :table do
      list.each do |c|
        content_tag :td, c
      end
    end
  end
end

Any hints of what's wrong? I've also seen that there's a XmlBuilder is that one better for my purpose?

回答1:

ActionView::Base has built into it the Context module, which provides the methods output_buffer() and output_buffer=().

So you can solve your problem either by having your class do:

include ActionView::Context

Or even more simply:

attr_accessor :output_buffer


回答2:

I think there were some changes about this in 3.0, but in previous versions the trick was to pass self:

def table_for(list, &proc)
  Table.new(self)
  # ...

def initialize(binding)
  @binding = binding
  #...

def render_column
  @binding.content_tag :table do
    # ...
  end
end

I'm not sure if this is still how it's done in rails 3.

Another thing to fix in ordere for the code to work is to save the output of the inner content_tag somewhere, as with each the content is generated and then discarded. One of the possible solutions:

def render_column(list)
  @binding.content_tag :table do
    list.inject "" do |out, c|
      out << @binding.content_tag(:td, c)
    end.html_safe
  end
end


回答3:

With help from Nested content_tag throws undefined method `output_buffer=` in simple helper I ended up with the following solution inspired by the API for Formtastic.

<%= table_for(@users) do |t| %>
   <% t.col :name %>
   <% t.col :email %>
   <% t.col :test, :value => lambda { |u| u.email }, :th => 'Custom column name' %>
   <% t.col :static, :value => 'static value' %>
<% end %>

Using the output_buffer directly and probably reinventing the wheel the code looks like

module ApplicationHelper
  def table_for(list, &block)
    table = Table.new(self)
    block.call(table)
    table.show(list)
  end

  class Column
    include ActiveSupport::Inflector

    attr_accessor :name, :options

    def initialize(name, options = {})
      @name    = name
      @options = options
    end

    def td_value(item)
      value = options[:td]
      if (value)
        if (value.respond_to?('call'))
          value.call(item)
        else
          value
        end
      else
        item[name]
      end
    end

    def th_value
      options[:th] ||= humanize(name)
    end
  end

  class Table
    include ActionView::Helpers::TagHelper

    attr_accessor :template, :columns

    def initialize(temp)
      @columns  = Array.new
      @template = temp
    end

    def col(name, options = {})
      columns << Column.new(name, options)
    end


    def show(list)
      template.content_tag(:table) do
        template.output_buffer << template.content_tag(:tr) do
          columns.collect do |c|
            template.output_buffer << content_tag(:th, c.th_value)
          end
        end
        list.collect do |item|
          template.output_buffer << template.content_tag(:tr) do
            columns.collect do |c|
              template.output_buffer << template.content_tag(:td, c.td_value(item))
            end
          end
        end
      end
    end

  end
end