Przeglądaj źródła

feat: delegate event for vapor

closes #136
三咲智子 Kevin Deng 2 lat temu
rodzic
commit
669fec8dad

+ 1 - 1
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'

+ 1 - 1
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 {

+ 3 - 2
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("<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

+ 3 - 2
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("<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)

+ 119 - 87
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("<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
 }"
 `;

+ 64 - 42
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(`<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,
+      },
+    ])
   })
 })

+ 14 - 1
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<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) {

+ 18 - 6
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, ')']

+ 1 - 0
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 {

+ 11 - 1
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])

+ 67 - 12
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,15 +17,38 @@ 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 = eventHandler(handlerGetter, modifierOptions)
+  recordMetadata(el, 'events', event, handler)
+  const cleanup = addEventListener(el, event, handler, options)
+
+  const scope = getCurrentScope()
+  const effect = getCurrentEffect()
+
+  if (effect && effect.scope === scope) {
+    onEffectCleanup(cleanup)
+  } else if (scope) {
+    onScopeDispose(cleanup)
+  }
+}
+
+export function eventHandler(
+  getter: () => undefined | ((...args: any[]) => any),
+  { modifiers, keys }: ModifierOptions = {},
 ) {
-  const handler = (...args: any[]) => {
-    let handler = handlerGetter()
+  return (...args: any[]) => {
+    let handler = getter()
     if (!handler) return
 
     if (modifiers) {
@@ -36,15 +59,47 @@ export function on(
     }
     handler && handler(...args)
   }
-  recordMetadata(el, 'events', event, handler)
-  const cleanup = addEventListener(el, event, handler, options)
+}
 
-  const scope = getCurrentScope()
-  const effect = getCurrentEffect()
+/**
+ * Event delegation borrowed from solid
+ */
+const delegatedEvents = Object.create(null)
 
-  if (effect && effect.scope === scope) {
-    onEffectCleanup(cleanup)
-  } else if (scope) {
-    onScopeDispose(cleanup)
+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
   }
 }

+ 1 - 0
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'