Vuetify is one of the most popular high-level UI component frameworks for Vue.js. It's based on Google Material Design and provides a wide range of high-quality pre-made components so that you can start building functional, accessible, and aesthetically pleasing web apps right away.
Although the built-in styling of Vuetify is great, your project might require different styling or you just want to customize Vuetify components to your liking.
Vue wrapper components to the rescue!
Wrapper components
A wrapper component is a custom component that "wraps" (hence the name) a native element or another component in order to add some custom functionality, styles, or anything else really.
Wrapper components are a great way to keep our codebase DRY
by encapsulating functionality and/or styling that would otherwise have to be repeated.
Some of the advantages of wrapper components include:
- Coherence between different parts of the application.
- Simplified development by avoiding copy/paste.
- Reduced bundle size.
- Decouple the application from third-party components so that it's easier to switch from one component implementation to another.
Usecase
In this case, we are going to build a wrapper component for the Vuetify v-text-field
component as an example, putting a label on top of the input and setting some default styles as well.
In the picture below:
- On the left, there's the default Vuetify text field.
- On the right, we see the custom text field that we are going to create using a wrapper component.
Now that we know how our final result looks like, let's get to the code.
Our wrapper component
<template>
<div>
<label>{{ label }}</label>
<v-text-field v-bind="{ ...$attrs, ...commonAttrs }" v-on="$listeners">
<template v-for="(_, scopedSlotName) in $scopedSlots" #[scopedSlotName]="slotData">
<slot :name="scopedSlotName" v-bind="slotData" />
</template>
<template v-for="(_, slotName) in $slots" #[slotName]>
<slot :name="slotName" />
</template>
</v-text-field>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: {
label: {
type: String,
default: ''
}
},
computed: {
commonAttrs() {
return {
label: '',
persistentHint: true,
outlined: true,
dense: true,
hideDetails: false,
class: {
'mt-1': this.$props.label
}
}
}
}
}
</script>
Breakdown of the most important parts.
- Disable attribute inheritance
inheritAttrs: false
Setting inheritAttrs
to false
enables us to forward all attributes to v-text-field
using $attrs
.
- Bind all parent-scope attribute bindings to the Vuetify component.
v-bind="{ ...$attrs, ...commonAttrs }"
In this case, we are also merging $attrs
with our own attributes.
- Forward all event listeners on the component to the Vuetify component.
v-on="$listeners"
All event listeners such as e.g @click
, @input
etc will propagate to the Vuetify component.
- Pass down
slots
to the Vuetify component.
<template v-for="(_, scopedSlotName) in $scopedSlots" #[scopedSlotName]="slotData">
<slot :name="scopedSlotName" v-bind="slotData" />
</template>
<template v-for="(_, slotName) in $slots" #[slotName]>
<slot :name="slotName" />
</template>
Vuetify components provide slots for customization, we want them passed on from the wrapper component to the Vuetify component as well.
That's all there is to it!
You can now use the "component wrapper" technique to extend Vuetify components and also to build apps that are modular and more organized overall.
You can view the code used, in this CodeSandbox example.
Top comments (5)
Thanks, awesome write-up! Exactly what I was looking for.
Where defined
slotData
?Very good explanation! I use this technique a lot in my projects.
Your solution works, and your explanation helps to understand the underlying intricacies. Thanks for sharing very helpful indeed.
Thanks, it was very useful!