Search code examples
rubyruby-on-rails-5

Model before_save not using updated multiselect order coming from params/controller


In a form, I have a multiselect dropdown that I can manually sort. When I submit the form on edit, I get the following in the params:

"article" =>{"tag_ids"=>["", "4", "1", "2"]}

Which it is what I want, rather than 1, 2, 4.

Then, in the model I update the ordinal. I am unable to get the new order of tag_id from the parameter. When I call the article_tags I get the order of the table, and not the one in the parameters. I am assuming this is because there was no change in tag_ids (The same entries, different order). Is there a way I can access that order in model itself?

I can do some workarounds with hidden inputs, or attr_accessors, but I want to know if there is a way to do it from the model.

Controller:

def update
 if @article.update(article_params)
  redirect_to articles_path
 end
end

def article_params
 params.require(:article).permit(:title, tag_ids: [])
end

Models:

class Article
 has_many: :article_tags
 has_many: :tags, through: :article_tags

 before_save: :order

 def order
  self.article_tags.each_with_index do |i, idx|
   i.update_attribute(:ordinal, idx) 
  end
 end
end

class ArticleTag
 belongs_to: :article
 belongs_to: :tag
end

Tables

articles
 id  |   title
------------------
  1  |  Testing

tags
 id  |   name
------------------
  1  |  Business
  2  |  Education
  3  |  Health
  4  |  Social

article_tags
 id  |   article_id  |  tag_id  | ordinal
------------------------------------------
  1  |       1       |     1    |   2
  2  |       1       |     2    |   3
  3  |       1       |     4    |   1

Solution

  • You could do this by overriding the tag_ids= setter method created by has_many: :tags, through: :article_tags.

    class Article
      # ...
      def tags_ids=(ids)
        ids.reject(&:blank?).each_with_index do |id, index|
          article_tag = self.article_tags.find_or_intialize_by(tag_id: id)
          article_tag.update(
            ordinal: index
          )
          article_tag.where.not(tag_id: ids).destroy_all
        end
      end
    end