Astroで動的DOMを簡単スマートに扱うコツ
UI開発者 齋藤サイト構築を進める中で、「ある操作や状態変化に応じてDOMが動的に変わる」ことが求められる場面がよくあります。
例えば、
- フォームの入力内容に応じてスタイルを即時変更する
- ボタン押下時に要素をDOM上に動的追加する
- 外部から取得したJSONデータに基づいてリスト項目を生成する
などが挙げられます。従来のJavaScript(フレームワークを使わない場合)では、これらの動的処理はイベントリスナーとDOM操作を直接結びつけて実装されます。
下記は、ボタン押下でコンテナ内に要素を追加するだけのシンプルな例です。
<button id="button">要素を追加</button>
<div id="container"></div>
const container = document.getElementById('container')
const button = document.getElementById('button');
button.addEventListener('click', function() {
// 新しい要素を作成
const newElement = document.createElement('div');
newElement.textContent = '新しい要素';
// コンテナーに追加
container.appendChild(newElement);
});
できそうでできないこと
Astroでサイト構築している場合に、例えば下記のような構造のErrorMessage
というAstroコンポーネントがあったとします。
<p class="error-message">
<strong><slot /></strong>
</p>
<style>
.error-message {
color: red;
font-weight: bold;
}
</style>
これを下記のようにページ実装し
<ErrorMessage>入力必須の項目が残っています</ErrorMessage>
ビルドすると、このようになります。
<p class="error-message" data-astro-cid-xxxxxxxx>
<strong>入力必須の項目が残っています</strong>
</p>
<!-- CSS は .error-message[data-astro-cid-xxxxxxxx] に適用される -->
Astroコンポーネントはビルド時に静的なHTMLを生成し、CSSやJavaScriptはコンポーネントのスコープ(data-astro-cid-xxxxxxxx
)に関連付けられます。
そのため、HTMLとして展開された後(ビルド後)に発生するクリックイベントなどに連動した処理の中では、直接Astroコンポーネントを取得したり、DOM挿入したり、コンポーネントのスコープに関連付けられたCSSやJavaScriptを使用することはできません。
先ほどのJavaScriptに当てはめると、
button.addEventListener('click', function() {
// NG: 新しい要素としてErrorMessageコンポーネントを作成
const newElement = document.createElement('ErrorMessage');
newElement.textContent = '新しい要素';
// NG: ErrorMessageコンポーネントのスタイルを別の要素に適用
otherElement.classList.add('error-message');
// コンテナーに追加
container.appendChild(newElement);
});
このような書き方をすることはできません。
例えば、Astroコンポーネントと同等のHTMLをクライアントサイドで動的に挿入したり、グローバルスタイルを適用するなど、無理矢理に何とかする方法はありますが、コンポーネントの構造変更のたびにCSSやJavaScriptも同時に編集する必要があるため、コンポーネント設計によるスコープ管理のメリットを享受することができず実用的とは言えません。
button.addEventListener('click', function() {
// 新しい要素としてErrorMessageコンポーネントと同等のHTMLを作成
const newElement = document.createElement('p');
newElement.innerHTML = '<strong>新しい要素</strong>';
// ErrorMessageコンポーネントと同等のグローバルスタイルを適用
newElement.classList.add('global-error-message');
// コンテナーに追加
container.appendChild(newElement);
});
template要素を使ったアプローチ
このとき役立つのが、HTMLのtemplate
要素です。以下のようにAstroコンポーネントをtemplate
要素でラップします。
<template id="error-message">
<ErrorMessage>入力必須の項目が残っています</ErrorMessage>
</template>
この状態でビルドすると、以下のようにスコープ化されたHTMLが展開されます。
<template id="error-message" data-astro-cid-xxxxxxxx>
<p class="error-message" data-astro-cid-xxxxxxxx>
<strong>入力必須の項目が残っています</strong>
</p>
</template>
そのため、クライアントサイドで複雑なDOM構造を組み立てたり、グローバルスタイルを別途用意して適用したりする必要がなく、必要なタイミングでtemplate
要素の内容をcloneNode()
で複製して任意の場所に挿入するだけで、Astroコンポーネントのスコープ内のCSSやJavaScriptをまるごと再利用することが可能になります。
const tpl = document.querySelector('template');
const clone = tpl.content.cloneNode(true);
document.body.prepend(clone);
おわりに
今回紹介したtemplate
要素を使った方法を活用することで、Astroのビルド時に生成されるHTML構造やスコープ内のCSSやJavaScriptを正確に複製することができるため、クライアントサイドでの動的なDOM操作をスムーズに行うことができます。
これにより、開発者はAstroのメリットを損なわずに、柔軟かつ効率的にユーザーインタラクションに対応できるため、より保守性の高いモダンなWeb開発が可能になります。ぜひ、動的なUIが必要な場面ではこのアプローチを試してみてください。