I have read a number of discussions on this and think it's worthwhile to raise this as an enhancement.
The following esults in each element rendering as an <li> with appropriate list styling but acting like a link. The problem is that if one of the links is to an external site it fails.
<template>
<nav class="navs">
<ul class="nav">
<!-- -->
<router-link
class="nav__item nav__link"
tag="li"
active-class="-active"
:to="nav.link"
:key="nav.name"
exact
v-for="nav in navs">
{{ nav.name }}
</router-link>
</ul>
</nav>
</template>
It's possible to convolute the logic with something like the following. It requires directly inserting the <a> elements but works given minor changes to CSS or always inserting links inside <li> elements (even though if there are no external links it can be coded using <router-link tag="li" ...>).
<ul>
<li v-for="nav in navs">
<a v-if="nav.external" class="nav__item nav__link" :href="nav.link">{{ nav.name }}</a>
<router-link v-else
class="nav__item nav__link"
active-class="-active"
:to="nav.link"
:key="nav.name"
exact
>{{ nav.name }}</router-link>
</li>
</ul>
I previously tried just inserting external URLs into the <router-link> elements and handling them in router.beforeEach(). It's possible to apply some heuristic checks to see if the URL is external and set window.location.href if it is. But the problem I ran into then is that using the browser back button always resulted in an error because the history is stored as localhost:8080/http:/google.com (when testing with "http://google.com" as the external link).
It seems that that programmatically building lists of links is a common problem and that there should be a straightforward solution. Two solutions that would help me are 1) allowing the "external" prop on <router-link> elements that correctly stores history so the back button works or 2) provide some way in router.beforeEach() to prevent storing the unusable history entry. I think the first, or some variation where the externality of a link specified in the element, is the most straightforward.
I guess another solution is a strong recommendation in documentation on how to handle this seemingly common situation.
Thanks for considering it.
Same problem here to handle 301 redirection.
What does 301 redirection have to do with <router-link>?
Hi, relatively new Vue user here. We’re porting an existing React app to Vue using Nuxt and got tripped up on this: https://github.com/nuxt/nuxt.js/issues/770
Our existing app uses react-router and Express middleware to handle a number of legacy URLs, some of which redirect internally, and some of which redirect to external (absolute) URLs.
Digging through the source code of vue-router and nuxt, it looks like the issue is split between the two. Nuxt mangles URLs on the server side (before setting a Location header), and vue-router mangles URLs on the client side (before sending to HTML5 history).
It looks like the Nuxt folks are interested in handling this on their side (which would affect the server implementation). I’d love to see this handled in vue-router for the client side.
+1 here. It is quite common to implement dashboard of management system as a SPA while rendering pages in frontend in traditional way with Vue.js just as a convenient template engine. In this case, <navbar> problem described by OP could be annoying.
I'd like to see this in the configuration of a Router instance, so that I can have a named route pointing to an external site. In particular, I would like this since I am replacing an existing site, redirecting unimplemented pages to the original site, and this would prevent having to update every anchor tag with a the router-link once the page is implemented.
Indeed. The CMS for which I'm building a frontend has no distinction between internal or external links: it just stores the URL in a text field. This forces me to simply use <a> tags in places where I'd rather use <router-link>, because the template logic otherwise gets quite convoluted.
So, router links should ideally, with the same tag signature, handle:
I second this. Given a router definition, it would be pretty standard to programmatically check the link host against the window location. It's not that hard to implement manually, but requests mangling a bit with the tooling and lifecycle (first creating links to test, then replacing them by router-links). Would be better if avoidable.
Here is how this currently can be handled:
<component :is="el(item.href)"
:to="item.href" :href="item.href" class="item">
</component>
where
el(url) {
return _.startsWith(url, 'http') ? 'a' : 'router-link';
},
The test in el can of course be improved.
Neat ! I didn't know that "is" attribute could also handle HTML tags :)
This is how I solve it right now
{
path: '/twitter',
beforeEnter() { location.href = 'http://twitter.com' }
},
It's considerable that we may need a way to handle subdomains routes which don't necessarily change main Vue instance.
Similar to @balint42 this is what I came up with - I also make external links open in an external window with rel="noopener" for improved security (some info: https://mathiasbynens.github.io/rel-noopener/)
...
methods: {
linkProps (url) {
if (url.match(/^(http(s)?|ftp):\/\//)) {
return {
is: 'a',
href: url,
target: '_blank',
rel: 'noopener'
}
}
return {
is: 'router-link',
to: url
}
}
}
...
Your dynamic component can be nice and simple using v-bind
<component v-bind="linkProps('http://bulma.io/')"></component>
Or as a reusable component:
AppLink.vue
<template>
<component v-bind="linkProps(to)">
<slot></slot>
</component>
</template>
<script>
export default {
props: {
to: {
type: String,
required: true
}
},
methods: {
linkProps (url) {
if (url.match(/^(http(s)?|ftp):\/\//)) {
return {
is: 'a',
href: url,
target: '_blank',
rel: 'noopener'
}
}
return {
is: 'router-link',
to: url
}
}
}
}
</script>
SomeComponent.vue
<template>
<app-link to="https://router.vuejs.org/">Vue Router Docs</app-link>
</template>
<script>
import AppLink from './AppLink'
export default {
components: { AppLink }
}
</script>
Like @LFP6 I'm working on a new Vue Single Page Application that's taking over parts of the old website page by page. It goes without saying that it's much tidier to have named routes for the old, outside the SPA pages rather than litter the codebase with multiple links to it. This also makes the job of switching the URL to use the new SPA page setup much simpler as each page is migrated through.
I imagine this is a relatively common setup, integrating a new Vue SPA page by page whilst the old site remains.
Something like this would be great (not real code):
{
path: 'https://www.fulldomain.com/first-old-page',
name: 'first-old-page',
type: 'external',
}
Based on @silverbackdan's work above, I've tweaked it to allow me to set named routes for external links. This allows me to keep things DRY so I only say what each route is pointing to once (e.g. plans page) and then when I come to move that page over into the app I just remove it from externalRoutes and pop it into the normal routes setup with the same name.
<template>
<component v-bind="linkProperties(to)">
<slot></slot>
</component>
</template>
<script>
export default {
props: {
to: {
required: true,
},
},
data() {
return {
externalRoutes: [
{
name: 'an-old-page',
url: 'https://domain.com/old-page',
}, {
name: 'another-old-page',
url: 'https://sub.domain.com/another-old-page',
},
],
};
},
methods: {
linkProperties(route) {
const routeName = route.name ? route.name : route;
const externalRoute = this.externalRoutes.filter(r => r.name === routeName)[0];
let url = externalRoute ? externalRoute.url : routeName;
if (externalRoute || url.match(/^(http(s)?|ftp):\/\//)) {
if (route.query) {
url = `${url}?${$.param(route.query)}`;
}
return {
is: 'a',
href: url,
};
}
return {
is: 'router-link',
to: { name: url, query: route.query },
};
},
},
};
</script>
And used like so:
<app-link :to="routeName">text</app-link>
Or if preferring not to put a named route in for all external URL's, also accepts full URL's:
<app-link to="https://my-hard-coded.com/custom-link">text</app-link>
Edit: Have updated to handle query strings so you can use them in the same way as <router-link> (e.g. <app-link :to="{name: 'yourNamedRouteOrUrl', query: {foo: 'bar'}}"></app-link>.
@silverbackdan & @fredrivett I couldn't get @fredrivett working due to $ in $.param is not defined error so tried @silverbackdan which is all linked up and hits a breakpoint returning the tag ok but then I get a debugger error that I'm struggling to Google help on:
[Vue warn]: Failed to mount component: template or render function not defined.
vue.esm.js:591
found in
---> <Anonymous>
<AppLink> at src\components\AppLink.vue
<Footer> at src\components\PageWithAppLinkElement.vue
<App> at src\App.vue
<Root>
My AppLink.vue is pretty identical to @silverbackdan only changed v-bind to v-bind:is (compiler insisted) and made compliant with my linter:
<template>
<component v-bind:is="linkProps(to)">
<slot></slot>
</component>
</template>
<script>
export default {
props: {
to: {
type: String,
required: true,
},
},
methods: {
linkProps(url)
{
if (url.match(/^(http(s)?|ftp):\/\//))
{
return {
is: 'a',
href: url,
target: '_blank',
rel: 'noopener',
};
}
return {
is: 'router-link',
to: url,
};
},
},
};
</script>
Can you advise where I've gone wrong?
~@9swampy you don't have a template field on your export. That exactly what the error states.
Failed to mount component: template or render function not defined.~
Ignore me @silverbackdan is right you should be using v-bind.
@OmgImAlexis thx for that but could you explain further pls: it's a single file component and the template is declared at the top. Same as both @silverbackdan & @fredrivett have in their snippets. Why and how would I need to declare another template field on the export?
@9swampy you have v-bind:is instead of v-bind as your dynamic component attribute. The is attribute is returned from the linkProps method which either returns that the dynamic component is an a HTML tag or a router-link component in your snippet.
Submitted a pull request to support external routes.
Live demo:
https://7496x53w10.codesandbox.io/
Interactive example:
https://codesandbox.io/s/7496x53w10
Couldnt we just have a simple prop like 'refresh' on router-link to make the path reload as a new page rather then using the SPA routing?
@kgrosvenor, why you want to reinvent the wheel? After all, you can always use location.reload() if you want to really reload the app.
Fo some reason the below is working. It opens the external url google on a new tab
<q-item
:to="{path: '/google'}"
href="https://google.com"
clickable
target="_blank"
class="GL__menu-link"
>
Fo some reason the below is working. It opens the external url google on a new tab
<q-item :to="{path: '/google'}" href="https://google.com" clickable target="_blank" class="GL__menu-link" >
That is not the <router-link /> component, @apmcodes. :sweat_smile: Maybe a wrapper which uses a normal <a /> when href is not internal.
Whats wrong with the tag?
>
This is how I solve it right now
{ path: '/twitter', beforeEnter() { location.href = 'http://twitter.com' } },
Excellent, works perfectly!!!
I also have a need (or, I guess desire) to have router handle external URLs. In my case, it's when I'm redirecting after login via a successUrl query parameter - I don't have a way to know if that URL is going to be a fully qualified URL or just a path I can send to the router, without doing checking before redirecting. I'd rather have the router handle this so that I don't have to put logic every place I want to redirect.
To solve it, I came up with this solution, which seems to work but I'm not in love with.
In my router definition, I added a beforeEach hook that checks if the destination is a "fully qualified URL", and if so, set the page's location, and if not, call next().
router.beforeEach((to, from, next) => {
const extUrl = to.fullPath.startsWith("/") ? to.fullPath.substring(1) : to.fullPath
if (isExternalUrl(extUrl)) {
window.location.href = extUrl;
return;
} else {
next();
}
})
The function isExternalUrl is just a regex test that looks like this:
export function isExternalUrl(input?: string) {
if (!input || isBlank(input)) { //isBlank is a function that just checks for empty strings, etc
return false;
}
return new RegExp("^(?:(?:https?|ftp):\\/\\/|\\b(?:[a-z\\d]+\\.))(?:(?:[^\\s()<>]+|\\((?:[^\\s()<>]+|(?:\\([^\\s()<>]+\\)))?\\))+(?:\\((?:[^\\s()<>]+|(?:\\(?:[^\\s()<>]+\\)))?\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))?").test(input);
}
I'm not sure if this issue has been fixed since the posts in here, but it seems like vue-router now handles any path with the :// delimiter as an external URL, so this works perfectly for me:
{
name: "twitter",
path: "https://twitter.com/mytwitterhandle",
}
_EDIT: This works for me locally but not having built the files and deployed to prod, not sure why that's the case._
This is how I solve it right now
{ path: '/twitter', beforeEnter() { location.href = 'http://twitter.com' } },
And for anybody trying to open the target link in a new tab - this works for me
{
path: "/twitter",
beforeEnter() {
window.open("http://www.twitter.com", "_blank");
},
},
This is how I solve it right now
{ path: '/twitter', beforeEnter() { location.href = 'http://twitter.com' } },
work just on localhost, not working online
Most helpful comment
This is how I solve it right now