]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: support v-bind .prop & .attr modifiers
authorEvan You <yyx990803@gmail.com>
Tue, 13 Jul 2021 19:58:18 +0000 (15:58 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 16 Jul 2021 18:30:49 +0000 (14:30 -0400)
Also allows render function usage like the following:

```js
h({
  '.prop': 1, // force set as property
  '^attr': 'foo' // force set as attribute
})
```

packages/compiler-core/__tests__/parse.spec.ts
packages/compiler-core/__tests__/transforms/vBind.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/parse.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/vBind.ts
packages/compiler-core/src/transforms/vOn.ts
packages/runtime-dom/__tests__/patchProps.spec.ts
packages/runtime-dom/src/patchProp.ts

index 1c7c878539abde49133f5d7f5a0adf394771a8ae..d677f75dd747b4770e15a96f15ba1eab83fa4978 100644 (file)
@@ -1276,6 +1276,54 @@ describe('compiler: parse', () => {
       })
     })
 
+    test('v-bind .prop shorthand', () => {
+      const ast = baseParse('<div .a=b />')
+      const directive = (ast.children[0] as ElementNode).props[0]
+
+      expect(directive).toStrictEqual({
+        type: NodeTypes.DIRECTIVE,
+        name: 'bind',
+        arg: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'a',
+          isStatic: true,
+          constType: ConstantTypes.CAN_STRINGIFY,
+
+          loc: {
+            source: 'a',
+            start: {
+              column: 7,
+              line: 1,
+              offset: 6
+            },
+            end: {
+              column: 8,
+              line: 1,
+              offset: 7
+            }
+          }
+        },
+        modifiers: ['prop'],
+        exp: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'b',
+          isStatic: false,
+          constType: ConstantTypes.NOT_CONSTANT,
+
+          loc: {
+            start: { offset: 8, line: 1, column: 9 },
+            end: { offset: 9, line: 1, column: 10 },
+            source: 'b'
+          }
+        },
+        loc: {
+          start: { offset: 5, line: 1, column: 6 },
+          end: { offset: 9, line: 1, column: 10 },
+          source: '.a=b'
+        }
+      })
+    })
+
     test('v-bind shorthand with modifier', () => {
       const ast = baseParse('<div :a.sync=b />')
       const directive = (ast.children[0] as ElementNode).props[0]
index 287be477bb3511813e7fb05dcf00bf2681d692a3..27e0ae10c8124410a24b9e824645e7232e77d083 100644 (file)
@@ -172,22 +172,140 @@ describe('compiler: transform v-bind', () => {
     const node = parseWithVBind(`<div v-bind:[foo(bar)].camel="id"/>`, {
       prefixIdentifiers: true
     })
+    const props = (node.codegenNode as VNodeCall).props as CallExpression
+    expect(props).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: NORMALIZE_PROPS,
+      arguments: [
+        {
+          type: NodeTypes.JS_OBJECT_EXPRESSION,
+          properties: [
+            {
+              key: {
+                children: [
+                  `_${helperNameMap[CAMELIZE]}(`,
+                  `(`,
+                  { content: `_ctx.foo` },
+                  `(`,
+                  { content: `_ctx.bar` },
+                  `)`,
+                  `) || ""`,
+                  `)`
+                ]
+              },
+              value: {
+                content: `_ctx.id`,
+                isStatic: false
+              }
+            }
+          ]
+        }
+      ]
+    })
+  })
+
+  test('.prop modifier', () => {
+    const node = parseWithVBind(`<div v-bind:fooBar.prop="id"/>`)
     const props = (node.codegenNode as VNodeCall).props as ObjectExpression
     expect(props.properties[0]).toMatchObject({
       key: {
-        children: [
-          `_${helperNameMap[CAMELIZE]}(`,
-          `(`,
-          { content: `_ctx.foo` },
-          `(`,
-          { content: `_ctx.bar` },
-          `)`,
-          `) || ""`,
-          `)`
-        ]
+        content: `.fooBar`,
+        isStatic: true
       },
       value: {
-        content: `_ctx.id`,
+        content: `id`,
+        isStatic: false
+      }
+    })
+  })
+
+  test('.prop modifier w/ dynamic arg', () => {
+    const node = parseWithVBind(`<div v-bind:[fooBar].prop="id"/>`)
+    const props = (node.codegenNode as VNodeCall).props as CallExpression
+    expect(props).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: NORMALIZE_PROPS,
+      arguments: [
+        {
+          type: NodeTypes.JS_OBJECT_EXPRESSION,
+          properties: [
+            {
+              key: {
+                content: '`.${fooBar || ""}`',
+                isStatic: false
+              },
+              value: {
+                content: `id`,
+                isStatic: false
+              }
+            }
+          ]
+        }
+      ]
+    })
+  })
+
+  test('.prop modifier w/ dynamic arg + prefixIdentifiers', () => {
+    const node = parseWithVBind(`<div v-bind:[foo(bar)].prop="id"/>`, {
+      prefixIdentifiers: true
+    })
+    const props = (node.codegenNode as VNodeCall).props as CallExpression
+    expect(props).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: NORMALIZE_PROPS,
+      arguments: [
+        {
+          type: NodeTypes.JS_OBJECT_EXPRESSION,
+          properties: [
+            {
+              key: {
+                children: [
+                  `'.' + (`,
+                  `(`,
+                  { content: `_ctx.foo` },
+                  `(`,
+                  { content: `_ctx.bar` },
+                  `)`,
+                  `) || ""`,
+                  `)`
+                ]
+              },
+              value: {
+                content: `_ctx.id`,
+                isStatic: false
+              }
+            }
+          ]
+        }
+      ]
+    })
+  })
+
+  test('.prop modifier (shorthand)', () => {
+    const node = parseWithVBind(`<div .fooBar="id"/>`)
+    const props = (node.codegenNode as VNodeCall).props as ObjectExpression
+    expect(props.properties[0]).toMatchObject({
+      key: {
+        content: `.fooBar`,
+        isStatic: true
+      },
+      value: {
+        content: `id`,
+        isStatic: false
+      }
+    })
+  })
+
+  test('.attr modifier', () => {
+    const node = parseWithVBind(`<div v-bind:foo-bar.attr="id"/>`)
+    const props = (node.codegenNode as VNodeCall).props as ObjectExpression
+    expect(props.properties[0]).toMatchObject({
+      key: {
+        content: `^foo-bar`,
+        isStatic: true
+      },
+      value: {
+        content: `id`,
         isStatic: false
       }
     })
index b087a9845d631689caf4397358c6ad0b8dadba78..7bd714694d8a673f97dc3222d1e19e21b893338a 100644 (file)
@@ -221,6 +221,7 @@ export interface SimpleExpressionNode extends Node {
    * the identifiers declared inside the function body.
    */
   identifiers?: string[]
+  isHandlerKey?: boolean
 }
 
 export interface InterpolationNode extends Node {
@@ -243,6 +244,7 @@ export interface CompoundExpressionNode extends Node {
    * the identifiers declared inside the function body.
    */
   identifiers?: string[]
+  isHandlerKey?: boolean
 }
 
 export interface IfNode extends Node {
index 9e6ee3260961f43ff7850df50bab0089bf415fdd..6c1863f210b9f098d4a7865102b41d1f2a123baf 100644 (file)
@@ -772,14 +772,19 @@ function parseAttribute(
   }
   const loc = getSelection(context, start)
 
-  if (!context.inVPre && /^(v-|:|@|#)/.test(name)) {
-    const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
+  if (!context.inVPre && /^(v-|:|\.|@|#)/.test(name)) {
+    const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
       name
     )!
 
+    let isPropShorthand = startsWith(name, '.')
     let dirName =
       match[1] ||
-      (startsWith(name, ':') ? 'bind' : startsWith(name, '@') ? 'on' : 'slot')
+      (isPropShorthand || startsWith(name, ':')
+        ? 'bind'
+        : startsWith(name, '@')
+          ? 'on'
+          : 'slot')
     let arg: ExpressionNode | undefined
 
     if (match[2]) {
@@ -835,6 +840,7 @@ function parseAttribute(
     }
 
     const modifiers = match[3] ? match[3].substr(1).split('.') : []
+    if (isPropShorthand) modifiers.push('prop')
 
     // 2.x compat v-bind:foo.sync -> v-model:foo
     if (__COMPAT__ && dirName === 'bind' && arg) {
index 697e8e294a2dfb92d3082e457c83170db3a23a88..469670fc15936047a7ae19c1e01a8ba9c364d8b5 100644 (file)
@@ -700,21 +700,26 @@ export function buildProps(
         // but still need to deal with dynamic key binding
         let classKeyIndex = -1
         let styleKeyIndex = -1
-        let dynamicKeyIndex = -1
+        let hasDynamicKey = false
 
         for (let i = 0; i < propsExpression.properties.length; i++) {
-          const p = propsExpression.properties[i]
-          if (p.key.type !== NodeTypes.SIMPLE_EXPRESSION) continue
-          if (!isStaticExp(p.key)) dynamicKeyIndex = i
-          if (isStaticExp(p.key) && p.key.content === 'class') classKeyIndex = i
-          if (isStaticExp(p.key) && p.key.content === 'style') styleKeyIndex = i
+          const key = propsExpression.properties[i].key
+          if (isStaticExp(key)) {
+            if (key.content === 'class') {
+              classKeyIndex = i
+            } else if (key.content === 'style') {
+              styleKeyIndex = i
+            }
+          } else if (!key.isHandlerKey) {
+            hasDynamicKey = true
+          }
         }
 
         const classProp = propsExpression.properties[classKeyIndex]
         const styleProp = propsExpression.properties[styleKeyIndex]
 
         // no dynamic key
-        if (dynamicKeyIndex === -1) {
+        if (!hasDynamicKey) {
           if (classProp && !isStaticExp(classProp.value)) {
             classProp.value = createCallExpression(
               context.helper(NORMALIZE_CLASS),
index c1faed1a08728cba3fcc1e4e165ebeed9ae88a0e..e3ffed425e8f67ca20a6e9b1fcb35e57493f07aa 100644 (file)
@@ -1,5 +1,10 @@
 import { DirectiveTransform } from '../transform'
-import { createObjectProperty, createSimpleExpression, NodeTypes } from '../ast'
+import {
+  createObjectProperty,
+  createSimpleExpression,
+  ExpressionNode,
+  NodeTypes
+} from '../ast'
 import { createCompilerError, ErrorCodes } from '../errors'
 import { camelize } from '@vue/shared'
 import { CAMELIZE } from '../runtimeHelpers'
@@ -18,7 +23,6 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
     arg.content = `${arg.content} || ""`
   }
 
-  // .prop is no longer necessary due to new patch behavior
   // .sync is replaced by v-model:arg
   if (modifiers.includes('camel')) {
     if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
@@ -33,6 +37,14 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
     }
   }
 
+  if (modifiers.includes('prop')) {
+    injectPrefix(arg, '.')
+  }
+
+  if (modifiers.includes('attr')) {
+    injectPrefix(arg, '^')
+  }
+
   if (
     !exp ||
     (exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim())
@@ -47,3 +59,16 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
     props: [createObjectProperty(arg!, exp)]
   }
 }
+
+const injectPrefix = (arg: ExpressionNode, prefix: string) => {
+  if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
+    if (arg.isStatic) {
+      arg.content = prefix + arg.content
+    } else {
+      arg.content = `\`${prefix}\${${arg.content}}\``
+    }
+  } else {
+    arg.children.unshift(`'${prefix}' + (`)
+    arg.children.push(`)`)
+  }
+}
index bf51a5f62411d9ce3d954cce7984964eaa8fa5a6..68fd77b49bd38f0246f49ce33f1bb8327fec0fa3 100644 (file)
@@ -163,5 +163,7 @@ export const transformOn: DirectiveTransform = (
     ret.props[0].value = context.cache(ret.props[0].value)
   }
 
+  // mark the key as handler for props normalization check
+  ret.props.forEach(p => (p.key.isHandlerKey = true))
   return ret
 }
index 46cd8dc1e5f7658bec3160d55edc47f954731412..abcfb2c7123728885a115e6d7cf72432d26af4c1 100644 (file)
@@ -171,6 +171,20 @@ describe('runtime-dom: props patching', () => {
     patchProp(el, 'type', 'text', null)
   })
 
+  test('force patch as prop', () => {
+    const el = document.createElement('div') as any
+    patchProp(el, '.x', null, 1)
+    expect(el.x).toBe(1)
+  })
+
+  test('force patch as attribute', () => {
+    const el = document.createElement('div') as any
+    el.x = 1
+    patchProp(el, '^x', null, 2)
+    expect(el.x).toBe(1)
+    expect(el.getAttribute('x')).toBe('2')
+  })
+
   test('input with size', () => {
     const el = document.createElement('input')
     patchProp(el, 'size', null, 100)
index 2754f7426e159fbb35222843584af7949a40ecc2..125b64d0edfbf268f08a35adda34e0ff7748878a 100644 (file)
@@ -24,43 +24,42 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
   parentSuspense,
   unmountChildren
 ) => {
-  switch (key) {
-    // special
-    case 'class':
-      patchClass(el, nextValue, isSVG)
-      break
-    case 'style':
-      patchStyle(el, prevValue, nextValue)
-      break
-    default:
-      if (isOn(key)) {
-        // ignore v-model listeners
-        if (!isModelListener(key)) {
-          patchEvent(el, key, prevValue, nextValue, parentComponent)
-        }
-      } else if (shouldSetAsProp(el, key, nextValue, isSVG)) {
-        patchDOMProp(
-          el,
-          key,
-          nextValue,
-          prevChildren,
-          parentComponent,
-          parentSuspense,
-          unmountChildren
-        )
-      } else {
-        // special case for <input v-model type="checkbox"> with
-        // :true-value & :false-value
-        // store value as dom properties since non-string values will be
-        // stringified.
-        if (key === 'true-value') {
-          ;(el as any)._trueValue = nextValue
-        } else if (key === 'false-value') {
-          ;(el as any)._falseValue = nextValue
-        }
-        patchAttr(el, key, nextValue, isSVG, parentComponent)
-      }
-      break
+  if (key === 'class') {
+    patchClass(el, nextValue, isSVG)
+  } else if (key === 'style') {
+    patchStyle(el, prevValue, nextValue)
+  } else if (isOn(key)) {
+    // ignore v-model listeners
+    if (!isModelListener(key)) {
+      patchEvent(el, key, prevValue, nextValue, parentComponent)
+    }
+  } else if (
+    key[0] === '.'
+      ? ((key = key.slice(1)), true)
+      : key[0] === '^'
+        ? ((key = key.slice(1)), false)
+        : shouldSetAsProp(el, key, nextValue, isSVG)
+  ) {
+    patchDOMProp(
+      el,
+      key,
+      nextValue,
+      prevChildren,
+      parentComponent,
+      parentSuspense,
+      unmountChildren
+    )
+  } else {
+    // special case for <input v-model type="checkbox"> with
+    // :true-value & :false-value
+    // store value as dom properties since non-string values will be
+    // stringified.
+    if (key === 'true-value') {
+      ;(el as any)._trueValue = nextValue
+    } else if (key === 'false-value') {
+      ;(el as any)._falseValue = nextValue
+    }
+    patchAttr(el, key, nextValue, isSVG, parentComponent)
   }
 }