Search code examples
javascriptruby-on-railsruby-on-rails-5nested-formscocoon-gem

Nested form inside of a nested form - Cocoon Gem: 3rd level child doesn't save - Rails 5


For several hours trying to research a solution, found some very similar issues like this one, or this one, though all the proposed fixes don't solve my problem:

Attempting to build a nested form inside of a nested form with the Cocoon Gem, though the 3rd level child doesn't save to the database.

Pretty simple structure of the models, only "has_many / belongs_to" relationships:

A Text has many quotes. A Quote has many comments.

The dynamic UI interaction in the implementation works, adding and removing of fields work, unfortunately though only texts and quotes get saved, not the comments. No errors shown.

Here are the forms:

_text_form.html.erb

<%= form_for(@text, html: { multipart: true }) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_field :title, placeholder: "Enter Title" %>      
  </div>
  <div>
      <h3>Quotes:</h3>
      <div id="quotes">
        <%= f.fields_for :quotes do |quote| %>
          <%= render 'quote_fields', f: quote %>
        <% end %>

        <div class="links">
          <%= link_to_add_association 'add quote', f, :quotes, class: "btn btn-default" %>
        </div>
      </div>
  </div>

  <%= f.submit %>
<% end %>

_quote_fields.html.erb

<div class="nested-fields">
  <%= f.text_area :content, placeholder: "Compose new quote..." %>
  <span class="picture">
    <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
  </span>

  <div>
      <h3>Comments:</h3>
      <div id="comments">
        <%= f.fields_for :comments do |comment| %>
          <%= render 'comment_fields', f: comment %>
        <% end %>
        <div class="links">
          <%= link_to_add_association 'add comment', f, :comments, class: "btn btn-default" %>
        </div>
      </div>
  </div>
  <div class="links">
    <%= link_to_remove_association "remove quote", f, class: "btn btn-default" %>
  </div>
</div>

<script type="text/javascript">
  $('#quote_picture').bind('change', function() {
    var size_in_megabytes = this.files[0].size/1024/1024;
    if (size_in_megabytes > 2) {
      alert('Maximum file size is 2MB. Please choose a smaller file.');
    }
  });
</script>

_comment_fields.html.erb

<div class="nested-fields">
  <%= f.text_area :bodycomment, placeholder: "Write a comment..." %>
  <%= link_to_remove_association "remove comment", f, class: "btn btn-default" %>
</div>

Here are the models:

text.rb

class Text < ApplicationRecord
  belongs_to :user, inverse_of: :texts
  has_many :quotes, dependent: :destroy, inverse_of: :text
  has_many :comments, :through => :quotes
  accepts_nested_attributes_for :quotes, reject_if: :all_blank, allow_destroy: true
  accepts_nested_attributes_for :comments, reject_if: :all_blank, allow_destroy: true
  default_scope -> { order(created_at: :desc) }
  mount_uploader :coverimage, CoverimageUploader

  validates :user_id, presence: true
  validates :title, presence: true
  validate :coverimage_size

  private

      # Validates the size of an uploaded picture.
      def coverimage_size
        if coverimage.size > 5.megabytes
          errors.add(:coverimage, "should be less than 5MB")
        end
      end

end

quote.rb

class Quote < ApplicationRecord
  belongs_to :text, inverse_of: :quotes
  has_many :comments, dependent: :destroy, inverse_of: :quote
  accepts_nested_attributes_for :comments, reject_if: :all_blank, allow_destroy: true

  mount_uploader :picture, PictureUploader
  validates :content, presence: true, length: { maximum: 350 }
  validate :picture_size

private

  #Validates size of image upload
  def picture_size
    if picture.size > 2.megabytes
      errors.add(:picture, "should be less than 2MB")
    end
  end

end

comment.rb

class Comment < ApplicationRecord
  belongs_to :quote, inverse_of: :comments
  validates :quote_id, presence: true
  validates :bodycomment, presence: true

end

And here the controllers:

quotes_controller.rb

class QuotesController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
before_action :correct_user, only: :destroy


  def show
  end



  def create
    @quote = current_user.quotes.build(quote_params)
   if @quote.save
     flash[:success] = "Quote created!"
     redirect_to root_url
   else
     @feed_items = []
     render 'static_pages/home'
   end
  end

  def destroy
    @quote.destroy
    flash[:success] = "Quote deleted"
    redirect_to request.referrer || root_url
  end


  private

     def quote_params
       params.require(:quote).permit(:content, :picture, comments_attributes: [:id, :bodycomment
         , :_destroy])
     end

     def correct_user
       @quote = current_user.quotes.find_by(id: params[:id])
       redirect_to root_url if @quote.nil?

     end

end

comments_controller.rb

class CommentsController < ApplicationController
before_action :logged_in_user, only: [:create, :edit, :update, :destroy]
before_action :correct_user, only: :destroy


    def show
    end

    def create
        @comment = current_user.comments.build(comment_params)
        if @comment.save
          flash[:success] = "Comment created!"
          redirect_to root_url
        else
          @feed_items = []
          render 'static_pages/home'
        end
      end

    def destroy
      @comment.destroy
      flash[:success] = "Comment deleted"
      redirect_to request.referrer || root_url
    end

    private

    def comment_params
          params.require(:comment).permit(:bodycomment)
    end

    def correct_user
      @comment = current_user.comments.find_by(id: params[:id])
      redirect_to root_url if @comment.nil?

    end

