Просмотр исходного кода

feat(compiler-vapor): v-model for component (#180)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
Jevon 2 лет назад
Родитель
Сommit
1f28ae15cd

+ 81 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap

@@ -1,5 +1,86 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 
+exports[`compiler: vModel transform > component > v-model for component should generate modelModifiers 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+  const n0 = _createComponent(_resolveComponent("Comp"), [{
+    modelValue: () => (_ctx.foo),
+    "onUpdate:modelValue": () => $event => (_ctx.foo = $event),
+    modelModifiers: () => ({ trim: true, "bar-baz": true })
+  }], true)
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > component > v-model for component should work 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+  const n0 = _createComponent(_resolveComponent("Comp"), [{
+    modelValue: () => (_ctx.foo),
+    "onUpdate:modelValue": () => $event => (_ctx.foo = $event)
+  }], true)
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > component > v-model with arguments for component should generate modelModifiers 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+  const n0 = _createComponent(_resolveComponent("Comp"), [{
+    foo: () => (_ctx.foo),
+    "onUpdate:foo": () => $event => (_ctx.foo = $event),
+    fooModifiers: () => ({ trim: true }), 
+    bar: () => (_ctx.bar),
+    "onUpdate:bar": () => $event => (_ctx.bar = $event),
+    barModifiers: () => ({ number: true })
+  }], true)
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > component > v-model with arguments for component should work 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+  const n0 = _createComponent(_resolveComponent("Comp"), [{
+    bar: () => (_ctx.foo),
+    "onUpdate:bar": () => $event => (_ctx.foo = $event)
+  }], true)
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > component > v-model with dynamic arguments for component should generate modelModifiers  1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+  const n0 = _createComponent(_resolveComponent("Comp"), [{
+    [_ctx.foo]: () => (_ctx.foo),
+    ["onUpdate:" + _ctx.foo]: () => $event => (_ctx.foo = $event),
+    [_ctx.foo + "Modifiers"]: () => ({ trim: true }), 
+    [_ctx.bar]: () => (_ctx.bar),
+    ["onUpdate:" + _ctx.bar]: () => $event => (_ctx.bar = $event),
+    [_ctx.bar + "Modifiers"]: () => ({ number: true })
+  }], true)
+  return n0
+}"
+`;
+
+exports[`compiler: vModel transform > component > v-model with dynamic arguments for component should work 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+  const n0 = _createComponent(_resolveComponent("Comp"), [{
+    [_ctx.arg]: () => (_ctx.foo),
+    ["onUpdate:" + _ctx.arg]: () => $event => (_ctx.foo = $event)
+  }], true)
+  return n0
+}"
+`;
+
 exports[`compiler: vModel transform > modifiers > .lazy 1`] = `
 exports[`compiler: vModel transform > modifiers > .lazy 1`] = `
 "import { vModelText as _vModelText, withDirectives as _withDirectives, delegate as _delegate, template as _template } from 'vue/vapor';
 "import { vModelText as _vModelText, withDirectives as _withDirectives, delegate as _delegate, template as _template } from 'vue/vapor';
 const t0 = _template("<input>")
 const t0 = _template("<input>")

+ 171 - 1
packages/compiler-vapor/__tests__/transforms/vModel.spec.ts

@@ -1,5 +1,10 @@
 import { makeCompile } from './_utils'
 import { makeCompile } from './_utils'
-import { transformChildren, transformElement, transformVModel } from '../../src'
+import {
+  IRNodeTypes,
+  transformChildren,
+  transformElement,
+  transformVModel,
+} from '../../src'
 import { BindingTypes, DOMErrorCodes } from '@vue/compiler-dom'
 import { BindingTypes, DOMErrorCodes } from '@vue/compiler-dom'
 
 
 const compileWithVModel = makeCompile({
 const compileWithVModel = makeCompile({
@@ -198,4 +203,169 @@ describe('compiler: vModel transform', () => {
 
 
     expect(code).toMatchSnapshot()
     expect(code).toMatchSnapshot()
   })
   })
+
+  describe('component', () => {
+    test('v-model for component should work', () => {
+      const { code, ir } = compileWithVModel('<Comp v-model="foo" />')
+      expect(code).toMatchSnapshot()
+      expect(code).contains(
+        `modelValue: () => (_ctx.foo),
+    "onUpdate:modelValue": () => $event => (_ctx.foo = $event)`,
+      )
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'Comp',
+          props: [
+            [
+              {
+                key: { content: 'modelValue', isStatic: true },
+                model: true,
+                modelModifiers: [],
+                values: [{ content: 'foo', isStatic: false }],
+              },
+            ],
+          ],
+        },
+      ])
+    })
+
+    test('v-model with arguments for component should work', () => {
+      const { code, ir } = compileWithVModel('<Comp v-model:bar="foo" />')
+      expect(code).toMatchSnapshot()
+      expect(code).contains(
+        `bar: () => (_ctx.foo),
+    "onUpdate:bar": () => $event => (_ctx.foo = $event)`,
+      )
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'Comp',
+          props: [
+            [
+              {
+                key: { content: 'bar', isStatic: true },
+                model: true,
+                modelModifiers: [],
+                values: [{ content: 'foo', isStatic: false }],
+              },
+            ],
+          ],
+        },
+      ])
+    })
+
+    test('v-model with dynamic arguments for component should work', () => {
+      const { code, ir } = compileWithVModel('<Comp v-model:[arg]="foo" />')
+      expect(code).toMatchSnapshot()
+      expect(code).contains(
+        `[_ctx.arg]: () => (_ctx.foo),
+    ["onUpdate:" + _ctx.arg]: () => $event => (_ctx.foo = $event)`,
+      )
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'Comp',
+          props: [
+            [
+              {
+                key: { content: 'arg', isStatic: false },
+                values: [{ content: 'foo', isStatic: false }],
+                model: true,
+                modelModifiers: [],
+              },
+            ],
+          ],
+        },
+      ])
+    })
+
+    test('v-model for component should generate modelModifiers', () => {
+      const { code, ir } = compileWithVModel(
+        '<Comp v-model.trim.bar-baz="foo" />',
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contain(
+        `modelModifiers: () => ({ trim: true, "bar-baz": true })`,
+      )
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'Comp',
+          props: [
+            [
+              {
+                key: { content: 'modelValue', isStatic: true },
+                values: [{ content: 'foo', isStatic: false }],
+                model: true,
+                modelModifiers: ['trim', 'bar-baz'],
+              },
+            ],
+          ],
+        },
+      ])
+    })
+
+    test('v-model with arguments for component should generate modelModifiers', () => {
+      const { code, ir } = compileWithVModel(
+        '<Comp v-model:foo.trim="foo" v-model:bar.number="bar" />',
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contain(`fooModifiers: () => ({ trim: true })`)
+      expect(code).contain(`barModifiers: () => ({ number: true })`)
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'Comp',
+          props: [
+            [
+              {
+                key: { content: 'foo', isStatic: true },
+                values: [{ content: 'foo', isStatic: false }],
+                model: true,
+                modelModifiers: ['trim'],
+              },
+              {
+                key: { content: 'bar', isStatic: true },
+                values: [{ content: 'bar', isStatic: false }],
+                model: true,
+                modelModifiers: ['number'],
+              },
+            ],
+          ],
+        },
+      ])
+    })
+
+    test('v-model with dynamic arguments for component should generate modelModifiers ', () => {
+      const { code, ir } = compileWithVModel(
+        '<Comp v-model:[foo].trim="foo" v-model:[bar].number="bar" />',
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contain(`[_ctx.foo + "Modifiers"]: () => ({ trim: true })`)
+      expect(code).contain(`[_ctx.bar + "Modifiers"]: () => ({ number: true })`)
+      expect(ir.block.operation).toMatchObject([
+        {
+          type: IRNodeTypes.CREATE_COMPONENT_NODE,
+          tag: 'Comp',
+          props: [
+            [
+              {
+                key: { content: 'foo', isStatic: false },
+                values: [{ content: 'foo', isStatic: false }],
+                model: true,
+                modelModifiers: ['trim'],
+              },
+              {
+                key: { content: 'bar', isStatic: false },
+                values: [{ content: 'bar', isStatic: false }],
+                model: true,
+                modelModifiers: ['number'],
+              },
+            ],
+          ],
+        },
+      ])
+    })
+  })
 })
 })

