Tue, 2007 Nov 13

Building a Builder, Part 2

Posted in Computing at 19:09 by jmorgan

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…


0 Comments

Add a comment