How to generate vue-router links with dynamic HTML in VueJS (v-html)
So you have some HTML generated by an API or a Markdown parser that you need to render with v-html
.
In this HTML there are some links <a href="...">...</a>
. Unfortunately, these links are not handled by vue-router
as they are "normal" <a>
elements, not <router-link>
elements, which leads to a full-page refresh when one of them is clicked instead of a smooth Single-Page App transition.
Here is how to delegate these links in dynamic HTML to vue-router
in order to provide a seamless experience.
First we need to create a function that takes as input an HTML element, parses all the links, and, if they are internal links, adds an onclick
function that will use vue-router
to navigate to the target.
linkify.ts
export default function linkify(element: HTMLElement) {
const links = element.getElementsByTagName('a');
Array.from(links).forEach((link: HTMLAnchorElement) => {
if (link.hostname == window.location.hostname) {
// ignore if onclick is already set
// e.g. RouterLink
if (link.onclick) {
return;
}
link.onclick = (event: MouseEvent) => {
const { altKey, ctrlKey, metaKey, shiftKey, button, defaultPrevented } = event;
// ignore with control keys
if (metaKey || altKey || ctrlKey || shiftKey) {
return;
}
// ignore when preventDefault called
// e.g. if it's a router-link
if (defaultPrevented) {
return;
}
// ignore right clicks
if (button !== undefined && button !== 0) {
return;
}
// ignore if `target="_blank"`
const linkTarget = link.getAttribute('target');
if (linkTarget && /\b_blank\b/i.test(linkTarget)) {
return;
}
let url = null;
try {
url = new URL(link.href);
} catch (err) {
return;
}
const to = url.pathname;
// ignore same page links with anchors
if (url.hash && window.location.pathname === to) {
return;
}
event.preventDefault();
$router.push(to);
}
}
});
}
To use this function, you first need to get a reference to an HTML element using Vue's ref
.
Here is an example with a custom wrapper for HTML for v-html
(let's call it my_vhtml
):
my_vhtml.vue
<template>
<div ref="component" v-html="html" />
</template>
<script lang="ts" setup>
import linkify from '@/libs/linkify';
import { onMounted, ref, type PropType, type Ref, watch, nextTick } from 'vue';
import { useRouter } from 'vue-router';
// props
const props = defineProps({
html: {
type: String as PropType<string>,
required: true,
}
});
// events
// composables
const $router = useRouter();
// lifecycle
onMounted(() => linkify(component.value!, $router));
// variables
const component: Ref<HTMLElement | null> = ref(null);
// computed
// watch
watch(() => props.html, () => {
nextTick(() => linkify(component.value!, $router));
});
</script>
Finally, here is how to use it in you components:
<template>
<!-- ... -->
<my-html :html="htmlFromApi" />
<!-- ... -->
</template>
<script lang="ts" setup>
import MyHtml from '@/ui/components/my_html.vue';
// ...
<script>
And voilĂ ! All your links within your dynamic HTML are now handled by vue-router
to provide a smooth and pleasant experience to your users.