+ 33 - 5
packages/compiler-vapor/src/generators/component.ts

@@ -1,4 +1,4 @@
-import { extend, isArray } from '@vue/shared'
+import { camelize, extend, isArray } from '@vue/shared'
 import type { CodegenContext } from '../generate'
 import type { CodegenContext } from '../generate'
 import type { CreateComponentIRNode, IRProp } from '../ir'
 import type { CreateComponentIRNode, IRProp } from '../ir'
 import {
 import {
@@ -13,6 +13,8 @@ import { genExpression } from './expression'
 import { genPropKey } from './prop'
 import { genPropKey } from './prop'
 import { createSimpleExpression } from '@vue/compiler-dom'
 import { createSimpleExpression } from '@vue/compiler-dom'
 import { genEventHandler } from './event'
 import { genEventHandler } from './event'
+import { genDirectiveModifiers } from './directive'
+import { genModelHandler } from './modelValue'
 
 
 // TODO: generate component slots
 // TODO: generate component slots
 export function genCreateComponent(
 export function genCreateComponent(
@@ -23,7 +25,7 @@ export function genCreateComponent(
 
 
   const tag = genTag()
   const tag = genTag()
   const isRoot = oper.root
   const isRoot = oper.root
-  const props = genProps()
+  const rawProps = genRawProps()
 
 
   return [
   return [
     NEWLINE,
     NEWLINE,
@@ -31,7 +33,7 @@ export function genCreateComponent(
     ...genCall(
     ...genCall(
       vaporHelper('createComponent'),
       vaporHelper('createComponent'),
       tag,
       tag,
-      props || (isRoot ? 'null' : false),
+      rawProps || (isRoot ? 'null' : false),
       isRoot && 'true',
       isRoot && 'true',
     ),
     ),
   ]
   ]
@@ -47,11 +49,11 @@ export function genCreateComponent(
     }
     }
   }
   }
 
 
-  function genProps() {
+  function genRawProps() {
     const props = oper.props
     const props = oper.props
       .map(props => {
       .map(props => {
         if (isArray(props)) {
         if (isArray(props)) {
-          if (!props.length) return undefined
+          if (!props.length) return
           return genStaticProps(props)
           return genStaticProps(props)
         } else {
         } else {
           let expr = genExpression(props.value, context)
           let expr = genExpression(props.value, context)
@@ -79,8 +81,34 @@ export function genCreateComponent(
           ...(prop.handler
           ...(prop.handler
             ? genEventHandler(context, prop.values[0])
             ? genEventHandler(context, prop.values[0])
             : ['() => (', ...genExpression(prop.values[0], context), ')']),
             : ['() => (', ...genExpression(prop.values[0], context), ')']),
+          ...(prop.model
+            ? [...genModelEvent(prop), ...genModelModifiers(prop)]
+            : []),
         ]
         ]
       }),
       }),
     )
     )
+
+    function genModelEvent(prop: IRProp): CodeFragment[] {
+      const name = prop.key.isStatic
+        ? [JSON.stringify(`onUpdate:${camelize(prop.key.content)}`)]
+        : ['["onUpdate:" + ', ...genExpression(prop.key, context), ']']
+      const handler = genModelHandler(prop.values[0], context)
+
+      return [',', NEWLINE, ...name, ': ', ...handler]
+    }
+
+    function genModelModifiers(prop: IRProp): CodeFragment[] {
+      const { key, modelModifiers } = prop
+      if (!modelModifiers || !modelModifiers.length) return []
+
+      const modifiersKey = key.isStatic
+        ? key.content === 'modelValue'
+          ? [`modelModifiers`]
+          : [`${key.content}Modifiers`]
+        : ['[', ...genExpression(key, context), ' + "Modifiers"]']
+
+      const modifiersVal = genDirectiveModifiers(modelModifiers)
+      return [',', NEWLINE, ...modifiersKey, `: () => ({ ${modifiersVal} })`]
+    }
   }
   }
 }
 }

