diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 3f0d6597c..1b5eda990 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -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' diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts index 8c13bdae5..de7ecb262 100644 --- a/packages/compiler-core/src/transforms/vOn.ts +++ b/packages/compiler-core/src/transforms/vOn.ts @@ -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 { diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index 04c93bd8f..ef730023b 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -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("") +_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 diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap index 2eb371c8e..c79c66d79 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap @@ -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("
") +_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) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap index feb793a2f..05ce51952 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap @@ -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("
") +_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("") const t1 = _template("
") const t2 = _template("
") const t3 = _template("") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_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("
") +_delegateEvents("click") export function render(_ctx) { const n0 = t0() - _on(n0, "click", () => _ctx.handleClick) + _recordMetadata(n0, "events", "click", _eventHandler(() => _ctx.handleClick)) return n0 }" `; diff --git a/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts b/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts index 095ba730f..219c118f2 100644 --- a/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts @@ -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(`
`) - 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(`
`) + 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(`
`) + 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(`
`) + 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(`
`) + + expect(code).matchSnapshot() + expect(code).contains('_delegateEvents("click")') + expect(vaporHelpers).contains('delegateEvents') + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + delegate: true, + }, + ]) }) }) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index b10071724..6244fc019 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -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() + identifiers: Record = Object.create(null) withId = (fn: () => T, map: Record): 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) { diff --git a/packages/compiler-vapor/src/generators/event.ts b/packages/compiler-vapor/src/generators/event.ts index 240e42783..c9e7810da 100644 --- a/packages/compiler-vapor/src/generators/event.ts +++ b/packages/compiler-vapor/src/generators/event.ts @@ -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, ')'] diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 9ca16c14c..bc7defb45 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -116,6 +116,7 @@ export interface SetEventIRNode extends BaseIRNode { nonKeys: string[] } keyOverride?: KeyOverride + delegate: boolean } export interface SetHtmlIRNode extends BaseIRNode { diff --git a/packages/compiler-vapor/src/transforms/vOn.ts b/packages/compiler-vapor/src/transforms/vOn.ts index 3c433018a..63a52b2c5 100644 --- a/packages/compiler-vapor/src/transforms/vOn.ts +++ b/packages/compiler-vapor/src/transforms/vOn.ts @@ -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]) diff --git a/packages/runtime-vapor/src/dom/event.ts b/packages/runtime-vapor/src/dom/event.ts index d37fc793b..161ec2512 100644 --- a/packages/runtime-vapor/src/dom/event.ts +++ b/packages/runtime-vapor/src/dom/event.ts @@ -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 + } +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 61ccd5d56..266ee8ee3 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -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'