Tue, 2007 Sep 25
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
- 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.
- 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.
- Now I create a NodeBuilder instance. The arguments to the NodeBuilder is again something I hold until a later entry.
- 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
. - 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.
