feat: delegate event for vapor

closes #136
This commit is contained in:
三咲智子 Kevin Deng 2024-02-25 00:40:00 +08:00
parent e91dde5d22
commit 669fec8dad
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
12 changed files with 305 additions and 157 deletions

View File

@ -45,7 +45,7 @@ export * from './runtimeHelpers'
export { getBaseTransformPreset, type TransformPreset } from './compile'
export { transformModel } from './transforms/vModel'
export { transformOn } from './transforms/vOn'
export { transformOn, fnExpRE } from './transforms/vOn'
export { transformBind } from './transforms/vBind'
export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
export { processIf } from './transforms/vIf'

View File

@ -16,7 +16,7 @@ import { validateBrowserExpression } from '../validateExpression'
import { hasScopeRef, isMemberExpression } from '../utils'
import { TO_HANDLER_KEY } from '../runtimeHelpers'
const fnExpRE =
export const fnExpRE =
/^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
export interface VOnDirectiveNode extends DirectiveNode {

View File

@ -146,12 +146,13 @@ export function render(_ctx) {
`;
exports[`compile > dynamic root nodes and interpolation 1`] = `
"import { on as _on, renderEffect as _renderEffect, setText as _setText, setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, renderEffect as _renderEffect, setText as _setText, setDynamicProp as _setDynamicProp, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<button></button>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => _ctx.handleClick)
_recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.handleClick))
_renderEffect(() => _setText(n0, _ctx.count, "foo", _ctx.count, "foo", _ctx.count))
_renderEffect(() => _setDynamicProp(n0, "id", _ctx.count))
return n0

View File

