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

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
Jevon 2024-04-19 19:43:30 +08:00 committed by GitHub
parent 37df043adc
commit 1f28ae15cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 322 additions and 27 deletions

View File

@ -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("<input>")

View File

@ -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('<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'],
},
],
],
},
])
})
})
})

View File

@ -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} })`]
}
}
}

View File

@ -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
export function genDirectiveModifiers(modifiers: string[]) {
return modifiers
.map(
value =>
`${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`,
)
.join(', ')
}
}
}

View File

@ -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'),
')',
]
}

View File

@ -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;

View File

@ -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(