Conditional <router-link> in Vue.js dependant on prop value?

Hopefully this is a rather simple question / answer, but I can't find much info in the docs.

Is there a way to enable or disable the anchor generated by <router-link> dependent on whether a prop is passed in or not?

<router-link class="Card__link" :to="{ name: 'Property', params: { id: id }}">
  <h1 class="Card__title">{{ title }}</h1>
  <p class="Card__description">{{ description }}</p>
</router-link>

If there's no id passed to this component, I'd like to disable any link being generated.

Is there a way to do this without doubling up the content into a v-if?

Thanks!


Solution 1:

Assuming you want to disable anchor tag as in not clickable and look disabled the option is using CSS. isActive should return true by checking prop id.

<router-link class="Card__link" v-bind:class="{ disabled: isActive }" :to="{ name: 'Property', params: { id: id }}">
  <h1 class="Card__title">{{ title }}</h1>
  <p class="Card__description">{{ description }}</p>
</router-link>

<style>
 .disabled {
    pointer-events:none; 
    opacity:0.6;        
 }
<style>

If you want to just disable the navigation , you can use a route guard.

beforeEnter: (to, from, next) => {
            next(false);
 }

Solution 2:

The problem is that router-link renders as an html anchor tag, and anchor tags do not support the disabled attribute. However you can add tag="button" to router-link:

<router-link :to="myLink" tag="button" :disabled="isDisabled" >

Vue will then render your link as a button, which naturally supports the disabled attribute. Problem solved! The downside is that you have to provide additional styling to make it look like a link. However this is the best way to achieve this functionality and does not rely on any pointer-events hack.

Solution 3:

If you need to use it often, consider this:

Create new component

<template>
  <router-link
    v-if="!disabled"
    v-bind="$attrs"
  >
    <slot/>
  </router-link>

  <span
    v-else
    v-bind="$attrs"
  >
    <slot/>
  </span>
</template>

<script>
export default {
  name: 'optional-router-link',

  props: {
    params: Object,
    disabled: {
      type: Boolean,
      default: false,
    },
  },
};
</script>

Optional, register globally:

Vue.component('optional-router-link', OptionalRouterLink);

Use it as follows:

<optional-router-link
  :disabled="isDisabled"
  :to="whatever"
>
    My link
</optional-router-link>

Solution 4:

I sometimes do stuff like this:

<component
    :is="hasSubLinks ? 'button' : 'router-link'"
    :to="hasSubLinks ? undefined : href"
    :some-prop="computedValue"
    @click="hasSubLinks ? handleClick() : navigate"
>
    <!-- arbitrary markup -->
</component>

...

computed: {
    computedValue() {
        if (this.hasSubLinks) return 'something';
        if (this.day === 'Friday') return 'tgif';
        return 'its-fine';
    },
},

But I basically always wrap router-link, so you can gain control over disabled state, or pre-examine any state or props before rendering the link, with something like this:

<template>
    <router-link
        v-slot="{ href, route, navigate, isActive, isExactActive }"
        :to="to"
    >
        <a
            :class="['nav-link-white', {
                'nav-link-white-active': isActive,
                'opacity-50': isDisabled,
            }]"
            :href="isDisabled ? undefined : href"
            @click="handler => handleClick(handler, navigate)"
        >
            <slot></slot>
        </a>

    </router-link>
</template>

<script>
export default {
    name: 'top-nav-link',

    props: {
        isDisabled: {
            type: Boolean,
            required: false,
            default: () => false,
        },

        to: {
            type: Object,
            required: true,
        },
    },

    data() {
        return {};
    },

    computed: {},

    methods: {
        handleClick(handler, navigate) {
            if (this.isDisabled) return undefined;
            return navigate(handler);
        },
    },

};
</script>

In my app right now, I'm noticing that some combinations of @click="handler => handleClick(handler, navigate)" suffer significantly in performance.

For example this changes routes very slow:

@click="isDisabled ? undefined : handler => navigate(handler)"

But the pattern in my full example code above works and has no performance issue.

In general, ternary operator in @click can be very dicey, so if you get issues, don't give up right away, try many different ways to bifurcate on predicates or switch over <component :is="" based on state. navigate itself is an ornery one because it requires the implicit first parameter to work.

I haven't tried, but you should be able to use something like Function.prototype.call(), Function.prototype.apply(), or Function.prototype.bind().

For example, you might be able to do:

@click="handler => setupNavigationTarget(handler, navigate)"

...

setupNavigationTarget(handler, cb) {
    if (this.isDisabled) return undefined;
    return this.$root.$emit('some-event', cb.bind(this, handler));
},

...

// another component
mounted() {
    this.$root.$on('some-event', (navigate) => {
        if (['Saturday', 'Sunday'].includes(currentDayOfTheWeek)) {
            // halt the navigation event
            return undefined;
        }

        // otherwise continue (and notice how downstream logic
        // no longer has to care about the bound handler object)
        return navigate();
    });
},