概要
Svelteのeach
のなかで各ループごとにsetContext
を使用し、each
ごとに別のContextを配下のコンポーネントに渡します。
このTipsはeach
の中にslot
を配置してコンポーネントを差し込みたい時に利用します。
理由(長いです)
なんでそんなことしたいの?と思った人もいると思うので理由を書いておきます。きっかけはパンくずコンポーネントです。
パンくずの最後のリンクにはaria-current="page"
を指定してあげた方が親切です。
ここで、アクセシビリティの知識がない人がこのパンくずコンポーネントを使ったとしても、最後のリンクに必ずこの属性がつくようにしたいとします。
その場合
<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>
このように作っておくのもありでしょう。
パンくずのデザインがサイトの中で一つしかないのであればそもそもli
やa
にクラス名を振っておけばいいですが、複数の見た目があって別のクラス名をli
やa
に振りたい場合もあるでしょう。
その場合、Breadcrumb.svelte
のpropsやitems
のpropsとしてclass_li
やclass_link
を含める必要があります。
これって気持ち悪くないですか?
class_li
やclass_link
はli
やa
でしか使用しないクラス名です。
これを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
で渡すことを思いつきました。
コード
<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>
<script lang="ts">
let class_name: string | undefined = undefined;
export { class_name as class };
</script>
<li class={class_name}>
<slot />
</li>
<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個目以降は「>」を表示したい
>
{/if}
</svelte:fragment>
</BreadcrumbItemLink>
</BreadcrumbItem>
</Breadcrumb>
ポイント
{@const _ = setContext...}
の部分がポイントです。
each
の中でそのまま{setContext}
してしまうとsetContext
の中がそのまま展開されてしまいますが、@const
で変数に入れてしまうと実行できます。
おそらくこれはいいテクニックではないです。
今後動かなくなったりするかもしれません。
以上Tipsでした。