Half-Penny For Your Thoughts

rounded down to the nearest cent



Categories


Recent Articles




Computing

Building a Builder, Part 1

Rails has a lot of useful view helpers. If you’re writing your views in rhtml files, Rails can take care of a lot of the nitty-gritty html details, especially with forms. But, if you’re like me, you find yourself writing the same bloody tags over and over, especially for administrative views. It would (I think) be silly for Rails to include much more in the way of helpers than it already has, lest it blossom into 15,000 different “my favorite view setup is”… So, I really like what Rails offers, especially because it’s so easy to extend.

I noticed that for a lot of sites, I was using basically the same layout over and over, including a basic setup for the views, especially for resource controllers that needed a standard group of views: index, show, edit and new. I use tables for displaying the data, because, hey, that’s what tables are for. Yes, they are. No, do not use 30 thousand floated divs to display a table of information. That’s just wrong.

</rant>

So, I decided to build some helpers to make my life easier. And, rather than do everything from scratch, I started by extending Rails’ FormBuilder. Over the next 4 weeks or so, I’m going to go through some of the steps I used, for your reading pleasure. Yay! So, without further ado: NodeBuilder.

Step 1 - Creating a Builder based on FormBuilder

The Rails API docs discuss this (as does the Agile Development with Rails and probably many other blogs, but here’s the gist:

lib/node_builder.rb

class NodeBuilder < ActionView::Helpers::FormBuilder
  def text_field
    super + "<br />That was a text field"
  end
end

lib/nodebuilderhelper.rb

def node_for(name, object, options, &proc)
  form_for(name, object, options.merge(:builder => NodeBuilder), &proc)
end

Yay! That was worthless. But that’s the basics. The rest is just having fun.

Step 2 - I don’t want no sticking <%= … %> ‘s

Since one of my goals is to write as little html as possible, I decided I’d rather just have NodeBuilder take one big pure-ruby block. That is, instead of:

<% form_for :person, @person, :url => { :action => "update" } do |f| %>
  First name: <%= f.text_field :first_name %>
  ...
<% end %>

I wanted:

<% node_for :person, @person, :url => { :action => "update" } do |f|
  f.text_field "First name", :first_name
  ...
end %>

This means two important things: 1. NodeBuilder needs to produce all the needed html. Yes, I can throw occasional html in as a string, but I want to avoid that. 2. My node_for helper needs to push a string containing the full text it produces back to the view.

I’m going to look at #1 in detail in my next entry. So, what this entry is really about, is how do we do #2? Actually, I’m not really sure, but I got it working. Here’s how:

  def node_form(object_name, collection, &block)
    raise ArgumentError, "Missing block" unless block_given?
    collection = [collection] unless collection.kind_of?(Array)
    builder = NodeBuilder.new(collection, object_name, self, {:list_type => :col}, block)
    collection.each do |object|
      yield builder.row, object
    end
    concat(builder.to_s, block.binding)
  end
  1. The first line is just a sanity check that there’s actually a block in the calling code. Otherwise, there’s not much point here.
  2. The second line is something particular to NodeBuilder: all my helpers expect an Array of model objects, whereas Rails’ FormBuilder expects a single object. I’ll eventually get back to that point, but the helper can accept a single object and create an Array consisting of just that single object.
  3. Now I create a NodeBuilder instance. The arguments to the NodeBuilder is again something I hold until a later entry.
  4. Iterate through the collection and yield the NodeBuilder instance (builder.row updates which row we’re on and returns the instance) and the current object in the collection to the block in rhtml. I could, without the collection, do something like
    yield builder
    .
  5. Now, the magic that lets me push a string containing the full text. Instead of the different methods in NodeBuilder returning text, they add information to instance variables. The NodeBuilder.to_s method uses the variables to create the full html. The last line of this method concats that html to the block.binding. And, no, I don’t know what exactly that means, but it does seem to work. And the last line is the key to making #2 happen.

More to come.


Computing

Building a Builder, Part 2

Finally adding part 2 in my not-exactly-a-series series on create Rails Builders. Here’s the first part. As I mentioned way back then, one of my goals was to write a Builder that allowed me to send a full block without any ERB <%= %>’s and such. Now, in many cases, that would not be a good thing. After all, in many cases, most of an rhtml is html, not Ruby. But my target for this builder is “simple” forms and tables that are basically lists of attributes. So, html out.

The key lines from the helper method are:

    builder = NodeBuilder.new(collection, object_name, self, {:list_type => :col}, block)
    collection.each { |object| yield builder.row, object }
    concat(builder.to_s, block.binding)

So, there’s three methods I’ll need for sure in my Builder class: 1. initialize 2. row 3. to_s

I think taking this out of order may make the most sense, so I’m going to start with to_s. Starting very simply:

class NodeBuilder < ActionView::Helpers::FormBuilder
  def to_s
    "Hello World"
  end
end

The

concat(builder.to_s, block.binding)
take the takes the to_s output and places it into the view. So, in this case, regardless of what is in the node_for block in the view, it’s going to be replaced with “Hello World”. Let’s make it do something more useful:

  def to_s
    "<div>" + @rows.join("<br />") + "</div>"
  end

Now the to_s is going to return a series of lines based on whatever is in that @rows instance variable. Somehow, we need to populate @rows. That’s where the yield line comes in.

collection.each { |object| yield builder.row, object }
says that for each object in collection (an enumerable passed to the node_for helper method), we’re going to yield
builder.row
and that object in the collection back to the block in the view. So what does row do?

The node_builder.row needs to return the node_builder object. The block can then be:

<% node\_for :person, @person, :url => { :action => "update" } do |f, object|
  f.text_field "First name", :first_name
  ...
end %>

That “f” is a NodeBuilder instance. So f.text_field says send text_field method to the NodeBuilder object (created by the node_for helper). Oh, boy. So the row method has some work to besides just returning self (which is why the row method exists at all.

  1. it needs to create a variable for node_builder.text_field to stick its return value into, which can then be accessed by to_s.
  2. row needs to set a variable indicating what object in the collection we’re on. (I’ll probably talk about why I’m always using a collection of some sort in a later entry; or, I might forget).

(by the way, calling the method “row” makes sense for my usage; there’s probably a better generic name for it).

  def row
    @item ? @item += 1 : @item = 0
    @object = @collection[@item]
    @rows[@item] = ""

    return self
  end

So each time the yield is called, row tells the node_builder instance that we’re working with the next object, and the returns the whole node_builder object for use by the block. It also adds an entry to the @rows Array, which text_field and other methods will make use of.

initialize could do a lot but for now, let’s just initialize that @rows Array.

  def initialize(collection, object_name, template, options = {}, proc = Proc.new)
    @rows = []
  end

One more thing before I end this article, a way too simple text_field method:

  def text_field(*args)
    @rows[@item] << super(*args)
  end

Okay, that was a lot to get to using

<%
instead of
<%=
, but it also sets up for later. Continued at some point…


Computing

Building a Builder, Part 3

This continues on two previous articles about setting up a custom FormBuilder for use in your Rails applications. See Part 1 (Introduction) and Part 2 (The helper method and related work on the NodeBuilder class). The particular Builder I’m working through is meant to generate simple tables and forms, nothing real fancy, with minimal view code. So, today, I’m going to look at creating a basic table via the builder.

In my last entry, I added the NodeBuilder#to_s method:

  def to_s
    "<div>" + @rows.join("<br />") + "</div>"
  end

I’m going to flesh this out to create a table instead of a div with line breaks:

  def to_s

    # Setup table header
    headers = ""
    @headers.each { |h| headers << @template.content_tag("th", "#{h[:label]}") }
    
    headers = @template.content_tag("thead",
              @template.content_tag("tr", headers) )
    
    # Setup content rows of table
    content = ""
    @rows.each do |items|
      row_content = ""
      items.each { |item| row_content << @template.content_tag("td", item) }
      content << @template.content_tag("tr", row_content)
    end
    content = @template.content_tag("tbody", content)

    # Put header rows in content rows into table element
    return @template.content_tag("table", headers + content)
  end

This takes information from the @headers and @rows variables and puts that data in a table, great for index views. The trickier part is getting that information into the variables. I’ll use the #field method to do that:

  def field(label, content = "", options = {})

    # Allow just a symbol representing the attribute name to be passed. Use a titleized version of the field name for the label
    if label.kind_of?(Symbol) && content.empty?
      content = label
      label = label.to_s.titleize
    end
    
   # If this is the first item (row) populate the @header array; otherwise, it's already populated and can be skipped.
    if @item == 0
      @headers << {:label => label} # This is a hash because I might want to add more information later.
    end

    # If the content is a symbol, assume it's a method name on the current object.
    if content.kind_of?(Symbol)
      if content == :type # To avoid deprecation warnings
        content = @object[content]
      else
        content = @template.escape_once(@object.send(content)) 
      end
    end

    # Now, add the content to the @rows variable.
    @rows[@item] << content
  end

Well, that’s pretty close to what I want, except that I want to also be able to use the with text_fields, password_fields, etc. So:

  # create_labelled_field defines text_field and similar methods.
  def self.create_labelled_field(method_name)
    define_method(method_name) do |label, *args|

      # if label is a symbol, assume its an attribute name;
      # use the titleized form of attribute name as the label.
      # In either case, call super without the label to get the content.
      if label.kind_of?(Symbol)
        args = [label] + args
        field(label.to_s.titleize, super(*args))
      else
        field(label, super(*args))
      end
    end
  end
  
  # This creates the methods for text_field, etc, which call create_labelled_field
  (field_helpers + ["select"]).each do |name|
    create_labelled_field(name)
  end

Yay. I may come back later and add some clearer explanations but this works (more or less) and provides a nice starting point to DRY-ing up forms. I may add a few more articles to this section, but they’ll probably be along the lines of “and here’s the goofy stuff you can do now…”