コメント機能の非同期通信 js.hamlバージョン


#what
create.js.hamlなどのアクション名のjs.hamlファイルを利用して、
コメント投稿機能を非同期通信にします！

#初めに
自分は某スクールに通っていて、その時にjbuilderとjsファイルを使って
コメント投稿の非同期化を行いました。
ですが、そのやり方だとコメントの投稿はできても編集や削除を行うのが非常に難しいのと、
コードが非常に冗長になるので短くしたい。
なので、別のやり方で実装しようと考えて実行しました。

#注意事項
初学者のコメント機能を実装した時の備忘録として書いてます。
長々と解説してますが、まずはコピペして実践してみるのをおすすめします。
筆者が言語化して理解するためにこの記事を書いてます。
ご指摘がありましたら何なりとお申し付け下さい。

#この記事を読むことで得られるメリット
コメント投稿機能をjubilder形式で冗長なコードを書かずに、
可読性が高く短いコードで簡単に非同期通信が実装できる。

#前提条件
userテーブル、tweetテーブルは作成済みとして進みます。

#全体の流れ
1，モデルを作成、マイグレーションファイルを編集
2，各テーブルとアソシエーションを組む
3，tweetsとcommentsコントローラーの編集
4，コメントのviewファイルの作成
5，js.hamlファイルを作成

#今回実装する機能
投稿、編集、削除機能の3つです。

まずは、commentテーブルを作成します
#
```ruby:ターミナル
rails g model comment
```

マイグレーションファイルを編集します。
#
```
class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.integer :user_id, null: false
      t.integer :tweet_id, null: false
      t.text :content, null: false
      t.timestamps
    end
  end
end
```
終わったらrails db:migrateする。

🔳解説
user_idとtweet_idに外部キー制約をかけていないのは、
該当するtweet（例えばtweet_idが1のtweet)が削除された時に、
commentテーブルのtweet_idが保てなくなり
削除ができないエラーが発生するため。

言い換えると、
tweet_idに外部キーをかけちゃうと、もしtweetが削除された時に
commentテーブル君が
「そのtweet_idは俺の情報を維持するために必要なんじゃ！消さないでくれ！」 と、言われてtweetの削除をしようとするとエラーが発生して削除できなくなるから。
（実際にはずらーっとMySQLエラーが出ます）
難しく言うと、情報の整合性が保てなくなるから。 って言う認識です。

user_idは消えることがない（予定）なので、外部キー制約かけてもいいと思うけど、
一応なしで。問題ないと思うので 
次は各モデルの編集

コメントモデルのカラム
#
```ruby:comment.rb
  belongs_to :tweet
  belongs_to :user

  validates :content, presence: true
```

tweetテーブルとuserテーブルにアソシエーションを組みます。
commentテーブルの1つのidごとににtweet_idとuser_idは1つしかないので、
belongs_toになります。

#
```ruby:tweet.rb
  belongs_to :user
  has_many :comments, dependent: :destroy
```
  has_many :commentsにdependent: :destroyを追加してる理由は、
tweetが削除された時にcommentの内容も削除して欲しいからです。

#
```ruby:user.rb
  has_many :tweets
  has_many :comments
```


次にcommentsコントローラーを実装していきます。

commentsコントローラーを作成
#
```ruby:ターミナル
rails g controller comments
```

commentsコントローラーを編集
#
```ruby:comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_tweet, only: [:create, :edit, :update, :destroy]
  before_action :set_comment, only: [:edit, :update, :destroy]

  def create
    @comment = @tweet.comments.create(comment_params)
    if @comment.save
        @comment = Comment.new
        get_all_comments
    end
  end

  def edit
  end

  def update
    if @comment.update(comment_params)
        get_all_comments
    end
  end


  def destroy
    if @comment.destroy
        get_all_comments
    end
  end

  private

  def set_tweet
      @tweet = Tweet.find(params[:tweet_id])
  end

  def set_comment
      @comment = Comment.find(params[:id])
  end

  def get_all_comments
    @comments = @tweet.comments.includes(:user).order('created_at asc')
  end

  def comment_params
      params.require(:comment).permit(:content).merge(user_id: current_user.id)
  end

end
```
上から順番に解説してきます。

#
```
  before_action :set_tweet, only: [:create, :edit, :update, :destroy]
  before_action :set_comment, only: [:edit, :update, :destroy]

private
 def set_tweet
      @tweet = Tweet.find(params[:tweet_id])
  end

  def set_comment
      @comment = Comment.find(params[:id])
  end
```
before_actionにset_tweetメソッドとset_commentメソッドを設定しているのは、
各アクションが発火した時に、
現在操作している@tweetの情報と@commentの情報が必要だからです。
createだけはset_coomentメソッドは設定してません。
理由は新しい@commentを作成している情報が代入されているからです。

