Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails has_many through form with additional attributes

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.

List of locations to add to a campaign

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" />
like image 469
jamgood96 Avatar asked Feb 24 '15 19:02

jamgood96


1 Answers

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 and edit actions.

class CampaignController < ApplicationController
  def new
    empty_locations = Location.where.not(id: @campaign.locations.pluck(:id))
    empty_locations.each { |l| @campaign.campaign_locations.build(location: l) }
  end

  def edit
    # do same thing as new
  end
end

Then, in your edit and update actions, you need to remove any locations that have been left blank from the params hash when the user submits the form.

class CampaignController < ApplicationController
  def create
    params[:campaign][:campaign_locations].reject! do |cl|
     cl[:pickup_time_start].blank? && cl[:pickup_time_end].blank? && cl[:pickup_timezone].blank?
    end
  end

  def update
    # do same thing as create
  end
end

Also, I think you'll need a hidden field for the location_id.

like image 165
Richard Jones Avatar answered Nov 15 '22 11:11

Richard Jones