Creating custom components with Vuetify - Inheriting props/events/slots in Composition API

onurelibol

Onur Elibol

Posted on November 14, 2020

Creating custom components with Vuetify - Inheriting props/events/slots in Composition API

Hi everyone!

Lately I have been working on customising Vuetify components to have default component look/feel and I wanted to share some best practices that I have learned. In this article, I will try to show you how to inherit/forward props, events and slots from the parent component with minimal effort. I took Vuetify as an example here, but the practices could be applied on any other UI framework that offers props/events/slots. Hope it will be useful for someone at some point.

Why

Basically the main reason is that you sometimes have a use-case to cover, and you need to create a re-usable/customisable component based on a Vuetify (or any other UI framework) component. Well here comes the why; when you create a custom component, you don't want to lose what the UI framework you use offers by default.

I am going to define 2 different use-cases here to base my examples on.

  • First use-case is to be able to use Vuetify components with pre-defined props, for example v-text-field by default outlined, with a standard color etc. But you still want to be able override outlined or color property outside even if they are defined by default.

  • Second use-case is building a custom component based on Vuetify components. Let's say you are going to build a component that is a combination of v-text-field and v-card. But you still want to be able to use default props/events/slots of v-text-field on your custom component without defining it all manually.

With the next version of Vuetify (v3.0 Titan), this cases will be covered easily actually. Here is an example how it is going to look like:
vuetify3
Vuetify 3 updates

But as long as we don't have any access to these stuff yet, we are still stuck with the current approach. So here comes how we can do it with minimal effort.

How

Now when you are using Vuetify, all the components have some sort of pre-defined props/events/slots. To cover both use-cases above, most important thing is inheriting these props/events/slots from the parent component. So how do we do that?

Lets start with creating our custom text field with pre-defined props:



// CustomTextField.vue
<template>
  <v-text-field
    :value="value"
    outlined
    color="primary"
    @input="v => $emit('input', v)" />
</template>


Enter fullscreen mode Exit fullscreen mode

Great, now we can use it like:



// Parent.vue
<custom-text-field v-model="value" />


Enter fullscreen mode Exit fullscreen mode

Note: v-model is sugar syntax for :value="value" @input="v => $emit('input', v)"

We have created our CustomTextField and it is by default outlined and has primary color. Now what about if we want to use flat or dense or any other prop that v-text-field has oncustom-text-field? Or what if we need to override outlined and color property at some point, how are we going to do that? Basically we can't, as outlined and color is defined statically here and nothing is changing them.

So currently adding these props to our custom component as below would NOT work (for now):



<custom-text-field v-model="value" dense :outlined="false" />


Enter fullscreen mode Exit fullscreen mode

This because of we are not inheriting any props that were sent from Parent.

Inheriting Props

To inherit props from parent, we can use a small trick that would help us. In Vue, each parent component sends the attributes that are added itself. To access those we can simply use $attrs in our template to bind everything that were sent from parent like this:



// CustomTextField.vue
<template>
  <v-text-field
    v-bind="$attrs"
    :value="value"
    outlined
    color="primary"
    @input="v => $emit('input', v)" />
</template>
<script>
  export default {
    inheritAttrs: false
  }
</script>


Enter fullscreen mode Exit fullscreen mode

And voila! Now our <v-text-field> inside CustomTextField component inherits all attributes that were added to <custom-text-field>. So we can now easily use every prop that v-text-field provides on <custom-text-field> and override anything pre-defined props inside like this:



// Parent.vue
<custom-text-field v-model="value" dense :outlined="false" />


Enter fullscreen mode Exit fullscreen mode

IMPORTANT

The way $attrs behave differs between Vue 2.x and 3.x! Some differences are:

  • In Vue 2.x $attrs does not include styles and classes that were sent from parent
  • In Vue 3.x $attrs includes styles and classes that were sent from parent. Also $listeners are now included inside $attrs which I will talk about later

For more information check details at Vue 3 docs.

Inheriting Events

Alright, we are now inheriting props from parent so we can use our custom text field as we are using v-text-field with props. So what about events? How can we forward all the events that are happening on <v-text-field> to <custom-text-field>?

The solution is simple here as well:



// CustomTextField.vue
<template>
  <v-text-field
    v-bind="$attrs"
    :value="value"
    outlined
    color="primary"
    v-on="$listeners"
    @input="v => $emit('input', v)" />
</template>
<script>
  export default {
    inheritAttrs: false
  }
</script>


Enter fullscreen mode Exit fullscreen mode

We just bind $listeners with v-on and thats it! So now we can easily add any event that <v-text-field> provides to <custom-text-field> like this:



// Parent.vue
<custom-text-field
  v-model="value"
  dense
  :outlined="false"
  @blur="onFocus"
  @keypress="onKeypress" />


Enter fullscreen mode Exit fullscreen mode

IMPORTANT

$listeners is removed in Vue 3.x and is included inside $attrs. So if you are using Vue 3.x, binding the component with $attrs will be enough to bind $listeners, like here:



// bind props, attrs, class, style in Vue 3.x
<v-text-field v-bind="$attrs" />


Enter fullscreen mode Exit fullscreen mode

For more information check details at Vue 3 docs.

Inheriting Slots

Slots are a little bit different than props or events. There is for sure different ways to do this, but here is what I am doing to forward all the slots that were sent from parent to child.

I start with picking all the slot names that were sent from the parent inside a computed:



// CustomTextField.vue
export default {
  setup(props, ctx) {
    const parentSlots = computed(() => Object.keys(ctx.slots))

    return { parentSlots }
  }  
}


Enter fullscreen mode Exit fullscreen mode

Then inside the <template> part I am looping through the slots to declare all the slots dynamically like this:



// CustomTextField.vue
// Vue 2.x way, binding $listeners with v-on not needed in Vue 3.x
<template>
  <v-text-field
    v-bind="$attrs"
    :value="value"
    outlined
    color="primary"
    v-on="$listeners"
    @input="v => $emit('input', v)"
  >
    <!-- Dynamically inherit slots from parent -->
    <template v-for="slot in parentSlots" #[slot]>
      <slot :name="slot" />
    </template>
  </v-text-field>

</template>
<script>
export default {
  setup(props, ctx) {
    const parentSlots = computed(() => Object.keys(ctx.slots))

    return { parentSlots }
  }  
}
</script>


Enter fullscreen mode Exit fullscreen mode

Note that # is shorthand for v-slot. Here we could also use:



<template v-for="slot in parentSlots" #[slot]="props">
  <slot :name="slot" :props="props" />
</template>


Enter fullscreen mode Exit fullscreen mode

to forward slot props as well. But the v-text-field component then does not render the slots that has not any props. I suppose this is a bug in Vuetify. Issue here

Great! So now we are even forwarding v-text-field slots from parent to child which means we can use slots of <v-text-field> like this:



// Parent.vue
<custom-text-field
  v-model="value"
  dense
  :outlined="false"
  @blur="onFocus"
  @keypress="onKeypress"
>
  <template #label>Custom Label</template>
  <template #message>Custom Message</template>
</custom-text-field>


Enter fullscreen mode Exit fullscreen mode

BONUS: Custom usage of props/events/slots

We are now done with inheritance. But what if you need to use some of your $attrs on another element? For example inside your custom component, you have <v-text-field> and <v-card> and you want share color property in both. At this point there are different ways to go. But as long as I like to keep things organised, I use computed to organise/control it from one point.

Example:



// CustomTextField.vue
// Vue 2.x way, binding $listeners with v-on not needed in Vue 3.x
<template>
  <div>
    <v-text-field
      v-bind="[textFieldDefaults, $attrs]"
      :value="value"
      v-on="$listeners"
      @input="v => $emit('input', v)"
    >
      <template v-for="slot in parentSlots" #[slot]>
        <slot :name="slot" />
      </template>
    </v-text-field>

    <v-card v-bind="cardDefaults">
      <v-card-text>I am a card</v-card-text>
    </v-card>

  </div>
</template>
<script>
export default {
  setup(props, ctx) {
    const parentSlots = computed(() => Object.keys(ctx.slots))

    const textFieldDefaults = computed(() => ({
      outlined: true,
      dense: true,
      color: 'primary'
    }))

    const cardDefaults = computed(() => ({
      color: ctx.attrs.color || 'primary'
    }))

    return { parentSlots, textFieldDefaults, cardDefaults }
  }  
}
</script>


Enter fullscreen mode Exit fullscreen mode

So what is happening here? We have created 2 computed variables, one for v-text-field defaults and one for v-card.

  • In textFieldDefaults we define our default text field props and then binding it with v-bind=[textFieldDefaults, $attrs]. If any of default values were sent from top, values inside $attrs overrides our default props.
  • In cardDefaults we only take color property. If you wish, you could easily add any other prop or listener that were sent from the parent here.
  • Both textFieldDefaults and cardDefaults must be declared as computed, to be able make them reactive and listen to the changes that are happening in parent.

Conclusion

To sum up, Vue offers many different options for us to achieve what we need to do. It is very easy to create custom components that are based on any UI framework without losing what the framework already offers us. There could be for sure some edge cases, but I think with the approach I tried to explain above, you can solve most of them. Hopefully, this post helped you to understand the concept!

Thank you for reading!

💖 💪 🙅 🚩
onurelibol
Onur Elibol

Posted on November 14, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related