wip(ssr): v-model w/ dynamic type & props

This commit is contained in:
Evan You 2020-02-05 17:01:00 -05:00
parent 1f2de9e232
commit 201f18b58b
10 changed files with 204 additions and 23 deletions

View File

@ -238,7 +238,7 @@ export function generate(
for (let i = 0; i < ast.temps; i++) { for (let i = 0; i < ast.temps; i++) {
push(`${i > 0 ? `, ` : ``}_temp${i}`) push(`${i > 0 ? `, ` : ``}_temp${i}`)
} }
newline() push(`\n`)
} }
if (ast.components.length || ast.directives.length || ast.temps) { if (ast.components.length || ast.directives.length || ast.temps) {
newline() newline()

View File

@ -34,6 +34,7 @@ export { transformOn } from './transforms/vOn'
export { transformBind } from './transforms/vBind' export { transformBind } from './transforms/vBind'
// exported for compiler-ssr // exported for compiler-ssr
export { MERGE_PROPS } from './runtimeHelpers'
export { processIfBranches } from './transforms/vIf' export { processIfBranches } from './transforms/vIf'
export { processForNode, createForLoopParams } from './transforms/vFor' export { processForNode, createForLoopParams } from './transforms/vFor'
export { export {

View File

@ -16,7 +16,8 @@ import {
ComponentCodegenNode, ComponentCodegenNode,
createCallExpression, createCallExpression,
CacheExpression, CacheExpression,
createCacheExpression createCacheExpression,
TemplateLiteral
} from './ast' } from './ast'
import { import {
isString, isString,
@ -62,6 +63,7 @@ export type DirectiveTransform = (
export interface DirectiveTransformResult { export interface DirectiveTransformResult {
props: Property[] props: Property[]
needRuntime?: boolean | symbol needRuntime?: boolean | symbol
ssrTagParts?: TemplateLiteral['elements']
} }
// A structural directive transform is a technically a NodeTransform; // A structural directive transform is a technically a NodeTransform;

View File

@ -65,4 +65,73 @@ describe('ssr: v-model', () => {
}" }"
`) `)
}) })
test('<input :type="x">', () => {
expect(compile(`<input :type="x" v-model="foo">`).code)
.toMatchInlineSnapshot(`
"const { _renderAttr, _renderDynamicModel } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_push(\`<input\${
_renderAttr(\\"type\\", _ctx.x)
}\${
_renderDynamicModel(_ctx.x, _ctx.foo, null)
}>\`)
}"
`)
expect(compile(`<input :type="x" v-model="foo" value="bar">`).code)
.toMatchInlineSnapshot(`
"const { _renderAttr, _renderDynamicModel } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_push(\`<input\${
_renderAttr(\\"type\\", _ctx.x)
}\${
_renderDynamicModel(_ctx.x, _ctx.foo, \\"bar\\")
} value=\\"bar\\">\`)
}"
`)
expect(compile(`<input :type="x" v-model="foo" :value="bar">`).code)
.toMatchInlineSnapshot(`
"const { _renderAttr, _renderDynamicModel } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_push(\`<input\${
_renderAttr(\\"type\\", _ctx.x)
}\${
_renderDynamicModel(_ctx.x, _ctx.foo, _ctx.bar)
}\${
_renderAttr(\\"value\\", _ctx.bar)
}>\`)
}"
`)
})
test('<input v-bind="obj">', () => {
expect(compile(`<input v-bind="obj" v-model="foo">`).code)
.toMatchInlineSnapshot(`
"const { mergeProps } = require(\\"vue\\")
const { _renderAttrs, _getDynamicModelProps } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
let _temp0
_push(\`<input\${_renderAttrs(_temp0 = _ctx.obj, mergeProps(_temp0, _getDynamicModelProps(_temp0, _ctx.foo)))}>\`)
}"
`)
expect(compile(`<input id="x" v-bind="obj" v-model="foo" class="y">`).code)
.toMatchInlineSnapshot(`
"const { mergeProps } = require(\\"vue\\")
const { _renderAttrs, _getDynamicModelProps } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
let _temp0
_push(\`<input\${_renderAttrs(_temp0 = mergeProps({ id: \\"x\\" }, _ctx.obj, { class: \\"y\\" }), mergeProps(_temp0, _getDynamicModelProps(_temp0, _ctx.foo)))}>\`)
}"
`)
})
}) })

View File

