0%

留言及回覆功能-self-join

在設計model的時候,會發現有時候model需要跟自己有關連,這次要紀錄的留言及回覆功能就是一個很好的例子,回覆基本上也是留言的一種,我們可以運用self-join這個方法,建立它們之間的關聯。

註:以下範例是基於募資網站的題目,功能設計是需要在募資專案的show頁面當中,user可以留言而提案者可以針對每則留言個別回覆,以下我就針對實作的內容分成conmment跟reply兩個部分跟大家分享。

Comment

  1. 建立comment model,parent_id是為了comment和reply之間的關聯設定(別忘了rails db:migrate)

    1
    rails g model Comment user:references project:references parent_id:integer content:text
  2. 回到project和user設定has many comment,另外也限定留言不得空白

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # app/models/project.rb
    has_many :comments

    # app/models/user.rb
    has_many :comments

    # app/models/comments.rb
    belongs_to :project
    belongs_to :user
    validates :content, presence :true
  3. 設定comment的路徑

    1
    2
    3
    resources :projects, shallow: true do
    resources :comments, shallow: true, only: [:new, :create, :destroy]
    end

    這邊我只需要三個路徑,並且destroy只需要comment的id就可以抓到它進行刪除,所以使用shallow: true可以遮蔽掉前面的project,避免太冗長的網址 (完整說明可以參考官方文件https://guides.rubyonrails.org/routing.html#shallow-nesting

  4. 在project的show頁面放入comment的form

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # view/projects/show.html.erb
    <% if user_signed_in? %>
    <%= form_with model: Comment.new, url: project_comments_path(@project) do |f| %>
    <div>
    <%= f.text_area :content, placeholder: '請留言' %>
    </div>
    <%= f.submit '送出' %>
    <% end %>
    <% end %>
  5. 建立comment的controller,然後新增一個create的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # app/controllers/comments_controller.rb
    def create
    @project = Project.find(params[:project_id])
    @comment = current_user.comments.new(comment_params)
    @comment.project = @project
    if @comment.save
    redirect_to project_path(@project)
    end
    end

    private
    def comment_params
    params.require(:comment).permit(:content, :parent_id)
    end

    這時候如果我們在頁面上新增留言就可以順利create,但除非我們去看log紀錄,否則在畫面上還看不出來,因為我們根本還沒把它渲染出來。

  6. 在project的show頁面把comment渲染出來。

    1
    <%= render @project.comments, comment: @comment %>
    1
    2
    3
    4
    5
    6
    #views/comments/_comment.html.erb
    <div>
    [<%= comment.user.name %>]
    <%= comment.created_at.strftime("%Y/%m/%d") %>
    <%= comment.content %>
    </div>

    如果覺得comment建立時間的預設格式太冗長,可以用strftime改成自己喜歡的格式。https://apidock.com/ruby/DateTime/strftime

Reply

我們並不會建立reply的model,而是透過self-join的方法利用我們一開始在comment的model裡面建立的parent_id欄位來關聯他們,基本上reply就是comment。

https://guides.rubyonrails.org/association_basics.html#self-joins

  1. 首先我們先把comment跟reply的關係設定好

    1
    2
    3
    4
    5
    6
    7
    class Comment < ApplicationRecord
    belongs_to :project
    belongs_to :user
    has_many :replies, class_name: 'Comment', foreign_key: :parent_id, dependent: :destroy

    validates :content, presence: :true
    end
  2. 接下來就讓我們來建立reply的form吧,如果comment有任何reply就把它們印出來。這邊使用remote:true代表我們要用ajax來執行非同步處理。

    1
    2
    3
    4
    5
    6
    7
    8
    #views/comments/_comment.html.erb
    <%= link_to '回覆', new_project_comment_path(@project, parent_id: comment.id), remote: true %>

    <% if comment.replies.any? %>
    <% comment.replies.each do |reply| %>
    <%= render 'comments/reply', reply: reply %>
    <% end %>
    <% end %>
  3. 建立_reply頁面

    1
    2
    3
    4
    5
    6
    #/comments/_reply.html.erb
    <div class='ml-5'>
    [<%= reply.user.name %>]
    <%= reply.created_at.strftime("%Y/%m/%d") %>
    <%= reply.content %>
    </div>
  4. 建立reply的form,這邊也放了一個parent_id的隱藏欄位,透過comment的new action來把它填入表單

    1
    2
    3
    4
    5
    6
    7
    8
    <%= form_with model: [@project, @comment] do |f| %>
    <%= f.hidden_field :parent_id %>
    <div>
    <%= f.text_area :content, placeholder: '請回覆', class:'border-2' %>
    </div>
    <%= f.submit '送出回覆' %>
    <%= link_to '取消', project_path(@project) %>
    <% end %>
  5. 生成一個new方法,填入parent_id到隱藏欄位裡面

    1
    2
    3
    4
    5
    #comments_controller.eb
    def new
    @project = Project.find(params[:project_id])
    @comment = current_user.comments.new(parent_id: params[:parent_id])
    end
  6. 接下來我們要使用ajax方式生成回覆的資料,先建一個空的div容器,待會塞資料給它。這邊的動態id是為了後續能用JS抓到指定的那一個form。

    1
    2
    #app/views/_comment.html.erb
    <div id="reply-form-<%= comment.id %>"></div>
  7. 新建一個new.js.erb的檔案,當點擊新增回覆的按鈕時會找到它,把reply_form的資料渲染在這個位置。
    (j的用法:https://api.rubyonrails.org/classes/ActionView/Helpers/JavaScriptHelper.html#method-i-escape_javascript)

    1
    2
    #comments/new.js.erb
    document.querySelector("#reply-form-<%= @comment.parent_id %>").innerHTML = ("<%= j render 'reply_form', comment: @comment %>")
  8. 最後填完回覆內容按下送出,就可以把這筆資料存進資料庫了。