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.

1 email / week to learn how to (ab)use technology for fun & profit: Programming, Hacking & Entrepreneurship.
I hate spam even more than you do. I'll never share your email, and you can unsubscribe at any time.

Tags: programming, webdev, vuejs

Want to learn Rust, Cryptography and Security? Get my book Black Hat Rust!