Skip to content

Commit ccaefc2

Browse files
committed
feat: add getOptionValue prop
1 parent a6fe1ea commit ccaefc2

File tree

8 files changed

+235
-59
lines changed

8 files changed

+235
-59
lines changed

docs/.vitepress/config.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@ export default defineConfig({
4444
{
4545
text: "Demo links",
4646
items: [
47-
{ text: "Single Select", link: "/demo/single-select" },
47+
{ text: "Single select", link: "/demo/single-select" },
4848
{ text: "Multiple Select", link: "/demo/multiple-select" },
49-
{ text: "Custom Option Slot", link: "/demo/custom-option-slot" },
50-
{ text: "Custom Tag Slot", link: "/demo/custom-tag-slot" },
51-
{ text: "Pre-Selected Values", link: "/demo/pre-selected-values" },
52-
{ text: "Disabled Options", link: "/demo/disabled-options" },
53-
{ text: "With Menu Header", link: "/demo/with-menu-header" },
54-
{ text: "With Complex Menu Filter", link: "/demo/with-complex-menu-filter.md" },
49+
{ text: "Custom option slot", link: "/demo/custom-option-slot" },
50+
{ text: "Custom tag slot", link: "/demo/custom-tag-slot" },
51+
{ text: "Custom value/label properties", link: "/demo/custom-option-label-value" },
52+
{ text: "Pre-selected values", link: "/demo/pre-selected-values" },
53+
{ text: "Disabled options", link: "/demo/disabled-options" },
54+
{ text: "With menu-header", link: "/demo/with-menu-header" },
55+
{ text: "With complex menu filter", link: "/demo/with-complex-menu-filter.md" },
5556
],
5657
},
5758
],
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
title: 'Custom Option Label and Value properties'
3+
---
4+
5+
# Custom Option Label and Value properties
6+
7+
::: warning
8+
This isn't a common use-case. You should use the `label` and `value` properties of the option object when possible.
9+
Doing this will break the type-safety of the component.
10+
Read more about [`getOptionLabel` and `getOptionValue` props](../props.md).
11+
:::
12+
13+
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.
14+
15+
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.
16+
17+
<script setup>
18+
import { ref } from "vue";
19+
20+
import VueSelect from "../../src";
21+
22+
const selected = ref("");
23+
</script>
24+
25+
<VueSelect
26+
v-model="selected"
27+
:get-option-label="option => option.id"
28+
:get-option-value="option => option.key"
29+
:options="[
30+
{ id: 'France', key: 'fr' },
31+
{ id: 'USA', key: 'us' },
32+
{ id: 'Germany', key: 'de' },
33+
]"
34+
/>
35+
36+
## Demo source-code
37+
38+
```vue
39+
<script setup lang="ts">
40+
import { ref } from "vue";
41+
import VueSelect from "vue3-select-component";
42+
43+
const selected = ref("");
44+
</script>
45+
46+
<template>
47+
<VueSelect
48+
v-model="selected"
49+
:get-option-label="option => option.id"
50+
:get-option-value="option => option.key"
51+
:options="[
52+
{ key: 'fr', id: 'France' },
53+
{ key: 'us', id: 'USA' },
54+
{ key: 'de', id: 'Germany' },
55+
]"
56+
/>
57+
</template>
58+
```

docs/props.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ Aria attributes to be passed to the select control to improve accessibility.
182182

183183
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.
184184

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

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

205-
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.
205+
Resolves option data to a string to render the option label.
206206

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

209-
## getMultiValueLabel
209+
The label of an option is displayed in the dropdown and as the selected option (**single-value**) in the select.
210+
211+
## getOptionValue
210212

211213
**Type**:
212214

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

219221
```ts
220-
(option) => option.label;
222+
(option) => option.value;
221223
```
222224

223-
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.
225+
Resolves option data to a string to compare options and specify value attributes.
226+
227+
This function can be used if you don't want to use the standard `option.value` as the value of the option.
224228

225-
This function is used to display the selected options (**multi-value**) in the select.
229+
::: warning
230+
If you are using TypeScript, TODO.
231+
:::

docs/typescript.md

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ title: 'TypeScript'
66

77
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.
88

9-
## About generics in TypeScript & Vue
9+
## Generics with Vue & TypeScript
1010

1111
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).
1212

1313
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.
1414

15-
A common type you'll see is the `Option` type, which is used to define the options of the select component.
15+
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:
1616

1717
```ts
1818
type Option<T> = {
@@ -22,18 +22,21 @@ type Option<T> = {
2222
};
2323
```
2424

25-
## Custom option value
25+
## Customizing `option.value` type
2626

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

