}"
`;
+exports[`v-on > complex member expression w/ prefixIdentifiers: true 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click", (...args) => (_ctx.a['b' + _ctx.c] && _ctx.a['b' + _ctx.c](...args)))
+ return n0
+}"
+`;
+
exports[`v-on > dynamic arg 1`] = `
"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on } from 'vue/vapor';
}"
`;
+exports[`v-on > dynamic arg with complex exp prefixing 1`] = `
+"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _renderEffect(() => {
+ _on(n1, _ctx.event(_ctx.foo), (...args) => (_ctx.handler && _ctx.handler(...args)))
+ })
+ return n0
+}"
+`;
+
+exports[`v-on > dynamic arg with prefixing 1`] = `
+"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _renderEffect(() => {
+ _on(n1, _ctx.event, (...args) => (_ctx.handler && _ctx.handler(...args)))
+ })
+ return n0
+}"
+`;
+
exports[`v-on > event modifier 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers, withKeys as _withKeys } from 'vue/vapor';
}"
`;
+exports[`v-on > function expression w/ prefixIdentifiers: true 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click", e => _ctx.foo(e))
+ return n0
+}"
+`;
+
+exports[`v-on > inline statement w/ prefixIdentifiers: true 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click", $event => (_ctx.foo($event)))
+ return n0
+}"
+`;
+
+exports[`v-on > multiple inline statements w/ prefixIdentifiers: true 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click", $event => {_ctx.foo($event);_ctx.bar()})
+ return n0
+}"
+`;
+
+exports[`v-on > should NOT add a prefix to $event if the expression is a function expression 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click", $event => {_ctx.i++;_ctx.foo($event)})
+ return n0
+}"
+`;
+
+exports[`v-on > should NOT wrap as function if expression is already function expression (with Typescript) 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click", (e: any): any => _ctx.foo(e))
+ return n0
+}"
+`;
+
+exports[`v-on > should NOT wrap as function if expression is already function expression (with newlines) 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click",
+ $event => {
+ _ctx.foo($event)
+ }
+ )
+ return n0
+}"
+`;
+
+exports[`v-on > should NOT wrap as function if expression is already function expression 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click", $event => _ctx.foo($event))
+ return n0
+}"
+`;
+
+exports[`v-on > should NOT wrap as function if expression is complex member expression 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click", (...args) => (_ctx.a['b' + _ctx.c] && _ctx.a['b' + _ctx.c](...args)))
+ return n0
+}"
+`;
+
+exports[`v-on > should handle multi-line statement 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click", $event => {
+_ctx.foo();
+_ctx.bar()
+})
+ return n0
+}"
+`;
+
+exports[`v-on > should handle multiple inline statement 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click", $event => {_ctx.foo();_ctx.bar()})
+ return n0
+}"
+`;
+
exports[`v-on > should not wrap keys guard if no key modifier is present 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
}"
`;
+exports[`v-on > should wrap as function if expression is inline statement 1`] = `
+"import { template as _template, children as _children, on as _on } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _on(n1, "click", $event => (_ctx.i++))
+ return n0
+}"
+`;
+
+exports[`v-on > should wrap both for dynamic key event w/ left/right modifiers 1`] = `
+"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const n0 = t0()
+ const { 0: [n1],} = _children(n0)
+ _renderEffect(() => {
+ _on(n1, _ctx.e, _withKeys(_withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["left"]), ["left"]))
+ })
+ return n0
+}"
+`;
+
exports[`v-on > should wrap keys guard for keyboard events or dynamic events 1`] = `
"import { template as _template, children as _children, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor';
expect(code).matchSnapshot()
})
- test.todo('dynamic arg with prefixing')
- test.todo('dynamic arg with complex exp prefixing')
- test.todo('should wrap as function if expression is inline statement')
- test.todo('should handle multiple inline statement')
- test.todo('should handle multi-line statement')
- test.todo('inline statement w/ prefixIdentifiers: true')
- test.todo('multiple inline statements w/ prefixIdentifiers: true')
- test.todo(
- 'should NOT wrap as function if expression is already function expression',
- )
- test.todo(
+ test('dynamic arg with prefixing', () => {
+ const { code } = compileWithVOn(`<div v-on:[event]="handler"/>`, {
+ prefixIdentifiers: true,
+ })
+
+ expect(code).matchSnapshot()
+ })
+
+ test('dynamic arg with complex exp prefixing', () => {
+ const { ir, code } = compileWithVOn(`<div v-on:[event(foo)]="handler"/>`, {
+ prefixIdentifiers: true,
+ })
+
+ expect(ir.vaporHelpers).contains('on')
+ expect(ir.vaporHelpers).contains('renderEffect')
+ expect(ir.helpers.size).toBe(0)
+ expect(ir.operation).toEqual([])
+
+ expect(ir.effect[0].operations[0]).toMatchObject({
+ type: IRNodeTypes.SET_EVENT,
+ element: 1,
+ key: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: 'event(foo)',
+ isStatic: false,
+ },
+ value: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: 'handler',
+ isStatic: false,
+ },
+ })
+
+ expect(code).matchSnapshot()
+ })
+
+ test('should wrap as function if expression is inline statement', () => {
+ const { code, ir } = compileWithVOn(`<div @click="i++"/>`)
+
+ expect(ir.vaporHelpers).contains('on')
+ expect(ir.helpers.size).toBe(0)
+ expect(ir.effect).toEqual([])
+
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ element: 1,
+ value: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: 'i++',
+ isStatic: false,
+ },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ expect(code).contains('_on(n1, "click", $event => (_ctx.i++))')
+ })
+
+ test('should handle multiple inline statement', () => {
+ const { ir, code } = compileWithVOn(`<div @click="foo();bar()"/>`)
+
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ value: { content: 'foo();bar()' },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ // should wrap with `{` for multiple statements
+ // in this case the return value is discarded and the behavior is
+ // consistent with 2.x
+ expect(code).contains('_on(n1, "click", $event => {_ctx.foo();_ctx.bar()})')
+ })
+
+ test('should handle multi-line statement', () => {
+ const { code, ir } = compileWithVOn(`<div @click="\nfoo();\nbar()\n"/>`)
+
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ value: { content: '\nfoo();\nbar()\n' },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ // should wrap with `{` for multiple statements
+ // in this case the return value is discarded and the behavior is
+ // consistent with 2.x
+ expect(code).contains(
+ '_on(n1, "click", $event => {\n_ctx.foo();\n_ctx.bar()\n})',
+ )
+ })
+
+ test('inline statement w/ prefixIdentifiers: true', () => {
+ const { code, ir } = compileWithVOn(`<div @click="foo($event)"/>`, {
+ prefixIdentifiers: true,
+ })
+
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ value: { content: 'foo($event)' },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ // should NOT prefix $event
+ expect(code).contains('_on(n1, "click", $event => (_ctx.foo($event)))')
+ })
+
+ test('multiple inline statements w/ prefixIdentifiers: true', () => {
+ const { ir, code } = compileWithVOn(`<div @click="foo($event);bar()"/>`, {
+ prefixIdentifiers: true,
+ })
+
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ value: { content: 'foo($event);bar()' },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ // should NOT prefix $event
+ expect(code).contains(
+ '_on(n1, "click", $event => {_ctx.foo($event);_ctx.bar()})',
+ )
+ })
+
+ test('should NOT wrap as function if expression is already function expression', () => {
+ const { code, ir } = compileWithVOn(`<div @click="$event => foo($event)"/>`)
+
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ value: { content: '$event => foo($event)' },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ expect(code).contains('_on(n1, "click", $event => _ctx.foo($event))')
+ })
+
+ test.fails(
'should NOT wrap as function if expression is already function expression (with Typescript)',
+ () => {
+ const { ir, code } = compileWithVOn(
+ `<div @click="(e: any): any => foo(e)"/>`,
+ { expressionPlugins: ['typescript'] },
+ )
+
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ value: { content: '(e: any): any => foo(e)' },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ expect(code).contains('_on(n1, "click", e => _ctx.foo(e))')
+ },
)
- test.todo(
- 'should NOT wrap as function if expression is already function expression (with newlines)',
- )
- test.todo(
- 'should NOT wrap as function if expression is already function expression (with newlines + function keyword)',
- )
- test.todo(
- 'should NOT wrap as function if expression is complex member expression',
- )
- test.todo('complex member expression w/ prefixIdentifiers: true')
- test.todo('function expression w/ prefixIdentifiers: true')
+
+ test('should NOT wrap as function if expression is already function expression (with newlines)', () => {
+ const { ir, code } = compileWithVOn(
+ `<div @click="
+ $event => {
+ foo($event)
+ }
+ "/>`,
+ )
+
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ value: {
+ content: `
+ $event => {
+ foo($event)
+ }
+ `,
+ },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ })
+
+ test('should NOT add a prefix to $event if the expression is a function expression', () => {
+ const { ir, code } = compileWithVOn(
+ `<div @click="$event => {i++;foo($event)}"></div>`,
+ {
+ prefixIdentifiers: true,
+ },
+ )
+
+ expect(ir.operation[0]).toMatchObject({
+ type: IRNodeTypes.SET_EVENT,
+ value: { content: '$event => {i++;foo($event)}' },
+ })
+
+ expect(code).matchSnapshot()
+ })
+
+ test('should NOT wrap as function if expression is complex member expression', () => {
+ const { ir, code } = compileWithVOn(`<div @click="a['b' + c]"/>`)
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ value: { content: `a['b' + c]` },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ })
+
+ test('complex member expression w/ prefixIdentifiers: true', () => {
+ const { ir, code } = compileWithVOn(`<div @click="a['b' + c]"/>`)
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ value: { content: `a['b' + c]` },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ expect(code).contains(
+ `_on(n1, "click", (...args) => (_ctx.a['b' + _ctx.c] && _ctx.a['b' + _ctx.c](...args)))`,
+ )
+ })
+
+ test('function expression w/ prefixIdentifiers: true', () => {
+ const { code, ir } = compileWithVOn(`<div @click="e => foo(e)"/>`, {
+ prefixIdentifiers: true,
+ })
+
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ value: { content: `e => foo(e)` },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ expect(code).contains('_on(n1, "click", e => _ctx.foo(e))')
+ })
test('should error if no expression AND no modifier', () => {
const onError = vi.fn()
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
- modifiers: { keys: ['left'] },
+ modifiers: {
+ keys: ['left'],
+ nonKeys: [],
+ options: [],
+ },
},
])
expect(code).matchSnapshot()
})
- test.todo('should wrap both for dynamic key event w/ left/right modifiers')
+ test('should wrap both for dynamic key event w/ left/right modifiers', () => {
+ const { code, ir } = compileWithVOn(`<div @[e].left="test"/>`, {
+ prefixIdentifiers: true,
+ })
+
+ expect(ir.effect[0].operations).toMatchObject([
+ {
+ type: IRNodeTypes.SET_EVENT,
+ key: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: 'e',
+ isStatic: false,
+ },
+ modifiers: {
+ keys: ['left'],
+ nonKeys: ['left'],
+ options: [],
+ },
+ },
+ ])
+
+ expect(code).matchSnapshot()
+ })
test('should transform click.right', () => {
const { code, ir } = compileWithVOn(`<div @click.right="test"/>`)
advancePositionWithClone,
advancePositionWithMutation,
createSimpleExpression,
+ isMemberExpression,
isSimpleIdentifier,
locStub,
walkIdentifiers,
import { camelize, isGloballyAllowed, isString, makeMap } from '@vue/shared'
import type { Identifier } from '@babel/types'
+// TODO: share this with compiler-core
+const fnExpRE =
+ /^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
+
// remove when stable
// @ts-expect-error
function checkNever(x: never): never {}
;(keys.length ? pushWithKeys : pushNoop)(() =>
(nonKeys.length ? pushWithModifiers : pushNoop)(() => {
- if (oper.value && oper.value.content.trim()) {
- push('(...args) => (')
- genExpression(oper.value, context)
- push(' && ')
- genExpression(oper.value, context)
- push('(...args))')
- } else {
- push('() => {}')
- }
+ genEventHandler()
}),
)
},
!!options.length &&
(() => push(`{ ${options.map((v) => `${v}: true`).join(', ')} }`)),
)
+
+ function genEventHandler() {
+ const exp = oper.value
+ if (exp && exp.content.trim()) {
+ const isMemberExp = isMemberExpression(exp.content, {
+ // TODO: expression plugins
+ expressionPlugins: [],
+ })
+ const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
+ const hasMultipleStatements = exp.content.includes(`;`)
+
+ if (isInlineStatement) {
+ push('$event => ')
+ push(hasMultipleStatements ? '{' : '(')
+ const knownIds = Object.create(null)
+ knownIds['$event'] = 1
+ genExpression(exp, context, knownIds)
+ push(hasMultipleStatements ? '}' : ')')
+ } else if (isMemberExp) {
+ push('(...args) => (')
+ genExpression(exp, context)
+ push(' && ')
+ genExpression(exp, context)
+ push('(...args))')
+ } else {
+ genExpression(exp, context)
+ }
+ } else {
+ push('() => {}')
+ }
+ }
}
function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) {
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
-function genExpression(node: IRExpression, context: CodegenContext): void {
+function genExpression(
+ node: IRExpression,
+ context: CodegenContext,
+ knownIds: Record<string, number> = Object.create(null),
+): void {
const { push } = context
if (isString(node)) return push(node)
const ids: Identifier[] = []
walkIdentifiers(
ast!,
- (id) => {
+ (id, parent, parentStack, isReference, isLocal) => {
+ if (isLocal) return
ids.push(id)
},
true,
+ [],
+ knownIds,
)
if (ids.length) {
ids.sort((a, b) => a.start! - b.start!)
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
export {
baseWatch,
+ getCurrentEffect,
onEffectCleanup,
traverse,
BaseWatchErrorCodes,
// effect
stop,
ReactiveEffect,
+ getCurrentEffect,
onEffectCleanup,
// effect scope
effectScope,
+import { getCurrentEffect, onEffectCleanup } from '@vue/reactivity'
+
export function on(
el: HTMLElement,
event: string,
options?: AddEventListenerOptions,
) {
el.addEventListener(event, handler, options)
+ if (getCurrentEffect()) {
+ onEffectCleanup(() => el.removeEventListener(event, handler, options))
+ }
}
<script setup lang="ts">
+import { ref } from 'vue/vapor'
const handleClick = () => {
console.log('Hello, Vapor!')
}
+
+const i = ref(0)
+const event = ref('click')
</script>
<template>
<form>
<button @click.prevent="handleClick">no submit</button>
</form>
+
+ <div>
+ {{ i }}
+ <button @[event].prevent="i++">Add by {{ event }}</button>
+ <button @click="event = event === 'click' ? 'contextmenu' : 'click'">
+ Change Event
+ </button>
+ </div>
</template>