Creating custom components with Vuetify - Inheriting props/events/slots in Composition API
Tags: vue, vuetify, javascript, components
Published: 2020-11-14 11:46:51

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](https://dev-to-uploads.s3.amazonaws.com/i/usb10dc3rnwbzerf2f2u.png)
[Vuetify 3 updates](https://www.youtube.com/watch?v=aXY4upCtiPI)

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:
```javascript
// CustomTextField.vue
<template>
  <v-text-field
    :value="value"
    outlined
    color="primary"
    @input="v => $emit('input', v)" />
</template>
```

Great, now we can use it like:
```javascript
// Parent.vue
<custom-text-field v-model="value" />
```
**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 on`custom-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):
```javascript
<custom-text-field v-model="value" dense :outlined="false" />
``` 
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:

```javascript
// 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>
```
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: 

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

#### 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.](https://v3.vuejs.org/guide/migration/attrs-includes-class-style.html#overview)

### 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:

```javascript
// 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>
```

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:

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

#### 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:
```javascript
// bind props, attrs, class, style in Vue 3.x
<v-text-field v-bind="$attrs" />
```
For more information check details at [Vue 3 docs.](https://v3.vuejs.org/guide/migration/listeners-removed.html#overview)

### 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: 
```javascript
// CustomTextField.vue
export default {
  setup(props, ctx) {
    const parentSlots = computed(() => Object.keys(ctx.slots))

    return { parentSlots }
  }  
}
```

Then inside the `<template>` part I am looping through the slots to declare all the slots dynamically like this:
```javascript
// 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>
```
Note that **#** is shorthand for `v-slot`. Here we could also use:
```javascript
<template v-for="slot in parentSlots" #[slot]="props">
  <slot :name="slot" :props="props" />
</template>
```
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](https://github.com/vuetifyjs/vuetify/issues/12583)

Great! So now we are even forwarding [v-text-field slots](https://vuetifyjs.com/en/api/v-text-field/#slots) from parent to child which means we can use slots of `<v-text-field>` like this:

```javascript
// 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>
``` 

### 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:

```javascript
// 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>
```

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!