Hone logo
Hone
Problems

Building a Controlled Input Component in Vue.js

In Vue.js, controlled components are a fundamental pattern for managing form input state. They allow you to synchronize the component's state with the DOM element's value, providing greater control over input behavior and validation. This challenge will guide you through building a reusable controlled input component.

Problem Description

Your task is to create a custom Vue.js component named ControlledInput.vue that functions as a controlled input field. This component should accept a modelValue prop (for two-way data binding with v-model) and emit an update:modelValue event when the input's value changes. This will enable parent components to manage the input's state externally.

Key Requirements:

  • Component Structure: Create a single-file Vue component (ControlledInput.vue).
  • Input Element: The component should render a standard HTML <input type="text"> element.
  • modelValue Prop: Accept a prop named modelValue of type string (or any appropriate type if you want to extend this later, but string is sufficient for this challenge). This prop represents the current value of the input field.
  • update:modelValue Event: Emit an event named update:modelValue whenever the user types into the input field. The payload of this event should be the new value of the input field.
  • Two-way Data Binding: The component should seamlessly integrate with Vue's v-model directive.

Expected Behavior:

When ControlledInput is used in a parent component with v-model, typing into the input field should update the parent component's data, and changes to the parent component's data should be reflected in the input field.

Edge Cases to Consider:

  • Initial Value: The input should correctly display its initial modelValue when the component is first rendered.
  • Empty Input: The component should handle an empty modelValue gracefully.

Examples

Example 1: Basic Usage

Parent Component (App.vue):

<template>
  <div>
    <ControlledInput v-model="message" />
    <p>Message in parent: {{ message }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import ControlledInput from './ControlledInput.vue';

export default defineComponent({
  components: {
    ControlledInput,
  },
  setup() {
    const message = ref('Hello!');
    return {
      message,
    };
  },
});
</script>

ControlledInput.vue (expected implementation):

<template>
  <input
    type="text"
    :value="modelValue"
    @input="onInput"
  />
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  props: {
    modelValue: {
      type: String,
      required: true,
    },
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const onInput = (event: Event) => {
      const target = event.target as HTMLInputElement;
      emit('update:modelValue', target.value);
    };

    return {
      onInput,
    };
  },
});
</script>

Output in browser:

The input field will display "Hello!". When you type " World" into the input, it becomes "Hello World". The paragraph below the input will also update to "Message in parent: Hello World".

Explanation:

The ControlledInput component receives "Hello!" via the modelValue prop. When the user types, the @input event is triggered, calling onInput. onInput then emits update:modelValue with the new input value, which v-model in the parent component captures and updates the message ref.

Example 2: Initial Empty Value

Parent Component (App.vue):

<template>
  <div>
    <ControlledInput v-model="username" />
    <p>Username in parent: {{ username }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import ControlledInput from './ControlledInput.vue';

export default defineComponent({
  components: {
    ControlledInput,
  },
  setup() {
    const username = ref(''); // Initial empty value
    return {
      username,
    };
  },
});
</script>

ControlledInput.vue (same as Example 1):

<template>
  <input
    type="text"
    :value="modelValue"
    @input="onInput"
  />
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  props: {
    modelValue: {
      type: String,
      required: true,
    },
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const onInput = (event: Event) => {
      const target = event.target as HTMLInputElement;
      emit('update:modelValue', target.value);
    };

    return {
      onInput,
    };
  },
});
</script>

Output in browser:

The input field will be empty. As the user types into the input, the username ref in the parent component will be updated, and the paragraph will display the entered text.

Explanation:

This demonstrates that the component correctly handles an initial empty modelValue.

Constraints

  • The ControlledInput.vue component must be written in TypeScript.
  • The component must use the Options API or Composition API as you see fit, but demonstrate a clear understanding of how to handle props and emits.
  • The solution should be a single Vue component file.
  • No external libraries beyond Vue.js itself are permitted.

Notes

  • Consider how the v-model directive internally works with props and events. This challenge aims to replicate that behavior for a custom input.
  • Pay attention to type safety when handling the DOM event and extracting the input's value.
  • Think about accessibility: while not a primary requirement for this challenge, in a real-world scenario, you would add attributes like aria-label for better accessibility.
  • You can extend this component later to handle different input types (e.g., password, number, email) or add validation.
Loading editor...
typescript