Svelte5でdetailsタグのAccordionコンポーネント作成メモ
Svelte5で学習用Webアプリを作成している中でAccordionコンポーネントが欲しくなったので作成しました。閉じる時にtransitionが機能しないように見えたり、他コンポーネントとの連携用$effect
の使い方に若干迷ったりしたのでスタイリング部分以外を備忘用に残しておきます。
作成したコンポーネント
REPL
コード全体
code
<script context="module" lang="ts">
import Accordion from "./Accordion.svelte";
</script>
<script>
let singleStatus = $state(false);
let groupStatus = $state([false, false, false]);
</script>
{#snippet content()}
Lorem ipsum dolor sit amet lorem et lorem ipsum lorem dolor ea nibh sed. Magna ea et kasd ut elitr vero exerci diam et ipsum dolore consetetur stet gubergren. Dolor aliquyam vel autem sed takimata vulputate stet suscipit et invidunt molestie et elitr dolor. Ut ea iriure sadipscing dolore. Erat stet ipsum nonummy sea no lorem accusam.
{/snippet}
<Accordion label="Single test" bind:isOpen={singleStatus}>
{@render content()}
</Accordion>
<br>
<hr>
<br>
{#each groupStatus as _, i}
<Accordion label="Grouping test {i}" bind:isOpen={groupStatus[i]} bind:group={groupStatus}>
{@render content()}
</Accordion>
{/each}
<script context="module" lang="ts">
/*** Export ***/
export type Props = {
children: Snippet,
label: string,
isOpen?: boolean, // if link some, specify array[i] of the some
group?: boolean[], // if link some, specify array of the some
};
/*** Others ***/
const duration = 400; // duration of open,close transition
/*** import ***/
import type { Snippet } from "svelte";
import { slide } from 'svelte/transition';
</script>
<!---------------------------------------->
<script lang="ts">
let { children, label, isOpen = $bindable(false), group = $bindable() }: Props = $props();
/*** Initialize ***/
let open: boolean = $state(isOpen);
let guard: boolean = false;
/*** Sync ***/
$effect.pre(() => {
isOpen; // trigger of the $effect
toggleDetails();
});
/*** Others ***/
function prevent<T extends Element>(func?: (this: T, evt: Event) => void) {
return function(this: T, evt: Event) {
if (func !== undefined) {
evt.preventDefault();
func.call(this, evt);
}
};
}
async function sleep(msec: number) {
return new Promise(resolve => setTimeout(resolve, msec));
}
function closeAllGroup(): void {
if (group !== undefined) {
group.forEach((_,i) => group[i] = false);
}
}
function toggleDetails(): void {
if (guard) { return; }
guard = true;
if (isOpen) {
open = true;
} else {
sleep(duration).then(() => open = false);
}
sleep(duration).then(() => guard = false);
}
/*** Handle events ***/
function updateIsOpen(): void {
let tmp = !isOpen;
closeAllGroup();
isOpen = tmp;
}
</script>
<!---------------------------------------->
<details open={open}>
<summary onclick={prevent(updateIsOpen)}>
{label}
</summary>
{#if isOpen}
<div transition:slide={{ duration: duration }}>
{@render children()}
</div>
{/if}
</details>
簡単な解説
開閉状態の制御
details
タグはopen
属性で開閉状態を指定可能。transition
を使用しない場合は$props
の値をそのままopen
属性に渡せば事足りる。transition
を使用する場合は、open
属性がfalse
になった時点でコンテンツの描画が瞬間的に無くなりアニメーションしていないように見えてしまう。そのため、transition
のアニメーションが終わったタイミングでopen
属性をfalse
にする必要がある。すなわち$props
の閉じる状態の値とは別に、実際に閉じるタイミングを制御する変数を持つ必要がある。
開閉トリガー
details
タグの開閉のトリガーは複数Accordion間の連携のために以下の2パターンある。
- 自身のイベント
- 親コンポーネントからの
$props
変更
このため、上記コードで開閉指示は$props
の受け取り変数isOpen
の変更に統一し、2パターンどちらもisOpen
の変更のみ行う。実際の描画制御はisOpen
の変化を検知した後に起動する$effect.pre
で行う。$effect.pre
内で変数の明示が無いと他の$state
変数の変化でも起動してしまうので、isOpen
の変化でのみ起動するように$effect.pre
内で意味のない記述isOpen;
を記述している。
開閉連携
上記コードではAccordion自身の開閉状態をisOpen
変数、連携しているAccordion群の開閉状態をgroup
配列変数で表現している。実質的には連携という連携はしておらず、イベントがトリガした際にAccordion群を全て閉じた後に本来の自身の開閉処理をしているだけ。
動作中のトリガ抑制
開閉を制御しているtoggleDetails
関数が動作中に再度起動しないようにguard
変数で起動状態を保持。
Svelte側の制御が関連する部分
上記コードのイベントハンドラupdateIsOpen
関数ではgroup
を変更した後にisOpen
を変更している。group
を変更すると間接的に自身のisOpen
を変更しているため、表面上は2回isOpen
が変更され2回$effect.pre
が実行されるはずだが、実際は1回しか実行されないように見えうまくいっているように見える。
所感
実際は外部からのアイコンの描画有無,スタイリング制御等を盛り込んだのでもう少し複雑になってしまいましたが、スタイリングの記述が無いとシンプルでいいですね。
Discussion