例えば、editアクションが発火した時にどのtweetに紐付いた（ネストされた）commentか
情報が必要ですよね。他のアクションも同じ理由です。

#
```
    def create
    @comment = @tweet.comments.create(comment_params)
    if @comment.save
        @comment = Comment.new
        get_all_comments
    end
  end
```

#
```
    @comment = @tweet.comments.create(comment_params)
```
2行目でこのように記述している理由は、どの@tweetに紐付いた（ネストされた）コメントかを分かりようにするため。だと思ってます。
createよりbuildの方が適切かも？
comment_paramsメソッドは読んで字の通りなので省略します。

もし、コメント投稿が成功したら、新しいコメントとして代入されます。
#
```
def get_all_comments
    @comments = @tweet.comments.includes(:user).order('created_at asc')
  end
```
次にget_all_commentsメソッドが発火されます。
これは、投稿が終わった時に全てのコメントの内容を取得する必要があるためです。

🔳updateとdestroyにif文がある理由
もし、コメントが更新（編集、削除）された時に限定しないと、
後述するjs.hamlファイルの関係でget_all_commentsが正しく動作しないから。
（詳しく言うと、js.hamlファイルでcommentのviewをrenderして読み込むのですが、その時に読み込むことができない。）
と、考えてます。

次はtweets_controllerのshowアクションを編集します。
#
```
  def show
    @tweet = Tweet.find(params[:id])
    @comment = Comment.new
    @comments = @tweet.comments.includes(:user).order('created_at asc')
  end
```
@tweetでクリックしたtweet_idを取得する。
@comment = Comment.newで新しいcommentを代入する
コメントを新規投稿できるようにする。
これがないと当たり前ですが新しいコメント投稿できません。
変数が定義されてないから。
@commentsで該当する全てのcommentを取得する。N+1問題解消のためにincludes

次はビューを編集します。
コメント表示部分部分のみ抜粋してます。
#
```ruby:tweets/show.html.haml
= render "comments/index"
```
renderしてcommentsページに飛ばしてます。

#
```ruby:comments/index.html.haml
.div#comment_form
  = render "comments/form"
.div#comment_area
  = render "comments/comment"
```
ここで投稿フォームとコメント一覧にrenderして分かれさせてます。
フォームを分ける理由はjs.hamlファイルのrenderする指定先の関係です。

フォームの内容です
#
```ruby:comments/_form.html.haml
- if user_signed_in?
  = form_with(model: [@tweet, @comment]) do |form| 
    = form.text_area :content, class: 'textarea'
    = form.submit value: "送信"
- else
  コメントの投稿にはログインが必要です。
```
いつも通りですね。form_withで入力した情報を飛ばしてcreateかupdateアクションを発火させます。
form_withはデフォルトでremote: tureなので、省略。
remote: trueに関しては次のviewで解説します。
htmlでデータを送ってほしい時はlocal: trueが必要です。


次はコメント一覧です。
#
```ruby:comments/_comment.html.haml
- @comments.each do |comment|
  %div{id: "comment_#{comment.id}"}
    = comment.user.nickname
    = comment.content
    = comment.updated_at.strftime("%Y-%m-%d %H:%M")
  .list-inline-item
    - if user_signed_in? && comment.user_id == current_user.id
      = link_to edit_tweet_comment_path(comment.tweet_id, comment.id),remote: true do
        %input{type: "submit", value: "編集"}
      = link_to tweet_comment_path(comment.tweet_id, comment.id), method: :delete, remote: true, data: { confirm: '削除してよろしいですか？' } do
        %input{type: "submit", value: "削除"}
```
ここではコメント一覧と、その編集と削除のリンクがあります。
2行目でdivクラスにcommentのidを指定している理由は最後に説明します。

link_toの中で大事な点が2つあります。
1つ目はprefixのルート、2つ目はremote: trueの仕様についてです。

🔳1つ目
prefixのパスが見慣れない形（idを2つ指定している）になってますが、
rails routesを確認していただければ分かります。
#
```
       edit_tweet_comment GET    /tweets/:tweet_id/comments/:id/edit(.:format)                                            comments#edit
            tweet_comment PATCH  /tweets/:tweet_id/comments/:id(.:format)                                                 comments#update
                          PUT    /tweets/:tweet_id/comments/:id(.:format)                                                 comments#update
                          DELETE /tweets/:tweet_id/comments/:id(.:format)                                                 comments#destroy
```
commentはtweetにネストされているので、tweet_idの中のcommentっていう形になってます。
例）tweet_idが1に紐付いたcomment_idみたいなイメージ
なので、tweet_idとcomment_idの2つを指定する必要があります。

◎余談
routes.rbでresources :tweetsに「shallow true」を追加すると
ネストされたprefixを以下のように省略できます。
#
```
       edit_comment GET    /comments/:id/edit(.:format)                                            comments#edit
            comment PATCH  /comments/:id(.:format)                                                 comments#update
                          PUT    /comments/:id(.:format)                                                 comments#update
                          DELETE //comments/:id(.:format)                                                 comments#destroy
```
「edit_commentのcomments/:id」と、tweetが省略されます。
ですが、このprefixに変更すると上手くルートを辿ってくれません。
routing errorじゃなくてtweetが見つけられません。っていうエラーが出ました。
解決が難しそうなので、とりあえず今回はshallow trueはなしで実装しました。

🔳2つ目
= link_toでremote: trueを書いてる理由は、js形式で送られてほしいからです。
デフォルトはhtml形式でデータが送られるけど、remote: trueを書くことで
js形式でデータが送られるようになります。すごい。
後1つ、js形式になることで、次に紹介するjs.hamlファイルの対応するアクション名の内容が発火されるようになります。remote: true有能。

例えば、comments_controllerのcreateアクションが発火された場合は、命令通りcreateアクションが発火されますが、その時に同時に同じファイルにあるアクション名のjs.hamlファイルも発火されます（今回の場合はcomments/create.js.hamlファイルが発火する）

これで最後です。js.hamlファイルの解説です。
ファイルの場所はcommentsフォルダの中に作成します。

🔳js.hamlファイルとは？
Javascriptのファイル形式だけど、hamlの記述の仕方ができる
ハイブリットのすごいやつです。
これを利用してviewを作っていきます。

#
```ruby:comments/create.js.haml
$('#comment_area').html("#{ j(render 'comments/comment') }");
$('#comment_form').html("#{ j(render 'comments/form') }");
```
🔳解説
comments/indexに書いてある「.div#comment_area」にcreateアクションで発火されたhtmlファイルを投げつける（formをrenderする）命令をします。

j っていう文字はescape_javascriptの略です。

公式リファレンスだと「escape_javascriptはJavaScriptセグメントから改行 (CR) と一重引用符と二重引用符をエスケープします」って言ってますが意味わからないですね。
renderのformの中で生成した内容を、
上手いこと('#comment_area’)にhtml形式でformをrenderして投げつけてくれる。
っていうイメージでいます。
すごい感覚的な理解なので、違ったらごめんなさい。

[公式リファレンス以外で参考になったURL]
(https://books.google.co.jp/books?id=XHFVzFCefI8C&pg=PA361&lpg=PA361&dq=escape_javascript+%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9&source=bl&ots=-xQg5QycWC&sig=ACfU3U0KDnokWfgHSlZQd0fL8K29ONP0TQ&hl=ja&sa=X&ved=2ahUKEwjAiczJiLvpAhXQdd4KHYhJAgYQ6AEwA3oECAkQAQ#v=onepage&q=escape_javascript%20%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9&f=false)

#
```ruby:comments/edit.js.haml
$('#comment_#{ @comment.id }').html("#{ j(render 'comments/form') }");
```
ここも大事です。
editなのでcommentを編集したいです。
そのためには、どのcommentか見分ける必要があります。
なので、comments/commentでdivクラスにcommentのidを指定しました。
そのidのcommentを編集するよーっていうイメージです。
後はさっきと同じく、編集したいcommentのidに
html形式でformをrenderして投げつけます。

#
```ruby:comments/update.js.haml
$('#comment_area').html("#{ j(render 'comments/comment') }");
```
comment一覧フォームを再表示（更新した情報を反映）させたいので、
commentページをhtml形式でrenderしてます。

#
```ruby:comments/destroy.js.haml
$('#comment_area').html("#{ j(render 'comments/comment') }");
```
updateと全く同じです。
comment一覧フォームを再表示させたいので、commentページをhtml形式でrenderしてます。

大変長くなりましたが、以上でコメント投稿機能の非同期通信が完了しました。
ここまで読んでいただいて本当にありがとうございます！

最初の注意事項に書きましたが、まずはコピペして意味を考えるのがおすすめです。
自分の頭で考えるのが効率的なインプットになると思うからです。
私もそうやりました。笑

では！