Merge remote-tracking branch 'origin' into elliott/variadic-snippets

pull/10320/head
S. Elliott Johnson 2 years ago
commit 486370e4ee

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow transition undefined payload

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: apply key animations on proxied arrays

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve signal consumer tracking behavior

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve internal signal dependency checking logic

@ -20,6 +20,7 @@
"chilled-pumas-invite",
"chilly-dolphins-lick",
"clean-eels-beg",
"clever-chefs-relate",
"clever-rockets-burn",
"cold-birds-own",
"cool-ants-leave",
@ -66,6 +67,7 @@
"itchy-beans-melt",
"itchy-kings-deliver",
"itchy-lions-wash",
"itchy-terms-guess",
"khaki-mails-draw",
"khaki-moose-arrive",
"kind-deers-lay",
@ -98,6 +100,7 @@
"old-houses-drum",
"old-mails-sneeze",
"old-oranges-compete",
"olive-kangaroos-brake",
"orange-dingos-poke",
"polite-dolphins-care",
"polite-pumpkins-guess",
@ -125,6 +128,7 @@
"shiny-baboons-play",
"shiny-shrimps-march",
"slimy-clouds-talk",
"slimy-walls-draw",
"slow-chefs-dream",
"small-papayas-laugh",
"smart-parents-swim",
@ -134,6 +138,7 @@
"sour-forks-stare",
"sour-rules-march",
"spicy-plums-admire",
"stale-books-perform",
"stale-comics-look",
"strong-gifts-smoke",
"strong-lemons-provide",
@ -156,6 +161,7 @@
"thirty-impalas-repair",
"thirty-wombats-relax",
"three-suits-grin",
"tidy-buses-whisper",
"tiny-kings-whisper",
"twelve-dragons-join",
"twelve-onions-juggle",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly call exported state

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: take into account setters when spreading and binding

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: transform `{@render ...}` expression

