Reputation: 1079
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
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