Reputation: 8121
I have some serious problems with reactivity in Svelte. I have isolated what I think is at least one of my main issues. When bind:ing a variable to a checkbox, the reactivity seems to break when setting the variable inside a function, but not outside. Is this intended behavior? In that case why? And what is the intended work flow?
Example code, a Svelte component:
<script>
let foo = true;
// This assignment works both on the plain text view (Foo: true/false)
// and on the checkbox
// setInterval(() => foo = !foo, 500)
// This assignment works only on the plain text view (Foo: true/false)
// but not for the checkbox
function action() {
foo = !foo;
}
</script>
Foo: { foo }<br />
Checkbox: <input type="checkbox" bind:checked={ foo } on:click|preventDefault={ action } />
Svelte REPL to see this problem in action: https://svelte.dev/repl/73de7d705ab3442786710cd88ea3f625?version=3.12.1
Upvotes: 12
Views: 2824
Reputation: 36
This binding issue isn't with Svelte's reactivity, it's using preventDefault
on the click event. The HTML checked
attribute never changes, because the checked attribute doesn't fire any JS events to update the DOM.
In Svelte, you don't need to set foo
since it's already bound to the HTML state.
...
function action() {
console.log(foo)
}
</script>
Foo: { foo }<br />
Checkbox: <input type="checkbox" bind:checked={ foo } on:change={ action } />
But if you don't want HTML to change before something else, declaring action
as async works fine with on|click|preventDefault
:
async function action() {
let data = await new Promise((resolve) => setTimeout(resolve, 500))
foo = !foo
console.log("changed!")
return data
}
Upvotes: 1
Reputation: 179
The best solution is much simpler, because the two-way binding by itself does everything you need. So, this does it:
<script>
let foo = true;
</script>
Foo: { foo }<br />
Checkbox: <input type="checkbox" bind:checked={ foo } />
I know the question is two years old, but I think it needed better closure.
Upvotes: 3
Reputation: 1523
I would suggest not to cancel the event with the usage of preventDefault, because it would stop the default behaviour of the checkbox.
And therefore the reactivity
is broken
. If you wrap the reassignment of foo in a setTimeout you just tricking the event loop and the default is not prevented.
So if you like to stop the propagation of the event use preventDefault with the suggestion from Filip.
<script>
let foo = true;
// This assignment works both on the plain text view (Foo: true/false)
// and on the checkbox
// setInterval(() => foo = !foo, 500)
// This assignment works only on the plain text view (Foo: true/false)
// but not for the checkbox
async function action(e) {
e.preventDefault();
setTimeout(() => {foo =! foo});
}
</script>
Foo: { foo }<br />
Checkbox: <input type="checkbox" bind:checked={ foo } on:click={ action } />
Upvotes: 0
Reputation: 16411
Seems your problem is cause by the bind:
option. Having this will cause the foo to be updated twice: once for the 'action' and once behind the scenes when the binding kicks in.
Just removing the bind:
should work
<script>
let foo = true;
// This assignment works both on the plain text view (Foo: true/false)
// and on the checkbox
// setInterval(() => foo = !foo, 500)
// This assignment works only on the plain text view (Foo: true/false)
// but not for the checkbox
function action() {
setTimeout(() => foo = !foo, 1000);
}
</script>
Foo: { foo }<br />
Checkbox: <input type="checkbox" checked={ foo } on:click|preventDefault={ action } />
This make sense because you do not want to have two-way-binding for the checked state to foo. You only want the checkbox to reflect the value of foo.
Upvotes: 1
Reputation: 2522
If you wrap your variable declaration in a setTimeout
without any time limit (making it default to 0 and therefore run as soon as possible) your code will work.
function action() {
setTimeout(() => {
foo = !foo;
});
}
I wish I could explain why this works, but I can't...
Upvotes: 3