Skip to content

Commit 3672e29

Browse files
committed
feat: attachments fromAction utility
1 parent a5a0b49 commit 3672e29

File tree

7 files changed

+257
-0
lines changed

7 files changed

+257
-0
lines changed

.changeset/wise-tigers-happen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: attachments `fromAction` utility

documentation/docs/03-template-syntax/[email protected]

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,29 @@ This allows you to create _wrapper components_ that augment elements ([demo](/pl
126126
</Button>
127127
```
128128

129+
### Converting actions to attachments
130+
131+
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.
132+
133+
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.
134+
135+
```svelte
136+
<script>
137+
import Button from "./Button.svelte";
138+
import { log } from "log-my-number";
139+
import { fromAction } from "svelte/attachments";
140+
141+
let count = $state(0);
142+
</script>
143+
144+
<Button
145+
onclick={() => count++}
146+
{@attach fromAction(log, () => count)}
147+
>
148+
{count}
149+
</Button>
150+
```
151+
129152
## Creating attachments programmatically
130153

131154
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).

packages/svelte/src/attachments/index.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* @import { FromAction } from "./public.js";
3+
* @import { ActionReturn } from "svelte/action";
4+
*/
5+
import { render_effect } from 'svelte/internal/client';
16
import { ATTACHMENT_KEY } from '../constants.js';
27

38
/**
@@ -25,3 +30,42 @@ import { ATTACHMENT_KEY } from '../constants.js';
2530
export function createAttachmentKey() {
2631
return Symbol(ATTACHMENT_KEY);
2732
}
33+
34+
/**
35+
* Converts an Action into an Attachment keeping the same behavior. It's useful if you want to start using
36+
* attachments on Components but you have library provided actions.
37+
* @type {FromAction}
38+
*/
39+
export function fromAction(action, args) {
40+
return (element) => {
41+
/**
42+
* @typedef {typeof args} Args;
43+
*/
44+
45+
/**
46+
* @type {ReturnType<NonNullable<Args>> | undefined}
47+
*/
48+
let actual_args;
49+
/**
50+
* @type {ActionReturn['update']}
51+
*/
52+
let update;
53+
/**
54+
* @type {ActionReturn['destroy']}
55+
*/
56+
let destroy;
57+
render_effect(() => {
58+
actual_args = args?.();
59+
update?.(/** @type {any} */ (actual_args));
60+
});
61+
62+
render_effect(() => {
63+
return () => {
64+
destroy?.();
65+
};
66+
});
67+
({ update, destroy } = /** @type {ActionReturn} */ (
68+
action(element, /** @type {any} */ (actual_args))
69+
));
70+
};
71+
}

packages/svelte/src/attachments/public.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ActionReturn } from 'svelte/action';
2+
13
/**
24
* An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted
35
* 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> {
911
(element: T): void | (() => void);
1012
}
1113

14+
export interface FromAction<Element extends EventTarget = HTMLElement, Par = unknown> {
15+
<Node extends Element, Parameter extends Par>(
16+
...args: undefined extends NoInfer<Parameter>
17+
? [
18+
action: (node: Node, parameter?: Parameter) => void | ActionReturn<Parameter>,
19+
parameter?: () => NoInfer<Parameter>
20+
]
21+
: [
22+
action: (node: Node, parameter: Parameter) => void | ActionReturn<Parameter>,
23+
parameter: () => NoInfer<Parameter>
24+
]
25+
): Attachment<Node>;
26+
}
27+
1228
export * from './index.js';
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { ok, test } from '../../test';
2+
import { flushSync } from 'svelte';
3+
4+
export default test({
5+
async test({ assert, target, logs }) {
6+
const [btn, btn2, btn3] = target.querySelectorAll('button');
7+
8+
// both logs on creation it will not log on change
9+
assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment']);
10+
11+
// clicking the first button logs the right value
12+
flushSync(() => {
13+
btn?.click();
14+
});
15+
assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0]);
16+
17+
// clicking the second button logs the right value
18+
flushSync(() => {
19+
btn2?.click();
20+
});
21+
assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0, 0]);
22+
23+
// updating the arguments logs the update function for both
24+
flushSync(() => {
25+
btn3?.click();
26+
});
27+
assert.deepEqual(logs, [
28+
'create',
29+
0,
30+
'action',
31+
'create',
32+
0,
33+
'attachment',
34+
0,
35+
0,
36+
'update',
37+
1,
38+
'action',
39+
'update',
40+
1,
41+
'attachment'
42+
]);
43+
44+
// clicking the first button again shows the right value
45+
flushSync(() => {
46+
btn?.click();
47+
});
48+
assert.deepEqual(logs, [
49+
'create',
50+
0,
51+
'action',
52+
'create',
53+
0,
54+
'attachment',
55+
0,
56+
0,
57+
'update',
58+
1,
59+
'action',
60+
'update',
61+
1,
62+
'attachment',
63+
1
64+
]);
65+
66+
// clicking the second button again shows the right value
67+
flushSync(() => {
68+
btn2?.click();
69+
});
70+
assert.deepEqual(logs, [
71+
'create',
72+
0,
73+
'action',
74+
'create',
75+
0,
76+
'attachment',
77+
0,
78+
0,
79+
'update',
80+
1,
81+
'action',
82+
'update',
83+
1,
84+
'attachment',
85+
1,
86+
1
87+
]);
88+
89+
// unmounting logs the destroy function for both
90+
flushSync(() => {
91+
btn3?.click();
92+
});
93+
assert.deepEqual(logs, [
94+
'create',
95+
0,
96+
'action',
97+
'create',
98+
0,
99+
'attachment',
100+
0,
101+
0,
102+
'update',
103+
1,
104+
'action',
105+
'update',
106+
1,
107+
'attachment',
108+
1,
109+
1,
110+
'destroy',
111+
'action',
112+
'destroy',
113+
'attachment'
114+
]);
115+
}
116+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<script>
2+
import { fromAction } from 'svelte/attachments';
3+
let { count = 0 } = $props();
4+
5+
function test(node, thing) {
6+
const kind = node.dataset.kind;
7+
console.log('create', thing, kind);
8+
let t = thing;
9+
const controller = new AbortController();
10+
node.addEventListener(
11+
'click',
12+
() => {
13+
console.log(t);
14+
},
15+
{
16+
signal: controller.signal
17+
}
18+
);
19+
return {
20+
update(new_thing) {
21+
console.log('update', new_thing, kind);
22+
t = new_thing;
23+
},
24+
destroy() {
25+
console.log('destroy', kind);
26+
controller.abort();
27+
}
28+
};
29+
}
30+
</script>
31+
32+
{#if count < 2}
33+
<button data-kind="action" use:test={count}></button>
34+
<button data-kind="attachment" {@attach fromAction(test, ()=>count)}></button>
35+
{/if}
36+
37+
<button onclick={()=> count++}></button>

packages/svelte/types/index.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,7 @@ declare module 'svelte/animate' {
625625
}
626626

627627
declare module 'svelte/attachments' {
628+
import type { ActionReturn } from 'svelte/action';
628629
/**
629630
* An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted
630631
* 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' {
635636
export interface Attachment<T extends EventTarget = Element> {
636637
(element: T): void | (() => void);
637638
}
639+
640+
export interface FromAction<Element extends EventTarget = HTMLElement, Par = unknown> {
641+
<Node extends Element, Parameter extends Par>(
642+
...args: undefined extends NoInfer<Parameter>
643+
? [
644+
action: (node: Node, parameter?: Parameter) => void | ActionReturn<Parameter>,
645+
parameter?: () => NoInfer<Parameter>
646+
]
647+
: [
648+
action: (node: Node, parameter: Parameter) => void | ActionReturn<Parameter>,
649+
parameter: () => NoInfer<Parameter>
650+
]
651+
): Attachment<Node>;
652+
}
638653
/**
639654
* Creates an object key that will be recognised as an attachment when the object is spread onto an element,
640655
* as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though
@@ -658,6 +673,7 @@ declare module 'svelte/attachments' {
658673
* @since 5.29
659674
*/
660675
export function createAttachmentKey(): symbol;
676+
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>;
661677

662678
export {};
663679
}

0 commit comments

Comments
 (0)