Rails nested attributes form for polymorphic/singl

2019-02-14 08:20发布

问题:

I am working on a form (using SimpleForm) that allows you to edit embedded associations. The problem that I'm running into is that the nested models are subclasses so they are different types with potentially different fields. I'm creating hidden forms for each type of model, and using JavaScript to display the form for the selected type.

FYI, I'm using the following gems:

  • Rails 3.2
  • Mongoid
  • SimpleForm

Here's a simplified example of what I have so far:

class Garage
  include Mongoid::Document
  embeds_one :vehicle
  accepts_nested_attributes_for :vehicle
end

class Vehicle
  include Mongoid::Document
  embedded_in :garage
  attr_accessible :_type
end

class Car < Vehicle
  field :car_field
  attr_accessible :car_field
end

class Truck < Vehicle
  field :truck_field
  attr_accessible :truck_field
end

In the console:

> garage = Garage.new
> garage.vehicle = Car.new(car_field: 'something')
> garage.save!

In the form:

= simple_form_for @garage do |f|
  = f.input :vehicle do |vehicle_form|
     = vehicle_form.input :_type, collection: ['Car', 'Truck']

  %span.hide{data:{fields-for:'Car'}}
    = vehicle_form.input :car_field

  %span.hide{data:{fields-for:'Truck'}}
    = vehicle_form.input :truck_field

:coffeescript
  $('#garage_vehicle_attributes__type').change ->
    type = $(@).find('option:selected').val()
    $('[data-fields-for="' + type + '"]').show()

The problem that will occur in this example is that it won't be able to render the truck_field because Car does not have a truck_field method. I'm not sure how to solve this problem besides throwing out any form helpers and managing the html and field values manually. Even after much Googling, I haven't been able to find any examples of this type of form.

How can this problem be solved in a standard, "Rails way" using existing form helpers?

回答1:

I think I had a similar problem, however instead of a has_one relationship, I have has_many.

Basically I used cocoon gem to dynamically add fields for each decorated class (such as Car or Truck) and then I accept_nested_attributes_for :vehicle. The form is built dynamically and on submit, the parameters go inside vehicle_attributes.

The code looks something like (updated for has_one association):

# _form.html.haml

= simple_form_for @garage, :html => { :multipart => true } do |f|

  = f.simple_fields_for :vehicles do |vehicle|
    = render 'vehicle_fields', :f => item
    = link_to_add_association 'Add a Car', f, :vehicles, :wrap_object => Proc.new { |vehicle| vehicle = Car.new }
    = link_to_add_association 'Add a Truck', f, :vehicles, :wrap_object => Proc.new { |vehicle| vehicle = Truck.new }

= f.button :submit, :disable_with => 'Please wait ...', :class => "btn btn-primary", :value => 'Save' 

Then inside _vehicle_fields partial you check what object it is (Car or Truck) and you render the correct fields.

I think this works quite good, and was exactly what I needed. Hope it helps.

I wrote a longer explanation at: http://www.powpark.com/blog/programming/2014/05/07/rails_nested_forms_for_single_table_inheritance_associations



回答2:

This is one of those situations where directly mapping a form to a model is not ideal. I think a user-filled form map and a persistence model instance are two very distinct concepts.

You might try subclassing Vehicle into a class that is used to accept form data. Then mix in all the extra code you need to handle what is specific to the form. This way, you keep your Vehicle model clean. You can also override methods in VehicleFormModel to work like a factory, to build the correct instance when the object is being created. In your controller, instantiate a VehicleFormModel instead of a Vehicle.

class VehicleFormModel < Vehicle
  include Car::FormModel
  include Truck::FormModel

  def self.build
    # Use a form field to handle specifics for each type,
    # delegating to the mixed in FormModel as needed
  end

end

class Car < Vehicle
  module FormModel
    def self.included(base)
      base.class_eval do
        field :car_field
        attr_accessible :car_field
      end
    end
  end
end