56

I want to get possibility to select several Categories for one Post with multiple select.

I have next models: Post, Category and PostCategory.

class Post < ActiveRecord::Base
  has_many :post_categories
  has_many :categories, :through => :post_categories
end

class Category < ActiveRecord::Base
  has_many :post_categories
  has_many :posts, :through => :post_categories
end

class PostCategory < ActiveRecord::Base
  has_one    :post
  has_one    :category
  belongs_to :post      # foreign key - post_id
  belongs_to :category  # foreign key - category_id
end

In my controller I have something like @post = Post.new . I've created some categories.

And in view I have:

<%= form_for @post do |f| %>
    <%= f.text_field :title %>
    <%= f.select :categories, :multiple => true %>
    <%= f.submit %>
<% end %>

And... where is my categories? I have only "multiple" in select options. I think it's something wrong with my form.

Oleg Pasko
  • 2,831
  • 5
  • 35
  • 44
  • Now I have `<%= select_tag "categories", options_from_collection_for_select(Categories.all, 'id', 'name'), :multiple => true %>`. And in "create" action of controller I have `@post = Post.new(params[:post])`. What I need to create new records in PostCategory? – Oleg Pasko Jan 12 '12 at 17:49

5 Answers5

136

Sorry to resurrect the dead, but I found a much simpler solution that lets one use the default controller action code and use the ActiveModel setter logic for a has_many. Yes, it's totally magic.

<%= f.select :category_ids, Category.all.collect {|x| [x.name, x.id]}, {}, :multiple => true %>

Specifically, using the :category_ids (or :your_collection_ids) param name will automagically tell Rails to call @post.category_ids = params[:post][:category_ids] to set the categories for that post accordingly, all without modifying the default controller/scaffold #create and #update code.

Oh, and it works with has_many :something, through: :something_else automatically managing the join model. Freaking awesome.

So from the OP, just change the field/param name to :category_ids instead of :categories.

This will also automatically have the model's selected categories populate the select field as highlighted when on an edit form.

References:

From the has_many API docs where I found this.

Also the warning from the form helpers guide explains this "type mismatch" when not using the proper form-field/parameter name.

By using the proper form-field/param name, you can dry up new and edit forms and keep the controllers thin, as encouraged by the Rails way.

note for rails 4 and strong parameters:

def post_params
  params.require(:post).permit(:title, :body, category_ids: [])
end
Alok Swain
  • 6,409
  • 5
  • 36
  • 57
