Skip to content

Commit

Permalink
feat: add getOptionValue prop
Browse files Browse the repository at this point in the history
  • Loading branch information
TotomInc committed Jan 30, 2025
1 parent a6fe1ea commit ccaefc2
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 59 deletions.
15 changes: 8 additions & 7 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ export default defineConfig({
{
text: "Demo links",
items: [
{ text: "Single Select", link: "/demo/single-select" },
{ text: "Single select", link: "/demo/single-select" },
{ text: "Multiple Select", link: "/demo/multiple-select" },
{ text: "Custom Option Slot", link: "/demo/custom-option-slot" },
{ text: "Custom Tag Slot", link: "/demo/custom-tag-slot" },
{ text: "Pre-Selected Values", link: "/demo/pre-selected-values" },
{ text: "Disabled Options", link: "/demo/disabled-options" },
{ text: "With Menu Header", link: "/demo/with-menu-header" },
{ text: "With Complex Menu Filter", link: "/demo/with-complex-menu-filter.md" },
{ text: "Custom option slot", link: "/demo/custom-option-slot" },
{ text: "Custom tag slot", link: "/demo/custom-tag-slot" },
{ text: "Custom value/label properties", link: "/demo/custom-option-label-value" },
{ text: "Pre-selected values", link: "/demo/pre-selected-values" },
{ text: "Disabled options", link: "/demo/disabled-options" },
{ text: "With menu-header", link: "/demo/with-menu-header" },
{ text: "With complex menu filter", link: "/demo/with-complex-menu-filter.md" },
],
},
],
Expand Down
58 changes: 58 additions & 0 deletions docs/demo/custom-option-label-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: 'Custom Option Label and Value properties'
---

# Custom Option Label and Value properties

::: warning
This isn't a common use-case. You should use the `label` and `value` properties of the option object when possible.
Doing this will break the type-safety of the component.
Read more about [`getOptionLabel` and `getOptionValue` props](../props.md).
:::

In the rare case you need to use different properties for the `label` and `value` of an option, you can use the `getOptionLabel` and `getOptionValue` props.

If you're using TypeScript, be sure to read the [type-safety guide for these props](../typescript.md#using-custom-label-value-with-options) section.

<script setup>
import { ref } from "vue";

import VueSelect from "../../src";

const selected = ref("");
</script>

<VueSelect
v-model="selected"
:get-option-label="option => option.id"
:get-option-value="option => option.key"
:options="[
{ id: 'France', key: 'fr' },
{ id: 'USA', key: 'us' },
{ id: 'Germany', key: 'de' },
]"
/>

## Demo source-code

```vue
<script setup lang="ts">
import { ref } from "vue";
import VueSelect from "vue3-select-component";
const selected = ref("");
</script>
<template>
<VueSelect
v-model="selected"
:get-option-label="option => option.id"
:get-option-value="option => option.key"
:options="[
{ key: 'fr', id: 'France' },
{ key: 'us', id: 'USA' },
{ key: 'de', id: 'Germany' },
]"
/>
</template>
```
20 changes: 13 additions & 7 deletions docs/props.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ Aria attributes to be passed to the select control to improve accessibility.

Callback function to determine if the current option should match the search query. This function is called for each option and should return a boolean.

The `label` is provided as a convenience, using `getOptionLabel` or `getMultiValueLabel` depending on the `isMulti` prop.
The `label` is provided as a convenience, processed from `getOptionLabel` prop.

::: info
By default, the following callback function is used `(option, label, search) => label.toLowerCase().includes(search.toLowerCase())`
Expand All @@ -202,11 +202,13 @@ By default, the following callback function is used `(option, label, search) =>
(option) => option.label;
```

A function to get the label of an option. This is useful when you want to use a property different from `label` as the label of the option.
Resolves option data to a string to render the option label.

This function is used to display the options in the dropdown, and to display the selected option (**single-value**) in the select.
This function can be used if you don't want to use the standard `option.label` as the label of the option.

## getMultiValueLabel
The label of an option is displayed in the dropdown and as the selected option (**single-value**) in the select.

