]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(ssr): v-model w/ dynamic type & props
authorEvan You <yyx990803@gmail.com>
Wed, 5 Feb 2020 22:01:00 +0000 (17:01 -0500)
committerEvan You <yyx990803@gmail.com>
Wed, 5 Feb 2020 22:01:00 +0000 (17:01 -0500)
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/transform.ts
packages/compiler-ssr/__tests__/ssrElement.spec.ts
packages/compiler-ssr/__tests__/ssrVModel.spec.ts
packages/compiler-ssr/src/runtimeHelpers.ts
packages/compiler-ssr/src/transforms/ssrTransformElement.ts
packages/compiler-ssr/src/transforms/ssrVModel.ts
packages/server-renderer/src/helpers/vModelHelpers.ts [new file with mode: 0644]
packages/server-renderer/src/index.ts

index 250c7fb683a3bae3682ec0853272e75a11bfd32e..d58c83bb1975bfa63c7cf9e064d4119f7bbedd35 100644 (file)
@@ -238,7 +238,7 @@ export function generate(
     for (let i = 0; i < ast.temps; i++) {
       push(`${i > 0 ? `, ` : ``}_temp${i}`)
     }
-    newline()
+    push(`\n`)
   }
   if (ast.components.length || ast.directives.length || ast.temps) {
     newline()
index 07d1a4052038c0e1a80696e21aca4ced25e912af..e807b0e50d555a56484389715205aa3ecbc1cd75 100644 (file)
@@ -34,6 +34,7 @@ export { transformOn } from './transforms/vOn'
 export { transformBind } from './transforms/vBind'
 
 // exported for compiler-ssr
+export { MERGE_PROPS } from './runtimeHelpers'
 export { processIfBranches } from './transforms/vIf'
 export { processForNode, createForLoopParams } from './transforms/vFor'
 export {
index 3d7167c04c1d23f1b1badb5b5cc52bca1941f2f7..81b386998613fcb27572440a4325125c834aecbc 100644 (file)
@@ -16,7 +16,8 @@ import {
   ComponentCodegenNode,
   createCallExpression,
   CacheExpression,
-  createCacheExpression
+  createCacheExpression,
+  TemplateLiteral
 } from './ast'
 import {
   isString,
@@ -62,6 +63,7 @@ export type DirectiveTransform = (
 export interface DirectiveTransformResult {
   props: Property[]
   needRuntime?: boolean | symbol
+  ssrTagParts?: TemplateLiteral['elements']
 }
 
 // A structural directive transform is a technically a NodeTransform;
index 1f08706fd3f84ea3ab590258d02c3d02af1063c4..9769709a9d03e1dbe8f157c3e1bfef2943fa545d 100644 (file)
@@ -55,7 +55,7 @@ describe('ssr: element', () => {
 
         return function ssrRender(_ctx, _push, _parent) {
           let _temp0
-          
+
           _push(\`<textarea\${
             _renderAttrs(_temp0 = _ctx.obj)
           }>\${
index 31ee50cc98c2a76438109a75c0715de15b797650..a5ffed49559873f924b135c56eee86ed39dad58f 100644 (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)))}>\`)
+      }"
+    `)
+  })
 })
index 87fd6b35f17446db8c157d2f34b695ad0738ccc3..843586090bc374b61d09891ce7a1f6f5c7e1031e 100644 (file)
@@ -11,6 +11,8 @@ export const SSR_RENDER_DYNAMIC_ATTR = Symbol(`renderDynamicAttr`)
 export const SSR_RENDER_LIST = Symbol(`renderList`)
 export const SSR_LOOSE_EQUAL = Symbol(`looseEqual`)
 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 = {
   [SSR_INTERPOLATE]: `_interpolate`,
@@ -23,7 +25,9 @@ export const ssrHelpers = {
   [SSR_RENDER_DYNAMIC_ATTR]: `_renderDynamicAttr`,
   [SSR_RENDER_LIST]: `_renderList`,
   [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
index feacbd893c405363436a914a6cd265888d13420e..5345c10a469d55f29cb9884621cc47b4f68678fa 100644 (file)
@@ -20,7 +20,8 @@ import {
   ArrayExpression,
   createAssignmentExpression,
   TextNode,
-  hasDynamicKeyVBind
+  hasDynamicKeyVBind,
+  MERGE_PROPS
 } from '@vue/compiler-dom'
 import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared'
 import { createSSRCompilerError, SSRErrorCodes } from '../errors'
@@ -30,7 +31,8 @@ import {
   SSR_RENDER_STYLE,
   SSR_RENDER_DYNAMIC_ATTR,
   SSR_RENDER_ATTRS,
-  SSR_INTERPOLATE
+  SSR_INTERPOLATE,
+  SSR_GET_DYNAMIC_MODEL_PROPS
 } from '../runtimeHelpers'
 
 export const ssrTransformElement: NodeTransform = (node, context) => {
@@ -55,6 +57,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
             context.helper(SSR_RENDER_ATTRS),
             [props]
           )
+
           if (node.tag === 'textarea') {
             // <textarea> with dynamic v-bind. We don't know if the final props
             // 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)
         }
       }
@@ -122,7 +149,14 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
                 )
               )
             } 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++) {
                 const { key, value } = props[j]
                 if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
@@ -254,3 +288,9 @@ function removeStaticBinding(
     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
+}
index 61933fe9b52bc46f0fcdb96048928c896f80cc26..1af92879c0670b38d6b011a48a8ea25adddbe3df 100644 (file)
@@ -6,16 +6,21 @@ import {
   NodeTypes,
   createDOMCompilerError,
   DOMErrorCodes,
-  Property,
   createObjectProperty,
   createSimpleExpression,
   createCallExpression,
   PlainElementNode,
   ExpressionNode,
   createConditionalExpression,
-  createInterpolation
+  createInterpolation,
+  hasDynamicKeyVBind
 } 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) => {
   const model = dir.exp!
@@ -33,7 +38,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
   }
 
   if (node.tagType === ElementTypes.ELEMENT) {
-    let props: Property[] = []
+    const res: DirectiveTransformResult = { props: [] }
     const defaultProps = [
       // default value binding for text type inputs
       createObjectProperty(createSimpleExpression(`value`, true), model)
@@ -41,26 +46,32 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
     if (node.tag === 'input') {
       const type = findProp(node, 'type')
       if (type) {
+        const value = findValueBinding(node)
         if (type.type === NodeTypes.DIRECTIVE) {
           // dynamic type
-          // TODO
+          res.ssrTagParts = [
+            createCallExpression(context.helper(SSR_RENDER_DYNAMIC_MODEL), [
+              type.exp!,
+              model,
+              value
+            ])
+          ]
         } else if (type.value) {
           // static type
           switch (type.value.content) {
             case 'radio':
-              props = [
+              res.props = [
                 createObjectProperty(
                   createSimpleExpression(`checked`, true),
                   createCallExpression(context.helper(SSR_LOOSE_EQUAL), [
                     model,
-                    findValueBinding(node)
+                    value
                   ])
                 )
               ]
               break
             case 'checkbox':
-              const value = findValueBinding(node)
-              props = [
+              res.props = [
                 createObjectProperty(
                   createSimpleExpression(`checked`, true),
                   createConditionalExpression(
@@ -84,13 +95,18 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
               break
             default:
               checkDuplicatedValue()
-              props = defaultProps
+              res.props = defaultProps
               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 {
+        // text type
         checkDuplicatedValue()
-        props = defaultProps
+        res.props = defaultProps
       }
     } else if (node.tag === 'textarea') {
       checkDuplicatedValue()
@@ -107,7 +123,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
       )
     }
 
-    return { props }
+    return res
   } else {
     // component v-model
     return transformModel(dir, node, context)
diff --git a/packages/server-renderer/src/helpers/vModelHelpers.ts b/packages/server-renderer/src/helpers/vModelHelpers.ts
new file mode 100644 (file)
index 0000000..2767c19
--- /dev/null
@@ -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 }
+  }
+}
index 099a2e0348cf2e32664f7b71fb4c50c15219410b..3f190b95d57e37adee2c3175da42efc81b8b4957 100644 (file)
@@ -17,7 +17,9 @@ export { interpolate as _interpolate } from './helpers/interpolate'
 export { renderList as _renderList } from './helpers/renderList'
 
 // v-model helpers
-import { looseEqual, looseIndexOf } from '@vue/shared'
-export const _looseEqual = looseEqual as (a: unknown, b: unknown) => boolean
-export const _looseContain = (arr: unknown[], value: unknown): boolean =>
-  looseIndexOf(arr, value) > -1
+export {
+  looseEqual as _looseEqual,
+  looseContain as _looseContain,
+  renderDynamicModel as _renderDynamicModel,
+  getDynamicModelProps as _getDynamicModelProps
+} from './helpers/vModelHelpers'