Vue 3 Select Binding with TypeScript
This challenge focuses on creating a custom, reusable select component in Vue 3 using TypeScript. You'll implement a component that allows for two-way binding of a selected value, similar to the native <select> element but with enhanced flexibility and type safety. This is a fundamental building block for creating sophisticated forms and user interfaces in Vue applications.
Problem Description
You need to build a Vue 3 component, let's call it CustomSelect, that mimics the behavior of a native <select> element. This component should accept an array of options and a model value, and allow for two-way binding of the selected option's value. The component should be fully type-safe using TypeScript.
Key Requirements:
- Props:
options: An array of objects. Each object should have at least avalueproperty (of any type) and alabelproperty (string).modelValue: The current selected value. This should be compatible with thevaluetypes in youroptions.
- Emits:
update:modelValue: An event emitted when the selected value changes, passing the new selected value.
- Rendering:
- The component should render a
<select>element. - The
<select>element should have<option>elements generated from theoptionsprop. Each<option>should display itslabeland have itsvalueattribute set to the corresponding option'svalue.
- The component should render a
- Two-Way Binding:
- The
modelValueprop should be used to set the initially selected option. - When the user selects a different option in the native
<select>element, theupdate:modelValueevent should be emitted with the new value.
- The
- Type Safety:
- The component should be written in TypeScript, ensuring type safety for props, emits, and internal state.
Expected Behavior:
When the CustomSelect component is used with v-model, changing the selection in the dropdown should update the bound data property in the parent component, and vice-versa.
Edge Cases:
- An empty
optionsarray. modelValuenot matching any of thevalueproperties in theoptions. In this case, the select should ideally not have a pre-selected value, or fall back to the first option if appropriate (though not strictly required for this challenge).
Examples
Example 1:
{
"parentData": {
"selectedFruit": "apple"
},
"options": [
{ "value": "apple", "label": "Apple" },
{ "value": "banana", "label": "Banana" },
{ "value": "cherry", "label": "Cherry" }
]
}
Vue Template Usage:
<template>
<CustomSelect
:options="options"
v-model="selectedFruit"
/>
<p>Selected: {{ selectedFruit }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import CustomSelect from './CustomSelect.vue';
interface FruitOption {
value: string;
label: string;
}
const selectedFruit = ref<string>('apple');
const options = ref<FruitOption[]>([
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'cherry', label: 'Cherry' },
]);
</script>
Expected Output (Initial Render):
A <select> element is rendered. The option "Apple" is pre-selected. The paragraph displays "Selected: apple".
Expected Output (After User selects "Banana"):
The <select> element now shows "Banana" as selected. The paragraph displays "Selected: banana".
Example 2:
{
"parentData": {
"selectedNumber": 2
},
"options": [
{ "value": 1, "label": "One" },
{ "value": 2, "label": "Two" },
{ "value": 3, "label": "Three" }
]
}
Vue Template Usage:
<template>
<CustomSelect
:options="options"
v-model="selectedNumber"
/>
<p>Selected: {{ selectedNumber }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import CustomSelect from './CustomSelect.vue';
interface NumberOption {
value: number;
label: string;
}
const selectedNumber = ref<number>(2);
const options = ref<NumberOption[]>([
{ value: 1, label: 'One' },
{ value: 2, label: 'Two' },
{ value: 3, label: 'Three' },
]);
</script>
Expected Output (Initial Render):
A <select> element is rendered. The option "Two" is pre-selected. The paragraph displays "Selected: 2".
Expected Output (After User selects "Three"):
The <select> element now shows "Three" as selected. The paragraph displays "Selected: 3".
Example 3 (Edge Case: Empty Options):
{
"parentData": {
"selectedItem": null
},
"options": []
}
Vue Template Usage:
<template>
<CustomSelect
:options="options"
v-model="selectedItem"
/>
<p>Selected: {{ selectedItem }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import CustomSelect from './CustomSelect.vue';
interface ItemOption {
value: string | null;
label: string;
}
const selectedItem = ref<string | null>(null);
const options = ref<ItemOption[]>([]);
</script>
Expected Output:
A <select> element is rendered, but it is empty (no options). The paragraph displays "Selected: null".
Constraints
- Vue.js version: 3.x
- Language: TypeScript
- The
optionsarray can contain up to 100 items. - The
valueproperty of options can be of any primitive type (string, number, boolean) ornull.
Notes
- Consider how you will define the generic types for the
optionsandmodelValueto achieve maximum type safety. - You will need to use Vue 3's Composition API (
setupfunction) for this challenge. - Think about how to handle the
v-modeldirective by defining the correct props and emits. - The challenge is about creating a functional component that binds data. Styling is not a primary concern, but ensuring the basic HTML structure is correct is important.
- You can leverage Vue's built-in
v-modelsupport for custom components.