I am trying to create a set list app for musicians. Users can upload their repertoire of songs & pick songs from a list of those songs to populate a set list for a specific performance.
See image: setlist_screenshot. Clicking the arrow icon on the songs on the left should add the song to a sortable list on the right.
Models in question:
class Setlist < ActiveRecord::Base
belongs_to :organization
# join between Set Lists & Arrangements(songs)
has_many :items, -> { order(position: :asc) }
has_many :arrangements, through: :items
end
-
class Item < ActiveRecord::Base
# join between Set Lists & Arrangements(songs)
belongs_to :arrangement
belongs_to :setlist
acts_as_list scope: :setlist
end
-
class Arrangement < ActiveRecord::Base
belongs_to :song
belongs_to :organization
has_many :attachments
# join between Set Lists & Arrangements(songs)
has_many :items
has_many :setlists, through: :items
end
Basically a setlist has many arrangements(which are songs) thru items(join table), and arrangements(songs) of course can be on many setlists.
The code I have in the view for the button to add a song is:
<%= link_to(organization_setlists_path(:arrangement_id => song.arrangements.first.id, setlist_id: @new_set.id, remote: true), method: "post", class: "secondary-content" )
which is a post request (including params for the song's arrangement id & id of a Setlist.new made in the new action) to the create action of the setlists controller, as below:
class SetlistsController < ApplicationController
before_action :authenticate_user!
def new
if @new_set.nil?
@new_set = Setlist.create
@new_item = Item.new
end
end
def create
@arrangement = Arrangement.find(params[:arrangement_id])
@new_item = Item.new
@new_item.setlist_id = Setlist.find(params[:setlist_id])
@new_item.arrangement_id = @arrangement.id
if @new_item.save
flash[:message] = "Song Sucessfully Added!"
else
flash[:message] = "Something Went Wrong"
end
end
Ideally, I would like a new item to be created(remotely) for the same setlist after each click of the add/arrow button, until the user is done creating that setlist. To display that on the right side, I have:
<% if @new_set && @new_set.items.count >=1 %>
<ul>
<% @new_set.items.each do |item| %>
<li class="center-align"><%= item.arrangement.title %></li>
<% end %>
</ul>
<% else %>
<p class="center-align">Add Songs From The List On The Left</p>
<% end %>
Eventually, the user should be able to finalize it and move on to creating another set list if they want.
I really have no idea how to go about this other than what I have done here, which is not working. The issues are:
For one, the create action is actually creating an item & assigning the new item the foreign key for arrangements but not for the setlists for some reason, even tho the setlist id is being passed in correctly from the link_to params.
Also, each time the page is refreshed, a new set list is generated from what I have in the new action, so each item added will go into a setlist, then a blank set list is generated, so nothing will ever appear on the right.
And finally, this just seems messy all together. Anyone have a better design strategy to accomplish this? Any help would be severely appreciated!!!
The main issue here is that your controller violates conventions.
Your SetlistController
should create a Setlist
when the user POSTs to /setlists
.
If you want to create a new child record you would POST to /setlists/:setlist_id/items
. This would be handled by ItemsController
.
# routes.rb
resources :setlists, shallow: true do
resources :items
end
class ItemsController < ApplicationController
respond_to :html
before_action :find_setlist!, only: [:create, :index]
def create
@item = @setlist.items.build(item_params)
@item.save
respond_with @item
end
# ...
private
def find_setlist!
@setlist = Setlist.includes(:items).find!(params[:setlist_id])
end
def items_params
params.require(:item)
.permit(
:arrangement_id
)
end
end
<% @arrangements.each do |a| %>
<%= button_to 'Add to set', setlist_items_path(@setlist),
arrangement_id: a.id, remote: true
%>
<% end %>
An alternative is to use accepts_nested_attibutes_for :items
to allow the Setlist
to accept the parameters for the items as well.
The key difference here is when adding additional items to an existing setlist it would for example use:
PATCH /setlist/:id
params: {
item_attributes: [
{
arrangement_id: 2
},
{
arrangement_id: 3
}
]
}
The real benefit is that the user can have performed multiple operations such as adding/removing and you just need to push the single setlist to the database to update the state, instead of using multiple POST / DELETE calls for each child record.
class Setlist < ActiveRecord::Base
belongs_to :organization
# join between Set Lists & Arrangements(songs)
has_many :items, -> { order(position: :asc) }
has_many :arrangements, through: :items
accepts_nested_attributes_for :items, allow_destroy: true
end
class SetlistsController < ApplicationController
respond_to :html
before_action :set_setlist, only: [:show, :edit, :update, :destroy]
def new
@setlist = Setlist.new
@setlist.items.build
end
def create
@setlist = Setlist.new(setlist_params)
@setlist.save
respond_with @setlist
end
def update
@setlist.update(setlist_params)
@setlist.items.build
respond_with @setlist
end
# ...
private
def set_setlist
@setlist = Setlist.includes(:items).find(params[:id])
end
def setlist_params
params.require(:setlist)
.permit(
:foo, :bar # attributes for the list
items_attributes: [ :position, :arrangement_id, :_destroy ]
)
end
end
The basic form setup to support this would look something like this:
<%= form_for(@setlist) do |f| %>
<%= fields_for :items do |item| %>
<%= item.number_field :postition %>
<%= item.collection_select(:arrangement_id, @setlist.organization.arrangements, :id, :name) %>
<%= item.checkbox :_destroy, label: 'Delete?' %>
<% end %>
<% end %>
Of course this just a plain old synchronous HTML form - adding AJAX functionality and drag'n'drop re-ordering etc is a bit out of scope.
Note that these solutions are not exclusive. You could easily have both options (nested attributes and a designated controller) in your application depending on if your requirements need you to be able to manipulate the child records individually as well or if you want to provide it as part of your API.