Final result example: polymorphic nested commits

Note: This is a clean CRUD approach.

No responsive behaviours (they can be added with TURBO in the future).

Prerequisites:

  • Rails 7 (button_to delete, render :new, status: :unprocessable_entity)
  • Devise for Users
  • Flash messages rendered via app/views/shared/_flash
  • An Inboxes model (to which we will connect polymorphic comments)

1 A migration for comments #

# console
rails generate model Comment user:references body:text commentable:references{polymorphic} deleted_at:datetime:index

2 The Comment model. #

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :commentable, polymorphic: true, inverse_of: :comments
  has_many :comments, as: :commentable, dependent: :destroy

  MIN_BODY_LENGTH = 2
  MAX_BODY_LENGTH = 1000

  validates :body, presence: true
  validates :body, length: { minimum: MIN_BODY_LENGTH, maximum: MAX_BODY_LENGTH }

  # soft delete
  def destroy
    update(deleted_at: Time.zone.now)
  end

  def find_top_parent
    return commentable unless commentable.is_a?(Comment)

    commentable.find_top_parent
  end
end

3 Inbox - Comment relationships #

# app/models/inbox.rb
  has_many :comments, -> { order(created_at: :desc) }, as: :commentable, dependent: :destroy, inverse_of: :commentable

4 We will have 3x comments_controller #

# config/routes.rb
  resources :comments, only: [] do
    resources :comments, only: %i[new create destroy], module: :comments
  end

  resources :inboxes do
    resources :comments, only: %i[new create destroy], module: :inboxes
  end
  • 1 general comments controller with all the logic
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_commentable

  def new
    @comment = Comment.new
  end

  def create
    @comment = @commentable.comments.build(comment_params)
    if @comment.save
      redirect_to @commentable unless @commentable.is_a?(Comment)
      redirect_to @commentable.find_top_parent if @commentable.is_a?(Comment)
      flash[:notice] = 'Comment created'
    else
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    @comment = Comment.find(params[:id])
    if @comment.destroy
      redirect_to @commentable unless @commentable.is_a?(Comment)
      redirect_to @commentable.find_top_parent if @commentable.is_a?(Comment)
      flash[:notice] = 'Comment deleted'
    else
      redirect_to @commentable, alert: 'Something went wrong'
    end
  end

  private

  # not very nice, in my opinion
  # def set_commentable
  #   if params[:inbox_id].present?
  #     @commentable = Inbox.find(params[:inbox_id])
  #   elsif params[:comment_id]
  #     @commentable = Comment.find(params[:comment_id])
  #   else
  #     "SOME ERROR"
  #   end
  # end

  def comment_params
    params.require(:comment).permit(:body).merge(user: current_user)
  end
end
  • 2 controllers designed only to find a comment parent. No chance for If-Else mess.
# controllers/inboxes/comments_controller.rb
module Inboxes
  class CommentsController < CommentsController
    private

    def set_commentable
      @commentable = Inbox.find(params[:inbox_id])
    end
  end
end
# app/controllers/comments/comments_controller.rb
module Comments
  class CommentsController < CommentsController
    private

    def set_commentable
      @commentable = Comment.find(params[:comment_id])
    end
  end
end

5 Comment views #

-> NEW

# app/views/comments/new.html.erb
New comment for
<%= link_to @commentable.name, @commentable unless @commentable.is_a?(Comment) %>
<%= link_to @commentable.find_top_parent.name, @commentable.find_top_parent if @commentable.is_a?(Comment) %>
<%= render 'comments/form', comment: @comment %>

-> FORM

# app/views/comments/_form.html.erb
<%= form_with(model: [@commentable, comment]) do |form| %>
  <%= render 'shared/errors', form: form %>

  <div class="field">
    <%= form.text_area :body,
      style: "width: 100%",
      maxlength: Comment::MAX_BODY_LENGTH, 
      placeholder: 'Add a comment here' %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

-> SHOW

# app/views/comments/_comment.html.erb
<div class='comment'>
  <% if comment.deleted_at.present? %>
    <i>Comment has been deleted</i>
  <% else %>
    <%= comment.created_at %>
    by
    <%= link_to comment.user.name, comment.user %>
    <%= simple_format(comment.body) %>
    <%= button_to 'Delete', [@commentable, comment], method: :delete %>
  <% end %>
  comments:
  <%= comment.comments.count %>
  <%= link_to 'Reply', new_comment_comment_path(comment) %>
  <br>
  <%= render comment.comments %>
</div>

6 Render comment form and list in an inbox view #

# app/controllers/inboxes_controller.rb
def show
  @commentable = @inbox
  @comment = Comment.new
  @comments = @inbox.comments
end
# app/views/inboxes/show.html.erb
<%= render template: 'comments/new' %>
<%= render @comments %>

7 Add some css #

app/assets/stylesheets/application.css

.comment {
  margin: 1em 0em 1em 1em;
  padding-left: 1em;
  border-left: 2px solid lightgray;
}