Amit Biswas
Amit's Blog

Follow

Amit's Blog

Follow
Building Input Components in Vue

Building Input Components in Vue

Amit Biswas's photo
Amit Biswas
·Feb 20, 2023·

7 min read

The web is made up of forms! It's hard to find a public-facing website without a way of taking some kind of input from the visitor. And providing an effective yet simple way to build a form is an essential aspect of a good frontend framework. Enters Vue 🎉

Vue provides a pleasant two-way data binding API, in the form of the v-model derivative. v-model creates a reactive connection between an HTML element and JavaScript, making it fun to work with inputs of all sorts! Today we'll look into a few ways of building reusable Vue components with HTML input elements. Let's get started!

Creating a reactive input field

Input fields are pretty straightforward in Vue, visibly not so different from HTML.

<template>
  <label for="username">
    Username
    <input type="text" name="username" id="username" />
  </label>
</template>

Here, we're taking the username as a text type input. It doesn't do anything with the input yet, we'll add v-model to handle that.

<template>
  <div>
    <label for="username">
      Username
      <input type="text" name="username" id="username" v-model="username" />
    </label>
    <p>{{ username }}</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        username: "",
      };
    },
  };
</script>

Firstly, we're adding v-model for the input element, which effectively binds the data username with the input value. It's a two-way bind, meaning the value inside input will change if we change the username inside data() and vice-versa. I've added the username inside <p> just below the input to show this change live.

1.gif

It seems to be working nicely! However, updating the input value in real-time is not a common scenario for forms, the value should only get updated when the user completes writing or focuses somewhere else on the page. What v-model does is that it updates the data in every oninput event (Not for languages that use IME, more here), whereas we want it to update on every onchange event. v-model has a built-in modifier that can achieve that, .lazy. Let's try that.

<input type="text" name="username" id="username" v-model.lazy="username" />

The username only updates when we're done writing and press enter or focus outside the input box.

2.gif

Great! All Good for now. In the next step, we'll try to consolidate this input-related logic into a component.

Creating reactive components

It's a common practice to build Vue components based on standard HTML form fields to use across the project. While building such components, sometimes we have to make the input value available from outside the component or from the parent component. The most common way of doing so without using a state management library is to use the $emit instance method. It's available as,

  • $emit inside <template></template>

  • this.$emit inside <script></script>

When called, the $emit(eventName, args) method triggers an event and passes arguments to that event. Then, the listeners that listen to that specific event, process the arguments and take necessary actions. To implement this pattern of using $emit, first, let's create the BaseTextInput component,

  • Create a file named BaseTextInput.vue (or something else)

  • Move the input field-related code there. It'll primarily look like this,

<template>
  <div>
    <label for="username">
      Username
      <input
        type="text"
        name="username"
        id="username"
        v-model.lazy="username"
      />
    </label>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        username: "",
      };
    },
  };
</script>
  • Now let's add a custom event that'll emit from our component when the input changes. In Vue, the @change derivative is used as the onchange event handler.
<input
  type="text"
  name="username"
  id="username"
  v-model.lazy="username"
  @change="$emit('input-changed', username)"
/>

This emits an event called input-changed and passes the username as the argument of the listeners' callback function.

Note: use kebab-case when naming events, as suggested by Vue documentation here.

  • Now let's add a listener in the parent/enclosing component where the input component is used. The listener will trigger a callback function that we'll define in the methods attribute inside <script>. The parent component will look something like this,
<template>
  <BaseTextInput @input-changed="handleInput" />
  <p>{{ username }}</p>
</template>

<script>
  import BaseTextInput from "./components/BaseTextInput";

  export default {
    name: "App",
    data() {
      return {
        username: "",
      };
    },
    methods: {
      handleInput(value) {
        this.username = value;
      },
    },
    components: {
      BaseTextInput,
    },
  };
</script>

Result:

3.gif

Looks like it's working like before. Great! Let's see how we can improve on this.

