Vuetify: Sorting data for a v-select component and adding a horizontal line
Tags: vue, typescript
Published: 2024-05-27 08:26:48

I recently needed to implement a select component in [Vuetify](https://vuetifyjs.com/en/) for a list of countries, which has a couple of countries at the top at the list, divided with a horizontal line. It took me a while to figure out how to do this, so I figured I’d share in case someone else needs to do the same.

## Sorting the data

In my example I used an interface for the Countries that allows for multiple translations to be saved as well:

```typescript
export interface Country {
  id: string;
  translations: {
      [key: string]: string;
  };
}
```

Before rendering the data in the `<v-select>` component, the array of countries needed some sorting. To manage the highlighted countries, you can either fetch this data from a backend,or — so simplicity’s sake — use a `const`:

```typescript
export const preferredCountryOrder = ['AT', 'CZ', 'DE', 'HU', 'SI'];
```

I then used a basic sorting algorithm that compares two countries based on their id property.

More specifically, the algorithm checks

- if both countries are part of the `preferredCountryOrder` array, and if so, sorts it relative to each other based on their id;
- If only one of the countries are part of the `preferredCountryOrder` array, it should be prioritized;
- And, finally, if none of the two compared countries are in the `preferredCountryOrderarray`, the original order should be kept.

This logic translates to this Typescript code:

```typescript
function sortCountries(a: Country, b: Country): number {
    const indexA = preferredCountryOrder.indexOf(a.id);
    const indexB = preferredCountryOrder.indexOf(b.id);

    // If both countries are in the preferred order,
    // sort them by their index in the preferred order
    if (indexA !== -1 && indexB !== -1) {
        return indexA - indexB;
    }

    // If only one of the countries is in the preferred order,
    // prioritize it
    if (indexA !== -1) {
        return -1;
    }
    if (indexB !== -1) {
        return 1;
    }

    // If neither country is in the preferred order,
    // maintain their original order
    return 0;
}
```

Finally, we can apply the sorting algorithm to our data. In my case, I use [useQuery](https://tanstack.com/query/latest/docs/framework/vue/overview) to fetch data from the backend, but it doesn’t really matter where your data is coming from, it could also come from a static array.

```typescript
const { data: countries, isLoading: countriesAreLoading } = useQuery({
    queryKey: [Resources.Countries],
    queryFn: () => fetchAll<Country>(URL),
    select: (data) => data.sort(sortCountries),
});
```

## Adding the sorted array to the select component

Now that our data is sorted, we can add it to our `<v-select>` component. This is relatively straightforward, as we can just pass it as items prop:

```vue
<template>
  <v-select
    :items="countries ?? []"
  >
  </v-select>
</template>
```

However, the sorted countries on top should be separated with a horizontal line. Luckily, Vuetify lets us directly use the slots so we can modify the items in the v-select component.

To do so, we use `<template #item="{ props, index}">` inside the `<v-select>` component. It allows us to directly target the list item and modify it. We get the props and the index, which hold the data of the list item and the index of the list item.

We can render the `<v-select>` component and bind the `<v-list-item>` to the props:

```vue
<template>
  <v-select
    :items="countries ?? []"
  >
    <template #item="{ props, index }">
      <v-list-item v-bind="props" />
    </template>
  </v-select>
</template>
```

This basically doesn’t change anything to how the `<v-select>` would’ve been rendered by Vuetify in the first place, but it allows us to change the list item.

We then want to check if the rendered list item is still part of the `preferredCountryOrder` array, and if it’s not, we can render a horizontal line. The easiest way to do this is to check if the index of the rendered list item equals the length of the the `preferredCountryOrder` array. It that’s the case, the rendered list item is the first item that’s not highlighted (the index starts at 0).

In code, it would look like this:

```vue
<v-divider v-if="index === preferredCountryOrder.length" />
```

Adding it all together:

```vue
<template>
  <v-select
    :items="countries ?? []"
  >
    <template #item="{ props, index }">
      <v-divider v-if="index === preferredCountryOrder.length" />
      <v-list-item v-bind="props" />
    </template>
  </v-select>
</template>
```

## Conclusion

slots in vuetify can be very powerful, but aren’t always fully documented. In this case, we overwrote the list items of a `<v-select>` and added some custom logic to it. By using `v-bind="props"` on the <v-list-item>, we ensure all the necessary data is passed directly to the component, as we don’t need to modify the data itself.

I hope this is useful and saves you some time. Happy coding!