Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] API for rendering VNodes to string #10183

Closed
AlbertMarashi opened this issue Jun 23, 2019 · 21 comments
Closed

[Feature] API for rendering VNodes to string #10183

AlbertMarashi opened this issue Jun 23, 2019 · 21 comments

Comments

@AlbertMarashi
Copy link

AlbertMarashi commented Jun 23, 2019

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

@vue-bot vue-bot closed this as completed Jun 23, 2019
@AlbertMarashi
Copy link
Author

AlbertMarashi commented Jun 23, 2019

@yyx990803

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

@LinusBorg LinusBorg reopened this Jun 23, 2019
@LinusBorg
Copy link
Member

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

@posva
Copy link
Member

posva commented Jun 24, 2019

This is pretty much a duplicate of your own issue: #9205 (comment)

@vuejs vuejs deleted a comment from vue-bot Jun 24, 2019
@vedmant
Copy link

vedmant commented Jun 24, 2019

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

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Jun 24, 2019

@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

@posva
Copy link
Member

posva commented Jun 24, 2019

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.

@AlbertMarashi
Copy link
Author

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

@AlbertMarashi
Copy link
Author

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

@bmarkovic
Copy link

bmarkovic commented Nov 22, 2019

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.

@6XGate
Copy link

6XGate commented May 4, 2020

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.

@miltzi

This comment has been minimized.

@lyswhut
Copy link

lyswhut commented Aug 30, 2022

Any news?

@aentwist
Copy link

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.

@johnwc
Copy link

johnwc commented Jan 5, 2023

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

@samkevin1
Copy link

Any updates on this?

@martinschilliger
Copy link

martinschilliger commented Jun 14, 2023

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);

@kedniko
Copy link

kedniko commented Jun 14, 2023

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>

@johnwc
Copy link

johnwc commented Jun 14, 2023

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

@martinschilliger
Copy link

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. 🤯

@johnwc
Copy link

johnwc commented Nov 16, 2023

@AlbertMarashi this is done and in vue js prod?

@AlbertMarashi
Copy link
Author

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 AlbertMarashi closed this as not planned Won't fix, can't repro, duplicate, stale Nov 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests