views:

33

answers:

0

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