Question

I currently have a complex form with deep nesting, and I am using the Cocoon gem to dynamically add sections as required (e.g. if a user wants to add another vehicle to the sale form). Code looks like this:

<%= sale.fields_for :sale_vehicles do |sale_vehicles_builder| %>
    <%= render :partial => "sale_vehicles/form", :locals => {:f => sale_vehicles_builder, :form_actions_visible => false} %>    
<% end -%>
<div class="add-field-links">
    <%= link_to_add_association '<i></i> Add Vehicle'.html_safe, sale, :sale_vehicles, :partial => 'sale_vehicles/form', :render_options => {:locals => {:form_actions_visible => 'false', :show_features => true, :fieldset_label => 'Vehicle Details'}}, :class => 'btn' %>
</div>

This works very nicely for the first level of nesting - the sale_vehicle object is built correctly by Cocoon, and the form renders as expected.

The problem comes when there is another level of nesting - the sale_vehicle partial looks like this:

<%= f.fields_for :vehicle do |vehicle_builder| %>
    <%= render :partial => "vehicles/form", :locals => {:f => vehicle_builder, :f_parent => f, :form_actions_visible => false, :show_features => true, :fieldset_label => 'Vehicle Details'} %>
<% end -%>

The partial for the vehicle is rendered with no fields, because no sale_vehicle.vehicle object has been built.

What I need to do is thus build the nested object along with the main object (Cocoon does not currently build any nested objects), but how best to do this? Is there any way to select nested forms from the helper code so that these can be built?

Cocoon currently build the main object like this:

if  instance.collection?
    f.object.send(association).build
else
    f.object.send("build_#{association}")
end

If I could do something like the following, it would keep things nice and simple, but I'm not sure how to get f.children - is there any way to access nested form builders from the parent form builder?

f.children.each do |child|
    child.object.build
end

Any help appreciated to get this working, or suggest another way of building these objects dynamically.

Thanks!

EDIT: Probably worth mentioning that this question appears to be relevant to both the Cocoon gem mentioned above, and also Ryan Bates' nested_form gem. Issue #91 for the Cocoon gem appears to be the same problem as this one, but the workaround suggested by dnagir (delegating the building of the objects) is not ideal in this situation, as that will cause issues on other forms.

Was it helpful?

Solution

I can see in your second nested form there is no link_to_add_association.

Inside cocoon, the link_to_add_association does the building of a new element, for when a user wants to dynamically add it.

Or, are you implying that once a sale_vehicle is built, it should automatically contain a vehicle? I would assume a user would have to select the vehicle that is sold?

I have a test-project that demonstrates the double nested forms: a project has tasks, which can have sub-tasks.

But maybe that does not relate good enough to what you want to do?

You do not show your models, but if I understand correctly the relations are

sale 
  has_many :sale_vehicles
sale_vehicle
  has_one :vehicle (has_many?)

So if you have a sale_vehicle that can have a vehicle, then I would assume your user would first add the sale_vehicle to the sale and then, click the link to add the vehicle. That is what cocoon can do perfectly well. If, on the other hand, you want that when cocoon dynamically creates a sale_vehicle, a vehicle is also created, I see a few different options.

Use after_initialize

Can't say I am a real fan of this, but in the after_initialize callback of your sale_vehicle, you could always build the required vehicle model.

I am assuming here that since your sale_Vehicle is not valid/cannot exist without a vehicle model, it is the responsability of the model to create the nested model immediately on build.

Note that after_initialize is executed for each object creation, so this could be costly. But this could be a quick fix. If you reject empty nested models, this should work imho.

Use Decorator/Presenter

For the user, the sale_vehicle and vehicle seem one object, so why not create a decorator, composed of a sale_vehicle and a vehicle, which is presented into one (nested) form and when saving this, the decorator knows it needs to be saved into the correct models.

Note: there are different terms for this. A decorator usually only extends a single class with some view-methods, but it could as well be a composition of different models. Alternativd terms: presenter, view-model.

Anyway, the function of a decorator/presenter is to abstract away the underlying datamodel for your users. So, for whatever reason you needed to split up a single entity into two database models (e.g. to limit nr of columns, to keep models readable, ...) but for the user it still is a single entity. So "present" it as one.

Allow cocoon to call a custom build method

Not sure if I am a fan of this, but this definitely is a possibility. It is already supported if the "nested model" is not an ActiveRecord::Association, so this should not be too hard to add that. But I am hesitant about that addition. All those options make it more complicated.

EDIT: The simplest fix

Inside your partial just build the needed child object. This has to happen before the fields_for and then you are good to go. Something like

<% f.object.build_vehicle %>
<%= f.fields_for :vehicle do |vehicle_builder| %>
    <%= render :partial => "vehicles/form", :locals => {:f => vehicle_builder, :f_parent => f, :form_actions_visible => false, :show_features => true, :fieldset_label => 'Vehicle Details'} %>
<% end -%>

Conclusion

I personally really like the decorator approach, but it could be a bit heavy. Just build the object before you render call the fields_for, that way you are always sure there is at least one.

I am interested to hear your thoughts.

Hope this helps.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top