简体   繁体   中英

In Rails How to display errors in my comment form after I submit it?

I have a very straight-forward task to fulfil --- just to be able to write comments under posts and if the comments fail validation display error messages on the page.

My comment model uses a gem called Acts_as_commentable_with_threading, which creates a comment model after I installed.

On my post page, the logic goes like this:

Posts#show => display post and a form to enter comments => after the comment is entered, redisplay the Post#show page which has the new comment if it passes validation, otherwise display the error messages above the form.

However with my current code I can't display error messages if the comment validation fails. I think it is because when I redisplay the page it builds a new comment so the old one was erased. But I don't know how to make it work.

My codes are like this:

Comment.rb:

class Comment < ActiveRecord::Base
  include Humanizer
  require_human_on :create

  acts_as_nested_set :scope => [:commentable_id, :commentable_type]

  validates :body, :presence => true
  validates :first_name, :presence => true
  validates :last_name, :presence => true

  # NOTE: install the acts_as_votable plugin if you
  # want user to vote on the quality of comments.
  #acts_as_votable

  belongs_to :commentable, :polymorphic => true

  # NOTE: Comments belong to a user
  belongs_to :user

  # Helper class method that allows you to build a comment
  # by passing a commentable object, a user (could be nil), and comment text
  # example in readme
  def self.build_from(obj, user_id, comment, first_name, last_name)
     new \
        :commentable => obj,
        :body        => comment,
        :user_id     => user_id,
        :first_name  => first_name,
        :last_name   => last_name
  end
end

PostController.rb:

class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]

  def show
      @post = Post.friendly.find(params[:id])
      @new_comment = Comment.build_from(@post, nil, "", "", "")
  end
end

CommentsController:

class CommentsController < ApplicationController

def create
    @comment = build_comment(comment_params)
    respond_to do |format|
        if @comment.save
            make_child_comment
            format.html
            format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
        else
            format.html
            format.json { redirect_to(:back, :flash => {:error => @comment.errors}) }
        end
    end
end

private
    def comment_params
        params.require(:comment).permit(:user, :first_name, :last_name, :body, :commentable_id, :commentable_type, :comment_id,
            :humanizer_answer, :humanizer_question_id)
    end

    def commentable_type
        comment_params[:commentable_type]
    end

    def commentable_id
        comment_params[:commentable_id]
    end

    def comment_id
        comment_params[:comment_id]
    end

    def body
        comment_params[:body]
    end

    def make_child_comment
        return "" if comment_id.blank?

        parent_comment = Comment.find comment_id
        @comment.move_to_child_of(parent_comment)
    end

    def build_comment(comment_params)
        if current_user.nil?
            user_id = nil 
            first_name =  comment_params[:first_name]
            last_name = comment_params[:last_name]
        else
            user_id = current_user.id
            first_name = current_user.first_name
            last_name = current_user.last_name
        end
        commentable = commentable_type.constantize.find(commentable_id)
        Comment.build_from(commentable, user_id, comment_params[:body],
                    first_name, last_name)
    end
end

