How to dynamically render <router-link> or <a> in Vue depending on whether the link is exrternal or internal?
Asked Answered
B

4

5
<template>
  <component
    :is="type === 'internal' ? 'router-link' : 'a'"
    :to="type === 'internal' ? link : null"
    :href="type !== 'internal' ? link : null"
  >
    <slot />
  </component>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

@Component
export default class SiteLink extends Vue {
  @Prop({
    validator: (value: string) =>
      ["external", "internal"].includes(value)
  })
  private readonly type!: string;

  @Prop({ type: String })
  private readonly link!: string;
}
</script>

Above is a Vue component where it will render a link. I have stripped out anything not relevant to the problem (I.e. rel, target, class, etc.).

Understanding - My understanding of Vue Router is that <router-link to="/about">About</router-link> and <a href="/about">About</a> will both render as<a href="/about">About</a> in the DOM, with a difference being the <router-link> version will give the link the SPA functionality (I.e. doesn't load a new page, it dynamically renders a component).

Expected - When type="internal", it will render the <router-link> version. When type="external", it will render the <a> version.

<site-link type="external" link="https://stackoverflow.com">Stack Overflow</site-link>

Will render

<a href="https://stackoverflow.com">Stack Overflow</a>
<site-link type="internal" link="/about">About</site-link>

Will render

<router-link to="/about">About</router-link>

Which is then handle by VueRouter to render

<a href="/about">About</a>

Actual - When type="internal", a <a> with no href rendered in the DOM. When type="external", it renders as expected.

<site-link type="external" link="https://stackoverflow.com">Stack Overflow</site-link>

Will render

<a href="https://stackoverflow.com">Stack Overflow</a>
<site-link type="internal" link="/about">About</site-link>

Will render

<router-link to="/about">About</router-link>

Which is then handle by VueRouter to render

<a>About</a> <!-- Notice there is no href -->

Any ideas how I can achieve what I want?

Barbarism answered 31/3, 2020 at 14:31 Comment(4)
Why can't you use router-link for both?Thanet
I attempted that suggestion, always using router-link for both internal and external links and it seems, for external links, it prefixes localhost:8080 (or whatever the root URL is) the link that is passed as a prop.Barbarism
Please check out my proposed solution below and let me know if it helps?Thanet
OK thank you. Let me give it a whirl.Barbarism
P
1

The official documentation includes an example of how to do this now.

Their approach is to create a custom template that handles external links.

Pyrosis answered 28/7, 2022 at 20:29 Comment(0)
E
5

The better and cleaner approach:

  <router-link v-if="type === 'internal' :to="link">
    <slot />
  </router-link>
  <a v-else :ref="link"> <slot /> </a>

You can use v-if in root element so it solves you the case

Or may be you just missed path part?

  <component
    :is="type === 'internal' ? 'router-link' : 'a'"
    :to="type === 'internal' ? { path: link } : null"
    :href="type !== 'internal' ? link : null"
  >
    <slot />
  </component>
Exo answered 31/3, 2020 at 14:37 Comment(6)
This was going to be my second approach, however my other properties, such as rel, target, class, etc. would be duplicated between them. I was trying not to duplicate properties.Barbarism
Updated answer, check nowExo
Tried the second option, doesn't seem to render anything now.Barbarism
This is a really good alternative solution with the only downfall being duplicating properties, such as rel, class and target in the <router-link> and again in the <a>. If a clean method can be suggested where properties are not duplicated, I'd be happy to make this the accepted answer.Barbarism
@JDomleo It would've been helpful if you'd add the expected output for both "internal" and "external" anchors, e.g. which one to take which attributes (or if it's okay to make them both have everything). I'm assuming that these stripped out attributes are meant for the external link, but I wasn't quite sure.Thanet
@YomS. Please see revised question with updated expected and actual code examples.Barbarism
T
1

...with a difference being the <router-link> version will give the link the SPA functionality (I.e. doesn't load a new page, it dynamically renders a component).

By not loading a new page, I suppose you mean it does not reload the page. And yes, it doesn't because the onclick handler actually gets assigned to a function that does a preventDefault (preventing page redirection) while pushing a new entry into the history stack.

If you take a look at the API reference, the most noticeable thing <router-link> does for you is it toggles between the active-classes depending on the active/current route.

So, that being said, you could do your dynamic <a>nchor rendering inside the default slot via the v-slot; because at this point, the href slot prop would have been a resolved URL which you can then safely bind to the DOM href attribute.


Edit

Added an example (untested).

<router-link
  :to="link"
  v-slot="{ href, route, navigate, isActive, isExactActive }">
  <a
    :class="isActive ? 'your-custom-class' : 'anything'"
    :href="type !== 'internal' ? href : 'javascript:void(0);'"
    @click="customNavigate(navigate.bind($event))">
    <slot></slot>
  </a>
</router-link>

Where the customNavigate handler could be something like:

{
  methods: {
    customNavigate(navigate) {
      if (this.type === 'internal') {
        navigate();
        return false;
      }
    }
  }
}

You can basically add any attributes on the in-component anchor tag based on the slot props, like navigating in certain ways, adding special classes, depending on your use case.

Thanet answered 31/3, 2020 at 15:17 Comment(4)
Hmm I'm not sure I've understood this correctly. Could you possibly an example from the code in the question? I have attempted the above suggestion, but I found it difficult. Not sure if it's because I've misunderstood.Barbarism
@JDomleo Sure, gimme a min.Thanet
Thanks @Yom S. This is the closest I'm looking for. However, not exactly.Barbarism
@JDomleo Since we both agree that <router-link> resolves to <a>, is there any specific reason why we can't jump straight to the final output (<a>)? Isn't the link/href the essential part here?Thanet
P
1

The official documentation includes an example of how to do this now.

Their approach is to create a custom template that handles external links.

Pyrosis answered 28/7, 2022 at 20:29 Comment(0)
B
0

Your code is correct, but it contains one fatal bug:

:href="type !== 'internal' ? link : null"

This line is saying "If type is internal, set the href prop as null".

The router-link component converts the to prop to a string, which is then binded as the href attribute, but it cannot set the resolved href attribute because you are already setting it to null.

One solution is to bind only the necessary attributes:

const params = computed(() => {
    if (type === "internal") {
        return {
            to: props.to,
        };
    }

    return {
        href: props.href,
    };
})
<component
    :is="type === 'internal' ? 'router-link' : 'a'"
    v-bind="params"
>

Hope that helps!

Brothel answered 25/5, 2024 at 11:22 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.