feat(ssr): support getSSRProps for vnode directives
This commit is contained in:
parent
a46f3b354d
commit
c450ede12d
@ -14,7 +14,7 @@ return withDirectives(h(comp), [
|
|||||||
import { VNode } from './vnode'
|
import { VNode } from './vnode'
|
||||||
import { isFunction, EMPTY_OBJ, makeMap, EMPTY_ARR } from '@vue/shared'
|
import { isFunction, EMPTY_OBJ, makeMap, EMPTY_ARR } from '@vue/shared'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { ComponentInternalInstance } from './component'
|
import { ComponentInternalInstance, Data } from './component'
|
||||||
import { currentRenderingInstance } from './componentRenderUtils'
|
import { currentRenderingInstance } from './componentRenderUtils'
|
||||||
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
|
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
|
||||||
import { ComponentPublicInstance } from './componentProxy'
|
import { ComponentPublicInstance } from './componentProxy'
|
||||||
@ -35,6 +35,11 @@ export type DirectiveHook<T = any> = (
|
|||||||
prevVNode: VNode<any, T> | null
|
prevVNode: VNode<any, T> | null
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
|
export type SSRDirectiveHook = (
|
||||||
|
binding: DirectiveBinding,
|
||||||
|
vnode: VNode
|
||||||
|
) => Data | undefined
|
||||||
|
|
||||||
export interface ObjectDirective<T = any> {
|
export interface ObjectDirective<T = any> {
|
||||||
beforeMount?: DirectiveHook<T>
|
beforeMount?: DirectiveHook<T>
|
||||||
mounted?: DirectiveHook<T>
|
mounted?: DirectiveHook<T>
|
||||||
@ -42,6 +47,7 @@ export interface ObjectDirective<T = any> {
|
|||||||
updated?: DirectiveHook<T>
|
updated?: DirectiveHook<T>
|
||||||
beforeUnmount?: DirectiveHook<T>
|
beforeUnmount?: DirectiveHook<T>
|
||||||
unmounted?: DirectiveHook<T>
|
unmounted?: DirectiveHook<T>
|
||||||
|
getSSRProps?: SSRDirectiveHook
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FunctionDirective<T = any> = DirectiveHook<T>
|
export type FunctionDirective<T = any> = DirectiveHook<T>
|
||||||
@ -81,7 +87,7 @@ const directiveToVnodeHooksMap = /*#__PURE__*/ [
|
|||||||
const prevBindings = prevVnode ? prevVnode.dirs! : EMPTY_ARR
|
const prevBindings = prevVnode ? prevVnode.dirs! : EMPTY_ARR
|
||||||
for (let i = 0; i < bindings.length; i++) {
|
for (let i = 0; i < bindings.length; i++) {
|
||||||
const binding = bindings[i]
|
const binding = bindings[i]
|
||||||
const hook = binding.dir[key]
|
const hook = binding.dir[key] as DirectiveHook
|
||||||
if (hook != null) {
|
if (hook != null) {
|
||||||
if (prevVnode != null) {
|
if (prevVnode != null) {
|
||||||
binding.oldValue = prevBindings[i].value
|
binding.oldValue = prevBindings[i].value
|
||||||
|
@ -218,7 +218,7 @@ function callModelHook(
|
|||||||
binding: DirectiveBinding,
|
binding: DirectiveBinding,
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
prevVNode: VNode | null,
|
prevVNode: VNode | null,
|
||||||
hook: keyof ObjectDirective
|
hook: 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated'
|
||||||
) {
|
) {
|
||||||
let modelToUse: ObjectDirective
|
let modelToUse: ObjectDirective
|
||||||
switch (el.tagName) {
|
switch (el.tagName) {
|
||||||
@ -243,3 +243,24 @@ function callModelHook(
|
|||||||
const fn = modelToUse[hook]
|
const fn = modelToUse[hook]
|
||||||
fn && fn(el, binding, vnode, prevVNode)
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 {
|
function setDisplay(el: VShowElement, value: unknown): void {
|
||||||
el.style.display = value ? el._vod : 'none'
|
el.style.display = value ? el._vod : 'none'
|
||||||
}
|
}
|
||||||
|
393
packages/server-renderer/__tests__/ssrDirectives.spec.ts
Normal file
393
packages/server-renderer/__tests__/ssrDirectives.spec.ts
Normal 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>`)
|
||||||
|
})
|
||||||
|
})
|
@ -12,7 +12,10 @@ import {
|
|||||||
Slots,
|
Slots,
|
||||||
createApp,
|
createApp,
|
||||||
ssrContextKey,
|
ssrContextKey,
|
||||||
warn
|
warn,
|
||||||
|
DirectiveBinding,
|
||||||
|
VNodeProps,
|
||||||
|
mergeProps
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import {
|
import {
|
||||||
ShapeFlags,
|
ShapeFlags,
|
||||||
@ -289,10 +292,12 @@ function renderElementVNode(
|
|||||||
parentComponent: ComponentInternalInstance
|
parentComponent: ComponentInternalInstance
|
||||||
) {
|
) {
|
||||||
const tag = vnode.type as string
|
const tag = vnode.type as string
|
||||||
const { props, children, shapeFlag, scopeId } = vnode
|
let { props, children, shapeFlag, scopeId, dirs } = vnode
|
||||||
let openTag = `<${tag}`
|
let openTag = `<${tag}`
|
||||||
|
|
||||||
// TODO directives
|
if (dirs !== null) {
|
||||||
|
props = applySSRDirectives(vnode, props, dirs)
|
||||||
|
}
|
||||||
|
|
||||||
if (props !== null) {
|
if (props !== null) {
|
||||||
openTag += ssrRenderAttrs(props, tag)
|
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(
|
function renderPortalVNode(
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parentComponent: ComponentInternalInstance
|
parentComponent: ComponentInternalInstance
|
||||||
|
Loading…
Reference in New Issue
Block a user