昔話
こういう記事をSvelte4の時に書いていました。
Svelte5でアップデートしたので以下で紹介します。
構成
使用するコンポーネントは3つです。
- Section.svelte
- Heading.svelte
- Element.svelte
Element.svelteはただのsvelte:elementのラッパーなので使わなくても問題ありませんが、
この記事では使用します。
最終的にこうなる
<Section>
<Heading>Heading Level 2</Heading>
<p>Content</p>
<Section>
<Heading>Heading Level 3</Heading>
<p>Content</p>
<Section>
<Heading>Heading Level 4</Heading>
<p>Content</p>
<Section>
<Heading>Heading Level 5</Heading>
<p>Content</p>
<Section>
<Heading>Heading Level 6</Heading>
<p>Content</p>
<Section>
<Heading>Heading Level 6(Max)</Heading>
<p>Content</p>
</Section>
</Section>
</Section>
</Section>
</Section>
</Section>
<Section>
<Heading>Heading Level 2</Heading>
<p>Content</p>
</Section>
Heading.svelte
<script lang="ts" module>
import { getContext, setContext, type Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
const CONTEXT_KEY_HEADING = Symbol('heading');
type HeadingContext = {
level: 2 | 3 | 4 | 5 | 6;
};
export const setHeadingContext = (v: HeadingContext) =>
setContext<HeadingContext>(CONTEXT_KEY_HEADING, v);
export const getHeadingContext = () => getContext<HeadingContext>(CONTEXT_KEY_HEADING);
const HEADING_LEVEL_MAX = 6;
export const getHeadingLevel = (level: number) =>
Math.min(level, HEADING_LEVEL_MAX) as HeadingContext['level'];
</script>
<script lang="ts">
import Element from '$lib/components/element/ui/Element.svelte';
let {
children,
...restProps
}: {
children: Snippet<[ReturnType<typeof getHeadingContext>]>;
} & HTMLAttributes<HTMLHeadingElement> = $props();
let headingContext = getHeadingContext();
const tag = `h${headingContext.level}` as const;
</script>
<Element as={tag} {...restProps}>
{@render children(headingContext)}
</Element>
getContext, setContextする関数をexportしておきます。
{@render children(headingContext)}
この部分ではsectionのbody部分で見出しレベルに合わせてベースのfont-sizeを変更したりpaddingを調整したりすることが考えられるので、headingContextを渡しておきます。
Section.svelte
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import {
getHeadingContext,
getHeadingLevel,
setHeadingContext
} from '../../heading/ui/Heading.svelte';
let { children, ...restProps }: { children: Snippet } & HTMLAttributes<HTMLElement> = $props();
let headingContext = getHeadingContext();
setHeadingContext({
level: headingContext ? getHeadingLevel(headingContext.level + 1) : 2
});
</script>
<section {...restProps}>
{@render children()}
</section>
Contextに渡す値をリアクティブにするためには
<script>
let hoge = $state({
fuga: 2
});
setContext('key', hoge);
</script>
のようにした上で、
<script>
let context = getContext('key');
context.fuga = 3;
</script>
のようにして更新する必要がありますが、
今回の例では見出しレベルが動的に変化することはないのと、親のコンテキストを変更したくない(してしまうと末端で計算されたレベルが全ての親階層のものまで辿って変更されてしまう)ので$stateを使用していません。
逆に同期したいのであれば上に書いたような方法で更新できます。
個人的にはGlobalStoreよりもProviderで影響範囲を絞った方がマシかなと思っているのでContextのほうが使用頻度は高いです。
というわけでこの記事はこれで終わります。
ありがとうございました!
補足
最初に紹介した前回の記事では
$: if ($$slots.heading)のようにheadingがある時だけ見出しレベルをインクリメントさせていましたが、
そもそも見出しを必ず含めるようにした方がいいので今回は分岐をしないようにしました。
余談
Contextに$stateを渡す方法など、Svelteのドキュメントが昔よりも詳しく書かれるようになっています。
しばらくドキュメントをみていなかった人はたまにみてみると面白いかも👀