form_for and overriding methods

From time to time, we override ActiveRecord attribute methods to add some additional behavior. Yesterday, I was working on some code to require a user to approve a new email address before the address is updated. Unfortunately, this broke our form handling. Let’s take a look at what happened and how we can fix it.

First, let’s take a look at the code in question. I have a User model with an email attribute. When a user wants to change their email address, I want to send an email to their new address and delay the actual change until they click a confirmation link.

To implement this, I added a pending_email attribute on the user mode. I then use this method in all of my forms for users. By default, the pending_email attribute is blank. When a user views their profile edit form, I want them to see their existing email address. It should be easy to make the pending_email attribute return either itself if it is set, or the email attribute if it is blank. That method looks like:

  def pending_email
    read_attribute(:pending_email) || email
  end

That does exactly what we want. Calling pending_email gives us the email address by default. Unfortunately, things break down when we try to use this method in a form. When we do something like:

  <% form_for @user do |f| %>
    <%=f.text_field :pending_email %>
  <% end %>

pending email never displays the existing email address. To understand what was happening, I had to dig in to the Rails source. I started by looking at the text_field method in form_helper.rb. That pointed me to the InstanceTag class. That finally led me to the following code:

class << self
        
        def value_before_type_cast(object, method_name)
          unless object.nil?
            object.respond_to?(method_name + "_before_type_cast") ?
            object.send(method_name + "_before_type_cast") :
            object.send(method_name)
          end
        end
end

That code looks at the object in our form, and sees whether it has a pending_email_before_type_cast method. If it does, it calls it to get the value. If it doesn’t, it calls pending_email. By default, ActiveRecord creates the _before_type_cast methods so that code can access the textual value of a field before it is turned into a ruby object. For example, calling created_at_before_type_cast will give you the textual representation of a date inside the database. There are good reasons for this behavior, but they don’t really matter to us. All we need to know is that our pending email method doesn’t every get called.

So how do we fix this? We’re going to exploit the trick that the value_before_type_cast method checks to see if our object responds to a method before it calles the _before_type_cast method. We can easily override respond_to? to make it lie about the existence of our method. Here’s the code that I used:

  # fake out the helpers to use the method and not the raw attribute
  def respond_to?(*args)
    if args.first.to_s == "pending_email_before_type_cast"
      false
    else
      super
    end
  end

That’s all it takes! Our code checks to see if the caller is asking about our pending_email method. If they are, it lies and says the _before_type_cast method doesn’t exist. For anything else, we fall back to the default behavior. Now, our users see their existing email address when they view their profile edit form.

You can see why we want to clean up this code a little. It’s not obvious what that code is doing, and it’s really easy to get wrong. (I spent 20 minutes debugging because I spelled type_cast wrong. I don’t want to have to ever do that again)

Expect to see more frequent posting here. My book, Developing Facebook Platform Applications with Rails is almost done and I will have a little bit more time to write.