📄

[TIPS] Svelte でスニペットのリストを渡したいとき

2024/09/21に公開

前置き

下記のイシューのコメントママの話なのですが tips としてまとめておきます。

問題

Svelte (5)で UI コンポーネントを作成するとき、コンポーネントにスニペットのリストを渡したいときがあります。わかりやすい例としてはタブバーに対するタブ要素などです。ここで実装にあたり問題となるのが Svelte では同名のスニペットは作成できないことです。上記のイシューでは同名のスニペットを定義可能にする提案がなされていましたが、(本人が提示していたのも含め)現状の仕様のままでもやり方はいくつかあります。

<TabBar>
  {#snippet tab()}
    tab1
  {/snippet}
  <!-- ↓エラーになる -->
  {#snippet tab()}
    tab2
  {/snippet}
</TabBar>

対応方法

1. プロパティで渡す

スニペットはプロパティとして渡すこともできるため、コンポーネントの外側でスニペットを定義してコンポーネントに渡す、という方法がまず一つあります。

<!-- この場合スニペットの名前はなんでもよい -->
{#snippet tab1()}
  tab1
{/snippet}
{#snippet tab2()}
  tab2
{/snippet}
<TabBar tabs={[ tab1, tab2 ]}></TabBar>
TabBar.svelte
<script lang="ts">
  import type { Snippet } from "svelte"

  let { tabs }: { tabs: Snippet[] } = $props()
</script>
<div>
  {#each tabs as tab }
    {@render tab()}
  {/each}
</div>

これでも出来はしますが、できればスニペットは内側に閉じたいですね。

2. コンポーネント側で tabN をリストとして扱う

スニペット名がかぶらなければそれぞれのタブを定義することはできるので、以下のようにスニペットに連番を振りコンポーネント側でリストにする方法があります。

<TabBar>
  {#snippet tab1()}
    tab1
  {/snippet}
  {#snippet tab2()}
    tab2
  {/snippet}
</TabBar>
TabBar.svelte
<script lang="ts">
  import type { Snippet } from "svelte"

  let props: { [key: `tab${number}`]: Snippet } = $props()

  // 若干雑ですがイメージはこんな感じということで
  let tabs: Snippet[] = $derived(
      Object.keys(props).filter(key => key.startsWith("tab"))
      .map(key => props[key])
  )
</script>
<div>
  {#each tabs as tab }
    {@render tab()}
  {/each}
</div>

連番を振るのが若干手間ですね

3. コンポーネントからインデックスや名前を渡す

イシューのコメントにて紹介されていた、リストの要素に対して単一のスニペットを用意する方法です。

<script lang="ts">
  const tabs = ["tab1", "tab2]
</script>
<TabBar {tabs}>
  {#snippet tabTemplate(name, i)}
    {#if i == 0}
      <Icon />{ name }
    {:else}
      { name }
    {/if}
  {/snippet}
</TabBar>
TabBar.svelte
<script lang="ts">
  import type { Snippet } from "svelte"

  let {
    tabs,
    tabTemplate,
  }: {
    tabs: string[]
    tabTemplate: Snippet<[string, number]>
  } = $props()
</script>
<div>
  {#each tabs as tab, i }
    {@render tabTemplate(tab, i)}
  {/each}
</div>

たしかに、連続した要素であれば基本的に同じ要素構成になっている可能性が高く、仮に要素により大きく内容が異なる場合でも上記のコード例のようにインデックスで分岐可能なので、この方法でいいなとなりました。元々、スニペット自体が「繰り返し使える要素のまとまり」として導入されたものなのでそこにもマッチしていると思います。

まとめ

方法3 で良さそう。

Discussion