vue
vue copied to clipboard
[Feature] API for rendering VNodes to string
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
@yyx990803
The feature request text was too large to do through the issue helper
Sorry about that, the system isn't perfect. Thanks for understanding :)
This is pretty much a duplicate of your own issue: https://github.com/vuejs/vue/issues/9205#issuecomment-454261156
I also trying to render Vnode but in the browser so SSR is not an option here.
@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
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.
yeah, but the API is already there internally, it just needs to be exposed
@posva will vue 3 have a feature like this, since they are opening up to a custom renderer API?
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.
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.
+1
Any news?
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 VNode
s and then grab node contents.
We have a need for this as well when needing to dynamically render content for google map's info window objects.
Any updates on this?
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);
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>
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.vueNote 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
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. 🤯
@AlbertMarashi this is done and in vue js prod?
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