Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { on } from 'svelte/events';

export function trapFocus(node) {
const previous = document.activeElement;

Expand Down Expand Up @@ -25,8 +27,6 @@ export function trapFocus(node) {
}
}

$effect(() => {
focusable()[0]?.focus();
// TODO finish writing the action
});
focusable()[0]?.focus();
// TODO finish writing the action
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script>
import Canvas from './Canvas.svelte';
import { trapFocus } from './actions.svelte.js';
import { trapFocus } from './attachments.svelte.js';

const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];

Expand All @@ -27,7 +27,7 @@
}
}}
>
<div class="menu" use:trapFocus>
<div class="menu" {@attach trapFocus}>
<div class="colors">
{#each colors as color}
<button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { on } from 'svelte/events';

export function trapFocus(node) {
const previous = document.activeElement;

Expand Down Expand Up @@ -25,13 +27,11 @@ export function trapFocus(node) {
}
}

$effect(() => {
focusable()[0]?.focus();
node.addEventListener('keydown', handleKeydown);
focusable()[0]?.focus();
const off = on(node, 'keydown', handleKeydown);

return () => {
node.removeEventListener('keydown', handleKeydown);
previous?.focus();
};
});
return () => {
off();
previous?.focus();
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: The attach tag
---

Attachments are essentially element-level lifecycle functions. They're useful for things like:

- interfacing with third-party libraries
- lazy-loaded images
- tooltips
- adding custom event handlers

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.

We can fix that with an attachment. Import `trapFocus` from `attachments.svelte.js`...

```svelte
/// file: App.svelte
<script>
import Canvas from './Canvas.svelte';
+++import { trapFocus } from './attachments.svelte.js';+++

const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];

let selected = $state(colors[0]);
let size = $state(10);
let showMenu = $state(true);
</script>
```

...then add it to the menu with the `{@attach}` tag:

```svelte
/// file: App.svelte
<div class="menu" +++{@attach trapFocus}+++>
```

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.

First, we need to add an event listener that intercepts Tab key presses:

```js
/// file: attachments.svelte.js
focusable()[0]?.focus();
+++const off = on(node, 'keydown', handleKeydown);+++
```

> [!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.

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:

```js
/// file: attachments.svelte.js
focusable()[0]?.focus();
const off = on(node, 'keydown', handleKeydown);

+++return () => {
off();
previous?.focus();
};+++
```

Now, when you open the menu, you can cycle through the options with the Tab key.
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@
let content = $state('Hello!');

function tooltip(node) {
$effect(() => {
const tooltip = tippy(node);

return tooltip.destroy;
});
const tooltip = tippy(node);
return tooltip.destroy;
}
</script>

<input bind:value={content} />

<button use:tooltip>
<button {@attach tooltip}>
Hover me
</button>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@

let content = $state('Hello!');

function tooltip(node, fn) {
$effect(() => {
const tooltip = tippy(node, fn());

function tooltip(content) {
return (node) => {
const tooltip = tippy(node, { content });
return tooltip.destroy;
});
};
}
</script>

<input bind:value={content} />

<button use:tooltip={() => ({ content })}>
<button {@attach tooltip(content)}>
Hover me
</button>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
title: Attachment factories
---

Often, you need an attachment to depend on some parameters or component state. In this scenario, you can use an [attachment factory](/docs/svelte/@attach#Attachment-factories) — a function that _returns_ an attachment.

In this exercise, we want to add a tooltip to the `<button>` using the [`Tippy.js`](https://atomiks.github.io/tippyjs/) library. The attachment is already wired up with `{@attach tooltip}`, but if you hover over the button (or focus it with the keyboard) the tooltip contains no content.

First, we need to convert our simple attachment into a _factory_ function that returns an attachment.

```js
/// file: App.svelte
function tooltip(---node---) {
+++ return (node) => {+++
const tooltip = tippy(node);
return tooltip.destroy;
+++ };+++
}
```

Next, the factory needs to accept the options we want to pass to Tippy (in this case just `content`):

```js
/// file: App.svelte
function tooltip(+++content+++) {
return (node) => {
const tooltip = tippy(node+++, { content }+++);
return tooltip.destroy;
};
}
```

> [!NOTE] The `tooltip(content)` expression runs inside an effect, so the attachment is destroyed and recreated whenever content changes.

Finally, we need to call the attachment factory and pass the `content` argument in our `{@attach}` tag:

```svelte
/// file: App.svelte
<button {@attach tooltip+++(content)+++}>
Hover me
</button>
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Actions
title: Attachments
scope: { 'prefix': '/src/lib/', 'name': 'src' }
focus: /src/lib/App.svelte
---
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script>
import Canvas from './Canvas.svelte';
import { trapFocus } from './actions.svelte.js';
import { trapFocus } from './attachments.svelte.js';

const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];

Expand All @@ -27,7 +27,7 @@
}
}}
>
<div class="menu" use:trapFocus>
<div class="menu" {@attach trapFocus}>
<div class="colors">
{#each colors as color}
<button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { on } from 'svelte/events';

export function trapFocus(node) {
const previous = document.activeElement;

Expand Down Expand Up @@ -25,13 +27,11 @@ export function trapFocus(node) {
}
}

$effect(() => {
focusable()[0]?.focus();
node.addEventListener('keydown', handleKeydown);
focusable()[0]?.focus();
const off = on(node, 'keydown', handleKeydown);

return () => {
node.removeEventListener('keydown', handleKeydown);
previous?.focus();
};
});
return () => {
off();
previous?.focus();
};
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script>
import Canvas from './Canvas.svelte';
import { trapFocus } from './actions.svelte.js';
import { trapFocus } from './attachments.svelte.js';

const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];

Expand Down Expand Up @@ -29,7 +29,7 @@
}
}}
>
<div class="menu" use:trapFocus>
<div class="menu" {@attach trapFocus}>
<div class="colors">
{#each colors as color}
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: Binding to component instances

Just as you can bind to DOM elements, you can bind to component instances themselves with `bind:this`.

This is useful in the rare cases that you need to interact with a component programmatically (rather than by providing it with updated props). Revisiting our canvas app from [a few exercises ago](actions), it would be nice to add a button to clear the screen.
This is useful in the rare cases that you need to interact with a component programmatically (rather than by providing it with updated props). Revisiting our canvas app from [a few exercises ago](attach), it would be nice to add a button to clear the screen.

First, let's export a function from `Canvas.svelte`:

Expand Down