Delegating HTML links to vue-router

When you are dealing with dynamic or user generated content in a Vue.js application, you might want vue-router to handle internal HTML links. Links that are not implemented via <router-link> will trigger a full page reload. So we need a way to hijack clicks on <a href> and delegate them to vue-router in case they reference an internal resource. There are two ways to intercept the clicks, depending on your use case and needs.

Application-wide handling

In case you have dynamic links all over your application you can intercept them globally. To do this, bind the event listener to the window in your top-most/main app components mounted lifecycle hook:

mounted () {
  window.addEventListener('click', event => {
    const { target } = event
    // handle only links that do not reference external resources
    if (target && target.matches("a:not([href*='://'])") && target.href) {
      // some sanity checks taken from vue-router:
      // https://github.com/vuejs/vue-router/blob/dev/src/components/link.js#L106
      const { altKey, ctrlKey, metaKey, shiftKey, button, defaultPrevented } = event
      // don't handle with control keys
      if (metaKey || altKey || ctrlKey || shiftKey) return
      // don't handle when preventDefault called
      if (defaultPrevented) return
      // don't handle right clicks
      if (button !== undefined && button !== 0) return
      // don't handle if `target="_blank"`
      if (target && target.getAttribute) {
        const linkTarget = target.getAttribute('target')
        if (/\b_blank\b/i.test(linkTarget)) return
      }
      // don't handle same page links/anchors
      const url = new URL(target.href)
      const to = url.pathname
      if (window.location.pathname !== to && event.preventDefault) {
        event.preventDefault()
        this.$router.push(to)
      }
    }
  })
}

Encapsulation in a component

If there are only certain places where this kind of link handling must occur, you should encapsulate the event handling in a component. The advantage of this is, that it will be more performant and there is no need to interfere with the rest of the apps links.

The components template might look like this:

<div
  class="dynamic-content"
  @click="handleClicks"
  v-html="dynamicContent"
/>

Here dynamicContent is a html string containing the <a href> links, which the handleClicks method takes care of:

methods: {
  handleClicks ($event) {
    const { target } = $event
    // handle only links that occur inside the component and do not reference external resources
    if (target && target.matches(".dynamic-content a:not([href*='://'])") && target.href) {
      // some sanity checks taken from vue-router:
      // https://github.com/vuejs/vue-router/blob/dev/src/components/link.js#L106
      const { altKey, ctrlKey, metaKey, shiftKey, button, defaultPrevented } = $event
      // don't handle with control keys
      if (metaKey || altKey || ctrlKey || shiftKey) return
      // don't handle when preventDefault called
      if (defaultPrevented) return
      // don't handle right clicks
      if (button !== undefined && button !== 0) return
      // don't handle if `target="_blank"`
      if (target && target.getAttribute) {
        const linkTarget = target.getAttribute('target')
        if (/\b_blank\b/i.test(linkTarget)) return
      }
      // don't handle same page links/anchors
      const url = new URL(target.href)
      const to = url.pathname
      if (window.location.pathname !== to && $event.preventDefault) {
        $event.preventDefault()
        this.$router.push(to)
      }
    }
  }
}

As you can see, this hijacks only a subset of the link clicks – those that occur inside of this component.