While working on Rails 3 app, I came to the problem of nested forms.
One collection is a set of predefined objects (created with db:seed). The other collection should show a form to allow to choose a few elements.
An example is better than a long description, so here it is.
Suppose you have 2 models: User and Group.
Suppose there are a few groups: Member, Admins, Guests, ...
You want your users to have multiple groups, and so you need a intermediate table: memberships.
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
The controller should not need to be changed. The view, however is more complicated.
I want to show a list of checkboxes to choose a few groups of the predefined ones.
I am using here the special _destroy
field, with reversed value, to destroy when it is actually unchecked (and so add the user to the group when it is checked)
%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 do not work as expected, because the form g
will always create an input with an arbitrary id after each group (and even break the layout by including it after the </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" />
# ...
Knowing the syntax of nested attributes is the following:
{: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
Sending every time the id will not work, because it will try to update a record which does not exist instead of creating it, and try to destroy when the record does not exist.
The current solution I found is to remove all the ids, which are wrong anyway (they should be the ids of the memberships, instead of simple indexes), and add the real id when the user already has the group. (this is called in the controller before create and update)
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 it adds a lot of logic in the controller, just to control the bad behavior of the view fields_for
helper.
Another solution is to create all the form html yourself, trying to mimic the Rails conventions, and avoid the id problem, but that is really noisy in the code and I believe there is a better way.
- Is it a way to make
fields_for
work better? - Is there any helper more appropriate ?
- Am I reasoning wrong somewhere in this question?
- How would you do to achieve this?
Thanks