Using Slot Text Content in Vue 3

Published Monday, October 19th 2020 · 2min read

Vue 3 is out and while it’s not quite ready for every project just yet, I’ve been upgrading some recently started projects to it and so far the experience has been mostly fine. I have come across something, however, that I didn’t really find documented in the migration guide and which threw me off for a moment.

Changes in Slot Structure

More than once I have used something like the following in order to get just the text-content of a <slot />:

<template>
  <button class="icon-button">
    <MyIconComponent>{{ $slots.default[0].text }}</MyIconComponent>
  </button>
</template>

Like it’s mentioned in the migration guide, slots now have to be accessed using functions—so far so good. However, there were some changes to the V-Node structure as well, which means that $slots.default()[0].text would always return undefined. Instead, the content of a V-Node is now located in a property called children.

If the slot only contains text, or a single node, $slots.default()[0].children will return the text content. The problem, however, arises if you have a nested node structure within the slot, because children will then be an array of V-Nodes and we just want text content.

The Solution

Since Vue 3 is so fresh, there really wasn’t anything I could find online on the topic, except an old question on StackOverflow—which pointed me in the right direction. The mentioned example is in the Vue 2 docs, but has already also been ported to the Vue 3 docs.

It’s a little more verbose than just writing $slots.default[0].text, but also more robust (since it actually can handle the entire slot, not just the first node within it. Since I plan on reusing it throughout my application whenever the need arises, I put it into its own little module:

// @/assets/js/getSlotTextContent.js
// Takes a Slot and returns all its content as plain text
export default function getSlotTextContent(children) {
  return children
    .map((node) => {
      if (typeof node.children === 'string') return node.children;
      if (Array.isArray(node.children)) return getSlotTextContent(node.children);
      return '';
    })
    .join('');
}

Now I can just import it in my <script></script> block for my components and get the text content more or less like I’m used to:

<template>
  <button class="icon-button">
    <MyIconComponent>{{ slotText }}</MyIconComponent>
  </button>
</template>

<script>
import '@/assets/js/getSlotTextContent';

export default {
  computed: {
    slotText() {
      return this.$slot.default && getSlotTextContent(this.$slot.default());
    },
  },
}
</script>

Conclusion

Would all of this be less of an issue if I just passed the icon (in this case) via a prop? Yes. But sometimes I have components that I want to behave more like native HTML elements, while still not allowing HTML within them—maybe that’s just me, but in any case, I hope this solution can be useful to someone else out there! 😊

Vue 3 is a very interesting release, but as with any new major version, there’s a bit of a learning curve. Like how the new <teleport> component is awesome, yet has caveats when working with scoped styles—but that’s a topic for another article.

Thank you for reading!