Rails - Dynamically build deeply nested objects (Cocoon / nested_form) - ruby-on-rails

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.

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.

Related

What's the best way to edit many objects of a single class in one Rails form?

I'm working on a Rails form that will allow the user to edit the attributes of many objects of a class with a single submission. My initial instinct was to create an outer form_for block and then iterate through the items within it using fields_for.
However, there is no object that bears a one-many relation to the objects the form will modify, and so it seems to me that there is no object that would be correct to pass into form_for.
In any case, what I'd like to see is an example of a form that modifies multiple objects simultaneously without appealing to a "parent" object. Perhaps this will involve form_tag?
(Note: I'm working in haml, so answers in haml would be awesome though unnecessary.)
Well, having a parent object will make it easier.
For bulk updates of many objects, the key is to use the right input name so that rails will parse the params as a array, i.e.
#posts.each do |post|
fields_for "posts[#{post.id}]", post do |p|
p.text_field :name
p.hidden_field :id
end
end
Have a look at the generated html source to see what name attribute the text input gets. If this is done right, params[:posts] will now be a hash in the controller which you can then update.
http://railscasts.com/episodes/165-edit-multiple should be relevant too
There are some extra wrinkles to my actual situation, but here's some pseudocode that I hope will illustrate the approach I've taken:
= form_tag url_for(:controller => "the_controller",
:action => "update") do
#objects_to_be_updated.each do |object|
= check_box_tag "[desired_path][through_the][parameters_hash]", true, object.boolean_attibute
= text_field_tag "[another_path][through_the][parameters_hash]", object.text_attribute
end
end
And so on.
Using the _tag variants of the form helpers, which don't require association with an Active Record model, is a bit of a pain but also seems to give you more control over structure of the resulting parameters.

Formtastic checkboxes not getting checked when editing resource, when using MongoMapper

With the following Store and Service models, managed with MongoMapper:
class Store
include MongoMapper::Document
key :service_ids, Array, :typecast => 'ObjectId'
many :services, :in => :service_ids
end
class Service
include MongoMapper::Document
key :name, String
many :stores, :foreign_key => :service_ids
end
I have this form, done with Formtastic:
<%= semantic_form_for #store, :url => admin_store_path(#store), :method => :put do |form| %>
<%= form.input :service_ids, :label => "Select Store Services",
:as => :check_boxes,
:collection => Service.all %>
<% end -%>
The controller uses Inherited Resources, and the edit action is implicit.
When editing a #store with services already associated with it, the checkboxes for the latter don't show as checked.
Formtastic's README warns it doesn't support MongoMapper officially, but it also says people have been using both together successfully, and I've seen some examples of this online.
I suspect Inherited Resources also doesn't support it, from what I've seen from Devise + Simple Form, both from the same authors and which don't support MM. They're working towards using an ORM adapter in their gems, but it isn't ready yet AFAIK.
And I've had problems with it already, I'm overriding the update action to get it to work:
def update
store = Store.find(params[:id])
if store.update_attributes!(params[:store])
flash[:notice] = 'Store was successfully updated.'
redirect_to admin_store_path(store)
else
redirect_to new_store_path
end
end
Does anybody know where the conflict with MM is, either in Formtastic or IR, and a hack just to get these checkboxes checking?
Most likely a Formtastic issue. It looks like the problem is here: https://github.com/justinfrench/formtastic/blob/master/lib/formtastic/inputs/check_boxes_input.rb#L122
Formtastic calls #store.service_ids to find the selected boxes. Service_ids returns an array of ObjectId's, but Formtastic was expecting an array of Store objects. If we follow Formtastic's code we'll see it tries a couple methods to find out how to get the "value" out of those ObjectId's and will eventually settle on "to_s" (see https://github.com/justinfrench/formtastic/blob/master/lib/formtastic/form_builder.rb#L20). Unfortunately, the to_s of an ObjectId is not the same as the id of your Store objects.
A hack that might make it work is to add an "id" method to ObjectId that returns self (Formtastic looks for id before it looks for to_s). A more appropriate patch would be to override this method https://github.com/justinfrench/formtastic/blob/master/lib/formtastic/inputs/base.rb#L104 to properly introspect MongoMapper associations, so that you could write form.input :services and it would turn that into an input with name of "service_ids" while still using the services method of your object. With that change it would still properly call #store.services and find the same kind of objects as Store.all and just work.
If you want to go that route, Store.associations[:services] should get you MongoMapper's definition of the association which you can introspect (see https://github.com/jnunemaker/mongomapper/blob/master/lib/mongo_mapper/plugins/associations/base.rb) but note that associations have been refactored a bit since the 0.8.6 gem, they're now in the separate classes BelongsToAssociation, OneAssociation, and ManyAssociation that each inherit from Associations::Base.
So, it doesn't seem like there's a simple fix. The other option is to generate your checkboxes by hand.
(Aside: I'm a little confused by your update method because I'd expect IR to do exactly what you've written internally, but if you had to write it that way to get it to work, so it is...)

One model and controller but slightly different views and routes in rails3

I have a model Notifications, and it basically handles the same few things. A simple contact form, an invitations form, etc. They all have the same generic items... ie Name, email, comment, blah.
There might be one slightly different field but they are optional, so I'd like to treat them as one model with a differentiating field called: notification_type so Invitation or Feedback, etc. And only render different views which i created in subfolders under notifications (notifications/invitations/).
I have it working fine with something like this in my routes:
routes
resources :notifications
match 'invites' => 'notifications#new', :defaults => { :notification_type => 'invitation' }
I pass the notification_type...
new.html.erb
<% if params[:notification_type] or params[:notification][:notification_type] == "invitation" %>
<%= render "notifications/invitations/form" %>
<% end %>
form.html.erb
I pass a hidden field for the notification_type
<%= f.input :notification_type, :as => 'hidden', :input_html => { :value => #notification.notification_type ||= params[:notification_type] } %>
It all seems to work.. the only caveat being that if they create an error, it sends them to the /notification route instead of being in invites.. but it still works correctly otherwise but I'm wondering it there's a simpler way to do the same thing? From within the controller layer? I feel like something's going to surprise me later as it stands.
I'd do the inheritance in the model and have two controllers. My controllers are as small as possible so normally I'm ok with adding many of them. But this is a pretty neat solution outside of the form errors. I think your hunch might be right as it gets more complex.
So to be clear, I'd have to models Invitation and Feedback. Each model would set_table_name to "notifications", inherit from Notification and then set the default notification in the initialize method or use the :defaults block you have in your routes already. This is a bit more flexible and explicit imo.

help a n00b understand rails, specifically inheritance, acts_as_nested_set, awesome_nested_set, sortable_element_for_nested_set

hey there, i'm trying to implement a drag and drop interface for a nested set in my first rails project. i'm new to rails so bear with me. my code is basically identical to this project: http://gist.github.com/128779. my problem is in the partial at this line:
<% for child in root.direct_children do %>
I'm getting a NoMethodError for direct_children which is an instance method of acts_as_nested_set, I believe. At the console if I try to create a new instance of my model, it is likewise unable to access the acts_as_nested_set instance methods, so I don't think the problem is in the partial but in the model.
Again, sorry if my terminology is wrong, I'm new to rails. Anyway, what am I doing wrong? I've got "acts_as_nested_set" in my model, just like the gist example above but my model does not appear to act as a nested set. How do I go about fixing this?
Thanks!
Here's the code for the model I am using (todo.rb):
class Todo < ActiveRecord::Base
acts_as_nested_set
end
And here's the partial:
<% content_tag :li, :id => dom_id(root) do %>
<%= content_tag :span, root.text %>
<% content_tag :ul do %>
<% for child in root.direct_children do %>
<%= render :partial => "tree", :locals => {:root => child}%>
<%end %>
<%end unless root.direct_children.empty? %>
<%end%>
root is passed to the partial from the view like:
<%= render :partial => "tree", :locals => {:root => #root} %>
and #root is defined in the controller like:
#root = Todo.find_by_parent_id(nil)
Again, the code is mostly copied wholesale with very few modifications from the gist link above.
A few things:
Have you checked that you installed the plugin properly? ./script/plugin install git://github.com/rails/acts_as_nested_set.git
Have you set up your table properly? Your model needs to have at least the following 3 columns by default (unless you want to override them): parent_id, lft, rgt. Without these acts_as_nested_set is going to have a hard time figuring out what's going on. I suggest you read the documentation at the top of this file because the readme doesn't say squat, nor does that gist for that matter.
If you've done the above, have you created a root element (not set the parent_id to anything) and then added at least one child to it?
m = Model.new
m.title = "My model's title"
m.save!
m2 = Model.new
m2.title = "My child"
m2.save!
m.add_child(m2)
I just did a quick test using the above, and afterwards I was able to do things like m.root? and m.direct_children. Good luck.
What I make from your title is that you're using quite a bit more than acts_as_nested_set. Try removing some plugins and try again.

Rails Form Builders - How to display a read only field or protect a field

I have created a form that needs to show data from 2 tables (parent and child). When the form is submitted only the child fields are updated (the parent fields are meant to be display only). While the parent model fields are displayed these need to be protected from updates (preferably via the formbuilder, rather than via css).
FWIW this is a pretty common master/detail use case. However I have not been able to find any examples of this - most of the examples I've seen seem to be trivial/single model display/update where all displayed fields are updateable).
Any ideas/samples/suggestion/tutorials/examples of real world, multi model Rails forms would be helpful.
TIA
Just out of interest, why bother going through the motions of creating a multi-model form when you only want to update the child record? My advice would be keep your form simple, I.e. make it a child form and just display the data from the parent record. If needs be, you could even style that display to look like part of the form, although I think that may throw the user off.
If you really need to do what you are doing, I would still use CSS to disable/readonly the input fields and in your controller update action, only pass the attributes you want to update into the update_attributes method call.
Finally, maybe look into the attr_protected method to prevent the fields you may want protecting from accidental mass-assignment.
I agree with tsdbrown, I don't think a complex form is required. If you'd like to learn more about complex forms or you really have your heart set on using a complex form I'd recommend watching the Railscasts episodes (73 - 75).
As tsdbrown said before, you are adding a complexity layer to your forms that's not need. If all you want is to update a detail model, while showing some info of it's parent, you could just do so with something like:
Order number: <%= #line_item.order.number %>
Order date: <%= #line_item.order.order_date %>
<% form_for #line_item do |f| %>
<%= f.label :quantity %>
<%= f.text_edit :quantity %>
<% end %>
When you'd like to edit both, then you can research on the field_for and nested forms methods (the Railscasts suggestion mentioned before it's great).
Thx for the responses which helped resolve my problem/question. Just want to close this out in case it helps others in the future.
Turns out I had been getting an error trying to reference my parent data element (patients.lname) as it was being passed in an array of results, rather than as a single result. In my view controller I had:
#visit = Visit.all(:select => "visits.id, visits.visittype, visits.visitdate, events.patient_id, patients.lname",
:conditions => ["visits.id = ?",params[:id] ],
:joins => "INNER JOIN events on events.id = visits.event_id INNER JOIN patients on patients.id = events.patient_id" )
In my view I had (this was giving me an invalid reference as I was doing a find all above):
<h1>Editing visit for patient :
<%= #visit.lname %> # should be <%= #visit[0].lname %>
</h1>
Below is the improved (and simpler) version where I find the specific record I need (basically replacing find all with find first):
#visit = Visit.find(:first, :select => "visits.id, visits.visittype, visits.visitdate, events.patient_id, patients.lname",
:conditions => ["visits.id = ?",params[:id] ],
:joins => "INNER JOIN events on events.id = visits.event_id INNER JOIN patients on patients.id = events.patient_id" )
And in the view:
<% form_for(#visit, :builder => LabelFormBuilder) do |f| %>
<%= f.error_messages %>
Name: <%= #visit.lname %>
<%= f.text_field :visittype %>
<%= f.date_select :visitdate %>
<p>
<%= f.submit 'Update' %>
</p>
<% end %>
Sometimes it's hard to see the wood for the trees! Hope this helps someone else.

Resources