Forms with nested models

I recently built a form with nested models. It works, but want to know if what I did is best practice and how I can refactor it.

We have Posts with Tags. From the user’s perspective on the Post form they had a field where they could start typing into the Tag field and it would offer them existing tags to select, or it would allow them to type up a new tag word. The user would be able to add multiple tags this way.

For the front end I used the Select2 js library, which works well.

I created a PostTag model to record the association between Posts and Tags.

In the Post model I added: accepts_nested_attributes_for :tags, allow_destroy: true.

So far so good, however the controller for Post is quite complicated.

The params passed on the controller is a challenge to process, because the params were inconsistent. If the tag already exists, the params would be integers of tag ids wrapped into a string (ex. “3”), but if the tag was new it would be a straight string (ex. “history”).

I’m not sure if it’s the correct way to do it, but I basically did this:

  1. I would cycle through all the parameters and find the param called tags_attributes.
  2. Take the array of values (tags) within this and cycle through them as well.
  3. Identify if the param value is NOT an integer. This means it’s a new tag word.
  4. If so, I would create a tag in the database, and then replace the tag word within the params with the id of the newly created tag.
  5. Finally I created the Post with the cleaned up parameters, which now includes the nested tags through PostTag.

I feel like there has to be a more elegant solution and I shouldn’t have to hack the controller this way to create new tags, but I could be wrong.

Thanks for reading and let me know what you think!

About the front-end, I would be interested to know about other solutions than select2 which is jQuery based. Any way to achieve this in a hotwire-friendly way?

About nested tags which can be created on the fly I’m also interested to know. In my admin dashboards I currently have a very hacky solution for this.

1 Like

I think I wouldn’t use accepts_nested_attributes_for in this case since you are getting a value that’s not what accepts_nested_attribues_for expects (you have an array of ids in a comma-separated string, right?)

I would try a custom setter method to encapsulate that logic maybe:

def tags_as_string=(tags_string)
  # here you can check the value you get from Select2 and find/create Tags
end

and then you can use that for the input name post[tags_as_string] and permit it with strong params.

2 Likes

I use acts_as_ordered_taggable and in the same time I have these two methods in the model

  def tag_names=(names)
    self.tag_list = names
  end

  def tag_names
    self.taggings.map{|tagging| tagging.tag.name }.join(",")
  end

This allows me to have a tag_names param, but in my cases I’ve decided to be able to assign only tags that are already existing as tags are created by the admins

1 Like

That’s cool. And how would you handle it if there were new tags added dynamically by the user?

We decided not to do it for our case. Tags are created by admins and only selected.