LoginSignup
1

Svelteでeachの各ループの中でsetContextする

Posted at

概要

Svelteのeachのなかで各ループごとにsetContextを使用し、eachごとに別のContextを配下のコンポーネントに渡します。

このTipsはeachの中にslotを配置してコンポーネントを差し込みたい時に利用します。

理由(長いです) なんでそんなことしたいの?と思った人もいると思うので理由を書いておきます。

きっかけはパンくずコンポーネントです。

パンくずの最後のリンクにはaria-current="page"を指定してあげた方が親切です。
ここで、アクセシビリティの知識がない人がこのパンくずコンポーネントを使ったとしても、最後のリンクに必ずこの属性がつくようにしたいとします。

その場合

Breadcrumb.svelte
<script lang="ts">
  import { setContext } from 'svelte';
  import type { BreadcrumbItemLinkProps } from './BreadcrumbItemLink.svelte';

  export let label: string;
  export let items: BreadcrumbItemLinkProps[];
</script>
<nav aria-label={label}>
  <ol>
    {#each items as { href, label }, index (href)}
      <li>
        <a href={href} aria-current={items.length - 1 === index ? 'page' : undefined}>{ label }</a>
      </li>
    {/each}
  </ol>
</nav>

このように作っておくのもありでしょう。

パンくずのデザインがサイトの中で一つしかないのであればそもそもliaにクラス名を振っておけばいいですが、複数の見た目があって別のクラス名をliaに振りたい場合もあるでしょう。
その場合、Breadcrumb.svelteのpropsやitemsのpropsとしてclass_liclass_linkを含める必要があります。

これって気持ち悪くないですか?
class_liclass_linkliaでしか使用しないクラス名です。
これをBreadcrumbコンポーネントのpropsとして渡すのは違う気がします。

itemsに渡す場合、itemsの各オブジェクトにそのpropsを渡さなければなりません。
これもちょっと気持ち悪いですよね。
各オブジェクトに毎回渡すのもめんどくさいです。

以下のようなイメージで

<Breadcrumb>
 // eachが必要
  <BreadcrumbItem class="item">
    <BreadcrumbItemLink class="link">
    </BreadcrumbItemLink>
  </BreadcrumbItem>
</Breadcrumb>

各コンポーネントに必要なpropsは各コンポーネントに渡すと自然です。

ですが、この構成だとBreadcrumbItemの部分をeachで毎回回さなければなりませんし、BreadcrumbItemLinkにループがどこまで進んだかの情報をpropsをして渡す必要が出てきてしまいます。(最後のループかどうかを判定する必要があります)

これでは当初の目的の「パンくずの最後のリンクにはaria-current="page"を指定」をBreadcrumbを呼び出すたびに書かなくてはならず、漏れも出てきそうです。

これを解決するために、Breadcrumbコンポーネントの中でeachを回し、中の要素はslotで渡すことを思いつきました。

コード

Breadcrumb.svelte
<script lang="ts">
  import { setContext } from 'svelte';
  import type { BreadcrumbItemLinkProps } from './BreadcrumbItemLink.svelte';

  export let label: string;
  export let items: BreadcrumbItemLinkProps[];
</script>
<nav aria-label={label}>
  <ol>
    {#each items as item, index (item)}
      {@const _ = setContext('breadcrumb_item', {
        item,
        index,
        is_last: items.length - 1 === index
      })}
      <slot />
    {/each}
  </ol>
</nav>
BreadcrumbItem.svelte
<script lang="ts">
  let class_name: string | undefined = undefined;
  export { class_name as class };
</script>
<li class={class_name}>
  <slot />
</li>
BreadcrumbItemLink.svelte
<script context="module" lang="ts">
  export type BreadcrumbItemLinkProps = {
    href: string;
    label: string; //ここには読み上げのためにテキストを必須で入れさせたい
    hide_label?: boolean;
  };
</script>
<script lang="ts">
  import { getContext } from 'svelte';

  let { item, index, is_last } = getContext<{
    item: BreadcrumbItemLinkProps;
    index: number;
    is_last: boolean;
  }>('breadcrumb_item');

  let current = is_last;
  let href: string = item.href ?? '';
  let label: string = item.label ?? '';
  let hide_label = item.hide_label ?? false;
</script>

{#if $$slots.prefix && !hide_label}
  <span aria-hidden="true"> //アイコンは読み上げさせない
    <slot name="prefix" {index} />
  </span>
{/if}
<a
  aria-current={current ? 'page' : undefined}
  aria-label={hide_label ? label : undefined}
  {href}
>
  {#if hide_label && $$slots.prefix}
    <slot name="prefix" {index} />
  {:else}
    {label}
  {/if}
</a>

使用する側
<Breadcrumb class="list" {items} label="breadcrumb">
  <BreadcrumbItem class="item">
    <BreadcrumbItemLink class="link">
      <svelte:fragment slot="prefix" let:index>
        {#if index === 0} //最初のリンクはアイコンを表示したい
          <HomeIcon />
        {:else} //2個目以降は「>」を表示したい
          &gt;
        {/if}
      </svelte:fragment>
    </BreadcrumbItemLink>
  </BreadcrumbItem>
</Breadcrumb>

ポイント

{@const _ = setContext...}の部分がポイントです。

eachの中でそのまま{setContext}してしまうとsetContextの中がそのまま展開されてしまいますが、@constで変数に入れてしまうと実行できます。

おそらくこれはいいテクニックではないです。
今後動かなくなったりするかもしれません。

以上Tipsでした。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1