I spent nearly the whole day making this work.
Invoice, has_many :items
Item belongs_to :itemizable, polymorphic: true
Domain & Service has_many :items, as: :itemizable
The problem was multiple things:
- The automagic of Formtastic can’t detect the collection if it’s a polymorphic association
- Formtastic doesn’t really play well with non-existent attributes
Initially, I’ve thought of just doing:
ActiveAdmin.register Invoice do form do |f| # ... f.has_many :items do |item| item.input :itemizable, collection: (Domain.all + Service.all) item.input :quantity item.input :price_per_piece end f.actions end end
But this fails because 1) domains and service can share the same id and 2) I have no way to tell what the item was.
A few hours in and I was going nowhere. It’s surprisingly hard to look for anything related to polymorphic associations on Formtastic. This post gave me an idea however.
So, I’ve thought, why not just hold the id temporarily on an accessor attribute and just do the assignment from a callback before validation kicks in based on which attribute it went into? Raise an error if both were filled up.
It worked! I can now save new polymorphic records. (look at Item#assign_itemizable)
There’s a small problem however. The form to edit an existing record doesn’t pre-populate the corresponding select dropdowns. The solutions was rather simple, override the reader method to return the id of the itemizable if the itemizable is a member of the class.
Maintenance-wise, everything here would add overhead for every new itemizable model I would associate to item, but overall, I think it was a pretty elegant hack. *pats self at back*
Here’s the complete code:
# app/models/invoice.rb class Invoice < ActiveRecord::Base has_many :items accepts_nested_attributes_for :items end # app/models/item.rb class Item < ActiveRecord::Base before_validation :assign_itemizable belongs_to :invoice belongs_to :itemizable, polymorphic: true validates :itemizable, presence: true attr_accessor :itemizable_domain, :itemizable_service def itemizable_domain self.itemizable.id if self.itemizable.is_a? Domain end def itemizable_service self.itemizable.id if self.itemizable.is_a? Service end protected def assign_itemizable if [email protected]_domain.blank? && [email protected]_service.blank? errors.add(:itemizable, "can't have both a domain and a service") end unless @itemizable_domain.blank? self.itemizable = Domain.find(@itemizable_domain) end unless @itemizable_service.blank? self.itemizable = Service.find(@itemizable_service) end end end # app/admin/invoice.rb ActiveAdmin.register Invoice do form do |f| f.inputs "Invoice" do f.input :customer f.input :invoice_number f.input :issuing_person f.input :issued_on f.input :remarks end f.has_many :items do |item| item.input :itemizable_domain, collection: Domain.all item.input :itemizable_service, collection: Service.all item.input :quantity item.input :price_per_piece end f.actions end end
Edit: Fixed a bug in Item because I overwrote the accessors, which were being called in the validation. This caused a sort of chicken-egg situation, and ultimately causes the validation to fail all the time. Changed .empty? to .blank? so it doesn't barf out on nils.