]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(compiler-ssr): dynamic v-bind + class/style merging
authorEvan You <yyx990803@gmail.com>
Tue, 4 Feb 2020 23:37:23 +0000 (18:37 -0500)
committerEvan You <yyx990803@gmail.com>
Tue, 4 Feb 2020 23:37:32 +0000 (18:37 -0500)
packages/compiler-core/src/options.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-dom/src/index.ts
packages/compiler-dom/src/transforms/transformStyle.ts
packages/compiler-ssr/__tests__/ssrElement.spec.ts
packages/compiler-ssr/src/index.ts
packages/compiler-ssr/src/transforms/ssrTransformElement.ts

index 8cb753a68821490ebb4f90c13886a94a2f0dbf97..f108edcf80e57133132b7116ca43024b2e1435a1 100644 (file)
@@ -29,7 +29,6 @@ export interface ParserOptions {
 export interface TransformOptions {
   nodeTransforms?: NodeTransform[]
   directiveTransforms?: Record<string, DirectiveTransform | undefined>
-  ssrDirectiveTransforms?: Record<string, DirectiveTransform | undefined>
   isBuiltInComponent?: (tag: string) => symbol | void
   // Transform expressions like {{ foo }} to `_ctx.foo`.
   // If this option is false, the generated code will be wrapped in a
index 940e7fe4eb763e90908581fadd54f9c6d90bc999..4959fb7fc2f1989dc8c90e335dc9d94832c7ba60 100644 (file)
@@ -114,7 +114,6 @@ function createTransformContext(
     cacheHandlers = false,
     nodeTransforms = [],
     directiveTransforms = {},
-    ssrDirectiveTransforms = {},
     isBuiltInComponent = NOOP,
     ssr = false,
     onError = defaultOnError
@@ -127,7 +126,6 @@ function createTransformContext(
     cacheHandlers,
     nodeTransforms,
     directiveTransforms,
-    ssrDirectiveTransforms,
     isBuiltInComponent,
     ssr,
     onError,
index a9203d8f8d1a13f9a91dce2078325f0aaf804138..23ece5ed47483845310405e958c27a2828ce7b0c 100644 (file)
@@ -233,7 +233,8 @@ export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
 export function buildProps(
   node: ElementNode,
   context: TransformContext,
-  props: ElementNode['props'] = node.props
+  props: ElementNode['props'] = node.props,
+  ssr = false
 ): {
   props: PropsExpression | undefined
   directives: DirectiveNode[]
@@ -320,9 +321,15 @@ export function buildProps(
         continue
       }
 
-      // special case for v-bind and v-on with no argument
       const isBind = name === 'bind'
       const isOn = name === 'on'
+
+      // skip v-on in SSR compilation
+      if (ssr && isOn) {
+        continue
+      }
+
+      // special case for v-bind and v-on with no argument
       if (!arg && (isBind || isOn)) {
         hasDynamicKeys = true
         if (exp) {
@@ -360,7 +367,7 @@ export function buildProps(
       if (directiveTransform) {
         // has built-in directive transform.
         const { props, needRuntime } = directiveTransform(prop, node, context)
-        props.forEach(analyzePatchFlag)
+        !ssr && props.forEach(analyzePatchFlag)
         properties.push(...props)
         if (needRuntime) {
           runtimeDirectives.push(prop)
@@ -446,12 +453,7 @@ function dedupeProperties(properties: Property[]): Property[] {
     const name = prop.key.content
     const existing = knownProps.get(name)
     if (existing) {
-      if (
-        name === 'style' ||
-        name === 'class' ||
-        name.startsWith('on') ||
-        name.startsWith('vnode')
-      ) {
+      if (name === 'style' || name === 'class' || name.startsWith('on')) {
         mergeAsArray(existing, prop)
       }
       // unexpected duplicate, should have emitted error during parse
index 6013ba931d1e76a255f259f5082865824a98eda2..01673e847a8eb5090a61de025d52ea1aefc97ab9 100644 (file)
@@ -56,5 +56,6 @@ export function parse(template: string, options: ParserOptions = {}): RootNode {
   })
 }
 
+export { transformStyle } from './transforms/transformStyle'
 export { DOMErrorCodes } from './errors'
 export * from '@vue/compiler-core'
index cb58f53a39cfe0f89bc8583a3514121353b03bf9..3c232db4357447239fe580a79b5bacd83e4374ca 100644 (file)
@@ -1,7 +1,9 @@
 import {
   NodeTransform,
   NodeTypes,
-  createSimpleExpression
+  createSimpleExpression,
+  SimpleExpressionNode,
+  SourceLocation
 } from '@vue/compiler-core'
 
 // Parse inline CSS strings for static style attributes into an object.
@@ -15,8 +17,7 @@ export const transformStyle: NodeTransform = (node, context) => {
     node.props.forEach((p, i) => {
       if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
         // replace p with an expression node
-        const parsed = JSON.stringify(parseInlineCSS(p.value.content))
-        const exp = context.hoist(createSimpleExpression(parsed, false, p.loc))
+        const exp = context.hoist(parseInlineCSS(p.value.content, p.loc))
         node.props[i] = {
           type: NodeTypes.DIRECTIVE,
           name: `bind`,
@@ -33,7 +34,10 @@ export const transformStyle: NodeTransform = (node, context) => {
 const listDelimiterRE = /;(?![^(]*\))/g
 const propertyDelimiterRE = /:(.+)/
 
-function parseInlineCSS(cssText: string): Record<string, string> {
+function parseInlineCSS(
+  cssText: string,
+  loc: SourceLocation
+): SimpleExpressionNode {
   const res: Record<string, string> = {}
   cssText.split(listDelimiterRE).forEach(item => {
     if (item) {
@@ -41,5 +45,5 @@ function parseInlineCSS(cssText: string): Record<string, string> {
       tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
     }
   })
-  return res
+  return createSimpleExpression(JSON.stringify(res), false, loc)
 }
index 994fbeb8d17f63427750370cb28e1df2916f051e..7d807e4692b6d0a5843130bcd84935360f46835a 100644 (file)
@@ -46,6 +46,10 @@ describe('ssr: element', () => {
         getCompiledString(`<textarea value="fo&gt;o"/>`)
       ).toMatchInlineSnapshot(`"\`<textarea>fo&gt;o</textarea>\`"`)
     })
+
+    test('<textarea> with dynamic v-bind', () => {
+      // TODO
+    })
   })
 
   describe('attrs', () => {
@@ -63,6 +67,14 @@ describe('ssr: element', () => {
       )
     })
 
+    test('static class + v-bind:class', () => {
+      expect(
+        getCompiledString(`<div class="foo" :class="bar"></div>`)
+      ).toMatchInlineSnapshot(
+        `"\`<div\${_renderClass([_ctx.bar, \\"foo\\"])}></div>\`"`
+      )
+    })
+
     test('v-bind:style', () => {
       expect(
         getCompiledString(`<div id="foo" :style="bar"></div>`)
@@ -71,6 +83,14 @@ describe('ssr: element', () => {
       )
     })
 
+    test('static style + v-bind:style', () => {
+      expect(
+        getCompiledString(`<div style="color:red;" :style="bar"></div>`)
+      ).toMatchInlineSnapshot(
+        `"\`<div\${_renderStyle([_hoisted_1, _ctx.bar])}></div>\`"`
+      )
+    })
+
     test('v-bind:key (boolean)', () => {
       expect(
         getCompiledString(`<input type="checkbox" :checked="checked">`)
@@ -86,5 +106,85 @@ describe('ssr: element', () => {
         `"\`<div\${_renderAttr(\\"id\\", _ctx.id)} class=\\"bar\\"></div>\`"`
       )
     })
+
+    test('v-bind:[key]', () => {
+      expect(
+        getCompiledString(`<div v-bind:[key]="value"></div>`)
+      ).toMatchInlineSnapshot(
+        `"\`<div\${_renderAttrs({ [_ctx.key]: _ctx.value })}></div>\`"`
+      )
+
+      expect(getCompiledString(`<div class="foo" v-bind:[key]="value"></div>`))
+        .toMatchInlineSnapshot(`
+        "\`<div\${_renderAttrs({
+            class: \\"foo\\",
+            [_ctx.key]: _ctx.value
+          })}></div>\`"
+      `)
+
+      expect(getCompiledString(`<div :id="id" v-bind:[key]="value"></div>`))
+        .toMatchInlineSnapshot(`
+        "\`<div\${_renderAttrs({
+            id: _ctx.id,
+            [_ctx.key]: _ctx.value
+          })}></div>\`"
+      `)
+    })
+
+    test('v-bind="obj"', () => {
+      expect(
+        getCompiledString(`<div v-bind="obj"></div>`)
+      ).toMatchInlineSnapshot(`"\`<div\${_renderAttrs(_ctx.obj)}></div>\`"`)
+
+      expect(
+        getCompiledString(`<div class="foo" v-bind="obj"></div>`)
+      ).toMatchInlineSnapshot(
+        `"\`<div\${_renderAttrs(mergeProps({ class: \\"foo\\" }, _ctx.obj))}></div>\`"`
+      )
+
+      expect(
+        getCompiledString(`<div :id="id" v-bind="obj"></div>`)
+      ).toMatchInlineSnapshot(
+        `"\`<div\${_renderAttrs(mergeProps({ id: _ctx.id }, _ctx.obj))}></div>\`"`
+      )
+
+      // dynamic key + v-bind="object"
+      expect(
+        getCompiledString(`<div :[key]="id" v-bind="obj"></div>`)
+      ).toMatchInlineSnapshot(
+        `"\`<div\${_renderAttrs(mergeProps({ [_ctx.key]: _ctx.id }, _ctx.obj))}></div>\`"`
+      )
+
+      // should merge class and :class
+      expect(getCompiledString(`<div class="a" :class="b" v-bind="obj"></div>`))
+        .toMatchInlineSnapshot(`
+        "\`<div\${_renderAttrs(mergeProps({
+            class: [\\"a\\", _ctx.b]
+          }, _ctx.obj))}></div>\`"
+      `)
+
+      // should merge style and :style
+      expect(
+        getCompiledString(
+          `<div style="color:red;" :style="b" v-bind="obj"></div>`
+        )
+      ).toMatchInlineSnapshot(`
+        "\`<div\${_renderAttrs(mergeProps({
+            style: [_hoisted_1, _ctx.b]
+          }, _ctx.obj))}></div>\`"
+      `)
+    })
+
+    test('should ignore v-on', () => {
+      expect(
+        getCompiledString(`<div id="foo" @click="bar"/>`)
+      ).toMatchInlineSnapshot(`"\`<div id=\\"foo\\"></div>\`"`)
+      expect(
+        getCompiledString(`<div id="foo" v-on="bar"/>`)
+      ).toMatchInlineSnapshot(`"\`<div id=\\"foo\\"></div>\`"`)
+      expect(
+        getCompiledString(`<div v-bind="foo" v-on="bar"/>`)
+      ).toMatchInlineSnapshot(`"\`<div\${_renderAttrs(_ctx.foo)}></div>\`"`)
+    })
   })
 })
index 295c923ad59cd97cf0e806d0b527cae42d980543..e018e2d905db285f79fe88344bf866d27307b57b 100644 (file)
@@ -9,7 +9,8 @@ import {
   trackVForSlotScopes,
   trackSlotScopes,
   noopDirectiveTransform,
-  transformBind
+  transformBind,
+  transformStyle
 } from '@vue/compiler-dom'
 import { ssrCodegenTransform } from './ssrCodegenTransform'
 import { ssrTransformElement } from './transforms/ssrTransformElement'
@@ -49,15 +50,20 @@ export function compile(
       ssrTransformElement,
       ssrTransformComponent,
       trackSlotScopes,
+      transformStyle,
       ...(options.nodeTransforms || []) // user transforms
     ],
-    ssrDirectiveTransforms: {
-      on: noopDirectiveTransform,
-      cloak: noopDirectiveTransform,
-      bind: transformBind, // reusing core v-bind
+    directiveTransforms: {
+      // reusing core v-bind
+      bind: transformBind,
+      // model and show has dedicated SSR handling
       model: ssrTransformModel,
       show: ssrTransformShow,
-      ...(options.ssrDirectiveTransforms || {}) // user transforms
+      // the following are ignored during SSR
+      on: noopDirectiveTransform,
+      cloak: noopDirectiveTransform,
+      once: noopDirectiveTransform,
+      ...(options.directiveTransforms || {}) // user transforms
     }
   })
 
index 7d86478283edd08228b098ccc4ff48239760a0a2..bea23c677911071412a512e348f1d6344ee7b6b5 100644 (file)
@@ -7,7 +7,16 @@ import {
   createInterpolation,
   createCallExpression,
   createConditionalExpression,
-  createSimpleExpression
+  createSimpleExpression,
+  buildProps,
+  DirectiveNode,
+  PlainElementNode,
+  createCompilerError,
+  ErrorCodes,
+  CallExpression,
+  createArrayExpression,
+  ExpressionNode,
+  JSChildNode
 } from '@vue/compiler-dom'
 import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared'
 import { createSSRCompilerError, SSRErrorCodes } from '../errors'
@@ -15,7 +24,8 @@ import {
   SSR_RENDER_ATTR,
   SSR_RENDER_CLASS,
   SSR_RENDER_STYLE,
-  SSR_RENDER_DYNAMIC_ATTR
+  SSR_RENDER_DYNAMIC_ATTR,
+  SSR_RENDER_ATTRS
 } from '../runtimeHelpers'
 
 export const ssrTransformElement: NodeTransform = (node, context) => {
@@ -40,11 +50,22 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
           p.arg.type !== NodeTypes.SIMPLE_EXPRESSION || // v-bind:[_ctx.foo]
             !p.arg.isStatic) // v-bind:[foo]
       )
-
       if (hasDynamicVBind) {
-        // TODO
+        const { props } = buildProps(node, context, node.props, true /* ssr */)
+        if (props) {
+          openTag.push(
+            createCallExpression(context.helper(SSR_RENDER_ATTRS), [props])
+          )
+        }
       }
 
+      // book keeping static/dynamic class merging.
+      let dynamicClassBinding: CallExpression | undefined = undefined
+      let staticClassBinding: string | undefined = undefined
+      // all style bindings are converted to dynamic by transformStyle.
+      // but we need to make sure to merge them.
+      let dynamicStyleBinding: CallExpression | undefined = undefined
+
       for (let i = 0; i < node.props.length; i++) {
         const prop = node.props[i]
         // special cases with children override
@@ -54,22 +75,28 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
             rawChildren = prop.exp
           } else if (prop.name === 'text' && prop.exp) {
             node.children = [createInterpolation(prop.exp, prop.loc)]
-          } else if (
-            // v-bind:value on textarea
-            node.tag === 'textarea' &&
-            prop.name === 'bind' &&
-            prop.exp &&
-            prop.arg &&
-            prop.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
-            prop.arg.isStatic &&
-            prop.arg.content === 'value'
-          ) {
-            node.children = [createInterpolation(prop.exp, prop.loc)]
-            // TODO handle <textrea> with dynamic v-bind
-          } else if (!hasDynamicVBind) {
+          } else if (prop.name === 'slot') {
+            context.onError(
+              createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc)
+            )
+          } else if (isTextareaWithValue(node, prop) && prop.exp) {
+            if (!hasDynamicVBind) {
+              node.children = [createInterpolation(prop.exp, prop.loc)]
+            } else {
+              // TODO handle <textrea> with dynamic v-bind
+            }
+          } else {
             // Directive transforms.
-            const directiveTransform = context.ssrDirectiveTransforms[prop.name]
-            if (directiveTransform) {
+            const directiveTransform = context.directiveTransforms[prop.name]
+            if (!directiveTransform) {
+              // no corresponding ssr directive transform found.
+              context.onError(
+                createSSRCompilerError(
+                  SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM,
+                  prop.loc
+                )
+              )
+            } else if (!hasDynamicVBind) {
               const { props } = directiveTransform(prop, node, context)
               for (let j = 0; j < props.length; j++) {
                 const { key, value } = props[j]
@@ -78,16 +105,23 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
                   // static key attr
                   if (attrName === 'class') {
                     openTag.push(
-                      createCallExpression(context.helper(SSR_RENDER_CLASS), [
-                        value
-                      ])
+                      (dynamicClassBinding = createCallExpression(
+                        context.helper(SSR_RENDER_CLASS),
+                        [value]
+                      ))
                     )
                   } else if (attrName === 'style') {
-                    openTag.push(
-                      createCallExpression(context.helper(SSR_RENDER_STYLE), [
-                        value
-                      ])
-                    )
+                    if (dynamicStyleBinding) {
+                      // already has style binding, merge into it.
+                      mergeCall(dynamicStyleBinding, value)
+                    } else {
+                      openTag.push(
+                        (dynamicStyleBinding = createCallExpression(
+                          context.helper(SSR_RENDER_STYLE),
+                          [value]
+                        ))
+                      )
+                    }
                   } else if (isBooleanAttr(attrName)) {
                     openTag.push(
                       createConditionalExpression(
@@ -126,14 +160,6 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
                   )
                 }
               }
-            } else {
-              // no corresponding ssr directive transform found.
-              context.onError(
-                createSSRCompilerError(
-                  SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM,
-                  prop.loc
-                )
-              )
             }
           }
         } else {
@@ -143,6 +169,9 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
             rawChildren = escapeHtml(prop.value.content)
           } else if (!hasDynamicVBind) {
             // static prop
+            if (prop.name === 'class' && prop.value) {
+              staticClassBinding = JSON.stringify(prop.value.content)
+            }
             openTag.push(
               ` ${prop.name}` +
                 (prop.value ? `="${escapeHtml(prop.value.content)}"` : ``)
@@ -151,6 +180,12 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
         }
       }
 
+      // handle co-existence of dynamic + static class bindings
+      if (dynamicClassBinding && staticClassBinding) {
+        mergeCall(dynamicClassBinding, staticClassBinding)
+        removeStaticBinding(openTag, 'class')
+      }
+
       openTag.push(`>`)
       if (rawChildren) {
         openTag.push(rawChildren)
@@ -159,3 +194,34 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
     }
   }
 }
+
+function isTextareaWithValue(
+  node: PlainElementNode,
+  prop: DirectiveNode
+): boolean {
+  return !!(
+    node.tag === 'textarea' &&
+    prop.name === 'bind' &&
+    prop.arg &&
+    prop.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
+    prop.arg.isStatic &&
+    prop.arg.content === 'value'
+  )
+}
+
+function mergeCall(call: CallExpression, arg: string | JSChildNode) {
+  const existing = call.arguments[0] as ExpressionNode
+  call.arguments[0] = createArrayExpression([existing, arg])
+}
+
+function removeStaticBinding(
+  tag: TemplateLiteral['elements'],
+  binding: string
+) {
+  const i = tag.findIndex(
+    e => typeof e === 'string' && e.startsWith(` ${binding}=`)
+  )
+  if (i > -1) {
+    tag.splice(i, 1)
+  }
+}