🌺

Svelte5のSnippet引数型付けと動的生成についてメモ

2024/07/24に公開


Tabsコンポーネントを作るために色々とSnippetについて調べましたが、結局活用できませんでした。調べた内容で忘れそうなところをメモとして残しておきます。

Snippetの型付けと活用

型付け

Snippet引数の型はタプル[T, U, ...]として表現する。

Example.svelte
<script context="module" lang="ts">
  import type { Snippet } from "svelte";
  export type Props = {
    foo: Snippet<[number]>,
  };
</script>

活用例

表示制御の追加を含めたWrapperコンポーネントのようなものも作成可能。

REPL

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACqVSwYrbMBD9lYm2hcTNxruUXhzbtJReS6HH1R4SaxxErZGQx2GD8L8XSc4uy1JYqE-emfee9J4miF4POIrqIQg6GBSV-Oac2Aq-uFiMZxwYxVaMdvJd7NRj57XjVpJkbZz1DN-tcdB7a0CKXRmrXeZJsZdUly8MquM0ccPNSNo5ZDhadVnTZDZz7EuuXftzMkf0oEcINJl5tVrV5UIrF9oclRc1sRXGKt1rVKJiP-G8fbYTIe_wA50lxiduZJSaBpQChgOdGil4lKKVBFAWRQE_npLnoijKeB_MZZSHX966ERoI2Uf0VcHvfN36gZKnx3Ybp9nqVXPJMWvCtUya4SoA8zXif2a7un3nl-GL8dcuJXeWRoaQngXm6tnVBxd_1pt9JEseMGY2EVeQnUXIyAfG9X3CcD9Rx9oSWOoG3f1Zbyo4W62u-SQ2fGrgfr9E8l92jhOzpZRaI0WupICwnD63mjqPBonrMk8TL9zoPhuBj_AZVk0Dd-l1aqXPbb5p-OqRFPq8qgmcl7UuMyiUup9zqHwZMNGUPr9sglfob9OsAmWn44D7PDIHf9JUwd3ui0ezNN1BKU2nV90cT1J_u-6P819qpM1CyQMAAA==

コード全体

code
App.svelte
<script>
  import Comp from "./Comp.svelte";
</script>

