diff --git a/.changeset/wise-tigers-happen.md b/.changeset/wise-tigers-happen.md
new file mode 100644
index 0000000000..53cc671859
--- /dev/null
+++ b/.changeset/wise-tigers-happen.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: attachments `fromAction` utility
diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md
index 2df0882e34..1736a82e77 100644
--- a/documentation/docs/03-template-syntax/09-@attach.md
+++ b/documentation/docs/03-template-syntax/09-@attach.md
@@ -126,6 +126,29 @@ This allows you to create _wrapper components_ that augment elements ([demo](/pl
```
+### Converting actions to attachments
+
+If you want to use this functionality on Components but you are using a library that only provides actions you can use the `fromAction` utility exported from `svelte/attachments` to convert between the two.
+
+This function accept an action as the first argument and a function returning the arguments of the action as the second argument and returns an attachment.
+
+```svelte
+
+
+
+```
+
## Creating attachments programmatically
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js
index 948a19f4dd..9d40fe95f0 100644
--- a/packages/svelte/src/attachments/index.js
+++ b/packages/svelte/src/attachments/index.js
@@ -1,3 +1,8 @@
+/**
+ * @import { FromAction } from "./public.js";
+ * @import { ActionReturn } from "svelte/action";
+ */
+import { render_effect } from 'svelte/internal/client';
import { ATTACHMENT_KEY } from '../constants.js';
/**
@@ -25,3 +30,42 @@ import { ATTACHMENT_KEY } from '../constants.js';
export function createAttachmentKey() {
return Symbol(ATTACHMENT_KEY);
}
+
+/**
+ * Converts an Action into an Attachment keeping the same behavior. It's useful if you want to start using
+ * attachments on Components but you have library provided actions.
+ * @type {FromAction}
+ */
+export function fromAction(action, args) {
+ return (element) => {
+ /**
+ * @typedef {typeof args} Args;
+ */
+
+ /**
+ * @type {ReturnType> | undefined}
+ */
+ let actual_args;
+ /**
+ * @type {ActionReturn['update']}
+ */
+ let update;
+ /**
+ * @type {ActionReturn['destroy']}
+ */
+ let destroy;
+ render_effect(() => {
+ actual_args = args?.();
+ update?.(/** @type {any} */ (actual_args));
+ });
+
+ render_effect(() => {
+ return () => {
+ destroy?.();
+ };
+ });
+ ({ update, destroy } = /** @type {ActionReturn} */ (
+ action(element, /** @type {any} */ (actual_args))
+ ));
+ };
+}
diff --git a/packages/svelte/src/attachments/public.d.ts b/packages/svelte/src/attachments/public.d.ts
index caf1342d0a..bda77a8e7d 100644
--- a/packages/svelte/src/attachments/public.d.ts
+++ b/packages/svelte/src/attachments/public.d.ts
@@ -1,3 +1,5 @@
+import type { ActionReturn } from 'svelte/action';
+
/**
* An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted
* to the DOM, and optionally returns a function that is called when the element is later removed.
@@ -9,4 +11,18 @@ export interface Attachment {
(element: T): void | (() => void);
}
+export interface FromAction {
+ (
+ ...args: undefined extends NoInfer
+ ? [
+ action: (node: Node, parameter?: Parameter) => void | ActionReturn,
+ parameter?: () => NoInfer
+ ]
+ : [
+ action: (node: Node, parameter: Parameter) => void | ActionReturn,
+ parameter: () => NoInfer
+ ]
+ ): Attachment;
+}
+
export * from './index.js';
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js
new file mode 100644
index 0000000000..2e53f0d29d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js
@@ -0,0 +1,116 @@
+import { ok, test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [btn, btn2, btn3] = target.querySelectorAll('button');
+
+ // both logs on creation it will not log on change
+ assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment']);
+
+ // clicking the first button logs the right value
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0]);
+
+ // clicking the second button logs the right value
+ flushSync(() => {
+ btn2?.click();
+ });
+ assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0, 0]);
+
+ // updating the arguments logs the update function for both
+ flushSync(() => {
+ btn3?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment'
+ ]);
+
+ // clicking the first button again shows the right value
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment',
+ 1
+ ]);
+
+ // clicking the second button again shows the right value
+ flushSync(() => {
+ btn2?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment',
+ 1,
+ 1
+ ]);
+
+ // unmounting logs the destroy function for both
+ flushSync(() => {
+ btn3?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment',
+ 1,
+ 1,
+ 'destroy',
+ 'action',
+ 'destroy',
+ 'attachment'
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte
new file mode 100644
index 0000000000..35079aa15e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte
@@ -0,0 +1,37 @@
+
+
+{#if count < 2}
+
+
+{/if}
+
+
\ No newline at end of file
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index bb958c5108..1c96afd61f 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -625,6 +625,7 @@ declare module 'svelte/animate' {
}
declare module 'svelte/attachments' {
+ import type { ActionReturn } from 'svelte/action';
/**
* An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted
* to the DOM, and optionally returns a function that is called when the element is later removed.
@@ -635,6 +636,20 @@ declare module 'svelte/attachments' {
export interface Attachment {
(element: T): void | (() => void);
}
+
+ export interface FromAction {
+ (
+ ...args: undefined extends NoInfer
+ ? [
+ action: (node: Node, parameter?: Parameter) => void | ActionReturn,
+ parameter?: () => NoInfer
+ ]
+ : [
+ action: (node: Node, parameter: Parameter) => void | ActionReturn,
+ parameter: () => NoInfer
+ ]
+ ): Attachment;
+ }
/**
* Creates an object key that will be recognised as an attachment when the object is spread onto an element,
* as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though
@@ -658,6 +673,7 @@ declare module 'svelte/attachments' {
* @since 5.29
*/
export function createAttachmentKey(): symbol;
+ export function fromAction(...args: undefined extends NoInfer ? [action: (node: Node, parameter?: Parameter | undefined) => void | ActionReturn>, parameter?: (() => NoInfer) | undefined] : [action: (node: Node, parameter: Parameter) => void | ActionReturn>, parameter: () => NoInfer]): Attachment;
export {};
}