@ -1,5 +1,21 @@
# svelte
## 5.0.0-next.30
### Patch Changes
- fix: allow transition undefined payload ([#10117](https://github.com/sveltejs/svelte/pull/10117))
- fix: apply key animations on proxied arrays ([#10113](https://github.com/sveltejs/svelte/pull/10113))
- fix: improve internal signal dependency checking logic ([#10111](https://github.com/sveltejs/svelte/pull/10111))
- fix: correctly call exported state ([#10114](https://github.com/sveltejs/svelte/pull/10114))
- fix: take into account setters when spreading and binding ([#10091](https://github.com/sveltejs/svelte/pull/10091))
- fix: transform `{@render ...}` expression ([#10116](https://github.com/sveltejs/svelte/pull/10116))
## 5.0.0-next.29
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.29",
"version": "5.0.0-next.30",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -7,7 +7,7 @@ import { global_visitors } from './visitors/global.js';
import { javascript_visitors } from './visitors/javascript.js';
import { javascript_visitors_runes } from './visitors/javascript-runes.js';
import { javascript_visitors_legacy } from './visitors/javascript-legacy.js';
import { serialize_get_binding } from './utils.js';
import { is_state_source, serialize_get_binding } from './utils.js';
import { remove_types } from '../typescript.js';
/**
@ -242,9 +242,7 @@ export function client_component(source, analysis, options) {
const properties = analysis.exports.map(({ name, alias }) => {
const binding = analysis.instance.scope.get(name);
const is_source =
(binding?.kind === 'state' || binding?.kind === 'frozen_state') &&
(!state.analysis.immutable || binding.reassigned);
const is_source = binding !== null && is_state_source(binding, state);
// TODO This is always a getter because the `renamed-instance-exports` test wants it that way.
// Should we for code size reasons make it an init in runes mode and/or non-dev mode?

@ -45,6 +45,18 @@ export function get_assignment_value(node, { state, visit }) {
}
}
/**
* @param {import('#compiler').Binding} binding
* @param {import('./types').ClientTransformState} state
* @returns {boolean}
*/
export function is_state_source(binding, state) {
return (
(binding.kind === 'state' || binding.kind === 'frozen_state') &&
(!state.analysis.immutable || binding.reassigned || state.analysis.accessors)
);
}
/**
* @param {import('estree').Identifier} node
* @param {import('./types').ClientTransformState} state
@ -93,8 +105,7 @@ export function serialize_get_binding(node, state) {
}
if (
((binding.kind === 'state' || binding.kind === 'frozen_state') &&
(!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) ||
is_state_source(binding, state) ||
binding.kind === 'derived' ||
binding.kind === 'legacy_reactive'
) {
@ -491,33 +502,6 @@ export function get_prop_source(binding, state, name, initial) {
return b.call('$.prop', ...args);
}
/**
* Creates the output for a state declaration.
* @param {import('estree').VariableDeclarator} declarator
* @param {import('../../scope').Scope} scope
* @param {import('estree').Expression} value
*/
export function create_state_declarators(declarator, scope, value) {
// in the simple `let count = $state(0)` case, we rewrite `$state` as `$.source`
if (declarator.id.type === 'Identifier') {
return [b.declarator(declarator.id, b.call('$.mutable_source', value))];
}
const tmp = scope.generate('tmp');
const paths = extract_paths(declarator.id);
return [
b.declarator(b.id(tmp), value), // TODO inject declarator for opts, so we can use it below
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const binding = scope.get(/** @type {import('estree').Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value
);
})
];
}
/** @param {import('estree').Expression} node */
export function should_proxy_or_freeze(node) {
if (

@ -1,7 +1,33 @@
import { is_hoistable_function } from '../../utils.js';
import * as b from '../../../../utils/builders.js';
import { extract_paths } from '../../../../utils/ast.js';
import { create_state_declarators, get_prop_source, serialize_get_binding } from '../utils.js';
import { get_prop_source, serialize_get_binding } from '../utils.js';
/**
* Creates the output for a state declaration.
* @param {import('estree').VariableDeclarator} declarator
* @param {import('../../../scope.js').Scope} scope
* @param {import('estree').Expression} value
*/
function create_state_declarators(declarator, scope, value) {
if (declarator.id.type === 'Identifier') {
return [b.declarator(declarator.id, b.call('$.mutable_source', value))];
}
const tmp = scope.generate('tmp');
const paths = extract_paths(declarator.id);
return [
b.declarator(b.id(tmp), value),
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const binding = scope.get(/** @type {import('estree').Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value
);
})
];
}
/** @type {import('../types.js').ComponentVisitors} */
export const javascript_visitors_legacy = {

@ -2,8 +2,8 @@ import { get_rune } from '../../../scope.js';
import { is_hoistable_function, transform_inspect_rune } from '../../utils.js';
import * as b from '../../../../utils/builders.js';
import * as assert from '../../../../utils/assert.js';
import { create_state_declarators, get_prop_source, should_proxy_or_freeze } from '../utils.js';
import { unwrap_ts_expression } from '../../../../utils/ast.js';
import { get_prop_source, is_state_source, should_proxy_or_freeze } from '../utils.js';
import { extract_paths, unwrap_ts_expression } from '../../../../utils/ast.js';
/** @type {import('../types.js').ComponentVisitors} */
export const javascript_visitors_runes = {
@ -223,66 +223,79 @@ export const javascript_visitors_runes = {
}
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
let value =
const value =
args.length === 0
? b.id('undefined')
: /** @type {import('estree').Expression} */ (visit(args[0]));
if (declarator.id.type === 'Identifier') {
if (rune === '$state') {
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
if (rune === '$state' || rune === '$state.frozen') {
/**
* @param {import('estree').Identifier} id
* @param {import('estree').Expression} value
*/
const create_state_declarator = (id, value) => {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
if (should_proxy_or_freeze(value)) {
value = b.call('$.proxy', value);
value = b.call(rune === '$state' ? '$.proxy' : '$.freeze', value);
}
if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) {
if (is_state_source(binding, state)) {
value = b.call('$.source', value);
}
} else if (rune === '$state.frozen') {
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
if (should_proxy_or_freeze(value)) {
value = b.call('$.freeze', value);
}
return value;
};
if (binding.reassigned) {
value = b.call('$.source', value);
}
if (declarator.id.type === 'Identifier') {
declarations.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, value))
);
} else {
value = b.call('$.derived', b.thunk(value));
const tmp = state.scope.generate('tmp');
const paths = extract_paths(declarator.id);
declarations.push(
b.declarator(b.id(tmp), value),
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const binding = state.scope.get(
/** @type {import('estree').Identifier} */ (path.node).name
);
return b.declarator(
path.node,
binding?.kind === 'state' || binding?.kind === 'frozen_state'
? create_state_declarator(binding.node, value)
: value
);
})
);
}
declarations.push(b.declarator(declarator.id, value));
continue;
}
if (rune === '$derived') {
const bindings = state.scope.get_bindings(declarator);
const id = state.scope.generate('derived_value');
declarations.push(
b.declarator(
b.id(id),
b.call(
'$.derived',
b.thunk(
b.block([
b.let(declarator.id, value),
b.return(b.array(bindings.map((binding) => binding.node)))
])
if (declarator.id.type === 'Identifier') {
declarations.push(b.declarator(declarator.id, b.call('$.derived', b.thunk(value))));
} else {
const bindings = state.scope.get_bindings(declarator);
const id = state.scope.generate('derived_value');
declarations.push(
b.declarator(
b.id(id),
b.call(
'$.derived',
b.thunk(
b.block([
b.let(declarator.id, value),
b.return(b.array(bindings.map((binding) => binding.node)))
])
)
)
)
)
);
for (let i = 0; i < bindings.length; i++) {
bindings[i].expression = b.member(b.call('$.get', b.id(id)), b.literal(i), true);
);
for (let i = 0; i < bindings.length; i++) {
bindings[i].expression = b.member(b.call('$.get', b.id(id)), b.literal(i), true);
}
}
continue;
}
declarations.push(...create_state_declarators(declarator, state.scope, value));
}
if (declarations.length === 0) {

@ -1120,9 +1120,10 @@ const template_visitors = {
state.init.push(anchor);
state.template.push(t_expression(anchor_id));
const expression = /** @type {import('estree').Expression} */ (context.visit(node.expression));
const snippet_function = state.options.dev
? b.call('$.validate_snippet', node.expression)
: node.expression;
? b.call('$.validate_snippet', expression)
: expression;
const snippet_args = node.arguments.map((arg) => {
return /** @type {import('estree').Expression} */ (context.visit(arg));

@ -417,7 +417,9 @@ function reconcile_tracked_array(
insert_each_item_block(block, dom, is_controlled, null);
}
} else {
var should_update_block = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update_block =
(flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0 || is_animated;
var start = 0;
/** @type {null | Text | Element | Comment} */
@ -500,7 +502,6 @@ function reconcile_tracked_array(
mark_lis(sources);
}
// If keys are animated, we need to do updates before actual moves
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_create;
if (is_animated) {
var i = b_length;

@ -2646,8 +2646,13 @@ const spread_props_handler = {
if (typeof p === 'object' && p !== null && key in p) return p[key];
}
},
getOwnPropertyDescriptor() {
return { enumerable: true, configurable: true };
getOwnPropertyDescriptor(target, key) {
let i = target.props.length;
while (i--) {
let p = target.props[i];
if (is_function(p)) p = p();
if (typeof p === 'object' && p !== null && key in p) return get_descriptor(p, key);
}
},
has(target, key) {
for (let p of target.props) {

@ -50,6 +50,8 @@ let current_queued_effects = [];
/** @type {Array<() => void>} */
let current_queued_tasks = [];
/** @type {Array<() => void>} */
let current_queued_microtasks = [];
let flush_count = 0;
// Handle signal reactivity tree dependencies and consumer
@ -349,15 +351,21 @@ function execute_signal_fn(signal) {
if (current_dependencies !== null) {
let i;
if (dependencies !== null) {
const dep_length = dependencies.length;
// Include any dependencies up until the current_dependencies_index.
const full_dependencies =
current_dependencies_index === 0
? dependencies
: dependencies.slice(0, current_dependencies_index).concat(current_dependencies);
const dep_length = full_dependencies.length;
// If we have more than 16 elements in the array then use a Set for faster performance
// TODO: evaluate if we should always just use a Set or not here?
const current_dependencies_set = dep_length > 16 ? new Set(current_dependencies) : null;
const current_dependencies_set = dep_length > 16 ? new Set(full_dependencies) : null;
for (i = current_dependencies_index; i < dep_length; i++) {
const dependency = dependencies[i];
const dependency = full_dependencies[i];
if (
(current_dependencies_set !== null && !current_dependencies_set.has(dependency)) ||
!current_dependencies.includes(dependency)
!full_dependencies.includes(dependency)
) {
remove_consumer(signal, dependency, false);
}
@ -448,10 +456,14 @@ function remove_consumer(signal, dependency, remove_unowned) {
function remove_consumers(signal, start_index, remove_unowned) {
const dependencies = signal.d;
if (dependencies !== null) {
const active_dependencies = start_index === 0 ? null : dependencies.slice(0, start_index);
let i;
for (i = start_index; i < dependencies.length; i++) {
const dependency = dependencies[i];
remove_consumer(signal, dependency, remove_unowned);
// Avoid removing a consumer if we know that it is active (start_index will not be 0)
if (active_dependencies === null || !active_dependencies.includes(dependency)) {
remove_consumer(signal, dependency, remove_unowned);
}
}
}
}
@ -573,6 +585,11 @@ function flush_queued_effects(effects) {
function process_microtask() {
is_micro_task_queued = false;
if (current_queued_microtasks.length > 0) {
const tasks = current_queued_microtasks.slice();
current_queued_microtasks = [];
run_all(tasks);
}
if (flush_count > 101) {
return;
}
@ -631,6 +648,18 @@ export function schedule_task(fn) {
current_queued_tasks.push(fn);
}
/**
* @param {() => void} fn
* @returns {void}
*/
export function schedule_microtask(fn) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(process_microtask);
}
current_queued_microtasks.push(fn);
}
/**
* @returns {void}
*/
@ -691,6 +720,9 @@ export function flushSync(fn) {
if (current_queued_pre_and_render_effects.length > 0 || effects.length > 0) {
flushSync();
}
if (is_micro_task_queued) {
process_microtask();
}
if (is_task_queued) {
process_task();
}

@ -21,7 +21,7 @@ import {
managed_effect,
managed_pre_effect,
mark_subtree_inert,
schedule_task,
schedule_microtask,
untrack
} from './runtime.js';
import { raf } from './timing.js';
@ -279,6 +279,9 @@ function create_transition(dom, init, direction, effect) {
// @ts-ignore
payload = payload({ direction: curr_direction });
}
if (payload == null) {
return;
}
const duration = payload.duration ?? 300;
const delay = payload.delay ?? 0;
const css_fn = payload.css;
@ -354,11 +357,15 @@ function create_transition(dom, init, direction, effect) {
cancelled = false;
create_animation();
}
dispatch_event(dom, 'introstart');
if (needs_reverse) {
/** @type {Animation | TickAnimation} */ (animation).reverse();
if (animation === null) {
transition.x();
} else {
dispatch_event(dom, 'introstart');
if (needs_reverse) {
/** @type {Animation | TickAnimation} */ (animation).reverse();
}
/** @type {Animation | TickAnimation} */ (animation).play();
}
/** @type {Animation | TickAnimation} */ (animation).play();
},
// out
o() {
@ -368,11 +375,15 @@ function create_transition(dom, init, direction, effect) {
cancelled = false;
create_animation();
}
dispatch_event(dom, 'outrostart');
if (needs_reverse) {
/** @type {Animation | TickAnimation} */ (animation).reverse();
if (animation === null) {
transition.x();
} else {
/** @type {Animation | TickAnimation} */ (animation).play();
dispatch_event(dom, 'outrostart');
if (needs_reverse) {
/** @type {Animation | TickAnimation} */ (animation).reverse();
} else {
/** @type {Animation | TickAnimation} */ (animation).play();
}
}
},
// cancel
@ -671,7 +682,7 @@ function each_item_animate(block, transitions, index, index_is_reactive) {
transition.c();
}
}
schedule_task(() => {
schedule_microtask(() => {
trigger_transitions(transitions, 'key', from);
});
}

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.29';
export const VERSION = '5.0.0-next.30';
export const PUBLIC_VERSION = '5';

@ -1,13 +1,11 @@
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
html: `<button>0 / 0</button>`,
async test({ assert, target, window }) {
async test({ assert, target }) {
const btn = target.querySelector('button');
const clickEvent = new window.Event('click', { bubbles: true });
await btn?.dispatchEvent(clickEvent);
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>1 / 1</button>`);
}
});

@ -2,6 +2,7 @@
import { setup } from './utils.js';
let { num } = $state(setup());
let { num: num_frozen } = $state(setup());
</script>
<button on:click={() => num++}>{num}</button>
<button on:click={() => { num++; num_frozen++; }}>{num} / {num_frozen}</button>

@ -0,0 +1,21 @@
import { test } from '../../test';
export default test({
html: `<button class="foo">0</button><button class="foo">0</button>`,
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
await btn1?.click();
assert.htmlEqual(
target.innerHTML,
`<button class="foo">1</button><button class="foo">1</button>`
);
await btn2?.click();
assert.htmlEqual(
target.innerHTML,
`<button class="foo">2</button><button class="foo">2</button>`
);
}
});

@ -0,0 +1,6 @@
<script>
let { value, ...props } = $props();
</script>
<button {...props} onclick={() => value++}>{value}</button>

@ -0,0 +1,12 @@
<script>
import Button from './button.svelte';
let value = $state(0);
const props = {
class: 'foo'
};
</script>
<Button {...props} bind:value />
<button {...props} onclick={() => value++}>{value}</button>

@ -0,0 +1,40 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>Add Item</button>`,
async test({ assert, target }) {
const [btn1] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>Add Item</button><button>Index 0\n|\nItem 0</button><button>Index 1\n|\nItem 1</button>`
);
let [btn2, btn3, btn4] = target.querySelectorAll('button');
flushSync(() => {
btn4.click();
btn3.click();
});
assert.htmlEqual(target.innerHTML, `<button>Add Item</button>`);
let [btn5] = target.querySelectorAll('button');
flushSync(() => {
btn5.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>Add Item</button><button>Index 0\n|\nItem 2</button>`
);
}
});

@ -0,0 +1,18 @@
<script>
let arr = $state([]);
let counter = 0
function addItem() {
arr.push(`${counter++}`)
}
function removeItem(i) {
arr.splice(i, 1)
}
</script>
<button onclick={addItem}>Add Item</button>
{#each arr as item, i}
<button onclick={() => removeItem(i)}>Index {i} | Item {item}</button>
{/each}

@ -33,7 +33,7 @@ export default test({
assert.htmlEqual(
target.innerHTML,
`<p>test costs $1</p><p>test 2 costs $2000</p><p>test costs $1</p><p>test 2 costs $2000</p><button>add</button><button>change</button><button>reload</button>`
`<p>test costs $1</p><p>test 2 costs $2</p><p>test costs $1</p><p>test 2 costs $2</p><button>add</button><button>change</button><button>reload</button>`
);
}
});

@ -0,0 +1,12 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
assert.htmlEqual(target.innerHTML, `0 0 <button>0 / 0</button>`);
const [btn] = target.querySelectorAll('button');
btn?.click();
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '0 1 <button>0 / 1</button>');
}
});

@ -0,0 +1,7 @@
<script>
import Sub from './sub.svelte'
let sub = $state();
</script>
<Sub bind:this={sub} />
<button on:click={() => sub.increment()}>{sub?.count1.value} / {sub?.count2.value}</button>

@ -0,0 +1,15 @@
<script>
export const count1 = $state.frozen({value: 0});
export const count2 = $state({value: 0});
export function increment() {
count2.value += 1;
}
</script>
{count1.value}
{count2.value}
<!-- so that count1/2 become sources -->
<svelte:options accessors />

@ -0,0 +1,40 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>show</button><button>animate</button>`,
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>show</button><button>animate</button><h1>Hello\n!</h1>`
);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(target.innerHTML, `<button>show</button><button>animate</button>`);
flushSync(() => {
btn2.click();
});
assert.htmlEqual(target.innerHTML, `<button>show</button><button>animate</button>`);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>show</button><button>animate</button><h1 style="opacity: 0;">Hello\n!</h1>`
);
}
});

@ -0,0 +1,16 @@
<script>
import { fade } from 'svelte/transition';
let show = $state(false);
let animate = $state(false);
function maybe(node, animate) {
if (animate) return fade(node);
}
</script>
<button onclick={() => show = !show}>show</button><button onclick={() => animate = !animate}>animate</button>
{#if show}
<h1 transition:maybe={animate}>Hello {name}!</h1>
{/if}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<p>hello world</p>`
});

@ -0,0 +1,11 @@
<script>
import { writable } from 'svelte/store';
let snippet = writable(hello);
</script>
{#snippet hello()}
<p>hello world</p>
{/snippet}
{@render $snippet()}

@ -0,0 +1,22 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<p>0 - 0</p><button>+</button`,
async test({ assert, target }) {
const [btn1] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
assert.htmlEqual(target.innerHTML, `<p>1 - 1</p><button>+</button`);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(target.innerHTML, `<p>2 - 2</p><button>+</button`);
}
});

@ -0,0 +1,12 @@
<script>
let x = $state({a: 0, b:0});
let count = 0;
</script>
<p>{x.a} - {x.b}</p>
<button onclick={() => {
const a = ++count;
x = {a, b: a};
}}>+</button>
Loading…
Cancel
Save