I am attempting to create a form that allows a user to add/edit/remove locations to a campaign. All the examples I have currently found are either for HABTM
forms (that do not allow the editing of additional attributes that exist in a has_many through
configuration) or only list out the existing relationships.
Below is an image showing what I am trying to accomplish.
The list would show every available location. Locations that have a relationship via the campaign_locations model will be checked and have their campaign_location specific attributes editable. Locations that are non-checked should be able to be checked, campaign_location specific data entered, and a new relationship created upon submission.
Below is the code I currently have implemented. I have tried making use of collection_check_boxes
, which is very close to what I need except it does not allow me to edit the campaign_location attributes.
I have been able to successfully edit/remove existing campaign_locations, but I cannot figure out how to incorporate this to also show all available locations (like the attached image).
Models
campaign.rb
class Campaign < ActiveRecord::Base
has_many :campaign_locations
has_many :campaign_products
has_many :products, through: :campaign_products
has_many :locations, through: :campaign_locations
accepts_nested_attributes_for :campaign_locations, allow_destroy: true
end
campaign_location.rb
class CampaignLocation < ActiveRecord::Base
belongs_to :campaign
belongs_to :location
end
location.rb
class Location < ActiveRecord::Base
has_many :campaign_locations
has_many :campaigns, through: :campaign_locations
end
View
campaign/_form.html.haml
= form_for @campaign do |campaign_form|
# this properly shows existing campaign_locations, and properly allows me
# to edit the campaign_location attributes as well as destroy the relationship
= campaign_form.fields_for :campaign_locations do |cl_f|
= cl_f.check_box :_destroy, {:checked => cl_f.object.persisted?}, false, true
= cl_f.label cl_f.object.location.title
= cl_f.datetime_field :pickup_time_start
= cl_f.datetime_field :pickup_time_end
= cl_f.text_field :pickup_timezone
# this properly lists all available locations as well as checks the ones
# which have a current relationship to the campaign via campaign_locations
= campaign_form.collection_check_boxes :location_ids, Location.all, :id, :title
Portion of Form HTML
<input name="campaign[campaign_locations_attributes][0][_destroy]" type="hidden" value="true" /><input id="campaign_campaign_locations_attributes_0__destroy" name="campaign[campaign_locations_attributes][0][_destroy]" type="checkbox" value="false" />
<label for="campaign_campaign_locations_attributes_0_LOCATION 1">Location 1</label>
<label for="campaign_campaign_locations_attributes_0_pickup_time_start">Pickup time start</label>
<input id="campaign_campaign_locations_attributes_0_pickup_time_start" name="campaign[campaign_locations_attributes][0][pickup_time_start]" type="datetime" />
<label for="campaign_campaign_locations_attributes_0_pickup_time_end">Pickup time end</label>
<input id="campaign_campaign_locations_attributes_0_pickup_time_end" name="campaign[campaign_locations_attributes][0][pickup_time_end]" type="datetime" />
<input id="campaign_campaign_locations_attributes_0_location_id" name="campaign[campaign_locations_attributes][0][location_id]" type="hidden" value="1" />
<input id="campaign_campaign_locations_attributes_0_pickup_timezone" name="campaign[campaign_locations_attributes][0][pickup_timezone]" type="hidden" value="EST" />
<input name="campaign[campaign_locations_attributes][1][_destroy]" type="hidden" value="true" /><input id="campaign_campaign_locations_attributes_1__destroy" name="campaign[campaign_locations_attributes][1][_destroy]" type="checkbox" value="false" />
<label for="campaign_campaign_locations_attributes_1_LOCATION 2">Location 2</label>
<label for="campaign_campaign_locations_attributes_1_pickup_time_start">Pickup time start</label>
<input id="campaign_campaign_locations_attributes_1_pickup_time_start" name="campaign[campaign_locations_attributes][1][pickup_time_start]" type="datetime" />
<label for="campaign_campaign_locations_attributes_1_pickup_time_end">Pickup time end</label>
<input id="campaign_campaign_locations_attributes_1_pickup_time_end" name="campaign[campaign_locations_attributes][1][pickup_time_end]" type="datetime" />
<input id="campaign_campaign_locations_attributes_1_location_id" name="campaign[campaign_locations_attributes][1][location_id]" type="hidden" value="2" />
<input id="campaign_campaign_locations_attributes_1_pickup_timezone" name="campaign[campaign_locations_attributes][1][pickup_timezone]" type="hidden" value="EST" />
<input name="campaign[campaign_locations_attributes][2][_destroy]" type="hidden" value="true" /><input id="campaign_campaign_locations_attributes_2__destroy" name="campaign[campaign_locations_attributes][2][_destroy]" type="checkbox" value="false" />
<label for="campaign_campaign_locations_attributes_2_LOCATION 3">Location 3</label>
<label for="campaign_campaign_locations_attributes_2_pickup_time_start">Pickup time start</label>
<input id="campaign_campaign_locations_attributes_2_pickup_time_start" name="campaign[campaign_locations_attributes][2][pickup_time_start]" type="datetime" />
<label for="campaign_campaign_locations_attributes_2_pickup_time_end">Pickup time end</label>
<input id="campaign_campaign_locations_attributes_2_pickup_time_end" name="campaign[campaign_locations_attributes][2][pickup_time_end]" type="datetime" />
<input id="campaign_campaign_locations_attributes_2_location_id" name="campaign[campaign_locations_attributes][2][location_id]" type="hidden" value="3" />
<input id="campaign_campaign_locations_attributes_2_pickup_timezone" name="campaign[campaign_locations_attributes][2][pickup_timezone]" type="hidden" value="EST" />
You should add a non-model attribute checkbox to your model and form, signifying whether to save or remove the relation. Add a hidden field with the relation id to the form, and finally override
accepts_nested_attributes_for
to save or destroy based on the checkbox and call super.The form:
option_included
will return true if there is a saved relation, otherwise false.The problem you're running into is that the blank locations haven't been instantiated, so your view has nothing to build form elements for. To fix this, you need to build the blank locations in your controller's
new
andedit
actions.Then, in your
edit
andupdate
actions, you need to remove any locations that have been left blank from theparams
hash when the user submits the form.Also, I think you'll need a hidden field for the
location_id
.