From 3672e29e684701fb82b9a848e2726f5d36ed54b9 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Thu, 15 May 2025 22:51:01 +0200 Subject: [PATCH 01/14] 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 000000000000..53cc6718595e --- /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 2df0882e34d4..1736a82e77f2 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 948a19f4dd4b..9d40fe95f026 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 caf1342d0a09..bda77a8e7dfe 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 000000000000..2e53f0d29dd4 --- /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 000000000000..35079aa15e5c --- /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 bb958c510807..1c96afd61fb9 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 {}; } From 5a18de243bf61a16896ab4bd9ce7575a665cc595 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Fri, 16 May 2025 11:33:53 +0200 Subject: [PATCH 02/14] fix: typing of the utility Co-authored-by: Aidan Bleser <117548273+ieedan@users.noreply.github.com> --- packages/svelte/src/attachments/public.d.ts | 2 +- packages/svelte/types/index.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/attachments/public.d.ts b/packages/svelte/src/attachments/public.d.ts index bda77a8e7dfe..8680139bc7ea 100644 --- a/packages/svelte/src/attachments/public.d.ts +++ b/packages/svelte/src/attachments/public.d.ts @@ -15,7 +15,7 @@ export interface FromAction( ...args: undefined extends NoInfer ? [ - action: (node: Node, parameter?: Parameter) => void | ActionReturn, + action: (node: Node, parameter?: never) => void | ActionReturn, parameter?: () => NoInfer ] : [ diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 1c96afd61fb9..0822c02d6dda 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -641,7 +641,7 @@ declare module 'svelte/attachments' { ( ...args: undefined extends NoInfer ? [ - action: (node: Node, parameter?: Parameter) => void | ActionReturn, + action: (node: Node, parameter?: never) => void | ActionReturn, parameter?: () => NoInfer ] : [ @@ -673,7 +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 function fromAction(...args: undefined extends NoInfer ? [action: (node: Node, parameter?: never) => void | ActionReturn>, parameter?: (() => NoInfer) | undefined] : [action: (node: Node, parameter: Parameter) => void | ActionReturn>, parameter: () => NoInfer]): Attachment; export {}; } From 7d6fea7b14ac27159e0a8f021d41e5b87ecafb08 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 14:52:31 -0400 Subject: [PATCH 03/14] simplify implementation - create action first, then only create update/destroy effects if necessary --- packages/svelte/src/attachments/index.js | 50 ++++++++---------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js index 9d40fe95f026..0bb16cc514c8 100644 --- a/packages/svelte/src/attachments/index.js +++ b/packages/svelte/src/attachments/index.js @@ -1,9 +1,8 @@ -/** - * @import { FromAction } from "./public.js"; - * @import { ActionReturn } from "svelte/action"; - */ -import { render_effect } from 'svelte/internal/client'; +/** @import { FromAction } from './public.js' */ +import { noop, render_effect } from 'svelte/internal/client'; import { ATTACHMENT_KEY } from '../constants.js'; +import { untrack } from 'svelte'; +import { teardown } from '../internal/client/reactivity/effects.js'; /** * Creates an object key that will be recognised as an attachment when the object is spread onto an element, @@ -36,36 +35,21 @@ export function createAttachmentKey() { * attachments on Components but you have library provided actions. * @type {FromAction} */ -export function fromAction(action, args) { +export function fromAction(action, /** @type {() => any} */ get_arg = noop) { return (element) => { - /** - * @typedef {typeof args} Args; - */ + const { update, destroy } = untrack(() => action(element, get_arg()) ?? {}); - /** - * @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)); - }); + if (update) { + var ran = false; + render_effect(() => { + const arg = get_arg(); + if (ran) update(arg); + }); + ran = true; + } - render_effect(() => { - return () => { - destroy?.(); - }; - }); - ({ update, destroy } = /** @type {ActionReturn} */ ( - action(element, /** @type {any} */ (actual_args)) - )); + if (destroy) { + teardown(destroy); + } }; } From a4981b942cb7dffd36906a9b02734e897cdea4f2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 14:52:44 -0400 Subject: [PATCH 04/14] add since --- packages/svelte/src/attachments/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js index 0bb16cc514c8..bfd5525850df 100644 --- a/packages/svelte/src/attachments/index.js +++ b/packages/svelte/src/attachments/index.js @@ -34,6 +34,7 @@ export function createAttachmentKey() { * 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} + * @since 5.32 */ export function fromAction(action, /** @type {() => any} */ get_arg = noop) { return (element) => { From 42662e4008e7098095490abf61bb0b90b5481e59 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 15:04:14 -0400 Subject: [PATCH 05/14] regenerate --- packages/svelte/types/index.d.ts | 44 +++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 0822c02d6dda..5520ccabe9d6 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -673,7 +673,49 @@ declare module 'svelte/attachments' { * @since 5.29 */ export function createAttachmentKey(): symbol; - export function fromAction(...args: undefined extends NoInfer ? [action: (node: Node, parameter?: never) => void | ActionReturn>, parameter?: (() => NoInfer) | undefined] : [action: (node: Node, parameter: Parameter) => void | ActionReturn>, parameter: () => NoInfer]): Attachment; + export function fromAction(...args: undefined extends NoInfer ? [action: (node: Node, parameter?: never) => void | ActionReturn_1>, parameter?: (() => NoInfer) | undefined] : [action: (node: Node, parameter: Parameter) => void | ActionReturn_1>, parameter: () => NoInfer]): Attachment; + /** + * Actions can return an object containing the two properties defined in this interface. Both are optional. + * - update: An action can have a parameter. This method will be called whenever that parameter changes, + * immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn` both + * mean that the action accepts no parameters. + * - destroy: Method that is called after the element is unmounted + * + * Additionally, you can specify which additional attributes and events the action enables on the applied element. + * This applies to TypeScript typings only and has no effect at runtime. + * + * Example usage: + * ```ts + * interface Attributes { + * newprop?: string; + * 'on:event': (e: CustomEvent) => void; + * } + * + * export function myAction(node: HTMLElement, parameter: Parameter): ActionReturn { + * // ... + * return { + * update: (updatedParameter) => {...}, + * destroy: () => {...} + * }; + * } + * ``` + */ + interface ActionReturn_1< + Parameter = undefined, + Attributes extends Record = Record + > { + update?: (parameter: Parameter) => void; + destroy?: () => void; + /** + * ### DO NOT USE THIS + * This exists solely for type-checking and has no effect at runtime. + * Set this through the `Attributes` generic instead. + */ + $$_attributes?: Attributes; + } + + // Implementation notes: + // - undefined extends X instead of X extends undefined makes this work better with both strict and nonstrict mode export {}; } From 585d81c93909f71baa44363cb1f442e6365409b3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 15:42:08 -0400 Subject: [PATCH 06/14] remove FromAction interface --- packages/svelte/src/attachments/index.js | 19 +++++-- packages/svelte/src/attachments/public.d.ts | 14 ----- packages/svelte/types/index.d.ts | 63 +++------------------ 3 files changed, 23 insertions(+), 73 deletions(-) diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js index bfd5525850df..785a4e096a78 100644 --- a/packages/svelte/src/attachments/index.js +++ b/packages/svelte/src/attachments/index.js @@ -1,4 +1,5 @@ -/** @import { FromAction } from './public.js' */ +/** @import { Action, ActionReturn } from 'svelte/action' */ +/** @import { Attachment } from 'svelte/attachments' */ import { noop, render_effect } from 'svelte/internal/client'; import { ATTACHMENT_KEY } from '../constants.js'; import { untrack } from 'svelte'; @@ -33,17 +34,25 @@ export function createAttachmentKey() { /** * 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} + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * @template {EventTarget} E + * @template {unknown} T + * @param {Action | function(E, T): void | ActionReturn} action The action function + * @param {() => T} [fn] A function that returns the argument for the action + * @returns {Attachment} * @since 5.32 */ -export function fromAction(action, /** @type {() => any} */ get_arg = noop) { +export function fromAction(action, fn = /** @type {() => T} */ (noop)) { return (element) => { - const { update, destroy } = untrack(() => action(element, get_arg()) ?? {}); + const { update, destroy } = untrack(() => action(element, fn()) ?? {}); if (update) { var ran = false; render_effect(() => { - const arg = get_arg(); + const arg = fn(); if (ran) update(arg); }); ran = true; diff --git a/packages/svelte/src/attachments/public.d.ts b/packages/svelte/src/attachments/public.d.ts index 8680139bc7ea..44b0e2a225b4 100644 --- a/packages/svelte/src/attachments/public.d.ts +++ b/packages/svelte/src/attachments/public.d.ts @@ -11,18 +11,4 @@ export interface Attachment { (element: T): void | (() => void); } -export interface FromAction { - ( - ...args: undefined extends NoInfer - ? [ - action: (node: Node, parameter?: never) => void | ActionReturn, - parameter?: () => NoInfer - ] - : [ - action: (node: Node, parameter: Parameter) => void | ActionReturn, - parameter: () => NoInfer - ] - ): Attachment; -} - export * from './index.js'; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 5520ccabe9d6..056c00d7da2b 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -625,7 +625,7 @@ declare module 'svelte/animate' { } declare module 'svelte/attachments' { - import type { ActionReturn } from 'svelte/action'; + import type { ActionReturn, Action } 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. @@ -636,20 +636,6 @@ declare module 'svelte/attachments' { export interface Attachment { (element: T): void | (() => void); } - - export interface FromAction { - ( - ...args: undefined extends NoInfer - ? [ - action: (node: Node, parameter?: never) => 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 @@ -673,49 +659,18 @@ declare module 'svelte/attachments' { * @since 5.29 */ export function createAttachmentKey(): symbol; - export function fromAction(...args: undefined extends NoInfer ? [action: (node: Node, parameter?: never) => void | ActionReturn_1>, parameter?: (() => NoInfer) | undefined] : [action: (node: Node, parameter: Parameter) => void | ActionReturn_1>, parameter: () => NoInfer]): Attachment; /** - * Actions can return an object containing the two properties defined in this interface. Both are optional. - * - update: An action can have a parameter. This method will be called whenever that parameter changes, - * immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn` both - * mean that the action accepts no parameters. - * - destroy: Method that is called after the element is unmounted - * - * Additionally, you can specify which additional attributes and events the action enables on the applied element. - * This applies to TypeScript typings only and has no effect at runtime. + * 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. * - * Example usage: - * ```ts - * interface Attributes { - * newprop?: string; - * 'on:event': (e: CustomEvent) => void; - * } + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. * - * export function myAction(node: HTMLElement, parameter: Parameter): ActionReturn { - * // ... - * return { - * update: (updatedParameter) => {...}, - * destroy: () => {...} - * }; - * } - * ``` + * @param action The action function + * @param fn A function that returns the argument for the action + * @since 5.32 */ - interface ActionReturn_1< - Parameter = undefined, - Attributes extends Record = Record - > { - update?: (parameter: Parameter) => void; - destroy?: () => void; - /** - * ### DO NOT USE THIS - * This exists solely for type-checking and has no effect at runtime. - * Set this through the `Attributes` generic instead. - */ - $$_attributes?: Attributes; - } - - // Implementation notes: - // - undefined extends X instead of X extends undefined makes this work better with both strict and nonstrict mode + export function fromAction(action: Action | ((arg0: E, arg1: T) => void | ActionReturn), fn?: (() => T) | undefined): Attachment; export {}; } From 7c2a49e326c0b38a06ec7939c16cf41074eef75f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 16:54:31 -0400 Subject: [PATCH 07/14] overload --- packages/svelte/src/attachments/index.js | 16 +++++++++++++++- packages/svelte/types/index.d.ts | 16 ++++------------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js index 785a4e096a78..514c9433f444 100644 --- a/packages/svelte/src/attachments/index.js +++ b/packages/svelte/src/attachments/index.js @@ -31,6 +31,20 @@ export function createAttachmentKey() { return Symbol(ATTACHMENT_KEY); } +/** + * @template {EventTarget} E + * @template {unknown} T + * @overload + * @param {Action | function(E, T): void | ActionReturn} action The action function + * @param {() => T} fn A function that returns the argument for the action + * @returns {Attachment} + */ +/** + * @template {EventTarget} E + * @overload + * @param {Action | function(E): void | ActionReturn} action The action function + * @returns {Attachment} + */ /** * 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. @@ -41,7 +55,7 @@ export function createAttachmentKey() { * @template {EventTarget} E * @template {unknown} T * @param {Action | function(E, T): void | ActionReturn} action The action function - * @param {() => T} [fn] A function that returns the argument for the action + * @param {() => T} fn A function that returns the argument for the action * @returns {Attachment} * @since 5.32 */ diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 14a04f1f3a45..067e04db5b3e 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -659,18 +659,10 @@ declare module 'svelte/attachments' { * @since 5.29 */ export function createAttachmentKey(): symbol; - /** - * 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. - * - * Note that the second argument, if provided, must be a function that _returns_ the argument to the - * action function, not the argument itself. - * - * @param action The action function - * @param fn A function that returns the argument for the action - * @since 5.32 - */ - export function fromAction(action: Action | ((arg0: E, arg1: T) => void | ActionReturn), fn?: (() => T) | undefined): Attachment; + + export function fromAction(action: Action | ((arg0: E, arg1: T) => void | ActionReturn), fn: () => T): Attachment; + + export function fromAction(action: Action | ((arg0: E) => void | ActionReturn)): Attachment; export {}; } From d190509144b08dbd913e44d6b1ac8083151efa2e Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Mon, 19 May 2025 22:57:07 +0200 Subject: [PATCH 08/14] fix: use typedef instead of exported interface --- packages/svelte/src/attachments/index.js | 21 +++++++- packages/svelte/src/attachments/public.d.ts | 16 ------ packages/svelte/types/index.d.ts | 59 +-------------------- 3 files changed, 22 insertions(+), 74 deletions(-) diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js index bfd5525850df..ec568f1bcc24 100644 --- a/packages/svelte/src/attachments/index.js +++ b/packages/svelte/src/attachments/index.js @@ -1,4 +1,7 @@ -/** @import { FromAction } from './public.js' */ +/** + * @import { Attachment } from "./public.js"; + * @import { ActionReturn } from "svelte/action"; + */ import { noop, render_effect } from 'svelte/internal/client'; import { ATTACHMENT_KEY } from '../constants.js'; import { untrack } from 'svelte'; @@ -30,6 +33,22 @@ export function createAttachmentKey() { return Symbol(ATTACHMENT_KEY); } +/** + * @template {EventTarget} [Element=HTMLElement] + * @template {*} [Par=unknown] + * @typedef {( + * ...args: (undefined extends NoInfer + * ? [ + * action: (node: Node, parameter?: never) => void | ActionReturn, + * parameter?: () => NoInfer + * ] + * : [ + * action: (node: Node, parameter: Parameter) => void | ActionReturn, + * parameter: () => NoInfer + * ]) + * ) => Attachment} FromAction + */ + /** * 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. diff --git a/packages/svelte/src/attachments/public.d.ts b/packages/svelte/src/attachments/public.d.ts index 8680139bc7ea..caf1342d0a09 100644 --- a/packages/svelte/src/attachments/public.d.ts +++ b/packages/svelte/src/attachments/public.d.ts @@ -1,5 +1,3 @@ -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. @@ -11,18 +9,4 @@ export interface Attachment { (element: T): void | (() => void); } -export interface FromAction { - ( - ...args: undefined extends NoInfer - ? [ - action: (node: Node, parameter?: never) => void | ActionReturn, - parameter?: () => NoInfer - ] - : [ - action: (node: Node, parameter: Parameter) => void | ActionReturn, - parameter: () => NoInfer - ] - ): Attachment; -} - export * from './index.js'; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index fa0d7014220f..d1b3ecd9d5ef 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -636,20 +636,6 @@ declare module 'svelte/attachments' { export interface Attachment { (element: T): void | (() => void); } - - export interface FromAction { - ( - ...args: undefined extends NoInfer - ? [ - action: (node: Node, parameter?: never) => 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 @@ -673,49 +659,8 @@ declare module 'svelte/attachments' { * @since 5.29 */ export function createAttachmentKey(): symbol; - export function fromAction(...args: undefined extends NoInfer ? [action: (node: Node, parameter?: never) => void | ActionReturn_1>, parameter?: (() => NoInfer) | undefined] : [action: (node: Node, parameter: Parameter) => void | ActionReturn_1>, parameter: () => NoInfer]): Attachment; - /** - * Actions can return an object containing the two properties defined in this interface. Both are optional. - * - update: An action can have a parameter. This method will be called whenever that parameter changes, - * immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn` both - * mean that the action accepts no parameters. - * - destroy: Method that is called after the element is unmounted - * - * Additionally, you can specify which additional attributes and events the action enables on the applied element. - * This applies to TypeScript typings only and has no effect at runtime. - * - * Example usage: - * ```ts - * interface Attributes { - * newprop?: string; - * 'on:event': (e: CustomEvent) => void; - * } - * - * export function myAction(node: HTMLElement, parameter: Parameter): ActionReturn { - * // ... - * return { - * update: (updatedParameter) => {...}, - * destroy: () => {...} - * }; - * } - * ``` - */ - interface ActionReturn_1< - Parameter = undefined, - Attributes extends Record = Record - > { - update?: (parameter: Parameter) => void; - destroy?: () => void; - /** - * ### DO NOT USE THIS - * This exists solely for type-checking and has no effect at runtime. - * Set this through the `Attributes` generic instead. - */ - $$_attributes?: Attributes; - } - - // Implementation notes: - // - undefined extends X instead of X extends undefined makes this work better with both strict and nonstrict mode + export function fromAction(...args: undefined extends NoInfer ? [action: (node: Node, parameter?: never) => void | ActionReturn, parameter?: (() => NoInfer) | undefined] : [action: (node: Node, parameter: Parameter) => void | ActionReturn, parameter: () => NoInfer]): Attachment; + export type FromAction = (...args: (undefined extends NoInfer ? [action: (node: Node, parameter?: never) => void | ActionReturn, parameter?: () => NoInfer] : [action: (node: Node, parameter: Parameter) => void | ActionReturn, parameter: () => NoInfer])) => Attachment; export {}; } From 11e086bb2596301302fef27c6a3c9d30aee57da6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 17:18:14 -0400 Subject: [PATCH 09/14] get rid of the arg0, arg1 stuff --- packages/svelte/src/attachments/index.js | 6 +++--- packages/svelte/types/index.d.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js index 514c9433f444..0ff1f60fb166 100644 --- a/packages/svelte/src/attachments/index.js +++ b/packages/svelte/src/attachments/index.js @@ -35,14 +35,14 @@ export function createAttachmentKey() { * @template {EventTarget} E * @template {unknown} T * @overload - * @param {Action | function(E, T): void | ActionReturn} action The action function + * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function * @param {() => T} fn A function that returns the argument for the action * @returns {Attachment} */ /** * @template {EventTarget} E * @overload - * @param {Action | function(E): void | ActionReturn} action The action function + * @param {Action | ((element: E) => void | ActionReturn)} action The action function * @returns {Attachment} */ /** @@ -54,7 +54,7 @@ export function createAttachmentKey() { * * @template {EventTarget} E * @template {unknown} T - * @param {Action | function(E, T): void | ActionReturn} action The action function + * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function * @param {() => T} fn A function that returns the argument for the action * @returns {Attachment} * @since 5.32 diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 067e04db5b3e..59c8d7ff153d 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -625,7 +625,7 @@ declare module 'svelte/animate' { } declare module 'svelte/attachments' { - import type { ActionReturn, Action } from 'svelte/action'; + import type { Action, 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. @@ -660,9 +660,9 @@ declare module 'svelte/attachments' { */ export function createAttachmentKey(): symbol; - export function fromAction(action: Action | ((arg0: E, arg1: T) => void | ActionReturn), fn: () => T): Attachment; + export function fromAction(action: Action | ((element: E, arg: T) => void | ActionReturn), fn: () => T): Attachment; - export function fromAction(action: Action | ((arg0: E) => void | ActionReturn)): Attachment; + export function fromAction(action: Action | ((element: E) => void | ActionReturn)): Attachment; export {}; } From c7bd1750da186bc48d4bd44efb0c814e8024c3e2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 17:20:59 -0400 Subject: [PATCH 10/14] oops --- documentation/docs/03-template-syntax/09-@attach.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md index 0d43ab6cfa26..4b9303023bdd 100644 --- a/documentation/docs/03-template-syntax/09-@attach.md +++ b/documentation/docs/03-template-syntax/09-@attach.md @@ -163,7 +163,7 @@ To add attachments to an object that will be spread onto a component or element, ## 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`](svelte/attachments#fromAction) utility. +If you want to use this functionality on components but you are using a library that only provides actions you can use the [`fromAction`](svelte-attachments#fromAction) utility. 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. @@ -181,4 +181,4 @@ This function accept an action as the first argument and a function returning th {@attach fromAction(log, () => count)} > {count} - \ No newline at end of file + From 09da0215729eb24ec67cd6427b2ec7855d4a6cda Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 17:33:34 -0400 Subject: [PATCH 11/14] god i hate overloads --- packages/svelte/src/attachments/index.js | 38 ++++++++++++++++++++++-- packages/svelte/types/index.d.ts | 32 ++++++++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js index 0ff1f60fb166..f5e26571c771 100644 --- a/packages/svelte/src/attachments/index.js +++ b/packages/svelte/src/attachments/index.js @@ -32,6 +32,19 @@ export function createAttachmentKey() { } /** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` * @template {EventTarget} E * @template {unknown} T * @overload @@ -40,18 +53,39 @@ export function createAttachmentKey() { * @returns {Attachment} */ /** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` * @template {EventTarget} E * @overload * @param {Action | ((element: E) => void | ActionReturn)} action The action function * @returns {Attachment} */ /** - * 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. + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. * * Note that the second argument, if provided, must be a function that _returns_ the argument to the * action function, not the argument itself. * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * * @template {EventTarget} E * @template {unknown} T * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 59c8d7ff153d..c03da5685625 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -659,9 +659,37 @@ declare module 'svelte/attachments' { * @since 5.29 */ export function createAttachmentKey(): symbol; - + /** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * */ export function fromAction(action: Action | ((element: E, arg: T) => void | ActionReturn), fn: () => T): Attachment; - + /** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * */ export function fromAction(action: Action | ((element: E) => void | ActionReturn)): Attachment; export {}; From 5a963d14d74c9118d3e3035b9f851eee7194f163 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 17:35:00 -0400 Subject: [PATCH 12/14] defer to the reference documentation --- .../docs/03-template-syntax/09-@attach.md | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md index 4b9303023bdd..b25fbb32a678 100644 --- a/documentation/docs/03-template-syntax/09-@attach.md +++ b/documentation/docs/03-template-syntax/09-@attach.md @@ -163,22 +163,4 @@ To add attachments to an object that will be spread onto a component or element, ## 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`](svelte-attachments#fromAction) utility. - -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 - - - +If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components. From 7e1e277502e9809950b1309028950b53c05635fe Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 17:49:56 -0400 Subject: [PATCH 13/14] damn ur weird typescript --- packages/svelte/src/attachments/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js index f5e26571c771..dc2f3e93d791 100644 --- a/packages/svelte/src/attachments/index.js +++ b/packages/svelte/src/attachments/index.js @@ -1,5 +1,5 @@ -/** @import { Action, ActionReturn } from 'svelte/action' */ -/** @import { Attachment } from 'svelte/attachments' */ +/** @import { Action, ActionReturn } from '../action/public' */ +/** @import { Attachment } from './public' */ import { noop, render_effect } from 'svelte/internal/client'; import { ATTACHMENT_KEY } from '../constants.js'; import { untrack } from 'svelte'; From e12c331e444d473304665a58bd42dfcf97969eab Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 17:58:56 -0400 Subject: [PATCH 14/14] gah --- packages/svelte/types/index.d.ts | 70 +++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index c03da5685625..fcc1ec315cdd 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -625,7 +625,6 @@ declare module 'svelte/animate' { } declare module 'svelte/attachments' { - import type { Action, 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. @@ -691,6 +690,75 @@ declare module 'svelte/attachments' { * ``` * */ export function fromAction(action: Action | ((element: E) => void | ActionReturn)): Attachment; + /** + * Actions can return an object containing the two properties defined in this interface. Both are optional. + * - update: An action can have a parameter. This method will be called whenever that parameter changes, + * immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn` both + * mean that the action accepts no parameters. + * - destroy: Method that is called after the element is unmounted + * + * Additionally, you can specify which additional attributes and events the action enables on the applied element. + * This applies to TypeScript typings only and has no effect at runtime. + * + * Example usage: + * ```ts + * interface Attributes { + * newprop?: string; + * 'on:event': (e: CustomEvent) => void; + * } + * + * export function myAction(node: HTMLElement, parameter: Parameter): ActionReturn { + * // ... + * return { + * update: (updatedParameter) => {...}, + * destroy: () => {...} + * }; + * } + * ``` + */ + interface ActionReturn< + Parameter = undefined, + Attributes extends Record = Record + > { + update?: (parameter: Parameter) => void; + destroy?: () => void; + /** + * ### DO NOT USE THIS + * This exists solely for type-checking and has no effect at runtime. + * Set this through the `Attributes` generic instead. + */ + $$_attributes?: Attributes; + } + + /** + * Actions are functions that are called when an element is created. + * You can use this interface to type such actions. + * The following example defines an action that only works on `
` elements + * and optionally accepts a parameter which it has a default value for: + * ```ts + * export const myAction: Action = (node, param = { someProperty: true }) => { + * // ... + * } + * ``` + * `Action` and `Action` both signal that the action accepts no parameters. + * + * You can return an object with methods `update` and `destroy` from the function and type which additional attributes and events it has. + * See interface `ActionReturn` for more details. + */ + interface Action< + Element = HTMLElement, + Parameter = undefined, + Attributes extends Record = Record + > { + ( + ...args: undefined extends Parameter + ? [node: Node, parameter?: Parameter] + : [node: Node, parameter: Parameter] + ): void | ActionReturn; + } + + // Implementation notes: + // - undefined extends X instead of X extends undefined makes this work better with both strict and nonstrict mode export {}; }