# 非同期読み込みと複数のチャットルーム

> uchat.jsが読み込まれる前にイベントを登録する方法、1ページの複数のチャットルームにイベントを分けて登録する方法、SPAでチャットルームを設置・後片付けする方法、右下のフローティングウィジェットを設置する方法を説明します。

`uchat.js` は `async` で読み込むため、ページスクリプトとの実行順序は決まっていません。このページでは、読み込み順序に関係なくイベントを登録する方法、複数のチャットルームを扱う方法、SPAでチャットルームを付け外しする方法を扱います。

## 事前登録コード {#preload}

次のコードを**`uchat.js` より前に**入れます。`uchat.js` の準備ができる前に登録したイベントを `U.events` に保管しておき、準備ができたらそのまま使います。

```html
<script>U=window.U=window.U||{},U.events=U.events||[],U.chat=function(n){return{on:function(e,t){U.events.push([n,e,t])},off:function(e){for(var t=U.events.length;t>0;)U.events[--t][0]==n&&U.events[t][1]==e&&U.events.splice(t,1)}}};</script>
<script async src="https://client.uchat.io/uchat.js"></script>
```

`uchat.js` の準備ができると、`U.chat` は本物の関数に置き換わります。前と後の違いは1つです。

| | 準備前(事前登録コード) | 準備後 |
| --- | --- | --- |
| `on(name, callback)` | 保管 | 登録(すでに準備できたルームの `after.create` はすぐに呼び出し) |
| `off(name)` | 名前が**文字どおり同じ**ものだけを削除 | ラベル・イベント名で選んで削除(下記参照) |

準備前に `off('#label')` のようにラベルだけを渡すと、何も削除されません。いつ呼ばれるかわからない後片付けのコードでは、`on` で使った名前全体(`'after.message#alarm'`)をそのまま `off` に渡してください。この方法は準備前と準備後のどちらでも動作します。

## チャットルームを選ぶ {#select-room}

```js
U.chat('*').on('after.join', handler);      // ページのすべてのチャットルーム
U.chat('room-a').on('after.join', handler); // room-aだけ
U.chat('room-b').on('after.join', handler); // room-bだけ
```

- `U.chat('room-id')` はそのチャットルームだけに、`U.chat('*')` はページのすべてのチャットルーム(あとから追加されるチャットルームを含む)に登録します。
- 複数のIDを一度に渡すことはできません。それぞれ登録するか、`'*'` で登録してコールバック内で `room.id` によって分岐してください。
- チャットルームがすでに開いていれば、`U.chat('room-id')` はそのチャットルームの `room` オブジェクトを返します。まだなければ `on`・`off` だけを持つ登録用オブジェクトを返します。どちらの場合も `on`・`off` の使い方は同じです。→ [U.chat() と room オブジェクト](/ja/doc/3.2)

## ラベルと解除 {#labels}

イベント名の後ろに `#label` を付けると、あとでラベルで選んで解除できます。

```js
U.chat('*').on('after.message#alarm', onMessage);
U.chat('*').on('after.user.join#alarm', onJoin);

U.chat('*').off('#alarm');              // ラベルがalarmのものすべて
U.chat('*').off('after.message');       // after.messageすべて(ラベルに関係なく)
U.chat('*').off('after.message#alarm'); // 両方に一致するものだけ
```

`U.chat('*').off(...)` は、`'*'` で登録したものだけでなく、**チャットルームごとに登録したものまですべて**削除します。ほかのスクリプト(プラグイン、ほかのウィジェット)が登録したイベントまで削除しないよう、自分のコードのイベントには重ならないラベル(`#my-widget` など)を付け、ラベルで解除してください。

## after.createと準備のタイミング {#after-create}

`after.create` はチャットルームの画面(スキン)の準備ができたときに発生します。入室より先に来るので、[スキンUI API](/ja/doc/3.4)とチャンネルの登録はこの中で行います。

すでに準備ができたチャットルームに `after.create` を登録すると、待たずにすぐ1回呼び出されます。そのため、スクリプトの実行が遅れても取りこぼしません。

```js
U.chat('*').on('after.create#my-widget', function (room) {
	if (room.id !== 'my-room') return;
	// room.skin, room.pluginを使用
});
```

## タグの追加・削除と属性の変更 {#tag-lifecycle}

`uchat.js` はページの `<u-chat>` タグを監視しています。