31-
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.
31+
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`.
32+
33+
To do this, import the `Option` type from the component and define a custom type that extends the `Option` type with a generic type:
3234

3335
```vue
3436
<script setup lang="ts">
37+
import type { Option } from "vue3-select-component";
3538
import { ref } from "vue";
36-
import VueSelect, { type Option } from "vue3-select-component";
39+
import VueSelect from "vue3-select-component";
3740
3841
// Define a custom type for the option value.
3942
// It takes a generic type that defines the type of the `value` property.
@@ -60,19 +63,19 @@ const userOptions: UserOption[] = [
6063
</template>
6164
```
6265

63-
## Custom option properties
64-
65-
It is possible to **add properties** to the options passed inside the `:options` prop, while still being type-safe.
66+
## Adding properties to `option`
6667

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

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

7172
```vue
7273
<script setup lang="ts">
74+
import type { Option } from "vue3-select-component";
7375
import { ref } from "vue";
74-
import VueSelect, { type Option } from "vue3-select-component";
76+
import VueSelect from "vue3-select-component";
7577
78+
// Define a custom type for the option value with an additional `username` property.
7679
type UserOption = Option<number> & { username: string };
7780
7881
const selectedUser = ref<number>();
@@ -101,16 +104,17 @@ const userOptions: UserOption[] = [
101104
</template>
102105
```
103106

104-
## Type-safe relationship between `option.value` & `v-model`
107+
## Type-safety between `option.value` & `v-model`
105108

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

108111
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.
109112

110113
```vue
111114
<script setup lang="ts">
115+
import type { Option } from "vue3-select-component";
112116
import { ref } from "vue";
113-
import VueSelect, { type Option } from "vue3-select-component";
117+
import VueSelect from "vue3-select-component";
114118
115119
type UserOption = Option<number>;
116120
@@ -137,3 +141,43 @@ const userOptions: UserOption[] = [
137141
/>
138142
</template>
139143
```
144+
145+
## Using custom label/value with options
146+
147+
::: warning
148+
`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.
149+
:::
150+
151+
If you're using the `getOptionValue` or `getOptionLabel` props, there are a few gotchas to be aware of with the types:
152+
153+
- Local array of options cannot be typed as `Option<T>[]`
154+
- When passing the array of options to the component, you need to cast it to `unknown` then `Option<T>[]`.
155+
156+
Here's an example usage of the `getOptionValue` and `getOptionLabel` props with TypeScript:
157+
158+
```vue
159+
<script setup lang="ts">
160+
import type { Option } from "vue3-select-component";
161+
162+
const activeRole = ref<string>("");
163+
164+
// You cannot type the `roleOptions` as `Option<string>[]`.
165+
const roleOptions = [
166+
{ id: "Admin", key: "admin" },
167+
{ id: "User", key: "user" },
168+
{ id: "Guest", key: "guest" },
169+
];
170+
</script>
171+
172+
<template>
173+
<!-- Casting of the `roleOptions` must be done at the `:options` prop-level. -->
174+
<VueSelect
175+
v-model="activeRole"
176+
:options="(roleOptions as unknown as Option<string>[])"
177+
:is-multi="false"
178+
:get-option-label="option => (option.id as string)"
179+
:get-option-value="option => (option.key as string)"
180+
placeholder="Pick a role"
181+
/>
182+
</template>
183+
```

playground/Playground.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type UserOption = Option<number> & { username: string };
99
1010
const activeBook = ref<string | null>(null);
1111
const activeUsers = ref<number[]>([1, 3]);
12+
const activeRole = ref<string | null>(null);
1213
const isLoading = ref(false);
1314
1415
const bookOptions: BookOption[] = [
@@ -27,6 +28,12 @@ const userOptions: UserOption[] = [
2728
{ label: "Admin", value: 6, username: "admin" },
2829
{ label: "Root", value: 6, username: "root" },
2930
];
31+
32+
const roleOptions = [
33+
{ id: "Admin", key: "admin" },
34+
{ id: "User", key: "user" },
35+
{ id: "Guest", key: "guest" },
36+
];
3037
</script>
3138

3239
<template>
@@ -55,6 +62,19 @@ const userOptions: UserOption[] = [
5562
<p class="selected-value">
5663
Selected user value: {{ activeUsers || "none" }}
5764
</p>
65+
66+
<VueSelect
67+
v-model="activeRole"
68+
:options="(roleOptions as unknown as Option<string>[])"
69+
:is-multi="false"
70+
:get-option-label="option => (option.id as string)"
71+
:get-option-value="option => (option.key as string)"
72+
placeholder="Pick a role"
73+
/>
74+
75+
<p class="selected-value">
76+
Selected role value: {{ activeRole || "none" }}
77+
</p>
5878
</form>
5979
</div>
6080
</template>

src/Select.spec.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import type { Option } from "./types";
12
import { mount } from "@vue/test-utils";
2-
import { describe, expect, it } from "vitest";
33

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

67
const options = [
@@ -407,6 +408,51 @@ describe("search emit", () => {
407408
});
408409

409410
describe("component props", () => {
411+
it("should use getOptionValue prop to get custom option value", async () => {
412+
const options = [
413+
{ id: "Admin", key: "admin" },
414+
{ id: "User", key: "user" },
415+
];
416+
417+
const wrapper = mount(VueSelect, {
418+
props: {
419+
modelValue: null,
420+
options: options as unknown as Option<string>[],
421+
getOptionValue: (option: any) => option.key,
422+
},
423+
});
424+
425+
await openMenu(wrapper);
426+
await wrapper.get("div[role='option']").trigger("click");
427+
428+
expect(wrapper.emitted("update:modelValue")).toStrictEqual([["admin"]]);
429+
});
430+
431+
it("should use getOptionLabel prop to display custom option label", async () => {
432+
const options = [
433+
{ id: "Admin", key: "admin" },
434+
{ id: "User", key: "user" },
435+
];
436+
437+
const wrapper = mount(VueSelect, {
438+
props: {
439+
modelValue: null,
440+
options: options as unknown as Option<string>[],
441+
getOptionLabel: (option: any) => option.id,
442+
},
443+
});
444+
445+
await openMenu(wrapper);
446+
447+
const optionElements = wrapper.findAll("div[role='option']");
448+
expect(optionElements[0].text()).toBe("Admin");
449+
expect(optionElements[1].text()).toBe("User");
450+
451+
await wrapper.get("div[role='option']").trigger("click");
452+
453+
expect(wrapper.get(".single-value").text()).toBe("Admin");
454+
});
455+
410456
it("should display the placeholder in the input when no option is selected", () => {
411457
const wrapper = mount(VueSelect, { props: { modelValue: null, options, placeholder: "Pick an option" } });
412458

0 commit comments

Comments
 (0)