19

I'm using a my-link component to wrap an anchor tag on demand around various items. For that purpose a custom render method is used - however the createElement method can only create HTML nodes, creating plain text nodes does not seem to be possible.

Current scenario

Usage of my-link component

<template v-for="item in items">
  <h4>
    <my-link :url="item.url">{{ item.text }}</my-link>
  </h4>
</template>

Implementation of my-link component as Link.vue

<script>
export default {
  name: 'my-link',
  props: { url: String },
  render(createElement) {
    if (this.url) {
      return createElement(
          'a', {
            attrs: { href: this.url }
          }, this.$slots.default
      );
    }

    return createElement(
        'span',
        this.$slots.default
    );
  }
};
</script>

Resulting HTML

<h4>
  <a url="/some-link">This item is linked</a>
</h4>
<h4>
  <span>Plain text item</span>
</h4>

Desired scenario

The span tag in this particular scenario is superfluous and could be avoided - however, it's not clear to me, how and whether at all this is possible with Vue.js. In general, I'd like to know how to create plain text nodes using a custom render method.

<h4>
  <a url="/some-link">This item is linked</a>
</h4>
<h4>
  Plain text item
</h4>

Side-notes:

  • question was originally raised for VueJS v2.1 & v2.2-beta (February 2017)
  • focus was on proper semantic nodes (text node vs. element node)
5
  • Hey just did some research, ultimately createElement will call document.createElement. So although there may be other ways to do what you are needing, you should always have a tag name as sending null will have random affects developer.mozilla.org/en-US/docs/Web/API/Document/createElement hope that helps Commented Feb 23, 2017 at 13:11
  • 1
    Not sure about the advisability of using it, but Vue exposes a _v method which is an alias for createTextVNode. You can write return this._v("some text") from a render function. Your second issue would be determining if the default slot contained only a text node and extracting the text from it. codepen.io/Kradek/pen/xqxgza?editors=1010 Commented Feb 23, 2017 at 17:00
  • @Austio Thanks for the research on that, I'm pretty sure as well this might have side-effects for different browser vendors. createElement(null) created an empty VNode, but I could not find a way to properly wrap that in that scope. Commented Feb 23, 2017 at 20:01
  • @BertEvans Perfect! this._v(getChildrenTextContent(this.$slots.default)) works like a charm, using an text extraction method similar to vuejs.org/v2/guide/render-function#Complete-Example. Please consider adding this as an answer here, with the remark of _v() probably being part of the internal API. Thanks! Commented Feb 23, 2017 at 20:20
  • @OliverHader sure thing :) Commented Feb 23, 2017 at 22:08

8 Answers 8

24

Vue exposes an internal method on it's prototype called _v that creates a plain text node. You can return the result of calling this method from a render function to render a plain text string:

render(h){
    return this._v("my string value");
}

Exposing it in this way, prefixed with an underscore, likely indicates it's intended as a private API method, so use with care.

If you use a functional component, "this" is also not available. In this case, you should call context._v(), for example:

functional: true,
render(h, context){
    return context._v("my string value")
}

This, combined with extracting the text from the slot (as in your comment, using the helpful getChildrenTextContent) will produce the desired result.

Sign up to request clarification or add additional context in comments.

2 Comments

@AlainDuchesneau It works in 2.5. codepen.io/Kradek/pen/xYJNPY?editors=1010
_v() is a one-liner: return new VNode(undefined, undefined, undefined, String(val))
2

You can actually get around having to use the _v method mentioned in answers above (and potentially avoid using an internal Vue method that might get renamed later) by changing your implementation of the the MyLink component to be a functional component. Functional components do not require a root element, so it would get around having to put a span around the non-link element.

MyLink could be defined as follows:

const MyLink = {
  functional: true,
  name: 'my-link',
  props: { url: String },
  render(createElement, context) {
    let { url } = context.props
    let slots = context.slots()
    if (url) {
      return createElement(
          'a', {
            attrs: { href: url }
          }, slots.default
      );
    }
    else {
      return slots.default 
    }
  }
};

Then to use it, you could do something like this in a different component:

<div>
  <h4 v-for="item in items">
    <my-link :url="item.url">{{ item.text }}</my-link>
  </h4>
</div>

See: https://codepen.io/hunterae/pen/MZrVEK?editors=1010

Also, as a side-note, it appears your original code snippets are naming the file as Link.vue. Since you are defining your own render function instead of using Vue's templating system, you could theoretically rename the file to Link.js and remove the beginning and closing script tags and just have a completely JS component file. However, if you component includes custom systems (which your snippet did not), this approach will not work. Hope this helps.

1 Comment

the use of functional component is definitely the way to go.
2

In case someone comes across this question and is currently using Vue 3: the API has changed. You currently have 2 options:

  • install @vue/compat to access _v() as in Vue 2
  • otherwise, opt for createTextVNode() instead:
import { createTextVNode, VNode } from 'vue';

export const renderMoney = (data: number): VNode =>
  createTextVNode(data.toLocaleString('en'));

In this example (Typescript), you can pass in a number and get a plain text VNode back.

Comments

1

Found a few workarounds if the answer above doesn't work
You can create the VNode object and set the text:

render(createElement) {
  const vNode = createElement();
  vNode.text = 'some text';
  vNode.isComment = false;
  return vNode;
}

Or return the first child, which is not ideal:

render(createElement) {
  return createElement('p', 'some text').children[0];
}

Comments

0

It's actually very simple to do this without needing to use any private methods.

A VNode which just renders text has all properties undefined, expect for text.

So you can just

return { text: 'my text' };

If you're using TypeScript, you may need to assert it is a VNode.

Comments

0

Vue3 provides two symbols Fragment and Text.

import { Fragment, Text } from '@vue/runtime-core'

h(Fragment, [
  h(Text, 'text1'),
  h(Text, 'text2')
])

Comments

-4

In Vue 2.5.x, you can use this to render text nodes:

render(createElement) {
    return createElement(() => {
        return document.createTextNode('Text');
    });
}

1 Comment

This definitely doesn't work. The render function should return a vNode.
-4

You need a root element for a component. In your case, maybe you can use 'h4' as the root element since I see you include that anyway in the v-for loop. Once you have done that, you can then create a text node like this.

return createElement(
    'h3',
    {},
    ['Plain text item']
);

According to Vue documentation, the third argument of createElement could be string or array, if it is a string it will be converted to text node.

https://v2.vuejs.org/v2/guide/render-function.html#createElement-Arguments

2 Comments

That was exactly the question how to avoid that - the original example already shows that by using a span tag (which is quite similar to your h4 suggestion).
This fails to answer the question, also there is a way to achieve it with functional components.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.