DEV Community

Cover image for Transparent input wrapper in Vue.js 3
Giannis Koutsaftakis
Giannis Koutsaftakis

Posted on

Transparent input wrapper in Vue.js 3

The transparent wrapper component is one of the most commonly used patterns in frontend development. It's a great way to hide complexity and standardize the look and feel of form controls, making sure surrounding elements such as labels, validation messages, etc appear correctly and consistently across our app.

We want wrapper components to behave as closely as possible to native elements - so that attributes, event listeners, and two-way binding pass-through transparently.

Let's see how we can implement it with Vue.js 3 using two variations. As an example, we're gonna go with a simple version of an input text wrapper with a label.

Variation 1: using :value and @input

<template>
  <label>{{ label }}</label>
  <input
    type="text"
    :value="modelValue"
    v-bind="$attrs"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script>
export default {
  name: 'InputWrapper',
  props: {
    modelValue: {
      type: String,
      default: ''
    },
    label: {
      type: String,
      default: ''
    }
  },
  emits: ['update:modelValue']
};
</script>
Enter fullscreen mode Exit fullscreen mode

This can be simplified even more by:

  • removing the name attribute
  • list props as an Array of Strings
  • omit the emits attribute

but we are sticking with the best practices here.

This input wrapper component can now be used simply as

<input-wrapper v-model="myValue" label="My label" />
Enter fullscreen mode Exit fullscreen mode

Some things to note here:

  • We want to make sure our component feels like a native element so we implement two-way data binding using v-model, with the new syntax for custom components in Vue.js 3. In order to do so, we bind the value of the input with the modelValue prop and we emit the update:modelValue event on input change.

  • We are passing all the non-prop attributes and the event listeners to the input using v-bind="$attrs". We have to explicitly define this since we don't have a root element in our template so Vue wouldn't know in which element we want them to be inherited. It's also worth noting that since we've created a component with multiple root nodes we don't need to set the inheritAttrs attribute to false.

Variation 2: using the Composition API with v-model and computed

<template>
  <label>{{ label }}</label>
  <input v-model="localValue" type="text" v-bind="$attrs" />
</template>

<script>
import { computed } from 'vue';

export default {
  name: 'InputWrapperComputed',
  props: {
    modelValue: {
      type: String,
      default: ''
    },
    label: {
      type: String,
      default: ''
    }
  },
  emits: ['update:modelValue'],
  setup(props, context) {
    const localValue = computed({
      get: () => props.modelValue,
      set: (value) => context.emit('update:modelValue', value)
    });
    return {
      localValue
    };
  }
};
</script>
Enter fullscreen mode Exit fullscreen mode

In this variation, we use v-model on the input of our wrapper component. Using a special computed property that has a getter but also a setter, so that not only can we read its derived value but also assign it a new value too.
When the value changes the setter is invoked and emits the new value back to the parent.

Examples

You can find live examples of both variations as well as pass-through of attributes, event listeners, and two-way binding, on this CodeSandbox.

Conclusion

Vue.js 3 is in its early days and many of the examples floating around the internet regarding transparent wrappers are either for Vue.js 2 or for Vue.js 3 RC versions so they don't play well with the final version of Vue.js 3.

The variations shared above are just some of the ways the transparent wrapper pattern can be implemented using Vue.js 3 which proves just how versatile the new version of the framework can be.

If you are doing things differently, let me know in the comments.

| Thanks for reading

Top comments (2)

Collapse
 
luckylooke profile image
Ctibor Laky • Edited

Thanks for providing examples, here is my edit for setup script:

<script setup lang="ts">
import { computed } from 'vue'

const emit = defineEmits<{
  (e: 'update:modelValue', value: string | number): void
}>()

const props = defineProps({
  modelValue: [String, Number],
  label: String,
});

const localValue = computed({
  get: () => props.modelValue,
  set: value => emit('update:modelValue', value!)
});
</script>  

<template>
  <label>{{ label }}</label>
  <input v-model="localValue" type="text" v-bind="$attrs" />
</template>
Enter fullscreen mode Exit fullscreen mode