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 { 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 | ||||
|  | ||||
| @ -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 } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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' | ||||
| } | ||||
|  | ||||
							
								
								
									
										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, | ||||
|   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 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user