How to create a PORO model and use it in Rails Form Helpers

First off, I’ll start with our use-case:

We have a fairly simple form for an item with some complexity. It would have default values derived from the current_user‘s attributes but would still have the field on its own. Overriding it in Item model feels like overkill (and starts to be a violation of SRP) so we set out to create a PORO model for it. Full code and explanation below the fold.

class ItemDecorator
  include ActiveModel::Model

  def self.model_name
    ActiveModel::Name.new(self, nil, 'Item')
  end

  def method_missing(method, *args, &block)
    return self.send("default_#{method}") if self.respond_to?("default_#{method}")
    return @item.send(method, *args, &block) if @item.respond_to?(method)
    return super
  end

  def initialize(user:, item:)
    @user = user
    @item = item
  end

  def id
    @item.id
  end

  def persisted?
    # this is required for form_for to use the proper URL
    @item.persisted?
  end

  fields = %i( address_1 address_2 city contact_number contact_email nearest_landmark )
  fields.each do |field|
    define_method "default_#{field}" do
      return @user.send(field) if @item.new_record?
      return @item.send(field)
    end
  end
  # For search:
  # default_address_1 
  # default_address_2 
  # default_city 
  # default_contact_number 
  # default_contact_email 
  # default_nearest_landmark
  
  def default_min_order_unit
    return Item::UNIT_OPTIONS.first if @item.min_order_unit.blank?
    return @item.min_order_unit
  end

  def images_attributes=(attributes)
    # http://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for, Search: One-to-Many
    @item.images_attributes=(attributes)
  end

  def self.columns_hash
    Item.columns_hash
  end
end

# item = ItemFactory.new(current_user, Item.new)
# item.address_1

self.model_name and persisted? is required for form_for to work. Without overriding persisted?, editing an existing item doesn’t work properly with form_for (it always gets sent to #create).

I overrode method_missing to first check if default_#{method} already exists and call it instead. So, the call stack looks like the following when, for example, address_1 is called on the decorator:

  • If default_address_1 is defined, it calls it, which in turn checks if the current item is new or not.
  • If it’s a new item (e.g. form in ItemsController#new) it calls address_1 on user
  • If it’s not, then it calls the original address_1 of the item
  • If default_address_1 is not defined, then it checks of item responds to address_1 and calls it, if it does.
  • If it doesn’t, then it raises the usual NoMethodError exception

images_attributes= is required because the form craps out once we’re using nested fields (via fields_for). This is for attaching images via an imageable / Paperclip.

self.columns_hash is only required because we have a helper that requires it, but can otherwise be omitted.