sveltesveltekitsvelte-componentshadcnuisvelte-store

Access svelte writable located in object


In shadcn ui port for svelte 5 - Select component (using bitsui select) bind:value asks for state(string), but Writable<string> works too as well, so bind:value={$theme or $language} is properly getting and setting value. But my settings component works with additional setting passed to it as props with store as one of elements of object and i have trouble getting writable properly work from object.

<script lang="ts" module>
    interface SelectItem {
        label: string;
        labelRu: string;
        value: string;
    }

    export interface Setting {
        store: Writable<string>;
        name: string;
        nameRu: string;
        values: SelectItem[];
    }
</script>

<script lang="ts">
    import * as Select from '$shared/components/ui/select/index.js';

    import { theme } from '$shared/stores/theme';
    import { language } from '$shared/stores/language';
    import { derived, type Writable } from 'svelte/store';

    let { additionalSettings = null }: { additionalSettings?: { [key: string]: Setting } | null } =
        $props();
    let clazz = $state('');
    export { clazz as class };

    let settings: { [key: string]: Setting } = {
        theme: {
            store: theme,
            name: 'Theme',
            nameRu: 'Тема',
            values: [
                { label: 'Light', labelRu: 'Светлая', value: 'light' },
                { label: 'Dark', labelRu: 'Темная', value: 'dark' }
            ]
        },
        language: {
            store: language,
            name: 'Lingo',
            nameRu: 'Язык',
            values: [
                { label: 'English', labelRu: 'Английский', value: 'en' },
                { label: 'Russian', labelRu: 'Русский', value: 'ru' }
            ]
        }
    };

    settings = { ...settings, ...(additionalSettings ?? {}) };

    const settingLabel = Object.fromEntries(
        Object.entries(settings).map(([key, setting]) => [
            key,
            derived([setting.store, language], ([$store, $language]) => {
                const selectedValue = setting.values.find((value) => value.value === $store);
                return $language === 'ru' ? selectedValue?.labelRu : selectedValue?.label;
            })
        ])
    );
</script>

<div
    class="{clazz !== '' ? clazz + ' ' : ''}flex min-w-[240px] flex-col space-y-4 px-2 py-4 text-sm"
>
    {#each Object.entries(settings) as [key, setting]}
        <div class="flex items-center space-x-2">
            <p class="min-w-[48px]">{$language === 'ru' ? setting.nameRu : setting.name}:</p>
            <Select.Root type="single" name={key} bind:value={$setting.store}>
                <Select.Trigger>
                    {#await settingLabel[key] then label}
                        {(label ?? $language === 'ru')
                            ? 'Выберите ' + setting.nameRu
                            : 'Select ' + setting.name}
                    {/await}
                </Select.Trigger>
                <Select.Content>
                    {#each setting.values as value}
                        <Select.Item value={value.value}>
                            {$language === 'ru' ? value.labelRu : value.label}
                        </Select.Item>
                    {/each}
                </Select.Content>
            </Select.Root>
        </div>
    {/each}
</div>

I'm newby to Svelte 5 reactive functionality, so maybe i'm asking something easy fixing. Sorry for this(


Solution

  • Stores can only be used with $-syntax if they are declared at the root of the component which is not the case here since the store variable comes from an #each block.

    You can extract the contents of the #each to a new component, pass the store to said component then it will satisfy the necessary criteria there.

    But generally I would not recommend using stores at all in Svelte 5 unless it's a third party dependency, though even then you can use the fromStore helper function to turn the store into a state object. This should be possible here as well.

    A simplified example:

    <script>
        import { writable, derived, fromStore } from 'svelte/store';
    
        const value = writable(2);
        const double = derived(value, v => v * 2);
    
        const settings = [
            { name: 'Original', store: value },
            { name: 'Doubled', store: double },
        ];
    </script>
    
    {#each settings as { name, store }}
        {@const state = fromStore(store)}
        <label>
            {name}
            {#if 'set' in store}
                <input type=number bind:value={state.current} />
            {:else}
                {state.current}
            {/if}
        </label> <br>
    {/each}
    
    Stores: {$value} * 2 = {$double}
    

    Playground