diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index 12267adb..bbd4dc98 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -104,13 +104,14 @@ export { registerRuntimeCompiler } from './component'
// SSR -------------------------------------------------------------------------
import { createComponentInstance, setupComponent } from './component'
import { renderComponentRoot } from './componentRenderUtils'
-import { normalizeVNode } from './vnode'
+import { isVNode, normalizeVNode } from './vnode'
// SSR utils are only exposed in cjs builds.
const _ssrUtils = {
createComponentInstance,
setupComponent,
renderComponentRoot,
+ isVNode,
normalizeVNode
}
diff --git a/packages/server-renderer/__tests__/renderProps.spec.ts b/packages/server-renderer/__tests__/renderProps.spec.ts
index f5a6f60d..25efd80f 100644
--- a/packages/server-renderer/__tests__/renderProps.spec.ts
+++ b/packages/server-renderer/__tests__/renderProps.spec.ts
@@ -55,6 +55,17 @@ describe('ssr: renderProps', () => {
})
).toBe(` readonly for="foobar"`)
})
+
+ test('preserve name on custom element', () => {
+ expect(
+ renderProps(
+ {
+ fooBar: 'ok'
+ },
+ 'my-el'
+ )
+ ).toBe(` fooBar="ok"`)
+ })
})
describe('ssr: renderClass', () => {
diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts
index ffab23a0..a41c4eed 100644
--- a/packages/server-renderer/__tests__/renderToString.spec.ts
+++ b/packages/server-renderer/__tests__/renderToString.spec.ts
@@ -1,5 +1,5 @@
-import { createApp, h } from 'vue'
-import { renderToString, renderComponent, renderSlot } from '../src'
+import { createApp, h, createCommentVNode } from 'vue'
+import { renderToString, renderComponent, renderSlot, escapeHtml } from '../src'
describe('ssr: renderToString', () => {
describe('components', () => {
@@ -251,21 +251,82 @@ describe('ssr: renderToString', () => {
})
})
+ describe('vnode element', () => {
+ test('props', async () => {
+ expect(
+ await renderToString(
+ h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello')
+ )
+ ).toBe(`
hello
`)
+ })
+
+ test('text children', async () => {
+ expect(await renderToString(h('div', 'hello'))).toBe(`hello
`)
+ })
+
+ test('array children', async () => {
+ expect(
+ await renderToString(
+ h('div', [
+ 'foo',
+ h('span', 'bar'),
+ [h('span', 'baz')],
+ createCommentVNode('qux')
+ ])
+ )
+ ).toBe(
+ `foobarbaz
`
+ )
+ })
+
+ test('void elements', async () => {
+ expect(await renderToString(h('input'))).toBe(``)
+ })
+
+ test('innerHTML', async () => {
+ expect(
+ await renderToString(
+ h(
+ 'div',
+ {
+ innerHTML: `hello`
+ },
+ 'ignored'
+ )
+ )
+ ).toBe(`hello
`)
+ })
+
+ test('textContent', async () => {
+ expect(
+ await renderToString(
+ h(
+ 'div',
+ {
+ textContent: `hello`
+ },
+ 'ignored'
+ )
+ )
+ ).toBe(`${escapeHtml(`hello`)}
`)
+ })
+
+ test('textarea value', async () => {
+ expect(
+ await renderToString(
+ h(
+ 'textarea',
+ {
+ value: `hello`
+ },
+ 'ignored'
+ )
+ )
+ ).toBe(``)
+ })
+ })
+
describe('scopeId', () => {
// TODO
})
-
- describe('vnode', () => {
- test('text children', () => {})
-
- test('array children', () => {})
-
- test('void elements', () => {})
-
- test('innerHTML', () => {})
-
- test('textContent', () => {})
-
- test('textarea value', () => {})
- })
})
diff --git a/packages/server-renderer/src/renderProps.ts b/packages/server-renderer/src/renderProps.ts
index 112055d8..53536f70 100644
--- a/packages/server-renderer/src/renderProps.ts
+++ b/packages/server-renderer/src/renderProps.ts
@@ -8,16 +8,23 @@ import {
isNoUnitNumericStyleProp,
isOn,
isSSRSafeAttrName,
- isBooleanAttr
+ isBooleanAttr,
+ makeMap
} from '@vue/shared'
+const shouldIgnoreProp = makeMap(`key,ref,innerHTML,textContent`)
+
export function renderProps(
props: Record,
- isCustomElement: boolean = false
+ tag?: string
): string {
let ret = ''
for (const key in props) {
- if (key === 'key' || key === 'ref' || isOn(key)) {
+ if (
+ shouldIgnoreProp(key) ||
+ isOn(key) ||
+ (tag === 'textarea' && key === 'value')
+ ) {
continue
}
const value = props[key]
@@ -26,9 +33,10 @@ export function renderProps(
} else if (key === 'style') {
ret += ` style="${renderStyle(value)}"`
} else if (value != null) {
- const attrKey = isCustomElement
- ? key
- : propsToAttrMap[key] || key.toLowerCase()
+ const attrKey =
+ tag && tag.indexOf('-') > 0
+ ? key // preserve raw name on custom elements
+ : propsToAttrMap[key] || key.toLowerCase()
if (isBooleanAttr(attrKey)) {
if (value !== false) {
ret += ` ${attrKey}`
diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts
index 1cc3706c..7cd7c25c 100644
--- a/packages/server-renderer/src/renderToString.ts
+++ b/packages/server-renderer/src/renderToString.ts
@@ -12,7 +12,8 @@ import {
Portal,
ShapeFlags,
ssrUtils,
- Slot
+ Slot,
+ createApp
} from 'vue'
import {
isString,
@@ -25,6 +26,7 @@ import { renderProps } from './renderProps'
import { escapeHtml } from './ssrUtils'
const {
+ isVNode,
createComponentInstance,
setupComponent,
renderComponentRoot,
@@ -81,7 +83,15 @@ function unrollBuffer(buffer: ResolvedSSRBuffer): string {
return ret
}
-export async function renderToString(app: App): Promise {
+export async function renderToString(input: App | VNode): Promise {
+ if (isVNode(input)) {
+ return renderAppToString(createApp({ render: () => input }))
+ } else {
+ return renderAppToString(input)
+ }
+}
+
+async function renderAppToString(app: App): Promise {
const resolvedBuffer = await renderComponent(app._component, app._props, null)
return unrollBuffer(resolvedBuffer)
}
@@ -143,7 +153,7 @@ function renderComponentSubTree(
return hasAsync() ? Promise.all(buffer) : (buffer as ResolvedSSRBuffer)
}
-export function renderVNode(
+function renderVNode(
push: PushFn,
vnode: VNode,
parentComponent: ComponentInternalInstance | null = null
@@ -203,7 +213,7 @@ function renderElement(
// TODO directives
if (props !== null) {
- openTag += renderProps(props, tag.indexOf(`-`) > 0)
+ openTag += renderProps(props, tag)
}
if (scopeId !== null) {