Create new record with tags (has_many_through) when record isn’t persisted yet

211 Views Asked by At

I would like to implement custom tagging feature. So far I have Item and Tag models but I struggle with submission of tags – Rails keep telling me that Tag assignment is invalid whenever I try to create new Item.

(Creation of new item represents text area for item description and bunch of checkboxes with all existing tags from db.)

Curiously updating of tags of existing items works fine. This observation led me to discovery that culprit lies in fact that during build the item object is not persisted so it does not have an id yet, hence the relationship between item and tags cannot be established at the moment.

Based on this I have augmented create action in ItemsController with following contraption. While this works it seems like ugly hack to me, so I would like to know what is the proper way™ to handle this situation.

class ItemsController < ApplicationController
⋮
  def create
    tags = {}
    tags[:tag_ids] = item_params.delete('tag_ids')
    reduced_params = item_params.reject{|k,v| k == 'tag_ids'}
    @item = current_user.items.build(reduced_params)
    @item.save
    @item.update_attributes(tags)


Relevant code

class Item < ApplicationRecord
  has_many :tag_assignments, foreign_key: 'tagged_item_id'
  has_many :tags, through: :tag_assignments
  ⋮
class TagAssignment < ApplicationRecord
  belongs_to :tagged_item, class_name: 'Item'
  belongs_to :tag
  ⋮
class Tag < ApplicationRecord
  has_many :tag_assignments
  has_many :items, through: :tag_assignments
  ⋮

items_controller.rb

def create
    @item = current_user.items.build(item_params)
    ⋮
private

  def item_params
    params.require(:item).permit(:description, :tag_ids => [])
  end

_item_form.html.erb

⋮
<section class="tag_list">
  <%= f.collection_check_boxes :tag_ids, Tag.all, :id, :name do |cb| %>
      <% cb.label {cb.check_box + cb.text} %>
  <% end %>
</section>
2

There are 2 best solutions below

0
On

You might want to use virtual attributes, in this case tag_list , which will use either new or already created and persisted tags present in the database.

class Item < ApplicationRecord
  def tag_list
    self.tags.collect do |tag|
    tag.name
    end.join(", ")
   end

   def tag_list=(tags_string)
    tag_names = tags_string.split(",").collect{|s| s.strip.downcase}.uniq
    new_or_found_tags = tag_names.collect { |name| Tag.find_or_create_by(name: name) }
    self.tags = new_or_found_tags
   end

Now in your items form all you need from tags is to just refer the virtual attribute instead of

 <%= f.label :tag_list %><br />
 <%= f.text_field :tag_list %>

This should be simple and should result in less complicated code overall. Also do you not forget to change your current code accordingly. For example, in your items controller

private

  def item_params
    params.require(:item).permit(:description, :tag_list)
  end

Now your item new and create actions can be as simple as

def new
    @item = Item.new
end

def create
    @item = Item.new(item_params)
    @item.save
    redirect_to ......
end
1
On

I would recommend using the acts_as_taggable_on gem. It solves almost all situations regarding the use of tags.

class User < ActiveRecord::Base
  acts_as_taggable # Alias for acts_as_taggable_on :tags
  acts_as_taggable_on :skills, :interests
end

class UsersController < ApplicationController
  def user_params
    params.require(:user).permit(:name, :tag_list) ## Rails 4 strong params usage
  end
end

@user = User.new(:name => "Bobby")

@user.tag_list.add("awesome")   # add a single tag. alias for <<
@user.tag_list.remove("awesome") # remove a single tag