fix: invalidate store when mutated inside each block (#10785)

* fix: invalidate store when mutated inside each block

fixes #10771

* Update packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
pull/10793/head
Simon H 1 year ago committed by GitHub
parent bd687c6504
commit fbbd89a917
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: invalidate store when mutated inside each block

@ -2344,8 +2344,20 @@ export const template_visitors = {
each_type |= EACH_IS_STRICT_EQUALS; each_type |= EACH_IS_STRICT_EQUALS;
} }
// Find the parent each blocks which contain the arrays to invalidate // If the array is a store expression, we need to invalidate it when the array is changed.
// TODO decide how much of this we want to keep for runes mode. For now we're bailing out below // This doesn't catch all cases, but all the ones that Svelte 4 catches, too.
let store_to_invalidate = '';
if (node.expression.type === 'Identifier' || node.expression.type === 'MemberExpression') {
const id = object(node.expression);
if (id) {
const binding = context.state.scope.get(id.name);
if (binding?.kind === 'store_sub') {
store_to_invalidate = id.name;
}
}
}
// Legacy mode: find the parent each blocks which contain the arrays to invalidate
const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => { const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => {
const array = /** @type {import('estree').Expression} */ (context.visit(block.expression)); const array = /** @type {import('estree').Expression} */ (context.visit(block.expression));
const transitive_dependencies = serialize_transitive_dependencies( const transitive_dependencies = serialize_transitive_dependencies(
@ -2382,15 +2394,24 @@ export const template_visitors = {
'$.invalidate_inner_signals', '$.invalidate_inner_signals',
b.thunk(b.sequence(indirect_dependencies)) b.thunk(b.sequence(indirect_dependencies))
); );
const invalidate_store = store_to_invalidate
? b.call('$.invalidate_store', b.id('$$subscriptions'), b.literal(store_to_invalidate))
: undefined;
const sequence = [];
if (!context.state.analysis.runes) sequence.push(invalidate);
if (invalidate_store) sequence.push(invalidate_store);
if (left === assignment.left) { if (left === assignment.left) {
const assign = b.assignment('=', expression_for_id, value); const assign = b.assignment('=', expression_for_id, value);
return context.state.analysis.runes ? assign : b.sequence([assign, invalidate]); sequence.unshift(assign);
return b.sequence(sequence);
} else { } else {
const original_left = /** @type {import('estree').MemberExpression} */ (assignment.left); const original_left = /** @type {import('estree').MemberExpression} */ (assignment.left);
const left = context.visit(original_left); const left = context.visit(original_left);
const assign = b.assignment(assignment.operator, left, value); const assign = b.assignment(assignment.operator, left, value);
return context.state.analysis.runes ? assign : b.sequence([assign, invalidate]); sequence.unshift(assign);
return b.sequence(sequence);
} }
}; };
}; };

@ -89,6 +89,17 @@ export function store_set(store, value) {
return value; return value;
} }
/**
* @param {import('#client').StoreReferencesContainer} stores
* @param {string} store_name
*/
export function invalidate_store(stores, store_name) {
const store = stores[store_name];
if (store.store) {
store_set(store.store, store.value.v);
}
}
/** /**
* Unsubscribes from all auto-subscribed stores on destroy * Unsubscribes from all auto-subscribed stores on destroy
* @param {import('#client').StoreReferencesContainer} stores * @param {import('#client').StoreReferencesContainer} stores

@ -0,0 +1,29 @@
import { ok, test } from '../../test';
export default test({
skip_if_ssr: 'permanent',
html: `
<input type="checkbox">
<input type="checkbox">
<input type="checkbox">
0
`,
async test({ assert, target, window }) {
const input = target.querySelector('input');
ok(input);
input.checked = true;
await input.dispatchEvent(new window.Event('change', { bubbles: true }));
assert.htmlEqual(
target.innerHTML,
`
<input type="checkbox">
<input type="checkbox">
<input type="checkbox">
1
`
);
}
});

@ -0,0 +1,12 @@
<script>
import { derived, writable } from "svelte/store";
const checks = writable([false, false, false])
const countChecked = derived(checks, ($checks) => $checks.filter(Boolean).length)
</script>
{#each $checks as checked}
<input type="checkbox" bind:checked />
{/each}
{$countChecked}
Loading…
Cancel
Save