🗿

Svelte5でコンポーネントとSnippetを統一的に扱うメモ

2024/10/31に公開


色々処理の共通化などを模索しているとSvelteコンポーネントとSnippetを統一的に扱いたくなることがありました。その時に調べた内容を備忘として残しておきます。おまけで文字列も統一的に扱う例にしています。Svelteのバージョンアップにより、そのうちもっとスマートにできるようになる気がします。

具体的な処理

Snippetに統一する場合

https://svelte.dev/playground/hello-world?version=5.1.4#H4sIAAAAAAAACpVUUY-bMAz-K77sHkCi5bZHjvY2nfY-Xfc2JhWK24sESZSY9irGf58SKA3cbbvlBWJ_tj87tlsm8hpZwh5zQ0ASNoIrhcQitucVGpb8aBmdlYVYAYsuBl-UWpojVmRlRW7wLflOCkJBhiUsNTvNFUEty6ZCqHJxWGWMTMbWmciI10pqghZ2uaFHWavvcqASOdGG9CiBDvZa1pCxZdwQr8wQMGP3nivr5IqztylsJ4UhMKRhBRn7_swNcAMb0lwcHCSNe8rrTFzIz1n3PnayVp8u3FavMwjsLZwGneH99AJDeoIWijz4Riiasms_mEFrdUHYZSIt-dExTIv1mFoPSuPCZhT3iDYebK3RzWKxgCcUJWrIRwNY_PXYMKL9rHszvxaOyajxkp4pvPycZmDy38c9FJ0rdJknh0oWeRVgHdq2kpXUCWgs76Hz1UUI0NpSO31RNTgD8AngoBHFDIF1BEUEPITWijPaS0GLE_LDMyXw8e7u3hM7ggkIqeu8corOPebAm0WM8IVYQrrBLvrD8HndPJ2-qcIfP6zHRrAgKVBQGmP97pj-pE2DzjTvHXqAcehtwCutqL9f519jTviUn0bJ87nUOeF1EXiT7bkdGmyGig3qI-rJGii0PBm0q8CGlns4cVHKE9ysVtCIEvdcYAkQx9CYJq-qMzQGS9iOoS4Oxli3uVIxiiPXUtQoKGNbF8-lds3lm5bKwAqCIITVGp5wJ3WZGreFIsjFeR3CrysF60JkhC8u7L4RO-JSvLFy7Bwm15K61gQA0EiNFq9KGgTKEknmzBypYLCFoaAJ9GS3bsvctpfcHyBjGYMEzLEHOhbhspDluesXzja6uDJIjQoo1wekBL5WWPs07Rke2TmJbJM4bAQDU_d5WAYhdOH9xarrf7qwF7nrW8Wa79sE-pL_u05DRd4sRcrXt60h3aUxXw8J-1y8nT2duZ_dbwN715KJBwAA

code
App.svelte
<script module lang="ts">
  import { castCompToSnippet, castStrToSnippet } from "./utils.svelte";
  import Comp from "./Comp.svelte";
  const str = "This is String";
</script>
<script lang="ts">
  const comp2Snippet = castCompToSnippet(Comp);
  const str2Snippet = castStrToSnippet(str);
  const snpt2Snippet = Snpt;
