eggroll
eggroll

Reputation: 1079

Rails 4 - Order of has_many, through nested attributes in form

tl,dr: How can I ensure that the order of my has_many, through nested attributes, with an attribute value set in the build, are always assigned the same nested parameters hash key number (0, 1, etc.) and always appear in the form in the same order?

Hopefully I can describe this so it makes sense. I have a small prototype app that simulates a simple bank transfer between two accounts, a source account and a destination account. I have a Transfer class, an Account class and a TransferAccounts class that is the through join for the many_to_many association between Transfer and Account.

Here is the new action in the TransfersController:

def new
  @transfer = Transfer.new
  @transfer.transfer_accounts.build(account_transfer_role: 'source').build_account
  @transfer.transfer_accounts.build(account_transfer_role: 'destination').build_account
  bank_selections
  account_selections
end  

And the strong parameters:

def transfer_params
  params.require(:transfer).
    permit(:name, :description,
           transfer_accounts_attributes:
             [:id, :account_id, :account_transfer_role,
              account_attributes:
                [:id, :bank_id, :name, :description, :user_name,
                 :password, :routing_number, :account_number
                ]
             ])
end

So, the two transfer_accounts associated with a transfer each have an account_transfer_role attribute, with one of them set to source and the other set to destination.

Now, when filling in the form and submitting, the parameters look like the following in console:

Parameters: {"utf8"=>"✓", "authenticity_token"=>"xxxxxxxxxxxxxxxxxx==",
"transfer"=>{"name"=>"Test Transfer 2", "description"=>"Second test transfer",
"transfer_accounts_attributes"=>{"0"=>{"account_transfer_role"=>"source", "account_id"=>"1",
"account_attributes"=>{"bank_id"=>"1", "name"=>"George's Checking",
"description"=>"George's personal checking account", "user_name"=>"georgeckg",
"password"=>"[FILTERED]", "account_number"=>"111111111", "routing_number"=>"101010101",
"id"=>"3"}, "id"=>"3"}, "1"=>{"account_transfer_role"=>"destination", "account_id"=>"2",
"account_attributes"=>{"bank_id"=>"2", "name"=>"George's Savings",
"description"=>"George's personal savings account", "user_name"=>"georgesav",
"password"=>"[FILTERED]", "account_number"=>"111101111", "routing_number"=>"100100100",
"id"=>"4"}, "id"=>"4"}}}, "commit"=>"Update Transfer", "id"=>"2"}

As you can see, each transfer_account in the the transfer_account_attributes hash has an id key, either a 0 or a 1 (e.g. ..."0"=>{"account_transfer_role"=>"source"...). Now, I have been working under the assumption (which I thought might come back to bite me, and it has) that because of the order they are built in the new action, that the source transfer_account would always have an id key of 0 and the destination transfer_account would always have an id key of 1, which led me to use these id keys elsewhere in the controller as though 0 represented source and 1 represented destination.

And all seemed to be working fine until I was trying different permutations of creating new or using existing accounts, creating new or editing existing transfers when suddenly the form appears with destination listed first and source second, which hadn't occurred before, causing the entries to now have destination associated with 0 and source associated with 1, breaking the code in the controller referred to above.

To make it clearer, here is the form:

#transfer_form
  = simple_form_for @transfer do |t|
    .form-inputs

      = t.input :name, label: 'Transfer Name'
      = t.input :description, required: false, label: 'Transfer Description'

      = t.simple_fields_for :transfer_accounts do |ta|

        - role = ta.object.account_transfer_role.titleize
        = ta.input :account_transfer_role, as: :hidden

        = ta.input :account_id, collection:    @valid_accounts,
                                include_blank: 'Select account...',
                                label:         "#{ role } Account",
                                error:         'Account selection is required.'

        .account_fields{id: "#{ role.downcase }_account_fields"}

          = ta.simple_fields_for :account do |a|

            = a.input :bank_id,        collection:    @valid_banks,
                                       include_blank: 'Select bank...',
                                       label:         "#{ role } Bank",
                                       error:         'Bank selection is required.',
                                       class:         "#{ role.downcase }_account_input_field"

            = a.input :name,           label:    "#{ role } Account Name",
                                       class:    "#{ role.downcase }_account_input_field"

            = a.input :description,    required: false,
                                       label:    "#{ role } Account Description",
                                       class:    "#{ role.downcase }_account_input_field"

            = a.input :user_name,      label:    "#{ role } Account User Name",
                                       class:    "#{ role.downcase }_account_input_field"

            = a.input :password,       label:    "#{ role } Account Password",
                                       class:    "#{ role.downcase }_account_input_field"

            = a.input :account_number, label:    "#{ role } Account Number",
                                       class:    "#{ role.downcase }_account_input_field"

            = a.input :routing_number, label:    "#{ role } Account Routing Number",
                                       class:    "#{ role.downcase }_account_input_field"

    = t.submit

How do I ensure that source is always first and, thus, always associated with the id key 0 and destination is always second, always associated with the id key 1?

Upvotes: 1

Views: 403

Answers (1)

eggroll
eggroll

Reputation: 1079

The answer appears to be as simple as changing the :transfer_accounts association line in my Transfer model from this:

has_many  :transfer_accounts, inverse_of: :transfer

to this:

has_many  :transfer_accounts, -> { order('account_transfer_role DESC') }, inverse_of: :transfer

If anyone believes it is not doing what I think it is doing, please let me know, because, at the moment it appears to be resolving my issue.

Upvotes: 1

Related Questions