## getOptionValue

**Type**:

Expand All @@ -217,9 +219,13 @@ This function is used to display the options in the dropdown, and to display the
**Default**:

```ts
(option) => option.label;
(option) => option.value;
```

A function to get the label of an option. This is useful when you want to use a property different from `label` as the label of the option.
Resolves option data to a string to compare options and specify value attributes.

This function can be used if you don't want to use the standard `option.value` as the value of the option.

This function is used to display the selected options (**multi-value**) in the select.
::: warning
If you are using TypeScript, TODO.
:::
70 changes: 57 additions & 13 deletions docs/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ title: 'TypeScript'

In order to provide flexibility with TypeScript, Vue 3 Select Component has been written in TypeScript. This means that you can take advantage of TypeScript's type checking and autocompletion features.

## About generics in TypeScript & Vue
## Generics with Vue & TypeScript

Vue 3 Select Component uses a feature that has been released on Vue 3.3 called [**Generics**](https://vuejs.org/api/sfc-script-setup.html#generics).

Generics allow you to define a type that can be used in multiple places with different types. This is useful when you want to create a component that can be used with different types of data.

A common type you'll see is the `Option` type, which is used to define the options of the select component.
A common type taking use of the Vue Generic is the `Option` type, which is used to define the `:options` prop of the select component:

```ts
type Option<T> = {
Expand All @@ -22,18 +22,21 @@ type Option<T> = {
};
```

## Custom option value
## Customizing `option.value` type

::: info
Ensure you are familiar with the [`:options` prop](/props#options) before reading this section.
:::

By default, the `value` property of the option object is a `string`. However, it is possible to use a custom type, such as a `number` or a complex object.
By default, the `value` property of the option object is a `string`. However, it is possible to use a different type, such as a `number`.

To do this, import the `Option` type from the component and define a custom type that extends the `Option` type with a generic type:

```vue
<script setup lang="ts">
import type { Option } from "vue3-select-component";
import { ref } from "vue";
import VueSelect, { type Option } from "vue3-select-component";
import VueSelect from "vue3-select-component";
// Define a custom type for the option value.
// It takes a generic type that defines the type of the `value` property.
Expand All @@ -60,19 +63,19 @@ const userOptions: UserOption[] = [
</template>
```

## Custom option properties

It is possible to **add properties** to the options passed inside the `:options` prop, while still being type-safe.
## Adding properties to `option`

Let's say you want to add a `username` property to the option object.
It is possible to **add properties** to the options, while still being type-safe across the `<slot />` and various props.

This `username` property will be available on **all available props and slots** that receive the `option` object.
New option properties will be available on **all available props and slots** that receive the `option` object.

```vue
<script setup lang="ts">
import type { Option } from "vue3-select-component";
import { ref } from "vue";
import VueSelect, { type Option } from "vue3-select-component";
import VueSelect from "vue3-select-component";
// Define a custom type for the option value with an additional `username` property.
type UserOption = Option<number> & { username: string };
const selectedUser = ref<number>();
Expand Down Expand Up @@ -101,16 +104,17 @@ const userOptions: UserOption[] = [
</template>
```

## Type-safe relationship between `option.value` & `v-model`
## Type-safety between `option.value` & `v-model`

Vue 3 Select Component creates a type-safe relationship between the `option.value` and the `v-model` prop.

This means that if you have a custom type for the `value` property of the option object, the `v-model` prop will also be type-safe.

```vue
<script setup lang="ts">
import type { Option } from "vue3-select-component";
import { ref } from "vue";
import VueSelect, { type Option } from "vue3-select-component";
import VueSelect from "vue3-select-component";
type UserOption = Option<number>;
Expand All @@ -137,3 +141,43 @@ const userOptions: UserOption[] = [
/>
</template>
```

## Using custom label/value with options

::: warning
`getOptionValue` and `getOptionLabel` props are not compatible with the type-safety of the component. Therefore, you should use them with caution and only as a last resort.
:::

If you're using the `getOptionValue` or `getOptionLabel` props, there are a few gotchas to be aware of with the types:

- Local array of options cannot be typed as `Option<T>[]`
- When passing the array of options to the component, you need to cast it to `unknown` then `Option<T>[]`.

Here's an example usage of the `getOptionValue` and `getOptionLabel` props with TypeScript:

```vue
<script setup lang="ts">
import type { Option } from "vue3-select-component";
const activeRole = ref<string>("");
// You cannot type the `roleOptions` as `Option<string>[]`.
const roleOptions = [
{ id: "Admin", key: "admin" },
{ id: "User", key: "user" },
{ id: "Guest", key: "guest" },
];
</script>
<template>
<!-- Casting of the `roleOptions` must be done at the `:options` prop-level. -->
<VueSelect
v-model="activeRole"
:options="(roleOptions as unknown as Option<string>[])"
:is-multi="false"
:get-option-label="option => (option.id as string)"
:get-option-value="option => (option.key as string)"
placeholder="Pick a role"
/>
</template>
```
20 changes: 20 additions & 0 deletions playground/Playground.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type UserOption = Option<number> & { username: string };
const activeBook = ref<string | null>(null);
const activeUsers = ref<number[]>([1, 3]);
const activeRole = ref<string | null>(null);
const isLoading = ref(false);
const bookOptions: BookOption[] = [
Expand All @@ -27,6 +28,12 @@ const userOptions: UserOption[] = [
{ label: "Admin", value: 6, username: "admin" },
{ label: "Root", value: 6, username: "root" },
];
const roleOptions = [
{ id: "Admin", key: "admin" },
{ id: "User", key: "user" },
{ id: "Guest", key: "guest" },
];
</script>

<template>
Expand Down Expand Up @@ -55,6 +62,19 @@ const userOptions: UserOption[] = [
<p class="selected-value">
Selected user value: {{ activeUsers || "none" }}
</p>

<VueSelect
v-model="activeRole"
:options="(roleOptions as unknown as Option<string>[])"
:is-multi="false"
:get-option-label="option => (option.id as string)"
:get-option-value="option => (option.key as string)"
placeholder="Pick a role"
/>

<p class="selected-value">
Selected role value: {{ activeRole || "none" }}
</p>
</form>
</div>
</template>
Expand Down
48 changes: 47 additions & 1 deletion src/Select.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Option } from "./types";
import { mount } from "@vue/test-utils";
import { describe, expect, it } from "vitest";

import { describe, expect, it } from "vitest";
import VueSelect from "./Select.vue";

const options = [
Expand Down Expand Up @@ -407,6 +408,51 @@ describe("search emit", () => {
});

describe("component props", () => {
it("should use getOptionValue prop to get custom option value", async () => {
const options = [
{ id: "Admin", key: "admin" },
{ id: "User", key: "user" },
];

const wrapper = mount(VueSelect, {
props: {
modelValue: null,
options: options as unknown as Option<string>[],
getOptionValue: (option: any) => option.key,
},
});

await openMenu(wrapper);
await wrapper.get("div[role='option']").trigger("click");

expect(wrapper.emitted("update:modelValue")).toStrictEqual([["admin"]]);
});

it("should use getOptionLabel prop to display custom option label", async () => {
const options = [
{ id: "Admin", key: "admin" },
{ id: "User", key: "user" },
];

const wrapper = mount(VueSelect, {
props: {
modelValue: null,
options: options as unknown as Option<string>[],
getOptionLabel: (option: any) => option.id,
},
});

await openMenu(wrapper);

const optionElements = wrapper.findAll("div[role='option']");
expect(optionElements[0].text()).toBe("Admin");
expect(optionElements[1].text()).toBe("User");

await wrapper.get("div[role='option']").trigger("click");

expect(wrapper.get(".single-value").text()).toBe("Admin");
});

it("should display the placeholder in the input when no option is selected", () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options, placeholder: "Pick an option" } });

Expand Down
Loading

0 comments on commit ccaefc2

Please sign in to comment.