Svelte5のドキュメントに記載のない役立つか不明な小ネタ
Svelte5の地味な検証記事が役に立つ場合があるようだったので、その他の自分で色々やってみた時に発見した?小ネタも記事にしました。こんなの使わんやろ的な内容も多いので役立つかは不明です。一部ドキュメントに載ってたりドキュメントに載せるような基礎的内容じゃなかったり別にSvelte5からじゃないものも含まれてますがご了承ください。また、正しい内容だと自分では思っているのですが、私の認識が誤っている場合もありますのでご了承ください。
[svelte@5.0.0-next.242 が最新リリース状態でのREPLで確認]
(2024/09/05 追記)
任意のタイミングでコンポーネント内の関数を起動するに追記しました。また、 "$effect.tracking
はuntrack
内でfalse
を返す"を追記しました。またそのうち何か見つければこっそり追記するかもしれません。
children
を受け取る
残余引数でコンポーネントタグ内のコンテンツで{#snippet}{/snippet}
で囲まれていないものは全て以下のようにchildren
として受け取ることができます。
let { children } = $props();
これは特に明示して受け取らずとも、残余引数で受け取り呼び出すこともできます。
<script lang="ts">
let { ...rest }: Props = $props();
</script>
{@render rest["children"]()}
code
<script>
import Comp from "./Comp.svelte";
</script>
<Comp>
<p>Hello from children</p>
</Comp>
<script module lang="ts">
import type { Snippet } from "svelte";
type Props = {
[key: string]: unknown,
};
</script>
<script lang="ts">
let { ...rest }: Props = $props();
</script>
{@render rest["children"]()}
残余引数で任意のスニペットを受け取る
コンポーネントは通常以下のようにスニペットを受け取ることができます。
let { foo }: { foo: Snippet } = $props();
コンポーネント作成時点でスニペットの名前が確定していない又は個数が確定していない場合、以下のように残余引数で受け取り実行時に呼び出すことができます。(活用事例)
<script lang="ts">
let { ...rest }: Props = $props();
</script>
{#if typeof rest["foo"] === "function"}
{@render rest["foo"]()}
{/if}
ただし、以下の例でもただの関数なのかスニペットなのかの厳密な判別をしていないので注意が必要です。(判別方法不明)
code
<script>
import Comp from "./Comp.svelte";
</script>
<Comp>
{#snippet foo()}
<p>Hello from foo</p>
{/snippet}
{#snippet bar()}
<p>Hello from bar</p>
{/snippet}
<p>Hello from children</p>
</Comp>
<script module lang="ts">
import type { Snippet } from 'svelte';
type Props = {
[key: string]: unknown,
};
</script>
<script lang="ts">
let { ...rest }: Props = $props();
const snippetKey = Object.entries(rest)
.filter(([key, value]) => typeof value === "function")
.map(([key, _]) => key);
</script>
{#each snippetKey as key}
{@render rest[key]()}
{/each}
任意のタイミングでコンポーネント内の関数を起動する
仕様なのか不具合なのか理解していませんが、コンポーネントはモジュールレベルのscript
タグ内でしかexport
できません。
<script>
import { inModule, inScript } from "./Comp.svelte"
inModule();
//inScript(); // if uncomment, compile error occurs.
</script>
<script module>
export function inModule() { console.log("in module"); }
</script>
<script>
export function inScript() { console.log("in script"); }
</script>
そのため、コンポーネントの各インスタンス固有の状態に関する処理を外部から任意のタイミングで起動するには$effect
を使うしかないです。(たぶん)
関数をbind
とbindable
で共有するだけで普通にできました。なぜ最初にこの発想にならなかったのか。
コンポーネントの値検証はコンポーネントに任せ、任意のタイミングで外部から検証結果を取り出すには以下サンプルのようにすればできます。
- 関数を共有する方法
code
<script module lang="ts">
import Comp from "./Comp.svelte";
</script>
<script lang="ts">
let test: () => boolean = $state.raw();
let message = $state("display message here");
function onclick() {
message = test() ? "text is valid!!!" : "text is invalid...";
}
</script>
<fieldset>
<legend>Child component</legend>
<Comp bind:test />
</fieldset>
<fieldset>
<legend>Parent component</legend>
<button type="button" {onclick}>check</button>
<p>{message}</p>
</fieldset>
<script module lang="ts">
type Props = {
test: () => boolean;
};
import { untrack } from "svelte";
</script>
<script lang="ts">
let { test = $bindable() }: Props = $props();
let value = $state("");
test = isAsciiDigits;
function isAsciiDigits(): boolean {
return /^[0-9]+$/.test(value);
}
</script>
<input type="text" bind:value placeholder="Please enter number" />
-
$effect
を使う方法 (非推奨)
code
<script module lang="ts">
import Comp, {type Test, TEST} from "./Comp.svelte";
</script>
<script lang="ts">
let test = $state(TEST.VALID);
let message = $derived(test ? "text is valid!!!" : "text is invalid...");
function onclick() {
test = TEST.INVOKE;
}
</script>
<fieldset>
<legend>Child component</legend>
<Comp bind:test />
</fieldset>
<fieldset>
<legend>Parent component</legend>
<button type="button" {onclick}>check</button>
<p>{message}</p>
</fieldset>
<script module lang="ts">
export const TEST = {
INVOKE: null,
VALID: true,
INVALID: false,
} as const;
type Test = (typeof TEST)[keyof (typeof TEST)];
type Props = {
test: Test;
};
import { untrack } from "svelte";
</script>
<script lang="ts">
let { test = $bindable() }: Props = $props();
let value = $state("");
$effect.pre(() => {
if (test === TEST.INVOKE) {
untrack(() => test = isAsciiDigits());
}
});
function isAsciiDigits(): boolean {
return /^[0-9]+$/.test(value);
}
</script>
<input type="text" bind:value placeholder="Please enter number" />
$effect
が2回起動したりして微妙にスマートじゃないので、この方法以外で簡単な方法があれば知りたいです。
use
ディレクティブはundefined
を渡すとエラーになる
以下のようなタグの属性値にundefined
を渡すと、その属性がないタグが生成されます。(以下例ではspanClass
がundefined
だとclass
属性がなくなる)
<span class={spanClass}></span>
bind
ディレクティブのある属性でもundefined
でエラーは発生しません。
<script>
let value = $state();
</script>
<input type="text" bind:value />
<p>{typeof value}</p>
しかし、use
ディレクティブはundefined
を渡すとエラーが発生します。コンパイルエラーではなく実行時エラーのようです。
<script>
let action;
</script>
<input type="text" use:action />
{#if}
で場合分けすることでエラーを回避することができます。
少し前、私の手元の開発環境では{#if}
で場合分けしてもエラーが発生するので仕方なく空アクションを作成していたのですが、不要になったようです。
string | Snippet
を受け取る
コンポーネントでコンポーネントでスニペットを受け取るようにすると色々なケースに対応できるコンポーネントが簡単に作成できます。しかし、そのコンポーネントを軽く使用する場合でも{#snippet}
をいちいち定義しなければならず少々ボイラープレート感があります。children
で受け取ることができれば良いですが、他の用途でchildren
が使われている場合は使えません。そこで、string
でも使用できるようにするとコンポーネントを使うときに使いやすい場合があります。
code
<script>
import Comp from "./Comp.svelte";
</script>
<Comp title="Title is here">
This is main contents.
</Comp>
<script module lang="ts">
import type { Snippet } from "svelte";
type Props = {
title: string | Snippet,
children: Snippet,
};
</script>
<script lang="ts">
let { title, children }: Props = $props();
</script>
<div class="frame">
{#if typeof title === "string"}
<span class="title">{title}</span>
{:else}
{@render title()}
{/if}
<div>
{@render children()}
</div>
</div>
<style>
.frame {
border-style: solid;
border-width: 1px;
}
.title {
font-weight: 800;
}
</style>
特定タグの属性やイベントを専用型を使って透過的にする
コンポーネント内の特定のタグの属性やイベントを$props
で受け取り透過的に扱いたい場合があります。そのような場合も簡単に型定義し、スプレッド構文で適用できるようになっています。イベント定義がon
ディレクティブではなくなったので、より簡単に適用できるようになったのだと思います。
code
<script module lang="ts">
import Comp from "./Comp.svelte";
</script>
<script lang="ts">
let input = $state();
const attributes = {id: "text1", class: "comp", name: "name1", value: "aaa"};
const events = {
onchange: (ev: Event) => {
ev.target.value = ev.target.value + "...zzz"
}
}
</script>
<Comp bind:element={input} {attributes} {events} />
<p>{input?.value}</p>
<script module lang="ts">
import type { HTMLAttributes } from "svelte/elements";
type Props = {
attributes?: HTMLAttributes<HTMLInputElement>,
events?: InputEvent,
element?: HTMLInputElement, // bindable
};
</script>
<script lang="ts">
let { attributes, events, element = $bindable() }: Props = $props();
</script>
<input type="text" {...attributes} {...events} bind:this={element} />
$effect.tracking
はuntrack
内でfalse
を返す
$effect.tracking
は$effect
の文脈で実行された時、true
を返します。これはマウント時に$effect
が処理される時も変わりません。
しかし、untrack
の中で$effect.tracking
が実行されるとfalse
を返します。どうやら$effect.tracking
は厳密に以下ドキュメントの記載通り"tracking context"、私の理解では"検知スコープ"内で実行された時にtrue
を返すようです。ドキュメント通りと言えばドキュメント通りですが若干困惑する場合がありそうです。
tells you whether or not the code is running inside a tracking context
$state
フィールドを透過的に扱う
クラスのプライベートクラスのプライベートフィールドを$state
で宣言した時、そのフィールドが配列であればgetterを{#each}
に書けば通常の$state
配列のように動的に更新してくれます。配列以外の値の場合、たとえオブジェクトであってもうまく動作しません。
Discussion