便利で快適な入力フォーム開発に特化したJavaScriptライブラリ「InputManJS(インプットマンJS)」は、チャットやフォーラム、会話アプリなどで見られる会話機能のUIが構築できる「コメントコンポーネント(GcComment)」を提供しています。
今回は以下で公開しているコメントコンポーネントとPythonのWebフレームワーク「FastAPI」を使用したアプリケーションをベースに、簡易的なログイン機能とリアルタイムな双方向通信機能を追加して、簡単なチャットアプリケーションを作成してみたいと思います。
開発環境
今回は開発環境として以下を使用します。
- InputManJS V5.1J
- Python 3.13.0
- FastAPI 0.115.5
- Visual Studio Code
- Live Server(Visual Studio Code拡張機能)
簡易的なログイン機能を追加する
まずはチャットアプリに必要なログイン機能を追加していきます。今回はパスワード不要でユーザーIDのみでログインする非常に簡易的なものを実装します。
フロント部分の「index.html」を以下のように修正します。今回は同じHTMLファイル内にログインフォームとコメントコンポーネントを配置し、ログイン/ログアウト処理が行われたらそれぞれ表示を切り替えます。
・・・(中略)・・・
<body>
<div id="login-area">
<input type="text" id="userid-input" placeholder="ユーザーID" />
<button id="login-btn">ログイン</button>
</div>
<div id="gcComment" style="display:none;"></div>
</body>
・・・(中略)・・・
「scripts/app.js」を以下のように修正し、ログインの処理とコメントコンポーネントの初期化処理などを定義します。「users」APIからユーザー情報を取得し、コメントコンポーネントに設定します。また、headerFooterItemsオプションを使用して、ログインしているユーザー名の表示や、ログアウトボタンをヘッダーに追加しています。
document.addEventListener('DOMContentLoaded', () => {
const baseURL = `http://localhost:8000/`;
const commentURL = `${baseURL}comments`;
const userURL = `${baseURL}users`;
const reactionURL = `${baseURL}reactions`;
// ログイン状態管理
let currentUser = null;
// コメントコンポーネント
let gcComment = null;
// ページロード時にlocalStorageから自動ログイン
let savedUser = localStorage.getItem('gcCommentUser');
if (savedUser) {
try {
const userInfo = JSON.parse(savedUser);
currentUser = userInfo;
document.getElementById('login-area').style.display = 'none';
document.getElementById('gcComment').style.display = '';
initGcComment(currentUser);
} catch (e) {
localStorage.removeItem('gcCommentUser');
}
}
// ログインボタン処理
document.getElementById('login-btn').addEventListener('click', async () => {
const userId = document.getElementById('userid-input').value.trim();
if (!userId) {
alert('ユーザーIDを入力してください');
return;
}
// バックエンドからユーザー情報取得
try {
const res = await fetch(`http://localhost:8000/users?id=${encodeURIComponent(userId)}`);
if (!res.ok) throw new Error('ユーザー取得失敗');
const user = await res.json();
if (user.length === 0) {
alert('ユーザーが見つかりません');
return;
}
currentUser = {
id: String(user[0].id),
username: user[0].username,
avatar: user[0].avatar,
avatarType: 'square',
};
// localStorageに保存
localStorage.setItem('gcCommentUser', JSON.stringify(currentUser));
document.getElementById('login-area').style.display = 'none';
document.getElementById('gcComment').style.display = '';
initGcComment(currentUser);
window.location.hash = '#chat';
} catch (e) {
alert('ユーザー情報の取得に失敗しました');
}
});
// コメントコンポーネント初期化関数
function initGcComment(userInfo) {
gcComment = new GC.InputMan.GcComment(document.getElementById('gcComment'), {
dataSource: {
enabled: true,
remote: {
comments: {
read: { url: commentURL },
create: { url: commentURL },
update: { url: commentURL },
delete: { url: commentURL }
},
users: {
read: {
url: userURL,
schema: {
dataSchema: {
name: 'username'
}
}
}
},
reactions: {
read: { url: reactionURL },
create: { url: reactionURL },
delete: { url: reactionURL }
},
}
},
editorConfig: { height: 150 },
commentMode: GC.InputMan.GcCommentMode.ThreadMode,
userInfo: userInfo,
header: [
'userinfo'
],
headerFooterItems: {
userinfo: (gcComment) => {
let container = document.createElement('div'); // 新しいコンテナ要素を作成
let label = document.createElement('span'); // テキスト用のspan要素を作成
label.innerText = 'ユーザー名:' + gcComment.userInfo.username; // ラベルのテキストを設定
label.style.marginRight = '10px'; // ボタンとの間に少し余白を追加
let btn = document.createElement('button');
btn.innerText = 'ログアウト';
btn.classList.add('btn');
btn.addEventListener('click', () => {
if (window.confirm('ログアウトしますか?')) {
localStorage.removeItem('gcCommentUser');
gcComment.destroy();
savedUser = null;
currentUser = null;
document.getElementById('login-area').style.display = '';
document.getElementById('gcComment').style.display = 'none';
window.location.hash = '';
}
});
container.appendChild(label); // ラベルをコンテナに追加
container.appendChild(btn); // ボタンをコンテナに追加
return {
getElement: () => container,
};
},
},
});
}
});
動作確認
ファイルを修正したら、以下のコマンドでバックエンドのAPIを起動します。
uvicorn app.main:app --reload
Visual Studio Code上で「index.html」を右クリックして、「Open with Live Server」を実行します。