- `<u-chat>` をページに入れるとチャットルームが開き、削除するとチャットルームが閉じます。
- チャットルームが閉じると、**そのチャットルームIDで登録したイベントも一緒に削除されます。**`'*'` で登録したイベントは残ります。タグを入れ直したときにもイベントが必要なら、もう一度登録するか `'*'` で登録してください。
- 同じ `room` IDのタグはページに1つだけ開きます。2つ目のタグは無視され、最初のタグを削除しても2つ目のタグが代わりに開くことはありません。
- `room`、`user_data` などの**属性の変更は検知しません。**値を変えるには、タグを削除して新しい属性で入れ直してください。

```js
function replaceRoom(container, roomId) {
	container.replaceChildren(); // 既存の<u-chat>を削除 → チャットルームが閉じる
	var tag = document.createElement('u-chat');
	tag.setAttribute('room', roomId);
	tag.style.cssText = 'display:block; width:100%; height:500px;';
	container.appendChild(tag); // 新しいチャットルームが開く
}
```

## SPAで使う {#spa}

SPAでは、画面にチャットルームが入るときに `uchat.js` を一度だけ読み込み、画面を離れるときに自分が登録したイベントを後片付けします。`<u-chat>` タグはフレームワークが削除すると、チャットルームも一緒に閉じます。

次の `uchat()` 関数は事前登録コードと同じ働きをするので、`uchat.js` より先に実行されても問題ありません。

```js
// uchat.js — フレームワークに関係なく使えるヘルパー
export function uchat(roomId) {
	const U = (window.U = window.U || {});
	U.events = U.events || [];
	if (!U.chat) {
		U.chat = (n) => ({
			on: (e, t) => U.events.push([n, e, t]),
			off: (e) => {
				for (let t = U.events.length; t > 0; )
					U.events[--t][0] == n && U.events[t][1] == e && U.events.splice(t, 1);
			}
		});
	}
	if (!document.querySelector('script[data-uchat]')) {
		const s = document.createElement('script');
		s.src = 'https://client.uchat.io/uchat.js';
		s.async = true;
		s.dataset.uchat = '';
		document.head.appendChild(s);
	}
	return U.chat(roomId);
}
```

React:

```jsx
import { useEffect } from 'react';
import { uchat } from './uchat';

export default function ChatRoom({ roomId }) {
	useEffect(() => {
		const name = 'after.message#my-widget-' + roomId;
		uchat('*').on(name, (room, data) => {
			if (room.id !== roomId) return;
			console.log(data.user.nick, data.content);
		});
		return () => uchat('*').off(name);
	}, [roomId]);

	// keyを変えるとタグが新しく作られるので、別のチャットルームに切り替わる
	return <u-chat key={roomId} room={roomId} style={{ display: 'block', width: '100%', height: '500px' }} />;
}
```

- イベントは `'*'` で登録し、`room.id` で分岐します。チャットルームIDで登録したイベントはタグが削除されるときに一緒に削除されるため、再マウント時に順序が乱れません。
- 後片付けのときは、`on` で使った名前全体を `off` に渡します。`uchat.js` の準備がまだでも削除されます。
- `room` 属性だけを変えてもチャットルームは切り替わらないので、`key` でタグを新しく作ります。

