diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
index bec7e119bd3..43bd2589df1 100644
--- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
@@ -314,6 +314,37 @@ describe('compiler: element transform', () => {
)
expect(root.helpers).toContain(MERGE_PROPS)
+ expect(node.props).toMatchObject({
+ type: NodeTypes.JS_CALL_EXPRESSION,
+ callee: MERGE_PROPS,
+ arguments: [
+ createObjectMatcher({
+ id: 'foo'
+ }),
+ {
+ type: NodeTypes.JS_CALL_EXPRESSION,
+ callee: TO_HANDLERS,
+ arguments: [
+ {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: `obj`
+ },
+ `true`
+ ]
+ },
+ createObjectMatcher({
+ class: 'bar'
+ })
+ ]
+ })
+ })
+
+ test('v-on="obj" on component', () => {
+ const { root, node } = parseWithElementTransform(
+ ``
+ )
+ expect(root.helpers).toContain(MERGE_PROPS)
+
expect(node.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
@@ -358,7 +389,8 @@ describe('compiler: element transform', () => {
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `handlers`
- }
+ },
+ `true`
]
},
{
diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts
index 2d15227f70f..0eb3bb57628 100644
--- a/packages/compiler-core/src/transforms/transformElement.ts
+++ b/packages/compiler-core/src/transforms/transformElement.ts
@@ -647,7 +647,7 @@ export function buildProps(
type: NodeTypes.JS_CALL_EXPRESSION,
loc,
callee: context.helper(TO_HANDLERS),
- arguments: [exp]
+ arguments: isComponent ? [exp] : [exp, `true`]
})
}
} else {
diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts
index 060a7ef9097..a9dfe77eff7 100644
--- a/packages/compiler-core/src/transforms/vOn.ts
+++ b/packages/compiler-core/src/transforms/vOn.ts
@@ -47,12 +47,17 @@ export const transformOn: DirectiveTransform = (
if (rawName.startsWith('vue:')) {
rawName = `vnode-${rawName.slice(4)}`
}
- // for all event listeners, auto convert it to camelCase. See issue #2249
- eventName = createSimpleExpression(
- toHandlerKey(camelize(rawName)),
- true,
- arg.loc
- )
+ const eventString =
+ node.tagType === ElementTypes.COMPONENT ||
+ rawName.startsWith('vnode') ||
+ !/[A-Z]/.test(rawName)
+ ? // for component and vnode lifecycle event listeners, auto convert
+ // it to camelCase. See issue #2249
+ toHandlerKey(camelize(rawName))
+ // preserve case for plain element listeners that have uppercase
+ // letters, as these may be custom elements' custom events
+ : `on:${rawName}`
+ eventName = createSimpleExpression(eventString, true, arg.loc)
} else {
// #2388
eventName = createCompoundExpression([
diff --git a/packages/runtime-core/src/helpers/toHandlers.ts b/packages/runtime-core/src/helpers/toHandlers.ts
index d366a9b76c9..78ad164d7c0 100644
--- a/packages/runtime-core/src/helpers/toHandlers.ts
+++ b/packages/runtime-core/src/helpers/toHandlers.ts
@@ -5,14 +5,21 @@ import { warn } from '../warning'
* For prefixing keys in v-on="obj" with "on"
* @private
*/
-export function toHandlers(obj: Record): Record {
+export function toHandlers(
+ obj: Record,
+ preserveCaseIfNecessary?: boolean
+): Record {
const ret: Record = {}
if (__DEV__ && !isObject(obj)) {
warn(`v-on with no argument expects an object value.`)
return ret
}
for (const key in obj) {
- ret[toHandlerKey(key)] = obj[key]
+ ret[
+ preserveCaseIfNecessary && /[A-Z]/.test(key)
+ ? `on:${key}`
+ : toHandlerKey(key)
+ ] = obj[key]
}
return ret
}
diff --git a/packages/runtime-dom/src/modules/events.ts b/packages/runtime-dom/src/modules/events.ts
index bd2279cf5f2..d0f8d364a29 100644
--- a/packages/runtime-dom/src/modules/events.ts
+++ b/packages/runtime-dom/src/modules/events.ts
@@ -101,7 +101,8 @@ function parseName(name: string): [string, EventListenerOptions | undefined] {
;(options as any)[m[0].toLowerCase()] = true
}
}
- return [hyphenate(name.slice(2)), options]
+ const event = name[2] === ':' ? name.slice(3) : hyphenate(name.slice(2))
+ return [event, options]
}
function createInvoker(
diff --git a/packages/runtime-test/src/patchProp.ts b/packages/runtime-test/src/patchProp.ts
index 9c4bfca583f..3ebc64d1b04 100644
--- a/packages/runtime-test/src/patchProp.ts
+++ b/packages/runtime-test/src/patchProp.ts
@@ -16,7 +16,7 @@ export function patchProp(
})
el.props[key] = nextValue
if (isOn(key)) {
- const event = key.slice(2).toLowerCase()
+ const event = key[2] === ':' ? key.slice(3) : key.slice(2).toLowerCase()
;(el.eventListeners || (el.eventListeners = {}))[event] = nextValue
}
}
diff --git a/packages/vue/__tests__/customElementCasing.spec.ts b/packages/vue/__tests__/customElementCasing.spec.ts
new file mode 100644
index 00000000000..90e4453bcd7
--- /dev/null
+++ b/packages/vue/__tests__/customElementCasing.spec.ts
@@ -0,0 +1,42 @@
+import { createApp } from '../src'
+
+// https://github.com/vuejs/docs/pull/1890
+// https://github.com/vuejs/core/issues/5401
+// https://github.com/vuejs/docs/issues/1708
+test('custom element event casing', () => {
+ customElements.define(
+ 'custom-event-casing',
+ class Foo extends HTMLElement {
+ connectedCallback() {
+ this.dispatchEvent(new Event('camelCase'))
+ this.dispatchEvent(new Event('CAPScase'))
+ this.dispatchEvent(new Event('PascalCase'))
+ }
+ }
+ )
+
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+
+ const handler = jest.fn()
+ const handler2 = jest.fn()
+ createApp({
+ template: `
+ `,
+ methods: {
+ handler,
+ handler2
+ }
+ }).mount(container)
+
+ expect(handler).toHaveBeenCalledTimes(3)
+ expect(handler2).toHaveBeenCalledTimes(3)
+})