feat(ssr): support getSSRProps for vnode directives

This commit is contained in:
Evan You 2020-03-16 18:36:19 -04:00
parent a46f3b354d
commit c450ede12d
5 changed files with 458 additions and 6 deletions

View File

@ -14,7 +14,7 @@ return withDirectives(h(comp), [
import { VNode } from './vnode'
import { isFunction, EMPTY_OBJ, makeMap, EMPTY_ARR } from '@vue/shared'
import { warn } from './warning'
import { ComponentInternalInstance } from './component'
import { ComponentInternalInstance, Data } from './component'
import { currentRenderingInstance } from './componentRenderUtils'
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
import { ComponentPublicInstance } from './componentProxy'
@ -35,6 +35,11 @@ export type DirectiveHook<T = any> = (
prevVNode: VNode<any, T> | null
) => void
export type SSRDirectiveHook = (
binding: DirectiveBinding,
vnode: VNode
) => Data | undefined
export interface ObjectDirective<T = any> {
beforeMount?: DirectiveHook<T>
mounted?: DirectiveHook<T>
@ -42,6 +47,7 @@ export interface ObjectDirective<T = any> {
updated?: DirectiveHook<T>
beforeUnmount?: DirectiveHook<T>
unmounted?: DirectiveHook<T>
getSSRProps?: SSRDirectiveHook
}
export type FunctionDirective<T = any> = DirectiveHook<T>
@ -81,7 +87,7 @@ const directiveToVnodeHooksMap = /*#__PURE__*/ [
const prevBindings = prevVnode ? prevVnode.dirs! : EMPTY_ARR
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i]
const hook = binding.dir[key]
const hook = binding.dir[key] as DirectiveHook
if (hook != null) {
if (prevVnode != null) {
binding.oldValue = prevBindings[i].value

View File

@ -218,7 +218,7 @@ function callModelHook(
binding: DirectiveBinding,
vnode: VNode,
prevVNode: VNode | null,
hook: keyof ObjectDirective
hook: 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated'
) {
let modelToUse: ObjectDirective
switch (el.tagName) {
@ -243,3 +243,24 @@ function callModelHook(
const fn = modelToUse[hook]
fn && fn(el, binding, vnode, prevVNode)
}
// SSR vnode transforms
if (__NODE_JS__) {
vModelText.getSSRProps = ({ value }) => ({ value })
vModelRadio.getSSRProps = ({ value }, vnode) => {
if (vnode.props && looseEqual(vnode.props.value, value)) {
return { checked: true }
}
}
vModelCheckbox.getSSRProps = ({ value }, vnode) => {
if (isArray(value)) {
if (vnode.props && looseIndexOf(value, vnode.props.value) > -1) {
return { checked: true }
}
} else if (value) {
return { checked: true }
}
}
}

View File

@ -40,6 +40,14 @@ export const vShow: ObjectDirective<VShowElement> = {
}
}
if (__NODE_JS__) {
vShow.getSSRProps = ({ value }) => {
if (!value) {
return { style: { display: 'none' } }
}
}
}
function setDisplay(el: VShowElement, value: unknown): void {
el.style.display = value ? el._vod : 'none'
}

View File

@ -0,0 +1,393 @@
import { renderToString } from '../src/renderToString'
import {
createApp,
h,
withDirectives,
vShow,
vModelText,
vModelRadio,
vModelCheckbox
} from 'vue'
describe('ssr: directives', () => {
describe('template v-show', () => {
test('basic', async () => {
expect(
await renderToString(
createApp({
template: `<div v-show="true"/>`
})
)
).toBe(`<div style=""></div>`)
expect(
await renderToString(
createApp({
template: `<div v-show="false"/>`
})
)
).toBe(`<div style="display:none;"></div>`)
})
test('with static style', async () => {
expect(
await renderToString(
createApp({
template: `<div style="color:red" v-show="false"/>`
})
)
).toBe(`<div style="color:red;display:none;"></div>`)
})
test('with dynamic style', async () => {
expect(
await renderToString(
createApp({
data: () => ({ style: { color: 'red' } }),
template: `<div :style="style" v-show="false"/>`
})
)
).toBe(`<div style="color:red;display:none;"></div>`)
})
test('with static + dynamic style', async () => {
expect(
await renderToString(
createApp({
data: () => ({ style: { color: 'red' } }),
template: `<div :style="style" style="font-size:12;" v-show="false"/>`
})
)
).toBe(`<div style="color:red;font-size:12;display:none;"></div>`)
})
})
describe('template v-model', () => {
test('text', async () => {
expect(
await renderToString(
createApp({
data: () => ({ text: 'hello' }),
template: `<input v-model="text">`
})
)
).toBe(`<input value="hello">`)
})
test('radio', async () => {
expect(
await renderToString(
createApp({
data: () => ({ selected: 'foo' }),
template: `<input type="radio" value="foo" v-model="selected">`
})
)
).toBe(`<input type="radio" value="foo" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ selected: 'foo' }),
template: `<input type="radio" value="bar" v-model="selected">`
})
)
).toBe(`<input type="radio" value="bar">`)
// non-string values
expect(
await renderToString(
createApp({
data: () => ({ selected: 'foo' }),
template: `<input type="radio" :value="{}" v-model="selected">`
})
)
).toBe(`<input type="radio">`)
})
test('checkbox', async () => {
expect(
await renderToString(
createApp({
data: () => ({ checked: true }),
template: `<input type="checkbox" v-model="checked">`
})
)
).toBe(`<input type="checkbox" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ checked: false }),
template: `<input type="checkbox" v-model="checked">`
})
)
).toBe(`<input type="checkbox">`)
expect(
await renderToString(
createApp({
data: () => ({ checked: ['foo'] }),
template: `<input type="checkbox" value="foo" v-model="checked">`
})
)
).toBe(`<input type="checkbox" value="foo" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ checked: [] }),
template: `<input type="checkbox" value="foo" v-model="checked">`
})
)
).toBe(`<input type="checkbox" value="foo">`)
})
test('textarea', async () => {
expect(
await renderToString(
createApp({
data: () => ({ foo: 'hello' }),
template: `<textarea v-model="foo"/>`
})
)
).toBe(`<textarea>hello</textarea>`)
})
test('dynamic type', async () => {
expect(
await renderToString(
createApp({
data: () => ({ type: 'text', model: 'hello' }),
template: `<input :type="type" v-model="model">`
})
)
).toBe(`<input type="text" value="hello">`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'checkbox', model: true }),
template: `<input :type="type" v-model="model">`
})
)
).toBe(`<input type="checkbox" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'checkbox', model: false }),
template: `<input :type="type" v-model="model">`
})
)
).toBe(`<input type="checkbox">`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'checkbox', model: ['hello'] }),
template: `<input :type="type" value="hello" v-model="model">`
})
)
).toBe(`<input type="checkbox" value="hello" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'checkbox', model: [] }),
template: `<input :type="type" value="hello" v-model="model">`
})
)
).toBe(`<input type="checkbox" value="hello">`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'radio', model: 'hello' }),
template: `<input :type="type" value="hello" v-model="model">`
})
)
).toBe(`<input type="radio" value="hello" checked>`)
expect(
await renderToString(
createApp({
data: () => ({ type: 'radio', model: 'hello' }),
template: `<input :type="type" value="bar" v-model="model">`
})
)
).toBe(`<input type="radio" value="bar">`)
})
test('with v-bind', async () => {
expect(
await renderToString(
createApp({
data: () => ({
obj: { type: 'radio', value: 'hello' },
model: 'hello'
}),
template: `<input v-bind="obj" v-model="model">`
})
)
).toBe(`<input type="radio" value="hello" checked>`)
})
})
describe('vnode v-show', () => {
test('basic', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('div'), [[vShow, true]])
}
})
)
).toBe(`<div></div>`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('div'), [[vShow, false]])
}
})
)
).toBe(`<div style="display:none;"></div>`)
})
test('with merge', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('div', {
style: {
color: 'red'
}
}),
[[vShow, false]]
)
}
})
)
).toBe(`<div style="color:red;display:none;"></div>`)
})
})
describe('vnode v-model', () => {
test('text', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('input'), [[vModelText, 'hello']])
}
})
)
).toBe(`<input value="hello">`)
})
test('radio', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'radio', value: 'hello' }),
[[vModelRadio, 'hello']]
)
}
})
)
).toBe(`<input type="radio" value="hello" checked>`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'radio', value: 'hello' }),
[[vModelRadio, 'foo']]
)
}
})
)
).toBe(`<input type="radio" value="hello">`)
})
test('checkbox', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('input', { type: 'checkbox' }), [
[vModelCheckbox, true]
])
}
})
)
).toBe(`<input type="checkbox" checked>`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('input', { type: 'checkbox' }), [
[vModelCheckbox, false]
])
}
})
)
).toBe(`<input type="checkbox">`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'checkbox', value: 'foo' }),
[[vModelCheckbox, ['foo']]]
)
}
})
)
).toBe(`<input type="checkbox" value="foo" checked>`)
expect(
await renderToString(
createApp({
render() {
return withDirectives(
h('input', { type: 'checkbox', value: 'foo' }),
[[vModelCheckbox, []]]
)
}
})
)
).toBe(`<input type="checkbox" value="foo">`)
})
})
test('custom directive w/ getSSRProps', async () => {
expect(
await renderToString(
createApp({
render() {
return withDirectives(h('div'), [
[
{
getSSRProps({ value }) {
return { id: value }
}
},
'foo'
]
])
}
})
)
).toBe(`<div id="foo"></div>`)
})
})

View File

@ -12,7 +12,10 @@ import {
Slots,
createApp,
ssrContextKey,
warn
warn,
DirectiveBinding,
VNodeProps,
mergeProps
} from 'vue'
import {
ShapeFlags,
@ -289,10 +292,12 @@ function renderElementVNode(
parentComponent: ComponentInternalInstance
) {
const tag = vnode.type as string
const { props, children, shapeFlag, scopeId } = vnode
let { props, children, shapeFlag, scopeId, dirs } = vnode
let openTag = `<${tag}`
// TODO directives
if (dirs !== null) {
props = applySSRDirectives(vnode, props, dirs)
}
if (props !== null) {
openTag += ssrRenderAttrs(props, tag)
@ -338,6 +343,25 @@ function renderElementVNode(
}
}
function applySSRDirectives(
vnode: VNode,
rawProps: VNodeProps | null,
dirs: DirectiveBinding[]
): VNodeProps {
const toMerge: VNodeProps[] = []
for (let i = 0; i < dirs.length; i++) {
const binding = dirs[i]
const {
dir: { getSSRProps }
} = binding
if (getSSRProps) {
const props = getSSRProps(binding, vnode)
if (props) toMerge.push(props)
}
}
return mergeProps(rawProps || {}, ...toMerge)
}
function renderPortalVNode(
vnode: VNode,
parentComponent: ComponentInternalInstance