HTML + Code Markup: Threat or Menace, Part Two

another_div_tag_joke.jpg

Last week's post about ERb and other HTML + Markup view languages got five actual comments. By the standards of my recent posts, that's practically a mob.

Most of the commenters wanted me to look at their favorite view tool, which I dutifully did. I then went out and did something completely different.

I covered HAML and Markaby a bit in Professional Ruby on Rails. Markaby I like conceptually, but the concerns about it's structure and speed have kept me from using it more (although the solution I did pick is probably not a speed demon either). I find HAML really opaque, but if it makes sense to you, more power to you. Erector is in some sense an enhanced Markaby.

Commenters also wanted me to look at Kwartz and Wicket, which are both tools where the template is valid HTML, but a special attribute namespace is used to tell the templating system to insert content and logic. I like the idea in theory, but anything that uses XML namespaces makes me break out in hives (to be fair, the Wicket folk say they aren't using XML, just the same namespace syntax). It's a little more high-ceremony then I needed.

Looking at all these solutions was quite helpful in making me think about what I actually wanted. One issue I had with Erector, and to some extent Markaby, is that the entire output page is an object -- to use a layout you subclass it. Using inheritance in this case doesn't work for me, I'm having a hard time seeing the layout/page relationship as IS-A, it really feels like a HAS-A relationship to me. Besides, I like the ability to use HTML for static parts and where appropriate -- I still haven't seen a markup language that handles a mixed mode case like this very well:

<div>This is <b>the</b> <i>best</> tool</div>

What I really want is the ability to treat chunks of output as objects and drop them into the output with a single line of code, and be able to cleanly specify the HTML related to the object in Ruby code. This is the Presenter pattern, and there have been a couple of different attempts to integrate it into Rails.

I don't quite have a full presenter pattern, but I do have a way of generating the HTML that I like. It's basically a wrapper around the Rails content_tag helper, but fixing two problems I had with content_tag.

The content_tag method takes an optional block which is evaluated as the contents of the tag. However, since it's an ordinary Ruby block, only the last value is returned. This is a problem if you are composing multiple subtags inside the block. You need to concatinate or join the subtags together, otherwise only the last one is used, making the code inside the block ugly. Also, the block version is dependent on ERb being around in certain cases, which makes content_tag hard to unit test.

My current solution is a Ruby evolution of the tag-generation library I mentioned in the last post. This being Ruby, it's about 25 lines of code.

Usage looks something like this:

def object_to_vertical_rows(obj, *rows)
  rows.map do |method, caption|
    Html.tr do |tr|
      caption = method.to_s.humanize unless caption
      tr < "caption")
      tr << Html.td(obj.send(method))
    end
  end
end

Html is a class with a tag name, a content array, and an options hash. There's class level method_missing function that just creates a new instance with method name, so Html.tr prepares a <tr> tag pair. The content of the tag can be specified as the second argument, or built up in the block. The block takes the tag being built as it's argument -- giving that block variable the same name as the tag is a trick to make the block more readable. Any options passed to the constructor are treated as attributes.

Inside the block, content can be added to the tag using the << operator, which just contatinates the new content to the existing content, eliminating the need for ugly join code in the block. You can also assign content to the content attribute directly. Inside the block, you can also assign attributes -- it's another method_missing deal, where any unknown method is treated as an attribute of the resulting HTML output.

Here's what I like about this setup:

  • It's meeting my needs -- I can easily convert from my ERb to the Html class. It matches how I think about HTML being constructed
  • The code is, to my eyes at least, significantly easier to read and follow then the equivalent code written with content_tag.
  • It's lightweight -- I don't have to convert everything to it, I can use it where appropriate and ignore it otherwise.
  • It's much easier to test then content_tag with block.

Here's what I don't like about it:

  • It's kind of idiosyncratic, and I'm not sure it's enough of an advantage over existing structures to justify the added complexity of a new tag generator.
  • I haven't done a speed check, but it's probably not all that fast. I suspect it's a bit slower than content_tag, although.
  • Having to specify the tag name again as the block argument is a little goofy, although I thought it was less goofy than the instance_eval kind of magic needed to reduce it completely. On the plus side, it does have a nice explicitness to it.

Anyway, here's the current implementation of the class -- if there's interest, I'll throw it on GitHub.

class Html
  include ActionView::Helpers::TagHelper

  attr_accessor :tag, :content, :options

  def self.method_missing(name, *args, &block)
    Html.new(name, *args, &block)
  end

  def initialize(tag, content = [], options = {}, &block)
    @tag = tag
    if content.is_a? Hash
      @options = content
      @content = []
    else
      @content = [content]
      @options = options
    end
    block.call(self) if block
  end

  def method_missing(name, args)
    options[name] = args
  end

  def <<(text)
    content << text
    content.compact.flatten
  end

  def to_s
    content_tag(tag, content.join("n"), options)
  end

  def to_str
    to_s
  end
end