+ 10 - 10
packages/compiler-vapor/src/generators/directive.ts

@@ -35,7 +35,7 @@ export function genWithDirective(
         ? NULL
         ? NULL
         : false
         : false
     const modifiers = dir.modifiers.length
     const modifiers = dir.modifiers.length
-      ? ['{ ', genDirectiveModifiers(), ' }']
+      ? ['{ ', genDirectiveModifiers(dir.modifiers), ' }']
       : false
       : false
 
 
     return genMulti(['[', ']', ', '], directive, value, argument, modifiers)
     return genMulti(['[', ']', ', '], directive, value, argument, modifiers)
@@ -61,14 +61,14 @@ export function genWithDirective(
         }
         }
       }
       }
     }
     }
-
-    function genDirectiveModifiers() {
-      return dir.modifiers
-        .map(
-          value =>
-            `${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`,
-        )
-        .join(', ')
-    }
   }
   }
 }
 }
+
+export function genDirectiveModifiers(modifiers: string[]) {
+  return modifiers
+    .map(
+      value =>
+        `${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`,
+    )
+    .join(', ')
+}

+ 19 - 11
packages/compiler-vapor/src/generators/modelValue.ts

@@ -3,28 +3,36 @@ import { genExpression } from './expression'
 import type { SetModelValueIRNode } from '../ir'
 import type { SetModelValueIRNode } from '../ir'
 import type { CodegenContext } from '../generate'
 import type { CodegenContext } from '../generate'
 import { type CodeFragment, NEWLINE, genCall } from './utils'
 import { type CodeFragment, NEWLINE, genCall } from './utils'
