Hone logo
Hone
Problems

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 a value property (of any type) and a label property (string).
    • modelValue: The current selected value. This should be compatible with the value types in your options.
  • 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 the options prop. Each <option> should display its label and have its value attribute set to the corresponding option's value.
  • Two-Way Binding:
    • The modelValue prop should be used to set the initially selected option.
    • When the user selects a different option in the native <select> element, the update:modelValue event should be emitted with the new value.
  • 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 options array.
  • modelValue not matching any of the value properties in the options. 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 options array can contain up to 100 items.
  • The value property of options can be of any primitive type (string, number, boolean) or null.

Notes

  • Consider how you will define the generic types for the options and modelValue to achieve maximum type safety.
  • You will need to use Vue 3's Composition API (setup function) for this challenge.
  • Think about how to handle the v-model directive 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-model support for custom components.
Loading editor...
typescript