diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap index 8f3203a9e..d75f1ea9b 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap @@ -1,5 +1,86 @@ // 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`] = ` "import { vModelText as _vModelText, withDirectives as _withDirectives, delegate as _delegate, template as _template } from 'vue/vapor'; const t0 = _template("") diff --git a/packages/compiler-vapor/__tests__/transforms/vModel.spec.ts b/packages/compiler-vapor/__tests__/transforms/vModel.spec.ts index 862f54480..80a22c880 100644 --- a/packages/compiler-vapor/__tests__/transforms/vModel.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vModel.spec.ts @@ -1,5 +1,10 @@ import { makeCompile } from './_utils' -import { transformChildren, transformElement, transformVModel } from '../../src' +import { + IRNodeTypes, + transformChildren, + transformElement, + transformVModel, +} from '../../src' import { BindingTypes, DOMErrorCodes } from '@vue/compiler-dom' const compileWithVModel = makeCompile({ @@ -198,4 +203,169 @@ describe('compiler: vModel transform', () => { expect(code).toMatchSnapshot() }) + + describe('component', () => { + test('v-model for component should work', () => { + const { code, ir } = compileWithVModel('') + 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('') + 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('') + 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( + '', + ) + 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( + '', + ) + 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( + '', + ) + 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'], + }, + ], + ], + }, + ]) + }) + }) }) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index ec9b9064e..8f1b01003 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/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 { CreateComponentIRNode, IRProp } from '../ir' import { @@ -13,6 +13,8 @@ import { genExpression } from './expression' import { genPropKey } from './prop' import { createSimpleExpression } from '@vue/compiler-dom' import { genEventHandler } from './event' +import { genDirectiveModifiers } from './directive' +import { genModelHandler } from './modelValue' // TODO: generate component slots export function genCreateComponent( @@ -23,7 +25,7 @@ export function genCreateComponent( const tag = genTag() const isRoot = oper.root - const props = genProps() + const rawProps = genRawProps() return [ NEWLINE, @@ -31,7 +33,7 @@ export function genCreateComponent( ...genCall( vaporHelper('createComponent'), tag, - props || (isRoot ? 'null' : false), + rawProps || (isRoot ? 'null' : false), isRoot && 'true', ), ] @@ -47,11 +49,11 @@ export function genCreateComponent( } } - function genProps() { + function genRawProps() { const props = oper.props .map(props => { if (isArray(props)) { - if (!props.length) return undefined + if (!props.length) return return genStaticProps(props) } else { let expr = genExpression(props.value, context) @@ -79,8 +81,34 @@ export function genCreateComponent( ...(prop.handler ? genEventHandler(context, prop.values[0]) : ['() => (', ...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} })`] + } } } diff --git a/packages/compiler-vapor/src/generators/directive.ts b/packages/compiler-vapor/src/generators/directive.ts index 6dcbef50c..452b79f9d 100644 --- a/packages/compiler-vapor/src/generators/directive.ts +++ b/packages/compiler-vapor/src/generators/directive.ts @@ -35,7 +35,7 @@ export function genWithDirective( ? NULL : false const modifiers = dir.modifiers.length - ? ['{ ', genDirectiveModifiers(), ' }'] + ? ['{ ', genDirectiveModifiers(dir.modifiers), ' }'] : false 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(', ') +} diff --git a/packages/compiler-vapor/src/generators/modelValue.ts b/packages/compiler-vapor/src/generators/modelValue.ts index fec871391..2a3ab4cda 100644 --- a/packages/compiler-vapor/src/generators/modelValue.ts +++ b/packages/compiler-vapor/src/generators/modelValue.ts @@ -3,28 +3,36 @@ import { genExpression } from './expression' import type { SetModelValueIRNode } from '../ir' import type { CodegenContext } from '../generate' import { type CodeFragment, NEWLINE, genCall } from './utils' +import type { SimpleExpressionNode } from '@vue/compiler-dom' export function genSetModelValue( oper: SetModelValueIRNode, context: CodegenContext, ): CodeFragment[] { - const { - vaporHelper, - - options: { isTS }, - } = context - + const { vaporHelper } = context const name = oper.key.isStatic ? [JSON.stringify(`update:${camelize(oper.key.content)}`)] : ['`update:${', ...genExpression(oper.key, context), '}`'] - const handler = [ - `() => ${isTS ? `($event: any)` : `$event`} => (`, - ...genExpression(oper.value, context, '$event'), - ')', - ] + + const handler = genModelHandler(oper.value, context) return [ NEWLINE, ...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'), + ')', + ] +} diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index b00069fbd..14ed88944 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -44,6 +44,8 @@ export interface DirectiveTransformResult { modifier?: '.' | '^' runtimeCamelize?: boolean handler?: boolean + model?: boolean + modelModifiers?: string[] } // A structural directive transform is technically also a NodeTransform; diff --git a/packages/compiler-vapor/src/transforms/vModel.ts b/packages/compiler-vapor/src/transforms/vModel.ts index 8831db92d..72382ffb4 100644 --- a/packages/compiler-vapor/src/transforms/vModel.ts +++ b/packages/compiler-vapor/src/transforms/vModel.ts @@ -65,6 +65,12 @@ export const transformVModel: DirectiveTransform = (dir, node, context) => { let runtimeDirective: VaporHelper | undefined if (isComponent) { + return { + key: arg ? arg : createSimpleExpression('modelValue', true), + value: exp, + model: true, + modelModifiers: dir.modifiers, + } } else { if (dir.arg) context.options.onError(