Materialize Tooltips のメッセージを動的に変更する

# 1. はじめに

Web サーバから非同期で取得したメッセージを [Materialize](https://materializecss.com/) の [Tooltips](https://materializecss.com/tooltips.html) で表示しようとしてハマったので、やり方をここに記載します。

# 2. 環境

Materialize 1.0.0
jQuery 3.7.1

# 3. やりたかったこと

特定の要素にマウスポインタが置かれた時に、Web サーバーからデータを取得し、それを Materialize のツールチップで表示しようとしました。
その際、ツールチップのメッセージを動的に変更する方法が分からず、手探りで動きを確認しながら実装を行いました。

# 4. デモ

「ここにマウスポインタを乗せてください」の上にマウスポインタを置くと、ローディングスピナーを載せたツールチップが表示され、2秒後にツールチップの表示内容が置き換わります。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
	<meta charset="UTF-8" />
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>Materialize Tooltips - dynamic display demo</title>
	<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
	<script src="script.js"></script>
	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
	<link rel="stylesheet" href="style.css">
<head>
</head>
<body>
	<div id="demo-container">
		<span
			class="btn tooltipped dynatp"
		 	data-tooltip="<div><div class='preloader-wrapper small active'><div class='spinner-layer spinner-blue-only'><div class='circle-clipper left'><div class='circle'></div></div><div class='gap-patch'><div class='circle'></div></div><div class='circle-clipper right'><div class='circle'></div></div></div></div></div>">
			ここにマウスポインタを乗せてください
		</span>
	</div>
</body>
</html>
```

```javascript:script.js
$(() => {
    $('.tooltipped').tooltip();

	$(document).on('mouseover', '.dynatp', (event) => {
		const dis = $(event.target);

		// サーバーからAjaxでデータ取得(ここではタイマーで代用)
		setTimeout(() => {
			const tltp = $(dis.attr('data-tooltip'));
			tltp.empty();
			tltp.append('<div>[動的メッセージデモ]<br>これは動的に設定されたメッセージです。</div>');
			dis.attr('data-tooltip', tltp.prop('outerHTML'));

			// ツールチップ表示中判定
			const isOpen = dis[0].M_Tooltip && dis[0].M_Tooltip.isOpen;
			if (isOpen) {
				dis.tooltip('close');
			}

			// ツールチップの再構成
            dis.tooltip('destroy');
			dis.tooltip();

			if (isOpen) {
				dis.tooltip('open');
			}
		}, 2000);
	});
});
```

```css:style.css
#demo-container {
	margin: 20px;
}
```

動きの確認は↓
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="MWdydYQ" data-pen-title="Materialize Tooltips - dynamic display demo" data-user="hataya_koji" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/hataya_koji/pen/MWdydYQ">
  Materialize Tooltips - dynamic display demo</a> by Koji Hataya (<a href="https://codepen.io/hataya_koji">@hataya_koji</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# 5. 解説

index.html と script.js について解説します。
※style.css は説明を割愛します。

### 5.1 index.html

ここでの注目箇所は、ツールチップが適用される `<span>` 要素の data-tooltip 属性になります。
この属性には HTML 文字列をセットしていますが、内容はローディングスピナーの定義です。
これにより、表示対象データの取得が完了するまで、ツールチップにはローディングスピナーが表示されるようになります。
ちなみにローディングスピナーは、[Materialize](https://materializecss.com/preloader.html) が提供しているものを使用しています。

```html:data-tooltip 属性の設定値（※見やすいように改行とインデントを入れました）
<div>
    <div class='preloader-wrapper small active'>
        <div class='spinner-layer spinner-blue-only'>
            <div class='circle-clipper left'>
                <div class='circle'></div>
            </div>
            <div class='gap-patch'>
                <div class='circle'></div>
            </div>
            <div class='circle-clipper right'>
                <div class='circle'></div>
            </div>
        </div>
    </div>
</div>
```

### 5.2 script.js

```javascript
$('.tooltipped').tooltip();
```
CSS クラス "tooltipped" を持つ要素に対し、ツールチップを適用しています。

```javascript
$(document).on('mouseover', '.dynatp', (event) => {
```
CSS クラス "dynatp" を持つ要素のマウスオーバーイベントを拾って処理しています。
この要素は、上記のツールチップを適用された要素("tooltipped")と同じものになりますが、ツールチップ適用対象は "tooltipped"、動的メッセージ対象は "dynatp" と区別して使用しています（ツールチップ適用対象だが、動的メッセージ対象ではない要素があると困るので）。

```javascript
const tltp = $(dis.attr('data-tooltip'));  // (1)
tltp.empty();  // (2)
tltp.append('<div>[動的メッセージデモ]<br>これは動的に設定されたメッセージです。</div>');  // (3)
dis.attr('data-tooltip', tltp.prop('outerHTML'));  // (4)
```
(1) data-tooltip 属性の設定値(HTML文字列)を jQuery オブジェクトに変換し、tltp 変数(定数)に代入しています。
(2) `<div>～</div>` の内側の定義(ローディングスピナー)を消去しています。
(3) `<div>～</div>` の内側に新規メッセージを追加しています。
(4) 書き換えた HTML 定義を対象要素の data-tooltip 属性に上書きしています。外側の `<div>` 要素も含めるため、～.prop('outerHTML') で HTML 文字列を出力しています。

```javascript
// ツールチップ表示中判定
const isOpen = dis[0].M_Tooltip && dis[0].M_Tooltip.isOpen;
if (isOpen) {
    dis.tooltip('close');
}

// ツールチップの再構成
dis.tooltip('destroy');
dis.tooltip();

if (isOpen) {
    dis.tooltip('open');
}
```
実は data-tooltip 属性に新たな内容を書き戻しても、ツールチップに表示される内容は変わりません。
したがって、
`dis.tooltip('destroy');`
でツールチップを破棄して
`dis.tooltip();`
で作り直しています。
これが今回のキモです。

その他、ツールチップの表示中に内容が作り直されると、勝手にツールチップが閉じられたようになってしまうため、ツールチップが表示中かどうかの判定と再表示処理を入れています。

# 6. おわりに

今回のデモでは必要最小限のコードしか記載していないため、私が実際に使用してるコードには少し処理が加えられています。

たとえば、運用では Web サーバーから非同期で取得したデータをツールチップに表示していますが、データ取得後は同処理が実行されないようにガードしています。
また、動的メッセージ表示対象の要素が複数あり、各々取得するデータが異なるため、それを識別するキーを `data-xxx` 属性で持つようにしています。

いずれも簡単に実現できますので、興味のある方は試してみてください。

# 7. 参考にさせていただいた情報

Documentation - Materialize
https://materializecss.com/