end

Wondering if it is some javascript issue...

Thanks so much for looking into this. Really appreciated, and thanks for helping out.

EDIT

Here is the texts_controller.rb

class TextsController < ApplicationController
  before_action :logged_in_user, only: [:create, :edit, :update, :destroy]
  before_action :correct_user, only: :destroy
  before_action :find_text, only: [:show, :edit, :update, :destroy]


  def show
  end

  def new
    @text = current_user.texts.build
  end

  def create
    @text = current_user.texts.build(text_params)
    if @text.save
     flash[:success] = "Text created!"
     render 'show'
    else
     render 'static_pages/home'
    end
  end


  def edit
  end

  def update
    if @text.update(text_params)
      redirect_to root_url
    else
      render 'edit'
    end
  end

  def destroy
    @text.destroy
    flash[:success] = "Text deleted"
    redirect_to request.referrer || root_url
  end

  private

    def text_params
    params.require(:text).permit(:url, :title, :coverimage,
                                  :publication, :author, :summary, quotes_attributes: [:id, :content, :picture, :_destroy], comments_attributes: [:id, :bodycomment, :_destroy])
    end

    def find_text
        @text = Text.find(params[:id])
      end

    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end

end

Here are some log infos that I get after saving the form field:

--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  utf8: "✓"
  authenticity_token: NQLh7TwlhfbV4ez91HGMyYZK6YYYiLXhHG/cAhrAsRylIAuFFhjnKX0vEO8ZIVbsxGES3byBgUMz21aSOlGiqw==
  text: !ruby/object:ActionController::Parameters
    parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
      title: Title of the Book
      quotes_attributes: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
        '1490626822148': !ruby/hash:ActiveSupport::HashWithIndifferentAccess
          content: This is a quote from the book.
          _destroy: 'false'
          comments_attributes: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
            '1490626833771': !ruby/hash:ActiveSupport::HashWithIndifferentAccess
              bodycomment: Here is a comment on the quote of the book.
              _destroy: 'false'
    permitted: false
  commit: Create Text
  controller: texts
  action: create
permitted: false 

Here the log file form the terminal:

Started POST "/texts" for ::1 at 2017-03-27 17:00:51 +0200
Processing by TextsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"NQLh7TwlhfbV4ez91HGMyYZK6YYYiLXhHG/cAhrAsRylIAuFFhjnKX0vEO8ZIVbsxGES3byBgUMz21aSOlGiqw==", "text"=>{"title"=>"Title of the Book", "publication"=>"", "author"=>"", "url"=>"", "summary"=>"", "quotes_attributes"=>{"1490626822148"=>{"content"=>"This is a quote from the book.", "_destroy"=>"false", "comments_attributes"=>{"1490626833771"=>{"bodycomment"=>"Here is a comment on the quote of the book.", "_destroy"=>"false"}}}}}, "commit"=>"Create Text"}
  User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
Unpermitted parameter: comments_attributes
   (0.1ms)  begin transaction
  SQL (0.7ms)  INSERT INTO "texts" ("user_id", "url", "title", "publication", "author", "summary", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?)  [["user_id", 1], ["url", ""], ["title", "Title of the Book"], ["created_at", 2017-03-27 15:00:51 UTC], ["updated_at", 2017-03-27 15:00:51 UTC]]
  SQL (0.4ms)  INSERT INTO "quotes" ("content", "created_at", "updated_at", "text_id") VALUES (?, ?, ?, ?)  [["content", "This is a quote from the book."], ["created_at", 2017-03-27 15:00:51 UTC], ["updated_at", 2017-03-27 15:00:51 UTC], ["text_id", 366]]
   (1.3ms)  commit transaction
  Rendering texts/show.html.erb within layouts/application
  Quote Load (0.2ms)  SELECT "quotes".* FROM "quotes" WHERE "quotes"."text_id" = ?  [["text_id", 366]]
  Rendered texts/show.html.erb within layouts/application (5.8ms)
  Rendered layouts/_shim.html.erb (0.5ms)
  Rendered layouts/_header.html.erb (1.4ms)
  Rendered layouts/_footer.html.erb (1.8ms)
Completed 200 OK in 127ms (Views: 100.1ms | ActiveRecord: 3.1ms)

Solution

    1. Models ... text.rb ... I had to put in accepts_nested_attributes_for :comments ... in addition to the already posted one
    2. TextController ... your nested permitted params needs to happen here, .permit(:content, :picture, quotes_attributes: [:id, :content, :picture, :_destroy, comments_attributes: [:id, :bodycomment, :_destroy]])

    I might have the variable names off (especially the TextController which you didn't list) a bit, but basically, it looks like your are trying to inherit the nesting via the other controllers - when the only controller being called is the TextController.

    To be sure, drop a tail -f log/<logname> in a 2nd console or view the terminal console when you execute the save/update to watch for permit issues.

    Let me know if there's still issues!

    ...update based on new controller

    Controller needs to be adjusted (you have one misplaced ']' right now).

    params.require(:text).permit(:url, :title, :coverimage,
                                      :publication, :author, :summary, quotes_attributes: [:id, :content, :picture, :_destroy, comments_attributes: [:id, :bodycomment, :_destroy]])
    

    That won't work till you fix the model too ...

    accepts_nested_attributes_for :comments