BillyBib
BillyBib

Reputation: 315

collection_select for polymorphic model

I have Tag set up as a polymorphic model in my Rails 5.1.3 app. I'm trying to set a collection_select up so that I can choose tags from a dropdown.

The form creates new tags and the association works great, however, I cannot get the tag name passed into the form, so my tags are saving name as an empty string.

_form.html.erb

<%= form_for [taggable, Tag.new] do |f| %>
  <%= f.collection_select :taggable_id, Tag.order(:name), :id, :name %>
  <%= f.submit %>
<% end %>

tags_controller.rb

class TagsController < ApplicationController
before_action :authenticate_user!

def create
  @tag = @taggable.tags.new(tag_params)
  @tag.user_id = current_user.id

  if @tag.save  
    redirect_to book_path(@taggable.book_id), notice: "Tag saved"
  else
    redirect_to root_path, notice: "Sorry, something went wrong"
  end
end

private

  def tag_params
    params.require(:tag).permit(:name)
  end

end

Tag params:

=> <ActionController::Parameters {"utf8"=>"✓", "authenticity_token"=>"z2PLrvETqtq742zmr3pghEYYqoGoLv05gLP3OXopLM+blWWw+HmR4AMMDB+5ET3E5YLXeyhMCFMHfdxJNHlkZA==", "tag"=><ActionController::Parameters {"taggable_id"=>"1"} permitted: false>, "commit"=>"Create Tag", "controller"=>"books/tags", "action"=>"create", "book_id"=>"26"} permitted: false>

edit: adding models and fields

book.rb

class Book < ApplicationRecord
  has_many :tags, as: :taggable
end

tag.rb

class Tag < ApplicationRecord
 belongs_to :taggable, polymorphic: true
end

tags table

create_table "tags", force: :cascade do |t|
  t.string "taggable_type"
  t.integer "taggable_id"
  t.integer "user_id"
  t.text "name"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

Upvotes: 1

Views: 373

Answers (1)

max
max

Reputation: 101811

The problem is that you are approaching the problem very wrong.

A tagging system where a tag only can belong to a single resource is pretty near worthless as a taxonomy. Instead you want a many to many association with a join table:

# app/models/tag.rb
class Tag < ApplicationRecord
  has_many :taggings
  has_many :taggables, through: :taggings
end

# This join model represents a tag attached to a resource
# app/models/tagging.rb
class Tagging < ApplicationRecord
  belongs_to :tag
  belongs_to :taggable, polymorphic: true
end

# We extract the taggable feature to a concern so that we don't have to repeat it.
# app/models/concerns/taggable.rb
module Taggable
  extends ActiveSupport::Concern

  included do
    has_many :taggings, as: :taggable
    has_many :tags, through: :taggings
  end
end

# app/models/book.rb
class Book < ApplicationRecord
  include Taggable
end

# just an example
class Film < ApplicationRecord
  include Taggable
end

This employs tags as a normalization table instead of duplicating the name over and over in the table. This lets you get books with a certain tags without using text searches which are much slower than using a join on an indexed column.

To set multiple tags on a Taggable you would use the tag_ids= setter which ActiveRecord creates for has_many associations:

<%= form_for(@book) do |f| %>
  <%= f.text_input :title %>
  <%= f.collection_select :tag_ids, Tag.order(:name), :id, :name, multiple: true %>
  <%= f.submit %>
<% end %>

This is done as part of the normal create / update action for the taggable (parent) resource. Creating a single tagging (creating a join record) at a time can be done but is not very useful.

POST /books/:book_id/tags would on the other hand be used to create a single tag and setup an association:

<%= form_for([@taggable, @tag]) do |f| %>
  <%= f.label :name do %>
    <%= f.text_input :name %>
  <% end %>
  <%= f.submit %>
<% end %>

class TagsController
  before_action :set_taggable

  # POST /books/:book_id/tags
  def create
    @tag = @taggable.tag.new(tag_params)

    if @tag.save
      redirect_to @taggable, success: "New tag created."
    else
      render :new
    end
  end

  def set_taggable
    @taggable = Book.find(params[:book_id])
  end

  def tag_params
    params.require(:tag).permit(:name)
  end
end

However with a decent use of Ajax you can let users create tags "inline" by sending a POST /tags request which gives a better UX.

Upvotes: 4

Related Questions