vue icon indicating copy to clipboard operation
vue copied to clipboard

[Feature] API for rendering VNodes to string

Open AlbertMarashi opened this issue 5 years ago • 14 comments

What problem does this feature solve?

I've created a <head> management system and an awesome feature would be a native way to render VNodes to strings, both in SSR and on client-side

Here's what I've been currently doing in user-land: hello-world.vue

<template>
    <master>
        <template slot="title">Hello World App</template>
        <template slot="description">Meta description here</template>
        <template slot="content">
            Hello World
        </template>
    </master>
</template>
<script>
import master from '@/layouts/master'

export default {
    components: {
        master
    }
}
</script>

master.vue

<template>
    <servue>
        <template slot="head">
            <meta name="viewport" content="width=device-width, initial-scale=1">
            <title>{{ this.$slots.title ? `${ this.$slots.title[0].text } - My App`: `My App` }}</title>
            <meta v-if="this.$slots.description" name="description" :content="this.$slots.description[0].text">
            <slot name="head"/>
        </template>
        <template slot="content">
            <slot name="content"/>
        </template>
    </servue>
</template>

<script>
import servue from './servue'

export default {
    components: {
        servue
    }
}
</script>

You can probably see some issues with how this is done, it only accounts for a single text node, and there may be more. Plus, it seems hacky to directly access slot data inside a template

The head is currently being stringified by a small component: servue.vue

<script>
const unaryTags = [
    "area",
    "base",
    "br",
    "col",
    "embed",
    "hr",
    "img",
    "input",
    "keygen",
    "link",
    "meta",
    "param",
    "source",
    "track",
    "wbr"
]

function renderStartTag(VNode) {
    let html = `<${VNode.tag}`

    if (VNode.data) {
        if (VNode.data.attrs) {
            let attr = VNode.data.attrs
            for (let name in attr) {
                if (attr[name] === "") {
                    html += ` ${name}`
                } else {
                    html += ` ${name}="${attr[name]}"`
                }
            }
        }
    }

    return html + ">";
}

function isUnaryTag(VNode) {
    return unaryTags.indexOf(VNode.tag) > -1
}

function getFullTag(VNode) {
    if (!VNode.tag) return VNode.text

    let html = renderStartTag(VNode)

    if (VNode.children) {
        html += getChildren(VNode)
    }
    if (!isUnaryTag(VNode)) {
        html += `</${VNode.tag}>`
    }
    return html;
}

function getChildren(VNode) {
    let html = ""
    for (let i in VNode.children) {
        let child = VNode.children[i]
        html += getFullTag(child)
    }
    return html
}
export default {
    created() {
        let VNodes = this.$slots.head
        let renderedHead = ""

        for (let i in VNodes) {
            let VNode = VNodes[i];
            renderedHead += getFullTag(VNode)
        }

        if (this.$isServer) {
            this.$ssrContext.head = `<!--VUESERVEHEAD START-->${renderedHead}<!--VUESERVEHEAD END-->`
        }else{
            let head = document.head
            let node
            let foundStart = false
            let startNode

            let children = head.childNodes

            for(let node of children){
                if(node.nodeType === Node.COMMENT_NODE){
                    if(node.nodeValue === "VUESERVEHEAD START"){
                        foundStart = true
                        startNode = node
                        continue
                    }
                }
                if(foundStart){
                    if(node.nodeType === Node.COMMENT_NODE){
                        if(node.nodeValue === "VUESERVEHEAD END"){
                            break
                        }
                    }
                    head.removeChild(node)
                }
            }

            if(startNode){
                let fakeMeta = document.createElement('meta')
                startNode.after(fakeMeta)

                fakeMeta.outerHTML = renderedHead
            }

        }
    },
    render(h){
        return h('div', {
            class: "servueWrapper"
        }, this.$slots.content)
    }
};
</script>

This whole process could be simplified by an API exposed by vue. The API already exists, it just needs to be exposed by Vue

What does the proposed API look like?

Vue.renderVNodesToString([VNode])
$vm.renderVNodesToString([VNode])

import { renderVNodesToString } from 'vue'

A few ideas

AlbertMarashi avatar Jun 23 '19 05:06 AlbertMarashi

@yyx990803

The feature request text was too large to do through the issue helper

AlbertMarashi avatar Jun 23 '19 05:06 AlbertMarashi

Sorry about that, the system isn't perfect. Thanks for understanding :)

LinusBorg avatar Jun 23 '19 06:06 LinusBorg

This is pretty much a duplicate of your own issue: https://github.com/vuejs/vue/issues/9205#issuecomment-454261156

posva avatar Jun 24 '19 09:06 posva

I also trying to render Vnode but in the browser so SSR is not an option here.

vedmant avatar Jun 24 '19 10:06 vedmant

@posva it's slightly different, it's not for SSR

We need a universal API to render VNodes to a string on the server and the client

What @vedmant said

AlbertMarashi avatar Jun 24 '19 11:06 AlbertMarashi

