In no particular order

reflections, observations, politics, and technology

Mar 27

Formtastic without ActiveRecord

So I have been doing a fair amount of Rails development recently. I’ve been using Formtastic, which provides simple and consistent semantics for creating web forms. It’s a nice library and quite well designed. It is intended for use with ActiveRecord model objects, and trying to create forms that aren’t populating an ActiveRecord object can be challenging. The library’s author, Justin French, made an intriguing comment on StackOverflow: “It’s really not that hard to create a model instance to wrap up the concerns of your model. Just keep implementing the methods Formtastic is expecting until you get it working!”

I needed to put together a form to gather information for submission to a remote website (SalesForce). I didn’t have a model for the resulting data; that’s being tracked by SalesForce. Since I needed exactly what Justin described, I created one. He’s right, it wasn’t hard. I thought others might find it useful. Note: this will only work with Rails 3, and the example below uses HAML as the templating language.

I’ll start by showing usage. This should be pretty familiar to a Rails coder.

class ContactRequest < FormtasticFauxModel
  attr_accessor :first_name, :last_name, :company, :email, :newsletter_opt_in

  validates :first_name, :last_name, :presence => true
  validates :email, :presence => true, :email => true

  self.types = {
    :newsletter_opt_in => :boolean,
  }
end

The only unusual part is the self.types section, which I believe should be pretty self-explanatory. It gets passed through to Formtastic to inform what type of input to render, and the supported types are defined by Formtastic. This could be achieved by using Formtastic’s “:as => [type]” directive directly, but I prefer to define things once and in one place.

The ContactRequest object can now be used much like a regular ActiveRecord object. The controller manages an @contact variable, eg @contact = ContactRequest.new(params[:contact_request]), and then the view looks like any other formtastic form:

#header
  %h1 Contact Us

#body
  %h2 Contact Us
  %p Please provide the following information:
  = semantic_form_for(@contact) do |form|
    = form.inputs do
      = form_errors(@contact)

      = form.input :first_name
      = form.input :last_name
      = form.input :email
      = form.input :newsletter_opt_in, :label => "Send me the newsletter"
      = form.input :company
    = form.buttons do
      = form.commit_button :label => "Send it to me!"

All pretty standard.

So, with no further ado, here’s the FormtasticFauxModel class.

# -------------------------------------------------------------------------------
# Faux model object that plays well with formtastic; this is used for example in
# a contact form which generates a request to salesforce
# -------------------------------------------------------------------------------
class FormtasticFauxModel
  include ActiveModel::Validations
  include ActiveModel::Conversion  
  extend  ActiveModel::Naming

  # Subclass may provide a types hash.  Any attributes not listed will
  # default to string.
  # self.types = {
  #   :description => :text,
  #   :state => {:type => :string, :limit => 2}
  #   :country => :string,
  #   :newsletter_opt_in => :boolean,
  # }

  class << self
    attr_accessor :types
  end
  self.types = {}

  # So the controller can say "@contact = Contact.new(params[:contact])"
  def initialize(attributes = {})  
    attributes.each do |name, value|  
      send("#{name}=", value)  
    end
  end  

  # So form_for works correctly -- we only do "new" forms
  def persisted?  ;   false  ;    end  

  # To provide the type information
  def column_for_attribute(attr)
    FauxColumnInfo.new(self.class.types[attr])
  end

  class FauxColumnInfo
    attr_accessor :type, :limit

    def initialize(type_info)
      type_info ||= :string
      case
      when  type_info.instance_of?(Hash), type_info.instance_of?(OpenStruct)
        self.type = type_info[:type].to_sym
        self.limit = type_info[:limit]
      else
        self.type = type_info.to_sym
        self.limit = nil
      end
    end
  end
end

Enjoy!


  1. dondoh posted this