Pothibo

Create complex HTML tags in Rails

A little more than a year ago, I build a gem that implemented the decorator pattern to help me built my views. It was nothing fancy and it got the job done. More I was using it, more I was thinking that it wasn't what I needed.

I started seeing overlaps between decorators and helpers. No clear line between when to use a decorator and when to use a helper. This led me to this past week end.

I started to look at what I was doing and searched for patterns that could make me figure out the underlying cause of using decorators.

What I discovered

Decorators and helpers are used to build HTML that would be otherwise a complicated mess to build in a view.

The reason I use them is because I need to use control flows (if/else) to build some of my HTML tags.

In some cases, I wanted to add a class to a selected object, or show some content about a model depending on the state of that object.

In the most basic form, here's why I would use a decorator/helper.

<ul class='users'>
    <% user.each do |u| %>
      <%= list_user(u) do %>
        Render the user profile here
      <% end %>
    <% end %>
  </ul>

This would allow me to add attributes to my tag dynamically depending if a user is a friend of mine or not.

def list_user(user)
  classes = %w(user)
  if user.friend_of?(current_user)
    classes << :friend
  end

  content_tag :li, class: classes do
    yield
  end
end

When you are building a method like this, it's useful to use that method in different places so you centralize how you handle a list of users. This means that if you ever need to change something in the project, you'd only have to modify this method and the change would propagate throughout your application.

This is, I believe, the raison d'ĂȘtre of helpers and decorators are, in hindsight, a different perspective to a same problem.

The reason why I started using decorators, a year ago is that I faced a situation where I was building a lot of different HTML from helpers and the name started to get confusing.

Confusing because I had many helpers doing similar things but yet were different enough to be rendered using a different method.

So I used decorator as a mean to namespace similar methods that was used once or twice in the whole project.

Last weekend, I realized that many of my problems could be fixed if I could just edit HTML attributes on multiple line inside a view.

The problem that drove me to use the decorators was that I hated to do stuff like this:

<li class="user <%= :selected if user.selected? %> <%= :active if user.active? %>">
 ... stuff
&lt/li>

I realized that I could lower the number of decorators/helpers if I could do something like this:

<%= content_tag :li, class: "user" do |el| %>

  <% if user.active? %>
    <% el[:class] << :active %>
  <% end %>

  <% if user.selected? %>
    <% el[:class] << :selected %>
  <% end %>

 ... stuff

&lt% end %>

Obviously, it's more verbose than the former, but you can't deny that it's much easier to read. Also, it's much easier to handle complex if/else scenarios as the flow is evident.

While it wasn't easy to figure out how to make this happen, I didn't fail.

Dynamic HTML attributes inside the view

The goal with this new content_tag is to be a drop-in replacement. That means that overriding this method should have no effect on your project except that you can now use an argument on the block you are calling.

To have it work in your project, add the following helper.

# app/helpers/content_tag_helper.rb
module ContentTagHelper
  def content_tag(*args)
    if block_given?
      tag = Tag.new(args[0], args[1] || {})
      old_buf = @output_buffer
      @output_buffer = ActionView::OutputBuffer.new
      value = yield(tag)
      content = tag.render(@output_buffer.presence || value)
      @output_buffer = old_buf
      content
    else
      super
    end
  end

  class Tag
    include ActionView::Helpers::CaptureHelper
    attr_accessor :id
    attr_reader :name, :css

    def initialize(name, attributes = {})
      @name = name
      @attributes = attributes.with_indifferent_access
      @attributes[:class] = Tag::CSS.new(attributes[:class])
    end

    def []=(k,v)
      @attributes[k] = v
    end
    
    def [](k)
      @attributes[k]
    end
    
    def render(content)
      "<#{name}#{render_attributes}>#{content.strip}</#{name}>".html_safe
    end

    def render_attributes
      attrs = @attributes.dup
      if self[:class].empty?
        attrs.delete :class
      else
        attrs[:class] = self[:class].to_s
      end

      attrs.keys.map do |k|
        "#{k}='#{ERB::Util.html_escape(attrs[k])}'".html_safe
      end.join(' ').prepend(' ')
    end

    class CSS
      
      def initialize(css)
        if css.is_a? String
          @internals = css.split(' ')
        else
          @internals = css.to_a
        end
      end

      def to_s
        @internals.uniq.join(' ')
      end

      def empty?
        @internals.empty?
      end

      def <<(name)
        @internals << name
        nil
      end
    end
  end
end

Now, you should be able to create dynamic HTML elements like this:

<%= content_tag :li, class: "user" do |el| %>

  <% if user.active? %>
    <% el[:class] << :active %>
  <% end %>

  <% if user.selected? %>
    <% el[:class] << :selected %>
  <% end %>

 ... stuff

&lt% end %>

But this method is much more powerful than it looks. Here's a few things that becomes possible with this upgraded helper.

Set any kind of attributes

Sometimes, setting data attributes to a DOM element wraps around and make it hard to read. Sometimes, I would like to split the data attributes so it's easier for me to read and maintain.

<%= content_tag :div, class: "user", id: "userProfile" do |el| %>
  <% if user.profile? %>
    <% el['data-image'] = user.profile.last.url %>
  <% else %>
    <% el[:class] << 'blank' %>
    <span>No profile picture set yet</span>
  <% end %>
&lt% end %>

You can set a parent element in a partial

Sometimes, you want to show a list of records or a message saying it's blank. And usually, the css needs to change depending if you have an empty set or not.

Here's one way to do it now:

# index.html.erb
<%= content_tag :ol, class: "entries" do |div| %>
  <%= render 'entries', element: div, entries: @entries %>
&lt% end %>
# _entries.html.erb
<% if entries.any? %>
    <%= content_tag_for :li, entries do |entry| %>
      <%= entry.name %>
    <% end %>
<% else %>
  <% element[:class] << :blank %>
  <p>No entry could be found. Try a broader search.</p>
<% end %>

It goes without saying that this latter trick can bring a lot of harm if not used carefully :).

Get more ideas like this to your inbox

You will never receive spam, ever.