Haml in Helpers

By Nathan Donaldson in Other on February 23, 2017

Image 0128 full 2x

Haml is one of my favourite Rails plugins – I really like the way it simplifies views, makes sure they’re syntactically correct, and outputs perfectly formatted HTML. However, this gets ruined when using helpers that output multiline HTML. It’d be great to use Haml generation in helpers so that everything keeps to the same formatting.

This class can help:

class HamlRenderer
  def self.haml(&block)
    HamlRenderer.new(&block).to_s
  end

  def initialize(&block)
    @block = block
    @binding = @block.binding
    @object = eval('self', @binding)

    @context = Object.new
    class << @context
      include Haml::Helpers
    end
    @context.init_haml_helpers

    @object.metaclass.send(:class_eval) do
      def haml_tag(*args, &block)
        @haml_context.send(:haml_tag, *args, &block)
      end

      def haml_concat(*args, &block)
        @haml_context.send(:haml_concat, *args, &block)
      end
    end

    @object.instance_variable_set([email protected]_context', @context)
    
    yield @context
    
    @object.instance_variable_set([email protected]_context', nil)
    @object.metaclass.send(:undef_method, :haml_tag)
    @object.metaclass.send(:undef_method, :haml_concat)
  end

  def method_missing(symbol, *args, &block)
    @context.send(symbol, *args, &block)
  end

  def to_s
    @context.send(:haml_buffer).buffer
  end
end

Now a helper can output HTML like so:

def table(headers, rows)
  HamlRenderer.haml do
    haml_tag :table do
      haml_tag :tr do
        headers.each do |header|
          haml_tag :th, header
        end
      end
      rows.each do |row|
        haml_tag :tr do
          row.each do |column|
            haml_tag :td do
              haml_concat column
            end
          end
        end
      end
    end
  end
end

This is rather contrived, but as you can see in the HamlRenderer.haml block there are two new functions available. haml_tag will create a new tag, and haml_concat will add some text content. In the end this is likely to be more verbose than constructing an HTML string, but the output will be cleaner and less prone to errors.