Creating better reactive components

The pattern we've followed till now is quite valid, but there's room for improvement. To do so, we have to dive deeper into the inner workings of the v-model directive. When we use it on input elements like this,

<input type="text" v-model="username" />

What Vue does under the hood is this,

<input
  type="text"
  v-bind:value="username"
  v-on:input="(e) => (username = e.target.value)"
/>

It gets the username data using one-way v-bind directive, and updates the username on input using an inline arrow function. So technically, we could've wrote our previous BaseTextInput components input element like this,

<input
  type="text"
  name="username"
  id="username"
  :value="username"
  @change="handleInput"
/>

The handleInput function is defined inside methods in a simple manner.

handleInput(e) {
  this.username = e.target.value;
  this.$emit("input-changed", this.username);
}

This approach makes us write a couple more lines of code than the previous implementation, but gives us finer control over the data flow both inside and outside the component. But have we reached the better part yet? Not quite.

  1. We are maintaining two local states inside both the parent and child components.

  2. We're resorting to custom events when we could've used Vue's built-in events.

  3. There's no way to dictate the value of input from the parent.

Let's solve these issues by dissecting the v-model in our child component!

  • Remove the local state variable from data() and update the value of the input element to be dependent on a prop from the parent.
<template>
  <div>
    <label for="username">
      Username
      <input
        type="text"
        name="username"
        id="username"
        :value="username"
        @change="$emit('update:username', $event.target.value)"
      />
    </label>
  </div>
</template>

<script>
  export default {
    props: {
      username: String,
    },
    emits: ["update:username"],
  };
</script>

We're getting a prop named username from the parent, and binding it with inputs value. This means whenever we pass a new prop from the parent, the input element will re-render and set the value of username as its value. To do the opposite, that is to reflect changes from input to parent, we're emitting an event on change (@change) and passing the input value as an argument. But what's with the event named update:username? It's a built-in event with a custom argument. Let's discuss this in the next step.

  • Update the parent component to use v-model instead of listening to our custom event. Previously, we mentioned that under the hood Vue uses :value and @input directives to achieve v-models functionalities. When used with components, v-models implementation is a bit different, where this,
<BaseTextInput v-model="username" />

means,

<BaseTextInput
  :modelValue="username"
  @update:modelValue="newValue => username = newValue"
/>

Please note, For Vue 2, the implementation is a bit different. Instead of @update:modelValue, it uses input event. Further details can be found at the Vue Documentation.

By default, it listens to this @update:modelValue event. We can change the modelValue variable to any other name we want if we use the explicit version instead of directly using v-model. So the parent component can be like this.

<template>
  <BaseTextInput v-model:username="username" />
  <p>{{ username }}</p>
</template>

<script>
  import BaseTextInput from "./components/BaseTextInput";

  export default {
    name: "App",
    data() {
      return {
        username: "",
      };
    },
    components: {
      BaseTextInput,
    },
  };
</script>

Check here we've passed an additional argument, username, with v-model. This changes the event signature from @update:modelValue to @update:username inside the child component. Pretty neat, right?

The Final result is available at this code sandbox.

Summary

The post already got quite big, so let's stop here today and recap what is what in v-model,

  • v-model is a two-way data-binding directive provided by Vue.

  • It reactively binds an element from template with the value from script.

  • Under the hood, it uses v-bind:value and v-on:input directives to achieve this reactivity.

  • We can pass modifiers like .lazy which updates v-on:input to v-on:change for v-model.

  • When used on a component level, v-model uses v-bind:modelValue and v-on:update:modelValue where modelValue is the default.

  • We can tap into these events and values using $emit from the child component and modify them to our needs.

Apart from these, there are some other topics related to models and components like using multiple v-models for a single component, custom model modifiers, and so on. Maybe we'll discuss those details, along with how this building pattern can be used with other types of inputs, such as number, radio, checkbox, and select some other day! Till then, Peace ☮️

 
Share this