I have a model called Person that the user selects five personality Traits for. However, the order they pick them for matters (they are choosing most descriptive to least descriptive).
I know how to create a join table with a poison an do ordering that way. I'm using acts_as_list as well.
But I can't seem to find any help on, is how to create a way for the user of my app to set the order of the traits. That is I want to have say five select boxes on in the HTML and have them pick each one, and use something like jQuery UI Sortable to allow them to move them around if they like.
Here is a basic idea of my models (simplified for the purpose of just getting the concept).
class Person < ActiveRecord::Base
has_many :personalizations
has_many :traits, :through => :personalizations, :order => 'personalizations.position'
end
class Personalization < ActiveRecord::Base
belongs_to :person
belongs_to :trait
end
class Trait < ActiveRecord::Base
has_many :persons
has_many :persons, :through => :personalizations
end
I just have no idea how to get positioning working in my view/controller, so that when submitting the form it knows which trait goes where in the list.
After a lot of research I'll post my results up to help someone else encase they need to have list of records attached to a model via many-to-many through relationship with being able to sort the choices in the view.
Ryan Bates has a great screencast on doing sorting with existing records: http://railscasts.com/episodes/147-sortable-lists-revised
However in my case I needed to do sorting before my Person model existed.
I can easily add an association field using builder or simple_form_for makes this even easier. The result will be params contains the attribute trait_ids (since my Person has_many Traits) for each association field:
#view code (very basic example)
<%= simple_form_for @character do |f| %>
<%= (1..5).each do |i| %>
<%= f.association :traits %>
<% end %>
<% end %>
#yaml debug output
trait_ids:
- ''
- '1'
- ''
- '2'
- ''
- '3'
- ''
- '4'
- ''
- '5'
So then the question is will the order of the elements in the DOM be respected whenever the form is submitted. Specially if I implement jQuery UI draggable? I found this Will data order in post form be the same to it in web form? and I agree with the answer. As I suspected, too risky to assume the order will always be preserved. Could lead to a bug down the line even if it works in all browsers now.
Therefore after much looking I've concluded jQuery is a good solution. Along with a virtual attribute in rails to handle the custom output. After a lot of testing I gave up on using acts_as_list for what I am trying to do.
To explain this posted solution a bit. Essentially I cache changes to a virtual property. Then if that cache is set (changes were made) I verify they have selected five traits. For my purposes I am preserving the invalid/null choices so that if validation fails when they go back to the view the order will remain the same (e.g. if they skipped the middle select boxes).
Then an after_save call adds these changes to the database. Any error in after_save is still wrapped in a transaction so if any part were to error out no changes will be made. It was easiest therefore to just delete all the endowments and save the new ones (there might be a better choice here, not sure).
class Person < ActiveRecord::Base
attr_accessible :name, :ordered_traits
has_many :endowments
has_many :traits, :through => :endowments, :order => "endowments.position"
validate :verify_list_of_traits
after_save :save_endowments
def verify_list_of_traits
return true if @trait_cache.nil?
check_list = @trait_cache.compact
if check_list.nil? or check_list.size != 5
errors.add(:ordered_traits, 'must select five traits')
elsif check_list.uniq{|trait| trait.id}.size != 5
errors.add(:ordered_traits, 'traits must be unique')
end
end
def ordered_traits
list = @trait_cache unless @trait_cache.nil?
list ||= self.traits
#preserve the nil (invalid) values with '-1' placeholders
list.map {|trait| trait.nil?? '-1' : trait.id }.join(",")
end
def ordered_traits=(val)
@trait_cache = ids.split(',').map { |id| Trait.find_by_id(id) }
end
def save_endowments
return if @trait_cache.nil?
self.endowments.each { |t| t.destroy }
i = 1
for new_trait in @trait_cache
self.endowments.create!(:trait => new_trait, :position => i)
i += 1
end
end
Then with simple form I add a hidden field
<%= f.hidden :ordered_traits %>
I use jQuery to move the error and hint spans to the correct location inside the div of five select boxes I build. Then I had a submit event handler on the form and convert the selection from the five text boxes in the order they are in the DOM to an array of comma separated numbers and set the value on the hidden field.
For completeness here is the other classes:
class Trait < ActiveRecord::Base
attr_accessible :title
has_many :endowments
has_many :people, :through => :endowments
end
class Endowment < ActiveRecord::Base
attr_accessible :person, :trait, :position
belongs_to :person
belongs_to :trait
end