実行後、ブラウザ上にログインページが表示されます。

あらかじめ前回登録しておいたユーザーのID(1~6)を入力しログインします。
ログイン後はコメントの投稿や、ログアウトから別ユーザーへの切り替えも可能です。
双方向通信機能を追加する
次はこのアプリケーションにリアルタイム双方向通信機能を追加し、新しくコメントが投稿された場合に、別の接続しているユーザーの画面のコメントコンポーネントに対して、画面を再読み込みすることなく即座に変更(別の画面で投稿されたコメント)を反映できるようにします。
バックエンド側
バックエンドのFastAPIのアプリケーションにpython-socketioを組み込み、コメントの登録、更新、削除、リアクションの登録、削除が行われた場合にクライアントに変更箇所を通知します。また、変更箇所の通知用に、リアクション情報取得とコメントを辞書形式に変換するヘルパー関数もそれぞれ追加しています。
import socketio
from typing import Any
from fastapi import Depends, FastAPI, Form, HTTPException, status, Query
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from datetime import datetime
from typing import Union
from .database import engine, get_db
from . import models, schemas
models.Base.metadata.create_all(bind=engine)
# コメント情報取得のヘルパー関数
def get_comment(id: int, db_session: Session):
return db_session.query(models.Comment).filter(models.Comment.id == id).first()
# ピン留めされたコメント情報取得のヘルパー関数
def get_sticked_comment(db_session: Session):
return db_session.query(models.Comment).filter(models.Comment.sticked == True).first()
# ユーザー情報取得のヘルパー関数
def get_user(id: int, db_session: Session):
return db_session.query(models.User).filter(models.User.id == id).first()
# リアクション情報取得のヘルパー関数(どのユーザーがどのリアクションをしたか)
def get_reaction(commentId: int, db_session: Session):
return db_session.query(models.Reaction.reactionChar, models.Reaction.userId).filter(models.Reaction.commentId == commentId).all()
# コメントを辞書形式に変換するヘルパー関数、ユーザー情報が必要な場合は、user引数を渡します
def comment_to_dict(comment: models.Comment, user: Union[models.User, None] = None):
comment_dict = {
"id": comment.id,
"parentCommentId": comment.parentCommentId,
"content": comment.content,
"sticked": comment.sticked,
"postTime": comment.postTime.strftime("%Y/%m/%d %H:%M:%S"),
"updateTime": comment.updateTime.strftime("%Y/%m/%d %H:%M:%S"),
"userId": comment.userId,
"mentionInfo": comment.mentionInfo,
}
# ユーザー情報が提供されている場合、辞書に追加
if user:
comment_dict["userInfo"] = {
"id": user.id,
"name": user.username,
"avatar": user.avatar,
}
return comment_dict
app = FastAPI()
# Socket.IO サーバーのセットアップ
sio: Any = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
# CORS対応
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
socket_app = socketio.ASGIApp(sio, app)
# Commentを全件取得
@app.get("/comments")
def read_comments(db: Session = Depends(get_db), type : str = Query("NONE")):
# type=stickの場合はピン留めするコメントの情報を返却
if type == "sticked":
sticked_comment = get_sticked_comment(db)
if sticked_comment is not None:
return {
"id": sticked_comment.id,
"parentCommentId": sticked_comment.parentCommentId,
"content": sticked_comment.content,
"sticked": sticked_comment.sticked,
"postTime": sticked_comment.postTime.strftime("%Y/%m/%d %H:%M:%S"),
"updateTime": sticked_comment.updateTime.strftime("%Y/%m/%d %H:%M:%S"),
"userId": sticked_comment.userId,
"mentionInfo": sticked_comment.mentionInfo,
}
else:
return {"hasMore": False, "comments": []}
else:
comments = db.query(models.Comment).all()
return {
"hasMore": False,
"comments": [
{
"id": comment.id,
"parentCommentId": comment.parentCommentId,
"content": comment.content,
"sticked": comment.sticked,
"postTime": comment.postTime.strftime("%Y/%m/%d %H:%M:%S"),
"updateTime": comment.updateTime.strftime("%Y/%m/%d %H:%M:%S"),
"userId": comment.userId,
"mentionInfo": comment.mentionInfo,
}
for comment in comments
],
}
# Commentを登録
@app.post("/comments")
async def create_comment(
userId: int = Form(...),
parentId: Union[int, str, None] = Form(None),
sticked: bool = Form(False),
content: str = Form(...),
mentionInfo: Union[str, None] = Form(None),
socketId: str = Form(None),
db: Session = Depends(get_db)
):
formdata = schemas.CommentIn(
userId=userId,
parentId=parentId,
sticked=sticked,
content=content,
mentionInfo=mentionInfo,
)
comment = models.Comment(
userId=formdata.userId,
parentCommentId = None if formdata.parentId == 'undefined' else formdata.parentId,
sticked=formdata.sticked,
content=formdata.content,
mentionInfo=formdata.mentionInfo,
postTime=datetime.now(),
updateTime=datetime.now()
)
db.add(comment)
db.commit()
db.refresh(comment)
user = get_user(comment.userId, db)
commentdict = comment_to_dict(comment, user)
await sio.emit("commentupdated", {"type": "add", "comment": commentdict}, skip_sid=socketId ) # socketIdを指定してemit
return comment
# Commentを更新
@app.put("/comments")
async def update_comment(
id: int = Form(...),
userId: int = Form(...),
parentCommentId: Union[int, str, None] = Form(None),
stick: bool = Form(False),
content: Union[str, None] = Form(None),
newContent: Union[str, None] = Form(None),
mentionInfo: Union[str, None] = Form(None),
socketId: str = Form(None),
db: Session = Depends(get_db)
):
formdata = schemas.CommentIn(
userId=userId,
parentId=parentCommentId,
sticked=True if stick is True else False, # ピン留めの状態を更新
content=newContent if newContent is not None else content,
mentionInfo=mentionInfo,
)
comment = models.Comment(
userId=formdata.userId,
parentCommentId=formdata.parentId,
sticked=formdata.sticked,
content=formdata.content,
mentionInfo=formdata.mentionInfo,
updateTime=datetime.now()
)
try:
sticked_comment = get_sticked_comment(db)
if sticked_comment is not None and sticked_comment.id != id and comment.sticked:
# 既にピン留めされているコメントがある場合は、ピン留めを解除
sticked_comment.sticked = False
db_comment = get_comment(id,db)
if db_comment is None:
raise HTTPException(status_code=404, detail="Comment not found")
else:
db_comment.userId = comment.userId
db_comment.parentCommentId = None if comment.parentCommentId == 'undefined' else comment.parentCommentId
db_comment.sticked = comment.sticked
db_comment.content = comment.content
db_comment.mentionInfo = comment.mentionInfo
db_comment.updateTime = comment.updateTime
db.commit()
db.refresh(db_comment)
commentdict = comment_to_dict(comment)
await sio.emit("commentupdated", {"type": "update", "comment": commentdict}, skip_sid=socketId)
return db_comment
except Exception as e:
db.rollback() # エラーが発生したらすべての変更をロールバック
raise HTTPException(status_code=500, detail=f"An error occurred: {e}")
# Commentを削除
@app.delete("/comments")
async def delete_comment(commentId: int, socketId: str, db: Session = Depends(get_db)):
db_comment = get_comment(commentId,db)
if db_comment is None:
raise HTTPException(status_code=404, detail="Comment not found")
else:
db_comment = db.query(models.Comment).filter(models.Comment.id == commentId).delete()
db.commit()
await sio.emit("commentupdated", {"type": "delete", "id": commentId}, skip_sid=socketId)
return True
# Userを取得
@app.get("/users")
def read_user(id: int, db: Session = Depends(get_db)):
user = get_user(id, db)
if user is None:
return []
return [user]
# Reactionを取得
@app.get("/reactions")
def read_reaction(commentId: int, userId: int, db: Session = Depends(get_db)):
reactions = db.query(models.Reaction.reactionChar, func.count(models.Reaction.reactionChar).label("count")
).filter(models.Reaction.commentId == commentId).group_by(models.Reaction.reactionChar).all()
user_reactions = db.query(models.Reaction.reactionChar).filter(models.Reaction.commentId == commentId, models.Reaction.userId == userId).all()
user_reacted_chars = {reaction[0] for reaction in user_reactions}
reaction_info = [
{
"reactionChar": reaction[0],
"count": reaction[1],
"currentUserReacted": reaction[0] in user_reacted_chars
}
for reaction in reactions
]
return reaction_info
# Reactionsを登録
@app.post("/reactions")
async def create_reaction(
reactChar: str = Form(...),
commentId: int = Form(...),
userId: int = Form(...),
socketId: str = Form(None),
db: Session = Depends(get_db)
):
formdata = schemas.ReactionIn(
reactionChar=reactChar,
commentId=commentId,
userId=userId,
)
reaction = models.Reaction(
reactionChar=formdata.reactionChar,
commentId=formdata.commentId,
userId=formdata.userId,
)
db.add(reaction)
db.commit()
db.refresh(reaction)
reactions = get_reaction(reaction.commentId, db)
reaction_info_list = [
{
"reactionChar": r[0],
"userId": r[1],
}
for r in reactions
]
await sio.emit("reactionupdated", {"type": "add", "commentId": reaction.commentId, "reactionInfo": reaction_info_list}, skip_sid=socketId)
return True
# Reactionを削除
@app.delete("/reactions")
async def delete_reaction(commentId: int, userId: int, reactChar: str,socketId: str = Form(None), db: Session = Depends(get_db)):
db_reaction = db.query(models.Reaction).filter(models.Reaction.userId == userId, models.Reaction.commentId == commentId, models.Reaction.reactionChar == reactChar).delete()
if db_reaction == 0:
raise HTTPException(status_code=404, detail="Reaction not found")
else:
db.commit()
reactions = get_reaction(commentId, db)
reaction_info_list = [
{
"reactionChar": r[0],
"count": r[1],
}
for r in reactions
]
await sio.emit("reactionupdated", {"type": "delete", "commentId": commentId, "reactionChar": reactChar, "reactionInfo": reaction_info_list}, skip_sid=socketId)
return True
# リクエストエラー時のハンドリング
@app.exception_handler(RequestValidationError)
async def handler(request:Request, exc:RequestValidationError):
print(exc)
return JSONResponse(content={}, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
# WebSocket 接続時の処理
@sio.event
def connect(sid, environ):
print(f"Client {sid} connected")
@sio.event
def disconnect(sid):
print(f"Client {sid} disconnected")
# FastAPIにASGIアプリをマウント
app.mount("/socket.io", socket_app)
コメントやリアクションの登録や更新、削除処理が完了したタイミングでemitメソッドを実行して接続しているクライアントにイベントを送信します。その際、skip_sid
のオプションで現在のユーザー(コメントの登録、更新、削除を実施したユーザー)のsocketId
を指定し、イベントを送信する対象から除外します。
・・・(中略)・・・
await sio.emit("commentupdated", {"type": "add", "comment": commentdict}, skip_sid=socketId ) # socketIdを指定してemit
・・・(中略)・・・
フロントエンド側
次にフロント側の「index.html」にCDNのsocket.ioの参照を追加します。
・・・(中略)・・・
<script src="scripts/gc.inputman.comment.ja.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.8.1/socket.io.js"></script>
・・・(中略)・・・
次に「script/app.js」を以下のように修正します。コメントやリアクション登録時に設定するパラメーターにsocketid
を追加し、接続しているクライアントをサーバー側で識別できるようにします。
document.addEventListener('DOMContentLoaded', () => {
const baseURL = `http://localhost:8000/`;
const commentURL = `${baseURL}comments`;
const userURL = `${baseURL}users`;
const reactionURL = `${baseURL}reactions`;
// ログイン状態管理
let currentUser = null;
// コメントコンポーネント
let gcComment = null;
let socket = io(baseURL, { transports: ["websocket", "polling"] });
// ページロード時にlocalStorageから自動ログイン
let savedUser = localStorage.getItem('gcCommentUser');
if (savedUser) {
try {
const userInfo = JSON.parse(savedUser);
currentUser = userInfo;
document.getElementById('login-area').style.display = 'none';
document.getElementById('gcComment').style.display = '';
if (socket.connected === false) {
socket.connect();
}
socket.on('connect', () => {
initGcComment(currentUser);
});
} catch (e) {
localStorage.removeItem('gcCommentUser');
}
}
// ログインボタン処理
document.getElementById('login-btn').addEventListener('click', async () => {
const userId = document.getElementById('userid-input').value.trim();
if (!userId) {
alert('ユーザーIDを入力してください');
return;
}
// バックエンドからユーザー情報取得
try {
const res = await fetch(`http://localhost:8000/users?id=${encodeURIComponent(userId)}`);
if (!res.ok) throw new Error('ユーザー取得失敗');
const user = await res.json();
if (user.length === 0) {
alert('ユーザーが見つかりません');
return;
}
currentUser = {
id: String(user[0].id),
username: user[0].username,
avatar: user[0].avatar,
avatarType: 'square',
};
// localStorageに保存
localStorage.setItem('gcCommentUser', JSON.stringify(currentUser));
document.getElementById('login-area').style.display = 'none';
document.getElementById('gcComment').style.display = '';
socket.connect();
socket.on('connect', () => {
if (Object.keys(gcComment).length === 0) {
initGcComment(currentUser);
}
});
window.location.hash = '#chat';
} catch (e) {
console.log('Error fetching user information:', e);
alert('ユーザー情報の取得に失敗しました');
}
});
// コメントコンポーネント初期化関数
function initGcComment(userInfo) {
gcComment = new GC.InputMan.GcComment(document.getElementById('gcComment'), {
dataSource: {
enabled: true,
remote: {
comments: {
read: { url: commentURL },
create: { url: commentURL, requestData: { socketId: socket.id } },
update: { url: commentURL, requestData: { socketId: socket.id } },
delete: { url: commentURL, requestData: { socketId: socket.id } }
},
users: {
read: {
url: userURL,
schema: {
dataSchema: {
name: 'username'
}
}
}
},
reactions: {
read: { url: reactionURL },
create: { url: reactionURL, requestData: { socketId: socket.id } },
delete: { url: reactionURL, requestData: { socketId: socket.id } }
},
}
},
editorConfig: { height: 150 },
commentMode: GC.InputMan.GcCommentMode.ThreadMode,
userInfo: userInfo,
header: [
'userinfo'
],
headerFooterItems: {
userinfo: (gcComment) => {
let container = document.createElement('div'); // 新しいコンテナ要素を作成
let label = document.createElement('span'); // テキスト用のspan要素を作成
label.innerText = 'ユーザー名:' + gcComment.userInfo.username; // ラベルのテキストを設定
label.style.marginRight = '10px'; // ボタンとの間に少し余白を追加
let btn = document.createElement('button');
btn.innerText = 'ログアウト';
btn.classList.add('btn');
btn.addEventListener('click', () => {
if (window.confirm('ログアウトしますか?')) {
localStorage.removeItem('gcCommentUser');
gcComment.destroy();
savedUser = null;
currentUser = null;
socket.disconnect();
document.getElementById('login-area').style.display = '';
document.getElementById('gcComment').style.display = 'none';
window.location.hash = '';
}
});
container.appendChild(label); // ラベルをコンテナに追加
container.appendChild(btn); // ボタンをコンテナに追加
return {
getElement: () => container,
};
},
},
});
}
// サーバー側で定義されているcommentupdatedイベントの発火を検知します。
socket.on('commentupdated', (msg) => {
handleCommentsChange(msg);
});
// サーバー側で定義されているreactionupdatedイベントの発火を検知します。
socket.on('reactionupdated', (msg) => {
handleReactionChange(msg);
});
function handleCommentsChange(msg) {
switch (msg.type) {
case 'add':
gcComment.execCommand(GC.InputMan.GcCommentCommand.AddCommentElement, {
comment: {
...msg.comment,
parentCommentId: String(msg.comment.parentCommentId) || null,
postTime: new Date(msg.comment.postTime),
updateTime: new Date(msg.comment.updateTime),
},
scrollIntoView: true
});
break;
case 'delete':
gcComment.execCommand(GC.InputMan.GcCommentCommand.DeleteCommentElement, {
commentId: String(msg.id)
});
break;
case 'update':
const comment = getComment(gcComment.comments, msg.comment.id);
if (!comment) {
console.warn('更新対象のコメントが見つかりません:', msg.comment.id);
return;
}
if (comment) {
gcComment.execCommand(GC.InputMan.GcCommentCommand.UpdateCommentElement, {
comment: {
...comment,
content: msg.comment.content,
updateTime: new Date(msg.comment.updateTime)
}
});
}
break;
default:
return;
}
}
function handleReactionChange(msg) {
const comment = getComment(gcComment.comments, msg.commentId);
const reaction = getReactionInfo(msg.commentId, currentUser.id, msg.reactionInfo);
if (comment) {
gcComment.execCommand(GC.InputMan.GcCommentCommand.UpdateCommentElement, {
comment: {
...comment,
reactions: reaction
},
});
}
}
function getComment(comments, commentId) {
for (const comment of comments) {
if (comment.id == commentId) {
return comment;
}
if (Array.isArray(comment.replies)) {
const res = getComment(comment.replies, commentId);
if (res) return res;
}
}
return null;
}
function getReactionInfo(commentId, currentUserId, reactions) {
const reactionMap = new Map();
reactions.forEach((reaction) => {
if (!reactionMap.has(reaction.reactionChar)) {
reactionMap.set(reaction.reactionChar, {
reactionChar: reaction.reactionChar,
count: 0,
currentUserReacted: false,
});
}
const reactionInfo = reactionMap.get(reaction.reactionChar);
reactionInfo.count++;
if (reaction.userId == currentUserId) {
reactionInfo.currentUserReacted = true;
}
});
return Array.from(reactionMap.values());
}
});
socket.onでサーバー側で発火したイベントを検知しています。
・・・(中略)・・・
// サーバー側で定義されているcommentupdatedイベントの発火を検知します。
socket.on('commentupdated', (msg) => {
handleCommentsChange(msg);
});
// サーバー側で定義されているreactionupdatedイベントの発火を検知します。
socket.on('reactionupdated', (msg) => {
handleReactionChange(msg);
});
・・・(中略)・・・
イベント検知後、コメントコンポーネントのexecCommandメソッドを使用して、変更内容を別画面に反映します。これにより、画面やコンポーネントをリロードすることなく、変更をその他の接続している画面に反映します。
・・・(中略)・・・
function handleCommentsChange(msg) {
switch (msg.type) {
case 'add':
gcComment.execCommand(GC.InputMan.GcCommentCommand.AddCommentElement, {
comment: {
...msg.comment,
parentCommentId: String(msg.comment.parentCommentId) || null,
postTime: new Date(msg.comment.postTime),
updateTime: new Date(msg.comment.updateTime),
},
scrollIntoView: true
});
break;
・・・(中略)・・・
動作確認
更新が完了したらAPIを再起動し、ブラウザを2つ立ち上げ、コメントコンポーネントを組み込んだ画面にアクセスします。それぞれの画面でコメントを投稿すると、もう一方の画面に即座に変更が反映されます。
今回作成したサンプルは以下よりダウンロード可能です。
さいごに
今回はWebアプリケーションにコメント機能を組み込むことができるInputManJSの「コメントコンポーネント(GcComment)」でリアルタイム双方向通信を行うチャットアプリを作成する方法をご紹介しました。
なお、今回ご紹介したコメントコンポーネントの機能はほんの一部です。製品サイトでは、InputManJSのコメントコンポーネントの機能を手軽に体験できるデモアプリケーションやトライアル版も公開しておりますので、こちらもご確認ください。
また、ご導入前の製品に関するご相談、ご導入後の各種サービスに関するご質問など、お気軽にお問合せください。