Create a reusable button with Vue Dynamic Components

If you are building any kind of application, it's very likely you have a Button component. You know, a button that can be clicked and multiple things can happen: go to a different page, open a new tab, submit a form… But, even though it can do so many different things, a button should always look like a button, right?

In the good old days, we used to style things using classes. We'd have a .button class that could be used anywhere in our application and that's it. Easy, right? Nowadays, some people think that global classes are not cool anymore. Since global stuff in JavaScript is evil, we assume that global stuff in CSS is evil too. I'm not going to talk about that today, for the sake of this post let's assume that this is 100% correct and that's how we want to work.

We are using Vue, because Vue is cool, and we create a Btn component. Something like this:

<template>
  <button class="button">
    <slot/>
  </button>
</template>

<script>
  export default {}
</script>
    
<style scoped>
  .button {
    display: inline-block;
    margin: 0.5em 0;
    padding: 1em 2em;
    background: #fff;
    border: 2px solid tomato;
    border-radius: 3px;
    color: tomato;
    font-family: "Quicksand", sans-serif;
    font-size: 1em;
    font-weight: 700;
    letter-spacing: 0.02em;
    line-height: 1;
    text-decoration: none;
    text-transform: uppercase;
    cursor: pointer;
    transition: 0.3s;
  }
  
  .button:hover {
    background: tomato;
    color: #fff;
  }
</style>

So now it can be used anywhere like this:

<Btn>I'm a Button!</Btn>

(We use Btn instead of Button to avoid using the same name as the HTML tag)

Conditional rendering

But now it's just a button, and we want to use it to link to other pages. Of course, we could capture the click event and change window.location, but we want to do things the right way and use a proper link. So we could change our component to render an a if we receive an href prop:

<template>
  <div>
    <a v-if="href" :href="href" class="button">
      <slot/>
    </a>
    <button v-else class="button">
      <slot/>
    </button>
  </div>
</template>
    
<script>
  export default {
    props: {
      href: {
        type: String,
        default: null
      }
    }
  }
</script>

And that works! But, as you can see, a wild div has appeared. That is because a Vue component needs to have a single root element. We could live with that, I've seen and worked on plenty of projects that use this pattern, and it works just fine for them. But there is a better way!

Dynamic components to the rescue!

A relatively less popular Vue feature are dynamic components. Vue offers a <component> element that has a special is attribute that we can use to render different components or elements dynamically. So we can refactor the previous code to get rid of the annoying div root element:

<template>
  <component :is="type" :href="href" class="button">
    <slot/>
  </component>
</template>
    
<script>
  export default {
    props: {
      href: {
        type: String,
        default: null
      },
      to: {
        type: String,
        default: null
      }
    },
    computed: {
      type() {
        if (this.href) {
          return 'a'
        } else {
          return 'button'
        }
      }
    }
  }
</script>

Beautiful! Plus, we avoid having to repeat the class attribute, we move the logic to a computed property… overall is much cleaner. And don't worry about adding href to a button, because if the value is falsy the attribute won't be rendered

(If you come from React, it will probably feel more natural to you to use a render function, and that's a perfectly valid solution as well, but I think that dynamic components are a more vuey solution)

With this, we can easily extend our component to also render a router-link (or nuxt-link if you are using Nuxt). You can check the whole code here:

Of course, dynamic components are very powerful, and you can use them to render your own components too. So many possibilities!

Further reading