4

I'm new using Vue.js and I had a difficulty creating a Button component.

How can I program this component to conditional rendering? In other words, maybe it should be rendering as a router-link maybe as a button? Like that:

<Button type="button" @click="alert('hi!')">It's a button.</Button>
// -> Should return as a <button>.

<Button :to="{ name: 'SomeRoute' }">It's a link.</Button>
// -> Should return as a <router-link>.
2
  • Have two elements in your component and use v-if to decide which one to render Commented Nov 29, 2017 at 1:27
  • 1
    I don't like this solution because I would need a parent element. Commented Dec 1, 2017 at 0:51

5 Answers 5

15
+50

You can toggle the tag inside render() or just use <component>.

According to the official specification for Dynamic Components:

You can use the same mount point and dynamically switch between multiple components using the reserved <component> element and dynamically bind to it's is attribute.

Here's an example for your case:

ButtonControl.vue

<template>
  <component :is="type" :to="to">
    {{ value }}
  </component>
</template>

<script>
export default {
  computed: {
    type () {
      if (this.to) {
        return 'router-link'
      }

      return 'button'
    }
  },

  props: {
    to: {
      required: false
    },
    value: {
      type: String
    }
  }
}
</script>

Now you can easily use it for a button:

<button-control value="Something"></button-control>

Or a router-link:

<button-control to="/" value="Something"></button-control>

This is an excellent behavior to keep in mind when it's necessary to create elements that may have links or not, such as buttons or cards.

Sign up to request clarification or add additional context in comments.

1 Comment

Awesome! Thank you.
4

You can create a custom component which can dynamically render as a different tag using the v-if, v-else-if and v-else directives. As long as Vue can tell that the custom component will have a single root element after it has been rendered, it won't complain.

But first off, you shouldn't name a custom component using the name of "built-in or reserved HTML elements", as the Vue warning you'll get will tell you.

It doesn't make sense to me why you want a single component to conditionally render as a <button> or a <router-link> (which itself renders to an <a> element by default). But if you really want to do that, here's an example:

Vue.use(VueRouter);

const router = new VueRouter({
  routes: [ { path: '/' } ]
})

Vue.component('linkOrButton', {
  template: `
    <router-link v-if="type === 'link'" :to="to">I'm a router-link</router-link>
    <button v-else-if="type ==='button'">I'm a button</button>
    <div v-else>I'm a just a div</div>
  `,
  props: ['type', 'to']
})

new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
  <link-or-button type="link" to="/"></link-or-button>
  <link-or-button type="button"></link-or-button>
  <link-or-button></link-or-button>
</div>


If you're just trying to render a <router-link> as a <button> instead of an <a>, then you can specify that via the tag prop on the <router-link> itself:

Vue.use(VueRouter);

const router = new VueRouter({
  routes: [ { path: '/' } ]
})

new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
  <router-link to="/">I'm an a</router-link>
  <router-link to="/" tag="button">I'm a button</router-link>
</div>

5 Comments

I'll up vote this if the first explanation is removed. =)
@BillCriswell why's that?
You can't have a component with multiple root elements I don't think, but the big thing is it's icky.
@BillCriswell. Hmm not sure what you mean. As I explain in the answer, the component won't have multiple root elements. And as far as ickiness, I think that's just a matter of opinion. I'm not sure why OP needs this use case, but if I needed to make a simple custom component that conditionally rendered either a <router-link> or <button>, that's how I'd do it.
I'd go with a render function or the tag on the router-link personally. The template switch is just messy in my opinion.
3

You can achieve that through render functions.

render: function (h) {
if(this.to){ // i am not sure if presence of to props is your condition
  return h(routerLink, { props: { to: this.to } },this.$slots.default)
}
return h('a', this.$slots.default)
}

That should help you start into the right direction

1 Comment

What is the import for routerLink?
1

I don't think you'd be able to render a <router-link> or <button> conditionally without having a parent element.

What you can do is decide what to do on click as well as style your element based on the props passed.

template: `<a :class="{btn: !isLink, link: isLink}" @click="handleClick"><slot>Default content</slot></a>`,
props: ['to'],
computed: {
  isLink () { return !!this.to }
},
methods: {
  handleClick () {
    if (this.isLink) {
      this.$router.push(this.to)
    }
    this.$emit('click') // edited this to always emit
  }
}

3 Comments

This solution also has accessibility issues. Sometimes the browser behavior needs to be a button and sometimes as the link. In addition, the href attribute is required (see my comment in another awnser).
Well, you've really requirements'd yourself into a corner then, haven't you
Thanks, @Phil. I don't want to make it difficult, I'm just following my intuition. If there is an elegant solution, it will serve as a reference for many people and projects, it's a common problem.
1

I would follow the advice by @Phil and use v-if but if you'd rather use one component, you can programmatically navigate in your click method.

Your code can look something like this:

<template>
  <Button type="button" @click="handleLink">It's a button.</Button>
</template>
<script>
  export default {
    name: 'my-button',
    props: {
      routerLink: {
        type: Boolean,
        default: false
      }
    },
    methods: {
      handleLink () {
        if (this.routerLink) {
          this.$router.push({ name: 'SomeRoute' })
        } else {
          alert("hi!")
        }
      }
    }
  }
</script>

2 Comments

I thought of a similar solution, but I'd like to avoid it by accessibility problems. In example, the user would not be able to check the address address in the browser status bar, open it in a new tab or copy the link with the right button.
Well then you can bind to and return null if you don't need it. E.g. :to="ok ? url : null"

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.