@ -1,13 +1,14 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: v-for > basic v-for 1`] = `
"import { on as _on, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.items), (_block) => {
const n2 = t0()
_on(n2, "click", () => $event => (_ctx.remove(_block.s[0])))
_recordMetadata(n2, "events", "click", _eventHandler(() => $event => (_ctx.remove(_block.s[0]))))
const _updateEffect = () => {
const [item] = _block.s
_setText(n2, item)

View File

@ -1,12 +1,13 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`v-on > complex member expression w/ prefixIdentifiers: true 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => _ctx.a['b' + _ctx.c])
_recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.a['b' + _ctx.c]))
return n0
}"
`;
@ -45,11 +46,12 @@ export function render(_ctx) {
`;
exports[`v-on > event modifier 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, on as _on, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<a></a>")
const t1 = _template("<form></form>")
const t2 = _template("<div></div>")
const t3 = _template("<input>")
_delegateEvents("click", "contextmenu", "mouseup", "keyup")
export function render(_ctx) {
const n0 = t0()
@ -74,224 +76,249 @@ export function render(_ctx) {
const n19 = t3()
const n20 = t3()
const n21 = t3()
_on(n0, "click", () => _ctx.handleEvent, undefined, {
_recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.handleEvent, {
modifiers: ["stop"]
})
}))
_on(n1, "submit", () => _ctx.handleEvent, undefined, {
modifiers: ["prevent"]
})
_on(n2, "click", () => _ctx.handleEvent, undefined, {
_recordMetadata(n2, "events", "click", _eventHandler(() => _ctx.handleEvent, {
modifiers: ["stop", "prevent"]
})
_on(n3, "click", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n3, "events", "click", _eventHandler(() => _ctx.handleEvent, {
modifiers: ["self"]
})
}))
_on(n4, "click", () => _ctx.handleEvent, { capture: true })
_on(n5, "click", () => _ctx.handleEvent, { once: true })
_on(n6, "scroll", () => _ctx.handleEvent, { passive: true })
_on(n7, "contextmenu", () => _ctx.handleEvent, undefined, {
_recordMetadata(n7, "events", "contextmenu", _eventHandler(() => _ctx.handleEvent, {
modifiers: ["right"]
})
_on(n8, "click", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n8, "events", "click", _eventHandler(() => _ctx.handleEvent, {
modifiers: ["left"]
})
_on(n9, "mouseup", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n9, "events", "mouseup", _eventHandler(() => _ctx.handleEvent, {
modifiers: ["middle"]
})
_on(n10, "contextmenu", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n10, "events", "contextmenu", _eventHandler(() => _ctx.handleEvent, {
modifiers: ["right"]
})
_on(n11, "keyup", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n11, "events", "keyup", _eventHandler(() => _ctx.handleEvent, {
keys: ["enter"]
})
_on(n12, "keyup", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n12, "events", "keyup", _eventHandler(() => _ctx.handleEvent, {
keys: ["tab"]
})
_on(n13, "keyup", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n13, "events", "keyup", _eventHandler(() => _ctx.handleEvent, {
keys: ["delete"]
})
_on(n14, "keyup", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n14, "events", "keyup", _eventHandler(() => _ctx.handleEvent, {
keys: ["esc"]
})
_on(n15, "keyup", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n15, "events", "keyup", _eventHandler(() => _ctx.handleEvent, {
keys: ["space"]
})
_on(n16, "keyup", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n16, "events", "keyup", _eventHandler(() => _ctx.handleEvent, {
keys: ["up"]
})
_on(n17, "keyup", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n17, "events", "keyup", _eventHandler(() => _ctx.handleEvent, {
keys: ["down"]
})
_on(n18, "keyup", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n18, "events", "keyup", _eventHandler(() => _ctx.handleEvent, {
keys: ["left"]
})
_on(n19, "keyup", () => _ctx.submit, undefined, {
}))
_recordMetadata(n19, "events", "keyup", _eventHandler(() => _ctx.submit, {
modifiers: ["middle"]
})
_on(n20, "keyup", () => _ctx.submit, undefined, {
}))
_recordMetadata(n20, "events", "keyup", _eventHandler(() => _ctx.submit, {
modifiers: ["middle", "self"]
})
_on(n21, "keyup", () => _ctx.handleEvent, undefined, {
}))
_recordMetadata(n21, "events", "keyup", _eventHandler(() => _ctx.handleEvent, {
modifiers: ["self"],
keys: ["enter"]
})
}))
return [n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16, n17, n18, n19, n20, n21]
}"
`;
exports[`v-on > function expression w/ prefixIdentifiers: true 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => e => _ctx.foo(e))
_recordMetadata(n0, "events", "click", _eventHandler(() => e => _ctx.foo(e)))
return n0
}"
`;
exports[`v-on > inline statement w/ prefixIdentifiers: true 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => $event => (_ctx.foo($event)))
_recordMetadata(n0, "events", "click", _eventHandler(() => $event => (_ctx.foo($event))))
return n0
}"
`;
exports[`v-on > multiple inline statements w/ prefixIdentifiers: true 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => $event => {_ctx.foo($event);_ctx.bar()})
_recordMetadata(n0, "events", "click", _eventHandler(() => $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 { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => $event => {_ctx.i++;_ctx.foo($event)})
_recordMetadata(n0, "events", "click", _eventHandler(() => $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 { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => (e: any): any => _ctx.foo(e))
_recordMetadata(n0, "events", "click", _eventHandler(() => (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 { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () =>
_recordMetadata(n0, "events", "click", _eventHandler(() =>
$event => {
_ctx.foo($event)
}
)
))
return n0
}"
`;
exports[`v-on > should NOT wrap as function if expression is already function expression 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => $event => _ctx.foo($event))
_recordMetadata(n0, "events", "click", _eventHandler(() => $event => _ctx.foo($event)))
return n0
}"
`;
exports[`v-on > should NOT wrap as function if expression is complex member expression 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => _ctx.a['b' + _ctx.c])
_recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.a['b' + _ctx.c]))
return n0
}"
`;
exports[`v-on > should delegate event 1`] = `
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.test))
return n0
}"
`;
exports[`v-on > should handle multi-line statement 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => $event => {
_recordMetadata(n0, "events", "click", _eventHandler(() => $event => {
_ctx.foo();
_ctx.bar()
})
}))
return n0
}"
`;
exports[`v-on > should handle multiple inline statement 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => $event => {_ctx.foo();_ctx.bar()})
_recordMetadata(n0, "events", "click", _eventHandler(() => $event => {_ctx.foo();_ctx.bar()}))
return n0
}"
`;
exports[`v-on > should not prefix member expression 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => _ctx.foo.bar)
_recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.foo.bar))
return n0
}"
`;
exports[`v-on > should not wrap keys guard if no key modifier is present 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("keyup")
export function render(_ctx) {
const n0 = t0()
_on(n0, "keyup", () => _ctx.test, undefined, {
_recordMetadata(n0, "events", "keyup", _eventHandler(() => _ctx.test, {
modifiers: ["exact"]
})
}))
return n0
}"
`;
exports[`v-on > should support multiple events and modifiers options w/ prefixIdentifiers: true 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click", "keyup")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => _ctx.test, undefined, {
_recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.test, {
modifiers: ["stop"]
})
_on(n0, "keyup", () => _ctx.test, undefined, {
}))
_recordMetadata(n0, "events", "keyup", _eventHandler(() => _ctx.test, {
keys: ["enter"]
})
}))
return n0
}"
`;
@ -310,14 +337,15 @@ export function render(_ctx) {
`;
exports[`v-on > should transform click.middle 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("mouseup")
export function render(_ctx) {
const n0 = t0()
_on(n0, "mouseup", () => _ctx.test, undefined, {
_recordMetadata(n0, "events", "mouseup", _eventHandler(() => _ctx.test, {
modifiers: ["middle"]
})
}))
return n0
}"
`;
@ -338,14 +366,15 @@ export function render(_ctx) {
`;
exports[`v-on > should transform click.right 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("contextmenu")
export function render(_ctx) {
const n0 = t0()
_on(n0, "contextmenu", () => _ctx.test, undefined, {
_recordMetadata(n0, "events", "contextmenu", _eventHandler(() => _ctx.test, {
modifiers: ["right"]
})
}))
return n0
}"
`;
@ -367,12 +396,13 @@ export function render(_ctx) {
`;
exports[`v-on > should wrap as function if expression is inline statement 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => $event => (_ctx.i++))
_recordMetadata(n0, "events", "click", _eventHandler(() => $event => (_ctx.i++)))
return n0
}"
`;
@ -398,9 +428,9 @@ exports[`v-on > should wrap in unref if identifier is setup-maybe-ref w/ inline:
const n0 = t0()
const n1 = t0()
const n2 = t0()
_on(n0, "click", () => $event => (x.value=_unref(y)))
_on(n1, "click", () => $event => (x.value++))
_on(n2, "click", () => $event => ({ x: x.value } = _unref(y)))
_recordMetadata(n0, "events", "click", _eventHandler(() => $event => (x.value=_unref(y))))
_recordMetadata(n1, "events", "click", _eventHandler(() => $event => (x.value++)))
_recordMetadata(n2, "events", "click", _eventHandler(() => $event => ({ x: x.value } = _unref(y))))
return [n0, n1, n2]
})()"
`;
@ -420,25 +450,27 @@ export function render(_ctx) {
`;
exports[`v-on > should wrap keys guard for static key event w/ left/right modifiers 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("keyup")
export function render(_ctx) {
const n0 = t0()
_on(n0, "keyup", () => _ctx.test, undefined, {
_recordMetadata(n0, "events", "keyup", _eventHandler(() => _ctx.test, {
keys: ["left"]
})
}))
return n0
}"
`;
exports[`v-on > simple expression 1`] = `
"import { on as _on, template as _template } from 'vue/vapor';
"import { recordMetadata as _recordMetadata, eventHandler as _eventHandler, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
_delegateEvents("click")
export function render(_ctx) {
const n0 = t0()
_on(n0, "click", () => _ctx.handleClick)
_recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.handleClick))
return n0
}"
`;

View File

@ -25,10 +25,10 @@ describe('v-on', () => {
},
)
expect(vaporHelpers).contains('on')
expect(code).matchSnapshot()
expect(vaporHelpers).not.contains('on')
expect(helpers.size).toBe(0)
expect(ir.block.effect).toEqual([])
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
@ -45,10 +45,9 @@ describe('v-on', () => {
},
modifiers: { keys: [], nonKeys: [], options: [] },
keyOverride: undefined,
delegate: true,
},
])
expect(code).matchSnapshot()
})
test('event modifier', () => {
@ -155,10 +154,10 @@ describe('v-on', () => {
const { code, ir, helpers, vaporHelpers } =
compileWithVOn(`<div @click="i++"/>`)
expect(vaporHelpers).contains('on')
expect(code).matchSnapshot()
expect(vaporHelpers).not.contains('on')
expect(helpers.size).toBe(0)
expect(ir.block.effect).toEqual([])
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
@ -168,11 +167,12 @@ describe('v-on', () => {
content: 'i++',
isStatic: false,
},
delegate: true,
},
])
expect(code).matchSnapshot()
expect(code).contains('_on(n0, "click", () => $event => (_ctx.i++))')
expect(code).contains(
'_recordMetadata(n0, "events", "click", _eventHandler(() => $event => (_ctx.i++)))',
)
})
test('should wrap in unref if identifier is setup-maybe-ref w/ inline: true', () => {
@ -192,49 +192,49 @@ describe('v-on', () => {
expect(vaporHelpers).contains('unref')
expect(helpers.size).toBe(0)
expect(code).contains(
'_on(n0, "click", () => $event => (x.value=_unref(y)))',
'_recordMetadata(n0, "events", "click", _eventHandler(() => $event => (x.value=_unref(y))))',
)
expect(code).contains('_on(n1, "click", () => $event => (x.value++))')
expect(code).contains(
'_on(n2, "click", () => $event => ({ x: x.value } = _unref(y)))',
'_recordMetadata(n1, "events", "click", _eventHandler(() => $event => (x.value++)))',
)
expect(code).contains(
'_recordMetadata(n2, "events", "click", _eventHandler(() => $event => ({ x: x.value } = _unref(y))))',
)
})
test('should handle multiple inline statement', () => {
const { ir, code } = compileWithVOn(`<div @click="foo();bar()"/>`)
expect(code).matchSnapshot()
expect(ir.block.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(n0, "click", () => $event => {_ctx.foo();_ctx.bar()})',
`_recordMetadata(n0, "events", "click", _eventHandler(() => $event => {_ctx.foo();_ctx.bar()}))`,
)
})
test('should handle multi-line statement', () => {
const { code, ir } = compileWithVOn(`<div @click="\nfoo();\nbar()\n"/>`)
expect(code).matchSnapshot()
expect(ir.block.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(n0, "click", () => $event => {\n_ctx.foo();\n_ctx.bar()\n})',
'_recordMetadata(n0, "events", "click", _eventHandler(() => $event => {\n_ctx.foo();\n_ctx.bar()\n})',
)
})
@ -243,17 +243,16 @@ describe('v-on', () => {
prefixIdentifiers: true,
})
expect(code).matchSnapshot()
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
value: { content: 'foo($event)' },
},
])
expect(code).matchSnapshot()
// should NOT prefix $event
expect(code).contains(
'_on(n0, "click", () => $event => (_ctx.foo($event)))',
'_recordMetadata(n0, "events", "click", _eventHandler(() => $event => (_ctx.foo($event))))',
)
})
@ -262,32 +261,32 @@ describe('v-on', () => {
prefixIdentifiers: true,
})
expect(code).matchSnapshot()
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
value: { content: 'foo($event);bar()' },
},
])
expect(code).matchSnapshot()
// should NOT prefix $event
expect(code).contains(
'_on(n0, "click", () => $event => {_ctx.foo($event);_ctx.bar()})',
'_recordMetadata(n0, "events", "click", _eventHandler(() => $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(code).matchSnapshot()
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
value: { content: '$event => foo($event)' },
},
])
expect(code).matchSnapshot()
expect(code).contains('_on(n0, "click", () => $event => _ctx.foo($event))')
expect(code).contains(
'_recordMetadata(n0, "events", "click", _eventHandler(() => $event => _ctx.foo($event)))',
)
})
test('should NOT wrap as function if expression is already function expression (with Typescript)', () => {
@ -296,16 +295,15 @@ describe('v-on', () => {
{ expressionPlugins: ['typescript'] },
)
expect(code).matchSnapshot()
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
value: { content: '(e: any): any => foo(e)' },
},
])
expect(code).matchSnapshot()
expect(code).contains(
'_on(n0, "click", () => (e: any): any => _ctx.foo(e))',
'_recordMetadata(n0, "events", "click", _eventHandler(() => (e: any): any => _ctx.foo(e)))',
)
})
@ -318,6 +316,7 @@ describe('v-on', () => {
"/>`,
)
expect(code).matchSnapshot()
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
@ -330,8 +329,6 @@ describe('v-on', () => {
},
},
])
expect(code).matchSnapshot()
})
test('should NOT add a prefix to $event if the expression is a function expression', () => {
@ -372,7 +369,9 @@ describe('v-on', () => {
])
expect(code).matchSnapshot()
expect(code).contains(`_on(n0, "click", () => _ctx.a['b' + _ctx.c])`)
expect(code).contains(
`_recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.a['b' + _ctx.c]))`,
)
})
test('function expression w/ prefixIdentifiers: true', () => {
@ -380,15 +379,16 @@ describe('v-on', () => {
prefixIdentifiers: true,
})
expect(code).matchSnapshot()
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
value: { content: `e => foo(e)` },
},
])
expect(code).matchSnapshot()
expect(code).contains('_on(n0, "click", () => e => _ctx.foo(e))')
expect(code).contains(
'_recordMetadata(n0, "events", "click", _eventHandler(() => e => _ctx.foo(e)))',
)
})
test('should error if no expression AND no modifier', () => {
@ -423,6 +423,7 @@ describe('v-on', () => {
},
)
expect(code).matchSnapshot()
expect(vaporHelpers).contains('on')
expect(ir.block.operation).toMatchObject([
{
@ -438,12 +439,13 @@ describe('v-on', () => {
options: ['capture', 'once'],
},
keyOverride: undefined,
delegate: false,
},
])
expect(code).matchSnapshot()
expect(code).contains(
'_on(n0, "click", () => _ctx.test, { capture: true, once: true }, {',
)
expect(code).contains('modifiers: ["stop", "prevent"]')
expect(code).contains('{ capture: true, once: true }')
})
test('should support multiple events and modifiers options w/ prefixIdentifiers: true', () => {
@ -494,10 +496,14 @@ describe('v-on', () => {
])
expect(code).matchSnapshot()
expect(code).contains(`_on(n0, "click", () => _ctx.test, undefined`)
expect(code).contains(
`_recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.test, {`,
)
expect(code).contains(`modifiers: ["stop"]`)
expect(code).contains(`_on(n0, "keyup", () => _ctx.test, undefined`)
expect(code).contains(
`_recordMetadata(n0, "events", "keyup", _eventHandler(() => _ctx.test, {`,
)
expect(code).contains(`keys: ["enter"]`)
})
@ -680,6 +686,22 @@ describe('v-on', () => {
})
expect(code).matchSnapshot()
expect(code).contains(`_on(n0, "click", () => _ctx.foo.bar)`)
expect(code).contains(
`_recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.foo.bar))`,
)
})
test('should delegate event', () => {
const { code, ir, vaporHelpers } = compileWithVOn(`<div @click="test"/>`)
expect(code).matchSnapshot()
expect(code).contains('_delegateEvents("click")')
expect(vaporHelpers).contains('delegateEvents')
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
delegate: true,
},
])
})
})