@ -11,6 +11,8 @@ export const SSR_RENDER_DYNAMIC_ATTR = Symbol(`renderDynamicAttr`)
export const SSR_RENDER_LIST = Symbol(`renderList`) export const SSR_RENDER_LIST = Symbol(`renderList`)
export const SSR_LOOSE_EQUAL = Symbol(`looseEqual`) export const SSR_LOOSE_EQUAL = Symbol(`looseEqual`)
export const SSR_LOOSE_CONTAIN = Symbol(`looseContain`) export const SSR_LOOSE_CONTAIN = Symbol(`looseContain`)
export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`renderDynamicModel`)
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`getDynamicModelProps`)
export const ssrHelpers = { export const ssrHelpers = {
[SSR_INTERPOLATE]: `_interpolate`, [SSR_INTERPOLATE]: `_interpolate`,
@ -23,7 +25,9 @@ export const ssrHelpers = {
[SSR_RENDER_DYNAMIC_ATTR]: `_renderDynamicAttr`, [SSR_RENDER_DYNAMIC_ATTR]: `_renderDynamicAttr`,
[SSR_RENDER_LIST]: `_renderList`, [SSR_RENDER_LIST]: `_renderList`,
[SSR_LOOSE_EQUAL]: `_looseEqual`, [SSR_LOOSE_EQUAL]: `_looseEqual`,
[SSR_LOOSE_CONTAIN]: `_looseContain` [SSR_LOOSE_CONTAIN]: `_looseContain`,
[SSR_RENDER_DYNAMIC_MODEL]: `_renderDynamicModel`,
[SSR_GET_DYNAMIC_MODEL_PROPS]: `_getDynamicModelProps`
} }
// Note: these are helpers imported from @vue/server-renderer // Note: these are helpers imported from @vue/server-renderer

View File

@ -20,7 +20,8 @@ import {
ArrayExpression, ArrayExpression,
createAssignmentExpression, createAssignmentExpression,
TextNode, TextNode,
hasDynamicKeyVBind hasDynamicKeyVBind,
MERGE_PROPS
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared' import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared'
import { createSSRCompilerError, SSRErrorCodes } from '../errors' import { createSSRCompilerError, SSRErrorCodes } from '../errors'
@ -30,7 +31,8 @@ import {
SSR_RENDER_STYLE, SSR_RENDER_STYLE,
SSR_RENDER_DYNAMIC_ATTR, SSR_RENDER_DYNAMIC_ATTR,
SSR_RENDER_ATTRS, SSR_RENDER_ATTRS,
SSR_INTERPOLATE SSR_INTERPOLATE,
SSR_GET_DYNAMIC_MODEL_PROPS
} from '../runtimeHelpers' } from '../runtimeHelpers'
export const ssrTransformElement: NodeTransform = (node, context) => { export const ssrTransformElement: NodeTransform = (node, context) => {
@ -55,6 +57,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
context.helper(SSR_RENDER_ATTRS), context.helper(SSR_RENDER_ATTRS),
[props] [props]
) )
if (node.tag === 'textarea') { if (node.tag === 'textarea') {
// <textarea> with dynamic v-bind. We don't know if the final props // <textarea> with dynamic v-bind. We don't know if the final props
// will contain .value, so we will have to do something special: // will contain .value, so we will have to do something special:
@ -81,7 +84,31 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
) )
] ]
) )
} else if (node.tag === 'input') {
// <input v-bind="obj" v-model>
// we need to determine the props to render for the dynamic v-model
// and merge it with the v-bind expression.
const vModel = findVModel(node)
if (vModel) {
// 1. save the props (san v-model) in a temp variable
const tempId = `_temp${context.temps++}`
const tempExp = createSimpleExpression(tempId, false)
propsExp.arguments = [
createAssignmentExpression(tempExp, props),
createCallExpression(context.helper(MERGE_PROPS), [
tempExp,
createCallExpression(
context.helper(SSR_GET_DYNAMIC_MODEL_PROPS),
[
tempExp, // existing props
vModel.exp! // model
]
)
])
]
}
} }
openTag.push(propsExp) openTag.push(propsExp)
} }
} }
@ -122,7 +149,14 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
) )
) )
} else if (!hasDynamicVBind) { } else if (!hasDynamicVBind) {
const { props } = directiveTransform(prop, node, context) const { props, ssrTagParts } = directiveTransform(
prop,
node,
context
)
if (ssrTagParts) {
openTag.push(...ssrTagParts)
}
for (let j = 0; j < props.length; j++) { for (let j = 0; j < props.length; j++) {
const { key, value } = props[j] const { key, value } = props[j]
if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) { if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
@ -254,3 +288,9 @@ function removeStaticBinding(
tag.splice(i, 1) tag.splice(i, 1)
} }
} }
function findVModel(node: PlainElementNode): DirectiveNode | undefined {
return node.props.find(
p => p.type === NodeTypes.DIRECTIVE && p.name === 'model' && p.exp
) as DirectiveNode | undefined
}

View File

