mirror of https://github.com/sveltejs/svelte
feat: attachments (#15000)
* parse attachments * basic attachments working * working * rename to attach * fix * restrict which symbols are recognised as attachment keys * allow cleanup to be returned directly * changeset * fix * lint * remove createAttachmentKey/isAttachmentKey * fix spreading of symbol properties onto component * types * fix * update name * reserve ability to use sequence expressions in future * Update packages/svelte/src/internal/client/dom/elements/attachments.js Co-authored-by: Leonidaz <lbarenblit@gmail.com> * actually let's do this instead * expose createAttachmentKey * make room for `@attach` docs * add docs * failing test * fix * lock down * add missing reference docs * prevent conflicts * update docs * regenerate * fix link * add Attachment interface * beef up test * regenerate * tweak types * fix --------- Co-authored-by: Leonidaz <lbarenblit@gmail.com>pull/15914/head
parent
7636031b0d
commit
b93a617aa8
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': minor
|
||||
---
|
||||
|
||||
feat: attachments
|
@ -0,0 +1,138 @@
|
||||
---
|
||||
title: {@attach ...}
|
||||
---
|
||||
|
||||
Attachments are functions that run when an element is mounted to the DOM. Optionally, they can return a function that is called when 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.
|
||||
|
||||
## Inline attachments
|
||||
|
||||
Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71WbW_aSBD-Kyt0VaBJyGKbqoUkOhdI68qGUkh6pPSDMY6xYwyH12Ab8d9vZtYE6DX38aQQe3fennlm1jvbUmTP3VKj9KcthO3MShelJz9041Ljx7YksiWKcAP2C0V9uazGazcUuDexY_d3-84iEm4kwE3pOnZW_lLcjqOx8OfLxUqwLVvafiTYjj2tFnN2Vr3yVvbUB4NqEJ81x9H11cEounbsaG3HaL_xp2J2s1WVHa5mru_NxMtyW6TAytKgwm5u2RYlYwF4YsEIVSrYDZMaVc8VLblXPlOmZ5UmxkP9P9ynJ9cR5fKxk7EIXQGQIV9wsXL_TtxY6JE_t4W_iO5wv_yURA6uWLhYLMuicrAdi_-2RAMCUGgTReUC8gUTB9mueC2WK1ckq4j9AhVytiPHDX_Fh_-PXBVvhcsdEHl7fSXZkeTHIgtdKp7c3UegUjRYjfM3hQ9ZjpOty407efbF5dyOnxssWYXlcWlqC7sBmDz3Kl575-k8bGIXvdMuvn7uKo_Zx3Ayv_Mnn-7FaH4X2Mo0m6gPyWObR5P5g2q0dc9q6fVeS8uMdifttRxvOg_DKf-ydkEHZBuj_ayZgeFZw472JfuoTb6niZPzyP78jTvtxdpUp-o0q6tWVl87c2dtBfrGan3Ip3Mn-hqkm9Ff3xbGp_6HLwqvWwOtDnFqZvAYmMPOxgyehTW8T7oZzy1fU8yhAXuW6La9xPJ5arW0fNLiWTfTamCnmsED2DlJd6BzM3DA1gBbPQVbDv444Qw6iTXgKfjxwC43B5QbyDzPgrXRNlAm0MZoW0nX5_B06Ak-Mc-k10L7kQe81M3gHvYAz0CvkTwAvC2IOdDT7kADDq0MdSHvxMp0XnAJeXyLrQCx8hTxj3J6L2Igbp5KDIRbSNw6YSPcuDfsI5ac8KI80yFWX0AeitHuox4-pa-BpoEvzKMOOSMfWDeBGIFXwP4gzOE9cu71kF_FEpgf8AF-eYq4wQZ5z8A_2HtUF_LRwjXEaYFvrBnkA7rg00L9pCfjJYjHxNzmG8qbeBlgjndBwT1ypyCG7gtPngcY-aTd8TBPM-h41vfiiX6hjsAT9g3yw4t9ReLGdR_rSjUEOfBDtQRcyKUhSI4cwG_SNlTiD3vou5XiO2IB_zniBhusJeanORnHPpLcU92oZ9F3RjUiTizkDnx2BPUv4KK6Qc9RHIwbTGPZ632vCzqjDHlxEFOK9l3C-Yx1UiQ_XDtgkjUkf0MjR59QJ5XiEqZ-geMZasBzmds9YIK-xadPfIkenTsPsWPP_YYHB2OkxXlIqT6DopYDXaOa-1i_jvwW0JkiPHhG8AwUsfpYV6gF4tFzeXYQD9ZDo76kHoV1l3r5MYa9WtG3VA-sPfYKxW5xhbiRvYm9IqhX8HwO8Ix0UL8471hLOtd16mPip4N5UR6AgRdnJ8dvCMip1vCjbw3khfFS6h9lI-jswjnHnpY16yPHWdGPGeHzMcdZTj1J_d3B_JVRjvnopCv5wD7RVdLDPqG4kscTTpQNfvPgbI3g_f-pS4--a3TGUynH_hvJb9QpDzXJg3fo3eyld1Xq3YHjmbn23lTh7sm1m3Gpwur8Df2umMq16vtlyqLF5cpdurb4zb12Gfu522Dv-HruR_IWpQGmuDdhGMILvNQQq8TdXbwyVB3NP6dT1angaKxyUxqlXuaNf40L8qKWg8-W0XV9weQdDYPXzX4YqsprvXlQpru5Dbf0kRIMSsZ-u8wvGPydeNxPTk-LFSvjlLQEY96Ex_XBXxWv_mroRp6Yoej8hmmV0wnNB7MlEK81j3dT2PXZGxnyRJKBpOyDAYkq7Pb2FsLupzips3KnoPVOY-esXFPes7csrewtYA8Eb5lli1k19qOyAAkMMLxyEsZbuW70i5MMnRR8HntxFvErXiZhguMfmL8gPOXmB3DC-E8aEafNVzVqqEGQXtdRUAcDvq6ioopSr-97tugAqvcyOar3iy3VnZLanbb1T1jZfrjxo2mp8WSHsbv7Bx1mHBBZDAAA)):
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import { paint } from './gradient.js';
|
||||
</script>
|
||||
|
||||
<canvas
|
||||
width={32}
|
||||
height={32}
|
||||
{@attach (canvas) => {
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
$effect(() => {
|
||||
let frame = requestAnimationFrame(function loop(t) {
|
||||
frame = requestAnimationFrame(loop);
|
||||
paint(context, t);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
};
|
||||
});
|
||||
}}
|
||||
></canvas>
|
||||
```
|
||||
|
||||
## 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>
|
||||
```
|
||||
|
||||
## Creating attachments programmatically
|
||||
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: svelte/attachments
|
||||
---
|
||||
|
||||
> MODULE: svelte/attachments
|
@ -0,0 +1,27 @@
|
||||
import { ATTACHMENT_KEY } from '../constants.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);
|
||||
}
|
@ -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,21 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {AST.AttachTag} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function AttachTag(node, context) {
|
||||
context.state.init.push(
|
||||
b.stmt(
|
||||
b.call(
|
||||
'$.attach',
|
||||
context.state.node,
|
||||
b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
|
||||
)
|
||||
)
|
||||
);
|
||||
context.next();
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { effect } from '../../reactivity/effects.js';
|
||||
|
||||
/**
|
||||
* @param {Element} node
|
||||
* @param {() => (node: Element) => void} get_fn
|
||||
*/
|
||||
export function attach(node, get_fn) {
|
||||
effect(() => {
|
||||
const fn = get_fn();
|
||||
|
||||
// we use `&&` rather than `?.` so that things like
|
||||
// `{@attach DEV && something_dev_only()}` work
|
||||
return fn && fn(node);
|
||||
});
|
||||
}
|
@ -0,0 +1 @@
|
||||
<div {@attach (node) => {}} {@attach (node) => {}}></div>
|
@ -0,0 +1,141 @@
|
||||
{
|
||||
"css": null,
|
||||
"js": [],
|
||||
"start": 0,
|
||||
"end": 57,
|
||||
"type": "Root",
|
||||
"fragment": {
|
||||
"type": "Fragment",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "RegularElement",
|
||||
"start": 0,
|
||||
"end": 57,
|
||||
"name": "div",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "AttachTag",
|
||||
"start": 5,
|
||||
"end": 27,
|
||||
"expression": {
|
||||
"type": "ArrowFunctionExpression",
|
||||
"start": 14,
|
||||
"end": 26,
|
||||
"loc": {
|
||||
"start": {
|
||||
"line": 1,
|
||||
"column": 14
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 26
|
||||
}
|
||||
},
|
||||
"id": null,
|
||||
"expression": false,
|
||||
"generator": false,
|
||||
"async": false,
|
||||
"params": [
|
||||
{
|
||||
"type": "Identifier",
|
||||
"start": 15,
|
||||
"end": 19,
|
||||
"loc": {
|
||||
"start": {
|
||||
"line": 1,
|
||||
"column": 15
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 19
|
||||
}
|
||||
},
|
||||
"name": "node"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"type": "BlockStatement",
|
||||
"start": 24,
|
||||
"end": 26,
|
||||
"loc": {
|
||||
"start": {
|
||||
"line": 1,
|
||||
"column": 24
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 26
|
||||
}
|
||||
},
|
||||
"body": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "AttachTag",
|
||||
"start": 28,
|
||||
"end": 50,
|
||||
"expression": {
|
||||
"type": "ArrowFunctionExpression",
|
||||
"start": 37,
|
||||
"end": 49,
|
||||
"loc": {
|
||||
"start": {
|
||||
"line": 1,
|
||||
"column": 37
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 49
|
||||
}
|
||||
},
|
||||
"id": null,
|
||||
"expression": false,
|
||||
"generator": false,
|
||||
"async": false,
|
||||
"params": [
|
||||
{
|
||||
"type": "Identifier",
|
||||
"start": 38,
|
||||
"end": 42,
|
||||
"loc": {
|
||||
"start": {
|
||||
"line": 1,
|
||||
"column": 38
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 42
|
||||
}
|
||||
},
|
||||
"name": "node"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"type": "BlockStatement",
|
||||
"start": 47,
|
||||
"end": 49,
|
||||
"loc": {
|
||||
"start": {
|
||||
"line": 1,
|
||||
"column": 47
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 49
|
||||
}
|
||||
},
|
||||
"body": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"fragment": {
|
||||
"type": "Fragment",
|
||||
"nodes": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": null
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
ssrHtml: `<div></div>`,
|
||||
html: `<div>DIV</div>`
|
||||
});
|
@ -0,0 +1 @@
|
||||
<div {@attach (node) => node.textContent = node.nodeName}></div>
|
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
let props = $props();
|
||||
</script>
|
||||
|
||||
<div {...props}></div>
|
@ -0,0 +1,15 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
html: `<button>update</button><div></div>`,
|
||||
|
||||
test({ target, assert, logs }) {
|
||||
const button = target.querySelector('button');
|
||||
|
||||
assert.deepEqual(logs, ['one DIV']);
|
||||
|
||||
flushSync(() => button?.click());
|
||||
assert.deepEqual(logs, ['one DIV', 'cleanup one', 'two DIV']);
|
||||
}
|
||||
});
|
@ -0,0 +1,30 @@
|
||||
<script>
|
||||
import { createAttachmentKey } from 'svelte/attachments';
|
||||
import Child from './Child.svelte';
|
||||
|
||||
let stuff = $state({
|
||||
[createAttachmentKey()]: (node) => {
|
||||
console.log(`one ${node.nodeName}`);
|
||||
|
||||
return () => {
|
||||
console.log('cleanup one');
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function update() {
|
||||
stuff = {
|
||||
[createAttachmentKey()]: (node) => {
|
||||
console.log(`two ${node.nodeName}`);
|
||||
|
||||
return () => {
|
||||
console.log('cleanup two');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={update}>update</button>
|
||||
|
||||
<Child {...stuff} />
|
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
let props = $props();
|
||||
</script>
|
||||
|
||||
<div {...props}></div>
|
@ -0,0 +1,14 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
ssrHtml: `<button>update</button><div></div>`,
|
||||
html: `<button>update</button><div>one</div>`,
|
||||
|
||||
test({ target, assert }) {
|
||||
const button = target.querySelector('button');
|
||||
|
||||
flushSync(() => button?.click());
|
||||
assert.htmlEqual(target.innerHTML, '<button>update</button><div>two</div>');
|
||||
}
|
||||
});
|
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
|
||||
let message = $state('one');
|
||||
|
||||
function attachment(message) {
|
||||
return (node) => {
|
||||
node.textContent = message;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => message = 'two'}>update</button>
|
||||
|
||||
<Child {@attach attachment(message)} />
|
@ -0,0 +1,14 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
ssrHtml: `<div></div><button>increment</button>`,
|
||||
html: `<div>1</div><button>increment</button>`,
|
||||
|
||||
test: ({ assert, target }) => {
|
||||
const btn = target.querySelector('button');
|
||||
|
||||
flushSync(() => btn?.click());
|
||||
assert.htmlEqual(target.innerHTML, `<div>2</div><button>increment</button>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,6 @@
|
||||
<script>
|
||||
let value = $state(1);
|
||||
</script>
|
||||
|
||||
<div {@attach (node) => node.textContent = value}></div>
|
||||
<button onclick={() => value += 1}>increment</button>
|
@ -0,0 +1,8 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, logs, target }) {
|
||||
assert.deepEqual(logs, ['hello']);
|
||||
}
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
import { createAttachmentKey } from 'svelte/attachments';
|
||||
|
||||
let stuff = $state({
|
||||
[createAttachmentKey()]: () => console.log('hello')
|
||||
});
|
||||
</script>
|
||||
|
||||
<div {...stuff}></div>
|
@ -0,0 +1,6 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
ssrHtml: `<div></div>`,
|
||||
html: `<div>DIV</div>`
|
||||
});
|
@ -0,0 +1 @@
|
||||
<svelte:element this={'div'} {@attach (node) => node.textContent = node.nodeName}></svelte:element>
|
Loading…
Reference in new issue