View File

@ -14,6 +14,7 @@ import {
LF,
NEWLINE,
buildCodeFragment,
genCall,
genCodeFragment,
} from './generators/utils'
@ -39,6 +40,8 @@ export class CodegenContext {
return `_${name}`
}
delegates = new Set<string>()
identifiers: Record<string, string[]> = Object.create(null)
withId = <T>(fn: () => T, map: Record<string, string | null>): T => {
const { identifiers } = this
@ -127,10 +130,11 @@ export function generate(
push('}')
}
const deligates = genDeligates(context)
// TODO source map?
const templates = genTemplates(ir.template, context)
const imports = genHelperImports(context)
const preamble = imports + templates
const preamble = imports + templates + deligates
const newlineCount = [...preamble].filter(c => c === '\n').length
if (newlineCount && !isSetupInlined) {
@ -152,6 +156,15 @@ export function generate(
}
}
function genDeligates({ delegates, vaporHelper }: CodegenContext) {
return delegates.size
? genCall(
vaporHelper('delegateEvents'),
...Array.from(delegates).map(v => `"${v}"`),
).join('') + '\n'
: ''
}
function genHelperImports({ helpers, vaporHelpers, options }: CodegenContext) {
let imports = ''
if (helpers.size) {

View File

@ -1,4 +1,4 @@
import { isMemberExpression } from '@vue/compiler-dom'
import { fnExpRE, isMemberExpression } from '@vue/compiler-dom'
import type { CodegenContext } from '../generate'
import type { SetEventIRNode } from '../ir'
import { genExpression } from './expression'
@ -11,10 +11,6 @@ import {
genCall,
} from './utils'
// TODO: share this with compiler-core
const fnExpRE =
/^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
export function genSetEvent(
oper: SetEventIRNode,
context: CodegenContext,
@ -25,6 +21,22 @@ export function genSetEvent(
const name = genName()
const handler = genEventHandler()
const modifierOptions = genModifierOptions()
if (oper.delegate) {
// oper.key is static
context.delegates.add(oper.key.content)
return [
NEWLINE,
...genCall(
vaporHelper('recordMetadata'),
`n${oper.element}`,
'"events"',
name,
genCall(vaporHelper('eventHandler'), handler, modifierOptions),
),
]
}
const handlerOptions = options.length
? `{ ${options.map(v => `${v}: true`).join(', ')} }`
: modifierOptions
@ -45,8 +57,8 @@ export function genSetEvent(
function genName(): CodeFragment[] {
const expr = genExpression(oper.key, context)
// TODO unit test
if (oper.keyOverride) {
// TODO unit test
const find = JSON.stringify(oper.keyOverride[0])
const replacement = JSON.stringify(oper.keyOverride[1])
const wrapped: CodeFragment[] = ['(', ...expr, ')']

View File

@ -116,6 +116,7 @@ export interface SetEventIRNode extends BaseIRNode {
nonKeys: string[]
}
keyOverride?: KeyOverride
delegate: boolean
}
export interface SetHtmlIRNode extends BaseIRNode {

View File

@ -2,9 +2,16 @@ import { ErrorCodes, createCompilerError } from '@vue/compiler-dom'
import type { DirectiveTransform } from '../transform'
import { IRNodeTypes, type KeyOverride, type SetEventIRNode } from '../ir'
import { resolveModifiers } from '@vue/compiler-dom'
import { extend } from '@vue/shared'
import { extend, makeMap } from '@vue/shared'
import { resolveExpression } from '../utils'
const delegatedEvents = /*#__PURE__*/ makeMap(
'beforeinput,click,dblclick,contextmenu,focusin,focusout,input,keydown,' +
'keyup,mousedown,mousemove,mouseout,mouseover,mouseup,pointerdown,' +
'pointermove,pointerout,pointerover,pointerup,touchend,touchmove,' +
'touchstart',
)
export const transformVOn: DirectiveTransform = (dir, node, context) => {
let { arg, exp, loc, modifiers } = dir
if (!exp && !modifiers.length) {
@ -29,6 +36,8 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => {
let keyOverride: KeyOverride | undefined
const isStaticClick = arg.isStatic && arg.content.toLowerCase() === 'click'
const delegate =
arg.isStatic && !eventOptionModifiers.length && delegatedEvents(arg.content)
// normalize click.right and click.middle since they don't actually fire
if (nonKeyModifiers.includes('middle')) {
@ -60,6 +69,7 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => {
options: eventOptionModifiers,
},
keyOverride,
delegate,
}
context.registerEffect([arg], [operation])

View File

@ -4,7 +4,7 @@ import {
onEffectCleanup,
onScopeDispose,
} from '@vue/reactivity'
import { recordMetadata } from '../metadata'
import { getMetadata, recordMetadata } from '../metadata'
import { withKeys, withModifiers } from '@vue/runtime-dom'
export function addEventListener(
@ -17,25 +17,19 @@ export function addEventListener(
return () => el.removeEventListener(event, handler, options)
}
interface ModifierOptions {
modifiers?: string[]
keys?: string[]
}
export function on(
el: HTMLElement,
event: string,
handlerGetter: () => undefined | ((...args: any[]) => any),
options?: AddEventListenerOptions,
{ modifiers, keys }: { modifiers?: string[]; keys?: string[] } = {},
modifierOptions?: ModifierOptions,
) {
const handler = (...args: any[]) => {
let handler = handlerGetter()
if (!handler) return
if (modifiers) {
handler = withModifiers(handler, modifiers)
}
if (keys) {
handler = withKeys(handler, keys)
}
handler && handler(...args)
}
const handler = eventHandler(handlerGetter, modifierOptions)
recordMetadata(el, 'events', event, handler)
const cleanup = addEventListener(el, event, handler, options)
@ -48,3 +42,64 @@ export function on(
onScopeDispose(cleanup)
}
}
export function eventHandler(
getter: () => undefined | ((...args: any[]) => any),
{ modifiers, keys }: ModifierOptions = {},
) {
return (...args: any[]) => {
let handler = getter()
if (!handler) return
if (modifiers) {
handler = withModifiers(handler, modifiers)
}
if (keys) {
handler = withKeys(handler, keys)
}
handler && handler(...args)
}
}
/**
* Event delegation borrowed from solid
*/
const delegatedEvents = Object.create(null)
export const delegateEvents = (...names: string[]) => {
for (const name of names) {
if (!delegatedEvents[name]) {
delegatedEvents[name] = true
// eslint-disable-next-line no-restricted-globals
document.addEventListener(name, delegatedEventHandler)
}
}
}
const delegatedEventHandler = (e: Event) => {
let node = ((e.composedPath && e.composedPath()[0]) || e.target) as any
if (e.target !== node) {
Object.defineProperty(e, 'target', {
configurable: true,
value: node,
})
}
Object.defineProperty(e, 'currentTarget', {
configurable: true,
get() {
// eslint-disable-next-line no-restricted-globals
return node || document
},
})
while (node !== null) {
const handler = getMetadata(node).events[e.type] as (...args: any[]) => any
if (handler && !node.disabled) {
handler(e)
if (e.cancelBubble) return
}
node =
node.host && node.host !== node && node.host instanceof Node
? node.host
: node.parentNode
}
}

View File

@ -57,6 +57,7 @@ export * from './apiLifecycle'
export * from './if'
export * from './for'
export { defineComponent } from './apiDefineComponent'
export { recordMetadata } from './metadata'
export * from './directives/vShow'
export * from './directives/vModel'