Skip to content

Commit 927547d

Browse files
kenkunzRich-Harris
andauthored
docs: attachment tutorial (#1690)
* docs: rename action tutorial files -> attachments * docs: update "The attach tag" tutorial step * docs: update "Attachment factories" tutorial step * docs: update "Binding to component instances" link and example Previously referenced the Actions tutorial example; updated to reference the new Attachments example. * Apply suggestions from code review * Apply suggestions from code review * Update apps/svelte.dev/content/tutorial/01-svelte/08-attachments/01-attach/index.md Co-authored-by: Rich Harris <hello@rich-harris.dev> --------- Co-authored-by: Rich Harris <hello@rich-harris.dev>
1 parent e9c119a commit 927547d

File tree

17 files changed

+140
-137
lines changed

17 files changed

+140
-137
lines changed

apps/svelte.dev/content/tutorial/01-svelte/08-actions/01-actions/index.md

Lines changed: 0 additions & 64 deletions
This file was deleted.

apps/svelte.dev/content/tutorial/01-svelte/08-actions/02-adding-parameters-to-actions/index.md

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { on } from 'svelte/events';
2+
13
export function trapFocus(node) {
24
const previous = document.activeElement;
35

@@ -25,8 +27,6 @@ export function trapFocus(node) {
2527
}
2628
}
2729

28-
$effect(() => {
29-
focusable()[0]?.focus();
30-
// TODO finish writing the action
31-
});
30+
focusable()[0]?.focus();
31+
// TODO finish writing the action
3232
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script>
22
import Canvas from './Canvas.svelte';
3-
import { trapFocus } from './actions.svelte.js';
3+
import { trapFocus } from './attachments.svelte.js';
44
55
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];
66
@@ -27,7 +27,7 @@
2727
}
2828
}}
2929
>
30-
<div class="menu" use:trapFocus>
30+
<div class="menu" {@attach trapFocus}>
3131
<div class="colors">
3232
{#each colors as color}
3333
<button
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { on } from 'svelte/events';
2+
13
export function trapFocus(node) {
24
const previous = document.activeElement;
35

@@ -25,13 +27,11 @@ export function trapFocus(node) {
2527
}
2628
}
2729

28-
$effect(() => {
29-
focusable()[0]?.focus();
30-
node.addEventListener('keydown', handleKeydown);
30+
focusable()[0]?.focus();
31+
const off = on(node, 'keydown', handleKeydown);
3132

32-
return () => {
33-
node.removeEventListener('keydown', handleKeydown);
34-
previous?.focus();
35-
};
36-
});
33+
return () => {
34+
off();
35+
previous?.focus();
36+
};
3737
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
title: The attach tag
3+
---
4+
5+
Attachments are essentially element-level lifecycle functions. They're useful for things like:
6+
7+
- interfacing with third-party libraries
8+
- lazy-loaded images
9+
- tooltips
10+
- adding custom event handlers
11+
12+
In this app, you can scribble on the `<canvas>`, and change colours and brush size via the menu. But if you open the menu and cycle through the options with the Tab key, you'll soon find that the focus isn't _trapped_ inside the modal.
13+
14+
We can fix that with an attachment. Import `trapFocus` from `attachments.svelte.js`...
15+
16+
```svelte
17+
/// file: App.svelte
18+
<script>
19+
import Canvas from './Canvas.svelte';
20+
+++import { trapFocus } from './attachments.svelte.js';+++
21+
22+
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];
23+
24+
let selected = $state(colors[0]);
25+
let size = $state(10);
26+
let showMenu = $state(true);
27+
</script>
28+
```
29+
30+
...then add it to the menu with the `{@attach}` tag:
31+
32+
```svelte
33+
/// file: App.svelte
34+
<div class="menu" +++{@attach trapFocus}+++>
35+
```
36+
37+
Let's take a look at the `trapFocus` function in `attachments.svelte.js`. An attachment function is called with a `node` — the `<div class="menu">` in our case — when the node is mounted to the DOM. Attachments run inside an [effect](effects), so they re-run whenever any state read inside the function changes.
38+
39+
First, we need to add an event listener that intercepts Tab key presses:
40+
41+
```js
42+
/// file: attachments.svelte.js
43+
focusable()[0]?.focus();
44+
+++const off = on(node, 'keydown', handleKeydown);+++
45+
```
46+
47+
> [!NOTE] [`on`](/docs/svelte/svelte-events#on) is a wrapper around `addEventListener` that uses <a href="/docs/svelte/basic-markup#Events-Event-delegation">event delegation</a>. It returns a function that removes the handler.
48+
49+
Second, we need to do some cleanup when the node is unmounted — removing the event listener, and restoring focus to where it was before the element mounted. As with effects, an attachment can return a teardown function, which runs immediately before the attachment re-runs or after the element is removed from the DOM:
50+
51+
```js
52+
/// file: attachments.svelte.js
53+
focusable()[0]?.focus();
54+
const off = on(node, 'keydown', handleKeydown);
55+
56+
+++return () => {
57+
off();
58+
previous?.focus();
59+
};+++
60+
```
61+
62+
Now, when you open the menu, you can cycle through the options with the Tab key.
Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,14 @@
44
let content = $state('Hello!');
55
66
function tooltip(node) {
7-
$effect(() => {
8-
const tooltip = tippy(node);
9-
10-
return tooltip.destroy;
11-
});
7+
const tooltip = tippy(node);
8+
return tooltip.destroy;
129
}
1310
</script>
1411

1512
<input bind:value={content} />
1613

17-
<button use:tooltip>
14+
<button {@attach tooltip}>
1815
Hover me
1916
</button>
2017

0 commit comments

Comments
 (0)