Is it possible to use <nuxt-link> in content rendered with v-html?
24th March 2024 • 6 min read — by Aleksandar Trpkovski
If you've used Vue or Nuxt, you're likely familiar with the v-html directive. This is used to dynamically insert HTML content into an element. It takes a string value containing HTML tags and text, replacing the inner HTML of the target element with this content.
One limitation of v-html is that it does not allow you to render <nuxt-link>. Instead, all <a> HTML tags do not use the native Vue router.
However, we can address this limitation by writing our own directive. This will allow all <a> tags to be converted into <nuxt-link>.
Thanks to Nuxt and its plugin system, we can easily expand the functionality of Nuxt. Let's get started.
Create a Nuxt plugin
Within the root of your Nuxt application, create a /plugins directory. In this directory, create a file named nuxt-html.ts. The following logic will be added to this file:
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.directive("nuxtHtml", {
mounted(el: HTMLElement, binding) {
el.innerHTML = assignAnchorsIds(binding.value);
convertAnchorToNuxtLink(el);
},
updated(el, binding) {
el.innerHTML = assignAnchorsIds(binding.value);
convertAnchorToNuxtLink(el);
},
unmounted() {
removeListeners();
},
});
});
Let's break down the code above to understand each part:
defineNuxtPlugin: This function creates a Nuxt plugin, making its features available throughout the application.nuxtApp.vueApp.directive: This registers a custom Vue directive callednuxtHtml. We can usev-nuxt-htmllater in the.vuefile.
Directive Lifecycle Hooks:
mounted: Executes when the element using the directive is inserted into the DOM.updated: Executes when the value bound to the directive changes.unmounted: Executes when the element is removed from the DOM.
Next, let's examine the key functions we'll use within our directive: assignAnchorsIds, convertAnchorToNuxtLink, and removeListeners. We'll add these functions in the nuxt-html.ts, above the plugin definition.
const idsWithListeners = new Set<string>();
function generateUuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0,
v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
function assignAnchorsIds(html: string) {
const domParser = new DOMParser();
const doc = domParser.parseFromString(html, "text/html");
const anchors = doc.querySelectorAll("a");
anchors.forEach((anchor) => {
if (!anchor.hasAttribute("id")) {
anchor.setAttribute("id", generateUuid());
}
});
return doc.documentElement.outerHTML;
}
function convertAnchorToNuxtLink(html: HTMLElement) {
const anchors = html.querySelectorAll("a");
anchors.forEach((anchor) => {
if (anchor.id && !idsWithListeners.has(anchor.id)) {
const link = document.getElementById(anchor.id);
link?.addEventListener("click", (event) => {
event.preventDefault();
const to = anchor.getAttribute("href");
if (to) {
useNuxtApp().$router.push(to);
}
});
// Add the ID to the Set
idsWithListeners.add(anchor.id);
}
});
}
function removeListeners() {
idsWithListeners.forEach((id) => {
const link = document.getElementById(id);
link?.removeEventListener("click", () => {});
idsWithListeners.delete(id);
});
}
Now, let's delve into the specifics of each function and understand how they contribute to the overall functionality of the directive.
assignAnchorsIds:- Parses the provided HTML string into a DOM document.
- Identifies anchor elements (
<a>) without IDs. - Assigns unique IDs (using
generateUuid) to those anchors for tracking and linking. - Returns the modified HTML string.
convertAnchorToNuxtLink:- Finds anchor elements within a given HTML element.
- Identifies anchors with IDs that haven't been handled yet.
- Attaches click event listeners to those anchors:
- Prevents default browser behaviour (page reload).
- Retrieves the anchor's
hrefattribute. - Utilises Nuxt's
$routerto programmatically navigate to the specified route, effectively converting basic anchors into Nuxt-powered navigation links.
removeListeners:- Iterates through the IDs of anchors with event listeners.
- Removes the click event listeners to prevent memory leaks or unintended behaviour.
Using the v-nuxt-html directive
Now, we can use our v-nuxt-html as follows in our .vue file:
<script setup lang="ts">
const htmlString = ref(`
<h1>Example Heading</h1>
<p>Example Paragraph</p>
<a href="/example-1">Example 1</a>
<a href="/example-2">Example 2</a>
<a href="/example-3">Example 3</a>`);
onMounted(() => {
setTimeout(() => {
htmlString.value = `
<h1>Example Heading</h1>
<p>Example Paragraph</p>
<a href="/example-1">Example 1</a>
<a href="/example-2">Example 2</a>
<a href="/example-3">Example 3</a>
<a href="/example-4">Example 4</a>`;
}, 4000);
});
</script>
<template>
<div>
<div v-nuxt-html="htmlString"></div>
</div>
</template>
Alright, let's break down the code above piece by piece, so we can get a good grasp on how everything fits together.
- Dynamic HTML Content: The code defines a
refnamedhtmlStringthat holds HTML content as a string. This allows for dynamic updates to the displayed HTML. - Initial Render:
- The template section includes a
divelement containing anotherdivwith thev-nuxt-htmldirective applied. - When the component mounts, the initial value of
htmlString(defined in thescript setupsection) is used. - The
v-nuxt-htmldirective intercepts this and performs the following actions:- It parses the HTML string using the
assignAnchorsIdsfunction. This ensures any anchor (<a>) elements without IDs are assigned unique ones. - It then calls the
convertAnchorToNuxtLinkfunction. This function finds the anchors within the parsed HTML and attaches click event listeners to them. - When an anchor with a
hrefattribute is clicked, the event listener:- Prevents default behavior (stopping the browser from navigating away from the current page).
- Extracts the
hrefattribute value (e.g.,/example-1). - Uses Nuxt's router (
$router) to programmatically navigate to the specified route, effectively turning the anchor into a Nuxt link.
- It parses the HTML string using the
- The template section includes a
- Dynamic Update: After a 4-second delay using
setTimeout, thehtmlStringis updated with a new string containing an additional anchor element (<a href="/example-4">Example 4</a>). - Re-rendering and Event Listener Management:
- The component re-renders due to the change in
htmlString. - The
v-nuxt-htmldirective again parses and processes the updated HTML. - Importantly, the directive's
unmountedlifecycle hook ensures any previously attached event listeners are removed using theremoveListenersfunction. This prevents memory leaks and unintended behavior when the HTML content changes.
- The component re-renders due to the change in
Conclusion
Using the v-html directive in a Nuxt application has a limitation - it doesn't render <nuxt-link>. You can overcome this by creating a custom Vue directive named v-nuxt-html. This directive helps convert all <a> HTML tags into <nuxt-link> behaviour, enabling dynamic, interactive HTML content in a Nuxt application. The code for this is available in the following GitHub repository here.
Further Reading
Explore more articles that might interest you.