JSでWebページのレスポンス待機メッセージを表示/非表示する

# はじめに

[前回](https://qiita.com/takahas_a/items/85abc1f5337aeb2afb71)、ChatGPTをSpringアプリケーションに組み込みました。しかし、ChatGPTのレスポンスを待機する間、ユーザーは待機中であることを認識しづらいという問題がありました。

そこで、**JavaScriptを使って待機メッセージを表示/非表示**させてみました。

### やりたいこと

主な処理の流れは、次の通りです。
1. ボタンをクリックすると、待機メッセージ「考え中…」が表示される（画像①）
    ※このとき、過去のレスポンスは消える。
2. レスポンスを受信すると、待機メッセージは消え、レスポンスが表示される（画像②）

▼ フォームに入力し、ボタンクリックした後（①）
![画像①](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654286/e96be44f-7ae8-2f54-f9c7-1e7167645b51.png "画像①")

▼ レスポンスを受信した後（②）
![画像②](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654286/8d9d0a11-0652-26c9-3679-88e2834849f8.png "画像②")


# 実装方針

- HTML上に、**待機メッセージ要素**と**レスポンス要素**を用意します。
- 要素が特定のクラスを持つ場合、**非表示に**します。非表示の処理はCSSで実装します。
- 要素へのクラスの追加・削除は、**イベント発生時**に行います。JSで実装します。

これらをまとめると、処理フローは次のようになります。

|  | ボタンクリック | → | レスポンス受信 | → |
| --- | --- | --- | --- | --- |
| 待機メッセージ要素 |  | 表示 |  | 非表示 |
| レスポンス要素 |  | 非表示 |  | 表示 |

上の表に、適切なイベントとクラス名・ID名を指定します。

|  | clickイベント |  | loadイベント |  |
| --- | --- | --- | --- | --- |
| 待機メッセージ要素（id="wait”, class="message”） | loadedクラス削除 | 表示 | loadedクラス追加 | #wait.loadedは非表示 |
| レスポンス要素（id=”resp”, class="message”） | loadedクラス削除 | #resp:not(.loaded)は非表示 | loadedクラス追加 | 表示 |
- `:not(クラス名)`を使用することで、特定のクラス名を含まない場合を指定しています。詳細は[こちら](https://developer.mozilla.org/ja/docs/Web/CSS/:not)をご確認ください。

# 実装する

実装方針に従い、実装したコードを以下に示します。

### 1．HTML

2つの要素を記述します。（サーバからレスポンスを受信するためにThymeleafを使用していますが、本記事とは直接的な関係が無い為、説明は省略します。）

```html
<!-- 待機メッセージ要素 -->
<p class="message" id="wait">考え中…</p>
<!-- レスポンス -->
<p class="message" id="resp" th:if="${apiMessage != null}" nl2br:text="${apiMessage}"></p>
```

### 2．JavaScript

2つのイベントに対する処理を記述します。2要素に対するクラスの追加・削除操作は、forEach文で行います。

```jsx
// ボタンクリックイベント
var button = document.getElementById("button");
button.addEventListener('click', () => {
	// 2要素を取得する
	var message =　document.querySelectorAll(".message");
	message.forEach(element => {
		// それぞれの要素に対してloadedクラスを削除
		element.classList.remove('loaded');
	});
});

// ロードイベント
window.addEventListener('load', () => {
	// 2要素を取得する
	var message = document.querySelectorAll(".message");
	message.forEach(element => {
		// それぞれの要素に対してloadedクラスを追加
		element.classList.add('loaded');
	});
});
```

### 3．CSS

要素を非表示にする処理を記述します。
上述の表をもとに、非表示の場合のクラス名・ID名をセレクタに指定します。

```css
#wait.loaded,
#resp:not(.loaded) {
	display: none; /* 非表示 */
}
```

以上が、JavaScriptを使った待機メッセージ表示処理のコードです。

### コード全体

[前回紹介したWebアプリケーション](https://qiita.com/takahas_a/items/85abc1f5337aeb2afb71)に、本記事の内容を追加したコードを示します。

<details><summary>form.html</summary>
    
```form.html
    <!DOCTYPE html>
    <html lang="ja" xmlns:th="http://www.thymeleaf.org" xmlns:nl2br="https://github.com/bufferings/thymeleaf-extras-nl2br">
    <head><!-- 省略 --></head>
    <body>
    	<div class="container">
    		<h1 class="m-5">ChatGPT API Demo</h1>
    
    		<form th:action="@{/chat}" method="post" class="form-floating m-5">
    			<textarea class="form-control" id="userMessage" name="userMessage" style="height: 100px"></textarea>
    			<label for="userMessage">Enter your message</label>
    			<button type="submit" class="btn btn-outline-primary mt-3" id="button">Generate Response</button>
    		</form>
    
    		<div class="m-5 p-3 bg-light rounded">
    			<h2 class="m-3">ChatGPT Response</h2>
    			<!-- 待機中 -->
    			<p class="m-3 message" id="wait">考え中…</p>
    			<!-- レスポンス -->
    			<p class="m-3 message" id="resp" th:if="${apiMessage != null}" nl2br:text="${apiMessage}"></p>
    		</div>
    	</div>
    
    	<style>
    		#wait.loaded,
    		#resp:not(.loaded) {
    			display: none; /* 非表示 */
    		}
    	</style>
    
    	<script>
    		var button = document.getElementById("button");
    		button.addEventListener('click', () => {
    			var message = document.querySelectorAll(".message");
    			message.forEach(element => {
    				element.classList.remove('loaded');
    			});
    		});
    
    		window.addEventListener('load', () => {
    			var message = document.querySelectorAll(".message");
    			message.forEach(element => {
    				element.classList.add('loaded');
    			});
    		});
    	</script>
    </body>
    </html>
```

</details>


# 【追記】戻るボタンを押すとどうなる？

※コメントを頂戴しましたので、追記させていだたきます！


レスポンス後（画像③）に戻るボタンを押した場合、「Generate Response」クリック前の画面に戻ります。（画像④）
待機メッセージは非表示、レスポンスは（前回の内容を）表示となっています。
※ただし、前回のレスポンスが存在しない場合は、表示されません。

▼ 「戻るボタン」クリック前（③）
![画像③](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654286/b973e351-fd61-df16-ceab-b9edb0384bf1.png "画像③")

▼「戻るボタン」クリック後（④）
![画像④](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654286/dcf5c4b1-ce1d-4a7e-8d13-07287f00c88e.png "画像④")


戻るボタンを押すと、loadイベントが発生する為、各要素の表示状態はレスポンス後と同じになるようです。

今回の目的である「**JSを使ってレスポンス待機メッセージを表示/非表示する**」という点において、**戻るボタン押下後は待機中では無い**為、待機メッセージは非表示であるべきです。
よって、上記の画面表示に問題はないと考えています。
（とはいえ、戻るボタンについてはすっかり失念しておりましたので、今後は注意を払いたいです。）


# おわりに

JavaScriptを使って待機メッセージの表示・非表示を管理することができました。
これにより、「ユーザーは待機中であることを認識しづらい」という問題を解決することができたかと思います。

お読みいただきありがとうございました。この記事が誰かのお役に立てることを祈っております。

今後、Ajaxを使った実装も試してみる予定です。（2024/01/11追記）


# 参考文献

- [:not() - CSS: カスケーディングスタイルシート | MDN (mozilla.org)](https://developer.mozilla.org/ja/docs/Web/CSS/:not)