incremental-batches
Rich Harris 3 weeks ago
commit b6ccd1465b

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent hydration error on async `{@html ...}`

@ -41,9 +41,11 @@ You can use `$effect` anywhere, not just at the top level of a component, as lon
> [!NOTE] Svelte uses effects internally to represent logic and expressions in your template — this is how `<h1>hello {name}!</h1>` updates when `name` changes.
An effect can return a _teardown function_ which will run immediately before the effect re-runs ([demo](/playground/untitled#H4sIAAAAAAAAE42SQVODMBCF_8pOxkPRKq3HCsx49K4n64xpskjGkDDJ0tph-O8uINo6HjxB3u7HvrehE07WKDbiyZEhi1osRWksRrF57gQdm6E2CKx_dd43zU3co6VB28mIf-nKO0JH_BmRRRVMQ8XWbXkAgfKtI8jhIpIkXKySu7lSG2tNRGZ1_GlYr1ZTD3ddYFmiosUigbyAbpC2lKbwWJkIB8ZhhxBQBWRSw6FCh3sM8GrYTthL-wqqku4N44TyqEgwF3lmRHr4Op0PGXoH31c5rO8mqV-eOZ49bikgtcHBL55tmhIkEMqg_cFB2TpFxjtg703we6NRL8HQFCS07oSUCZi6Rm04lz1yytIHBKoQpo1w6Gsm4gmyS8b8Y5PydeMdX8gwS2Ok4I-ov5NZtvQde95GMsccn_1wzNKfu3RZtS66cSl9lvL7qO1aIk7knbJGvefdtIOzi73M4bYvovUHDFk6AcX_0HRESxnpBOW_jfCDxIZCi_1L_wm4xGQ60wIAAA==)).
An effect can return a _teardown function_ which will run immediately before the effect re-runs:
<!-- codeblock:start {"title":"Effect teardown"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
let milliseconds = $state(1000);
@ -68,6 +70,7 @@ An effect can return a _teardown function_ which will run immediately before the
<button onclick={() => (milliseconds *= 2)}>slower</button>
<button onclick={() => (milliseconds /= 2)}>faster</button>
```
<!-- codeblock:end -->
Teardown functions also run when the effect is destroyed, which happens when its parent is destroyed (for example, a component is unmounted) or the parent effect re-runs.
@ -206,9 +209,11 @@ Apart from the timing, `$effect.pre` works exactly like `$effect`.
## `$effect.tracking`
The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/playground/untitled#H4sIAAAAAAAACn3PwYrCMBDG8VeZDYIt2PYeY8Dn2HrIhqkU08nQjItS-u6buAt7UDzmz8ePyaKGMWBS-nNRcmdU-hHUTpGbyuvI3KZvDFLal0v4qvtIgiSZUSb5eWSxPfWSc4oB2xDP1XYk8HHiSHkICeXKeruDDQ4Demlldv4y0rmq6z10HQwuJMxGVv4mVVXDwcJS0jP9u3knynwtoKz1vifT_Z9Jhm0WBCcOTlDD8kyspmML5qNpHg40jc3fFryJ0iWsp_UHgz3180oBAAA=)):
The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template:
<!-- codeblock:start {"title":"$effect.tracking()"} -->
```svelte
<!--- file: App.svelte --->
<script>
console.log('in component setup:', $effect.tracking()); // false
@ -219,14 +224,27 @@ The `$effect.tracking` rune is an advanced feature that tells you whether or not
<p>in template: {$effect.tracking()}</p> <!-- true -->
```
<!-- codeblock:end -->
It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svelte-reactivity#createSubscriber), which will create listeners to update reactive values but _only_ if those values are being tracked (rather than, for example, read inside an event handler).
## `$effect.pending`
When using [`await`](await-expressions) in components, the `$effect.pending()` rune tells you how many promises are pending in the current [boundary](svelte-boundary), not including child boundaries ([demo](/playground/untitled#H4sIAAAAAAAAE3WRMU_DMBCF_8rJdHDUqilILGkaiY2RgY0yOPYZWbiOFV8IleX_jpMUEAIWS_7u-d27c2ROnJBV7B6t7WDsequAozKEqmAbpo3FwKqnyOjsJ90EMr-8uvN-G97Q0sRaEfAvLjtH6CjbsDrI3nhqju5IFgkEHGAVSBDy62L_SdtvejPTzEU4Owl6cJJM50AoxcUG2gLiVM31URgChyM89N3JBORcF3BoICA9mhN2A3G9gdvdrij2UJYgejLaSCMsKLTivNj0SEOf7WEN7ZwnHV1dfqd2dTsQ5QCdk9bI10PkcxexXqcmH3W51Jt_le2kbH8os9Y3UaTcNLYpDx-Xab6GTHXpZ128MhpWqDVK2np0yrgXXqQpaLa4APDLBkIF8bd2sYql0Sn_DeE7sYr6AdNzvgljR-MUq7SwAdMHeUtgHR4CAAA=)):
When using [`await`](await-expressions) in components, the `$effect.pending()` rune tells you how many promises are pending in the current [boundary](svelte-boundary), not including child boundaries:
<!-- codeblock:start {"title":"$effect.pending"} -->
```svelte
<!--- file: App.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>
<button onclick={() => a++}>a++</button>
<button onclick={() => b++}>b++</button>
@ -236,6 +254,7 @@ When using [`await`](await-expressions) in components, the `$effect.pending()` r
<p>pending promises: {$effect.pending()}</p>
{/if}
```
<!-- codeblock:end -->
## `$effect.root`
@ -285,9 +304,11 @@ In general, `$effect` is best considered something of an escape hatch — useful
If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25.
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAAE5WRTWrDMBCFryKGLBJoY3fRjWIHeoiu6i6UZBwEY0VE49TB-O6VxrFTSih0qe_Ne_OjHpxpEDS8O7ZMeIAnqC1hAP3RA1990hKI_Fb55v06XJA4sZ0J-IjvT47RcYyBIuzP1vO2chVHHFjxiQ2pUr3k-SZRQlbBx_LIFoEN4zJfzQph_UMQr4hRXmBd456Xy5Uqt6pPKHmkfmzyPAZL2PCnbRpg8qWYu63I7lu4gswOSRYqrPNt3CgeqqzgbNwRK1A76w76YqjFspfcQTWmK3vJHlQm1puSTVSeqdOc_r9GaeCHfUSY26TXry6Br4RSK3C6yMEGT-aqVU3YbUZ2NF6rfP2KzXgbuYzY46czdgyazy0On_FlLH3F-UDXhgIO35UGlA1rAgAA)):
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Instead of using effects for this...
<!-- codeblock:start {"title":"Setting state in effects (don't do this!)"} -->
```svelte
<!--- file: App.svelte --->
<script>
const total = 100;
let spent = $state(0);
@ -311,11 +332,21 @@ You might be tempted to do something convoluted with effects to link one value t
<input type="range" bind:value={left} max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>
```
<!-- codeblock:end -->
Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE5VRvW7CMBB-FcvqECQK6dDFJEgsnfoGTQdDLsjSxVjxhYKivHvPBwFUsXS8774_nwftbQva6I_e78gdvNo6Xzu_j3quG4cQtfkaNJ1DIiWA8atkE8IiHgEpYVsb4Rm-O3gCT2yji7jrXKB15StiOJKiA1lUpXrL81VCEUjFwHTGXiJZgiyf3TYIjSxq6NwR6uyifr0ohMbEZnpHH2rWf7ImS8KZGtK6osl_UqelRIyVL5b3ir5AuwWUtoXzoee6fIWy0p31e6i0XMocLfZQDuI6qtaeykGcR7UU6XWznFAZU9LN_X9B2UyVayk9f3ji0-REugen6U9upDOCcAWcLlS7GNCejWoQTqsLtrfBqHzxDu3DrUTOf0xwIm2o62H85sk6_OHG2jQWI4y_3byXXGMCAAA=)):
...use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible:
<!-- codeblock:start {"title":"Setting state with function bindings"} -->
```svelte
<!--- file: App.svelte --->
<script>
const total = 100;
let spent = $state(0);
@ -335,6 +366,14 @@ Instead, use `oninput` callbacks or — better still — [function bindings](bin
<input type="range" +++bind:value={() => left, updateLeft}+++ max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>
```
<!-- codeblock:end -->
If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](svelte#untrack).

@ -64,8 +64,9 @@ let { a, b, c, ...others } = $props();
## Updating props
References to a prop inside a component update when the prop itself updates — when `count` changes in `App.svelte`, it will also change inside `Child.svelte`. But the child component is able to temporarily override the prop value, which can be useful for unsaved ephemeral state ([demo](/playground/untitled#H4sIAAAAAAAAE6WQ0WrDMAxFf0WIQR0Wmu3VTQJln7HsIfVcZubIxlbGRvC_DzuBraN92qPula50tODZWB1RPi_IX16jLALWSOOUq6P3-_ihLWftNEZ9TVeOWBNHlNhGFYznfqCBzeRdYHh6M_YVzsFNsNs3pdpGd4eBcqPVDMrNxNDBXeSRtXioDgO1zU8ataeZ2RE4Utao924RFXQ9iHXwvoPHKpW1xY4g_Bg0cSVhKS0p560Za95612ZC02ONrD8ZJYdZp_rGQ37ff_mSP86Np2TWZaNNmdcH56P4P67K66_SXoK9pG-5dF5Z9QEAAA==)):
References to a prop inside a component update when the prop itself updates — when `count` changes in `App.svelte`, it will also change inside `Child.svelte`. But the child component is able to temporarily override the prop value, which can be useful for unsaved ephemeral state:
<!-- codeblock:start {"title":"Temporarily updating props","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
@ -91,11 +92,13 @@ References to a prop inside a component update when the prop itself updates —
clicks (child): {count}
</button>
```
<!-- codeblock:end -->
While you can temporarily _reassign_ props, you should not _mutate_ props unless they are [bindable]($bindable).
If the prop is a regular object, the mutation will have no effect ([demo](/playground/untitled#H4sIAAAAAAAAE3WQwU7DMBBEf2W1QmorQgJXk0RC3PkBwiExG9WQrC17U4Es_ztKUkQp9OjxzM7bjcjtSKjwyfKNp1aLORA4b13ADHszUED1HFE-3eyaBcy-Mw_O5eFAg8xa1wb6T9eWhVgCKiyD9sZJ3XAjZnTWCzzuzfAKvbcjbPJieR2jm_uGy-InweXqtd0baaliBG0nFgW3kBIUNWYo9CGoxE-UsgvIpw2_oc9-LmAPJBCPDJCggqvlVtvdH9puErEMlvVg9HsVtzuoaojzkKKAfRuALVDfk5ZZW0fmy05wXcFdwyktlUs-KIinljTXrRVnm7-kL9dYLVbUAQAA)):
If the prop is a regular object, the mutation will have no effect:
<!-- codeblock:start {"title":"Non-reactive props","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
@ -118,9 +121,11 @@ If the prop is a regular object, the mutation will have no effect ([demo](/playg
clicks: {object.count}
</button>
```
<!-- codeblock:end -->
If the prop is a reactive state proxy, however, then mutations _will_ have an effect but you will see an [`ownership_invalid_mutation`](runtime-warnings#Client-warnings-ownership_invalid_mutation) warning, because the component is mutating state that does not 'belong' to it ([demo](/playground/untitled#H4sIAAAAAAAAE3WR0U7DMAxFf8VESBuiauG1WycheOEbKA9p67FA6kSNszJV-XeUZhMw2GN8r-1znUmQ7FGU4pn2UqsOes-SlSGRia3S6ET5Mgk-2OiJBZGdOh6szd0eNcdaIx3-V28NMRI7UYq1awdleVNTzaq3ZmB43CndwXYwPSzyYn4dWxermqJRI4Np3rFlqODasWRcTtAaT1zCHYSbVU3r4nsyrdPMKTUFKDYiE4yfLEoePIbsQpqfy3_nOVMuJIqg0wk1RFg7GOuWfwEbz2wIDLVatR_VtLyBagNTHFIUMCqtoZXeIfAOU1JoUJsR2IC3nWTMjt7GM4yKdyBhlAMpesvhydCC0y_i0ZagHByMh26WzUhXUUxKnpbcVnBfUwhznJnNlac7JkuIURL-2VVfwxflyrWcSQIAAA==)):
If the prop is a reactive state proxy, however, then mutations _will_ have an effect but you will see an [`ownership_invalid_mutation`](runtime-warnings#Client-warnings-ownership_invalid_mutation) warning, because the component is mutating state that does not 'belong' to it:
<!-- codeblock:start {"title":"Invalid mutation","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
@ -147,8 +152,19 @@ If the prop is a reactive state proxy, however, then mutations _will_ have an ef
clicks: {object.count}
</button>
```
<!-- codeblock:end -->
The fallback value of a prop not declared with `$bindable` is left untouched — it is not turned into a reactive state proxy — meaning mutations will not cause updates ([demo](/playground/untitled#H4sIAAAAAAAAE3WQwU7DMBBEf2VkIbUVoYFraCIh7vwA4eC4G9Wta1vxpgJZ_nfkBEQp9OjxzOzTRGHlkUQlXpy9G0gq1idCL43ppDrAD84HUYheGwqieo2CP3y2Z0EU3-En79fhRIaz1slA_-nKWSbLQVRiE9SgPTetbVkfvRsYzztttugHd8RiXU6vr-jisbWb8idhN7O3bEQhmN5ZVDyMlIorcOddv_Eufq4AGmJEuG5PilEjQrnRcoV7JCTUuJlGWq7-YHYjs7NwVhmtDnVcrlA3iLmzLLGTAdaB-j736h68Oxv-JM1I0AFjoG1OzPfX023c1nhobUoT39QeKsRzS8owM8DFTG_pE6dcVl70AQAA))
The fallback value of a prop not declared with `$bindable` is left untouched — it is not turned into a reactive state proxy — meaning mutations will not cause updates:
<!-- codeblock:start {"title":"Non-reactive fallback props","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Child from './Child.svelte';
</script>
<Child />
```
```svelte
<!--- file: Child.svelte --->
@ -163,6 +179,7 @@ The fallback value of a prop not declared with `$bindable` is left untouched —
clicks: {object.count}
</button>
```
<!-- codeblock:end -->
In summary: don't mutate props. Either use callback props to communicate changes, or — if parent and child should share the same object — use the [`$bindable`]($bindable) rune.

@ -5,9 +5,11 @@ tags: rune-inspect
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire ([demo](/playground/untitled#H4sIAAAAAAAACkWQ0YqDQAxFfyUMhSotdZ-tCvu431AXtGOqQ2NmmMm0LOK_r7Utfby5JzeXTOpiCIPKT5PidkSVq2_n1F7Jn3uIcEMSXHSw0evHpAjaGydVzbUQCmgbWaCETZBWMPlKj29nxBDaHj_edkAiu12JhdkYDg61JGvE_s2nR8gyuBuiJZuDJTyQ7eE-IEOzog1YD80Lb0APLfdYc5F9qnFxjiKWwbImo6_llKRQVs-2u91c_bD2OCJLkT3JZasw7KLA2XCX31qKWE6vIzNk1fKE0XbmYrBTufiI8-_8D2cUWBA_AQAA)):
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
<!-- codeblock:start {"title":"$inspect(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
let message = $state('hello');
@ -18,14 +20,17 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<button onclick={() => count++}>Increment</button>
<input bind:value={message} />
```
<!-- codeblock:end -->
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
## $inspect(...).with
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` ([demo](/playground/untitled#H4sIAAAAAAAACkVQ24qDMBD9lSEUqlTqPlsj7ON-w7pQG8c2VCchmVSK-O-bKMs-DefKYRYx6BG9qL4XQd2EohKf1opC8Nsm4F84MkbsTXAqMbVXTltuWmp5RAZlAjFIOHjuGLOP_BKVqB00eYuKs82Qn2fNjyxLtcWeyUE2sCRry3qATQIpJRyD7WPVMf9TW-7xFu53dBcoSzAOrsqQNyOe2XUKr0Xi5kcMvdDB2wSYO-I9vKazplV1-T-d6ltgNgSG1KjVUy7ZtmdbdjqtzRcphxMS1-XubOITJtPrQWMvKnYB15_1F7KKadA_AQAA)):
`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
@ -38,6 +43,7 @@ On updates, a stack trace will be printed, making it easy to find the origin of
<button onclick={() => count++}>Increment</button>
```
<!-- codeblock:end -->
## $inspect.trace(...)

@ -89,9 +89,11 @@ You can freely use destructuring and rest patterns in each blocks.
{#each expression, index}...{/each}
```
In case you just want to render something `n` times, you can omit the `as` part ([demo](/playground/untitled#H4sIAAAAAAAAE3WR0W7CMAxFf8XKNAk0WsSeUEaRpn3Guoc0MbQiJFHiMlDVf18SOrZJ48259_jaVgZmxBEZZ28thgCNFV6xBdt1GgPj7wOji0t2EqI-wa_OleGEmpLWiID_6dIaQkMxhm1UdwKpRQhVzWSaVORJNdvWpqbhAYVsYQCNZk8thzWMC_DCHMZk3wPSThNQ088I3mghD9UwSwHwlLE5PMIzVFUFq3G7WUZ2OyUvU3JOuZU332wCXTRmtPy1NgzXZtUFp8WFw9536uWqpbIgPEaDsJBW90cTOHh0KGi2XsBq5-cT6-3nPauxXqHnsHJnCFZ3CvJVkyuCQ0mFF9TZyCQ162WGvteLKfG197Y3iv_pz_fmS68Hxt8iPBPj5HscP8YvCNX7uhYCAAA=)):
In case you just want to render something `n` times, you can omit the `as` part:
<!-- codeblock:start {"title":"Chess board"} -->
```svelte
<!--- file: App.svelte --->
<div class="chess-board">
{#each { length: 8 }, rank}
{#each { length: 8 }, file}
@ -99,7 +101,22 @@ In case you just want to render something `n` times, you can omit the `as` part
{/each}
{/each}
</div>
<style>
.chess-board {
display: grid;
grid-template-columns: repeat(8, 1fr);
rows: repeat(8, 1fr);
border: 1px solid black;
aspect-ratio: 1;
.black {
background: black;
}
}
</style>
```
<!-- codeblock:end -->
## Else blocks

@ -57,9 +57,11 @@ Like function declarations, snippets can have an arbitrary number of parameters,
## Snippet scope
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks ([demo](/playground/untitled#H4sIAAAAAAAAE12P0QrCMAxFfyWrwhSEvc8p-h1OcG5RC10bmkyQ0n-3HQPBx3vCPUmCemiDrOpLULYbUdXqTKR2Sj6UA7_RCKbMbvJ9Jg33XpMcW9uKQYEAIzJ3T4QD3LSUDE-PnYA4YET4uOkGMc3W5B3xZrtvbVP9HDas2GqiZHqhMW6Tr9jGbG_oOCMImcUCwrIpFk1FqRyqpRpn0cmjHdAvnrIzuscyq_4nd3dPPD01ukE_NA6qFj9hvMYvGjJADw8BAAA=))...
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
<!-- codeblock:start {"title":"Snippets"} -->
```svelte
<!--- file: App.svelte --->
<script>
let { message = `it's great to see you!` } = $props();
</script>
@ -71,6 +73,7 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render hello('alice')}
{@render hello('bob')}
```
<!-- codeblock:end -->
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
@ -91,9 +94,11 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render x()}
```
Snippets can reference themselves and each other ([demo](/playground/untitled#H4sIAAAAAAAAE2WPTQqDMBCFrxLiRqH1Zysi7TlqF1YnENBJSGJLCYGeo5tesUeosfYH3c2bee_jjaWMd6BpfrAU6x5oTvdS0g01V-mFPkNnYNRaDKrxGxto5FKCIaeu1kYwFkauwsoUWtZYPh_3W5FMY4U2mb3egL9kIwY0rbhgiO-sDTgjSEqSTvIDs-jiOP7i_MHuFGAL6p9BtiSbOTl0GtzCuihqE87cqtyam6WRGz_vRcsZh5bmRg3gju4Fptq_kzQBAAA=)):
Snippets can reference themselves and each other:
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
```svelte
<!--- file: App.svelte --->
{#snippet blastoff()}
<span>🚀</span>
{/snippet}
@ -109,14 +114,17 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4
{@render countdown(10)}
```
<!-- codeblock:end -->
## Passing snippets to components
### Explicit props
Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE3VS247aMBD9lZGpBGwDASRegonaPvQL2qdlH5zYEKvBNvbQLbL875VzAcKyj3PmzJnLGU8UOwqSkd8KJdaCk4TsZS0cyV49wYuJuQiQpGd-N2bu_ooaI1YwJ57hpVYoFDqSEepKKw3mO7VDeTTaIvxiRS1gb_URxvO0ibrS8WanIrHUyiHs7Vmigy28RmyHHmKvDMbMmFq4cQInvGSwTsBYWYoMVhCSB2rBFFPsyl0uruTlR3JZCWvlTXl1Yy_mawiR_rbZKZrellJ-5JQ0RiBUgnFhJ9OGR7HKmwVoilXeIye8DOJGfYCgRlZ3iE876TBsZPX7hPdteO75PC4QaIo8vwNPePmANQ2fMeEFHrLD7rR1jTNkW986E8C3KwfwVr8HSHOSEBT_kGRozyIkn_zQveXDL3rIfPJHtUDwzShJd_Qk3gQCbOGLsdq4yfTRJopRuin3I7nv6kL7ARRjmLdBDG3uv1mhuLA3V2mKtqNEf_oCn8p9aN-WYqH5peP4kWBl1UwJzAEPT9U7K--0fRrrWnPTXpCm1_EVdXjpNmlA8G1hPPyM1fKgMqjFHjctXGjLhZ05w0qpDhksGrybuNEHtJnCalZWsuaTlfq6nPaaBSv_HKw-K57BjzOiVj9ZKQYKzQjZodYFqydYTRN4gPhVzTDO2xnma3HsVWjaLjT8nbfwHy7Q5f2dBAAA)):
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
<!-- codeblock:start {"title":"Explicit snippet props"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
@ -141,17 +149,65 @@ Within the template, snippets are values just like any other. As such, they can
<td>{d.qty * d.price}</td>
{/snippet}
<Table data={fruits} {header} {row} />
<Table data={fruits} +++{header} {row}+++ />
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
### Implicit props
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE3VSTa_aMBD8Kyu_SkAbCA-JSzBR20N_QXt6vIMTO8SqsY29tI2s_PcqTiB8vaPHs7MzuxuIZgdBMvJLo0QlOElIJZXwJHsLBBvb_XUASc7Mb9Yu_B-hsMMK5sUzvDQahUZPMkJ96aTFfKd3KA_WOISfrFACKmcOMFmk8TWUTjY73RFLoz1C5U4SPWzhrcN2GKDrlcGEWauEnyRwxCaDdQLWyVJksII2uaMWTDPNLtzX5YX8-kgua-GcHJVXI3u5WEPb0d83O03TMZSmfRzOkG1Db7mNacOL19JagVALxoWbztq-H8U6j0SaYp2P2BGbOyQ2v8PQIFMXLKRDk177pq0zf6d8bMrzwBdd0pamyPMb-IjNEzS2f86Gz_Dwf-2F9nvNSUJQ_EOSoTuJNvngqK5v4Pas7n4-OCwlEEJcQTIMO-nSQwtb-GSdsX46e9gbRoP9yGQ11I0rEuycunu6PHx1QnPhxm3SFN15MOlYEFJZtf0dUywMbwZOeBGsrKNLYB54-1R9WNqVdki7usim6VmQphf7mnpshiQRhNAXdoOfMyX3OgMlKtz0cGEcF27uLSul3mewjPjgOOoDukxjPS9rqfh0pb-8zs6aBSt_7505aZ7B9xOi0T9YKW4UooVsr0zB1BTrWQJ3EL-oWcZ572GxFoezCk37QLe3897-B2i2U62uBAAA)):
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
<!-- codeblock:start {"title":"Implicit snippet props"} -->
```svelte
<!-- this is semantically the same as the above -->
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
<Table data={fruits}>
{#snippet header()}
<th>fruit</th>
@ -169,12 +225,54 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
</Table>
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
### Implicit `children` snippet
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE3WOQQrCMBBFrzIMggql3ddY1Du4si5sOmIwnYRkFKX07lKqglqX8_7_w2uRDw1hjlsWI5ZqTPBoLEXMdy3K3fdZDzB5Ndfep_FKVnpWHSKNce1YiCVijirqYLwUJQOYxrsgsLmIOIZjcA1M02w4n-PpomSVvTclqyEutDX6DA2pZ7_ABIVugrmEC3XJH92P55_G39GodCmWBFrQJ2PrQAwdLGHig_NxNv9xrQa1dhWIawrv1Wzeqawa8953D-8QOmaEAQAA)):
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Button from './Button.svelte';
</script>
<Button>click me</Button>
```
@ -187,6 +285,7 @@ Any content inside the component tags that is _not_ a snippet declaration implic
<!-- result will be <button>click me</button> -->
<button>{@render children()}</button>
```
<!-- codeblock:end -->
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
@ -256,9 +355,21 @@ We can tighten things up further by declaring a generic, so that `data` and `row
## Exporting snippets
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) ([demo](/playground/untitled#H4sIAAAAAAAAE3WPwY7CMAxEf8UyB1hRgdhjl13Bga8gHFJipEqtGyUGFUX5dxJUtEB3b9bYM_MckHVLWOKut50TMuC5tpbEY4GnuiGP5T6gXG0-ykLSB8vW2oW_UCNZq7Snv_Rjx0Kc4kpc-6OrrfwoVlK3uQ4CaGMgwsl1LUwXy0f54J9-KV4vf20cNo7YkMu22aqAz4-oOLUI9YKluDPF4h_at-hX5PFyzA1tZ84N3fGpf8YfUU6GvDumLqDKmEqCjjCHUEX4hqDTWCU5PJ6Or38c4g1cPu9tnAEAAA==)):
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import { add } from './snippets.svelte';
</script>
{@render add(1, 2)}
```
```svelte
<!--- file: snippets.svelte --->
<script module>
export { add };
</script>
@ -267,6 +378,7 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
{a} + {b} = {a + b}
{/snippet}
```
<!-- codeblock:end -->
> [!NOTE]
> This requires Svelte 5.5.0 or newer

@ -54,9 +54,11 @@ A `bind:value` directive on an `<input>` element binds the input's `value` prope
<p>{message}</p>
```
In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number ([demo](/playground/untitled#H4sIAAAAAAAAE6WPwYoCMQxAfyWEPeyiOOqx2w74Hds9pBql0IllmhGXYf5dKqwiyILsLXnwwsuI-5i4oPkaUX8yo7kCnKNQV7dNzoty4qSVBSr8jG-Poixa0KAt2z5mbb14TaxA4OCtKCm_rz4-f2m403WltrlrYhMFTtcLNkoeFGqZ8yhDF7j3CCHKzpwoDexGmqCL4jwuPUJHZ-dxVcfmyYGe5MAv-La5pbxYFf5Z9Zf_UJXb-sEMquFgJJhBmGyTW5yj8lnRaD_w9D1dAKSSj7zqAQAA)):
In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number:
<!-- codeblock:start {"title":"Numeric bindings"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
@ -74,6 +76,7 @@ In the case of a numeric input (`type="number"` or `type="range"`), the value wi
<p>{a} + {b} = {a + b}</p>
```
<!-- codeblock:end -->
If the input is empty or invalid (in the case of `type="number"`), the value is `undefined`.
@ -144,10 +147,11 @@ Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs
## `<input bind:group>`
Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sIAAAAAAAAE62T32_TMBDH_5XDQkpbrct7SCMGEvCEECDxsO7BSW6L2c227EvbKOv_jp0f6jYhQKJv5_P3PvdL1wstH1Bk4hMSGdgbRzUssFaM9VJciFtF6EV23QvubNRFR_BPUVfWXvodEkdfKT3-zl8Zzag5YETuK6csF1u9ZUIGNo4VkYQNvPYsGRfJF5JKJ8s3QRJE6WoFb2Nq6K-ck13u2Sl9Vxxhlc6QUBIFnz9Brm9ifJ6esun81XoNd860FmtwslYGlLYte5AO4aHlVhJ1gIeKWq92COt1iMtJlkhFPkgh1rHZiiF6K6BUus4G5KafGznCTlIbVUMfQZUWMJh5OrL-C_qjMYSwb1DyiH7iOEuCb1ZpWTUjfHqcwC_GWDVY3ZfmME_SGttSmD9IHaYatvWHIc6xLyqad3mq6KuqcCwnWn9p8p-p71BqP2IH81zc9w2in-od7XORP7ayCpd5YCeXI_-p59mObPF9WmwGpx3nqS2Gzw8TO3zOaS5_GqUXyQUkS3h8hOSz0ZhMESHGc0c4Hm3MAn00t1wrb0l2GZRkqvt4sXwczm6Qh8vnUJzI2LV4vAkvqWgfehTZrSSPx19WiVfFfAQAAA==)):
Inputs that work together can use `bind:group`:
<!-- codeblock:start {"title":"bind:group"} -->
```svelte
<!--- file: BurritoChooser.svelte --->
<!--- file: App.svelte --->
<script>
let tortilla = $state('Plain');
@ -155,6 +159,8 @@ Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sI
let fillings = $state([]);
</script>
<h1>Customize your burrito</h1>
<!-- grouped radio inputs are mutually exclusive -->
<label><input type="radio" bind:group={tortilla} value="Plain" /> Plain</label>
<label><input type="radio" bind:group={tortilla} value="Whole wheat" /> Whole wheat</label>
@ -165,7 +171,17 @@ Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sI
<label><input type="checkbox" bind:group={fillings} value="Beans" /> Beans</label>
<label><input type="checkbox" bind:group={fillings} value="Cheese" /> Cheese</label>
<label><input type="checkbox" bind:group={fillings} value="Guac (extra)" /> Guac (extra)</label>
<p>Tortilla: {tortilla}</p>
<p>Fillings: {fillings.join(', ') || 'None'}</p>
<style>
label {
display: block;
}
</style>
```
<!-- codeblock:end -->
> [!NOTE] `bind:group` only works if the inputs are in the same Svelte component.

@ -25,9 +25,11 @@ The experimental flag will be removed in Svelte 6.
## 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)...
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...
<!-- codeblock:start {"title":"Synchronized updates"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
@ -43,6 +45,7 @@ When an `await` expression depends on a particular piece of state, changes to th
<p>{a} + {b} = {await add(a, b)}</p>
```
<!-- codeblock:end -->
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —

@ -208,7 +208,7 @@ export const myGlobalState = $state({
In many cases this is perfectly fine, but there is a risk: if you mutate the state during server-side rendering (which is discouraged, but entirely possible!)...
```svelte
<!--- file: App.svelte ---->
<!--- file: App.svelte --->
<script>
import { myGlobalState } from './state.svelte.js';

@ -101,7 +101,7 @@ export function process_children(nodes, initial, is_element, context) {
if (is_static_element(node)) {
skipped += 1;
} else if (
node.type === 'EachBlock' &&
(node.type === 'EachBlock' || node.type === 'HtmlTag') &&
nodes.length === 1 &&
is_element &&
// In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled
@ -109,8 +109,6 @@ export function process_children(nodes, initial, is_element, context) {
!node.metadata.expression.is_async()
) {
node.metadata.is_controlled = true;
} else if (node.type === 'HtmlTag' && nodes.length === 1 && is_element) {
node.metadata.is_controlled = true;
} else {
const id = flush_node(
false,

@ -0,0 +1,10 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, `<div><div><p>first test</p></div> other test</div>`);
}
});

@ -0,0 +1,14 @@
<script>
function firstTest() {
return Promise.resolve('<p>first test</p>');
}
function otherTest() {
return Promise.resolve('other test');
}
</script>
<div>
<div>{@html await firstTest()}</div>
{await otherTest()}
</div>

@ -0,0 +1,43 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true,
async test({ assert, target }) {
await tick();
const [a_b_fork, a_c, b_d, shift, pop, commit] = target.querySelectorAll('button');
const [p] = target.querySelectorAll('p');
a_b_fork.click();
await tick();
assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0');
a_c.click();
await tick();
assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0');
b_d.click();
await tick();
assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0');
shift.click();
await tick();
assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0');
shift.click();
await tick();
assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0');
shift.click();
await tick();
assert.htmlEqual(p.innerHTML, 'a 1 | b 0 | c 1 | d 0');
shift.click();
await tick();
assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1');
commit.click();
await tick();
assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1');
}
});

@ -0,0 +1,31 @@
<script>
import { fork } from 'svelte';
let a = $state(0);
let b = $state(0);
let c = $state(0);
let d = $state(0);
let f;
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<p>a {await delay(a)} | b {await delay(b)} | c {c} | d {d}</p>
<button onclick={() => {f = fork(() => {a++;b++;});}}>
a and b (fork)
</button>
<button onclick={() => {a++;c++;}}>
a and c
</button>
<button onclick={() => {b++;d++;}}>
b and d
</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred.pop()?.()}>pop</button>
<button onclick={() => f.commit()}>commit fork</button>

@ -0,0 +1,11 @@
<script>
let { x } = $props();
</script>
<!-- checks direct source, indirect derived, block effect, async effect -->
{x}
{JSON.stringify(x)}
{#if x === 'universe'}universe{:else}world{/if}
{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if}
{await Promise.resolve(x)}
{await Promise.resolve(JSON.stringify(x))}

@ -0,0 +1,171 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true, // TODO more combinations pass on https://github.com/sveltejs/svelte/pull/17971
timeout: 20_000,
async test({ assert, target }) {
const [x, fork_x, y, fork_y, shift, pop, commit_x, commit_y, reset] =
target.querySelectorAll('button');
const initial = `
<button>x</button>
<button>x (fork)</button>
<button>y++</button>
<button>y++ (fork)</button>
<button>shift</button>
<button>pop</button>
<button>commit x</button>
<button>commit y</button>
<button>reset</button>
<hr>
`;
const final = `
<button>x</button>
<button>x (fork)</button>
<button>y++</button>
<button>y++ (fork)</button>
<button>shift</button>
<button>pop</button>
<button>commit x</button>
<button>commit y</button>
<button>reset</button>
universe
universe
"universe"
universe
universe
universe
"universe"
<hr>
universe
"universe"
universe
universe
universe
"universe"
`;
/** @param {HTMLElement} button */
async function click(button) {
button.click();
await tick();
}
/**
* Generate all permutations of an array.
* @param {HTMLElement[]} actions
* @returns {HTMLElement[][]}
*/
function permutations(actions) {
if (actions.length <= 1) return [actions];
/** @type {HTMLElement[][]} */
const result = [];
for (let i = 0; i < actions.length; i++) {
const head = actions[i];
const rest = actions.slice(0, i).concat(actions.slice(i + 1));
for (const tail of permutations(rest)) {
result.push([head, ...tail]);
}
}
return result;
}
/**
* Keep only valid orders where fork commits happen after their fork action.
* @param {HTMLElement[]} order
*/
function is_valid_order(order) {
const x_fork_index = order.indexOf(fork_x);
const commit_x_index = order.indexOf(commit_x);
if (commit_x_index !== -1 && (x_fork_index === -1 || commit_x_index < x_fork_index)) {
return false;
}
const y_fork_index = order.indexOf(fork_y);
const commit_y_index = order.indexOf(commit_y);
if (commit_y_index !== -1 && (y_fork_index === -1 || commit_y_index < y_fork_index)) {
return false;
}
return true;
}
/**
* Four control scenarios:
* - x direct, y direct
* - x direct, y via fork+commit
* - x via fork+commit, y direct
* - x via fork+commit, y via fork+commit
*/
const control_scenarios = [
[x, y],
[x, fork_y, commit_y],
[fork_x, commit_x, y],
[fork_x, commit_x, fork_y, commit_y]
];
const control_orders = control_scenarios.flatMap((scenario) =>
permutations(scenario).filter(is_valid_order)
);
/**
* All shift/pop combinations for draining async work.
* We click three times because this scenario can queue up to 3 deferred resolutions.
*/
const resolve_orders = [
[shift, shift, shift],
[shift, pop, pop],
[pop, shift, shift],
[pop, pop, pop]
];
for (const controls of control_orders) {
for (const resolves of resolve_orders) {
for (const action of controls) {
await click(action);
}
for (const action of resolves) {
await click(action);
}
const failure_msg = `Failed for: ${controls
.map((btn) => btn.textContent)
.concat(...resolves.map((btn) => btn.textContent))
.join(', ')}`;
assert.htmlEqual(target.innerHTML, final, failure_msg);
await click(reset);
assert.htmlEqual(target.innerHTML, initial, failure_msg);
}
}
const other_scenarios = [
[x, shift, y, shift, shift],
[x, shift, y, pop, pop],
[fork_x, shift, y, shift, commit_x, shift],
[fork_x, shift, y, pop, commit_x, pop],
[y, shift, x, shift, shift],
[y, shift, x, pop, pop],
[fork_y, shift, x, shift, commit_y, shift],
[fork_y, shift, x, pop, commit_y, pop]
];
for (const scenario of other_scenarios) {
for (const action of scenario) {
await click(action);
}
const failure_msg = `Failed for: ${scenario.map((btn) => btn.textContent).join(', ')}`;
assert.htmlEqual(target.innerHTML, final, failure_msg);
await click(reset);
assert.htmlEqual(target.innerHTML, initial, failure_msg);
}
}
});

@ -0,0 +1,41 @@
<script>
import { fork } from 'svelte';
import Child from './Child.svelte';
let x = $state('world');
let y = $state(0);
let fx;
let fy;
const deferred = [];
function delay(value) {
if (value !== 'universe') return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
function delay2(value) {
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<button onclick={() => (x = 'universe')}>x</button>
<button onclick={() => (fx = fork(() => {x = 'universe';}))}>x (fork)</button>
<button onclick={() => y++}>y++</button>
<button onclick={() => (fy = fork(() => {y++;}))}>y++ (fork)</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred.pop()?.()}>pop</button>
<button onclick={() => fx.commit()}>commit x</button>
<button onclick={() => fy.commit()}>commit y</button>
<button onclick={() => {x = 'world'; y = 0;}}>reset</button>
{#if x === 'universe'}
{await delay(x)}
<Child {x} />
{/if}
<hr>
{#if y > 0}
<Child x={await delay2(x)} />
{/if}

@ -0,0 +1,11 @@
<script>
let { x } = $props();
</script>
<!-- checks direct source, indirect derived, block effect, async effect -->
{x}
{JSON.stringify(x)}
{#if x === 'universe'}universe{:else}world{/if}
{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if}
{await Promise.resolve(x)}
{await Promise.resolve(JSON.stringify(x))}

@ -0,0 +1,90 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) {
const [x, y, shift, pop, commit] = target.querySelectorAll('button');
x.click();
await tick();
y.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>shift</button>
<button>pop</button>
<button>commit</button>
<hr>
`
);
commit.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>shift</button>
<button>pop</button>
<button>commit</button>
<hr>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>shift</button>
<button>pop</button>
<button>commit</button>
universe
universe
"universe"
universe
universe
universe
"universe"
<hr>
`
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>shift</button>
<button>pop</button>
<button>commit</button>
universe
universe
"universe"
universe
universe
universe
"universe"
<hr>
universe
"universe"
universe
universe
universe
"universe"
`
);
}
});

@ -0,0 +1,36 @@
<script>
import { fork } from 'svelte';
import Child from './Child.svelte';
let x = $state('world');
let y = $state(0);
let f;
const deferred = [];
function delay(value) {
if (value !== 'universe') return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
function delay2(value) {
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<button onclick={() => (f = fork(() => {x = 'universe';}))}>x</button>
<button onclick={() => y++}>y++</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred.pop()?.()}>pop</button>
<button onclick={() => f.commit()}>commit</button>
{#if x === 'universe'}
{await delay(x)}
<Child {x} />
{/if}
<hr>
{#if y > 0}
<Child x={await delay2(x)} />
{/if}

@ -0,0 +1,11 @@
<script>
let { x } = $props();
</script>
<!-- checks direct source, indirect derived, block effect, async effect -->
{x}
{JSON.stringify(x)}
{#if x === 'universe'}universe{:else}world{/if}
{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if}
{await Promise.resolve(x)}
{await Promise.resolve(JSON.stringify(x))}

@ -0,0 +1,71 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) {
const [x, y, shift, pop, commit] = target.querySelectorAll('button');
y.click();
await tick();
x.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>shift</button>
<button>pop</button>
<button>commit</button>
<hr>
`
);
commit.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>shift</button>
<button>pop</button>
<button>commit</button>
<hr>
`
);
shift.click();
await tick();
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>shift</button>
<button>pop</button>
<button>commit</button>
universe
universe
"universe"
universe
universe
universe
"universe"
<hr>
universe
"universe"
universe
universe
universe
"universe"
`
);
}
});

@ -0,0 +1,36 @@
<script>
import { fork } from 'svelte';
import Child from './Child.svelte';
let x = $state('world');
let y = $state(0);
let f;
const deferred = [];
function delay(value) {
if (value !== 'universe') return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
function delay2(value) {
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<button onclick={() => (f = fork(() => {x = 'universe';}))}>x</button>
<button onclick={() => y++}>y++</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred.pop()?.()}>pop</button>
<button onclick={() => f.commit()}>commit</button>
{#if x === 'universe'}
{await delay(x)}
<Child {x} />
{/if}
<hr>
{#if y > 0}
<Child x={await delay2(x)} />
{/if}

@ -0,0 +1,11 @@
<script>
let { x } = $props();
</script>
<!-- checks direct source, indirect derived, block effect, async effect -->
{x}
{JSON.stringify(x)}
{#if x === 'universe'}universe{:else}world{/if}
{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if}
{await Promise.resolve(x)}
{await Promise.resolve(JSON.stringify(x))}

@ -0,0 +1,70 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [x, y, shift, pop, commit] = target.querySelectorAll('button');
y.click();
await tick();
x.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>shift</button>
<button>pop</button>
<button>commit</button>
<hr>
`
);
commit.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>shift</button>
<button>pop</button>
<button>commit</button>
<hr>
`
);
pop.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>shift</button>
<button>pop</button>
<button>commit</button>
universe
universe
"universe"
universe
universe
universe
"universe"
<hr>
universe
"universe"
universe
universe
universe
"universe"
`
);
}
});

@ -0,0 +1,36 @@
<script>
import { fork } from 'svelte';
import Child from './Child.svelte';
let x = $state('world');
let y = $state(0);
let f;
const deferred = [];
function delay(value) {
if (value !== 'universe') return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
function delay2(value) {
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<button onclick={() => (f = fork(() => {x = 'universe';}))}>x</button>
<button onclick={() => y++}>y++</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred.pop()?.()}>pop</button>
<button onclick={() => f.commit()}>commit</button>
{#if x === 'universe'}
{await delay(x)}
<Child {x} />
{/if}
<hr>
{#if y > 0}
<Child x={await delay2(x)} />
{/if}

@ -0,0 +1,11 @@
<script>
let { x } = $props();
</script>
<!-- checks direct source, indirect derived, block effect, async effect -->
{x}
{JSON.stringify(x)}
{#if x === 'universe'}universe{:else}world{/if}
{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if}
{await Promise.resolve(x)}
{await Promise.resolve(JSON.stringify(x))}

@ -0,0 +1,76 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) {
const [x, y, resolve, commit] = target.querySelectorAll('button');
x.click();
await tick();
y.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
<button>commit</button>
<hr>
world
"world"
world
world
world
"world"
`
);
commit.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
<button>commit</button>
<hr>
world
"world"
world
world
world
"world"
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
<button>commit</button>
universe
universe
"universe"
universe
universe
universe
"universe"
<hr>
universe
"universe"
universe
universe
universe
"universe"
`
);
}
});

@ -0,0 +1,32 @@
<script>
import { fork } from 'svelte';
import Child from './Child.svelte';
let x = $state('world');
let y = $state(0);
let f;
const deferred = [];
function delay(value) {
if (value !== 'universe') return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<button onclick={() => (f = fork(() => {x = 'universe';}))}>x</button>
<button onclick={() => y++}>y++</button>
<button onclick={() => deferred.pop()?.()}>resolve</button>
<button onclick={() => f.commit()}>commit</button>
{#if x === 'universe'}
{await delay(x)}
<Child {x} />
{/if}
<hr>
{#if y > 0}
<Child {x} />
{/if}

@ -0,0 +1,11 @@
<script>
let { x } = $props();
</script>
<!-- checks direct source, indirect derived, block effect, async effect -->
{x}
{JSON.stringify(x)}
{#if x === 'universe'}universe{:else}world{/if}
{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if}
{await Promise.resolve(x)}
{await Promise.resolve(JSON.stringify(x))}

@ -0,0 +1,85 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) {
const [x, y, resolve, commit] = target.querySelectorAll('button');
x.click();
await tick();
y.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
<button>commit</button>
<hr>
`
);
commit.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
<button>commit</button>
<hr>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
<button>commit</button>
<hr>
world
"world"
world
world
world
"world"
`
);
resolve.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
<button>commit</button>
universe
universe
"universe"
universe
universe
universe
"universe"
<hr>
universe
"universe"
universe
universe
universe
"universe"
`
);
}
});

@ -0,0 +1,36 @@
<script>
import { fork } from 'svelte';
import Child from './Child.svelte';
let x = $state('world');
let y = $state(0);
let f;
const deferred = [];
function delay(value) {
if (value !== 'universe') return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
function delay2(value) {
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<button onclick={() => (f = fork(() => {x = 'universe';}))}>x</button>
<button onclick={() => y++}>y++</button>
<button onclick={() => deferred.pop()?.()}>resolve</button>
<button onclick={() => f.commit()}>commit</button>
{#if x === 'universe'}
{await delay(x)}
<Child {x} />
{/if}
<hr>
{#if y > 0}
<Child x={await delay2(x)} />
{/if}

@ -4,6 +4,7 @@ import { it } from 'vitest';
export interface BaseTest {
skip?: boolean;
solo?: boolean;
timeout?: number;
}
/**
@ -30,7 +31,7 @@ export function suite<Test extends BaseTest>(fn: (config: Test, test_dir: string
await for_each_dir<Test>(cwd, samples_dir, (config, dir) => {
let it_fn = config.skip ? it.skip : config.solo ? it.only : it;
it_fn(dir, () => fn(config, `${cwd}/${samples_dir}/${dir}`));
it_fn(dir, { timeout: config.timeout }, () => fn(config, `${cwd}/${samples_dir}/${dir}`));
});
}
};
@ -57,7 +58,7 @@ export function suite_with_variants<Test extends BaseTest, Variants extends stri
const solo = config.solo;
let it_fn = skip ? it.skip : solo ? it.only : it;
it_fn(`${dir} (${variant})`, async () => {
it_fn(`${dir} (${variant})`, { timeout: config.timeout }, async () => {
if (!called_common) {
called_common = true;
common = await common_setup(config, `${cwd}/${samples_dir}/${dir}`);

Loading…
Cancel
Save