3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Rails,JS】コメントの非同期表示を実装する方法

Posted at

###この記事では、コメントを非同期で表示できる実装方法を解説します。

  • Rails_6.0.0_ を使用しています。
  • 商品が出品・購入できるアプリケーションにコメントをつけます。
  • それぞれの商品(item)の詳細ページにコメントをする場所があります。
  • コメントの保存や送信に必要なRubyのコーディングと、保存したコメントを即時に表示させるJavaScriptのコーディングを行います。
  • すでにコメント投稿機能は完成している体で、非同期機能だけ実装していきます。

画面収録 2020-10-06 -1.mov.gif

#実装内容

  • channelを用いて実装を行う
  • コメントを非同期で表示
  • コメントには名前(name)、コメントされた日付(created_at)、コメント内容(text)を表示

#channelとは?
####channelとは?
チャネルとは、即時更新機能を実現するサーバー側の仕組みのことです。データの経路を設定したり、送られてきたデータをクライアントの画面上に表示させたりします。
####channelでどんなことができる?
データの経路を設定したり、送られてきたデータを表示させるJavaScriptを記述すれば、送信したデータが非同期で表示できます。
####channelのファイル作成
ターミナルで以下コマンドを実行
% rails g channel comment
(commentには自分が作成するファイルの名前を記述)
いくつかファイルができますが、今回は以下2つのファイルを使用します。
#####app/channel/comment_channel.rb
クライアントとサーバーを結びつけるためのファイルです。

#####app/javascript/channels/comment_channel.js
サーバーから送られてきたデータをクライアントの画面に表示させるためのファイルです。
#comment_channel.rbの記述

class MessageChannel < ApplicationCable::Channel
  def subscribed
    stream_from "comment_channel"
  end

  def unsubscribed
  end
end

stream_from "comment_channel"を記述することでサーバーとクライアントを結びつけることができます。

#comments_controller.rbの記述
コントローラーの記述です。
すでに非同期でないコメント実装機能は済んでいる体なので、非同期に関する記述以外の説明は割愛します。
####データベースからJSに渡したいデータを記述する
今回JSで反映させたいデータは以下の通りです

  • ユーザーのニックネーム
  • コメントされた時間
  • コメントのテキスト
  • アイテムのid(どの商品にコメントするかを判断するために必要)

この3つの情報を、コントローラーでJSに渡してあげる必要があります。

class CommentsController < ApplicationController
  before_action :authenticate_user!
  def create
    @comment = Comment.new(comment_params)
    @item = Item.find(params[:item_id])
    @comments = @item.comments.includes(:user).order('created_at DESC')
    if @comment.valid?
      @comment.save
            ActionCable.server.broadcast  'comment_channel', content: @comment, nickname: @comment.user.nickname, time: @comment.created_at.strftime("%Y/%m/%d %H:%M:%S"), id: @item.id
    else
      render "items/show"
    end
  end

  private
    def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, item_id: params[:item_id])
    end
end

今回の実装で書き足したのは以下の1文だけです。
ActionCable.server.broadcast 'comment_channel', content: @comment, nickname: @comment.user.nickname, time: @comment.created_at.strftime("%Y/%m/%d %H:%M:%S"), id: @item.id

contentusertimeidをJSで使用するので、そちらの定義をしてあげました。
content
@commentで定義している:text、Commentテーブルのtextカラム、すなわち入力したコメントのことです。
user
@commentに紐づいているuserのnicknameをとってきています。(commentとuserにアソシエーションを組んでいます。)
time
Commentテーブルのcreated_atカラムです。strftime("%Y/%m/%d %H:%M:%S")と記述することで任意の日付、時刻設定を表示できます。以下の記事を参考にさせていただきました。
strftime を憶えられない (rubyの)
item
自分が今表示しているアイテムのページだけでJSが発火する必要があるので、それを判断するために使用します。

#comment_channel.rbの記述(JavaScript)

今回はapp/javascript/channels/comment_channel.jsreceived()部分に記述していきます。

app/javascript/channels/comment_channel.js
received(data) {
}

()の中にdataと記述してあげることで、コントローラーで定義した値がとってこれるようになります。receivedは、受け取るという意味なので、データを受け取ったら、この中に記述しているJSを実行してね!ということになります。
さあ、これからこの中にJSの記述をしていきます!

#####今開いているアイテムページだけでコメント表示できるように、条件分岐分を書きます。

app/javascript/channels/comment_channel.js
//現在開いているページのURLをゲット
let url = window.location.href
//スラッシュ(/)ごとに要素を取り出す
let param = url.split('/');
//このアプリの場合、URLの一番最後にアイテムidがきており、それをparamItemとして定義
let paramItem = param[param.length-1]
// パラメータid(URLの中に含まれているid)が、コントローラーから送った`data.id`かどうかを判断する
if (paramItem == data.id) {}

if文で、条件分岐をしてあげます。次は、処理の内容を条件分岐分の中に書いてあげます。
記述する内容は、

  • div要素を生成する
  • 生成した要素をブラウザに表示させる
  • 表示させるテキストを生成する

といった流れです。

#####表示させるためのdivを作る

app/javascript/channels/comment_channel.js
    //表示させる場所のdivのIDをとってくる
    const comments = document.getElementById('comments');
    // すでにあるビューファイルと同じになるようにdivを作成
    const textElement = document.createElement('div');
    textElement.setAttribute('class', "comment-display");
    
    const topElement = document.createElement('div');
    topElement.setAttribute('class', "comment-top");

    const nameElement = document.createElement('div');
    const timeElement = document.createElement('div');

    const bottomElement = document.createElement('div');
    bottomElement.setAttribute('class', "comment-bottom");

createElementメソッドを使用し、div要素を作成、必要なものにはそれぞれsetAttributeメソッドでclass名を与えてあげます。

ちなみに、コメントを表示するビューは以下の通り

  <div id='comments'>
  </div>
  <% @comments.each do |comment| %>
    <div class='comment-display'>
      <div class='comment-top'>
        <div><%= comment.user.nickname %></div>
        <div><%= l comment.created_at %></div>
      </div>
      <div class='comment-bottom'>
        <p><%= comment.text %></p>
      </div>
    </div>
  <% end %>

div要素を生成しましたが、まだブラウザに表示されていません。ブラウザに表示させ、かつ親子関係を作りましょう。

app/javascript/channels/comment_channel.js
      // 生成したHTMLの要素をブラウザに表示させる
      comments.insertBefore(textElement, comments.firstElementChild);
      textElement.appendChild(topElement);
      textElement.appendChild(bottomElement);
      topElement.appendChild(nameElement);
      topElement.appendChild(timeElement);

insertBefireメソッドと、appendChildメソッドを使用します。
親要素.insertBefore(追加する要素, どこに追加するのか)
親要素.appendChild(追加する要素)
で、insertBefireは任意の場所に、appendChildは親クラスの中の最後のに要素を入れることができます。

もう少し詳しく見たい方は以下をご覧ください
【JavaScript】appendChildとinsertBeforeの違い

div要素が作成できたら、次は表示させる情報をとってきましょう。

app/javascript/channels/comment_channel.js
      const name = `${data.nickname}`;
      nameElement.innerHTML = name;
      const time = `${data.time}`;
      timeElement.innerHTML = time;
      const text = `<p>${data.content.text}</p>`;
      bottomElement.innerHTML = text;

表示させる情報をそれぞれ変数に入れています。dataは、received(data) {}のdataです。コントローラーで定義した値のことです。それぞれcontent,nickname,time,を定義しましたね。
innerHTMLで、既存の要素にHTMLを上書きをします。

ここまでで表示は完了しました!
ですがこのままだと、2つ問題があります。

  1. データは表示されたが、コメント入力欄にコメントが残ったままであること
  2. HTMLはデフォルトでボタンが1回しか押せない仕様になっていること
    これを解決しましょう!
    #####データ送信した後にコメント入力欄のコメントを消す
app/javascript/channels/comment_channel.js
    const newComment = document.getElementById('comment_text');
    newComment.value='';

コメント入力欄のIDをとってきて、そこの値を空にする、という記述です。
#####何度もコメントボタンを押せるようにする

app/javascript/channels/comment_channel.js
    const inputElement = document.querySelector('input[name="commit"]');
    inputElement.disabled = false;

"コメントする"ボタンの、name属性をとってきて、そこをdisabled = falseとしてあげることで何度もクリック可能になります。

#####記述をまとめると以下の通りです。

app/javascript/channels/comment_channel.js
import consumer from "./consumer"

consumer.subscriptions.create("CommentChannel", {
  connected() {
  },

  disconnected() {
  },
  // ↓データを受け取ったら実行してね
  received(data) {
    let url = window.location.href
    let param = url.split('/');
    let paramItem = param[param.length-1]
    if (paramItem == data.id) {
      const comments = document.getElementById('comments');
      const comment = document.getElementsByClassName('comment-display');
      //使用する要素の作成
      const textElement = document.createElement('div');
      textElement.setAttribute('class', "comment-display");
      const topElement = document.createElement('div');
      topElement.setAttribute('class', "comment-top");
      const nameElement = document.createElement('div');
      const timeElement = document.createElement('div');
      const bottomElement = document.createElement('div');
      bottomElement.setAttribute('class', "comment-bottom");
      // 生成したHTMLの要素をブラウザに表示させる
      comments.insertBefore(textElement, comments.firstElementChild);
      textElement.appendChild(topElement);
      textElement.appendChild(bottomElement);
      topElement.appendChild(nameElement);
      topElement.appendChild(timeElement);
      // 表示するテキストを生成
      const name = `${data.nickname}`;
      nameElement.innerHTML = name;
      const time = `${data.time}`;
      timeElement.innerHTML = time;
      const text = `<p>${data.content.text}</p>`;
      bottomElement.innerHTML = text;
      //コメントを送った後、コメント欄をからにする
      const newComment = document.getElementById('comment_text');
      newComment.value='';
      //何度もボタンを押せるようにする
      const inputElement = document.querySelector('input[name="commit"]');
      inputElement.disabled = false;
    }
  }
});

##おわりに
完成したと思っていましたが、これを書いている時点でいくつもミスや、ちょっとよくわからない記述を発見しました。
リファクタリングがいかに大切かよく分かりました。
正しく無い記述があるかもしれませんが、誰かのお役に立てれば幸いです。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?