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

Implement new virtual API for the Combobox component #2779

Merged
merged 8 commits into from
Oct 2, 2023

Conversation

RobinMalfait
Copy link
Member

@RobinMalfait RobinMalfait commented Sep 29, 2023

This PR is an improvement on the previous PR where we introduced a new virtual prop to the Combobox component.

This new API requires you to pass in a list of options, and provide a template to the Combobox.Options such that we can only render the visible options (+ overscan).

React:

<Combobox
  virtual={{
    options: ["Foo", "Bar", "Baz"],
    disabled: (option) => option === "Bar",
  }}
>
  <Combobox.Input />
  <Combobox.Options>
    {({ option }) => <Combobox.Option value={option}>{option}</Combobox.Option>}
  </Combobox.Options>
</Combobox>

Vue:

<template>
  <Combobox :virtual="virtual">
    <ComboboxInput />
    <ComboboxOptions v-slot="{ option }">
      <ComboboxOption :value="option">{{ option }}</ComboboxOption>
    </ComboboxOptions>
  </Combobox>
</template>

<script setup>
import { ref } from "vue";
let virtual = ref({
  options: ["Foo", "Bar", "Baz"],
  disabled: (option) => option === "Bar",
});
</script>

The disabled function in the virtual object is optional, but this is used to determine whether an option is disabled or not. In a non-virtual Combobox, we can check the disabled prop on the Combobox.Option, but since we are not rendering all of those, we don't have access to that information anymore.

The API we exposed in #2740 is now deprecated and won't be supported in future version.

Further fixes: #2441

Playground:


Some background:

  • Why things are slow:

    Our default Combobox component requires you to render (and mount) all the available options so that we know which options are available and what the next and previous options are based on the current active option to ensure that using arrow keys work as expected.

  • Initial approach:

    To ensure that we always go to the correct next / previous option, we have to make sure that everything is sorted. React doesn't expose an "index" or other information about where a certain element is within the React tree (at least not as a public API). Instead, we sorted the options by the DOM node position. When you have a lot of items then this is very slow.

    A first approach, is that we introduced and order prop on the
    Combobox.Option to sort based on this instead of the DOM node, which is much
    much faster.

    React:

      <Combobox>
        <Combobox.Input />
        <Combobox.Options>
          {countries.map((country, idx) => (
    -       <Combobox.Option key={country} value={country}>
    +       <Combobox.Option key={country} value={country} order={idx}>
              {country}
            </Combobox.Option>
          ))}
        </Combobox.Options>
      </Combobox>

    Vue:

      <Combobox>
        <ComboboxInput />
        <ComboboxOptions
          <ComboboxOption
    -       v-for="country in countries"
    +       v-for="(country, idx) in countries"
    +       :order="idx"
            :key="country"
            :value="country"
          >
            {country}
          </ComboboxOption>
        </ComboboxOptions>
      </Combobox>
  • Next approach:

    This was still too slow, especially when you have a lot of items. Instead, to keep the API clean, there is another approach we can use, which is only mount the items that are necessary, but still have them in the React/Vue tree.

    This was a huge improvement for React, but it was still a bit too slow for Vue. This approach also required us to still register each individial option in memory. This was especially noticeable when you open the Combobox component.

    From an API perspective, this looks very clean, and it's easy to change a non-virtual Combobox to a virtual one.

    React:

    - <Combobox>
    + <Combobox virtual>
        <Combobox.Input />
        <Combobox.Options>
          {countries.map((country, idx) => (
            <Combobox.Option key={country} value={country} order={idx}>
              {country}
            </Combobox.Option>
          ))}
        </Combobox.Options>
      </Combobox>

    Vue:

    - <Combobox>
    + <Combobox virtual>
        <ComboboxInput />
        <ComboboxOptions>
          <ComboboxOption
            v-for="(country, idx) in countries"
            :order="idx"
            :key="country"
            :value="country"
          >
            {country}
          </ComboboxOption>
        </ComboboxOptions>
      </Combobox>
  • This PR:

    Next, the goal is to get rid of all the pre-registering of options. Instead, let's pass in all the available options. Then, we will make sure that only the visible options are being rendered. To do this, you have to provide a "template":

    React:

    <Combobox
      virtual={{
        options: ["Foo", "Bar", "Baz"],
        disabled: (option) => option === "Bar",
      }}
    >
      <Combobox.Input />
      <Combobox.Options>
        {({ option }) => (
          <Combobox.Option value={option}>{option}</Combobox.Option>
        )}
      </Combobox.Options>
    </Combobox>

    Vue:

    <template>
      <Combobox :virtual="virtual">
        <ComboboxInput />
        <ComboboxOptions v-slot="{ option }">
          <ComboboxOption :value="option">{{ option }}</ComboboxOption>
        </ComboboxOptions>
      </Combobox>
    </template>
    
    <script setup>
    import { ref } from "vue";
    let virtual = ref({
      options: ["Foo", "Bar", "Baz"],
      disabled: (option) => option === "Bar",
    });
    </script>

@vercel
Copy link

vercel bot commented Sep 29, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
headlessui-react ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 29, 2023 2:36pm
headlessui-vue ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 29, 2023 2:36pm

@davidmatter
Copy link

@RobinMalfait this one doesn't play nicely with allowing custom values https://headlessui.com/vue/combobox#allowing-custom-values
Does the virtual list just re-use the default slot of ComboboxOptions for rendering the virtual options? Where would I place the "Create $X" option?

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

Successfully merging this pull request may close these issues.

Combobox option list scrolling problem starting from v1.7.5
2 participants