Astroコンポーネントの実装パターンメモ

投稿日
2024年12月30日
更新日
2024年12月31日
Astroコンポーネントの実装パターンメモの見出し画像
目次

背景

本ブログはAstroで作っていますが、色々なパターンでAstroコンポーネントの実装を試みる機会があったので備忘録として残しておきます。

バージョン情報

astro
5.1.0
typescript
5.7.2

コンポーネント実装パターン

簡単のため、想定するコンポーネントは下図のようなシンプルなボックスです。 呼び出し元で、タイトルパラグラフを変更できるコンポーネントを目指します。 タイトルが指定されていない場合は、上部のタイトルバーも非表示にします。

2024-12-30-Component1

出力したい中身をシンプルです。
(tailwindを使っていますが、書き換えたいところはinnerText部のみなので気にしなくてOKです)

<!-- tailwindを使っています -->
<div class="rounded-md border">
<h2 class="bg-slate-200 px-2 py-1 text-lg text-black">タイトル</h2>
<div class="px-3 py-2">コンテンツ</div>
</div>

実現方法は大体4つくらいに分かれるでしょう。

  • Propsだけで対応
  • ContentのみSlotで対応
  • デフォルトSlot+名前付きSlotで対応
  • 両方とも名前付きSlotで対応

Propsだけで対応

Self-closingなタグで呼び出すことが可能です。

Astroコンポーネント

PropsBox.astro
---
interface Props {
title?: string;
content: string;
}
const { title, content } = Astro.props;
---
<div class='rounded-md border'>
{title && <h2 class='bg-slate-200 px-2 py-1 text-lg text-black'>{title}</h2>}
<div class='px-3 py-2'>{content}</div>
</div>

呼び出し

<PropsBox title='Props指定' content='Content指定' />
Propsに動的にHTMLタグを渡すことも可能

Fragmentを使うことでプレインテキストからHTMLを表示することも可能です。

ただし、Propsへ手入力でHTMLを引き渡すくらいなら、後述するSlotで対応するほうが断然良いです。エディタのハイライトやリントが効きますので品質を維持できます。

---
interface Props {
title?: string;
content: string;
}
const { title, content } = Astro.props;
---
<div class='rounded-md border'>
{
title && (
<h2 class='bg-slate-200 px-2 py-1 text-lg text-black'>{title}</h2>
)
}
<div class='px-3 py-2'><Fragment set:html={content} /></div>
</div>

ContentのみSlotで対応

Propsで受け取っていたcontent<slot />に変えるだけです。

Astroコンポーネント

SingleSlotBox.astro
---
interface Props {
title?: string;
}
const { title } = Astro.props;
---
<div class='rounded-md border'>
{title && <h2 class='bg-slate-200 px-2 py-1 text-lg text-black'>{title}</h2>}
<div class='px-3 py-2'><slot /></div>
</div>

呼び出し

<SingleSlotBox title='Props指定'>
中身は自由に<b>HTMLタグ</b>や別の<b>コンポーネント</b>を入れられる。
</SingleSlotBox>
{/* slotを省くと何も含まれない状態でレンダリング */}
<SingleSlotBox title='Props指定' />
呼び出し元にデフォルトslotの指定を強制させる

下記のように書くとデフォルトスロットが指定されていないことをTypeScriptのLintエラーで気づくことができます。
https://docs.astro.build/ja/guides/typescript/

---
interface Props {
title?: string;
children: any;
}
---

デフォルトSlot+名前付きSlotで対応

<slot name="スロット名">で名前付きSlotを作成できます。

スロット有無の判別

AstroのランタイムAPIにAstro.slots.has()メソッドがあります。引数にスロット名を入れることで、Slotがセットされているかどうかを判定できます。 なお、デフォルトSlotはdefaultという名前が自動的に付きます。

DoubleSlotBox.astro
<div class='rounded-md border'>
{
Astro.slots.has('default') && (
<h2 class='bg-slate-200 px-2 py-1 text-lg text-black'>
<slot />
</h2>
)
}
<div class='px-3 py-2'><slot name='content' /></div>
</div>

呼び出し側のスロット名指定

HTMLタグや別コンポーネントの属性にslot = "content"を追記すればOKです。 ただし呼び出し元では、そのコンポーネントの直接の子ノードがslotの評価対象になることので注意しましょう。 また、私の環境(v5.1)では同名スロットが複数ある場合、それらは連結されて表示されました。

{/* デフォルトスロット無し */}
<DoubleSlotBox>
<div class='bg-amber-200' slot='content'>
<div>コンテント用のスロット</div>
</div>
</DoubleSlotBox>
{/* デフォルトスロットをテキストのみで */}
<DoubleSlotBox>
タイトル用スロット
<div class='bg-amber-200' slot='content'>
<div>コンテント用のスロット</div>
</div>
</DoubleSlotBox>
{/* デフォルトスロット混在 */}
<DoubleSlotBox>
<div slot='default'>タイトル用スロット</div>
<div class='bg-amber-200' slot='content'>
<div>コンテント用のスロット</div>
</div>
<div>
<div slot='content' class='bg-cyan-200'>これはデフォルトスロット扱い</div>
</div>
<div class='text-sm italic'>(タイトル用スロット)ここの部分はどうなる?</div>
</DoubleSlotBox>

実行結果

2024-12-30-DoubleSlotBox実行結果

両方とも名前付きSlotで対応

Astroコンポーネント

hasメソッドのチェック対象をdefaultからtitleに変更して、スロット名を追加するだけです。

DoubleNamedSlotBox.astro
<div class='rounded-md border'>
{
Astro.slots.has('title') && (
<h2 class='bg-slate-200 px-2 py-1 text-lg text-black'>
<slot name='title' />
</h2>
)
}
<div class='px-3 py-2'><slot name='content' /></div>
</div>

呼び出し

<DoubleNamedSlotBox>
<div slot='title'>タイトル用スロット</div>
<div class='bg-amber-200' slot='content'>
<div>コンテント用のスロット</div>
</div>
</DoubleNamedSlotBox>

【番外】コンポーネント拡張に便利なComponentProps型

ちょっと本筋とは逸れますが、Propsで値を受け取る独自コンポーネントを拡張したいときにはComponentPropsが便利です。 同じPropsタイプまたはそれを拡張したタイプの実装を簡単に実現できます。

元のコンポーネント

PropsBox.astro
---
interface Props {
title?: string;
content: string;
}
const { title, content } = Astro.props;
---
<div class='rounded-md border'>
{title && <h2 class='bg-slate-200 px-2 py-1 text-lg text-black'>{title}</h2>}
<div class='px-3 py-2'>{content}</div>
</div>

新しく作るコンポーネント

SecondPropsBox.astro
---
import type { ComponentProps } from 'astro/types';
import PropsBox from './PropsBox.astro';
// PropsBoxのタイプを参照
type Props = ComponentProps<typeof PropsBox>;
const props = Astro.props;
---
<div>
<PropsBox {...props} />
<hr />
<div>{props.title} - {props.content}</div>
</div>

型定義が面倒にならなくて大変助かります。もちろんこのタイプはextends等で拡張できます。

関連記事