How to write a nested form with the ratio NN

While working on a Rails 3 application, I ran into the problem of nested forms.

One collection is a collection of predefined objects (created using db: seed). Another collection should show a form that allows you to select multiple items.

An example is better than a long description, so here it is.

Suppose you have 2 models: user and group.

Suppose there are several groups: member, admins, guests, ...

You want your users to have multiple groups, so you need a staging table: membership.

The model code is obvious:

class User < ActiveRecord::Base has_many :memberships has_many :groups, :through => :memberships accepts_nested_attributes_for :memberships, :allow_destroy => true end class Membership < ActiveRecord::Base belongs_to :user belongs_to :group end class Group < ActiveRecord::Base has_many :memberships has_many :users, :through => :memberships end 

No need to change the controller. However, the opinion is more complicated.

I want to show a list of checkboxes to select multiple groups from predefined ones.

I use the special _destroy field with the changed value here to destroy when it is not actually checked (and therefore add the user to the group when it is installed)

 %p = f.label :name %br = f.text_field :name %ul = f.fields_for :memberships, @groups do |g| %li - group = g.object = g.hidden_field :group_id, :value => group.id = g.check_box :_destroy, {:checked => @user.groups.include?(group)}, 0, 1 = g.label :_destroy, group.name 

However, this does not work as expected, because the form g will always create an input with an arbitrary identifier after each group (and even break the layout by including it after </li> ):

 <input id="user_memberships_attributes_0_id" name="user[memberships_attributes][0][id]" type="hidden" value="1" /> <input id="user_memberships_attributes_1_id" name="user[memberships_attributes][1][id]" type="hidden" value="2" /> # ... 

The knowledge of the syntax of nested attributes is as follows:

 {:group_id => group.id, :_destroy => 0} # Create {:group_id => group.id, :_destroy => 0, :id => membership.id} # Update {:group_id => group.id, :_destroy => 1, :id => membership.id} # Destroy {:group_id => group.id, :_destroy => 1} # Do nothing 

Send every time the identifier will not work, because it will try to update the record that does not exist, and not create it, and try to destroy it when the record does not exist.

The current solution I found is to remove all identifiers that are wrong in any case (they should be membership identifiers, not simple indexes) and add a real identifier when the user already has a group. (this is called in the controller before creating and updating)

 def clean_memberships_attributes if membership_params = params[:user][:memberships_attributes] memberships = Membership.find_all_by_user_id params[:id] membership_params.each_value { |membership_param| membership_param.delete :id if m = memberships.find { |m| m[:group_id].to_s == membership_param[:group_id] } membership_param[:id] = m.id end } end end 

This seems so wrong, and adds a lot of logic to the controller, just to control the bad behavior of the fields_for .

Another solution is to create the whole html form yourself, trying to mimic the Rails conventions and avoid the identifier problem, but this is really noisy in the code, and I think there is a better way.

  • Is it possible to improve the performance of fields_for ?
  • Is there any helper?
  • Am I not mistaken in this matter?
  • How would you do that?

thanks

+4
source share
1 answer

I hope I understand you correctly?

Groups are predefined and you want to add the user to the group. The User Edit screen displays all or some of the predefined groups. You want to add a user by checking the box and save the entry. If you uncheck the box, the membership of this user in the group without participation should be equal to zero.

Here's how I do it with companies and projects:

 Class Company has_many :datasets has_many :projects, :through => :datasets Class Project has_many :datasets has_many :companies, :through => :datasets <% for company in Company.all %> <tr> <td> <%= check_box_tag 'project[company_ids][]', company.id, @project.companies.include?(company) %> </td> </tr> <% end %> 

I list all the Companies and check those that I want to include in the project.

Please tell me if this already helps you? I believe that you are using haml in your example. I'm not very used to these notations.

If you want to use a subset, you can use the scope:

 scope :recent, Company.where(:created_at => (Time.now.midnight - 1.day)..Time.now.midnight) 

Then you can use this area as the .all method:

 Company.recent 

Does it help?

+1
source

All Articles