Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: How to provide dynamic data to combobox? #52

Open
coryvirok opened this issue Apr 4, 2023 · 5 comments
Open

Question: How to provide dynamic data to combobox? #52

coryvirok opened this issue Apr 4, 2023 · 5 comments

Comments

@coryvirok
Copy link

Hello and thanks for taking on HeadlessUI for Svelte! I'm just starting to play around with it and am having some trouble understanding the right way to provide dynamic data to combobox results.

I'd like to use the combobox as an autocomplete for a dynamic form. As the user types, I'll send out an API call to find all of the matching entries and fill in the results in the dropdown as they stream in.

I've tried updating the combobox state directly, e.g. combobox.set({items: itemsFromServer}) whenever the API call finishes. However, I'm not able to use the combobox's filter without ending up in an infinite loop.

What's the right way to go about this?

Thanks!

e.q.

<script lang="ts">
// Autocomplete.svelte

export let filter = ''
export let items: {id: string; name: string; value: any}[] = []
const combobox = createCombobox({filter})

let prevFilter = ''
combobox.subscribe((state) => {
  if (state.filter !== prevFilter) {
    filter = state.filter
  }
})

$: updateCombobox(items)

function updateCombobox(items) {
  combobox.set({ items })
}
</script>

<input use:combobox.input value={$combobox.selected?.name ?? ''} />
<button use:combobox.button type="button" />

<ul use:combobox.items>
  {#each items as value}
    <li use:combobox.item={{ value }} />
  {/each}
</ul>
<script lang="ts">
// +page.svelte

import Autocomplete from './Autocomplete.svelte'

let searchTerm = ''
let items = writable([])

$: updateSearchResults(searchTerm)

function updateSearchResults(searchTerm) {
  makeApiCall(searchTerm).then(results => {
    items.set(results)
  })
}

</script>

<Autocomplete {items} bind:filter={searchTerm} />
@CaptainCodeman
Copy link
Owner

Great question! Off the top of my head I can't think of anywhere I've used dynamic data like this ... well, I have data loaded, but it's effectively static to the control. You're really talking about using it for a possibly unbounded set of data (or more than you'd want to load and have in memory upfront).

I'll have a think about the best way to do it. I'd imagine having some function you can pass to the control factory to fetch the data is what's needed.

@coryvirok
Copy link
Author

That seems to be the approach used in simple-svelte-autocomplete which seems to work fairly well. Although it's overloaded with more functionality than I need.

Also, I wouldn't include "unbounded sets" of data in the requirement since pulling in the top 10-20 items is all I'm interested in.

@CaptainCodeman
Copy link
Owner

Yeah, I meant a subset from a potentially larger set (the unbounded).

I'd probably include some form of virtualization for the list too, as you may have many more entries in a result than you want to load / render unless and until someone scrolls.

@wesharper
Copy link

wesharper commented Aug 2, 2023

The solution I'm using basically just hijacks the input change option in a search input and feeds the subsequent results in to the combobox, while providing my own logic for the isOpen state.

<script lang="ts">
  const combobox = createCombobox();
  let searchPromise: Promise<void>;
  let query: string;
  let searchResults: any[];

  async function search() {
    try {
      const result = await fetch(
        `${import.meta.env.VITE_API_BASE_URL}/api/v1/search?query=${query}`
      );
      searchResults = await result.json();
    }
  }
</script>

<input
  type="search"
  name="search"
  use:combobox.input
  on:input={() => {
    searchPromise = search();
  }}
  on:select={onSelect}
  on:keydown={scrollToActiveItem}
  bind:value={query}
/>
{#if Boolean(query)}
  <!-- optionally await searchPromise -->
  <ul
    use:combobox.items
  >
    {#each searchResults as result}
      {@const active = $combobox.active === result}
      <li
        <!-- optional custom active classes -->
        use:combobox.item={{ value: result }}
      >
        {result.toString()}
      </li>
    {/each}
  </ul>
{/if}

@CaptainCodeman
Copy link
Owner

Try this:

  1. Create derived store for the filter (this is so it can react when the filter part changes, not when any other state changes):
const combobox = createCombobox()
const filter = derived(combobox, $combobox => $combobox.filter)
  1. Use that filter store to trigger the data fetch:
let filtered: any[] = []

$: fetchData($filter)

async function fetchData(filter: string) {
  if (browser) {
    const q = filter.toLowerCase().replace(/\s+/g, '')
    const resp = await fetch('/api/data?q=' + q)
    const data = await resp.json()
    filtered = data
  }
}

3: Use the filtered array to render the combobox items:

{#each filtered as value}
...
{/each}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants