]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-core): create transform for v-model (#146)
authorRahul Kadyan <hi@znck.me>
Thu, 10 Oct 2019 14:33:58 +0000 (20:03 +0530)
committerEvan You <yyx990803@gmail.com>
Thu, 10 Oct 2019 14:33:58 +0000 (10:33 -0400)
packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap [new file with mode: 0644]
packages/compiler-core/__tests__/transforms/vModel.spec.ts [new file with mode: 0644]
packages/compiler-core/__tests__/utils.spec.ts
packages/compiler-core/src/errors.ts
packages/compiler-core/src/transforms/vModel.ts
packages/compiler-core/src/utils.ts

diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
new file mode 100644 (file)
index 0000000..ea11cf7
--- /dev/null
@@ -0,0 +1,97 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`compiler: transform v-model compound expression (with prefixIdentifiers) 1`] = `
+"import { createVNode, createBlock, openBlock } from \\"vue\\"
+
+export default function render() {
+  const _ctx = this
+  return (openBlock(), createBlock(\\"input\\", {
+    modelValue: _ctx.model[_ctx.index],
+    \\"onUpdate:modelValue\\": $event => (_ctx.model[_ctx.index] = $event)
+  }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
+}"
+`;
+
+exports[`compiler: transform v-model compound expression 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"input\\", {
+      modelValue: model[index],
+      \\"onUpdate:modelValue\\": $event => (model[index] = $event)
+    }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
+  }
+}"
+`;
+
+exports[`compiler: transform v-model simple exprssion (with prefixIdentifiers) 1`] = `
+"import { createVNode, createBlock, openBlock } from \\"vue\\"
+
+export default function render() {
+  const _ctx = this
+  return (openBlock(), createBlock(\\"input\\", {
+    modelValue: _ctx.model,
+    \\"onUpdate:modelValue\\": $event => (_ctx.model = $event)
+  }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
+}"
+`;
+
+exports[`compiler: transform v-model simple exprssion 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"input\\", {
+      modelValue: model,
+      \\"onUpdate:modelValue\\": $event => (model = $event)
+    }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
+  }
+}"
+`;
+
+exports[`compiler: transform v-model with argument 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"input\\", {
+      value: model,
+      \\"onUpdate:value\\": $event => (model = $event)
+    }, null, 8 /* PROPS */, [\\"value\\", \\"onUpdate:value\\"]))
+  }
+}"
+`;
+
+exports[`compiler: transform v-model with dynamic argument (with prefixIdentifiers) 1`] = `
+"import { createVNode, createBlock, openBlock } from \\"vue\\"
+
+export default function render() {
+  const _ctx = this
+  return (openBlock(), createBlock(\\"input\\", {
+    [_ctx.value]: _ctx.model,
+    [\\"onUpdate:\\"+_ctx.value]: $event => (_ctx.model = $event)
+  }, null, 16 /* FULL_PROPS */))
+}"
+`;
+
+exports[`compiler: transform v-model with dynamic argument 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"input\\", {
+      [value]: model,
+      [\\"onUpdate:\\"+value]: $event => (model = $event)
+    }, null, 16 /* FULL_PROPS */))
+  }
+}"
+`;
diff --git a/packages/compiler-core/__tests__/transforms/vModel.spec.ts b/packages/compiler-core/__tests__/transforms/vModel.spec.ts
new file mode 100644 (file)
index 0000000..818076a
--- /dev/null
@@ -0,0 +1,355 @@
+import {
+  parse,
+  transform,
+  generate,
+  ElementNode,
+  ObjectExpression,
+  CompilerOptions,
+  CallExpression
+} from '../../src'
+import { ErrorCodes } from '../../src/errors'
+import { transformModel } from '../../src/transforms/vModel'
+import { transformElement } from '../../src/transforms/transformElement'
+import { transformExpression } from '../../src/transforms/transformExpression'
+
+function parseWithVModel(template: string, options: CompilerOptions = {}) {
+  const ast = parse(template)
+
+  transform(ast, {
+    nodeTransforms: [transformExpression, transformElement],
+    directiveTransforms: {
+      ...options.directiveTransforms,
+      model: transformModel
+    },
+    ...options
+  })
+
+  return ast
+}
+
+describe('compiler: transform v-model', () => {
+  test('simple exprssion', () => {
+    const root = parseWithVModel('<input v-model="model" />')
+    const node = root.children[0] as ElementNode
+    const props = ((node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression).properties
+
+    expect(props[0]).toMatchObject({
+      key: {
+        content: 'modelValue',
+        isStatic: true
+      },
+      value: {
+        content: 'model',
+        isStatic: false
+      }
+    })
+
+    expect(props[1]).toMatchObject({
+      key: {
+        content: 'onUpdate:modelValue',
+        isStatic: true
+      },
+      value: {
+        children: [
+          '$event => (',
+          {
+            content: 'model',
+            isStatic: false
+          },
+          ' = $event)'
+        ]
+      }
+    })
+
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('simple exprssion (with prefixIdentifiers)', () => {
+    const root = parseWithVModel('<input v-model="model" />', {
+      prefixIdentifiers: true
+    })
+    const node = root.children[0] as ElementNode
+    const props = ((node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression).properties
+
+    expect(props[0]).toMatchObject({
+      key: {
+        content: 'modelValue',
+        isStatic: true
+      },
+      value: {
+        content: '_ctx.model',
+        isStatic: false
+      }
+    })
+
+    expect(props[1]).toMatchObject({
+      key: {
+        content: 'onUpdate:modelValue',
+        isStatic: true
+      },
+      value: {
+        children: [
+          '$event => (',
+          {
+            content: '_ctx.model',
+            isStatic: false
+          },
+          ' = $event)'
+        ]
+      }
+    })
+
+    expect(generate(root, { mode: 'module' }).code).toMatchSnapshot()
+  })
+
+  test('compound expression', () => {
+    const root = parseWithVModel('<input v-model="model[index]" />')
+    const node = root.children[0] as ElementNode
+    const props = ((node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression).properties
+
+    expect(props[0]).toMatchObject({
+      key: {
+        content: 'modelValue',
+        isStatic: true
+      },
+      value: {
+        content: 'model[index]',
+        isStatic: false
+      }
+    })
+
+    expect(props[1]).toMatchObject({
+      key: {
+        content: 'onUpdate:modelValue',
+        isStatic: true
+      },
+      value: {
+        children: [
+          '$event => (',
+          {
+            content: 'model[index]',
+            isStatic: false
+          },
+          ' = $event)'
+        ]
+      }
+    })
+
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('compound expression (with prefixIdentifiers)', () => {
+    const root = parseWithVModel('<input v-model="model[index]" />', {
+      prefixIdentifiers: true
+    })
+    const node = root.children[0] as ElementNode
+    const props = ((node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression).properties
+
+    expect(props[0]).toMatchObject({
+      key: {
+        content: 'modelValue',
+        isStatic: true
+      },
+      value: {
+        children: [
+          {
+            content: '_ctx.model',
+            isStatic: false
+          },
+          '[',
+          {
+            content: '_ctx.index',
+            isStatic: false
+          },
+          ']'
+        ]
+      }
+    })
+
+    expect(props[1]).toMatchObject({
+      key: {
+        content: 'onUpdate:modelValue',
+        isStatic: true
+      },
+      value: {
+        children: [
+          '$event => (',
+          {
+            content: '_ctx.model',
+            isStatic: false
+          },
+          '[',
+          {
+            content: '_ctx.index',
+            isStatic: false
+          },
+          ']',
+          ' = $event)'
+        ]
+      }
+    })
+
+    expect(generate(root, { mode: 'module' }).code).toMatchSnapshot()
+  })
+
+  test('with argument', () => {
+    const root = parseWithVModel('<input v-model:value="model" />')
+    const node = root.children[0] as ElementNode
+    const props = ((node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression).properties
+
+    expect(props[0]).toMatchObject({
+      key: {
+        content: 'value',
+        isStatic: true
+      },
+      value: {
+        content: 'model',
+        isStatic: false
+      }
+    })
+
+    expect(props[1]).toMatchObject({
+      key: {
+        content: 'onUpdate:value',
+        isStatic: true
+      },
+      value: {
+        children: [
+          '$event => (',
+          {
+            content: 'model',
+            isStatic: false
+          },
+          ' = $event)'
+        ]
+      }
+    })
+
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('with dynamic argument', () => {
+    const root = parseWithVModel('<input v-model:[value]="model" />')
+    const node = root.children[0] as ElementNode
+    const props = ((node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression).properties
+
+    expect(props[0]).toMatchObject({
+      key: {
+        content: 'value',
+        isStatic: false
+      },
+      value: {
+        content: 'model',
+        isStatic: false
+      }
+    })
+
+    expect(props[1]).toMatchObject({
+      key: {
+        children: [
+          {
+            content: 'onUpdate:',
+            isStatic: true
+          },
+          '+',
+          {
+            content: 'value',
+            isStatic: false
+          }
+        ]
+      },
+      value: {
+        children: [
+          '$event => (',
+          {
+            content: 'model',
+            isStatic: false
+          },
+          ' = $event)'
+        ]
+      }
+    })
+
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('with dynamic argument (with prefixIdentifiers)', () => {
+    const root = parseWithVModel('<input v-model:[value]="model" />', {
+      prefixIdentifiers: true
+    })
+    const node = root.children[0] as ElementNode
+    const props = ((node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression).properties
+
+    expect(props[0]).toMatchObject({
+      key: {
+        content: '_ctx.value',
+        isStatic: false
+      },
+      value: {
+        content: '_ctx.model',
+        isStatic: false
+      }
+    })
+
+    expect(props[1]).toMatchObject({
+      key: {
+        children: [
+          {
+            content: 'onUpdate:',
+            isStatic: true
+          },
+          '+',
+          {
+            content: '_ctx.value',
+            isStatic: false
+          }
+        ]
+      },
+      value: {
+        children: [
+          '$event => (',
+          {
+            content: '_ctx.model',
+            isStatic: false
+          },
+          ' = $event)'
+        ]
+      }
+    })
+
+    expect(generate(root, { mode: 'module' }).code).toMatchSnapshot()
+  })
+
+  describe('errors', () => {
+    test('missing expression', () => {
+      const onError = jest.fn()
+      parseWithVModel('<span v-model />', { onError })
+
+      expect(onError).toHaveBeenCalledTimes(1)
+      expect(onError).toHaveBeenCalledWith(
+        expect.objectContaining({
+          code: ErrorCodes.X_V_MODEL_NO_EXPRESSION
+        })
+      )
+    })
+
+    test('empty expression', () => {
+      const onError = jest.fn()
+      parseWithVModel('<span v-model="" />', { onError })
+
+      expect(onError).toHaveBeenCalledTimes(1)
+      expect(onError).toHaveBeenCalledWith(
+        expect.objectContaining({
+          code: ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION
+        })
+      )
+    })
+  })
+})
index b5d1bea04c95f2b894ea1b0768579e295337d86a..417d0d185341374a3fa02986472b8093e866fdac 100644 (file)
@@ -1,5 +1,9 @@
-import { Position } from '../src/ast'
-import { getInnerRange, advancePositionWithClone } from '../src/utils'
+import { Position, NodeTypes } from '../src/ast'
+import {
+  getInnerRange,
+  advancePositionWithClone,
+  isEmptyExpression
+} from '../src/utils'
 
 function p(line: number, column: number, offset: number): Position {
   return { column, line, offset }
@@ -67,3 +71,38 @@ describe('getInnerRange', () => {
     expect(loc2.end.offset).toBe(7)
   })
 })
+
+describe('isEmptyExpression', () => {
+  test('empty', () => {
+    expect(
+      isEmptyExpression({
+        content: '',
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        isStatic: true,
+        loc: null as any
+      })
+    ).toBe(true)
+  })
+
+  test('spaces', () => {
+    expect(
+      isEmptyExpression({
+        content: '  \t  ',
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        isStatic: true,
+        loc: null as any
+      })
+    ).toBe(true)
+  })
+
+  test('identifier', () => {
+    expect(
+      isEmptyExpression({
+        content: 'foo',
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        isStatic: true,
+        loc: null as any
+      })
+    ).toBe(false)
+  })
+})
index 8bd0089f91fd6437ca8c9add2c4d6c61a258e033..759f34579d13ba14d2dbce4a3ad3a6d0688f08f9 100644 (file)
@@ -79,6 +79,8 @@ export const enum ErrorCodes {
   X_V_SLOT_DUPLICATE_SLOT_NAMES,
   X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
   X_V_SLOT_MISPLACED,
+  X_V_MODEL_NO_EXPRESSION,
+  X_V_MODEL_MALFORMED_EXPRESSION,
 
   // generic errors
   X_PREFIX_ID_NOT_SUPPORTED,
@@ -167,6 +169,8 @@ export const errorMessages: { [code: number]: string } = {
     `Extraneous children found when component has explicit slots. ` +
     `These children will be ignored.`,
   [ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
+  [ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
+  [ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model has invalid expression.`,
 
   // generic errors
   [ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
index 70b786d12ed055a08b57f5cf47f717bf6a266301..3be84226932fd4850b3477e54b31bc6c04a88481 100644 (file)
@@ -1 +1,54 @@
-// TODO
+import { DirectiveTransform } from '../transform'
+import {
+  createSimpleExpression,
+  createObjectProperty,
+  createCompoundExpression,
+  NodeTypes,
+  Property
+} from '../ast'
+import { createCompilerError, ErrorCodes } from '../errors'
+import { isEmptyExpression } from '../utils'
+
+export const transformModel: DirectiveTransform = (dir, node, context) => {
+  const { exp, arg } = dir
+  if (!exp) {
+    context.onError(createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION))
+
+    return createTransformProps()
+  }
+
+  if (isEmptyExpression(exp)) {
+    context.onError(
+      createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION)
+    )
+
+    return createTransformProps()
+  }
+
+  const propName = arg ? arg : createSimpleExpression('modelValue', true)
+  const eventName = arg
+    ? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic
+      ? createSimpleExpression('onUpdate:' + arg.content, true)
+      : createCompoundExpression([
+          createSimpleExpression('onUpdate:', true),
+          '+',
+          ...(arg.type === NodeTypes.SIMPLE_EXPRESSION ? [arg] : arg.children)
+        ])
+    : createSimpleExpression('onUpdate:modelValue', true)
+
+  return createTransformProps([
+    createObjectProperty(propName, dir.exp!),
+    createObjectProperty(
+      eventName,
+      createCompoundExpression([
+        `$event => (`,
+        ...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
+        ` = $event)`
+      ])
+    )
+  ])
+}
+
+function createTransformProps(props: Property[] = []) {
+  return { props, needRuntime: false }
+}
index 83a71bb8f799d12034d6c95dd977dc9916985583..f6f28c684fb280dff5f52daeb438a8fb9eb0a903 100644 (file)
@@ -20,7 +20,8 @@ import {
   BlockCodegenNode,
   ElementCodegenNode,
   SlotOutletCodegenNode,
-  ComponentCodegenNode
+  ComponentCodegenNode,
+  ExpressionNode
 } from './ast'
 import { parse } from 'acorn'
 import { walk } from 'estree-walker'
@@ -237,3 +238,7 @@ export function toValidAssetId(
 ): string {
   return `_${type}_${name.replace(/[^\w]/g, '')}`
 }
+
+export function isEmptyExpression(node: ExpressionNode) {
+  return node.type === NodeTypes.SIMPLE_EXPRESSION && !node.content.trim()
+}