@ -6,16 +6,21 @@ import {
NodeTypes, NodeTypes,
createDOMCompilerError, createDOMCompilerError,
DOMErrorCodes, DOMErrorCodes,
Property,
createObjectProperty, createObjectProperty,
createSimpleExpression, createSimpleExpression,
createCallExpression, createCallExpression,
PlainElementNode, PlainElementNode,
ExpressionNode, ExpressionNode,
createConditionalExpression, createConditionalExpression,
createInterpolation createInterpolation,
hasDynamicKeyVBind
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { SSR_LOOSE_EQUAL, SSR_LOOSE_CONTAIN } from '../runtimeHelpers' import {
SSR_LOOSE_EQUAL,
SSR_LOOSE_CONTAIN,
SSR_RENDER_DYNAMIC_MODEL
} from '../runtimeHelpers'
import { DirectiveTransformResult } from 'packages/compiler-core/src/transform'
export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
const model = dir.exp! const model = dir.exp!
@ -33,7 +38,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
} }
if (node.tagType === ElementTypes.ELEMENT) { if (node.tagType === ElementTypes.ELEMENT) {
let props: Property[] = [] const res: DirectiveTransformResult = { props: [] }
const defaultProps = [ const defaultProps = [
// default value binding for text type inputs // default value binding for text type inputs
createObjectProperty(createSimpleExpression(`value`, true), model) createObjectProperty(createSimpleExpression(`value`, true), model)
@ -41,26 +46,32 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
if (node.tag === 'input') { if (node.tag === 'input') {
const type = findProp(node, 'type') const type = findProp(node, 'type')
if (type) { if (type) {
const value = findValueBinding(node)
if (type.type === NodeTypes.DIRECTIVE) { if (type.type === NodeTypes.DIRECTIVE) {
// dynamic type // dynamic type
// TODO res.ssrTagParts = [
createCallExpression(context.helper(SSR_RENDER_DYNAMIC_MODEL), [
type.exp!,
model,
value
])
]
} else if (type.value) { } else if (type.value) {
// static type // static type
switch (type.value.content) { switch (type.value.content) {
case 'radio': case 'radio':
props = [ res.props = [
createObjectProperty( createObjectProperty(
createSimpleExpression(`checked`, true), createSimpleExpression(`checked`, true),
createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ createCallExpression(context.helper(SSR_LOOSE_EQUAL), [
model, model,
findValueBinding(node) value
]) ])
) )
] ]
break break
case 'checkbox': case 'checkbox':
const value = findValueBinding(node) res.props = [
props = [
createObjectProperty( createObjectProperty(
createSimpleExpression(`checked`, true), createSimpleExpression(`checked`, true),
createConditionalExpression( createConditionalExpression(
@ -84,13 +95,18 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
break break
default: default:
checkDuplicatedValue() checkDuplicatedValue()
props = defaultProps res.props = defaultProps
break break
} }
} }
} else if (hasDynamicKeyVBind(node)) {
// dynamic type due to dynamic v-bind
// NOOP, handled in ssrTransformElement due to need to rewrite
// the entire props expression
} else { } else {
// text type
checkDuplicatedValue() checkDuplicatedValue()
props = defaultProps res.props = defaultProps
} }
} else if (node.tag === 'textarea') { } else if (node.tag === 'textarea') {
checkDuplicatedValue() checkDuplicatedValue()
@ -107,7 +123,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
) )
} }
return { props } return res
} else { } else {
// component v-model // component v-model
return transformModel(dir, node, context) return transformModel(dir, node, context)

View File

@ -0,0 +1,47 @@
import { looseEqual as _looseEqual, looseIndexOf } from '@vue/shared'
import { renderAttr } from './renderAttrs'
export const looseEqual = _looseEqual as (a: unknown, b: unknown) => boolean
export function looseContain(arr: unknown[], value: unknown): boolean {
return looseIndexOf(arr, value) > -1
}
// for <input :type="type" v-model="model" value="value">
export function renderDynamicModel(
type: unknown,
model: unknown,
value: unknown
) {
switch (type) {
case 'radio':
return _looseEqual(model, value) ? ' checked' : ''
case 'checkbox':
return (Array.isArray(model)
? looseContain(model, value)
: model)
? ' checked'
: ''
default:
// text types
return renderAttr('value', model)
}
}
// for <input v-bind="obj" v-model="model">
export function getDynamicModelProps(existingProps: any = {}, model: unknown) {
const { type, value } = existingProps
switch (type) {
case 'radio':
return _looseEqual(model, value) ? { checked: true } : null
case 'checkbox':
return (Array.isArray(model)
? looseContain(model, value)
: model)
? { checked: true }
: null
default:
// text types
return { value: model }
}
}

View File

@ -17,7 +17,9 @@ export { interpolate as _interpolate } from './helpers/interpolate'
export { renderList as _renderList } from './helpers/renderList' export { renderList as _renderList } from './helpers/renderList'
// v-model helpers // v-model helpers
import { looseEqual, looseIndexOf } from '@vue/shared' export {
export const _looseEqual = looseEqual as (a: unknown, b: unknown) => boolean looseEqual as _looseEqual,
export const _looseContain = (arr: unknown[], value: unknown): boolean => looseContain as _looseContain,
looseIndexOf(arr, value) > -1 renderDynamicModel as _renderDynamicModel,
getDynamicModelProps as _getDynamicModelProps
} from './helpers/vModelHelpers'