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}
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