From 3672e29e684701fb82b9a848e2726f5d36ed54b9 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Thu, 15 May 2025 22:51:01 +0200 Subject: [PATCH] feat: attachments `fromAction` utility --- .changeset/wise-tigers-happen.md | 5 + .../docs/03-template-syntax/09-@attach.md | 23 ++++ packages/svelte/src/attachments/index.js | 44 +++++++ packages/svelte/src/attachments/public.d.ts | 16 +++ .../samples/attachment-from-action/_config.js | 116 ++++++++++++++++++ .../attachment-from-action/main.svelte | 37 ++++++ packages/svelte/types/index.d.ts | 16 +++ 7 files changed, 257 insertions(+) create mode 100644 .changeset/wise-tigers-happen.md create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte 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 {}; }