rob-balfre / svelte-select

Svelte Select. A select component for Svelte
https://svelte-select-examples.vercel.app
Other
1.25k stars 175 forks source link

Load options async on select open #573

Closed Cluster2a closed 1 year ago

Cluster2a commented 1 year ago

Hey,

I want to load the options via API only if the user opens the select field. loadOptions seems to be called on mount.

I tried with on:focus, but the UI got a bit messy.

Is there a recommended way to do this? It would be awesome if the loadOptions would be called if listOpen = true, to avoid over-fetching data. Or if there would be a loadOptionsOnOpen-Parameter.

I really appreciate any help you can provide.

Cluster2a commented 1 year ago

I was playing with the code and made some minor changes (there might be a better approach).

export let loadOptionsOnOpen = false;
function setupFilterText() {
    if ((!loadOptions || loadOptionsOnOpen) && filterText.length === 0) return;

    if (loadOptions) {
        debounce(async function () {
            loading = true;
            let res = await getItems({
                dispatch,
                loadOptions,
                convertStringItemsToObjects,
                filterText,
            });

            if (res) {
                loading = res.loading;
                listOpen = listOpen ? res.listOpen : filterText.length > 0 ? true : false;
                focused = listOpen && res.focused;
                items = groupBy ? filterGroupedItems(res.filteredItems) : res.filteredItems;
            } else {
                loading = false;
                focused = true;
                listOpen = true;
            }
        }, debounceWait);
    } else {
        listOpen = true;

        if (multiple) {
            activeValue = undefined;
        }
    }
}
async function handleClick() {
    if (disabled) return;

    if(loadOptionsOnOpen && !items.length) {
        loading = true;
        let res = await getItems({
            dispatch,
            loadOptions,
            convertStringItemsToObjects,
            filterText,
        });

        if (res) {
            loading = res.loading;
            items = groupBy ? filterGroupedItems(res.filteredItems) : res.filteredItems;
            listOpen = !listOpen;
        } else {
            loading = false;
        }
    } else {
        listOpen = !listOpen;
    }
}

The ability to load the data just on open makes sense since some fields might not be used that often. Fetching the data should not happen on rendering in any case.

Creating your own implementation with the events is dirty since you have to take care of the rendering empty-slot, the loading prop, and so on. There should be a more user-friendly way of doing this.

rob-balfre commented 1 year ago

You could bind:listOpen and use that as a flag in your loadOptions method.

Basic loadOptions example: https://svelte-select-examples.vercel.app/examples/props/loadOptions

Cluster2a commented 1 year ago

@rob-balfre, then I could check in the loadOptions if the listOpen === true and skip the API call, if it's not open. But if I open the select field, the loadOptions is not executed again, so the data will never be fetched. Or am I getting something wrong?

Cluster2a commented 1 year ago

@rob-balfre, I tried something like this:

<script lang="ts">
    import Select from 'svelte-select';
    import LL from '$i18n/i18n-svelte';
    import { ApiProblemKind } from '$lib/services/api/api-problem';
    import { ProjectApi } from '$lib/services/api/project-api';
    import type { ProjectSelectionListResult } from '$lib/services/api/types/project-api.types';
    import ProjectItem from '$lib/Components/shared/SvelteSelect/Projects/Item.svelte';
    import IconClear from '$lib/Components/shared/SvelteSelect/Icons/Clear.svelte';

    export let value = [];
    export let justValue = '';
    export let id: string;

    let selectOptions = [];
    let listOpen = false;
    let loadingItems = false;
    const floatingConfig = {
        strategy: 'fixed'
    };

    $: {
        if (listOpen && selectOptions.length === 0) {
            loadOptions();
        }
    }

    const loadOptions = async () => {
        loadingItems = true;
        const projectAPI = new ProjectApi();

        try {
            const result: ProjectSelectionListResult = await projectAPI.selections();
            if (result.kind === ApiProblemKind.ok) {
                selectOptions = result.data.projects.map((project) => {
                    return {
                        value: project.id,
                        label: project.name,
                        name: project.name,
                        color: project.color,
                        number: project.number,
                        status: project.status
                    };
                });
            }
        } catch (error) {
            console.error(error);
        } finally {
            loadingItems = false;
        }
    };
</script>

<Select
    bind:id
    items={selectOptions}
    bind:value
    bind:justValue
    multiple={true}
    placeholder="Select project"
    bind:listOpen
    {floatingConfig}
    loading={loadingItems}
    --list-position="fixed"
>
    <div slot="item" let:item>
        <ProjectItem {item} />
    </div>

    <div slot="selection" let:selection>
        <ProjectItem item={selection} />
    </div>

    <div slot="clear-icon">
        <IconClear />
    </div>

    <div slot="empty" class="empty">
        {$LL.components.booking.modal.editNewModal.tabs.mainData.projectNoOptions()}
    </div>
</Select>

This works as desired, but the result looks like this: image

Since the items are loaded after the select is opened, it seems like the width of the items is not adjusted to the width of the select. The second time I open the select, everything looks fine.

rob-balfre commented 1 year ago

Basic example of loadOptions and listOpen here:

https://svelte.dev/repl/6f92e1b8b646438683d02766f7c20049?version=3.57.0

Can't really help with big detailed examples like that, too much to debug for little old me.

Cluster2a commented 10 months ago

@rob-balfre, I made a fork of your repo with a pre selection: https://svelte.dev/repl/cf6044a8389d4a25b42062f9aa9a627c?version=3.57.0

Sometimes, we already have the data, so fetching it is unnecessary. Is there a way to delete the selected item? The only way to do so is to open the selection once - then the clear component is rendered.