Vueでは `onMounted` で `on`、`onUnmounted` で `off` を呼び、`u-chat` をカスタム要素として登録します([5分で設置](/ja/doc/1.2#frameworks)参照)。Svelteでは `onMount` で `on` を登録し、返す後片付け関数で `off` を呼びます。チャットルームを切り替えるときは、Vueは `:key`、Svelteは `{#key roomId}` ブロックでタグを新しく作ります。

## フローティングウィジェット {#widget}

チャットルームをページ右下のボタンとして設置するには、npm パッケージ `@u-chat/embed` のウィジェットを使います。閉じているときは新着メッセージをプレビューと未読数で知らせ、押すとチャット画面が開きます。管理ページの設置コード画面の **フローティングウィジェット** タブでもこのコードを作れ、下の属性と位置・サイズはその画面の **ウィジェット設定** で選べます。

バンドラーを使わないサイト(HTML、グヌボード、WordPress など)は `<script>` 1行で読み込みます。`uchat.js` をすでに設置したページにそのまま追加できます。

```html
<script src="https://cdn.jsdelivr.net/npm/@u-chat/embed@latest/dist/widget.global.js"></script>
<u-chat-widget launcher-text="現在 {user_count}人">
	<u-chat room="my-room"></u-chat>
</u-chat-widget>
```

jsDelivr の代わりにチャットサーバーから読み込むには `https://client.uchat.io/widget.js` を使います。独立サーバーはそのサーバーの `widget.js` を使い、ウィジェットが同じサーバーの `uchat.js` に接続するので `src` 属性は不要です。

React(`npm install @u-chat/react`):

```jsx
import { UchatWidget } from '@u-chat/react';

export default function App() {
	return <UchatWidget room="my-room" launcherText="現在 {user_count}人" />;
}
```

Svelte 5(`npm install @u-chat/svelte`):

```svelte
<script>
	import { UchatWidget } from '@u-chat/svelte';
</script>

<!-- 波かっこが Svelte の式として読まれないよう、文字列を {'...'} で渡します -->
<UchatWidget room="my-room" launcherText={'現在 {user_count}人'} />
```

Vue 3(`npm install @u-chat/vue`)。`isCustomElement` の設定は不要です。

```vue
<script setup>
import { UchatWidget } from '@u-chat/vue';
</script>

<template>
	<UchatWidget room="my-room" launcher-text="現在 {user_count}人" />
</template>
```

ウィジェットの属性(React・Svelteでは `launcherText` のようなキャメルケース):

| 属性 | 説明 |
| --- | --- |
| `launcher-text` | ボタンの文字。`{user_count}` は接続者数、`{unread_count}` は未読数に置き換わります |
| `variant` | `bubble`(既定、丸いボタン)、`bar`(下端の長いバー。開くと下からせり上がります)または `side`(右端のサイドバー。最初から画面の高さいっぱいに開いてページ本文を左に押し、たたむと右端に縦のタブだけが残ります。768pxより狭い画面はたたんだ状態で始まり、開くと上に重なります) |
| `launcher-color`, `launcher-image` | ボタンの背景色、背景画像のURL(画像が優先) |
| `panel-title` | 開いた画面の上のタイトル |
| `preview-duration` | 新着メッセージのプレビューを表示する時間(ms、既定 5000) |
| `lang` | ウィジェットのボタン・案内文の言語(`ko`、`en`、`ja`)。なければ内側の `<u-chat lang>` → ページの `<html lang>` → ブラウザーの言語の順で、それ以外の言語は英語 |
| `src` | `uchat.js` のURL。独立サーバーはそのサーバーのURLを入れます |

- 会員連携のルームは、内側の `<u-chat>` に `user_data` を入れます(React・Svelte・Vue は `userData`)。設置コード画面で **設置の形 → フローティングウィジェット** を選ぶと、署名入りのウィジェットコードが出ます。
- ページを開くとすぐチャットルームに接続します。そのため閉じていてもプレビューと接続者数が表示されます。過去の会話(`after.message` の `past`)と自分のメッセージは未読数に入りません。
- 画面の `×` で隠すと接続を切り、このチャットルームに限って隠した状態を覚えます。右下に小さなオンにするボタンが残ります。
- スクリプトからは `open()`、`close()`、`toggle()`、`maximize()`、`restore()`、`hide()`、`show()`、`destroy()` を呼びます。React・Vue・Svelte ではコンポーネントに `ref`(Svelte は `bind:this`)を付けて同じ名前で呼びます。
- `uchat.js` を読み込めないと、ウィジェットに `uchat-widget-error` イベントが届きます(`event.detail` がエラー)。

タグではなくスクリプトで作るときは `createUchatWidget(入れる要素, 設定)` を使います。1行のスクリプトで読み込んだ場合は `window.UchatEmbed`、npm の場合は `@u-chat/embed/widget` にあります。

```js
const widget = UchatEmbed.createUchatWidget(document.body, {
	chat: { room: 'my-room', user_data: signedUserData },
	widget: { launcherText: '現在 {user_count}人', variant: 'bar' },
});
widget.addEventListener('uchat-widget-error', (event) => console.error(event.detail));
widget.open();
```

位置と大きさは CSS 変数で変えます。

```css
u-chat-widget {
	--uchat-widget-right: 20px;
	--uchat-widget-bottom: 20px;
	--uchat-widget-panel-width: 380px;
	--uchat-widget-panel-height: 640px;
	--uchat-widget-side-width: 360px;
	--uchat-widget-accent: #ff7e25;
	--uchat-widget-z-index: 2147483000;
}
```
