wip(ssr): v-model w/ dynamic type & props
This commit is contained in:
parent
1f2de9e232
commit
201f18b58b
@ -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()
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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)))}>\`)
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
47
packages/server-renderer/src/helpers/vModelHelpers.ts
Normal file
47
packages/server-renderer/src/helpers/vModelHelpers.ts
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
@ -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'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user