Search code examples
ruby-on-railsnested-formsnested-attributesstrong-parameters

Accepting nested strong_params using Cacoon


I have been trying to build a fairly complex recipe object that should have an indeterminate number of ingredients("macaroni", "cheese", "butter") and directions ("boil noodles", "add cheese").

The recipe form object is built using the Cacoon gem, allowing users to add those nested ingredient/direction values. I have defined the strong_params in the controller, and those parameters are successfully being sent to the create/update RecipesController action; however the controller is still rolling back the submission.

Any clues on what I have forgotten?

note

If I save a recipe without including any nested attributes, the recipe creates successfully, and then I can edit/update the recipe and add nested attributes. So it's only complaining on the create action...

Here's the relevant console output:

Processing by RecipesController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"[token removed for brevity]", "recipe"=>{"name"=>"French bread", "cuisine_id"=>"2", "ingredients_attributes"=>{"0"=>{"quantity"=>"500", "measure"=>"grams", "item"=>"flour", "_destroy"=>"false"}, "1"=>{"quantity"=>"250", "measure"=>"ml", "item"=>"water", "_destroy"=>"false"}, "2"=>{"quantity"=>"10", "measure"=>"grams ", "item"=>"yeast", "_destroy"=>"false"}}, "directions_attributes"=>{"0"=>{"direction"=>"Knead dough", "_destroy"=>"false"}, "1"=>{"direction"=>"Let rise 2 hours", "_destroy"=>"false"}, "2"=>{"direction"=>"Bake in 500 degree oven for 30 min.", "_destroy"=>"false"}}}, "button"=>""}
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?  [["id", 1], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  Cuisine Load (0.1ms)  SELECT  "cuisines".* FROM "cuisines" WHERE "cuisines"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
   (0.1ms)  rollback transaction

recipe.rb

    class Recipe < ApplicationRecord
     belongs_to :user
     belongs_to :cuisine

     has_many :ingredients, dependent: :destroy
     has_many :directions, dependent: :destroy
     validates :name, presence: true


     accepts_nested_attributes_for :ingredients,
                                  reject_if: :all_blank,
                                  allow_destroy: true

     accepts_nested_attributes_for :directions,
                                  reject_if: :all_blank,
                                  allow_destroy: true
   end

recipes_controller.rb

    class RecipesController < ApplicationController
      before_action :verify_user
      before_action :set_recipe, only: [:show, :edit, :update, :destroy]

      [...]

      def new
        @recipe = current_user.recipes.build
      end

      def create
        @recipe = current_user.recipes.build(recipe_params)

        if @recipe.save
         redirect_to @recipe, notice: "Recipe added!"
        else
         flash.now[:alert] = "There was a problem saving the recipe. Please try again."
         render :new
        end
      end

      [...]

    private

     def set_recipe
      @recipe = Recipe.find(params[:id])
     end

    def recipe_params
      params.require(:recipe).permit(:name, :cuisine_id,
                                   ingredients_attributes: [:quantity, :measure, :item, :_destroy],
                                   directions_attributes: [:direction, :_destroy])
     end
    end

_form.html.erb

<%= form_for recipe, html: { multipart: true } do |f| %>
  <div class="form-group">
    <%= f.label :name %>
    <%= f.text_field :name, placeholder: 'Recipe name', class: "form-control" %>
  </div>
  <div class="form-group">
    <%= f.label :cuisine_id %><br>
    <%= f.select(:cuisine_id, options_from_collection_for_select(Cuisine::all, :id, :name),
    {:prompt => 'Please Choose'}, :class => "form-control") %>
  </div>

    <div class="col-md-8">
      <div class="form-group">
        <h3>Ingredients</h3>
          <%= f.fields_for :ingredients do |ingredient| %>
            <%= render 'ingredient_fields', f: ingredient %>
          <% end %>
        <div class="links">
          <%= link_to_add_association 'Add Ingredient', f, :ingredients, class: "form-button btn btn-default" %>
        </div>
      </div>
    </div>

    <div class="col-md-4">
      <div class="form-group">
        <h3>Directions</h3>
        <%= f.fields_for :directions do |direction| %>
          <%= render 'direction_fields', f: direction %>
        <% end %>
        <div class="links">
          <%= link_to_add_association 'Add Direction', f, :directions, class: "form-button btn btn-default" %>
        </div>
      </div>
    </div>
    <div class="form-inline clearfix col-md-12">
      <%= f.button :submit, class: "form-button btn btn-success" %>
      <%= link_to "Back", url_for(:back), class: "form-button btn btn-default" %>
    </div>
<% end %>

_ingredient_fields.html.erb

<div class="form-inline clearfix">
  <div class="nested-fields">
    <%= f.number_field :quantity, placeholder: 'qnt', class: "form-control" %>
    <%= f.text_field :measure, placeholder: 'measure', class: "form-control" %>
    <%= f.text_field :item, placeholder: 'ingredient', class: "form-control" %>
    <%= link_to_remove_association "Remove", f, class: "form-button btn btn-default" %>
    </div>
  </div>

_direction_fields.html.erb

<div class="form-inline clearfix">
  <div class="nested-fields">
    <%= f.text_area :direction, placeholder: 'Direction', class: "form-control" %>
    <%= link_to_remove_association "Remove", f, class: "btn btn-default form-button" %>
  </div>
</div>

Solution

  • So the problem was a bug with Rails 5, related to the belongs_to_required_by_default method being set to false. This explains why records can be updated with nested attributes but not created. You can overcome the problem by either disabling the parent object validations entirely or by including inverse_of: :parent_model_name in your parent_model.rb file.

    More info about issue here