winfred
  • 3,053
  • 1
  • 25
  • 16
  • Thank you! I knew there had to be an easy and elegant way to get this working in Rails 3 :-) – gernberg May 11 '12 at 21:04
  • @winfred I have nearly the exact same setup except that my post has a `has_one` relationship with categories. Is there any `has_one` equivalent for `has_many`'s `name_ids` method? – cyreb7 Aug 12 '12 at 19:30
  • @cyreb7 I believe that would be ":category_id". =-) – winfred Aug 14 '12 at 15:56
  • 1
    In Formtastic it's `f.input :categories` – kim3er Oct 04 '12 at 15:20
  • 9
    This is what `collection_select` is intended for: `<%= f.collection_select :category_ids, Category.all, :id, :name, {}, { :multiple => true } %>` – graywh Apr 02 '13 at 16:25
  • I tried this, but I get a NoMethodError like this `undefined method 'category_ids' for #` when the view is loaded. I have the association `has_many :campaign_categories` and `has_many :categories, :through => :campaign_categories` in my Campaign model and the corresponding associations in the Category and CampaignCategory models. Does anyone know why this doesn't work? – Pedro Cori May 29 '13 at 20:42
  • category_ids is working great, but if you submit an invalid record -validation will fail- this will rerender the form without selecting the chosen categories. Ex: p=Post.new; p.category_ids = [1,2,3]; p.category_ids => [] – Mahmoud Khaled Mar 20 '14 at 12:18
  • @MahmoudKhaled that sounds unrelated to the attribute collection helper. I make sure to add something like "validates :collection_ids, length: { minimum: 1 }" in the model if I want to validate it, and re-rendering the form doesnt clear the errors on it either. You may have other code clearing the error unexpectedly, because I rely on re-rendered forms and these collection helpers correct validations every day – winfred Mar 31 '14 at 04:49
  • 7
    For people using strong parametres in Rails 4. to allow for array variable user :category_ids => [] in stead of just :category_ids it the permit params list. – Ole Henrik Skogstrøm Apr 11 '14 at 19:16
  • Does it work in Rails 5.1.4? I have problems getting it to work. I have permitted the attribute in strong parameters, but the associated model does not get touched (e.g. the `has_many, through: :post_categories won't create a new record). Any ideas? – mabu Nov 06 '17 at 15:27
  • Yeah it still works this way in 5.1, so I'd recommend checking your POST params and your model API (does the `#{collection}_ids=` setter exist, and do the post params have that in them?) – winfred Nov 22 '17 at 22:26
11

Final solution to organize categories in your posts, I hope it will be useful.

To use multiple we need select_tag:

<%= select_tag "categories", options_from_collection_for_select(Categories.all, 'id', 'name'), :multiple => true %>

Or f.select (many thanks to Tigraine and Brent!), it's more elegant way:

<%= f.select :categories, Category.all.collect {|x| [x.name, x.id]}, {}, :multiple => true %>

In create action of our controller we need:

def create
   @post = Post.new(params[:post])

if @post.save

  params[:categories].each do |categories|
     categories = PostCategory.new(:category_id => categories, :post_id => @post.id)
     if categories.valid?
       categories.save
     else
       @errors += categories.errors
     end
  end
  redirect_to root_url, :notice => "Bingo!"
else
  render "new"
end
end
Oleg Pasko
  • 2,831
  • 5
  • 35
  • 44
1

What you need is a list of options for the select:

<%= f.select :category_id, Category.all.collect {|x| [x.name, x.id]}, :multiple => true %>
Tigraine
  • 23,358
  • 11
  • 65
  • 110
  • Thanks! All categories now is available for select... but not for multiple select :) I'll be grateful if you have any ideas. – Oleg Pasko Jan 11 '12 at 21:19
  • I forgot the multiple => true in my answer at first.. I updated it an hour ago or so.. – Tigraine Jan 11 '12 at 23:16
  • Or is it maybe :html => { :multiple => true } – Tigraine Jan 11 '12 at 23:17
  • I've tried both variants, not works :( In html I've: `` – Oleg Pasko Jan 12 '12 at 14:48
  • Maybe problem with rails — I'm using rails3.2rc. Or form_for incompatible with multiple option. – Oleg Pasko Jan 12 '12 at 16:42
  • 1
    Yeah, multiple isn't compatible with f.select. I've converted it to `<%= select_tag "categories", options_from_collection_for_select(Categories.all, 'id', 'name'), :multiple => true %>` and now it works in html well! :) – Oleg Pasko Jan 12 '12 at 17:01
1

Tigraine almost had it, but you need to specify an additional empty hash:

<%= f.select :category_id, Category.all.collect {|x| [x.name, x.id]}, {}, :multiple => true %>

Brent Sowers
  • 633
  • 7
  • 9
0

As the @post does not have id, the from might not display categories as there is no association. You need to pass do a build on @post something like

 @post = Post.new(:categories => Category.all)
naren
  • 937
  • 1
  • 6
  • 21
  • Thanks for the reply. With "build" I have "undefined method `build' for ..." error :( – Oleg Pasko Jan 11 '12 at 21:15
  • That's because there is no such method. It's only available on associations.. He meant Post.new(:categories => Category.all) but that means your post is initialized to contain ALL categories. – Tigraine Jan 11 '12 at 21:17