Skip to main content

Form not working for polymorphic has_many through association

Rails • Asked by Nino Rosella

In my Rails 5.1 app I am trying to create a tagging system from scratch. I have been using the Comments with Polymorphic Associations episode as a template.

I want Tags to be a polymorphic has_many :through association so that I can tag multiple models.

Currently I'm able to create a Tag (and the associated Tagging) in the console by doing: Note.last.tags.create(name: "example") which generates the correct SQL:

Note Load (0.2ms)  SELECT  "notes".* FROM "notes" ORDER BY "notes"."id" DESC LIMIT $1  [["LIMIT", 1]]
(0.2ms)  BEGIN
SQL (0.4ms)  INSERT INTO "tags" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "example"], ["created_at", "2017-10-21 14:41:43.961516"], ["updated_at", "2017-10-21 14:41:43.961516"]]
Note Load (0.3ms)  SELECT  "notes".* FROM "notes" WHERE "notes"."id" = $1 LIMIT $2  [["id", 4], ["LIMIT", 1]]
SQL (0.4ms)  INSERT INTO "taggings" ("created_at", "updated_at", "tag_id", "taggable_id", "taggable_type") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["created_at", "2017-10-21 14:41:43.978286"], ["updated_at", "2017-10-21 14:41:43.978286"], ["tag_id", 9], ["taggable_id", 4], ["taggable_type", "Note"]]

But when trying to create a Tag and its associations through my form it doesn't work. I can create the Tag but no Tagging.

controllers/notes/tags_controller.rb

class Notes::TagsController < TagsController
  before_action :set_taggable

  private

  def set_taggable
    @taggable = Note.find(params[:note_id])
  end
end

controllers/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 @taggable, success: "New tag created."
    else
      render :new
    end
  end

  private

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

end

routes.rb

...
resources :notes, except: [:index] do
  resources :tags, module: :notes
end
...

.

class Note < ApplicationRecord
  belongs_to :notable, polymorphic: true
  has_many :taggings, as: :taggable
  has_many :tags, through: :taggings
end

class Tag < ApplicationRecord
  has_many :taggings
  has_many :taggables, through: :taggings
end

class Tagging < ApplicationRecord
  belongs_to :tag
  belongs_to :taggable, polymorphic: true
end

notes/show.html.erb

<p><%= @note.body %></p>
<%= render partial: 'tags/tags', locals: { taggable: @note } %>
<%= render partial: 'tags/form', locals: { taggable: @note }  %>

tags/form.html.erb

<%= simple_form_for [taggable, Tag.new] do |f| %>
  <%= f.input :name %>
  <%= f.submit %>
<% end %>

So Rails has some interesting behaviors to learn when saving models with join models. Most noteably you can save the join by adding the secondary child and passing it to the array on the parent. For example:

  note = Note.first
    tag = Tag.create(name: 'foo')
    note.tags << tag

Or you can do what the module does below here and build up all the objects and save them in a transaction (for proper rollback behavior).

module Taggable
  extends ActiveSupport::Concern

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

  def tag_list
    tags.pluck(:name)
  end

  def add_tag(tag_name)
    self.class.transaction do
      tagging = taggings.new
      tagging.tags.new(name: tag_name)
      tagging.save
    end
  end
end

class Note < ApplicationRecord
  include Taggable

  belongs_to :notable, polymorphic: true
end

class TagsController < ApplicationController
  before_action :authenticate_user!

  def create
    if @taggable.add_tag(tag_params[:name])
      redirect_to @taggable, success: "New tag created."
    else
      render :new
    end
  end

  private

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

Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 24,647+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.