0
votes

I have a model 'item', two models 'bar' and 'restaurant' and a model 'user'.

The relations between those models are:

  • User has_many :bars and has_many :restaurants
  • Item belongs_to :activity, polymorphic: true
  • Bar has_many :items, as: :activity
  • Restaurant has_many :items, as: :activity

How my _form view to create a new item should be like?
A user can create an item and assign it to a model that can be bar or restaurant, so i would like that user can choose in which activity the item should belongs to.
In my form i have something like <%= f.select :activity, @my_activities.collect { |a| [a.name, a.id] } %> but doesn't work.

2

2 Answers

0
votes

I used part of the solutions posted by 6ft Dan.

in items_controller i created a

before_action :set_activity, only: [:create, :update]


def set_activity
  @itemable = params["item"]["itemable"]

  if params["item"]["itemable"]["bar"]
    @activity = Bar.find(@itemable[3..-1])
  elsif params["item"]["itemable"]["res"]
    @activity = Restaurant.find(@itemable[3..-1])
  end
end

and then in create action i added

@item.update_attribute(:itemable, @activity)

after the

@item = Item.new(item_params)

and in my form i have

<%= f.select :itemable, options_for_select(@bars.map {|i| [i.name, "bar"+i.id.to_s]} + @restaurants.map {|i| [i.name, "res"+i.id.to_s]}) %>

Now item creates and has an itemable attribute linking to the activity which it belongs!

0
votes

For polymorphic associations you use a fields_for block:

<%= form_for(@bar) do |f| %>
  Bar<br />
  <%= select :id, options_for_select(@bars.map {|i| [i.name, i.id]}, include_blank: true) %><br />
  New Item<br />
  <%= f.fields_for :items do |a| %>
    Kind: <%= a.select :kind, options_for_select(Item.kinds.keys.map.with_index {|k,i| [k, i]}) %><br /> <!-- # If you have an enum of kind for item -->
    Vendor: <%= a.select :vendor_id, options_for_select(current_user.vendors.map {|i| [i.name, i.id]}) %><br /> <!-- # If you have specific vendors per user -->
    Name: <%= a.text_field :name %><br />
  <% end %>
  <%= f.submit %>
<% end %>

This will go inside your form_for tag. Use fields_for block to nest any polymorphic relation.

You would only use select for an attribute that exists when creating something new. The example you've partly written out looks like you're simply selecting from existing items. That would be a different answer if you're looking for that. You've specifically asked about creating an item. So you won't be using select for creating something by name, you will need a text_field to enter the new name.

You can ignore the two select fields for your solution. They are there to demonstrate select. I don't believe your answer needs a select field.

https://gorails.com/episodes/forum-nested-attributes-and-fields-for


On another note your naming scheme for Item may be confusing. The standard naming would be more like.

class Item < ActiveRecord::Base
  belongs_to :itemable, polymorphic: true
end

class Bar < ActiveRecord::Base
  has_many :items, as: :itemable, dependent: :destroy
  accepts_nested_attributes_for :items, reject_if: proc { |att| att['name'].blank? }
end

class Restaurant < ActiveRecord::Base
  has_many :items, as: :itemable, dependent: :destroy
  accepts_nested_attributes_for :items, reject_if: proc { |att| att['name'].blank? }
end

In this case you would use a fields_for on :items. The polymorphic relationship name activity or itemable is not referred to in the fields_for field. Rather it is the plural for Item so :items.


To answer:

i have a page to add an item, where the user fill informations like title, description, etc, and then chooses in which 'bar' or 'restaurant' he wants to publish it.

<%= form_for(@item) do |f| %>
  <%= f.select :itemable_type, options_for_select([Bar.name, Restaurant.name]) %>
  <%= f.select :itemable_id, [1,2,3] %># COMPLICATION, NEED AJAX/JS TO GET AVAILABLE IDs
  <%= f.text_field :name %>
<% end %> 

Well this is basically what you want to do. But you would need to have an Ajax/JavaScript call to change the available options for :itemable_id to the list of either Bars or Restaurants mapped with [:name, :id]. You could just use a text field to input the number of the ID of the Bar/Restaurant but this is not a user friendly experience.

If I was proficient in JavaScript I could give you a way to do this. One way would be to have duplicate :itemable_id fields and have javascript disable/remove whichever the select field it isn't using.

Alternative solution 1:

You could make a page for each type new_bar_item.html.erb and new_restaurant_item.html.erb and each of those you would simply put a hidden_field for itemable_type to be either Bar.name or Restaurant.name respectively. And then you would already know which collection to give to your select field. Map the collections for the select field to your :name, :id. This removes all the complication for doing this.

A workable solution 2:

A good way I can recommend to do it without JavaScript is to have both Bars and Restaurants listed.

<%= form_tag(@item) do |f| %>
  Choose either Bar or Restaurant.<br />
  Bar: <%= select_tag 'item[bar_id]', options_for_select(@bars.map {|i| [i.name, i.id]}, include_blank: true) %><br />
  Restaurant: <%= select_tag 'item[restaurant_id]', options_for_select(@restaurants.map {|i| [i.name, i.id]}, include_blank: true) %><br />
  Item name: <%= text_field_tag 'item[name]' %>
<% end %> 

Then in your ItemController you will need to write a method to check which field isn't blank and set the polymorphic type with that.

before_action :set_poly_by_params, only: [:create, :update]

private
def set_poly_by_params
  if !params["item"]["bar_id"].empty? ^ !params["item"]["restaurant_id"].empty?
    if !params["item"]["bar_id"].empty?
      params["item"]["itemable_id"] = params["item"].delete("bar_id")
      params["item"]["itemable_type"] = Bar.name
    else
      params["item"]["itemable_id"] = params["item"].delete("restaurant_id")
      params["item"]["itemable_type"] = Restaurant.name
    end
  else
    raise "some error about incorrect selection"
  end
end
# NOTE: The above code will fail if the form doesn't submit both a bar_id field and a restaurant_id.  It expects both, empty or not.

Solution 3 (revised #2)

<%= form_tag(@item) do |f| %>
  Location: <%= select_tag 'item[venue]', options_for_select(
    @bars.map {|i| [i.name, "b"+i.id.to_s]} +
    @restaurants.map {|i| i.name, "r"+i.id.to_s]}
  ) %><br />
  Item name: <%= text_field_tag 'item[name]' %>
<% end %> 

We've added a b before the ID for bar or r before the ID for restaurant. Then we simply need to parse the params for it.

before_action :set_poly_by_params, only: [:create, :update]

private
def set_poly_by_params
  if params["item"]["venue"]["b"]
    params["item"]["itemable_type"] = Bar.name
  else
    params["item"]["itemable_type"] = Restaurant.name
  end
  params["item"]["itemable_id"] = params["item"].delete("venue")[1..-1]
end

This meets your requirement of one select field with both Bars and Restaurants in it.