Sorry, I wasn't referring to the SSR part, that shouldn't change where the conversation went:

It still has to be optional on the client so it doesn't impact other users (rendering vnodes to a string is a niche case itself, even more on the client), like template compilation: it can be used on the client but can be stripped out as well.

posva avatar Jun 24 '19 11:06 posva

yeah, but the API is already there internally, it just needs to be exposed

AlbertMarashi avatar Jun 25 '19 04:06 AlbertMarashi

@posva will vue 3 have a feature like this, since they are opening up to a custom renderer API?

AlbertMarashi avatar Oct 29 '19 10:10 AlbertMarashi

A canonical example for this could be rendering HTML tooltip for V-Tooltip programatically.

So it's not that much of a niche use-case.

bmarkovic avatar Nov 22 '19 14:11 bmarkovic

The Vue integration for ApexCharts would also benefit from this for custom tooltips since it handles this with a callback that must return raw HTML.

6XGate avatar May 04 '20 16:05 6XGate

+1

miltzi avatar May 10 '21 14:05 miltzi

Any news?

lyswhut avatar Aug 30 '22 02:08 lyswhut

This would be very helpful when wrapping vanilla JS libraries that have APIs for HTMLElement / HTML string. I am in this situation now with AG Grid cellRenderer. The only workaround is to try to let Vue render VNodes and then grab node contents.

aentwist avatar Sep 20 '22 20:09 aentwist

We have a need for this as well when needing to dynamically render content for google map's info window objects.

johnwc avatar Jan 05 '23 06:01 johnwc

Any updates on this?

samkevin1 avatar Jun 12 '23 15:06 samkevin1

In case it's helpful for anyone, it is possible to use vue/server-renderer to archive this. I created a StackBlitz based on Nuxt to show how it works: https://stackblitz.com/edit/rendertostring-example?file=app.vue

Note that Nuxt is only the tool for creating a fast showcase, it's not needed at all.

import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';

const app = createSSRApp({
  data: () => {
    return {
      title: 'World',
      text: 'Laborum quisquam et error magni ut eum fugiat. Deserunt quae magni sed voluptatem ducimus consequatur quae aperiam. Recusandae et ducimus hic quis et excepturi officia. Voluptatem facilis ut quis velit consequatur. Id aspernatur quis est animi eaque corrupti. Iure quod quae nulla omnis molestiae tempora aut voluptatibus.',
    };
  },
  computed: {
    getTitle() {
      console.log('getTitle');
      return this.title;
    },
  },
  template: `
        <div>
          <h1>Hello {{ getTitle }}</h1>
          <p>{{ text }}</p>
        </div>`,
});
let htmlString = await renderToString(app);

martinschilliger avatar Jun 14 '23 07:06 martinschilliger

Simple example with import { renderToString } from 'vue/server-renderer'

<script setup lang="ts">
import { renderToString } from 'vue/server-renderer'
import { h } from 'vue'

const htmlString = await renderToString(h('div', 'Hello world'))
</script>

<template>
  <span v-html="htmlString" />
  {{ htmlString }}
</template>


kedniko avatar Jun 14 '23 08:06 kedniko

In case it's helpful for anyone, it is possible to use vue/server-renderer to archive this. I created a StackBlitz based on Nuxt to show how it works: https://stackblitz.com/edit/rendertostring-example?file=app.vue

Note that Nuxt is only the tool for creating a fast showcase, it's not needed at all.

import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';

const app = createSSRApp({
  data: () => {
    return {
      title: 'World',
      text: 'Laborum quisquam et error magni ut eum fugiat. Deserunt quae magni sed voluptatem ducimus consequatur quae aperiam. Recusandae et ducimus hic quis et excepturi officia. Voluptatem facilis ut quis velit consequatur. Id aspernatur quis est animi eaque corrupti. Iure quod quae nulla omnis molestiae tempora aut voluptatibus.',
    };
  },
  computed: {
    getTitle() {
      console.log('getTitle');
      return this.title;
    },
  },
  template: `
        <div>
          <h1>Hello {{ getTitle }}</h1>
          <p>{{ text }}</p>
        </div>`,
});
let htmlString = await renderToString(app);

@martinschilliger @kedniko Those work because you are pre-rendering upon compile time, not runtime. SSR functionality is not published with vue js browser runtime.

Example: https://stackblitz.com/edit/rendertostring-example-tn5hyf?file=app.vue

image

johnwc avatar Jun 14 '23 16:06 johnwc

Ah, I see. I use it in runtime, but in node on the server side. Sorry, thought I had the solution, but I was wrong. 🤯

martinschilliger avatar Jun 14 '23 17:06 martinschilliger

@AlbertMarashi this is done and in vue js prod?

johnwc avatar Nov 16 '23 04:11 johnwc

No I just don't use Vue anymore @johnwc and I haven't been keeping up with it. Feel free to re-open the issue

AlbertMarashi avatar Nov 16 '23 07:11 AlbertMarashi