mirror of https://github.com/sveltejs/svelte
commit
9ddff21579
@ -1,6 +1,3 @@
|
|||||||
{
|
{
|
||||||
"search.exclude": {
|
|
||||||
"sites/svelte-5-preview/static/*": true
|
|
||||||
},
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,166 @@
|
|||||||
|
---
|
||||||
|
title: {@attach ...}
|
||||||
|
---
|
||||||
|
|
||||||
|
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
|
||||||
|
|
||||||
|
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Attachments are available in Svelte 5.29 and newer.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!--- file: App.svelte --->
|
||||||
|
<script>
|
||||||
|
/** @type {import('svelte/attachments').Attachment} */
|
||||||
|
function myAttachment(element) {
|
||||||
|
console.log(element.nodeName); // 'DIV'
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log('cleaning up');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div {@attach myAttachment}>...</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
An element can have any number of attachments.
|
||||||
|
|
||||||
|
## Attachment factories
|
||||||
|
|
||||||
|
A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment ([demo](/playground/untitled#H4sIAAAAAAAAE3VT0XLaMBD8lavbDiaNCUlbHhTItG_5h5AH2T5ArdBppDOEMv73SkbGJGnH47F9t3un3TsfMyO3mInsh2SW1Sa7zlZKo8_E0zHjg42pGAjxBPxp7cTvUHOMldLjv-IVGUbDoUw295VTlh-WZslqa8kxsLL2ACtHWxh175NffnQfAAGikSGxYQGfPEvGfPSIWtOH0TiBVo2pWJEBJtKhQp4YYzjG9JIdcuMM5IZqHMPioY8vOSA997zQoevf4a7heO7cdp34olRiTGr07OhwH1IdoO2A7dLMbwahZq6MbRhKZWqxk7rBxTGVbuHmhCgb5qDgmIx_J6XtHHukHTrYYqx_YpzYng8aO4RYayql7hU-1ZJl0akqHBE_D9KLolwL-Dibzc7iSln9XjtqTF1UpMkJ2EmXR-BgQErsN4pxIJKr0RVO1qrxAqaTO4fbc9bKulZm3cfDY3aZDgvFGErWjmzhN7KmfX5rXyDeX8Pt1mU-hXjdBOrtuB97vK4GPUtmJ41XcRMEGDLD8do0nJ73zhUhSlyRw0t3vPqD8cjfLs-axiFgNBrkUd9Ulp50c-GLxlXAVlJX-ffpZyiSn7H0eLCUySZQcQdXlxj4El0Yv_FZvIKElqqGTruVLhzu7VRKCh22_5toOyxsWqLwwzK-cCbYNdg-hy-p9D7sbiZWUnts_wLUOF3CJgQAAA==)):
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!--- file: App.svelte --->
|
||||||
|
<script>
|
||||||
|
import tippy from 'tippy.js';
|
||||||
|
|
||||||
|
let content = $state('Hello!');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} content
|
||||||
|
* @returns {import('svelte/attachments').Attachment}
|
||||||
|
*/
|
||||||
|
function tooltip(content) {
|
||||||
|
return (element) => {
|
||||||
|
const tooltip = tippy(element, { content });
|
||||||
|
return tooltip.destroy;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input bind:value={content} />
|
||||||
|
|
||||||
|
<button {@attach tooltip(content)}>
|
||||||
|
Hover me
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
|
||||||
|
|
||||||
|
## Inline attachments
|
||||||
|
|
||||||
|
Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71Wf3OaWBT9KoyTTnW3MS-I3dYmnWXVtnRAazRJzbozRSQEApiRhwKO333vuY8m225m_9yZGOT9OPfcc84D943UTfxGr_G7K6Xr3TVeNW7D2M8avT_3DVk-YAoDNF4vNB8e2tnWjyXGlm7mPzfurVPpp5JgGmeZtwkf5PtFupCxLzVvHa832rl2lElX-s2Xm2DZFNqp_hs-rZetd4v07ORpT3qmQHu7MF2td0BZp8k6z_xkvfXP902_pZ2_1_aYWEiqm0kN8I4r79qbdZ6umnq3q_2iNf22F4dE6qt2oimwdpim_uY6XMm7Fuo-IQT_iTD_CeGTHwZ38ieIJUFQRxirR1Xf39Dw0X5z0I72Af4tD61vvPNwWKQnqmfPTbduhsEd2J3vO_oBd3dc6fF2X7umNdWGf0vBRhSS6qoV7cCXfTXWfKmvWG61_si_vfU92Wz-E4RhsLhNIYinsox9QKGVd8-tuACCeKXRX12P-T_eKf7fhTq0Hvt-f3ailtSeoxJHRo1-58NoPe1UiBc1hkL8Yeh45y_vQ3mcuNl9T8s3cXPRWLnS7YWJG_gn2Tb4tUjid8jua-PVl08j_ab8I14mH8Llx0s5Tz5Err4ql52r_GYg0mVy1bEGZuD0ze64b5TWYFiM-16wSuJ4JT5vfVpDcztrcG_YkRU4s6HxufzDWF4XuVeJ1P10IbzBemt3Vp1V2e04ZXfrJd7Wicyd039brRIv_RIVu_nXi7X1cfL2sy66ztToUp1TO7qJ7NlwZ0f30pld5qNSVE5o6PbMojFHjgZB7oSicPpGteyLclQap7SvY0dXtM_LR1NT2JFHey3aaxa0VxCeYJ7RMHemoiCcgPZV9pR7o7kgcOjeGliYk9hjDZx8FAq6enwlTPSZj_vYPw9Il64dXdIY8ZmapzwfEd8-1ZyaxWhqkIZOibXUd-6Upqi1pD4uMicCV1GA_7zi73UN8BaF4sC8peJtMjfmjbHZBFwq5ov50qRaE0l96NZggnW4KqypYRAW-uhSz9ADvklwJF2J-5W0Z5fQPBhDX92R6I_0IFxRgDftge4l4dP-gH1hjD7uqU6fsOEZ9UNrCdPB-nys6uXgY6O3ZMd9sy5T9PghqrWHdjo4jB51CgLiKJaDYYA-7WgYONf1FbjkI-mE3EAfUY_rijfuJ_CVPaR50oe9JF7Q0pI8Dw3osxxYHdYPGbp2CnwHF8KvwJv2wEv0Z3ilQI6U9uwbZxbYJXvEmjjQjjCHkvNLvNg3yhzXQd1olamsT4IRrZmX0MUDpwL7R8zzHj7pSh9hPHFSHjLezKqAST51uC5zmtQ87skDUaneLokT5RbXkPWSYz53Abgjc8_o4KFGUZ-Hgv2Z1l5OTYM9D-HfUD0L-EwxH5wRnIG61gS-khfgY1bq7IAP_DA4l5xRuh9xlm8yGjutc8t-wHtkhWv3hc7aqGwiK5KzgvM5xRkZYn193uEln-su55j1GaIv7oM4iPrsVHiG0Dx7TR9-1lBfqFdwfvSd5LNL5xyZVp5NoHFZ57FkfiF6vKs4k5zvIfrX5xX6MXmt0gM5MTu8DjnhukrHHzTRd3jm0dma0_f_x5cxP9f4jBdqHvmbq2fUjzqcKh2Cp-yWj9ntcHanXmBXxhu7Q--eyjhfNFpaV7zgz4nWEUb7zUOhpevjjf_gu_KZ99pxFlZ-T3sttkmYqrco_26q35v0Ewzv5EZPbnL_8BfduWGMnyyN3q0bZ_7hb_7KG_L4CQAA)):
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!--- file: App.svelte --->
|
||||||
|
<canvas
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
{@attach (canvas) => {
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
></canvas>
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state.
|
||||||
|
|
||||||
|
## Passing attachments to components
|
||||||
|
|
||||||
|
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
|
||||||
|
|
||||||
|
This allows you to create _wrapper components_ that augment elements ([demo](/playground/untitled#H4sIAAAAAAAAE3VUS3ObMBD-KxvajnFqsJM2PhA7TXrKob31FjITAbKtRkiMtDhJPfz3LiAMdpxhGJvdb1_fPnaeYjn3Iu-WIbJ04028lZDcetHDzsO3olbVApI74F1RhHbLJdayhFl-Sp5qhVwhufEWNjWiwJtYxSjyQhsEFEXxBiujcxg1_8O_dnQ9APwsEbVyiHDafjrvDZCgkiO4MLCEzxYZcn90z6XUZ6OxA61KlaIgV6i1pFC-sxjDrlbHaDiWRoGvdMbHsLzp5DES0mJnRxGaRBvcBHb7yFUTCQeunEWYcYtGv12TqgFUDbCK1WLaM6IWQhUlQiJUFm2ZLPly51xXMG0Rjoyd69C7UqqG2nu95QZyXvtvLVpri2-SN4hoLXXCZFfhQ8aQBU1VgdEaH_vSgyBZR_BpPp_vi0tY-rw2ulRZkGqpTQRbZvwa2BPgFC8bgbw31CbjJjAsE6WNYBZeGp7vtQXLMqHWnZx-5kM1TR5ycpkZXQR2wzL94l8Ur1C_3-g168SfQf1MyfRi3LW9fs77emJEw5QV9SREoLTq06tcczq7d6xEUcJX2vAhO1b843XK34e5unZEMBr15ekuKEusluWAF8lXhE2ZTP2r2RcIHJ-163FPKerCgYJLOB9i4GvNwviI5-gAQiFFBk3tBTOU3HFXEk0R8o86WvUD64aINhv5K3oRmpJXkw8uxMG6Hh6JY9X7OwGSqfUy9tDG3sHNoEi0d_d_fv9qndxRU0VClFqo3KVo3U655Hnt1PXB3Qra2Y2QGdEwgTAMCxopsoxOe6SD0gD8movDhT0LAnhqlE8gVCpLWnRoV7OJCkFAwEXitrYL1W7p7pbiE_P7XH6E_rihODm5s52XtiH9Ekaw0VgI9exadWL1uoEYjPtg2672k5szsxbKyWB2fdT0w5Y_0hcT8oXOlRetmLS8-g-6TLXXQgYAAA==)):
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!--- file: Button.svelte --->
|
||||||
|
<script>
|
||||||
|
/** @type {import('svelte/elements').HTMLButtonAttributes} */
|
||||||
|
let { children, ...props } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- `props` includes attachments -->
|
||||||
|
<button {...props}>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!--- file: App.svelte --->
|
||||||
|
<script>
|
||||||
|
import tippy from 'tippy.js';
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
|
||||||
|
let content = $state('Hello!');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} content
|
||||||
|
* @returns {import('svelte/attachments').Attachment}
|
||||||
|
*/
|
||||||
|
function tooltip(content) {
|
||||||
|
return (element) => {
|
||||||
|
const tooltip = tippy(element, { content });
|
||||||
|
return tooltip.destroy;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input bind:value={content} />
|
||||||
|
|
||||||
|
<Button {@attach tooltip(content)}>
|
||||||
|
Hover me
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Controlling when attachments re-run
|
||||||
|
|
||||||
|
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// @errors: 7006 2304 2552
|
||||||
|
function foo(bar) {
|
||||||
|
return (node) => {
|
||||||
|
veryExpensiveSetupWork(node);
|
||||||
|
update(node, bar);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// @errors: 7006 2304 2552
|
||||||
|
function foo(+++getBar+++) {
|
||||||
|
return (node) => {
|
||||||
|
veryExpensiveSetupWork(node);
|
||||||
|
|
||||||
|
+++ $effect(() => {
|
||||||
|
update(node, getBar());
|
||||||
|
});+++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating attachments programmatically
|
||||||
|
|
||||||
|
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
|
||||||
|
|
||||||
|
## Converting actions to attachments
|
||||||
|
|
||||||
|
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.
|
@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
title: await
|
||||||
|
---
|
||||||
|
|
||||||
|
As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable:
|
||||||
|
|
||||||
|
- at the top level of your component's `<script>`
|
||||||
|
- inside `$derived(...)` declarations
|
||||||
|
- inside your markup
|
||||||
|
|
||||||
|
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
/// file: svelte.config.js
|
||||||
|
export default {
|
||||||
|
compilerOptions: {
|
||||||
|
experimental: {
|
||||||
|
async: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The experimental flag will be removed in Svelte 6.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
Currently, you can only use `await` inside a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<svelte:boundary>
|
||||||
|
<MyApp />
|
||||||
|
|
||||||
|
{#snippet pending()}
|
||||||
|
<p>loading...</p>
|
||||||
|
{/snippet}
|
||||||
|
</svelte:boundary>
|
||||||
|
```
|
||||||
|
|
||||||
|
This restriction will be lifted once Svelte supports asynchronous server-side rendering (see [caveats](#Caveats)).
|
||||||
|
|
||||||
|
> [!NOTE] In the [playground](/playground), your app is rendered inside a boundary with an empty pending snippet, so that you can use `await` without having to create one.
|
||||||
|
|
||||||
|
## Synchronized updates
|
||||||
|
|
||||||
|
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like [this](/playground/untitled#H4sIAAAAAAAAE42QsWrDQBBEf2VZUkhYRE4gjSwJ0qVMkS6XYk9awcFpJe5Wdoy4fw-ycdykSPt2dpiZFYVGxgrf2PsJTlPwPWTcO-U-xwIH5zli9bminudNtwEsbl-v8_wYj-x1Y5Yi_8W7SZRFI1ZYxy64WVsjRj0rEDTwEJWUs6f8cKP2Tp8vVIxSPEsHwyKdukmA-j6jAmwO63Y1SidyCsIneA_T6CJn2ZBD00Jk_XAjT4tmQwEv-32eH6AsgYK6wXWOPPTs6Xy1CaxLECDYgb3kSUbq8p5aaifzorCt0RiUZbQcDIJ10ldH8gs3K6X2Xzqbro5zu1KCHaw2QQPrtclvwVSXc2sEC1T-Vqw0LJy-ClRy_uSkx2ogHzn9ADZ1CubKAQAA)...
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
let a = $state(1);
|
||||||
|
let b = $state(2);
|
||||||
|
|
||||||
|
async function add(a, b) {
|
||||||
|
await new Promise((f) => setTimeout(f, 500)); // artificial delay
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type="number" bind:value={a}>
|
||||||
|
<input type="number" bind:value={b}>
|
||||||
|
|
||||||
|
<p>{a} + {b} = {await add(a, b)}</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
|
||||||
|
|
||||||
|
```html
|
||||||
|
<p>2 + 2 = 3</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves.
|
||||||
|
|
||||||
|
Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing.
|
||||||
|
|
||||||
|
## Concurrency
|
||||||
|
|
||||||
|
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<p>{await one()}</p>
|
||||||
|
<p>{await two()}</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
|
||||||
|
|
||||||
|
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function one() { return 1; }
|
||||||
|
async function two() { return 2; }
|
||||||
|
// ---cut---
|
||||||
|
// these will run sequentially the first time,
|
||||||
|
// but will update independently
|
||||||
|
let a = $derived(await one());
|
||||||
|
let b = $derived(await two());
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
|
||||||
|
|
||||||
|
## Indicating loading states
|
||||||
|
|
||||||
|
In addition to the nearest boundary's [`pending`](svelte-boundary#Properties-pending) snippet, you can indicate that asynchronous work is ongoing with [`$effect.pending()`]($effect#$effect.pending).
|
||||||
|
|
||||||
|
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let color = 'red';
|
||||||
|
let answer = -1;
|
||||||
|
let updating = false;
|
||||||
|
// ---cut---
|
||||||
|
import { tick, settled } from 'svelte';
|
||||||
|
|
||||||
|
async function onclick() {
|
||||||
|
updating = true;
|
||||||
|
|
||||||
|
// without this, the change to `updating` will be
|
||||||
|
// grouped with the other changes, meaning it
|
||||||
|
// won't be reflected in the UI
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
color = 'octarine';
|
||||||
|
answer = 42;
|
||||||
|
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
// any updates affected by `color` or `answer`
|
||||||
|
// have now been applied
|
||||||
|
updating = false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.
|
||||||
|
|
||||||
|
Currently, server-side rendering is synchronous. If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, only the `pending` snippet will be rendered.
|
||||||
|
|
||||||
|
## Breaking changes
|
||||||
|
|
||||||
|
Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in [very rare situations](/playground/untitled?#H4sIAAAAAAAAE22R3VLDIBCFX2WLvUhnTHsf0zre-Q7WmfwtFV2BgU1rJ5N3F0jaOuoVcPbw7VkYhK4_URTiGYkMnIyjDjLsFGO3EvdCKkIvipdB8NlGXxSCPt96snbtj0gctab2-J_eGs2oOWBE6VunLO_2es-EDKZ5x5ZhC0vPNWM2gHXGouNzAex6hHH1cPHil_Lsb95YT9VQX6KUAbS2DrNsBdsdDFHe8_XSYjH1SrhELTe3MLpsemajweiWVPuxHSbKNd-8eQTdE0EBf4OOaSg2hwNhhE_ABB_ulJzjj9FULvIcqgm5vnAqUB7wWFMfhuugQWkcAr8hVD-mq8D12kOep24J_IszToOXdveGDsuNnZwbJUNlXsKnhJdhUcTo42s41YpOSneikDV5HL8BktM6yRcCAAA=) it is possible to update a block that should no longer exist, but only if you update state inside an effect, [which you should avoid]($effect#When-not-to-use-$effect).
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: svelte/attachments
|
||||||
|
---
|
||||||
|
|
||||||
|
> MODULE: svelte/attachments
|
@ -0,0 +1,113 @@
|
|||||||
|
/** @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 '../index-client.js';
|
||||||
|
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,
|
||||||
|
* as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though
|
||||||
|
* is generally not needed when building an app.
|
||||||
|
*
|
||||||
|
* ```svelte
|
||||||
|
* <script>
|
||||||
|
* import { createAttachmentKey } from 'svelte/attachments';
|
||||||
|
*
|
||||||
|
* const props = {
|
||||||
|
* class: 'cool',
|
||||||
|
* onclick: () => alert('clicked'),
|
||||||
|
* [createAttachmentKey()]: (node) => {
|
||||||
|
* node.textContent = 'attached!';
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
* </script>
|
||||||
|
*
|
||||||
|
* <button {...props}>click me</button>
|
||||||
|
* ```
|
||||||
|
* @since 5.29
|
||||||
|
*/
|
||||||
|
export function createAttachmentKey() {
|
||||||
|
return Symbol(ATTACHMENT_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* <!-- with an action -->
|
||||||
|
* <div use:foo={bar}>...</div>
|
||||||
|
*
|
||||||
|
* <!-- with an attachment -->
|
||||||
|
* <div {@attach fromAction(foo, () => bar)}>...</div>
|
||||||
|
* ```
|
||||||
|
* @template {EventTarget} E
|
||||||
|
* @template {unknown} T
|
||||||
|
* @overload
|
||||||
|
* @param {Action<E, T> | ((element: E, arg: T) => void | ActionReturn<T>)} action The action function
|
||||||
|
* @param {() => T} fn A function that returns the argument for the action
|
||||||
|
* @returns {Attachment<E>}
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* <!-- with an action -->
|
||||||
|
* <div use:foo={bar}>...</div>
|
||||||
|
*
|
||||||
|
* <!-- with an attachment -->
|
||||||
|
* <div {@attach fromAction(foo, () => bar)}>...</div>
|
||||||
|
* ```
|
||||||
|
* @template {EventTarget} E
|
||||||
|
* @overload
|
||||||
|
* @param {Action<E, void> | ((element: E) => void | ActionReturn<void>)} action The action function
|
||||||
|
* @returns {Attachment<E>}
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* <!-- with an action -->
|
||||||
|
* <div use:foo={bar}>...</div>
|
||||||
|
*
|
||||||
|
* <!-- with an attachment -->
|
||||||
|
* <div {@attach fromAction(foo, () => bar)}>...</div>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @template {EventTarget} E
|
||||||
|
* @template {unknown} T
|
||||||
|
* @param {Action<E, T> | ((element: E, arg: T) => void | ActionReturn<T>)} action The action function
|
||||||
|
* @param {() => T} fn A function that returns the argument for the action
|
||||||
|
* @returns {Attachment<E>}
|
||||||
|
* @since 5.32
|
||||||
|
*/
|
||||||
|
export function fromAction(action, fn = /** @type {() => T} */ (noop)) {
|
||||||
|
return (element) => {
|
||||||
|
const { update, destroy } = untrack(() => action(element, fn()) ?? {});
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
var ran = false;
|
||||||
|
render_effect(() => {
|
||||||
|
const arg = fn();
|
||||||
|
if (ran) update(arg);
|
||||||
|
});
|
||||||
|
ran = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destroy) {
|
||||||
|
teardown(destroy);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* It can be attached to an element with an `{@attach ...}` tag, or by spreading an object containing
|
||||||
|
* a property created with [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey).
|
||||||
|
*/
|
||||||
|
export interface Attachment<T extends EventTarget = Element> {
|
||||||
|
(element: T): void | (() => void);
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './index.js';
|
@ -0,0 +1,13 @@
|
|||||||
|
/** @import { AST } from '#compiler' */
|
||||||
|
/** @import { Context } from '../types' */
|
||||||
|
|
||||||
|
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AST.AttachTag} node
|
||||||
|
* @param {Context} context
|
||||||
|
*/
|
||||||
|
export function AttachTag(node, context) {
|
||||||
|
mark_subtree_dynamic(context.path);
|
||||||
|
context.next({ ...context.state, expression: node.metadata.expression });
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/** @import { AwaitExpression } from 'estree' */
|
||||||
|
/** @import { Context } from '../types' */
|
||||||
|
import * as e from '../../../errors.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AwaitExpression} node
|
||||||
|
* @param {Context} context
|
||||||
|
*/
|
||||||
|
export function AwaitExpression(node, context) {
|
||||||
|
let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1;
|
||||||
|
|
||||||
|
if (context.state.expression) {
|
||||||
|
context.state.expression.has_await = true;
|
||||||
|
suspend = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// disallow top-level `await` or `await` in template expressions
|
||||||
|
// unless a) in runes mode and b) opted into `experimental.async`
|
||||||
|
if (suspend) {
|
||||||
|
if (!context.state.options.experimental.async) {
|
||||||
|
e.experimental_async(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.state.analysis.runes) {
|
||||||
|
e.legacy_await_invalid(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.next();
|
||||||
|
}
|
@ -1,30 +1,107 @@
|
|||||||
/** @import { ClassBody } from 'estree' */
|
/** @import { AssignmentExpression, CallExpression, ClassBody, PropertyDefinition, Expression, PrivateIdentifier, MethodDefinition } from 'estree' */
|
||||||
|
/** @import { StateField } from '#compiler' */
|
||||||
/** @import { Context } from '../types' */
|
/** @import { Context } from '../types' */
|
||||||
|
import * as b from '#compiler/builders';
|
||||||
import { get_rune } from '../../scope.js';
|
import { get_rune } from '../../scope.js';
|
||||||
|
import * as e from '../../../errors.js';
|
||||||
|
import { is_state_creation_rune } from '../../../../utils.js';
|
||||||
|
import { get_name } from '../../nodes.js';
|
||||||
|
import { regex_invalid_identifier_chars } from '../../patterns.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ClassBody} node
|
* @param {ClassBody} node
|
||||||
* @param {Context} context
|
* @param {Context} context
|
||||||
*/
|
*/
|
||||||
export function ClassBody(node, context) {
|
export function ClassBody(node, context) {
|
||||||
/** @type {{name: string, private: boolean}[]} */
|
if (!context.state.analysis.runes) {
|
||||||
const derived_state = [];
|
context.next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {string[]} */
|
||||||
|
const private_ids = [];
|
||||||
|
|
||||||
for (const definition of node.body) {
|
for (const prop of node.body) {
|
||||||
if (
|
if (
|
||||||
definition.type === 'PropertyDefinition' &&
|
(prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') &&
|
||||||
(definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Identifier') &&
|
prop.key.type === 'PrivateIdentifier'
|
||||||
definition.value?.type === 'CallExpression'
|
|
||||||
) {
|
) {
|
||||||
const rune = get_rune(definition.value, context.state.scope);
|
private_ids.push(prop.key.name);
|
||||||
if (rune === '$derived' || rune === '$derived.by') {
|
}
|
||||||
derived_state.push({
|
}
|
||||||
name: definition.key.name,
|
|
||||||
private: definition.key.type === 'PrivateIdentifier'
|
/** @type {Map<string, StateField>} */
|
||||||
|
const state_fields = new Map();
|
||||||
|
|
||||||
|
context.state.analysis.classes.set(node, state_fields);
|
||||||
|
|
||||||
|
/** @type {MethodDefinition | null} */
|
||||||
|
let constructor = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PropertyDefinition | AssignmentExpression} node
|
||||||
|
* @param {Expression | PrivateIdentifier} key
|
||||||
|
* @param {Expression | null | undefined} value
|
||||||
|
*/
|
||||||
|
function handle(node, key, value) {
|
||||||
|
const name = get_name(key);
|
||||||
|
if (name === null) return;
|
||||||
|
|
||||||
|
const rune = get_rune(value, context.state.scope);
|
||||||
|
|
||||||
|
if (rune && is_state_creation_rune(rune)) {
|
||||||
|
if (state_fields.has(name)) {
|
||||||
|
e.state_field_duplicate(node, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
state_fields.set(name, {
|
||||||
|
node,
|
||||||
|
type: rune,
|
||||||
|
// @ts-expect-error for public state this is filled out in a moment
|
||||||
|
key: key.type === 'PrivateIdentifier' ? key : null,
|
||||||
|
value: /** @type {CallExpression} */ (value)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const child of node.body) {
|
||||||
|
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
|
||||||
|
handle(child, child.key, child.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.type === 'MethodDefinition' && child.kind === 'constructor') {
|
||||||
|
constructor = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constructor) {
|
||||||
|
for (const statement of constructor.value.body.body) {
|
||||||
|
if (statement.type !== 'ExpressionStatement') continue;
|
||||||
|
if (statement.expression.type !== 'AssignmentExpression') continue;
|
||||||
|
|
||||||
|
const { left, right } = statement.expression;
|
||||||
|
|
||||||
|
if (left.type !== 'MemberExpression') continue;
|
||||||
|
if (left.object.type !== 'ThisExpression') continue;
|
||||||
|
if (left.computed && left.property.type !== 'Literal') continue;
|
||||||
|
|
||||||
|
handle(statement.expression, left.property, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, field] of state_fields) {
|
||||||
|
if (name[0] === '#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deconflicted = name.replace(regex_invalid_identifier_chars, '_');
|
||||||
|
while (private_ids.includes(deconflicted)) {
|
||||||
|
deconflicted = '_' + deconflicted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private_ids.push(deconflicted);
|
||||||
|
field.key = b.private_id(deconflicted);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.next({ ...context.state, derived_state });
|
context.next({ ...context.state, state_fields });
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
/** @import { Literal } from 'estree' */
|
||||||
|
import * as w from '../../../warnings.js';
|
||||||
|
import { regex_bidirectional_control_characters } from '../../patterns.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Literal} node
|
||||||
|
*/
|
||||||
|
export function Literal(node) {
|
||||||
|
if (typeof node.value === 'string') {
|
||||||
|
if (regex_bidirectional_control_characters.test(node.value)) {
|
||||||
|
w.bidirectional_control_characters(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
/** @import { PropertyDefinition } from 'estree' */
|
||||||
|
/** @import { Context } from '../types' */
|
||||||
|
import * as e from '../../../errors.js';
|
||||||
|
import { get_name } from '../../nodes.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PropertyDefinition} node
|
||||||
|
* @param {Context} context
|
||||||
|
*/
|
||||||
|
export function PropertyDefinition(node, context) {
|
||||||
|
const name = get_name(node.key);
|
||||||
|
const field = name && context.state.state_fields.get(name);
|
||||||
|
|
||||||
|
if (field && node !== field.node && node.value) {
|
||||||
|
if (/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)) {
|
||||||
|
e.state_field_invalid_assignment(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.next();
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue