Customizing Markaby – Language Level Refactorings

It’s very easy to call out to methods in markaby, but it’d be nice if you could actually customize the dsl as well.

For example, on many of our pages we have a bottom row that has buttons that look a certain way. So on every page, we have :

  table(:width => "100%") do
    tr do
      td.left do
        previous_button
        first_button
      end
      td.center do
        print
      end
      td.right do
        next_button
        last_button
      end
    end
  end

The code for the actual buttons changes, and after a few tries to extract the whole thing into a single method, we gave up. Our efforts had made it harder to read, not easier. There was always just a little too much variance, and it didn’t feel right.

What we really wanted to write was :

  last_row do
    column do
      previous_button
      first_button
    end
    column do
      print
    end
    column do
      next_button
      last_button
    end
  end

This lets the buttons that change all stay in the view, and gets rid of the skeleton and positional stuff that doesn’t change. Furthermore, it’s DRY and puts all that positional logic in one place instead of scattered across 20 views.

How to do this?

I wrote a test (in RSpec) that looks something like :

describe ApplicationHelper do
  it "should generate a table from a buttons method" do
    last_row(:columns => 2) do
      column do
        "foo"
      end
      column do
        "bar"
      end
    end.should == '<table width="100%"><tr>' +
                    '<td class="left">foo</td>' + 
                    '<td class="right">bar</td>' +
                  '</tr></table>'
  end
end

After a bunch of fiddling and poking around, I finally made the test (and a couple others) pass with this code in my ApplicationHelper :

def last_row(options, &block)
  markaby do
    table(:width => "100%") do
      tr do
        LastRowContext.new(self, options[:columns]).
                                  instance_eval(&block)
      end
    end
  end
end
 
class LastRowContext
  def initialize(markaby, columns)
    @markaby, @column_count, @column_index = 
                            markaby, columns, 0
  end
 
  def column(&block)
    alignment = case @column_index += 1
    when 1 : :left
    when @column_count : :right
    else :center
    end
 
    @markaby.instance_eval do
      td(:class => alignment, &block)
    end
  end
end

I’m sure this could get cleaned up more; this was the work of less than an hour. In particular, if you did this often, you could extract a common MarkabyContext superclass that had some convenience methods. The point is, this is really easy to do, and we shouldn’t be scared to try “Language level refactorings” like this.

Tags: , , ,

2 Responses to “Customizing Markaby – Language Level Refactorings”

  1. Richard Says:

    Hello-
    I was looking at your work, and think you might be able to help out with an
    existing project I am working on (in-line text ad network)—I can explain
    further later, but was wondering what your hourly rate for php/js/ajax/maybe RoR development is and if you are available to help out with this….

    Currently the project is being developed in Bangelore, India—but I want some more reliable, experienced developers to help clean up the code and user experience with more ajax/RoR integration (drag + drop, etc.) on the clientfacing advertiser/publisger admin. Bottom line is that I need solid, local, experienced developers I can trust to finish out and make this application robust, scalable, and at the end of the day a success.

    Main Competitors: Kontera.com, vibrantmedia.com, mediatext.com

    Thanks. Please let me know either way.

    Best-
    Richard
    Seattle, WA

  2. Jeremy Stell-Smith Says:

    Hey Richard,

    I’m going to need some more contact info there, I’m with “pivotal”:http://pivotallabs.com/ these days and I’d love to talk to you and see whether we’d be a good fit for you. Shoot me an e-mail @ jeremystellsmith@gmail.com.