feat: attachments `fromAction` utility

pull/15958/head
paoloricciuti 4 months ago
parent a5a0b49003
commit 3672e29e68

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: attachments `fromAction` utility

@ -126,6 +126,29 @@ This allows you to create _wrapper components_ that augment elements ([demo](/pl
</Button>
```
### 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
<script>
import Button from "./Button.svelte";
import { log } from "log-my-number";
import { fromAction } from "svelte/attachments";
let count = $state(0);
</script>
<Button
onclick={() => count++}
{@attach fromAction(log, () => count)}
>
{count}
</Button>
```
## Creating attachments programmatically
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).

@ -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<NonNullable<Args>> | 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))
));
};
}

@ -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<T extends EventTarget = Element> {
(element: T): void | (() => void);
}
export interface FromAction<Element extends EventTarget = HTMLElement, Par = unknown> {
<Node extends Element, Parameter extends Par>(
...args: undefined extends NoInfer<Parameter>
? [
action: (node: Node, parameter?: Parameter) => void | ActionReturn<Parameter>,
parameter?: () => NoInfer<Parameter>
]
: [
action: (node: Node, parameter: Parameter) => void | ActionReturn<Parameter>,
parameter: () => NoInfer<Parameter>
]
): Attachment<Node>;
}
export * from './index.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'
]);
}
});

@ -0,0 +1,37 @@
<script>
import { fromAction } from 'svelte/attachments';
let { count = 0 } = $props();
function test(node, thing) {
const kind = node.dataset.kind;
console.log('create', thing, kind);
let t = thing;
const controller = new AbortController();
node.addEventListener(
'click',
() => {
console.log(t);
},
{
signal: controller.signal
}
);
return {
update(new_thing) {
console.log('update', new_thing, kind);
t = new_thing;
},
destroy() {
console.log('destroy', kind);
controller.abort();
}
};
}
</script>
{#if count < 2}
<button data-kind="action" use:test={count}></button>
<button data-kind="attachment" {@attach fromAction(test, ()=>count)}></button>
{/if}
<button onclick={()=> count++}></button>

@ -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<T extends EventTarget = Element> {
(element: T): void | (() => void);
}
export interface FromAction<Element extends EventTarget = HTMLElement, Par = unknown> {
<Node extends Element, Parameter extends Par>(
...args: undefined extends NoInfer<Parameter>
? [
action: (node: Node, parameter?: Parameter) => void | ActionReturn<Parameter>,
parameter?: () => NoInfer<Parameter>
]
: [
action: (node: Node, parameter: Parameter) => void | ActionReturn<Parameter>,
parameter: () => NoInfer<Parameter>
]
): Attachment<Node>;
}
/**
* 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<Node extends HTMLElement, Parameter extends any>(...args: undefined extends NoInfer<Parameter> ? [action: (node: Node, parameter?: Parameter | undefined) => void | ActionReturn<Parameter, Record<never, any>>, parameter?: (() => NoInfer<Parameter>) | undefined] : [action: (node: Node, parameter: Parameter) => void | ActionReturn<Parameter, Record<never, any>>, parameter: () => NoInfer<Parameter>]): Attachment<Node>;
export {};
}

Loading…
Cancel
Save