comments/form: (this is on the Posts#show page)

<%= form_for @new_comment do |f| %>  
  <% if @new_comment.errors.any? %>
    <div id="errors">
        <h2><%= pluralize(@new_comment.errors.count, "error") %> encountered, please check your input.</h2>

        <ul>
            <% @new_comment.errors.full_messages.each do |msg| %>
                <li><%= msg %></li>
            <% end %>
        </ul>
    </div>
  <% end %>
<% end %>

I'm assuming your form_for submits a POST request which triggers the HTML format in CommentsController#create :

def create
    @comment = build_comment(comment_params)
    respond_to do |format|
        if @comment.save
            make_child_comment
            format.html
            format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
        else
            format.html
            format.json { redirect_to(:back, :flash => {:error => @comment.errors}) }
        end
    end
end

So, if @comment.save fails, and this is an HTML request, the #create method renders create.html . I think you want to render Posts#show instead.

Keep in mind that if validations fail on an object (Either by calling save / create , or validate / valid? ), the @comment object will be populated with errors. In other words calling @comment.errors returns the relevant errors if validation fails. This is how your form is able to display the errors in @new_comment.errors.

For consistency, you'll need to rename @new_comment as @comment in the posts#show action, otherwise you'll get a NoMethodError on Nil::NilClass.

TL;DR: You're not rendering your form again with your failed @comment object if creation of that comment fails. Rename to @comment in posts, and render controller: :posts, action: :show if @comment.save fails from CommentsController#create

I would instead use nested routes to create a more restful and less tangled setup:

concerns :commentable do
  resources :comments, only: [:create]
end

resources :posts, concerns: :commentable

This will give you a route POST /posts/1/comments to create a comment.

In your controller the first thing you want to do is figure out what the parent of the comment is:

class CommentsController < ApplicationController
  before_action :set_commentable

  private
    def set_commentable
      if params[:post_id]
        @commentable = Post.find(params[:post_id])
      end
    end
end

This means that we no longer need to pass the commentable as form parameters. Its also eliminates this unsafe construct:

commentable = commentable_type.constantize.find(commentable_id)

Where a malicous user could potentially pass any class name as commentable_type and you would let them find it in the DB... Never trust user input to the point where you use it to execute any kind of code!

With that we can start building our create action:

class CommentsController < ApplicationController
  before_action :set_commentable

  def create
    @comment = @commentable.comments.new(comment_params) do |comment|
      if current_user
       comment.user = current_user
       comment.first_name =  current_user.first_name
       comment.last_name = current_user.last_name
      end
    end

    if @comment.save
      respond_to do |format|
        format.json { head :created, location: @comment }
        format.html { redirect_to @commentable, success: 'Comment created' }
      end
    else
      respond_to do |format|
        format.html { render :new }
        format.json { render json: @comment.errors, status: 422 }
      end
    end
  end


  private
  # ...

    def comment_params
      params.require(:comment).permit(:first_name, :last_name, :body, :humanizer_answer, :humanizer_question_id)
    end
end

In Rails when the user submits a form you do not redirect the user back to the form - instead you re-render the form and send it as a response.

While you could have your CommentsController render the show view of whatever the commentable is it will be quite brittle and may not even provide a good user experience since the user will see the top of the post they where commenting. Instead we would render app/views/comments/new.html.erb which should just contain the form.

Also pay attention to how we are responding. You should generally avoid using redirect_to :back since it relies on the client sending the HTTP_REFERRER header with the request. Many clients do not send this!

Instead use redirect_to @commentable or whatever resource you are creating.

In your original code you have totally mixed up JSON and HTML responses. When responding with JSON you do not redirect or send flash messages.

If a JSON POST request is successful you would either:

  • Respond with HTTP 201 - CREATED and a location header which contains the url to the newly created resource. This is preferred when using SPA's like Ember or Angular.
  • Respond with HTTP 200 - OK and the resource as JSON in the response body. This is often done in legacy API's.

If it fails do to validations you should respond with 422 - Unprocessable Entity - usually the errors are rendered as JSON in the response body as well.

Added.

You can scrap your Comment.build_from method as well which does you no good at all and is very idiosyncratic Ruby.

class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]

  def show
      @post = Post.friendly.find(params[:id])
      @new_comment = @post.comments.new
  end
end

Don't use line contiuation ( \\ ) syntax like that - use parens.

Don't:

new \
        :commentable => obj,
        :body        => comment,
        :user_id     => user_id,
        :first_name  => first_name,
        :last_name   => last_name

Do:

new(
 foo: a,
 bar: b
)

Added 2

When using form_for with nested resources you pass it like this:

<%= form_for([commentable, comment]) do |f| %>

<% end %>

This will create the correct url for the action attribute and bind the form to the comment object. This uses locals to make it resuable so you would render the partial like so:

I have figured out the answer myself with the help of others here.

The reason is that I messed up with the JSON format and html format (typical noobie error)

To be able to display the errors using the code I need to change two places ( and change @comment to @new_comment as per @Anthony's advice).

1.

routes.rb:

resources :comments, defaults: { format: 'html' } # I set it as 'json' before

2.

CommentsController.rb:

def create
    @new_comment = build_comment(comment_params)
    respond_to do |format|
        if @new_comment.save
            make_child_comment
            format.html { redirect_to(:back, :notice => 'Comment was successfully added.') }
        else
            commentable = commentable_type.constantize.find(commentable_id)
            format.html { render template: 'posts/show', locals: {:@post => commentable} }
            format.json { render json: @new_comment.errors }
        end
    end
end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM