###この記事では、コメントを非同期で表示できる実装方法を解説します。
- Rails_6.0.0_ を使用しています。
- 商品が出品・購入できるアプリケーションにコメントをつけます。
- それぞれの商品(item)の詳細ページにコメントをする場所があります。
- コメントの保存や送信に必要なRubyのコーディングと、保存したコメントを即時に表示させるJavaScriptのコーディングを行います。
- すでにコメント投稿機能は完成している体で、非同期機能だけ実装していきます。
#実装内容
- 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
content
とuser
とtime
とid
を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.js
のreceived()
部分に記述していきます。
received(data) {
}
()の中にdata
と記述してあげることで、コントローラーで定義した値がとってこれるようになります。received
は、受け取るという意味なので、データを受け取ったら、この中に記述しているJSを実行してね!ということになります。
さあ、これからこの中に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を作る
//表示させる場所の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要素を生成しましたが、まだブラウザに表示されていません。ブラウザに表示させ、かつ親子関係を作りましょう。
// 生成した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要素が作成できたら、次は表示させる情報をとってきましょう。
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つ問題があります。
- データは表示されたが、コメント入力欄にコメントが残ったままであること
- HTMLはデフォルトでボタンが1回しか押せない仕様になっていること
これを解決しましょう!
#####データ送信した後にコメント入力欄のコメントを消す
const newComment = document.getElementById('comment_text');
newComment.value='';
コメント入力欄のIDをとってきて、そこの値を空にする、という記述です。
#####何度もコメントボタンを押せるようにする
const inputElement = document.querySelector('input[name="commit"]');
inputElement.disabled = false;
"コメントする"ボタンの、name属性をとってきて、そこをdisabled = false
としてあげることで何度もクリック可能になります。
#####記述をまとめると以下の通りです。
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;
}
}
});
##おわりに
完成したと思っていましたが、これを書いている時点でいくつもミスや、ちょっとよくわからない記述を発見しました。
リファクタリングがいかに大切かよく分かりました。
正しく無い記述があるかもしれませんが、誰かのお役に立てれば幸いです。