Building a has_many, through model relationship and form with Cocoon

So, I’m working on a personal project to learn Ruby on Rails and the application structure that I desired required a complicated many-to-many relationship with a join model that itself contained data. This was a pretty complex model structure to setup and has numerous pitfall points that took a weekend of searching the Googles and reading a number of StackOverflow entries, GitHub gem documentation and RailsCasts to finally understand and get working the way I desired. Since all the documentation I found only dealt with small pieces of the whole and it took me all weekend to figure it out, I got to thinking there’s no way I’m the only one out there trying to grok this crap. So, now that I got it working, I’m going to share how the heck to do it so you can learn from my guinea pigging.

Application Summary

In this example, we’ll be setting up a Recipe book. Recipes have multiple meta properties like their title, a brief description, instructions and the like. Each recipe of course needs ingredients and each ingredient needs quantities of that ingredient for the recipe. What we will be setting up is a relationship of Recipes to Ingredients through Quantities.

Setting Up Your Models

recipe.rb

Adding the join model attributes to the mass assignment white-list

Our Recipe model contains the most configuration to set things up. As is expected, the attr_accessible property contains the properties of the Recipe model itself that can be modified, with the addition of a new property :quantities_attributes. This property will allow modification of the Recipe model (updating and creating) to also modify attributes of the associated Quantities records.

Glueing the models to each other through a join model

It used to be a complete pain in the butt to setup many to many relationships with Ruby on Rails and has_and_belongs_to_many configurations. Now, with Rails 3 though its super easy via the has_many, through property. This allows you to easily setup a relationship from one model to another with a join model between them to easily provide access to the models from each other. In this case for example @recipe.ingredients and with the through command on the Ingredient model end @ingredient.recipes.

Setting up the model’s ability to modify other model attributes

With the accepts_nested_attributes_for model property you can specify that a model will be able to also accept attributes for some of its relational models as setup through the has_many associations. You can also define rejection and deletion controls for the relational model through this method. See the official Active Record Nested Attributes documentation for more information.

quantity.rb

Adding the join model attributes to the …join model attribute white-list

Here we add the :ingredient and :ingredient_attributes to the mass-assignment white-list of the join model. This allows modification of the join model (Quantity) through the parent model (Recipe) to also modify and create entries in the final relational model (Ingredient). Note that the mass-assignment white-list on the join model also includes its own attribute of an :amount, something that using a has_many, through relationship gives us the ability to do.

Creating the join relationship

As this is a many to many relationship, there will be many recipes and many ingredients, but there will only be one quantity of an ingredient (or multiple ingredients) for each recipe. The belongs_to specification will explain to Ruby on Rails that each association of an Ingredient to a Recipe will be joined together by a record in the Quantity join table.

Allow the join model to modify its relational model

Setting up a similar accepts_nested_atrributes_for specification on the join model allows the join model (Quantity) to modify properties of the final relational model (Ingredient) so when a Recipe is saved with a nested form, the individual Quantity entries can have Ingredient properties and modify those Ingredient properties.

ingredient.rb

The final relational model doesn’t have a whole lot unique to it with the exception of the specification of the has_many :recipes, through: :quantities definition. This allows easy access to the recipes that an ingredient is associated to via simple @ingredient.recipes reference.

Installing Cocoon to make nested forms easy-peasy

To make setting up a nested form for a Recipe that can have 1 or more Ingredients each with a Quantity easy, we use a little gem called Cocoon. Cocoon provides drop-in JavaScript functionality to easily add and remove multiple entries of Ingredients to your nested form. As an added bonus, Cocoon can be combined with the Formtastic or Simple Form gems if you want to make your forms building even easier. To install Cocoon, just add it to your Rails project’s Gemfile:

After adding this line to your Gemfile, just run bundle install to get the gem installed in your application. With Cocoon installed now, just add this line to your application.js file to include Cocoon’s JavaScript to your asset pipeline:

This will allow all of Cocoon’s jQuery magic to do its thing.

Setting up the form code

The trixy part of getting all this model magic to work of course is getting your nested form setup. Nested forms are a pain in the butt, but hopefully this will help get you rolling on your project. You can build your form with the helper of your choice, but I’m going to just use Rails’ built in form helpers for this example to focus on the core of the setup instead of muddying the waters with a third-party helper. The only third party structure you may notice here is the Twitter Bootstrap structure (which, if you do want to use Formtastic, there is a handy-dandy Formtastic Bootstrap gem to modify its output to match Bootstrap’s structure).

The nested form

All of this form is pretty standard until you get to the nested portion, then it gets a bit tricky, so I’ll walk through each piece.

This loop creates all the form entry fields for your ingredients and their quantity amounts through a partial named after the Quantities themselves. The partial name here is important for Cocoon to work properly: _[model]_fields.html.erb.

This a new helper method introduced by Cocoon to help create additional Quantity/Ingredient fields in your form. Not all of the properties shown here on the link_to_add_association method are required, but some are necessary for our form interaction to work properly, notably the :wrap_object property:

This property will allow Cocoon to create a new, empty Ingredient object associated with the added Quantity entry on the form. Basically, without it you can’t add new ingredients, just a Quantity amount associated with no ingredient.

The data-association-insertion-node and data-association-insertion-method properties allow the “add” button to properly append new Quantity/Ingredient fields to our form structure. The nice thing is, once you have all this configured here, Cocoon does all the rest of the work – not a single line of JavaScript required.

The nested form partial

_quantity_fields.html.erb

The nested partial here will be used with the output form and the template for any added Quantity/Ingredient fields to your form interface. The unique piece of this template added for Cocoon is the link_to_remove_association method. This method automatically looks for the closest() element to it with the nested-fields class and will remove the fields from the form when clicked and therefor from the database record upon form submission. These properties cannot be modified, so make sure that the link_to_remove_association method is contained within an element in the partial file with the nested-fields class.

Voila!

That’s it! Now that we have sprinkled the appropriate Ruby pixie dust in the correct areas, the Rails framework will take over the rest of the work. Ain’t Rails beautiful?