<Comp>
  {#snippet body(num)}
    <p>Number is {num}!!!</p>
  {/snippet}
</Comp>
Comp.svelte
<script context="module" lang="ts">
  /*** Export ***/
  export type Props = {
    body: Snippet<[number]>,
  }
  
  /*** import ***/
  import type { Snippet } from "svelte";
</script>

<!---------------------------------------->

<script lang="ts">
  const { body }: Props = $props();

  let count: number = $state(1);
  function onclick(): void {
    count += 1;
  }
</script>

<!---------------------------------------->

<button type="button" {onclick}>increment</button>

{#if count % 3 !== 0}
  <div>
    {@render body(count)}
  </div>
{/if}

<style>
  div {
    border-style: double;
    margin: 0.5rem;
    padding: 0.5rem;
  }
</style>

簡単な説明

  • Comp.svelteの機能
    • 数値インクリメントボタン表示
    • 数値が3の倍数の時、結果表示を消す
    • 結果表示を二重線で囲む
    • 具体的な結果表示内容は呼び出し元が定義可能

Snippetの動的生成と活用

バージョン svelte@5.0.0-next.189 からcreateRawSnippet関数が追加されプログラムからSnippetを生成できるようになった。

簡単な例

REPL

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACo1S24rcMAz9FVUsbAJhhn3NJmnLUtrn9rEqTNYjz4Q6cnCUdkvwvxcnc9nLy2IwPrKOpHPsGW3neMTy54zS9owlfh4GLFD_DQmMf9gpY4Gjn4JJkWo0oRu0ISHt-sEHhRm-BmaFCDb4Hgg32wffD5uVTHhPkrIdK1jvoQZC6_0aVzuJ0c4LeDGuM7-zHOYUJ11zl72-cOAjED62gRDKF3UiSbW9zibV46TqBZKQmnBFhDCf-sTGHFs5MCg_abVd7xfm_Cmw7DmsqjLrfR5JsMDe7zvb8R5LDRPH4mJZUvsOz8B4Sd1qSqUmx4TgWjnUhDoSvrTUBG6Vv7d_f0g3DFd3b9fyt4toflqyjZdRT49Qv2FmWRqzhCyHuoFRQyeH5XjyObBOQc4o4aT-nL-rjnfNN3bOw82cCmV5_FBtj3fNrjgzRtZpKCETv-cSvjjuWfR5i7Ru2Fo2mmWvL0gTb5OceUgGSRKxe91yd39lxPwC4nqI6x9I8We_4O2b_Yr_Aem9dgvyAgAA

部分コード

Example.svelte
<script context="module" lang="ts">
  import { createRawSnippet } from 'svelte';
  export const Greet = createRawSnippet((name: () => string) => {
    return {
      render: () => `<h1>Hello ${name()}!</h1>`,
      setup: (node: Element) => {
        $effect(() => {
          node.textContent = `Hello ${name()}!`;
        });
      }
    };
  });
</script>

Svelte内の型付け

svelte/packages/svelte/types/index.d.ts

index.d.ts
export function createRawSnippet<Params extends unknown[]>(fn: (...params: Getters<Params>) => {
  render: () => string;
  setup?: (element: Element) => void;
}): Snippet<Params>;

活用例

1つのsvelteファイルで複数の要素を定義・エクスポートできる。

REPL

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACqVTTYvbMBD9K9MhsDZ1E9rcHNt0WXoo9FDa3qqFpM54I5BlIY3bFOP_XsaO42wC7UKtg6WZ995I89FhpQ0FTL93aHc1YYr3zmGC_NvJIfwkw4QJhqb1pViyUHrtuFBWsa5d4xkemtol0MEnHfgjUw09VL6pQeFyJb7lqKJwo2y2mvk2E--g1L33ZPfkzxqRQvm9VRj3fwO8-xdgPQKy1SkWJlg3e11p2mPKvqU-OT9dIC94O5SNZTpyrkSqNaQQzM4-5Qo5KCyUBTilRqSgg69WO0c8J-YiI-c0dlB62jF92f26wt-N8DtBW8V0nLU_-8YFyKETh-LyoM3ek02nkInY-wtW2djAc6nym6BRJG9LIYohLyCw1_Zp2E4hBgHnqdLHbzdIyE9HhUanoBBeg-hF8Wake-LW20lMzlK1SWSbGV0sulk9ivtsZXSxTSZCIG5dCpHIfzBUk-XL68laUFVRyVF07VBs9FKu8yAVtAw5bK-ibTdX8EB8z-z1j5YpUliaXQgKEyHyiRLPlD4-8_tx0w-GyX41AK_evPAb4af-e95sU0k6mKoPfXpujIWTzZD-_wp9WBfSNKCZ6pCtDutC2aw1z6d3ukA0jtzgvh24x_4PxTrrEncEAAA=

コード全体

code
App.svelte
<script>
  import Comp, { ListItem } from "./Comp.svelte";
</script>

<Comp>
  {@render ListItem("Item1")}
  {@render ListItem("Item2")}
  {@render ListItem("Item3")}
</Comp>
Comp.svelte
<script context="module" lang="ts">
  import type { Snippet } from "svelte";
  import { createRawSnippet } from 'svelte';

  export type Props = {
    children: Snippet,
  }
  export const ListItem = createRawSnippet((text: () => string) => {
    const prefixText: () => string = () => "li: " + text();
    return {
      render: () => `<li>${prefixText()}</li>`,
      setup: (li: Element) => {
        $effect(() => {
          li.textContent = `${prefixText()}`;
          li.setAttribute("class", `${text()}`)
        });
      }
    };
  });
  
</script>

<!---------------------------------------->

<script lang="ts">
  const { children }: Props = $props();
</script>

<!---------------------------------------->

<h3>List items</h3>
<ul>
  {@render children()}
</ul>

簡単な説明

  • Comp.svelteの機能
    • コンポーネントとしてリストの枠組みを提供
    • 子コンポーネントとしてリストアイテムとしてのSnippetを定義
      • 文字列のみ受け付けるリストアイテム
  • createRawSnippetについて
    • 引数は関数を取る
    • 例のprefixTextのように関数内で値の加工が可能
    • 返り値のrenderはHTML要素文字列を指定する
    • 返り値のsetupはSnippet引数の値が更新された際の動作を指定する
      • mount,hydrate時に呼び出される
      • setupが関数の場合、アンマウント時にも呼び出される
      • 上記REPL例のApp.svelteのような静的用途を想定する場合はsetupを省略可能

参考文献

Discussion