+import type { SimpleExpressionNode } from '@vue/compiler-dom'
 
 
 export function genSetModelValue(
 export function genSetModelValue(
   oper: SetModelValueIRNode,
   oper: SetModelValueIRNode,
   context: CodegenContext,
   context: CodegenContext,
 ): CodeFragment[] {
 ): CodeFragment[] {
-  const {
-    vaporHelper,
-
-    options: { isTS },
-  } = context
-
+  const { vaporHelper } = context
   const name = oper.key.isStatic
   const name = oper.key.isStatic
     ? [JSON.stringify(`update:${camelize(oper.key.content)}`)]
     ? [JSON.stringify(`update:${camelize(oper.key.content)}`)]
     : ['`update:${', ...genExpression(oper.key, context), '}`']
     : ['`update:${', ...genExpression(oper.key, context), '}`']
-  const handler = [
-    `() => ${isTS ? `($event: any)` : `$event`} => (`,
-    ...genExpression(oper.value, context, '$event'),
-    ')',
-  ]
+
+  const handler = genModelHandler(oper.value, context)
 
 
   return [
   return [
     NEWLINE,
     NEWLINE,
     ...genCall(vaporHelper('delegate'), `n${oper.element}`, name, handler),
     ...genCall(vaporHelper('delegate'), `n${oper.element}`, name, handler),
   ]
   ]
 }
 }
+
+export function genModelHandler(
+  value: SimpleExpressionNode,
+  context: CodegenContext,
+) {
+  const {
+    options: { isTS },
+  } = context
+
+  return [
+    `() => ${isTS ? `($event: any)` : `$event`} => (`,
+    ...genExpression(value, context, '$event'),
+    ')',
+  ]
+}

+ 2 - 0
packages/compiler-vapor/src/transform.ts

@@ -44,6 +44,8 @@ export interface DirectiveTransformResult {
   modifier?: '.' | '^'
   modifier?: '.' | '^'
   runtimeCamelize?: boolean
   runtimeCamelize?: boolean
   handler?: boolean
   handler?: boolean
+  model?: boolean
+  modelModifiers?: string[]
 }
 }
 
 
 // A structural directive transform is technically also a NodeTransform;
 // A structural directive transform is technically also a NodeTransform;

+ 6 - 0
packages/compiler-vapor/src/transforms/vModel.ts

@@ -65,6 +65,12 @@ export const transformVModel: DirectiveTransform = (dir, node, context) => {
   let runtimeDirective: VaporHelper | undefined
   let runtimeDirective: VaporHelper | undefined
 
 
   if (isComponent) {
   if (isComponent) {
+    return {
+      key: arg ? arg : createSimpleExpression('modelValue', true),
+      value: exp,
+      model: true,
+      modelModifiers: dir.modifiers,
+    }
   } else {
   } else {
     if (dir.arg)
     if (dir.arg)
       context.options.onError(
       context.options.onError(