🧶
Svelte5の@attach構文とAction比較メモ
Svelte5.29からuse:
ディレクティブを拡張したような{@attach ...}
という新しいテンプレート構文が実装されました。以前からあるAction
と比較しつつ、どういったものかをある程度調べた備忘メモです。公式サイトに書いてあることを網羅しているわけではないので補足資料の位置付けです。
基本
型定義
// import { type Attachment } from "svelte/attachments";
interface Attachment<T extends EventTarget = Element> {
(element: T): void | (() => void);
}
// import { type Action, type ActionReturn } from "svelte/action";
interface Action<
Element = HTMLElement,
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>
> {
<Node extends Element>(
...args: undefined extends Parameter
? [node: Node, parameter?: Parameter]
: [node: Node, parameter: Parameter]
): void | ActionReturn<Parameter, Attributes>;
}
interface ActionReturn<
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>
> {
update?: (parameter: Parameter) => void;
destroy?: () => void;
}
Actionとの比較
引数がない場合
code
<script module lang="ts">
import type { ActionReturn } from "svelte/action";
import type { Attachment } from "svelte/attachments";
function myAction(element): ActionReturn {
console.log(`action: ${element.nodeName}`);
return {
destroy: () => console.log("destroy action element"),
};
}
function myAttachment(element): () => void {
console.log(`attach: ${element.nodeName}`);
return () => console.log("destroy attachment element");
}
</script>
<script lang="ts">
let bool = $state(false);
const onclick = () => bool = !bool;
</script>
<button type="button" {onclick}>toggle</button>
{#if bool}
<p use:myAction>Action</p>
<div {@attach myAttachment}>Attachment</div>
{/if}
引数がある場合
code
<script module lang="ts">
import type { ActionReturn } from "svelte/action";
import type { Attachment } from "svelte/attachments";
function myAction(element, params: { text: string, num: number }): ActionReturn {
console.log(`action: ${params.text} ${params.num} (${element.nodeName})`);
return {
destroy: () => console.log("destroy action element"),
};
}
function myAttachment(text: string, num: number): Attachment {
return (element: Element) => {
console.log(`attach: ${text} ${num} (${element.nodeName})`);
return () => console.log("destroy attachment element");
};
}
</script>
<script lang="ts">
let bool = $state(false);
const onclick = () => bool = !bool;
</script>
<button type="button" {onclick}>toggle</button>
{#if bool}
<p use:myAction={{ text: "Hello!!", num: 1 }}>Action</p>
<div {@attach myAttachment("Hi!!", 2)}>Attachment</div>
{/if}
リアクティビティ比較
code
<script module lang="ts">
import type { ActionReturn } from "svelte/action";
import type { Attachment } from "svelte/attachments";
</script>
<script lang="ts">
let text = $state("Hi");
let color = $state("red");
let action = $state(myAction);
let attach = $state(myAttachment);
const toggleColor = () => color = color === "red" ? "blue" : "red";
const toggleText = () => text = text === "Hi" ? "Hello" : "Hi";
const toggleAction = () => action = action === myAction ? emptyAction : myAction;
const toggleAttach = () => attach = attach === myAttachment ? emptyAttachment : myAttachment;
function myAction(element, params: { text: string }): ActionReturn {
console.log(`init action: ${params.text}`);
$effect(() => {
element.style.color = `${color}`;
});
return {
destroy: () => {},
};
}
function emptyAction(element, params: { text: string }): ActionReturn {
console.log("empty action");
return { destroy: () => {} };
}
function myAttachment(text: string): Attachment {
return (element: Element) => {
console.log(`init attachment: ${text}`);
$effect(() => {
element.style.color = `${color}`;
});
return () => {};
};
}
function emptyAttachment(text: string): Attachment {
return (element: Element) => {
console.log("empty attachment");
return () => {};
};
}
</script>
<button type="button" onclick={toggleText}>text</button>
<button type="button" onclick={toggleColor}>color</button>
<button type="button" onclick={toggleAction}>action</button>
<button type="button" onclick={toggleAttach}>attach</button>
<p use:action={{ text }}>Action</p>
<div {@attach attach(text)}>Attachment</div>
Falsy値の許容
Actionではエラーになる。
code
<script lang="ts">
const myAction = undefined;
const myAttachment = undefined;
</script>
<!-- <p use:myAction>Action</p> -->
<div {@attach myAttachment}>Attachment</div>
<div {@attach myAttachment?.()}>Attachment</div>
コンポーネントへの受け渡し
通常の受け渡し
code
App.svelte
<script module lang="ts">
import type { ActionReturn } from "svelte/action";
import type { Attachment } from "svelte/attachments";
import Component from "./Component.svelte";
function myAction(element, params: { text: string }): ActionReturn {
console.log(`action: ${params.text}`);
return {
destroy: () => {},
};
}
function myActionFunction(text: string): Action {
return (element) => myAction(element, { text });
}
function myAttachment(text: string): Attachment {
return (element: Element) => {
console.log(`attachment: ${text}`);
return () => {};
};
}
</script>
<script lang="ts">
let bool = $state(false);
const onclick = () => bool = !bool;
</script>
<button type="button" {onclick}>toggle</button>
{#if bool}
<p use:myAction={{ text: "Hello!!" }}>Action</p>
<div {@attach myAttachment("Hi!!")}>Attachment</div>
<Component
action={myActionFunction("Component Hello!!")}
attachment={myAttachment("Component Hi!!")}
/>
{/if}
Component.svelte
<script module lang="ts">
import type { Action } from "svelte/action";
import type { Attachment } from "svelte/attachments";
interface Props {
action: Action;
attachment: Attachment;
}
</script>
<script>
const { action, attachment }: Props = $props();
</script>
<p use:action>Action Component</p>
<div {@attach attachment}>Attachment Component</div>
スプレッド構文での受け渡し
Actionでは不可能。
code
App.svelte
<script module lang="ts">
import { type Attachment, createAttachmentKey } from "svelte/attachments";
import Component from "./Component.svelte";
function myAttachment(text: string): Attachment {
return (element: Element) => {
console.log(`attachment: ${text}`);
return () => {};
};
}
</script>
<script lang="ts">
let bool = $state(false);
const onclick = () => bool = !bool;
const props = {
text: "Component",
[createAttachmentKey()]: myAttachment("Component"),
}
</script>
<button type="button" {onclick}>toggle</button>
{#if bool}
<Component {...props} />
{/if}
Component.svelte
<script module lang="ts">
import type { Attachment } from "svelte/attachments";
interface Props {
text: string;
attachment: Attachment;
}
</script>
<script>
const { text, ...rests }: Props = $props();
</script>
<div {...rests}>{text}</div>
複数のAttachment受け渡し
code
App.svelte
<script module lang="ts">
import { type Attachment } from "svelte/attachments";
import Component from "./Component.svelte";
function myAttachment1(text: string): Attachment {
return (element: Element) => {
console.log(`attachment1: ${text}`);
return () => console.log("destroy 1");
};
}
function myAttachment2(text: string): Attachment {
return (element: Element) => {
console.log(`attachment2: ${text}`);
return () => console.log("destroy 2");
};
}
function combineAttachments(...attachments: Attachment[]): Attachment {
return (element: HTMLElement) => {
const cleanups = attachments.map(x => x(element));
return () => () => cleanups.forEach(x => x?.());
};
}
</script>
<script lang="ts">
let bool = $state(false);
const onclick = () => bool = !bool;
const attachment = combineAttachments(
myAttachment1("Hello"),
myAttachment2("Hi")
);
</script>
<button type="button" {onclick}>toggle</button>
{#if bool}
<Component {attachment} />
{/if}
<script module lang="ts">
import type { Attachment } from "svelte/attachments";
interface Props {
attachment?: Attachment;
}
</script>
<script>
const { attachment }: Props = $props();
</script>
<div {@attach attachment?.()}>Component</div>
雑記
通常の要素とコンポーネントで統一的に扱えるようになり、かなりすっきり書けるようになった印象です。個人的にはFalsyな値がエラーにならなくなったのが非常に嬉しいです。HTML要素の中に{@attach ...}
が出てくるのは違和感が凄いですが、慣れだと思うので積極的に使っていきたいと思います。
Discussion