</script>
{#snippet Snpt()}
<div>
  <b>This is Snippet</b>
</div>
{/snippet}
<!--- Render as Snippet ------------------------------>

{@render comp2Snippet()}
{@render str2Snippet()}
{@render snpt2Snippet()}

<!---------------------------------------------------->
<style>
  :global(em) { color: red; }
  :global(b)  { color: blue; }
  :global(i)  { color: green; }
  :global(em, b, i) {
    font-weight: 100;
    font-style: normal;
  }
</style>
Comp.svelte
<em>This is Component</em>
utils.svelte
<script module lang="ts">
  import { type Component, type Snippet, createRawSnippet, hydrate } from "svelte";
  import { render } from "svelte/server";
  const browser = typeof window !== undefined  // usually used `import { browser } from "$app/environment"`;
  type RawSnippetProps = (() => Record<string, any>) | undefined;

  export function castCompToSnippet(comp: Component) {
    return createRawSnippet((props: RawSnippetProps) => ({
      render: () => `<div>${browser ? "" : svrender(comp).body}</div>`,
      setup(target: Element) {
        hydrate(comp, { target, props: props?.() });
      }
    }));
  }
  export function castStrToSnippet(str: string) {
    return createRawSnippet(() => ({ render: () => `<div><i>${str}</i></div>` }));
  }
</script>

簡単な内容

  • createRawSnippetを用いて全てSnippetに変換

コンポーネントを挟む場合

https://svelte.dev/playground/hello-world?version=5.1.4#H4sIAAAAAAAACq1UwY6bQAz9FXfaQ5BI2O1xAqirnnqrur0te4DEkNGCB804u12x_Hs1A6QQJVVXKqfEfs_22M_uBOUNCil-IO3RwIviA3zVTasJiUUoSlWjFfKhE_zaOqAziHCi3bXtxj5jzc5W5BYv2XeaGImtkCK2O6NahkbvjzVCnVOVZIJtJtKMMlZNqw3DUMzdPm8ZDZRGN5CJTbQwj-EzsZ0RXeV_8O7fErbTZBksG0ggEz8PyoKycM9GUeUhcTQUmGbUfbSk2hYZ7qnlVdBnFO_Vs68zLtITeQDFUZE6-oDoopHrSB_W6zVc7C-sr38uD8XLTijGJukcvYcoveK2bP7idW8Z3GNl7_9caMuvNfpWyKrWRV6vsAmgg52utZFgcL-Ffu4uAoDODcD7i_qIZwC1AFQGkc4Q2IRQhKAC6Jw541ITr19QVQeWcHtzs52ZfYESSJsmr72j99Md6xahYPzFQrI5Yh9eUfdMQEt5Lx1zfWNzUsZpzHGEzT_nvCTyZfIriHdvmUsP3STgcCbLftqhxfJ4_HejWwvJNAInKjmFgLdZjDe3ZoqqcSgPT_gqR9OjhCM9kX6h0A9mmxFAeaQdK02g7BhudTV4IKHQusacXB0AAAb5aMhrfFMjVXyAJEng1ofuz-Kf4vyHDJ-nDLPLMbX_vO_D9el8jBA2m41By9DLU1M_te7HKlgeIneKVOnHpUtP9pkzYce75bfE36ZYpZ0D9HGk0tMxklhbBFWetTbwvO6LGU6TM_krN8cvWzUxhod885V43lCBN3Tju4Yj00Wq7M-l_9j_BvtbIT93BgAA

code
App.svelte
<script module lang="ts">
  import RenderAdapter from "./RenderAdapter.svelte";
  import Comp from "./Comp.svelte";
  const str = "This is String";
</script>
{#snippet Snpt()}
<div>
  <b>This is Snippet</b>
</div>
{/snippet}
<!--- Render with Component --------------------------->

<RenderAdapter item={Comp} />
<RenderAdapter item={str} />
<RenderAdapter item={Snpt} />

<!----------------------------------------------------->
<style>
  :global(em) { color: red; }
  :global(b)  { color: blue; }
  :global(i)  { color: green; }
  :global(em, b, i) {
    font-weight: 100;
    font-style: normal;
  }
</style>
Comp.svelte
<em>This is Component</em>
RenderAdapter.svelte
<script module lang="ts">
  import type { Snippet, Component } from "svelte";
  type Props = {
    item: Snippet | Component | string;
    [key: string]: unknown,
  };
  function isSnippet(item: Snippet | Component): boolean {
    return item.length === 1;
  }
  function isComponent(item: Snippet | Component): boolean {
    return item.length === 2;
  }
</script>
<script lang="ts">
  const { item, ...rest }: Props = $props();
</script>

{#if typeof item === "string"}
  <div><i>{item}</i></div>
{:else if isSnippet(item)}
  {@render item()}
{:else if isComponent(item)}
  {@const Item = item}
  <Item {...rest} />
{/if}

簡単な内容

  • コンポーネント内でpropsの型を判別し、型に応じて表示

雑記

Svelte5がリリースされてから公式サイトが非常に見にくくなったと思うのは私だけなんでしょうか。フォントもそうだし、チュートリアル項目の俯瞰もやりにくくてしょうがないです。個人的には前の方が良かったな~

参考文献

Discussion