Written by: steve ross on July 10th 2006

Contact forms are a way of life on the Web and so we as developers implement them time and again. One nice touch is to do some validation of the data entered in the form to get higher quality data and lower the spam. Rails offers so much magic in how it joins models to views through controllers -- it's a shame to see that all go away when you implement your contact form. The good news is you can have your contact form and validations too!

I've tried several variations on how to perform ActiveRecord validations on non-database backed models, and keep coming back to the one from technoweenie. Here's how it works. You define a model thusly:


class Contact < ActiveRecord::Base
  def self.columns() @columns ||= []; end
  def self.column(name, sql_type = nil, default = nil, null = true)
    columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
  end

  column :full_name,          :string
  column :email,              :string
  column :comments,           :string
  column :address,            :string
  column :city,               :string
  column :state,              :string
  column :zip_postal,         :string
  column :phone,              :string

  validates_presence_of     :full_name
  validates_confirmation_of :email
  validates_format_of       :email, 
                            :with => /^[_a-z0-9-]+(.[_a-z0-9-]+)*@([0-9a-z](-?[0-9a-z])*.)+[a-z]{2}([zmuvtg]|fo|me)?$/i,
                            :message => 'does not appear to be a valid email'

  validates_presence_of     :address
  validates_presence_of     :city
  validates_length_of       :state, :is => 2
  validates_format_of       :phone,
                            :with => /\d(?{3})?[- ]?\d{3}-?\d{4}/,
                            :message => 'should be ten digit number in the form: 999-999-9999',
                            :on => :create
  validates_length_of       :zip_postal, :in => 5..10, 
                            :message => 'Zip/postal code must be a valid US or Canadian zip/postal code'
  validates_format_of       :zip_postal,
                            :with => /(\d{5}(-\d{4})?)|([ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d)/,
                            :message => 'Zip/postal code matches neither US nor Canadian formats'

  protected                                                  
  def validate
    errors.add(:state, 'unrecognized state name. use two-letter abbreviation.') unless State.valid?(self[:state])
  end
end

I left all the validations in there in case you want to use simple email and postal-code regex validation.

This model basically creates the columns from your specification on the fly. The key is in the first four lines of code which overrides a couple of ActiveRecord methods. Namely column and columns. You use this model exactly as you would a model that represents a database table.

Just to convince you I really use this, here's my controller code:


class ContactController < ApplicationController
  layout 'two-col'

  def index
    @contact = Contact.new
  end

  def thank_you
      @contact = Contact.new(params[:contact])
      if !@contact.valid?
        render(:action => 'index') and return
      else
        email = ContactMailer.create_sent(@contact)
        ContactMailer.deliver(email)
        email = ContactMailer.create_confirm(@contact)
        ContactMailer.deliver(email)
    end
  end
end

Now you can create all the forms you like the same way regardless of whether the results of form submission are saved to a database.