Skip to content

[Feature] API for rendering VNodes to string #10183

@AlbertMarashi

Description

@AlbertMarashi

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions