if it does't challenge you, it can't change you

RubyのWebエンジニアやってます。主にRuby on Railsのことで勉強になったこと書いていきます。

RailsでAjaxを使ってコメントの編集方法

インスタのクローンアプリを作っていて、コメント部分をajaxで追加・編集・削除してみたので、その時に勉強になったコメントをajaxを使って更新する方法を書いておきます。
※この記事は最初2017年の12月に書いていたのですが、今見直すとあまりにも何を書いているのか全くわからなかったので書き直しました。

バージョン

ruby 2.3.0
rails 5.1.4

Ajax処理概要

  1. リクエストをjsで送る。 = ajax処理をする。
    ビューでform_withヘルパーを使っているのであれば、local: trueを外す。明示的にremote: trueをつけてもok。
    link_toやbutton_toであれば、remote: trueをつける。

  2. ルーティングはajaxを特に意識することなく、いつも通り書いてok

  3. コントローラではいつも通り、createやdeleteしたりしてok。ただし、そのアクションがjsリクエストだけではなく、htmlリクエストも受け取るのであれば、response_toメソッドを使って振り分ける。

  4. アクション名.js.erb(または.js.haml / .js.coffeeなど)テンプレートファイルを用意し、そのテンプレートファイルにjsでの処理を記載する。

  5. 通常のajaxでのjs側の動きは、「要素を取得 → そのhtmlを書き換える 」ということをする。なので、その差し替えるパーシャルも用意する。

実装

1. リクエストをjsで送る。

今回、このコメントの編集と削除をjqueryajaxを使って実装していく。

f:id:shoheimoment:20171209154022p:plain

コメントを編集するためにまず、現在の特定のコメント部分だけを編集フォームに変えます。
今回必要なテンプレートファイル部分

<!-- index.html.erb -->
<div class="other_comments">
  <% if picture.other_comments.present? %>
    <% all_other_comments = picture.other_comments.order(created_at: :asc) %>
    <% all_other_comments.each do |each_other_comment| %>
      <div class="for_edit_comment">
        <%= render 'other_comments/each_other_comment', each_other_comment: each_other_comment %>
      </div>
    <% end %>
  <% end %>

render部分

<!-- view/other_coments/_each_other_comment.html.erb -->
<div class="each_other_comment row" data-comment="<%= each_other_comment.id %>">
  <p class="col-xs-10"><strong><%= each_other_comment.user.name %></strong>&nbsp;&nbsp;<span id="for_edit"><%= each_other_comment.other_comment %></span></p>
  <div class="col-xs-2">
    <div class="dropdown">
      <button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
      <span class="caret"></span>
      </button>
      <ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
      <li><%= link_to '編集', edit_other_comment_path(each_other_comment.id), remote: true %></li>
      <li><%= link_to '削除', other_comment_path(each_other_comment.id),
      remote: true, data: { confirm: '削除しますか?' },
      method: :delete %>
      </li>
      </ul>
    </div>
  </div>
</div>

コメント部分にマウスオーバーすると編集削除ボタンがでる。

f:id:shoheimoment:20171209161705p:plain

大事なところは編集の部分。
編集ボタンを押すと、編集できるように inputフォームに変えたいのですが、 このボタンのlink_toにremote: trueを仕込む。

<!-- _each_other_comment.html.erb -->
<li><%= link_to '編集', edit_other_comment_path(each_other_comment.id), remote: true %></li>

これでjsでリクエストが送られる。
※フォームに直すのはjqueryだけでできるが、要素の取得がajaxでやれば簡単だったから今回はajaxで行う。

2. ルーティング

ルーティングは通常どおりでok。今回ならこんな感じ。

resources :other_comments, only: [:edit, :create, :update, :destroy]

3. コントローラ

編集ボタンを押すとother_commentコントローラーのeditアクショへ行く。

# other_comments_controller.rb
def edit
  @id_comment = OtherComment.find(params[:id]).id
end

ajaxはjsでリクエストが送られる。
railsさんはそこは自動で判断してくれて、 何も指定しないでも、jsでリクエストがくれば、jsのテンプレートファイルを探してくれる。
今回であればview/other_comments/edit.js.erbが呼ばれることになる。
ただし、そのアクションがjsリクエストだけではなく、htmlリクエストも受け取るのであれば、response_toメソッドを使って振り分ける。

