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>
This can be simplified even more by:
- removing the name attribute
- list props as an
Array
ofStrings
- 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" />
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 themodelValue
prop and we emit theupdate: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 theinheritAttrs
attribute tofalse
.
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>
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)
Thanks for providing examples, here is my edit for setup script: