From 4b2360dc8335ebcdbbb67e104a953871ba50d9fc Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 2 Nov 2024 23:28:25 -0400 Subject: [PATCH] fix(MultiSelect): Resolve infinite loop when using `mode="immediate"` and Svelte 5. Resovles #507 --- .changeset/hot-stingrays-move.md | 5 ++++ .../src/lib/components/MultiSelect.svelte | 23 +++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 .changeset/hot-stingrays-move.md diff --git a/.changeset/hot-stingrays-move.md b/.changeset/hot-stingrays-move.md new file mode 100644 index 000000000..404af3b4e --- /dev/null +++ b/.changeset/hot-stingrays-move.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +fix(MultiSelect): Resolve infinite loop when using `mode="immediate"` and Svelte 5 diff --git a/packages/svelte-ux/src/lib/components/MultiSelect.svelte b/packages/svelte-ux/src/lib/components/MultiSelect.svelte index 6f487669e..24ca3f1d2 100644 --- a/packages/svelte-ux/src/lib/components/MultiSelect.svelte +++ b/packages/svelte-ux/src/lib/components/MultiSelect.svelte @@ -3,7 +3,8 @@ import { type ComponentProps, createEventDispatcher } from 'svelte'; import { flip } from 'svelte/animate'; - import { get, partition } from 'lodash-es'; + import { get } from 'svelte/store'; + import { partition, isEqual } from 'lodash-es'; import { mdiMagnify } from '@mdi/js'; @@ -17,6 +18,7 @@ import selectionStore from '../stores/selectionStore.js'; import uniqueStore from '../stores/uniqueStore.js'; import { cls } from '../utils/styles.js'; + import changeStore from '../stores/changeStore.js'; export let options: MenuOption[]; export let value: TValue[] = []; @@ -82,15 +84,11 @@ onChange(); } - $: if (mode === 'immediate' && $selection) { - applyChange(); - } - export let searchText = ''; let applying = false; // Partition options based on if they initially selected, which will be displayed at the top - $: [selectedOptions, unselectedOptions] = partition(options, (x) => value.includes(x.value)); + $: [selectedOptions, unselectedOptions] = partition(options, (o) => value.includes(o.value)); // Filter by search text function applyFilter(option: MenuOption, searchText: string) { @@ -105,10 +103,15 @@ $: filteredSelectedOptions = selectedOptions.filter((x) => applyFilter(x, searchText)); $: filteredUnselectedOptions = unselectedOptions.filter((x) => applyFilter(x, searchText)); - $: selection = selectionStore({ - initial: selectedOptions.map((x) => x.value), - max, - }); + const selection = selectionStore({ max }); + // Only "subscribe" to value changes (not `$selection`) to fix correct `value` / topological ordering. Should be simplified with Svelte 5 + $: get(selection).setSelected(value); + + // Immediately apply only if changed + const changed = changeStore(selection); + $: if (mode === 'immediate' && !isEqual($selection.selected, $changed.previous?.selected)) { + applyChange(); + } $: isSelectionDirty = dirtyStore(selection); $: indeterminateStore = uniqueStore(indeterminateSelected);