4. テンプレートファイル

今回のようにコントローラで何も指定しなければ、テンプレートファイルは指定のフォルダのアクション名がついたjsのファイルを自動で読み込む。
この.js.erbというのは本当に便利で、jsでrubyが使える。
とりあえず今回はこんな感じで書いた。

// views/other_comments/edit.js.erb
$('.row[data-comment="'+ @id_comment +'"]').parent().html("<%= escape_javascript(render 'edit_other_comment') %>");

5. jsでhtmlを置き換える

あまりjsを勉強していなければこのjs文もよくわからないと思う。
jsの処理に付いてはわからないメソッドはググってもらえればすぐに出て来るはず。
ちょっとわかりにくいのはこの部分かな。
$('.row[data-comment="'+ @id_comment +'"]') これも大したことではなく、ただ、文字列を結合しているだけ。

大事なのはこっちです。
data-comment
テンプレートファイルでは他の人のコメントはeach文繰り返し処理を行っているので、classやidだけで要素を取得するのが難しい。そこであらかじめdivタグにdata-commentと名付けたdata-attributesプロパティをつけて、値にそのレコードのid値を持たせておく。
この部分

<!-- view/other_coments/_each_other_comment.html.erb -->
<div class="each_other_comment row" data-comment="<%= each_other_comment.id %>">

それを元に取得している。
これがajaxでこの処理を行った理由。

なので、要はedit.js.erbでは取得した要素の一つ上の親要素(div class='for_edit_comment')を取得してhtmlをrender先のパーシャル_edit_other_comment.html.erbに書き換えなさいという意味。
※ここでparent()メソッドを使っているのは、html()メソッドは取得した要素の子孫要素を書き換えるメソッドであり、その要素自体を取得して書き換えると、コメントを編集する度に取得した要素が加えられていってしまい、cssがおかしくなったりするから。

次にedit.js.erbからhtmlを置き換えるためにrenderされるパーシャルを用意。

<!-- views/other_comments/_edit_other_comment.js.erb -->
<div class="each_other_comment row" data-comment="<%= @id_comment %>">
  <%= form_with(model: @other_comment, remote: true) do |form| %>
    <div class="add_other_comment row">
      <%= form.text_area :other_comment, rows: 3, placeholder: "コメントを追加...", class: "col-xs-11" %>
      <%= form.submit '投稿する', data: { confirm: '投稿しますか?' }, class: 'btn btn-default' %>
    </div>
  <% end %>
</div>

これで編集ボタンを押すとフォームに切り替わる。 f:id:shoheimoment:20171209164103p:plain

コメントを更新する

ここからコメントを更新する作業であるが、ajaxの処理はやり方はeditの時と同じ。

<!-- _edit_other_comment.html.erb -->
<%= form_with(model: @other_comment, remote: true) do |form| %>

この部分のremote: trueでajax化している。先ほどと同様にコントローラーで処理した後update.js.erbが呼ばれる。

# other_comments_controller.rb
def update
  @other_comment = OtherComment.find(params[:id])
  if @other_comment.update(other_comment: params[:other_comment][:other_comment])
    @id_comment = @other_comment.id
  else
    @status = 'fail'
  end
end
// views/other_comments/update.js.erb
// @statusを使ってエラー処理
$('.row[data-comment="'+idComment+'"]').parent().html("<%= escape_javascript(render 'each_other_comment', each_other_comment: @other_comment) %>");

編集の時と同様に、取得した要素のhtmlを書き換えている。 render先は上の方で書いているview/other_coments/_each_other_comment.html.erbである。 これで投稿ボタンを押すとajaxで更新される。

f:id:shoheimoment:20171209170119p:plain

学んだこと

・基本的なajaxの流れ。
data-attributesプロパティを使うと細かい操作ができる。
ajaxは初めは難しいと感じるかもしれませんが、慣れると、jsでajax処理するよりかなり簡単にわかりやすくできると思います。
行いたい処理により向き不向きがありますが、特に今回のように特定のidを指定して置き換えるような処理はjsでajax処理を書くのではなく、ビューヘルパーにremote: trueをつけるだけなので、楽に実装できます。
そして、何と言っても、jsファイルがassets配下ではなく、views配下で管理できるのがいいです。
assets配下のファイルたちはプリコンパイルのことなど考えることが増えてしまいますもんね。