Fresh(Deno) で Bootstrap 5 をプラグインで使う

Deno Advent Calendar 2023 12 日目の記事です。

Fresh 1.6 が公開されて、プラグインで link 要素が設定できるようになりました。
このことで、CSS をプラグインで設定できるようになりました。

ここで考えたのが、Bootstrap を Fresh にプラグインで設定できそうだったので試してみました。
Bootstrap の中でも js も入れて動くものにを確認したかったので、ツールチップの動作にフォーカス。
js も必要な Bootstrap の機能は同じように設定すれば動くでしょう。

参考

プラグインを使わないで bootstrap を入れる

プラグインを使わないので、_app.tsx に設定を追加する。

routes/_app.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { type PageProps } from "$fresh/server.ts";
export default function App({ Component }: PageProps) {
return (
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>fresh-bootstrap-</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"
/>
<script src="/script.js" />
</head>
<body>
<Component />
</body>
</html>
);
}

これで、Bootstrap の大部分の機能(特に CSS 周り)は動く。
ツールチップは、ページを読み込んだタイミングで、js を動かす必要がある。
これは、script.js を用意して対応する。

static/script.js
1
2
3
4
5
6
7
8
9
10
window.addEventListener("load", function () {
const tooltipTriggerList = document.querySelectorAll(
'[data-bs-toggle="tooltip"]',
);
[...tooltipTriggerList].forEach((tooltipTriggerEl) =>
new bootstrap.Tooltip(tooltipTriggerEl)
);

window.bsTooltip = bootstrap.Tooltip;
});

これで SSR された部分に対して+初期から表示されている islands に対してはツールチップも設定できている。

islands によって動的に追加されたエレメントについては、ツールチップは動かない。
また、最初から表示されていた islands も一度隠される処理を挟むと、再表示しても動かない。

なので、以下のようにした。

islands/SwitchCard.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { useEffect, useRef, useState } from "preact/hooks";

interface BsTooltip {
new (element: HTMLElement): {
dispose: () => void;
};
}

interface Window {
bsTooltip: BsTooltip;
}
declare const window: Window;

export default function SwitchCard() {
const tooltipTargetRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
if (!window.bsTooltip || !isOpen || !tooltipTargetRef.current) return;

const tooltip = new window.bsTooltip(tooltipTargetRef.current);

return () => {
tooltip?.dispose();
};
}, [isOpen]);

return (
<div class="card" style="width: 18rem;">
<img
src="/logo.svg"
class="card-img-top"
onClick={() => setIsOpen(!isOpen)}
/>
<div class="card-body">
<h5 class="card-title">カードタイトル</h5>
{isOpen && (
<>
<p class="card-text">
カードのテキスト
</p>
<a
href="#"
ref={tooltipTargetRef}
data-bs-toggle="tooltip"
data-bs-title="ツールチップ"
>
islans上のリンク
</a>
</>
)}
</div>
</div>
);
}

ツールチップを含んだテキストの表示状態に基づいてツールチップ機能をつけ外ししている。
最初は、body 全体を対象にした MutationObserver を使った DOM の変更検知での再設定も試みた。
が、1 回の表示切替で複数回発火したりとしたので、上記のように対応した。
上記のように対応したのもあり、script.jswindow.bsTooltip = bootstrap.Tooltip; を記述している。

動かすとこんな感じ。

プラグインを使って bootstrap を入れる

以下のディレクトリ構成で用意する。

1
2
3
4
5
6
├─ plugins
└─ bootstrap_plugin
├─ bootstrap_loader.ts
├─ mod.ts
├─ plugin.ts
└─ types.ts
plugins/bootstrap_plugin/mod.ts
1
2
export { BootstrapPlugin } from "./plugin.ts";
export type { BsTooltip, WindowWithBsTooltip, } from "./types.ts";
plugins/bootstrap_plugin/types.ts
1
2
3
4
5
6
7
8
9
export interface BsTooltip {
new (element: HTMLElement): {
dispose: { (): void };
};
}

export interface WindowWithBsTooltip extends Window {
bsTooltip: BsTooltip;
}
plugins/bootstrap_plugin/plugin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Plugin } from "$fresh/server.ts";

export function BootstrapPlugin(): Plugin {
return {
name: "bootstrap-plugin",
entrypoints: {
bootstrap_loader: import.meta.resolve(`./bootstrap_loader.ts`),
},
render(ctx) {
ctx.render();
return {
links: [{
rel: "stylesheet",
href:
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css",
integrity:
"sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM",
crossOrigin: "anonymous",
}],
scripts: [
{
entrypoint: "bootstrap_loader",
state: {},
},
],
};
},
};
}

プラグインのドキュメントを見るとあまり記述は無いのだが、このように使える。

Fresh 1.6 から登場した link 要素の設定は、render の中で設定する。
参考としては、twind プラグインの記述の紹介があるので、これを踏まえる。

scripts に登録するのが、プラグイン化する前の script.js に当たる。
設定できる要素は、entrypoints に記載した名称である必要がある。

plugins\bootstrap_plugin\bootstrap_loader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import * as bootstrap from "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js";

import type { BsTooltip } from "./types.ts";
declare global {
interface Window {
bsTooltip: BsTooltip;
}
}

export default function bootstrapLoader() {
window.addEventListener("load", function () {
const tooltipTriggerList = document.querySelectorAll(
'[data-bs-toggle="tooltip"]',
);
[...tooltipTriggerList].forEach((tooltipTriggerEl) =>
new bootstrap.Tooltip(tooltipTriggerEl)
);

window.bsTooltip = bootstrap.Tooltip;
});
}

_app.ts に記述した際には、トランスパイルがされない前提で記述していた。
こちらの機能を使うとトランスパイルの対象になるので、TypoScript で書きたいならこの点でも有利になる。

islands/SwitchCard.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useEffect, useRef, useState } from "preact/hooks";
import { WindowWithBsTooltip } from "bootstrap_plugin/mod.ts";
export declare const window: WindowWithBsTooltip;
// 型はプラグインから取得

export default function SwitchCard() {
const tooltipTargetRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
if (!window.bsTooltip || !isOpen || !tooltipTargetRef.current) return;

const tooltip = new window.bsTooltip(tooltipTargetRef.current);

return () => {
tooltip?.dispose();
};
}, [isOpen]);

return (
<div class="card" style="width: 18rem;">
 {/* 省略 */}
</div>
);
}

動作させると、非プラグインの時と同様に動作する。
1 点違ったのは、新しく js が取得されていることがわかる。
アクセス先は次の通り。
/_frsh/js/[ハッシュ値]/plugin-bootstrap-plugin-bootstrap_loader.js

バンドルされているのでそのままではないが、plugins\bootstrap_plugin\bootstrap_loader.ts が含まれているのが確認できる。


以上、Bootstrap を Fresh にプラグインで組み込みました。

ツールチップの初期化を、bootstrap_loader.ts の中で行わせたのだが、js を使う Bootstrap の機能はほかにもある。
window.bootstrap のように生やしてしまうのが、寄り広範な機能には対応できる。
render の scripts には、state を設定できるのでプラグインの引数として有効化したい機能を渡して、選択形式にできるはず。

では。