41 Replies to “Building a has_many, through model relationship and form with Cocoon”

    1. The fields in the database are not really relevant for the discussion on this post. The integration method being demonstrated here has more to do with the relationships between the Recipe, Ingredient and Quantities models than the fields of each. You can glean from the code snippets though the fields for each model:

      Recipe

      title string
      description text
      instructions text

      Quantity

      amount string

      Ingredient

      name string
    1. The quantity model needs to have its migrate file adjusted to include;

      t.integer :recipe_id
      t.integer :ingredient_id

      Models with a belongs_to relationship need to hold the foreign keys in their migrations. Reference the Rails Guides.

  1. Also, I run the scaffold for Recipe, Quantity and Ingredient, but now have the error: Missing partial ingredients/index when I try to navigate to localhost:3000/recipes! Why do I have that error? Any ideas? Thanks! 🙂

    1. Not really sure what’s going on there, but I’d check to make sure that you’re following the example structure. If you’re modifying the code base, I’d start with everything written exactly as the examples are written (green) and then tweak things to match your desired architecture one thing at a time (refactor) so you know what’s breaking your form. Keep in mind that in the example, Ingredient entries are never directly referenced, but only as child elements of the Quantity entries, which are themselves child elements of the Recipe model. Since the Ingredient model only has one property (name) it is not directly referenced as a separate partial, but part of the _quantity_fields.html.erb partial.

  2. Awesome! Great tutorial! One question, in my application I’m trying to do, what would be the equivalent of, having the user select an ingredient from a select box, instead of providing the attributes to create a new one. So In my _quantity_fields.html.erb I have


    Unfortunately I’m getting an error from the recipe’s controller “Can’t mass-assign protected attributes: quantity”. I’ve added attr_accessible :quantity to my recipe model with no luck. Any clues?

    Thanks!

    1. eek I can see it did not like my code tags, let me try this again.

      <%= f.fields_for :quantity do |quantity_ingredient| %>
      <%= quantity_ingredient.input :ingredient_ids, collection: Ingredients.all %>
      <% end %>

      1. Hey TJ,

        You want f.fields_for :ingredient, not f.fields_for :quantity there. Keep in mind that you may be programming yourself into a corner if all you want to do is present a drop-down. Even if you are the only one entering a fixed set of ingredients, you’ll have a pretty crappy user experience once the drop-down exceeds about 50 (which will happen very quickly, just think of how many spices are on a spice rack). If you are allowing other users to input ingredients, you’ll also need to provide an alternative interface still for input of the new ingredient. I’d recommend sticking with a text field and writing an AJAX based predictive text auto-complete of existing ingredients in the database to help reduce duplicates.

  3. Hi, I am trying to enforce uniqueness of ingredients so that I can later on select recipes with a certain ingredient; I tried doing it by validating the uniqueness of the :name attribute:

    validates :name, presence: true, uniqueness: { case_sensitive: false }

    The problem is that the form (understandably) throws an error and does not allow me to save; if I do not enforce uniqueness, the form will create a new ingredient with the same name and a different id.
    How would you suggest I proceed?

    Thanks

    1. I had this exact scenario in my application and solved it by adding a setter override on the Quantity model (my :through joiner between the Recipe and Ingredient model). I still have the validation on the Ingredient of course, but the setter override takes care of either assignment or creation using ActiveRecord’s find_or_create_by capability:

      def ingredient_attributes=(ingredient_attributes)
      ingredient = Ingredient.find_or_create_by_name(ingredient_attributes[:name].downcase)
      self.ingredient_id = ingredient.id
      end

      This will either look up an existing ingredient and assign it, or create a new one based off a downcase unique name. Of course, this will still be fuzzy due to the human factor if you open it up to the public (one man’s Stewed Tomatoes is another man’s Tomatoes, Stewed. I try and solve this in my application with a predictive text auto-complete as the user types in an ingredient name. Combined with a separate field for quantity (to avoid Stewed Tomatoes and Can of Stewed Tomatoes), this is about as good as you can get at avoiding duplicates. Of course, if you’re the only one inputting ingredients, you can police yourself here as well.

      1. Thanks, it worked!
        I’ll pass on the autocomplete for now, I’m just starting out and I’ll be the one inputting ingredients anyway…

  4. Thank you very much for putting this tutorial together. I’ve gone through twice to make sure everything is in order (even copying and pasting directly the second time), and I’m not sure why I’m getting this error in the ingredient form:

    uninitialized constant Recipe::Quantity

    Highlighted line:

    I don’t even know how to begin troubleshooting/debugging this. Relationships in Rails are still something that I struggle with.

    I’m using Rails 4 with the protected attributes gem, though I don’t think that should be a problem.

    Thanks.

    1. It still doesn’t look like your code came through. I haven’t tested this code at all in Rails 4, so I can’t tell you if something is awry there or not. You may want to check out Railscasts for walk throughs to help you out understanding model relationships better.

  5. Would this enable you to call “@ingredient.recipes”, plural, to find all the recipes that include a particular ingredient? In your example you mention “@ingredient.recipe”, but it would be really useful to do an easy search for “@ingredient.recipes”….would that work with this example as is?

  6. Hello, good post.
    I need to add a new link_to_add_association but for duplicate a ingredient, because my model has more fields, it is possible?

  7. Hi Kynatro, good post!!
    I need to duplicate registers, because in my model I have six or seven fields, and only change one or to fields, this is possible?
    Can I do this with link_to_association?
    thanks in advanced

    1. The link_to_add_association action will place an entire empty partial in the view. This can contain as many fields as you want (I’m using Cocoon in another application where the partials contain about 15 fields of varying types of interfaces, not to mention a few more nested associations with their own link_to_add_association buttons)

  8. I know you wrote this over a year ago, but I got to say you really helped me out here. I’ve been trying to figure out this issue for days. Thanks for the post!

    1. Though for rails 4, your params need to look like this params.require(:recipe).permit(:id, :_destroy, :title, :description, :instructions,
      :quantities_attributes => [:id, :_destroy, :amount, :recipe_id, :ingredient_id,
      :ingredient_attributes => [:id,:_destroy, :name]])

      special no that ingredient attributes is singular, got stuck on that for a while

  9. Just a few comments to help others out:
    1. Can’t validate presence of ingredient_id in quantity
    validates :ingredient_id, :presence=>true #doesn’t work because of the order of the validations done in rails(I assume). As quantity is a join object, having presence of recipe_id and ingredient_id is necessary for a robust application.
    2. Getting the Cocoon layout correct for Formtastic forms was a major ordeal.

    Having the wrapper class for the nested form as an li tag with and ol tag helped a lot.

    <%=f.input....


    Putting the link_to_add_association in an li tag as well as link_to_remove… in an li tag
    3.
    link_to_remove…must go outside the innermost semantic_fields_for(), otherwise you will only remove part of what you need removed.

  10. Hi,
    I am getting Missing Template Error when i tried to render “myModel_fields” while i have myModel_fields.html.erb file in my app/views// folder

    1. Without seeing much of your code its hard to give some suggestions, but here’s a few things to check for:

      Make sure your :partial argument is pathed correctly
      Check that if you are referencing a partial in a sub-folder you do the full path (myModels/sub folder/partial)
      Check for typos 😉

      If you’re still having problems, post me a Gist (gists.github.com) with an example of your view files and partials (make note of the paths) and I can offer better help. I saw the error messages you posted but there isn’t enough contextual data in them to give better advice.

  11. Got the issue. naming issue with partial. But now i am getting the following error:

    undefined local variable or method `f’ for #<#:0x49ab2a8>

    partial page code :

  12. Another error after resolving earlier one:
    undefined method `keys’ for #

    code to insert partial:

    “#product_item_storage_infos ol”, ‘data-association-insertion-method’ => “append”, :wrap_object => Proc.new { |product_item_storage_info| product_item_storage_info.build_storage_location; product_item_storage_info } %>

  13. Code is as below :
    Models :
    class ProductItem “categoryId”
    has_many :recipe_products, :foreign_key => “productItemId”
    has_many :master_products, :foreign_key => “productItemId”
    has_many :recipes, :foreign_key => “productItemId” , :through => :recipe_products
    has_many :product_item_storage_infos, :foreign_key => “productItem_id”
    has_many :storage_locations, :foreign_key => “productItem_id”, :through => :product_item_storage_infos
    validates :productItemName, :presence => true

    accepts_nested_attributes_for :product_item_storage_infos,
    :reject_if => :all_blank,
    :allow_destroy => true
    accepts_nested_attributes_for :storage_locations
    end

    class StorageLocation “storageLocation_id”
    has_many :product_items, :through => :product_item_storage_infos
    end

    class ProductItemStorageInfo “productItem_id”
    belongs_to :storage_location, :foreign_key => “storageLocation_id”
    accepts_nested_attributes_for :storage_location,
    :reject_if => :all_blank

    end

    “product_item-storage_locations ol”, ‘data-association-insertion-method’ => “append”, :wrap_object => Proc.new { |product_item_storage_info| product_item_storage_info.build_storage_location; product_item_storage_info } %>

    Cocoon already added in js file

    1. Vasuneet, it doesn’t look like your partial page code went through. It would be better if I could actually see the code if its all right for it to be public, could you post it on Github.com or in a Gist? Your posts of source code a really flooding the comments.

  14. Thank ‘s to all now . I face a problem on controller with post method . can anybody help me????????????????

Leave a Reply